From e4662def6b63d3475c22710e69e1f2ada5c479a4 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:04:17 +0800 Subject: [PATCH 01/49] fix(seed): preserve CAS cloud metadata (#78) Co-authored-by: wumingzhinu --- src/backend/internal/seed/codec.ts | 1 + src/backend/server/seed.test.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/backend/internal/seed/codec.ts b/src/backend/internal/seed/codec.ts index cb4772a7..b59722e5 100644 --- a/src/backend/internal/seed/codec.ts +++ b/src/backend/internal/seed/codec.ts @@ -458,6 +458,7 @@ export function decodeCas(data: Uint8Array): ParsedSeed { create_time: String(value.create_time || ""), slice_md5s: value.slice_md5s, slice_size: value.slice_size, + cloud: value.cloud, }), ], }), diff --git a/src/backend/server/seed.test.ts b/src/backend/server/seed.test.ts index 6b4dd129..1e5b871f 100644 --- a/src/backend/server/seed.test.ts +++ b/src/backend/server/seed.test.ts @@ -97,11 +97,27 @@ test("CAS codec matches casmeta base64 JSON field names", async () => { "slice_size", ].sort(), ) + assert.equal(payload.cloud, "189") + assert.deepEqual(payload.slice_md5s, ["5D41402ABC4B2A76B9719D911017C592"]) + assert.equal(payload.slice_size, 10 * 1024 * 1024) const parsed = await parseSeed(bytes, "cas") assert.equal(parsed.cas?.name, "hello.txt") assert.equal(parsed.cas?.md5, "5d41402abc4b2a76b9719d911017c592") }) +test("CAS codec preserves custom cloud across round trips", async () => { + const original = casSeed() + original.files[0].cas_cloud = "115" + const bytes = await encodeCas(original) + const parsed = await parseSeed(bytes, "cas") + assert.equal(parsed.seed.files[0].cas_cloud, "115") + const roundTrip = await encodeCas(parsed.seed) + const payload = JSON.parse( + Buffer.from(Buffer.from(roundTrip).toString(), "base64").toString("utf8"), + ) + assert.equal(payload.cloud, "115") +}) + test("incremental hashing computes whole-file and piece hashes without buffering the file", async () => { const torrent = await TorrentPieceHasher.create(4) const stream = new ReadableStream({ From 0a36d0da0648bceed93de2c43991b19f01a01b74 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:05:13 +0800 Subject: [PATCH 02/49] fix(ci): make edgeone artifact guard read-only (#76) Co-authored-by: wumingzhinu --- .github/workflows/edgeone-artifact-guard.yml | 47 +++++++------------- tests/artifact-guard.test.ts | 27 +++++++++++ 2 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 tests/artifact-guard.test.ts diff --git a/.github/workflows/edgeone-artifact-guard.yml b/.github/workflows/edgeone-artifact-guard.yml index 4fd224a3..22c6669d 100644 --- a/.github/workflows/edgeone-artifact-guard.yml +++ b/.github/workflows/edgeone-artifact-guard.yml @@ -5,9 +5,8 @@ name: EdgeOne Artifact Guard # 详见 docs/edgeone.md),因此必须提交进仓库并与后端源码、前端产物保持同步。 # # 本工作流按与部署一致的流程重新构建该产物并与已提交版本比对: -# - push 到 main / 手动触发:产物过期时自动重建并提交(bot 提交不会再次触发 -# workflow,无循环风险) -# - pull request:仅检查并失败,由 PR 作者本地重建后提交(便于审查) +# - push 到 main / 手动触发:只校验并失败,不直接修改受保护分支 +# - pull request:只校验并失败,由 PR 作者本地重建后提交(便于审查) on: push: @@ -35,7 +34,7 @@ on: workflow_dispatch: permissions: - contents: write + contents: read concurrency: group: edgeone-artifact-guard-${{ github.ref }} @@ -44,7 +43,7 @@ concurrency: jobs: guard: runs-on: ubuntu-latest - name: Rebuild and sync cloud-functions artifact + name: Rebuild and verify cloud-functions artifact steps: - name: Checkout uses: actions/checkout@v7 @@ -59,7 +58,7 @@ jobs: cache: pnpm - name: Install dependencies - run: pnpm install --no-frozen-lockfile + run: pnpm install --frozen-lockfile - name: Fetch official frontend dist run: node scripts/fetch-frontend.mjs @@ -70,42 +69,26 @@ jobs: - name: Check artifact freshness id: check run: | - if [ -n "$(git status --porcelain -- cloud-functions)" ]; then + if [ -z "$(git status --porcelain -- cloud-functions)" ]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else echo "changed=true" >> "$GITHUB_OUTPUT" + git status --short -- cloud-functions git diff --stat -- cloud-functions - else - echo "changed=false" >> "$GITHUB_OUTPUT" + git diff --exit-code -- cloud-functions || true + echo "::error::cloud-functions/[[default]].js 与源码不同步。请在本地运行 pnpm run build,并提交更新后的 cloud-functions/[[default]].js。" + exit 1 fi - - name: Auto-commit refreshed artifact - id: autocommit - if: steps.check.outputs.changed == 'true' && github.event_name != 'pull_request' - run: | - TRIGGERED=$(git rev-parse --short HEAD) - git config user.name "edgeone-deploy[bot]" - git config user.email "edgeone-deploy-bot@users.noreply.github.com" - git add cloud-functions - git commit -m "chore(edgeone): refresh cloud-functions artifact (after $TRIGGERED)" - git pull --rebase origin "$GITHUB_REF_NAME" - git push origin "HEAD:$GITHUB_REF_NAME" - echo "committed=$TRIGGERED" >> "$GITHUB_OUTPUT" - - name: Job summary if: always() run: | if [ "${{ steps.check.outputs.changed }}" = "false" ]; then - echo "✅ cloud-functions/[[default]].js 与源码同步,无需更新" >> "$GITHUB_STEP_SUMMARY" - elif [ -n "${{ steps.autocommit.outputs.committed }}" ]; then - echo "🤖 产物已过期,已自动重建并提交(触发提交:${{ steps.autocommit.outputs.committed }})" >> "$GITHUB_STEP_SUMMARY" + echo "cloud-functions/[[default]].js 与源码同步,无需更新" >> "$GITHUB_STEP_SUMMARY" + else + echo "cloud-functions/[[default]].js 已过期;请使用任务上传的刷新产物更新提交。" >> "$GITHUB_STEP_SUMMARY" fi - - name: Fail on out-of-sync artifact (pull request) - if: steps.check.outputs.changed == 'true' && github.event_name == 'pull_request' - run: | - echo "::error::cloud-functions/[[default]].js 与源码不同步(后端代码或官方前端产物已更新)。" - echo "::error::请在本地运行 pnpm run build 后提交更新后的 cloud-functions/[[default]].js;也可下载本任务上传的 cloud-functions-refreshed 产物覆盖提交。" - exit 1 - - name: Upload refreshed artifact on failure if: failure() uses: actions/upload-artifact@v4 diff --git a/tests/artifact-guard.test.ts b/tests/artifact-guard.test.ts new file mode 100644 index 00000000..20d5e973 --- /dev/null +++ b/tests/artifact-guard.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { describe, test } from "node:test" + +const workflowPath = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../.github/workflows/edgeone-artifact-guard.yml", +) +const workflow = fs.readFileSync(workflowPath, "utf8") + +describe("EdgeOne Artifact Guard workflow", () => { + test("uses the lockfile and never pushes generated artifacts", () => { + assert.match(workflow, /pnpm install --frozen-lockfile/) + assert.match(workflow, /contents: read/) + assert.doesNotMatch(workflow, /contents: write/) + assert.doesNotMatch(workflow, /git push origin/) + assert.doesNotMatch(workflow, /Auto-commit refreshed artifact/) + }) + + test("fails and uploads the refreshed artifact when it is stale", () => { + assert.match(workflow, /git status --porcelain -- cloud-functions/) + assert.match(workflow, /git diff --exit-code -- cloud-functions/) + assert.match(workflow, /if: failure\(\)/) + }) +}) From 99c4b03e0cd84ee5f71bc4109c672e81c5db394c Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Sun, 27 Sep 2026 20:12:51 +0800 Subject: [PATCH 03/49] test(db): type cipher producers (#80) Co-authored-by: wumingzhinu --- src/backend/internal/model/db_cipher.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/internal/model/db_cipher.test.ts b/src/backend/internal/model/db_cipher.test.ts index c8c8d6e9..196c2115 100644 --- a/src/backend/internal/model/db_cipher.test.ts +++ b/src/backend/internal/model/db_cipher.test.ts @@ -11,6 +11,7 @@ import { encryptConfigValue, isSealedCiphertext, resolveDbCipher, + type DbCipher, } from "../../pkg/crypto" import { readCipher } from "./store/backend" import { memoryDriver } from "./store/driver/memory" From 29f7a0abc3bd1fcc3f0e899083e225a229426d26 Mon Sep 17 00:00:00 2001 From: ShenLin <773933146@qq.com> Date: Tue, 29 Sep 2026 12:24:57 +0800 Subject: [PATCH 04/49] fix: resolve setup and runtime regressions (#89) Signed-off-by: jyxjjj <16695261+jyxjjj@users.noreply.github.com> Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 3 + scripts/fetch-frontend.mjs | 71 +++++++++++--- src/backend/drivers/autoindex/driver.test.ts | 25 +++++ src/backend/drivers/autoindex/driver.ts | 8 +- src/backend/drivers/autoindex/util.ts | 35 ++++--- src/backend/internal/model/store/driver/kv.ts | 61 +++++++++--- src/backend/server/auth.ts | 98 ++++++++++++------- src/backend/server/auth_2fa.test.ts | 59 +++++++++++ 9 files changed, 284 insertions(+), 77 deletions(-) create mode 100644 src/backend/server/auth_2fa.test.ts diff --git a/package.json b/package.json index 8662a0cd..93d16da6 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "mysql2": "^3.11.0", "openpgp": "^6.3.1", "otplib": "^12.0.1", + "parse5": "^7.3.0", "qrcode": "^1.5.4", "rehype-katex": "7.0.1", "rehype-raw": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a3fca1c..77b77caa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: otplib: specifier: ^12.0.1 version: 12.0.1 + parse5: + specifier: ^7.3.0 + version: 7.3.0 qrcode: specifier: ^1.5.4 version: 1.5.4 diff --git a/scripts/fetch-frontend.mjs b/scripts/fetch-frontend.mjs index ec3574bd..f3096115 100644 --- a/scripts/fetch-frontend.mjs +++ b/scripts/fetch-frontend.mjs @@ -8,9 +8,10 @@ * 1. FRONTEND_DIST 环境变量:已构建好的 dist 目录路径(最快,CI 缓存场景) * 2. FRONTEND_REPO 环境变量:本地官方前端仓库路径(自动 install + build) * 3. 同级目录 ../OpenList-Frontend(monorepo 布局,自动探测,自动 install + build) - * 4. 默认:下载 npm 上【已发布】的 dist(版本取 registry 的 latest, - * 可用 FRONTEND_VERSION 固定) - * 5. FRONTEND_BUILD_FROM_SOURCE=1:从 Git 克隆前端 main 分支并现构建 + * 4. 默认:下载 npm 上【已发布】且兼容 Worker 初始化协议的 dist(版本取 + * registry 的 latest,可用 FRONTEND_VERSION 固定) + * 5. 发布版尚未包含 Worker 初始化协议,或 FRONTEND_BUILD_FROM_SOURCE=1: + * 从 Git 克隆前端 main 分支并现构建 * * 为什么默认取「已发布 dist」而不是「克隆 main 现构建」: * 前端产物是内容哈希文件名(/assets/index-XXXX.js),而 CDN(jsdelivr / @@ -22,6 +23,10 @@ * 可用(等价 Go Release 版行为)。同时 stampFrontendVersion 会把该版本号写进 * index.html,使 ASSET_URLS 的 $version 正好解析到这份 dist 对应的版本。 * + * 已发布包可能落后于 Worker 后端。只有同时包含 /public/init_status 与 /@init + * 的产物才可使用;否则全新部署会只请求会被 503 拦截的 /public/settings,既不 + * 显示初始化向导也无法创建管理员。遇到这种版本会自动改为构建前端 main。 + * * 用法: * FRONTEND_DIST=/path/to/dist node scripts/fetch-frontend.mjs * FRONTEND_REPO=../OpenList-Frontend node scripts/fetch-frontend.mjs @@ -95,6 +100,32 @@ function requireDist(src) { } } +/** + * 已构建前端是否包含 Worker 首次初始化协议。 + * + * Vite 会保留路由和 API 路径字符串,因此无需执行或反编译 bundle。两项必须 + * 同时存在:init_status 负责识别空数据库,/@init 才能渲染创建管理员的向导。 + */ +function supportsWorkerSetup(src) { + let hasStatus = false + let hasRoute = false + const pending = [src] + while (pending.length > 0 && (!hasStatus || !hasRoute)) { + const current = pending.pop() + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const full = path.join(current, entry.name) + if (entry.isDirectory()) { + pending.push(full) + } else if (entry.isFile() && entry.name.endsWith(".js")) { + const code = fs.readFileSync(full, "utf-8") + hasStatus ||= code.includes("/public/init_status") + hasRoute ||= code.includes("/@init") + } + } + } + return hasStatus && hasRoute +} + function replaceDist(src) { console.log(` Copying frontend dist: ${src} -> ${DEST}`) fs.rmSync(DEST, { recursive: true, force: true }) @@ -113,7 +144,10 @@ function replaceDist(src) { function stampFrontendVersion(src) { try { const pkg = JSON.parse( - fs.readFileSync(path.join(path.resolve(src, ".."), "package.json"), "utf-8"), + fs.readFileSync( + path.join(path.resolve(src, ".."), "package.json"), + "utf-8", + ), ) // 只信任官方前端包的版本号:FRONTEND_DIST 可能指向任意目录, // 误读(例如 worker 自身 package.json 的 4.2.3)会戳出错误的 CDN 版本。 @@ -145,7 +179,9 @@ function stampFrontendVersion(src) { function fetchI18n(repo) { const langDir = path.join(repo, "src", "lang") if (!fs.existsSync(langDir)) { - console.warn(" [fetch-frontend] repo missing src/lang, skipping i18n fetch") + console.warn( + " [fetch-frontend] repo missing src/lang, skipping i18n fetch", + ) return } const tmpTar = path.join(os.tmpdir(), `openlist-i18n-${process.pid}.tar.gz`) @@ -172,8 +208,7 @@ function buildLocalRepo(repo) { } const pm = detectPackageManager(abs) const cmd = resolvePmCommand(abs, pm) - const install = (extra = "") => - run(`${cmd} install${extra}`, { cwd: abs }) + const install = (extra = "") => run(`${cmd} install${extra}`, { cwd: abs }) try { install() } catch { @@ -181,7 +216,9 @@ function buildLocalRepo(repo) { // minimumReleaseAge / trustPolicy 供应链复核,registry manifest 缺少 // 平台子包时会误报(如 @crowdin/cli-*-arm64)。lockfile 来自刚克隆的 // 官方前端仓库(HTTPS + 官方分支),属于可信来源,跳过复核安全。 - console.warn(" [fetch-frontend] pnpm install failed (lockfile supply-chain recheck or network issue), retrying once with --trust-lockfile...") + console.warn( + " [fetch-frontend] pnpm install failed (lockfile supply-chain recheck or network issue), retrying once with --trust-lockfile...", + ) install(" --trust-lockfile") } fetchI18n(abs) @@ -239,7 +276,14 @@ async function fetchPublishedDist() { run(`tar -xzf pkg.tgz package/dist package/package.json`, { cwd: tmp }) const src = path.join(tmp, "package", "dist") requireDist(src) + if (!supportsWorkerSetup(src)) { + console.warn( + ` Published frontend ${version} lacks the Worker setup protocol; building ${OFFICIAL_REPO_REF} instead`, + ) + return false + } replaceDist(src) + return true } finally { fs.rmSync(tmp, { recursive: true, force: true }) } @@ -272,17 +316,18 @@ async function main() { return } - // 4. 下载 npm 上已发布的 dist(默认) + // 4. 下载 npm 上已发布且兼容 Worker 初始化协议的 dist(默认) // 从 main 现构建的产物哈希与 CDN 不一致,会让路径 A 失效、npmmirror 之类的 // 镜像完全不可用(详见文件头),故默认改为取已发布产物。 if (process.env.FRONTEND_BUILD_FROM_SOURCE !== "1") { - await fetchPublishedDist() - return + if (await fetchPublishedDist()) return } - // 5. 从 Git 克隆 main 并构建(FRONTEND_BUILD_FROM_SOURCE=1 时使用) + // 5. 从 Git 克隆 main 并构建(显式要求源码构建,或发布版尚未兼容 Worker) const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "openlist-frontend-")) - console.log(` Cloning official frontend: ${OFFICIAL_REPO_URL}#${OFFICIAL_REPO_REF}`) + console.log( + ` Cloning official frontend: ${OFFICIAL_REPO_URL}#${OFFICIAL_REPO_REF}`, + ) try { run( // -c core.autocrlf=false:禁用克隆端的换行符转换。Windows 上 autocrlf diff --git a/src/backend/drivers/autoindex/driver.test.ts b/src/backend/drivers/autoindex/driver.test.ts index ed2596c5..40a4cd4b 100644 --- a/src/backend/drivers/autoindex/driver.test.ts +++ b/src/backend/drivers/autoindex/driver.test.ts @@ -54,6 +54,31 @@ test("parseAutoIndexHTML extracts files, dirs, size and modified", () => { assert.equal(nodes[1].isDir, true) }) +test("parseAutoIndexHTML tolerates malformed Apache directory HTML", () => { + // archive.apache.org/dist/tomcat/ 实际返回过这种重复 html/body/pre、只关闭一层 + // pre 的页面。浏览器与 Go htmlquery 都能解析,AutoIndex 也不能按严格 XML 拒绝。 + const html = ` +Index +Index
+
Parent Directory
+tomcat-10/ 2026-09-15 10:54 -
+
` + + const nodes = parseAutoIndexHTML( + html, + "//pre/a", + "@href", + "string(following-sibling::text()[1])", + "string(following-sibling::text()[2])", + ["Parent Directory"], + ) + + const tomcat = nodes.find((node) => node.name === "tomcat-10") + assert.ok(tomcat) + assert.equal(tomcat.url, "tomcat-10/") + assert.equal(tomcat.isDir, true) +}) + test("normalizeAutoIndexAddition fills defaults", () => { const a = normalizeAutoIndexAddition({ url: "example.com/files" }) assert.equal(a.url, "https://example.com/files/") diff --git a/src/backend/drivers/autoindex/driver.ts b/src/backend/drivers/autoindex/driver.ts index 4516adeb..c05185d5 100644 --- a/src/backend/drivers/autoindex/driver.ts +++ b/src/backend/drivers/autoindex/driver.ts @@ -13,6 +13,7 @@ const DefaultItemXPath = "//pre/a" const DefaultNameXPath = "@href" const DefaultSizeXPath = "string(following-sibling::text()[1])" const DefaultModifiedXPath = "string(following-sibling::text()[2])" +const FETCH_TIMEOUT_MS = 30_000 export function normalizeAutoIndexAddition(a: any): AutoIndexAddition { const norm = { ...(a || {}) } as any @@ -78,7 +79,12 @@ export class AutoIndexDriver implements StorageDriver { async list(virtualPath: string, physicalPath: string): Promise { const baseURL = this.buildDirURL(physicalPath) - const res = await fetch(baseURL) + // 边缘运行时通常会等到平台级超时(EdgeOne 为 120 秒)才中止不可达的 + // 上游请求。显式限制单次目录读取,避免一个失联的 AutoIndex 挂载长期占用 + // 实例并拖累同一服务的其他请求。 + const res = await fetch(baseURL, { + signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), + }) if (!res.ok) { throw new Error(`Failed to fetch ${baseURL}: HTTP ${res.status}`) } diff --git a/src/backend/drivers/autoindex/util.ts b/src/backend/drivers/autoindex/util.ts index 85a80eff..a3d3e474 100644 --- a/src/backend/drivers/autoindex/util.ts +++ b/src/backend/drivers/autoindex/util.ts @@ -1,6 +1,7 @@ // AutoIndex utility functions import * as xpath from "xpath" import { DOMParser } from "@xmldom/xmldom" +import { parse, serialize } from "parse5" import { AutoIndexNode } from "./types" /** @@ -20,19 +21,30 @@ function extractXPathValue(raw: unknown): string | undefined { } // 将宽松 HTML 清洗为可被 XML 解析器(xmldom)解析的良构 XML: -// 1) 移除 DOCTYPE 与注释;2) 把 void 标签(
//
等)转为自闭合。 -// 这样用 text/xml 解析后节点无命名空间,用户配置的无前缀 XPath(如 //pre/a) -// 才能正常匹配——与 Go 侧 htmlquery(golang.org/x/net/html)的行为保持一致。 -// 注意:若直接用 text/html 解析,xmldom 会注入 XHTML 命名空间,导致 //pre/a 匹配不到。 +// 1) 先用 parse5 按 HTML5 容错规则修复未闭合/错位/重复标签; +// 2) 移除 XML 不需要的 DOCTYPE、注释和 script/style; +// 3) 把 void 标签(
//
等)转为自闭合。 +// +// 不能直接把远端 HTML 交给 xmldom 的 XML 模式:真实目录页常包含浏览器可以 +// 正常处理的非严格 HTML。例如 archive.apache.org/dist/tomcat/ 同时有重复的 +// html/body/pre 标签,旧实现会抛 "Opening and ending tag mismatch"。parse5 与 +// Go 版 htmlquery 底层的 HTML parser 一样会容错,再转 XML 后仍可继续使用用户 +// 配置的无前缀 XPath(如 //pre/a)。 +// +// 注意:若直接用 xmldom 的 text/html 模式,它会注入 XHTML 命名空间,导致 +// //pre/a 匹配不到。 const HTML_VOID_TAGS = "area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr" function htmlToXml(html: string): string { - let s = html + let s = serialize(parse(html)) s = s.replace(/]*>/gi, "") s = s.replace(//g, "") + // script/style 的 raw text 可以包含 XML 非法的裸 <、&;AutoIndex XPath 不会 + // 依赖这些内容,移除可避免第二阶段 XML 解析被无关脚本或样式破坏。 + s = s.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "") s = s.replace( - new RegExp(`<(${HTML_VOID_TAGS})([^>]*?)/?>`, "gi"), + new RegExp(`<(${HTML_VOID_TAGS})([^>]*?)/?\\s*>`, "gi"), "<$1$2/>", ) return s @@ -44,7 +56,7 @@ export function parseAutoIndexHTML( nameXPath: string, sizeXPath: string, modifiedXPath: string, - ignoreNames: string[] + ignoreNames: string[], ): AutoIndexNode[] { // @xmldom/xmldom >= 0.9 已废弃 errorHandler,改用 onError 回调 const doc = new DOMParser({ @@ -146,10 +158,7 @@ export function parseSize(sizeStr: string): number { return Math.round(num * mul) } -export function parseTime( - timeStr: string, - format: string -): string { +export function parseTime(timeStr: string, format: string): string { if (!timeStr) return new Date().toISOString() try { @@ -161,7 +170,9 @@ export function parseTime( const isoMatch = timeStr.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/) if (isoMatch) { const [, year, month, day, hour, minute] = isoMatch - return new Date(`${year}-${month}-${day}T${hour}:${minute}:00`).toISOString() + return new Date( + `${year}-${month}-${day}T${hour}:${minute}:00`, + ).toISOString() } // Try common formats diff --git a/src/backend/internal/model/store/driver/kv.ts b/src/backend/internal/model/store/driver/kv.ts index 6962fd03..502a66ee 100644 --- a/src/backend/internal/model/store/driver/kv.ts +++ b/src/backend/internal/model/store/driver/kv.ts @@ -1,10 +1,10 @@ /** * KV 驱动(自动适配 Cloudflare / EdgeOne) - * + * * 支持两种模式: * 1. Binding 模式:直接访问 KV binding(Cloudflare Workers / EdgeOne Edge Functions) * 2. HTTP 代理模式:通过 Edge Function 代理访问(EdgeOne Node Functions) - * + * * 自动检测环境并选择合适的模式。 */ import { sanitizeProxyOrigin } from "../proxy" @@ -71,6 +71,33 @@ function getProxySecret(env?: EnvContext): string | null { } } +/** + * 当前环境是否有充分依据启用 EdgeOne KV HTTP 代理探测。 + * + * `__requestOrigin` 不能作为平台依据:index.ts 会给所有平台和自托管请求注入它。 + * 旧实现只要同时看到 origin 与 JWT_SECRET,就会在 DB_DRIVER=auto 时请求当前站点 + * 的 /kv-list;没有该代理的站点若恰好把未知路径回退成 HTML 200,便会被误判为 + * KV 可用,随后把 HTML 当 JSON 读取,并让 /env_check 错报 ready=true。 + * + * 只有以下情况才允许无 binding 的代理模式: + * 1. 用户显式指定 DB_DRIVER=kv; + * 2. 用户显式提供 EO_KV_URLS; + * 3. EdgeOne Node 云函数的可靠 SCF 运行时标记存在。 + */ +function shouldProbeProxy(env?: any): boolean { + const configured = String(env?.DB_DRIVER || "") + .trim() + .toLowerCase() + const procEnv: any = + typeof process !== "undefined" ? (process as any).env || {} : {} + return Boolean( + configured === "kv" || + env?.EO_KV_URLS || + env?.TENCENTCLOUD_SCF_FUNCTIONNAME || + procEnv.TENCENTCLOUD_SCF_FUNCTIONNAME, + ) +} + /** * Detect missing configuration for KV proxy mode. * @@ -258,9 +285,13 @@ export const kvDriver: Driver = { } // 模式2: HTTP 代理(EdgeOne Node Functions 拿不到 binding) + // __requestOrigin 本身不能证明代理存在;auto 模式下必须有显式配置或 + // EdgeOne Node 的平台标记,否则未知路由的 HTML 200 会造成假阳性。 + if (!shouldProbeProxy(env)) return false const probe = await probeProxy(env) if (!probeToAvailability(probe)) { - if (probe.error) console.error("[DB] KV proxy unavailable: " + probe.error) + if (probe.error) + console.error("[DB] KV proxy unavailable: " + probe.error) return false } return true @@ -272,7 +303,7 @@ export const kvDriver: Driver = { async get(key: string, env?: any): Promise { const kv = getKvBinding(env) - + // 模式1: Binding 模式 if (kv) { // Cloudflare KV 用 get(key, "text"),EdgeOne KV 用 get(key, {type:"text"}), @@ -312,7 +343,7 @@ export const kvDriver: Driver = { // 绑定误返回对象时统一序列化,保持 Driver.get 的 string 契约 return JSON.stringify(value) } - + // 模式2: HTTP 代理模式(无原生 binding → 经 Edge Function 代理) const baseUrl = requireProxyBaseUrl(env) const url = `${baseUrl}/kv-get?key=${encodeURIComponent(key)}` @@ -330,7 +361,7 @@ export const kvDriver: Driver = { throw new Error(`KV proxy get failed: ${response.status}`) } - const data = await response.json() as { value?: string | null } + const data = (await response.json()) as { value?: string | null } // 归一化为 string | null:Edge Function 在错误分支只返回 { error }, // 此时 data.value 为 undefined,不能直接透传(调用方按 === null 判断会漏掉)。 if (data?.value === undefined || data?.value === null) return null @@ -343,13 +374,13 @@ export const kvDriver: Driver = { async put(key: string, value: string, env?: any): Promise { const kv = getKvBinding(env) - + // 模式1: Binding 模式 if (kv) { await kv.put(key, value) return } - + // 模式2: HTTP 代理模式 const baseUrl = requireProxyBaseUrl(env) const url = `${baseUrl}/kv-put` @@ -372,13 +403,13 @@ export const kvDriver: Driver = { async delete(key: string, env?: any): Promise { const kv = getKvBinding(env) - + // 模式1: Binding 模式 if (kv) { await kv.delete(key) return } - + // 模式2: HTTP 代理模式 const baseUrl = requireProxyBaseUrl(env) const url = `${baseUrl}/kv-delete?key=${encodeURIComponent(key)}` @@ -400,7 +431,7 @@ export const kvDriver: Driver = { async list(prefix: string, env?: any): Promise { const kv = getKvBinding(env) - + // 模式1: Binding 模式 if (kv) { // EdgeOne KV list() 语义(依据官方 functions-kv 示例): @@ -430,7 +461,7 @@ export const kvDriver: Driver = { return keys } - + // 模式2: HTTP 代理模式 const baseUrl = requireProxyBaseUrl(env) const url = `${baseUrl}/kv-list?prefix=${encodeURIComponent(prefix)}` @@ -445,7 +476,7 @@ export const kvDriver: Driver = { throw new Error(`KV proxy list failed: ${response.status}`) } - const data = await response.json() as { keys: string[] } + const data = (await response.json()) as { keys: string[] } return data.keys || [] } catch (err) { console.error(`[KV] list(${prefix}) failed:`, err) @@ -455,7 +486,7 @@ export const kvDriver: Driver = { async health(env?: any): Promise { const kv = getKvBinding(env) - + // 模式1: Binding 模式 if (kv) { try { @@ -475,7 +506,7 @@ export const kvDriver: Driver = { } } } - + // 模式2: HTTP 代理模式。 // // 判定比 isAvailable 更严格(见 probeToHealth / probeToAvailability 的注释): diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index e903f475..a14447d9 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -42,7 +42,10 @@ const LOGIN_MAX_FAILURES = 5 const LOGIN_MAX_FAILURES_GLOBAL = 20 const LOGIN_LOCK_MS = 15 * 60 * 1000 const LOGIN_MAX_LOCK_MS = 24 * 60 * 60 * 1000 // 最长锁定 24 小时 -const loginFailures = new Map() +const loginFailures = new Map< + string, + { count: number; lockedUntil: number; attempts: number } +>() function clientIpOf(c: Context): string { return ( @@ -78,11 +81,11 @@ function calculateLockDuration(attempts: number): number { async function bumpLoginFailure( key: string, maxFailures: number, - env: any + env: any, ): Promise { const now = Date.now() let rec = loginFailures.get(key) || { count: 0, lockedUntil: 0, attempts: 0 } - + // 尝试从 KV 读取(多实例共享) try { const { getKvBinding } = await import("../internal/model/db") @@ -109,23 +112,23 @@ async function bumpLoginFailure( } catch { // KV 不可用,回退到内存模式 } - + if (rec.lockedUntil > now) return // already locked - + rec.count += 1 rec.attempts += 1 - + if (rec.count >= maxFailures) { const lockDuration = calculateLockDuration(rec.attempts) rec.lockedUntil = now + lockDuration rec.count = 0 console.warn( - `[Auth] Login attempts exceeded for ${key}. Locked for ${Math.round(lockDuration / 60000)} minutes (attempt #${rec.attempts}).` + `[Auth] Login attempts exceeded for ${key}. Locked for ${Math.round(lockDuration / 60000)} minutes (attempt #${rec.attempts}).`, ) } - + loginFailures.set(key, rec) - + // 持久化到 KV try { const { getKvBinding } = await import("../internal/model/db") @@ -144,7 +147,11 @@ async function bumpLoginFailure( } } -async function isLoginLocked(c: Context, username: string, env: any): Promise { +async function isLoginLocked( + c: Context, + username: string, + env: any, +): Promise { // 懒清理:Map 过大时清掉已过锁定期/无锁定的条目,防止无限增长 if (loginFailures.size > 10000) { const now = Date.now() @@ -152,15 +159,15 @@ async function isLoginLocked(c: Context, username: string, env: any): Promise now) return true if (grec && grec.lockedUntil > now) return true return false } -async function recordLoginFailure(c: Context, username: string, env: any): Promise { +async function recordLoginFailure( + c: Context, + username: string, + env: any, +): Promise { await bumpLoginFailure(loginKey(c, username), LOGIN_MAX_FAILURES, env) - await bumpLoginFailure(globalLoginKey(username), LOGIN_MAX_FAILURES_GLOBAL, env) + await bumpLoginFailure( + globalLoginKey(username), + LOGIN_MAX_FAILURES_GLOBAL, + env, + ) } -async function clearLoginFailures(c: Context, username: string, env: any): Promise { +async function clearLoginFailures( + c: Context, + username: string, + env: any, +): Promise { const ipKey = loginKey(c, username) const globalKey = globalLoginKey(username) - + loginFailures.delete(ipKey) loginFailures.delete(globalKey) - + // 从 KV 中删除 try { const { getKvBinding } = await import("../internal/model/db") @@ -230,7 +249,9 @@ async function clearLoginFailures(c: Context, username: string, env: any): Promi // OpenList/AList StaticHash —— 前端 /login/hash 提交的哈希算法。 // 与新密码模块 pkg/password.staticHash 一致,保留旧名以兼容既有调用。 -export async function hashPasswordSHA256(plainPassword: string): Promise { +export async function hashPasswordSHA256( + plainPassword: string, +): Promise { return staticHash(plainPassword) } @@ -245,8 +266,12 @@ export async function verifyUserStaticHash( user: any, inputStatic: string, ): Promise { - const stored = String(user?.password || "").trim().toLowerCase() - const input = String(inputStatic || "").trim().toLowerCase() + const stored = String(user?.password || "") + .trim() + .toLowerCase() + const input = String(inputStatic || "") + .trim() + .toLowerCase() if (!stored || !isHex64(input) || !isHex64(stored)) return false if (user?.salt) { const expect = (await saltedHash(input, String(user.salt))).toLowerCase() @@ -483,7 +508,7 @@ authRouter.post("/login", async (c) => { ) } await clearLoginFailures(c, username, c.env) - + // 生成 JWT Token const payload = { id: matchedUser.id, @@ -494,14 +519,14 @@ authRouter.post("/login", async (c) => { } const secret = await getJwtSecret(c) const token = await sign(payload, secret) - + // 生成 CSRF Token const csrfToken = setCSRFToken(c) - + // 记录审计日志 const auditLogger = getAuditLogger() await auditLogger.logLoginSuccess(c, username) - + return c.json({ code: 200, message: "success", @@ -513,7 +538,7 @@ authRouter.post("/login", async (c) => { // 登录失败 - 记录审计日志 const auditLogger = getAuditLogger() await auditLogger.logLoginFailure(c, username, "Invalid credentials") - + await recordLoginFailure(c, username, c.env) return c.json({ code: 401, message: "Invalid credentials", data: null }, 401) }) @@ -566,7 +591,7 @@ authRouter.post("/login/hash", async (c) => { ) } await clearLoginFailures(c, username, c.env) - + // 生成 JWT Token const payload = { id: matchedUser.id, @@ -577,14 +602,14 @@ authRouter.post("/login/hash", async (c) => { } const secret = await getJwtSecret(c) const token = await sign(payload, secret) - + // 生成 CSRF Token const csrfToken = setCSRFToken(c) - + // 记录审计日志 const auditLogger = getAuditLogger() await auditLogger.logLoginSuccess(c, username) - + return c.json({ code: 200, message: "success", @@ -596,7 +621,7 @@ authRouter.post("/login/hash", async (c) => { // 登录失败 - 记录审计日志 const auditLogger = getAuditLogger() await auditLogger.logLoginFailure(c, username, "Invalid credentials") - + await recordLoginFailure(c, username, c.env) return c.json({ code: 401, message: "Invalid credentials", data: null }, 401) }) @@ -683,14 +708,14 @@ export const logoutHandler = async (c: any) => { // token 无效则无需注销 } } - + // 清除 CSRF Token clearCSRFToken(c) - + // 记录审计日志 const auditLogger = getAuditLogger() await auditLogger.logLogout(c) - + return c.json({ code: 200, message: "success", @@ -733,10 +758,11 @@ authRouter.post("/2fa/generate", async (c) => { } const secret = generateTotpSecret() const otpauth = buildOtpauthUrl(secret, user.username) + const qr = await buildQrImageUrl(otpauth) return c.json({ code: 200, message: "success", - data: { qr: buildQrImageUrl(otpauth), secret }, + data: { qr, secret }, }) }) diff --git a/src/backend/server/auth_2fa.test.ts b/src/backend/server/auth_2fa.test.ts new file mode 100644 index 00000000..54ada784 --- /dev/null +++ b/src/backend/server/auth_2fa.test.ts @@ -0,0 +1,59 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { Hono } from "hono" +import { sign } from "hono/jwt" +import { saveDb } from "../internal/model/db" +import { authRouter } from "./auth" + +test("2fa/generate returns a resolved QR data URL", async () => { + const env: any = { + JWT_SECRET: "test-only-jwt-secret", + } + await saveDb( + { + settings: [], + users: [ + { + id: 1, + username: "admin", + password: "unused", + role: 2, + permission: 0, + base_path: "/", + disabled: false, + }, + ], + storages: [], + shares: [], + }, + env, + { force: true }, + ) + + const token = await sign( + { + id: 1, + username: "admin", + role: 2, + exp: Math.floor(Date.now() / 1000) + 60, + jti: "auth-2fa-generate-test", + }, + env.JWT_SECRET, + ) + const app = new Hono() + app.route("/api/auth", authRouter) + + const res = await app.request( + "/api/auth/2fa/generate", + { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + }, + env, + ) + assert.equal(res.status, 200) + const json: any = await res.json() + assert.equal(json.code, 200) + assert.match(json.data.qr, /^data:image\/png;base64,/) + assert.match(json.data.secret, /^[A-Z2-7]+$/) +}) From 2da42567e124d82a737b5503fc425d03445cb440 Mon Sep 17 00:00:00 2001 From: Astroptis Date: Tue, 29 Sep 2026 14:29:08 +0800 Subject: [PATCH 05/49] =?UTF-8?q?fix(drivers):=20=E4=BF=AE=E5=A4=8D=20remo?= =?UTF-8?q?ve/move/copy=20=E8=B7=AF=E5=BE=84=E9=87=8D=E5=A4=8D=E6=8B=BC?= =?UTF-8?q?=E6=8E=A5=E4=B8=8E=E5=88=86=E4=BA=AB=E5=A4=8D=E5=88=B6=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E4=B8=BA=E7=A9=BA=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(s3,share): 修复对象存储删除无效与分享复制链接为空 S3 的 remove/move/copy 按「目录 + name」拼接路径,但 op/storage.ts 传入的 physicalPath 已经是目标项自身的物理路径,结果指向 /:HEAD 404 后退化成按目录递归删除,listObjects 同样查不到对象,最后静默返回成功—— 接口报 Success,对象仍在(move/copy 同根因)。新增 s3_remove.test.ts 用内存 S3 锁定删除/移动实际命中的 key。 分享摘要模板 share_summary_content 被 seed 成空串,前端复制链接 = matchTemplate(getSetting("share_summary_content"), data),模板为空时 writeText("") 会清空剪贴板,却照样弹「已复制」。默认值改为与 Go setting.go 一致的模板,并加迁移把已部署实例 KV 里的空串换成默认值;/api/public/settings 同步补默认值兜底。 Co-Authored-By: opencode * fix(share): 「复制链接」改为只复制纯分享 URL 管理后台「复制链接」渲染 share_summary_content 模板。此前该模板被 seed 成空串,复制出的是空白(writeText("") 直接清空剪贴板却照样弹「已复制」); 上一版修空值时改用了上游 Go 的整段社交文案,按钮叫 Copy link 却复制出一堆 非链接内容——而文件工具栏「分享」对话框里的同名按钮 copy(link()) 复制的 一直是纯 URL。 默认模板改为 {{base_url}}/@s/{{id}},与按钮文案及另一处「复制链接」行为 保持一致;空串与旧文案两种已落盘的值都由 LEGACY_SETTING_MIGRATIONS 迁移 成纯链接,/api/public/settings 的默认值同步更新。 Co-Authored-By: opencode * fix(drivers): 修复 remove/move/copy 把 name 重复拼接到已含文件名的物理路径 op/storage.ts 的 removeItems/moveItems/copyItems 逐项调用 driver,传入的 physicalPath / srcPhys / dstPhys 是目标项自身的物理路径(resolvePath 已经把 name 解析进去)。多数 driver 却把它当作目录再拼一次 name,得到 `/` 这种不存在的路径: - remove:删除不存在的路径。接口静默返回成功,对象仍在——用户报的 「对象存储删除显示 Success 但文件还在」就是这个根因(s3 已单独修复); - move/copy:源/目标路径错位,操作静默失败或作用到错误对象。 按同一约定修复 20 个 driver:azure_blob, uss, chunk, terabox, mopan, onedrive, onedrive_app, pikpak, mediatrack, google_drive, mega, dropbox, wps, yandex, seafile, ftp, sftp, webdav, alias, 139。 各 API 的语义差异按「physical 即目标项」重新推导:如 terabox filemanager 的 dest 是目标目录取 dstPhys 父级、名字取末段;pikpak/mediatrack 的 resolveParentId 实际返回路径自身 id,故先去掉末段再解析。names.length > 1 时保留「目录 + 多个 name」的展开分支(调用方恒传 1 个,属防御性代码)。 审查后确认本就正确的实现未改动:kodbox, halalcloud_open, url_tree, netease_music, 189, 189_tv, smb, google_drive/util 等。 Co-Authored-By: opencode * style(drivers): 按 prettier 格式化本次新增的代码 mediatrack/pikpak 的三元表达式换行与 public_settings 测试断言 折行不符合仓库 prettier 配置,重排为单行/标准折行。 Co-Authored-By: opencode * fix(local): 修复本地盘驱动 remove/move/copy 路径重复拼接 评审指出 drivers/local.ts 是单文件(非 drivers/local/driver.ts),此前批量 修复时被漏检。remove 把项路径当目录再拼一次 name,对 / 执行 fs.rm({force:true}) 时不存在的路径被 force 忽略,静默成功但文件仍在; move/copy 则直接 ENOENT。 与其余 21 个驱动按同一约定修复:physicalPath/srcPhys/dstPhys 即目标项 自身,仅 names.length > 1 时按目录展开。新增 local_remove.test.ts, 用临时目录真实 fs 断言 remove/move/copy 命中目标项。 Co-Authored-By: opencode * refactor(drivers): 删除不可达的 names.length > 1 防御分支 评审指出:各驱动修复时加的 expand 分支在真实调用链里恒不可达 (op/storage.ts 的 removeItems/moveItems/copyItems 恒传单元素 names), 且它把参数语义从「项路径」反转成「目录」,属潜伏陷阱——将来若有 调用方传多 name,行为会与单 name 时不一致。 统一删除:所有 remove/move/copy 一律按「physicalPath/srcPhys/dstPhys 即目标项自身路径」直接处理,names 保留于签名(接口要求)。 行为不变(该分支本就不可达),342 项测试全过。 Co-Authored-By: opencode * test(drivers): 补 dropbox/yandex/webdav/onedrive/sftp/ftp 的 remove/move 回归测试 评审指出 22 个改动驱动里只有 s3/local 有回归保护。按可 mock 性补齐 6 个: - dropbox/yandex/webdav/onedrive:mock globalThis.fetch,断言 DELETE/POST/MOVE 请求的路径参数为目标项自身路径(无 / 二次拼接) - sftp:注入 fake sftpClient(getSFTP 缓存点),断言 unlink/rename 收到项路径 - ftp:替换私有 client(dropbox 先例),断言 removeRecursive/rename 参数 ftp/sftp 无法 mock.module(Node 无实验标志),故用注入方式;6 文件共 13 例, 均验证通过且不发起真实网络连接。 Co-Authored-By: opencode --------- Co-authored-by: Astroptis <250784029+Astroptis@users.noreply.github.com> Co-authored-by: opencode --- src/backend/drivers/139/driver.ts | 24 ++- src/backend/drivers/alias/driver.ts | 28 ++- src/backend/drivers/azure_blob/driver.ts | 21 +- src/backend/drivers/chunk/driver.ts | 7 +- src/backend/drivers/dropbox/driver.ts | 33 ++-- .../drivers/dropbox/dropbox_remove.test.ts | 123 ++++++++++++ src/backend/drivers/ftp/driver.ts | 30 +-- src/backend/drivers/ftp/ftp_remove.test.ts | 65 +++++++ src/backend/drivers/google_drive/driver.ts | 20 +- src/backend/drivers/local.ts | 27 ++- src/backend/drivers/local_remove.test.ts | 88 +++++++++ src/backend/drivers/mediatrack/driver.ts | 81 ++++---- src/backend/drivers/mega/driver.ts | 35 ++-- src/backend/drivers/mopan/driver.ts | 26 +-- src/backend/drivers/onedrive/driver.ts | 62 +++--- .../drivers/onedrive/onedrive_remove.test.ts | 134 +++++++++++++ src/backend/drivers/onedrive_app/driver.ts | 60 +++--- src/backend/drivers/pikpak/driver.ts | 86 ++++---- src/backend/drivers/s3/driver.ts | 74 +++---- src/backend/drivers/s3/s3_remove.test.ts | 127 ++++++++++++ src/backend/drivers/seafile/driver.ts | 87 +++++---- src/backend/drivers/sftp/driver.ts | 27 +-- src/backend/drivers/sftp/sftp_remove.test.ts | 81 ++++++++ src/backend/drivers/terabox/driver.ts | 39 ++-- src/backend/drivers/uss/driver.ts | 15 +- src/backend/drivers/webdav/driver.ts | 41 ++-- .../drivers/webdav/webdav_remove.test.ts | 183 ++++++++++++++++++ src/backend/drivers/wps/driver.ts | 77 ++++---- src/backend/drivers/yandex/driver.ts | 64 +++--- .../drivers/yandex/yandex_remove.test.ts | 132 +++++++++++++ src/backend/internal/model/db.ts | 25 ++- src/backend/server/public.ts | 4 + src/backend/server/public_settings.test.ts | 67 ++++++- 33 files changed, 1482 insertions(+), 511 deletions(-) create mode 100644 src/backend/drivers/dropbox/dropbox_remove.test.ts create mode 100644 src/backend/drivers/ftp/ftp_remove.test.ts create mode 100644 src/backend/drivers/local_remove.test.ts create mode 100644 src/backend/drivers/onedrive/onedrive_remove.test.ts create mode 100644 src/backend/drivers/s3/s3_remove.test.ts create mode 100644 src/backend/drivers/sftp/sftp_remove.test.ts create mode 100644 src/backend/drivers/webdav/webdav_remove.test.ts create mode 100644 src/backend/drivers/yandex/yandex_remove.test.ts diff --git a/src/backend/drivers/139/driver.ts b/src/backend/drivers/139/driver.ts index 1a26fb5a..f5b24c47 100644 --- a/src/backend/drivers/139/driver.ts +++ b/src/backend/drivers/139/driver.ts @@ -204,18 +204,22 @@ export class Yun139Driver implements StorageDriver { names: string[], ): Promise { const clean = this.cleanPath(physicalPath) - const catalogId = await this.resolveCatalogId(clean) + // physicalPath 是目标项自身的路径(op/storage.ts removeItems 逐项调用), + // 目标名取参数末段、父目录取去掉末段后的部分;把项路径当目录解析会走进 + // 目标项内部(目录项尤其明显),相当于找 /,永远落空并静默 + // 返回成功。 + const name = clean.substring(clean.lastIndexOf("/") + 1) + const dirPath = clean.substring(0, clean.lastIndexOf("/")) || "/" + const catalogId = await this.resolveCatalogId(dirPath) const disk = await this.client.listFiles(catalogId) - for (const name of names) { - const folder = disk.folders.find((f) => f.catalogName === name) - if (folder) { - await this.client.deleteCatalog(folder.catalogID) - } else { - const file = disk.files.find((f) => f.contentName === name) - if (file && file.contentID) { - await this.client.deleteFile(file.contentID) - } + const folder = disk.folders.find((f) => f.catalogName === name) + if (folder) { + await this.client.deleteCatalog(folder.catalogID) + } else { + const file = disk.files.find((f) => f.contentName === name) + if (file && file.contentID) { + await this.client.deleteFile(file.contentID) } } } diff --git a/src/backend/drivers/alias/driver.ts b/src/backend/drivers/alias/driver.ts index 20e1c9fc..1aa35068 100644 --- a/src/backend/drivers/alias/driver.ts +++ b/src/backend/drivers/alias/driver.ts @@ -22,6 +22,12 @@ export class AliasDriver implements StorageDriver { return s === "/" ? "/" : s } + /** 取所在目录:physicalPath 是目标项自身路径时,去掉最后一段即其父目录 */ + private parentPath(p: string): string { + const clean = this.cleanPath(p) + return clean.substring(0, clean.lastIndexOf("/")) || "/" + } + private parsePaths(): void { const raw = this.addition.paths || "" const lines = raw @@ -231,7 +237,11 @@ export class AliasDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const targets = this.getTargetsForPath(physicalPath) + // physicalPath 是目标项自身的路径(op/storage.ts removeItems 逐项调用), + // 而 removeItems 需要「父目录 + names」(内部会再拼 name);直接把项路径 + // 当目录传入会得到 /,目标不存在导致删除静默失败。 + const dirPath = this.parentPath(physicalPath) + const targets = this.getTargetsForPath(dirPath) if (targets.length === 0) return const { removeItems } = await import("../../internal/op/storage") await removeItems(targets[0].targetFullPath, names) @@ -244,8 +254,13 @@ export class AliasDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcTargets = this.getTargetsForPath(srcPhys) - const dstTargets = this.getTargetsForPath(dstPhys) + // srcPhys/dstPhys 是源/目标项自身的路径(已含 name),moveItems 需要的 + // 是各自所在父目录(内部会再拼 name);传项路径会得到 /, + // 操作静默失败。 + const srcDirPath = this.parentPath(srcPhys) + const dstDirPath = this.parentPath(dstPhys) + const srcTargets = this.getTargetsForPath(srcDirPath) + const dstTargets = this.getTargetsForPath(dstDirPath) if (srcTargets.length === 0 || dstTargets.length === 0) { throw new Error("[Alias] cannot resolve source or destination path") } @@ -264,8 +279,11 @@ export class AliasDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcTargets = this.getTargetsForPath(srcPhys) - const dstTargets = this.getTargetsForPath(dstPhys) + // 同 move:srcPhys/dstPhys 是项自身路径,copyItems 需要各自所在父目录。 + const srcDirPath = this.parentPath(srcPhys) + const dstDirPath = this.parentPath(dstPhys) + const srcTargets = this.getTargetsForPath(srcDirPath) + const dstTargets = this.getTargetsForPath(dstDirPath) if (srcTargets.length === 0 || dstTargets.length === 0) { throw new Error("[Alias] cannot resolve source or destination path") } diff --git a/src/backend/drivers/azure_blob/driver.ts b/src/backend/drivers/azure_blob/driver.ts index 8987df42..405a3938 100644 --- a/src/backend/drivers/azure_blob/driver.ts +++ b/src/backend/drivers/azure_blob/driver.ts @@ -302,12 +302,12 @@ export class AzureBlobDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - for (const name of names) { - const srcKey = joinPath(this.getKey(srcPhys), name) - const dstKey = joinPath(this.getKey(dstPhys), name) - await this.copyBlob(srcKey, dstKey) - await this.deleteBlob(srcKey) - } + // srcPhys/dstPhys 是目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 不得再拼 name,否则会指向 `/` 这种不存在的 blob。 + const srcKey = this.getKey(srcPhys) + const dstKey = this.getKey(dstPhys) + await this.copyBlob(srcKey, dstKey) + await this.deleteBlob(srcKey) } async copy( @@ -317,11 +317,10 @@ export class AzureBlobDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - for (const name of names) { - const srcKey = joinPath(this.getKey(srcPhys), name) - const dstKey = joinPath(this.getKey(dstPhys), name) - await this.copyBlob(srcKey, dstKey) - } + // 同 move:srcPhys/dstPhys 已是目标项自身路径,不得再拼 name。 + const srcKey = this.getKey(srcPhys) + const dstKey = this.getKey(dstPhys) + await this.copyBlob(srcKey, dstKey) } async put(_v: string, physicalPath: string, content: Buffer): Promise { diff --git a/src/backend/drivers/chunk/driver.ts b/src/backend/drivers/chunk/driver.ts index 9ad91afb..87d2f0e1 100644 --- a/src/backend/drivers/chunk/driver.ts +++ b/src/backend/drivers/chunk/driver.ts @@ -239,12 +239,14 @@ export class ChunkDriver implements StorageDriver { await this.remoteDriver!.get("", srcRemote) await this.remoteDriver!.move(srcDir, dstDir, names, srcRemote, dstRemote) } catch { + // 分片文件:dst 端必须是目标项自己的分片目录(与虚拟文件同级), + // 拼成 `<目标项>/` 会指向不存在的路径。 await this.remoteDriver!.move( srcDir, dstDir, names, this.chunkDirRemote(srcPhysical), - joinPath(dstRemote, this.chunkPrefix + basename(srcPhysical)), + this.chunkDirRemote(dstPhysical), ) } } @@ -263,12 +265,13 @@ export class ChunkDriver implements StorageDriver { await this.remoteDriver!.get("", srcRemote) await this.remoteDriver!.copy(srcDir, dstDir, names, srcRemote, dstRemote) } catch { + // 分片文件:同 move,dst 端用目标项自己的分片目录。 await this.remoteDriver!.copy( srcDir, dstDir, names, this.chunkDirRemote(srcPhysical), - joinPath(dstRemote, this.chunkPrefix + basename(srcPhysical)), + this.chunkDirRemote(dstPhysical), ) } } diff --git a/src/backend/drivers/dropbox/driver.ts b/src/backend/drivers/dropbox/driver.ts index 1569727b..4aefb63f 100644 --- a/src/backend/drivers/dropbox/driver.ts +++ b/src/backend/drivers/dropbox/driver.ts @@ -127,11 +127,10 @@ export class DropboxDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const cleanDir = this.cleanPath(physicalPath) - for (const name of names) { - const target = cleanDir === "" ? `/${name}` : `${cleanDir}/${name}` - await this.client.delete(target) - } + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 直接删除它即可;不得再拼 name,否则指向 `/`,delete 404 报错。 + const target = this.cleanPath(physicalPath) + await this.client.delete(target) } async move( @@ -141,13 +140,11 @@ export class DropboxDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const cleanSrc = this.cleanPath(srcPhys) - const cleanDst = this.cleanPath(dstPhys) - for (const name of names) { - const from = cleanSrc === "" ? `/${name}` : `${cleanSrc}/${name}` - const to = cleanDst === "" ? `/${name}` : `${cleanDst}/${name}` - await this.client.move(from, to) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 直接作为 from/to 使用;不得再拼 name,否则指向 `/`,源/目标错位。 + const from = this.cleanPath(srcPhys) + const to = this.cleanPath(dstPhys) + await this.client.move(from, to) } async copy( @@ -157,13 +154,11 @@ export class DropboxDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const cleanSrc = this.cleanPath(srcPhys) - const cleanDst = this.cleanPath(dstPhys) - for (const name of names) { - const from = cleanSrc === "" ? `/${name}` : `${cleanSrc}/${name}` - const to = cleanDst === "" ? `/${name}` : `${cleanDst}/${name}` - await this.client.copy(from, to) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts copyItems 逐项调用), + // 直接作为 from/to 使用;不得再拼 name,否则指向 `/`,源/目标错位。 + const from = this.cleanPath(srcPhys) + const to = this.cleanPath(dstPhys) + await this.client.copy(from, to) } async put( diff --git a/src/backend/drivers/dropbox/dropbox_remove.test.ts b/src/backend/drivers/dropbox/dropbox_remove.test.ts new file mode 100644 index 00000000..26bc6298 --- /dev/null +++ b/src/backend/drivers/dropbox/dropbox_remove.test.ts @@ -0,0 +1,123 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { DropboxDriver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * const resolved = await resolvePath(`${dir}/${name}`) + * await driver.remove(virtualPath, resolved.physical, [name]) + * + * 即 physicalPath/srcPhys/dstPhys 是【目标项自身】的物理路径(已含文件名), + * 调用方恒传单元素 names。driver 不得再把 name 拼到该路径后面,否则 body.path + * 变成 `/`:delete_v2/move_v2 找不到该路径 → 404 报错, + * 目标项既没被删除也没被移动。 + */ + +type Call = { method: string; url: string; body: string } + +/** 记录请求,恒返回 200 + application/json(util.request 走 res.json() 分支) */ +function mockDropbox(calls: Call[]) { + return async (input: any, init: any = {}) => { + const url = String(typeof input === "string" ? input : input.url) + calls.push({ + method: String(init.method || "GET").toUpperCase(), + url, + body: typeof init.body === "string" ? init.body : "", + }) + return new Response("{}", { + status: 200, + headers: { "content-type": "application/json" }, + }) + } +} + +function withMock(calls: Call[], fn: () => Promise): Promise { + const original = globalThis.fetch + globalThis.fetch = mockDropbox(calls) as any + return fn().finally(() => { + globalThis.fetch = original + }) +} + +function makeDriver() { + // access_token 必填:util.request 发现无 token 会先联网 refreshToken(), + // 测试不调用 init(),也不允许出现 oauth2/token 请求。 + return new DropboxDriver({ + access_token: "tok", + refresh_token: "r", + client_id: "id", + client_secret: "sec", + }) +} + +test("Dropbox remove() 删除项自身路径,不再拼一次 name", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().remove("/d/a.txt", "/d/a.txt", ["a.txt"]), + ) + + const deletes = calls.filter( + (c) => c.url === "https://api.dropboxapi.com/2/files/delete_v2", + ) + assert.equal( + deletes.length, + 1, + `应恰好发出 1 次 files/delete_v2 请求,实际 ${deletes.length}`, + ) + const body = JSON.parse(deletes[0].body) + assert.equal( + body.path, + "/d/a.txt", + `delete 的 path 应为项自身路径 /d/a.txt,实际 ${body.path}`, + ) + assert.ok( + !String(body.path).endsWith("/a.txt/a.txt"), + "path 不得出现二次拼接的 /", + ) + assert.ok( + calls.every((c) => !c.url.includes("/oauth2/token")), + "已带 access_token,不应触发 token 刷新请求", + ) +}) + +test("Dropbox move() 的 from_path/to_path 分别为源/目标项自身路径", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().move("/d", "/d/dst", ["a.txt"], "/d/a.txt", "/d/dst/a.txt"), + ) + + const moves = calls.filter( + (c) => c.url === "https://api.dropboxapi.com/2/files/move_v2", + ) + assert.equal( + moves.length, + 1, + `应恰好发出 1 次 files/move_v2 请求,实际 ${moves.length}`, + ) + const body = JSON.parse(moves[0].body) + assert.equal( + body.from_path, + "/d/a.txt", + `from_path 应为源项自身路径 /d/a.txt,实际 ${body.from_path}`, + ) + assert.equal( + body.to_path, + "/d/dst/a.txt", + `to_path 应为目标项自身路径 /d/dst/a.txt,实际 ${body.to_path}`, + ) + assert.ok( + !String(body.from_path).endsWith("/a.txt/a.txt"), + "from_path 不得出现二次拼接的 /", + ) + assert.ok( + !String(body.to_path).endsWith("/a.txt/a.txt"), + "to_path 不得出现二次拼接的 /", + ) + assert.ok( + calls.every((c) => !c.url.includes("/oauth2/token")), + "已带 access_token,不应触发 token 刷新请求", + ) +}) diff --git a/src/backend/drivers/ftp/driver.ts b/src/backend/drivers/ftp/driver.ts index 373d6dec..71e078f6 100644 --- a/src/backend/drivers/ftp/driver.ts +++ b/src/backend/drivers/ftp/driver.ts @@ -140,34 +140,24 @@ export class FTPDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const targetDir = cleanPosixPath(physicalPath) - if (names && names.length > 0) { - for (const name of names) { - await this.client.removeRecursive(posixJoin(targetDir, name)) - } - } else { - await this.client.removeRecursive(targetDir) - } + // physicalPath 已是目标项自身的物理路径(op/storage.ts 逐项解析后传入), + // 直接删除即可;不得再拼 name,否则指向不存在的 /,导致静默删除失败。 + const target = cleanPosixPath(physicalPath) + await this.client.removeRecursive(target) } async move( _srcDir: string, - dstDir: string, + _dstDir: string, names: string[], srcPhys: string, dstPhys: string, ): Promise { - if (names && names.length > 0) { - for (const name of names) { - const src = posixJoin(srcPhys, name) - const dst = posixJoin(dstPhys, name) - await this.client.rename(src, dst) - } - } else { - const filename = srcPhys.split("/").filter(Boolean).pop() || "" - const dst = posixJoin(dstDir, filename) - await this.client.rename(cleanPosixPath(srcPhys), dst) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径,直接重命名即可; + // 不得再拼 name,否则源/目标路径错位。 + const src = cleanPosixPath(srcPhys) + const dst = cleanPosixPath(dstPhys) + await this.client.rename(src, dst) } async copy( diff --git a/src/backend/drivers/ftp/ftp_remove.test.ts b/src/backend/drivers/ftp/ftp_remove.test.ts new file mode 100644 index 00000000..6d08a43a --- /dev/null +++ b/src/backend/drivers/ftp/ftp_remove.test.ts @@ -0,0 +1,65 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { FTPDriver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * await driver.remove(virtualPath, resolved.physical, [name]) + * await driver.move(srcDir, dstDir, [name], srcPhys, dstPhys) + * + * 即 physicalPath / srcPhys / dstPhys 都是【目标项自身】的物理路径(已含文件名), + * driver 不得再把 name 拼接到路径后面,否则会指向 `/`, + * removeRecursive/rename 打到不存在的路径 → 静默失败。 + * + * Node v24 下 mock.module 不可用(需实验标志),且不调 init()(否则真连 TCP): + * 用注入私有 client 的方式(同 dropbox/driver.test.ts 先例),替换 driver.client + * 为 fake 对象,全程不建立网络连接。只影响本用例新建的 driver 实例,不污染全局。 + */ + +function makeDriverWithFakeClient(calls: string[]): FTPDriver { + const driver = new FTPDriver({ + address: "127.0.0.1:21", + username: "u", + password: "p", + }) + ;(driver as any).client = { + removeRecursive: async (p: string) => { + calls.push(`rm ${p}`) + }, + rename: async (s: string, d: string) => { + calls.push(`mv ${s} -> ${d}`) + }, + } + return driver +} + +test("FTP remove() 删除项自身路径,不拼接 name", async () => { + const calls: string[] = [] + const driver = makeDriverWithFakeClient(calls) + + await driver.remove("/d/a.txt", "/d/a.txt", ["a.txt"]) + + assert.deepEqual( + calls, + ["rm /d/a.txt"], + "removeRecursive 应收到项自身路径(fake 必须被调用,否则注入未生效)", + ) + assert.ok( + !calls.some((c) => c.includes("a.txt/a.txt")), + `路径不得出现二次拼接的 a.txt/a.txt,实际:${calls.join(", ")}`, + ) +}) + +test("FTP move() 把源/目标项自身路径传给 rename", async () => { + const calls: string[] = [] + const driver = makeDriverWithFakeClient(calls) + + await driver.move("/d", "/d/bak", ["a.txt"], "/d/a.txt", "/d/bak/a.txt") + + assert.deepEqual( + calls, + ["mv /d/a.txt -> /d/bak/a.txt"], + "rename 的两个参数必须是源/目标项自身路径(fake 必须被调用,否则注入未生效)", + ) +}) diff --git a/src/backend/drivers/google_drive/driver.ts b/src/backend/drivers/google_drive/driver.ts index 0da07d4b..ba6bbcbc 100644 --- a/src/backend/drivers/google_drive/driver.ts +++ b/src/backend/drivers/google_drive/driver.ts @@ -106,11 +106,11 @@ export class GoogleDrive implements StorageDriver { } async move( - srcDir: string, - dstDir: string, + _srcDir: string, + _dstDir: string, _names: string[], srcPhysical: string, - _dstPhysical: string, + dstPhysical: string, ): Promise { const fileId = await this.client.resolveFileId(srcPhysical) const srcParts = srcPhysical.split("/").filter(Boolean) @@ -118,20 +118,24 @@ export class GoogleDrive implements StorageDriver { const srcParentId = await this.client.resolveFileId( "/" + srcParts.join("/"), ) - const dstParentId = await this.client.resolveFileId(dstDir) + // dstPhysical 是目标项自身的物理路径,父目录去掉末段即得; + // dstDir 是虚拟路径(含挂载前缀),而 resolveFileId 期望物理路径,直接用会错位。 + const { parentId: dstParentId } = + await this.client.resolveParentAndName(dstPhysical) await this.client.move(fileId, srcParentId, dstParentId) } async copy( _srcDir: string, - dstDir: string, + _dstDir: string, _names: string[], srcPhysical: string, - _dstPhysical: string, + dstPhysical: string, ): Promise { const fileId = await this.client.resolveFileId(srcPhysical) - const name = srcPhysical.split("/").filter(Boolean).pop() || "copy" - const dstParentId = await this.client.resolveFileId(dstDir) + // 目标父目录与目标名字均以 dstPhysical(目标项自身路径)为准。 + const { parentId: dstParentId, name } = + await this.client.resolveParentAndName(dstPhysical) await this.client.copy(fileId, dstParentId, name) } diff --git a/src/backend/drivers/local.ts b/src/backend/drivers/local.ts index e6bf8cb9..2bffdb9b 100644 --- a/src/backend/drivers/local.ts +++ b/src/backend/drivers/local.ts @@ -101,10 +101,10 @@ export class LocalDriver implements StorageDriver { await initNodeModules() if (!fs || !path) throw new Error("LocalDriver is not supported in Edge Runtime") - for (const name of names) { - const itemPath = path.join(physicalPath, name) - await fs.rm(itemPath, { recursive: true, force: true }) - } + // physicalPath 是目标项自身的路径(op/storage.ts removeItems 逐项调用), + // 直接删除即可;不得再拼 name,否则指向 /,配合 force:true 会 + // 静默"成功"但什么都没删。 + await fs.rm(physicalPath, { recursive: true, force: true }) } async move( @@ -117,12 +117,10 @@ export class LocalDriver implements StorageDriver { await initNodeModules() if (!fs || !path) throw new Error("LocalDriver is not supported in Edge Runtime") - for (const name of names) { - const src = path.join(srcPhys, name) - const dst = path.join(dstPhys, name) - await fs.mkdir(path.dirname(dst), { recursive: true }) - await fs.rename(src, dst) - } + // srcPhys/dstPhys 是源/目标项自身的路径(已含 name),直接移动即可; + // 不得再拼 name,否则指向不存在的 / 而 ENOENT。 + await fs.mkdir(path.dirname(dstPhys), { recursive: true }) + await fs.rename(srcPhys, dstPhys) } async copy( @@ -135,12 +133,9 @@ export class LocalDriver implements StorageDriver { await initNodeModules() if (!fs || !path) throw new Error("LocalDriver is not supported in Edge Runtime") - for (const name of names) { - const src = path.join(srcPhys, name) - const dst = path.join(dstPhys, name) - await fs.mkdir(path.dirname(dst), { recursive: true }) - await fs.cp(src, dst, { recursive: true }) - } + // 同 move:srcPhys/dstPhys 已是源/目标项自身路径,不得再拼 name。 + await fs.mkdir(path.dirname(dstPhys), { recursive: true }) + await fs.cp(srcPhys, dstPhys, { recursive: true }) } async put( diff --git a/src/backend/drivers/local_remove.test.ts b/src/backend/drivers/local_remove.test.ts new file mode 100644 index 00000000..580395b4 --- /dev/null +++ b/src/backend/drivers/local_remove.test.ts @@ -0,0 +1,88 @@ +import { test } from "node:test" +import assert from "node:assert/strict" +import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises" +import { tmpdir } from "node:os" +import * as path from "node:path" +import { LocalDriver } from "./local" + +async function exists(p: string): Promise { + try { + await readFile(p) + return true + } catch { + try { + await import("node:fs/promises").then((fs) => fs.stat(p)) + return true + } catch { + return false + } + } +} + +async function makeDir(): Promise { + return await mkdtemp(path.join(tmpdir(), "openlist-local-test-")) +} + +test("LocalDriver.remove 删除目标项自身", async (t) => { + const dir = await makeDir() + t.after(() => rm(dir, { recursive: true, force: true })) + const file = path.join(dir, "a.txt") + await writeFile(file, "x") + + const driver = new LocalDriver() + await driver.remove("/d/a.txt", file, ["a.txt"]) + + assert.equal(await exists(file), false, "目标文件应被删除") +}) + +test("LocalDriver.remove 删除目录", async (t) => { + const dir = await makeDir() + t.after(() => rm(dir, { recursive: true, force: true })) + const sub = path.join(dir, "sub") + await mkdir(sub) + await writeFile(path.join(sub, "inner.txt"), "x") + + const driver = new LocalDriver() + await driver.remove("/d/sub", sub, ["sub"]) + + assert.equal(await exists(path.join(sub, "inner.txt")), false, "目录应被删除") + assert.equal(await exists(sub), false, "目录应被删除") +}) + +test("LocalDriver.move 移动目标项自身", async (t) => { + const dir = await makeDir() + t.after(() => rm(dir, { recursive: true, force: true })) + const src = path.join(dir, "a.txt") + const dstDir = path.join(dir, "dst") + await mkdir(dstDir) + await writeFile(src, "hello") + + const driver = new LocalDriver() + await driver.move("/d", "/d/dst", ["a.txt"], src, path.join(dstDir, "a.txt")) + + assert.equal(await exists(src), false, "源文件应被移走") + assert.equal( + await readFile(path.join(dstDir, "a.txt"), "utf8"), + "hello", + "目标位置应有文件", + ) +}) + +test("LocalDriver.copy 复制目标项自身", async (t) => { + const dir = await makeDir() + t.after(() => rm(dir, { recursive: true, force: true })) + const src = path.join(dir, "a.txt") + const dstDir = path.join(dir, "dst") + await mkdir(dstDir) + await writeFile(src, "hello") + + const driver = new LocalDriver() + await driver.copy("/d", "/d/dst", ["a.txt"], src, path.join(dstDir, "a.txt")) + + assert.equal(await exists(src), true, "源文件应保留") + assert.equal( + await readFile(path.join(dstDir, "a.txt"), "utf8"), + "hello", + "目标位置应有副本", + ) +}) diff --git a/src/backend/drivers/mediatrack/driver.ts b/src/backend/drivers/mediatrack/driver.ts index 398a35c3..c1188086 100644 --- a/src/backend/drivers/mediatrack/driver.ts +++ b/src/backend/drivers/mediatrack/driver.ts @@ -202,18 +202,17 @@ export class MediatrackDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const parentId = await this.resolveParentId(physicalPath) + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 目标名取参数末段、父目录取去掉末段后的部分;resolveParentId 解析的是 + // 传入路径自身的 id,把项路径当父目录会去 的子项里按 title 查找 + //(相当于找 /),找不到时静默跳过,对象仍然存在。 + const clean = this.cleanPath(physicalPath) + const name = clean.split("/").pop() || "" + const parentPath = clean.split("/").slice(0, -1).join("/") + const parentId = await this.resolveParentId(parentPath) const files = await this.client.getFiles(parentId) - const ids: string[] = [] - - for (const name of names) { - const match = files.find((f) => f.title === name) - if (match) { - ids.push(match.id) - } - } - - if (ids.length === 0) return + const match = files.find((f) => f.title === name) + if (!match) return await this.client.request( "https://jayce.api.mediatrack.cn/v4/assets/batch/delete", @@ -221,15 +220,12 @@ export class MediatrackDriver implements StorageDriver { method: "DELETE", body: { origin_id: parentId, - ids, + ids: [match.id], }, }, ) - const clean = this.cleanPath(physicalPath) - for (const name of names) { - this.idCache.delete(clean ? `${clean}/${name}` : `/${name}`) - } + this.idCache.delete(clean) } async move( @@ -239,19 +235,21 @@ export class MediatrackDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcParentId = await this.resolveParentId(srcPhys) - const dstParentId = await this.resolveParentId(dstPhys) + // srcPhys/dstPhys 是源/目标项自身的物理路径(已含 name),父目录取去掉 + // 末段后的部分:源侧在其父目录的子项里按 title 找,目标侧解析目标项所在 + // 目录的 id。把项路径当父目录解析会指到项本身:源侧永远落空, + // 目标侧解析一个尚不存在的目标项必然报错。 + const srcClean = this.cleanPath(srcPhys) + const dstClean = this.cleanPath(dstPhys) + const srcName = srcClean.split("/").pop() || "" + const srcParentPath = srcClean.split("/").slice(0, -1).join("/") + const dstParentPath = dstClean.split("/").slice(0, -1).join("/") + + const srcParentId = await this.resolveParentId(srcParentPath) + const dstParentId = await this.resolveParentId(dstParentPath) const srcFiles = await this.client.getFiles(srcParentId) - const ids: string[] = [] - - for (const name of names) { - const match = srcFiles.find((f) => f.title === name) - if (match) { - ids.push(match.id) - } - } - - if (ids.length === 0) return + const match = srcFiles.find((f) => f.title === srcName) + if (!match) return await this.client.request( "https://jayce.api.mediatrack.cn/v4/assets/batch/move", @@ -259,7 +257,7 @@ export class MediatrackDriver implements StorageDriver { method: "POST", body: { parent_id: dstParentId, - ids, + ids: [match.id], }, }, ) @@ -272,19 +270,18 @@ export class MediatrackDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcParentId = await this.resolveParentId(srcPhys) - const dstParentId = await this.resolveParentId(dstPhys) + // 与 move 同理:源/目标项自身路径先去掉末段得到父目录,目标名取源路径末段。 + const srcClean = this.cleanPath(srcPhys) + const dstClean = this.cleanPath(dstPhys) + const srcName = srcClean.split("/").pop() || "" + const srcParentPath = srcClean.split("/").slice(0, -1).join("/") + const dstParentPath = dstClean.split("/").slice(0, -1).join("/") + + const srcParentId = await this.resolveParentId(srcParentPath) + const dstParentId = await this.resolveParentId(dstParentPath) const srcFiles = await this.client.getFiles(srcParentId) - const ids: string[] = [] - - for (const name of names) { - const match = srcFiles.find((f) => f.title === name) - if (match) { - ids.push(match.id) - } - } - - if (ids.length === 0) return + const match = srcFiles.find((f) => f.title === srcName) + if (!match) return await this.client.request( "https://jayce.api.mediatrack.cn/v4/assets/batch/clone", @@ -292,7 +289,7 @@ export class MediatrackDriver implements StorageDriver { method: "POST", body: { parent_id: dstParentId, - ids, + ids: [match.id], }, }, ) diff --git a/src/backend/drivers/mega/driver.ts b/src/backend/drivers/mega/driver.ts index 7d6b2fa1..3e232450 100644 --- a/src/backend/drivers/mega/driver.ts +++ b/src/backend/drivers/mega/driver.ts @@ -152,20 +152,18 @@ export class MegaDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstNode = this.resolveNodeByPath(this.cleanPath(dstPhys)) + // srcPhys/dstPhys 是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 `/` 导致静默失败。 + const srcPath = this.cleanPath(srcPhys) + const dstPath = this.cleanPath(dstPhys) + const dstParentPath = dstPath.substring(0, dstPath.lastIndexOf("/")) || "/" + const dstNode = this.resolveNodeByPath(dstParentPath) if (!dstNode || !dstNode.is_dir) { throw new Error("Destination folder not found") } - - for (const name of names) { - const srcItemPath = - this.cleanPath(srcPhys) === "/" - ? `/${name}` - : `${this.cleanPath(srcPhys)}/${name}` - const srcNode = this.resolveNodeByPath(srcItemPath) - if (srcNode) { - await this.client.moveNode(srcNode.id, dstNode.id) - } + const srcNode = this.resolveNodeByPath(srcPath) + if (srcNode) { + await this.client.moveNode(srcNode.id, dstNode.id) } } @@ -186,15 +184,12 @@ export class MegaDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - for (const name of names) { - const itemPath = - this.cleanPath(physicalPath) === "/" - ? `/${name}` - : `${this.cleanPath(physicalPath)}/${name}` - const node = this.resolveNodeByPath(itemPath) - if (node) { - await this.client.deleteNode(node.id) - } + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 `/`,resolveNodeByPath + // 返回 null 后被静默跳过,接口成功但节点仍在。 + const node = this.resolveNodeByPath(this.cleanPath(physicalPath)) + if (node) { + await this.client.deleteNode(node.id) } } diff --git a/src/backend/drivers/mopan/driver.ts b/src/backend/drivers/mopan/driver.ts index 6871f656..8b2b280d 100644 --- a/src/backend/drivers/mopan/driver.ts +++ b/src/backend/drivers/mopan/driver.ts @@ -275,12 +275,14 @@ export class MoPanDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstFolderId = await this.resolveFolderId(dstPhys) - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const srcItem = await this.get(srcDir, srcItemPath) - await this.performBatchTask(srcItem, dstFolderId, TaskTypeMove) - } + // srcPhys/dstPhys 是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 `/` 这种不存在的路径。 + // 目标文件夹取 dstPhys 去掉末段后的父目录。 + const srcItem = await this.get(srcDir, srcPhys) + const dstFolderPath = dstPhys.split("/").slice(0, -1).join("/") + const dstFolderId = await this.resolveFolderId(dstFolderPath) + + await this.performBatchTask(srcItem, dstFolderId, TaskTypeMove) } async copy( @@ -290,12 +292,12 @@ export class MoPanDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstFolderId = await this.resolveFolderId(dstPhys) - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const srcItem = await this.get(srcDir, srcItemPath) - await this.performBatchTask(srcItem, dstFolderId, TaskTypeCopy) - } + // 同 move:srcPhys/dstPhys 已是目标项自身路径,参数即目标项路径,不得再拼 name。 + const srcItem = await this.get(srcDir, srcPhys) + const dstFolderPath = dstPhys.split("/").slice(0, -1).join("/") + const dstFolderId = await this.resolveFolderId(dstFolderPath) + + await this.performBatchTask(srcItem, dstFolderId, TaskTypeCopy) } private async performBatchTask( diff --git a/src/backend/drivers/onedrive/driver.ts b/src/backend/drivers/onedrive/driver.ts index 1f648844..62898ba6 100644 --- a/src/backend/drivers/onedrive/driver.ts +++ b/src/backend/drivers/onedrive/driver.ts @@ -222,12 +222,10 @@ export class Onedrive implements StorageDriver { physicalPath: string, names: string[], ): Promise { - for (const name of names) { - const itemPath = - physicalPath === "/" ? `/${name}` : `${physicalPath}/${name}` - const url = this.getMetaUrl(false, itemPath) - await requestApi(this, url, "DELETE") - } + // physicalPath 是目标项自身的物理路径(op/storage.ts 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 /,DELETE 404 导致对象仍然存在。 + const url = this.getMetaUrl(false, physicalPath) + await requestApi(this, url, "DELETE") } async move( @@ -237,25 +235,25 @@ export class Onedrive implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - // Determine the destination parent reference - // Fetch dstPhys details to get its ID, or construct parentReference path - const dstUrl = this.getMetaUrl(false, dstPhys) + // srcPhys/dstPhys 是源/目标项自身的物理路径(已含 name,参数即目标项路径, + // 不得再拼 name,否则指向 /): + // 目标父目录取 dstPhys 去掉末段(直接 GET dstPhys 拿到的是目标项本身,通常还不存在), + // 目标名字以 dstPhys 末段为准,源项路径则直接使用 srcPhys。 + const dstParentPath = dstPhys.split("/").slice(0, -1).join("/") || "/" + const dstUrl = this.getMetaUrl(false, dstParentPath) const dstRes = await requestApi(this, dstUrl, "GET") const dstId = dstRes.id const driveId = dstRes.parentReference?.driveId - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const data = { - parentReference: { - id: dstId, - ...(driveId ? { driveId } : {}), - }, - name, - } - const url = this.getMetaUrl(false, srcItemPath) - await requestApi(this, url, "PATCH", data) + const data = { + parentReference: { + id: dstId, + ...(driveId ? { driveId } : {}), + }, + name: dstPhys.split("/").filter(Boolean).pop() || "", } + const url = this.getMetaUrl(false, srcPhys) + await requestApi(this, url, "PATCH", data) } async copy( @@ -265,23 +263,23 @@ export class Onedrive implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstUrl = this.getMetaUrl(false, dstPhys) + // 与 move 同理:参数是目标项自身路径,不得再拼 name; + // 目标父目录 = dstPhys 去掉末段,目标名字取 dstPhys 末段,源项路径直接使用 srcPhys。 + const dstParentPath = dstPhys.split("/").slice(0, -1).join("/") || "/" + const dstUrl = this.getMetaUrl(false, dstParentPath) const dstRes = await requestApi(this, dstUrl, "GET") const dstId = dstRes.id const driveId = dstRes.parentReference?.driveId - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const data = { - parentReference: { - id: dstId, - ...(driveId ? { driveId } : {}), - }, - name, - } - const url = this.getMetaUrl(false, srcItemPath, "copy") - await requestApi(this, url, "POST", data) + const data = { + parentReference: { + id: dstId, + ...(driveId ? { driveId } : {}), + }, + name: dstPhys.split("/").filter(Boolean).pop() || "", } + const url = this.getMetaUrl(false, srcPhys, "copy") + await requestApi(this, url, "POST", data) } async put( diff --git a/src/backend/drivers/onedrive/onedrive_remove.test.ts b/src/backend/drivers/onedrive/onedrive_remove.test.ts new file mode 100644 index 00000000..2d6efa05 --- /dev/null +++ b/src/backend/drivers/onedrive/onedrive_remove.test.ts @@ -0,0 +1,134 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Onedrive } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * await driver.remove(virtualPath, resolved.physical, [name]) + * await driver.move(srcDir, dstDir, [name], srcPhys, dstPhys) + * + * 即 physicalPath / srcPhys / dstPhys 都是【目标项自身】的物理路径(已含文件名), + * driver 不得再把 name 拼接到路径后面,否则会指向 `/`: + * DELETE 404 → 静默失败,对象仍然存在。 + * + * 构造 `new Onedrive({})` 不联网,不调 init()(fixture 没有 refresh_token); + * 401 会触发 refreshToken 真实联网,所以 mock fetch 一律返回 2xx。 + */ + +type Call = { method: string; url: string; body?: string } + +/** 按 (method, url) 分派的 Graph API mock,记录所有请求以便断言目标路径 */ +function mockGraph(calls: Call[]) { + return async (input: any, init: any = {}) => { + const url = new URL(String(typeof input === "string" ? input : input.url)) + const method = String(init.method || "GET").toUpperCase() + calls.push({ + method, + url: url.toString(), + body: typeof init.body === "string" ? init.body : undefined, + }) + + // DELETE → 204 无 body;GET → 目标父目录解析;PATCH → 更新项 + if (method === "DELETE") return new Response(null, { status: 204 }) + const json = JSON.stringify( + method === "GET" + ? { id: "parent-id", parentReference: { driveId: "d1" } } + : {}, + ) + return new Response(json, { + status: 200, + headers: { "content-type": "application/json" }, + }) + } +} + +function withMock(calls: Call[], fn: () => Promise): Promise { + const original = globalThis.fetch + globalThis.fetch = mockGraph(calls) as any + return fn().finally(() => { + globalThis.fetch = original + }) +} + +test("OneDrive remove() 删除目标项自身路径,不拼接 name", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + new Onedrive({}).remove("/d/a.txt", "/d/a.txt", ["a.txt"]), + ) + + const deletes = calls.filter((c) => c.method === "DELETE") + assert.equal( + deletes.length, + 1, + `应恰好发出 1 次 DELETE,实际请求:${calls.map((c) => c.method).join(", ") || "(无)"}`, + ) + assert.equal( + deletes[0].url, + "https://graph.microsoft.com/v1.0/me/drive/root:/d/a.txt:", + "DELETE 必须指向项自身路径(结尾裸冒号),即 physicalPath 本身", + ) + const pathname = decodeURIComponent(new URL(deletes[0].url).pathname) + assert.ok( + !pathname.includes("a.txt/a.txt"), + `路径不得出现二次拼接的 a.txt/a.txt,实际:${pathname}`, + ) +}) + +test("OneDrive move() 先 GET 目标父目录,再 PATCH 源项自身路径", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + new Onedrive({}).move( + "/d", + "/d/bak", + ["a.txt"], + "/d/a.txt", + "/d/bak/a.txt", + ), + ) + + const getIdx = calls.findIndex((c) => c.method === "GET") + const patchIdx = calls.findIndex((c) => c.method === "PATCH") + assert.ok( + getIdx >= 0 && patchIdx >= 0, + `move 应先 GET 目标父目录再 PATCH 源项,实际请求:${calls.map((c) => c.method).join(", ") || "(无)"}`, + ) + assert.ok( + getIdx < patchIdx, + "GET(解析目标父目录)必须先于 PATCH(更新源项)", + ) + + const getPathname = decodeURIComponent(new URL(calls[getIdx].url).pathname) + assert.equal( + getPathname, + "/v1.0/me/drive/root:/d/bak:", + "GET 应指向 dstPhys 去掉末段后的目标父目录", + ) + + const patchPathname = decodeURIComponent( + new URL(calls[patchIdx].url).pathname, + ) + assert.equal( + patchPathname, + "/v1.0/me/drive/root:/d/a.txt:", + "PATCH 必须指向源项自身路径(srcPhys),即结尾裸冒号", + ) + assert.ok( + !patchPathname.includes("a.txt/a.txt"), + `PATCH 路径不得出现二次拼接的 a.txt/a.txt,实际:${patchPathname}`, + ) + + const body = JSON.parse(calls[patchIdx].body || "{}") + assert.equal( + body.name, + "a.txt", + `PATCH body 的 name 应取 dstPhys 末段,实际:${body.name}`, + ) + assert.equal( + body.parentReference?.id, + "parent-id", + "PATCH body 应携带 GET 解析出的目标父目录 id", + ) +}) diff --git a/src/backend/drivers/onedrive_app/driver.ts b/src/backend/drivers/onedrive_app/driver.ts index 9b9b37f0..db27e8d9 100644 --- a/src/backend/drivers/onedrive_app/driver.ts +++ b/src/backend/drivers/onedrive_app/driver.ts @@ -161,12 +161,10 @@ export class OnedriveAPP implements StorageDriver { physicalPath: string, names: string[], ): Promise { - for (const name of names) { - const itemPath = - physicalPath === "/" ? `/${name}` : `${physicalPath}/${name}` - const url = getMetaUrl(this, false, itemPath) - await requestApi(this, url, "DELETE") - } + // physicalPath 是目标项自身的物理路径(op/storage.ts 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 /,DELETE 404 导致对象仍然存在。 + const url = getMetaUrl(this, false, physicalPath) + await requestApi(this, url, "DELETE") } async move( @@ -176,23 +174,25 @@ export class OnedriveAPP implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstUrl = getMetaUrl(this, false, dstPhys) + // srcPhys/dstPhys 是源/目标项自身的物理路径(已含 name,参数即目标项路径, + // 不得再拼 name,否则指向 /): + // 目标父目录取 dstPhys 去掉末段(直接 GET dstPhys 拿到的是目标项本身,通常还不存在), + // 目标名字以 dstPhys 末段为准,源项路径则直接使用 srcPhys。 + const dstParentPath = dstPhys.split("/").slice(0, -1).join("/") || "/" + const dstUrl = getMetaUrl(this, false, dstParentPath) const dstRes = await requestApi(this, dstUrl, "GET") const dstId = dstRes.id const driveId = dstRes.parentReference?.driveId - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const data = { - parentReference: { - id: dstId, - ...(driveId ? { driveId } : {}), - }, - name, - } - const url = getMetaUrl(this, false, srcItemPath) - await requestApi(this, url, "PATCH", data) + const data = { + parentReference: { + id: dstId, + ...(driveId ? { driveId } : {}), + }, + name: dstPhys.split("/").filter(Boolean).pop() || "", } + const url = getMetaUrl(this, false, srcPhys) + await requestApi(this, url, "PATCH", data) } async copy( @@ -202,23 +202,23 @@ export class OnedriveAPP implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstUrl = getMetaUrl(this, false, dstPhys) + // 与 move 同理:参数是目标项自身路径,不得再拼 name; + // 目标父目录 = dstPhys 去掉末段,目标名字取 dstPhys 末段,源项路径直接使用 srcPhys。 + const dstParentPath = dstPhys.split("/").slice(0, -1).join("/") || "/" + const dstUrl = getMetaUrl(this, false, dstParentPath) const dstRes = await requestApi(this, dstUrl, "GET") const dstId = dstRes.id const driveId = dstRes.parentReference?.driveId - for (const name of names) { - const srcItemPath = srcPhys === "/" ? `/${name}` : `${srcPhys}/${name}` - const data = { - parentReference: { - id: dstId, - ...(driveId ? { driveId } : {}), - }, - name, - } - const url = getMetaUrl(this, false, srcItemPath, "copy") - await requestApi(this, url, "POST", data) + const data = { + parentReference: { + id: dstId, + ...(driveId ? { driveId } : {}), + }, + name: dstPhys.split("/").filter(Boolean).pop() || "", } + const url = getMetaUrl(this, false, srcPhys, "copy") + await requestApi(this, url, "POST", data) } async put( diff --git a/src/backend/drivers/pikpak/driver.ts b/src/backend/drivers/pikpak/driver.ts index 40e09df4..ddd26e8d 100644 --- a/src/backend/drivers/pikpak/driver.ts +++ b/src/backend/drivers/pikpak/driver.ts @@ -257,33 +257,28 @@ export class PikPakDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const parentId = await this.resolveParentId(physicalPath) + // physicalPath 是目标项自身的物理路径(参数即目标项路径,不得再拼 name, + // 否则指向 /);父目录取 physicalPath 去掉末段后的部分, + // 目标名取其末段,在父目录子项中按名查找对应 id。 + const clean = this.cleanPath(physicalPath) + const targetName = clean.split("/").pop() || "" + const parentPath = clean.split("/").slice(0, -1).join("/") + const parentId = await this.resolveParentId(parentPath) const files = await this.getFiles(parentId) - const ids: string[] = [] - - for (const name of names) { - const match = files.find((f) => f.name === name) - if (match) { - ids.push(match.id) - } - } - - if (ids.length === 0) return + const match = files.find((f) => f.name === targetName) + if (!match) return await this.client.request( "https://api-drive.mypikpak.net/drive/v1/files:batchTrash", { method: "POST", body: { - ids, + ids: [match.id], }, }, ) - const clean = this.cleanPath(physicalPath) - for (const name of names) { - this.idCache.delete(clean ? `${clean}/${name}` : `/${name}`) - } + this.idCache.delete(clean) } async move( @@ -293,26 +288,28 @@ export class PikPakDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcParentId = await this.resolveParentId(srcPhys) - const dstParentId = await this.resolveParentId(dstPhys) - const srcFiles = await this.getFiles(srcParentId) - const ids: string[] = [] - - for (const name of names) { - const match = srcFiles.find((f) => f.name === name) - if (match) { - ids.push(match.id) - } - } + // srcPhys/dstPhys 是源/目标项自身的物理路径(参数即目标项路径,不得再拼 name): + // 直接当父目录解析会指到项本身——源侧会去 的子项里找 name(永远落空), + // 目标侧解析一个尚不存在的目标项必然报错。 + // 故两侧父目录均去掉末段,源名取 srcPhys 末段,在源父目录子项中按名查 id。 + const srcClean = this.cleanPath(srcPhys) + const dstClean = this.cleanPath(dstPhys) + const srcName = srcClean.split("/").pop() || "" + const srcParentPath = srcClean.split("/").slice(0, -1).join("/") + const dstParentPath = dstClean.split("/").slice(0, -1).join("/") - if (ids.length === 0) return + const srcParentId = await this.resolveParentId(srcParentPath) + const dstParentId = await this.resolveParentId(dstParentPath) + const srcFiles = await this.getFiles(srcParentId) + const match = srcFiles.find((f) => f.name === srcName) + if (!match) return await this.client.request( "https://api-drive.mypikpak.net/drive/v1/files:batchMove", { method: "POST", body: { - ids, + ids: [match.id], to: { parent_id: dstParentId, }, @@ -320,10 +317,7 @@ export class PikPakDriver implements StorageDriver { }, ) - const srcClean = this.cleanPath(srcPhys) - for (const name of names) { - this.idCache.delete(srcClean ? `${srcClean}/${name}` : `/${name}`) - } + this.idCache.delete(srcClean) } async copy( @@ -333,26 +327,26 @@ export class PikPakDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcParentId = await this.resolveParentId(srcPhys) - const dstParentId = await this.resolveParentId(dstPhys) - const srcFiles = await this.getFiles(srcParentId) - const ids: string[] = [] - - for (const name of names) { - const match = srcFiles.find((f) => f.name === name) - if (match) { - ids.push(match.id) - } - } + // 与 move 同理:参数是源/目标项自身路径,不得再拼 name; + // 两侧父目录均去掉末段,源名取 srcPhys 末段,在源父目录子项中按名查 id。 + const srcClean = this.cleanPath(srcPhys) + const dstClean = this.cleanPath(dstPhys) + const srcName = srcClean.split("/").pop() || "" + const srcParentPath = srcClean.split("/").slice(0, -1).join("/") + const dstParentPath = dstClean.split("/").slice(0, -1).join("/") - if (ids.length === 0) return + const srcParentId = await this.resolveParentId(srcParentPath) + const dstParentId = await this.resolveParentId(dstParentPath) + const srcFiles = await this.getFiles(srcParentId) + const match = srcFiles.find((f) => f.name === srcName) + if (!match) return await this.client.request( "https://api-drive.mypikpak.net/drive/v1/files:batchCopy", { method: "POST", body: { - ids, + ids: [match.id], to: { parent_id: dstParentId, }, diff --git a/src/backend/drivers/s3/driver.ts b/src/backend/drivers/s3/driver.ts index b2eb6304..b6afeb40 100644 --- a/src/backend/drivers/s3/driver.ts +++ b/src/backend/drivers/s3/driver.ts @@ -237,20 +237,18 @@ export class S3Driver implements StorageDriver { dstPhys: string, ): Promise { await this.checkDogeToken() - const srcBase = this.getRemotePath(srcPhys) - const dstBase = this.getRemotePath(dstPhys) - - for (const name of names) { - const srcPath = joinPath(srcBase, name) - const dstPath = joinPath(dstBase, name) - const head = await this.client.headObject(srcPath) - if (head) { - await this.client.copyObject(srcPath, dstPath, head.size) - await this.client.deleteObject(srcPath) - } else { - await this.copyDirRecursive(srcPath, dstPath) - await this.removeDirRecursive(srcPath) - } + // srcPhys/dstPhys 是源/目标项自身的物理路径(参数即目标项路径,不得再拼 name, + // 否则指向 / 导致静默失败)。 + const srcPath = this.getRemotePath(srcPhys) + const dstPath = this.getRemotePath(dstPhys) + + const head = await this.client.headObject(srcPath) + if (head) { + await this.client.copyObject(srcPath, dstPath, head.size) + await this.client.deleteObject(srcPath) + } else { + await this.copyDirRecursive(srcPath, dstPath) + await this.removeDirRecursive(srcPath) } } @@ -262,18 +260,15 @@ export class S3Driver implements StorageDriver { dstPhys: string, ): Promise { await this.checkDogeToken() - const srcBase = this.getRemotePath(srcPhys) - const dstBase = this.getRemotePath(dstPhys) - - for (const name of names) { - const srcPath = joinPath(srcBase, name) - const dstPath = joinPath(dstBase, name) - const head = await this.client.headObject(srcPath) - if (head) { - await this.client.copyObject(srcPath, dstPath, head.size) - } else { - await this.copyDirRecursive(srcPath, dstPath) - } + // 同 move:srcPhys/dstPhys 已是目标项自身路径,不得再拼 name。 + const srcPath = this.getRemotePath(srcPhys) + const dstPath = this.getRemotePath(dstPhys) + + const head = await this.client.headObject(srcPath) + if (head) { + await this.client.copyObject(srcPath, dstPath, head.size) + } else { + await this.copyDirRecursive(srcPath, dstPath) } } @@ -297,25 +292,16 @@ export class S3Driver implements StorageDriver { names: string[], ): Promise { await this.checkDogeToken() - const basePath = this.getRemotePath(physicalPath) - - if (names && names.length > 0) { - for (const name of names) { - const targetPath = joinPath(basePath, name) - const head = await this.client.headObject(targetPath) - if (head) { - await this.client.deleteObject(targetPath) - } else { - await this.removeDirRecursive(targetPath) - } - } + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 参数即目标项路径,直接删除它即可;若再拼一次 name 会指向 `/`, + // HEAD 404 后退化成按目录递归删除,prefix 同样查不到对象,最终静默返回成功但对象仍在。 + const targetPath = this.getRemotePath(physicalPath) + + const head = await this.client.headObject(targetPath) + if (head) { + await this.client.deleteObject(targetPath) } else { - const head = await this.client.headObject(basePath) - if (head) { - await this.client.deleteObject(basePath) - } else { - await this.removeDirRecursive(basePath) - } + await this.removeDirRecursive(targetPath) } } diff --git a/src/backend/drivers/s3/s3_remove.test.ts b/src/backend/drivers/s3/s3_remove.test.ts new file mode 100644 index 00000000..12cb84ef --- /dev/null +++ b/src/backend/drivers/s3/s3_remove.test.ts @@ -0,0 +1,127 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { S3Driver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems/copyItems): + * + * const resolved = await resolvePath(`${dir}/${name}`) + * await driver.remove(virtualPath, resolved.physical, [name]) + * + * 即 physicalPath 是【目标项自身】的物理路径(文件路径),与 123pan/github/ + * azure_blob 等 driver 的用法一致。driver 不得再把 name 拼接到 physicalPath 后面, + * 否则会得到 `/` 这种不存在的 key:HEAD 404 → 退化成按目录递归删 → + * prefix 同样查不到任何对象 → 静默返回成功(接口报 success,对象仍在)。 + */ + +type Call = { method: string; url: string } + +/** 内存版 S3:object key 存 URL pathname,记录所有请求以便断言目标 key */ +function mockS3(objects: Set, calls: Call[]) { + return async (input: any, init: any = {}) => { + const url = new URL(String(typeof input === "string" ? input : input.url)) + const method = String(init.method || "GET").toUpperCase() + calls.push({ method, url: url.toString() }) + + if (method === "HEAD") { + if (objects.has(url.pathname)) { + return new Response(null, { + status: 200, + headers: { + "content-length": "10", + etag: '"etag"', + "last-modified": "Wed, 01 Jan 2025 00:00:00 GMT", + }, + }) + } + return new Response(null, { status: 404 }) + } + if (method === "DELETE") { + objects.delete(url.pathname) + return new Response(null, { status: 204 }) + } + return new Response( + `false`, + { status: 200, headers: { "content-type": "application/xml" } }, + ) + } +} + +function withMock( + objects: Set, + calls: Call[], + fn: () => Promise, +): Promise { + const original = globalThis.fetch + globalThis.fetch = mockS3(objects, calls) as any + return fn().finally(() => { + globalThis.fetch = original + }) +} + +function makeDriver() { + return new S3Driver({ + bucket: "mybucket", + endpoint: "https://s3.amazonaws.com", + access_key_id: "key", + secret_access_key: "secret", + }) +} + +const FILE_KEY = "/Cloudflare%20R2/AzkiDeck.html" + +test("S3 remove() deletes the object key itself, not /", async () => { + const objects = new Set([FILE_KEY]) + const calls: Call[] = [] + + await withMock(objects, calls, () => + makeDriver().remove( + "/Cloudflare R2/AzkiDeck.html", + "/Cloudflare R2/AzkiDeck.html", + ["AzkiDeck.html"], + ), + ) + + assert.equal( + objects.size, + 0, + "object must be deleted (interface reported success)", + ) + const deletes = calls + .filter((c) => c.method === "DELETE") + .map((c) => new URL(c.url).pathname) + assert.ok( + deletes.includes(FILE_KEY), + `DELETE must target ${FILE_KEY}, got: ${deletes.join(", ") || "(none)"}`, + ) +}) + +test("S3 move() copies from / to the object key itself", async () => { + const objects = new Set([FILE_KEY]) + const calls: Call[] = [] + + await withMock(objects, calls, () => + makeDriver().move( + "/Cloudflare R2", + "/Cloudflare R2/bak", + ["AzkiDeck.html"], + "/Cloudflare R2/AzkiDeck.html", + "/Cloudflare R2/bak/AzkiDeck.html", + ), + ) + + const deletes = calls + .filter((c) => c.method === "DELETE") + .map((c) => new URL(c.url).pathname) + const puts = calls + .filter((c) => c.method === "PUT") + .map((c) => new URL(c.url).pathname) + assert.ok( + deletes.includes(FILE_KEY), + `move must delete source key ${FILE_KEY}, got: ${deletes.join(", ") || "(none)"}`, + ) + assert.ok( + puts.includes("/Cloudflare%20R2/bak/AzkiDeck.html"), + `move must write destination key, got: ${puts.join(", ") || "(none)"}`, + ) +}) diff --git a/src/backend/drivers/seafile/driver.ts b/src/backend/drivers/seafile/driver.ts index 7a432991..d17e73e4 100644 --- a/src/backend/drivers/seafile/driver.ts +++ b/src/backend/drivers/seafile/driver.ts @@ -264,17 +264,18 @@ export class SeafileDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // resolveRepoAndPath 已解析到目标项,参数即目标项路径,不得再拼 name,否则指向 + // `/`,DELETE 到不存在的路径,接口成功但文件仍在。 const { repoId, innerPath } = await this.resolveRepoAndPath(physicalPath) - for (const name of names) { - const targetPath = innerPath === "/" ? `/${name}` : `${innerPath}/${name}` - await this.client.request( - `/api2/repos/${encodeURIComponent(repoId)}/file/`, - { - method: "DELETE", - params: { p: targetPath }, - }, - ) - } + + await this.client.request( + `/api2/repos/${encodeURIComponent(repoId)}/file/`, + { + method: "DELETE", + params: { p: innerPath }, + }, + ) } async move( @@ -284,26 +285,27 @@ export class SeafileDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { + // srcPhys/dstPhys 是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 `/`; + // 源路径直接用 src.innerPath,dst_dir 需要目录,取 dst.innerPath 的父级。 const src = await this.resolveRepoAndPath(srcPhys) const dst = await this.resolveRepoAndPath(dstPhys) + const dstDirPath = + dst.innerPath.substring(0, dst.innerPath.lastIndexOf("/")) || "/" - for (const name of names) { - const targetPath = - src.innerPath === "/" ? `/${name}` : `${src.innerPath}/${name}` - await this.client.request( - `/api2/repos/${encodeURIComponent(src.repoId)}/file/`, - { - method: "POST", - isFormData: true, - params: { p: targetPath }, - body: { - operation: "move", - dst_repo: dst.repoId, - dst_dir: dst.innerPath, - }, + await this.client.request( + `/api2/repos/${encodeURIComponent(src.repoId)}/file/`, + { + method: "POST", + isFormData: true, + params: { p: src.innerPath }, + body: { + operation: "move", + dst_repo: dst.repoId, + dst_dir: dstDirPath, }, - ) - } + }, + ) } async copy( @@ -313,26 +315,27 @@ export class SeafileDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { + // srcPhys/dstPhys 是源/目标项自身的物理路径(op/storage.ts copyItems 逐项调用), + // 参数即目标项路径,不得再拼 name,否则指向 `/`; + // 源路径直接用 src.innerPath,dst_dir 需要目录,取 dst.innerPath 的父级。 const src = await this.resolveRepoAndPath(srcPhys) const dst = await this.resolveRepoAndPath(dstPhys) + const dstDirPath = + dst.innerPath.substring(0, dst.innerPath.lastIndexOf("/")) || "/" - for (const name of names) { - const targetPath = - src.innerPath === "/" ? `/${name}` : `${src.innerPath}/${name}` - await this.client.request( - `/api2/repos/${encodeURIComponent(src.repoId)}/file/`, - { - method: "POST", - isFormData: true, - params: { p: targetPath }, - body: { - operation: "copy", - dst_repo: dst.repoId, - dst_dir: dst.innerPath, - }, + await this.client.request( + `/api2/repos/${encodeURIComponent(src.repoId)}/file/`, + { + method: "POST", + isFormData: true, + params: { p: src.innerPath }, + body: { + operation: "copy", + dst_repo: dst.repoId, + dst_dir: dstDirPath, }, - ) - } + }, + ) } async put( diff --git a/src/backend/drivers/sftp/driver.ts b/src/backend/drivers/sftp/driver.ts index 7eefe8db..acb840f8 100644 --- a/src/backend/drivers/sftp/driver.ts +++ b/src/backend/drivers/sftp/driver.ts @@ -192,34 +192,21 @@ export class SFTPDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const targetDir = cleanPosixPath(physicalPath) - if (names && names.length > 0) { - for (const name of names) { - await this.client.removeRecursive(posixJoin(targetDir, name)) - } - } else { - await this.client.removeRecursive(targetDir) - } + // physicalPath 已是目标项自身的物理路径(op/storage.ts 逐项解析后传入), + // 参数即项路径,不得再拼 name,否则指向 / 导致静默删除失败。 + await this.client.removeRecursive(cleanPosixPath(physicalPath)) } async move( _srcDir: string, - dstDir: string, + _dstDir: string, names: string[], srcPhys: string, dstPhys: string, ): Promise { - if (names && names.length > 0) { - for (const name of names) { - const src = posixJoin(srcPhys, name) - const dst = posixJoin(dstPhys, name) - await this.client.rename(src, dst) - } - } else { - const filename = srcPhys.split("/").filter(Boolean).pop() || "" - const dst = posixJoin(dstDir, filename) - await this.client.rename(cleanPosixPath(srcPhys), dst) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径,参数即项路径, + // 不得再拼 name,否则源/目标会错位到 /。 + await this.client.rename(cleanPosixPath(srcPhys), cleanPosixPath(dstPhys)) } async copy( diff --git a/src/backend/drivers/sftp/sftp_remove.test.ts b/src/backend/drivers/sftp/sftp_remove.test.ts new file mode 100644 index 00000000..7777356c --- /dev/null +++ b/src/backend/drivers/sftp/sftp_remove.test.ts @@ -0,0 +1,81 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { SFTPDriver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * await driver.remove(virtualPath, resolved.physical, [name]) + * await driver.move(srcDir, dstDir, [name], srcPhys, dstPhys) + * + * 即 physicalPath / srcPhys / dstPhys 都是【目标项自身】的物理路径(已含文件名), + * driver 不得再把 name 拼接到路径后面,否则会指向 `/`, + * lstat 查不到 → removeRecursive 静默返回,接口报成功但文件仍在。 + * + * Node v24 下 mock.module 不可用(需实验标志),且不调 init()(避免真连 SSH): + * 注入点是 SFTPClientWrapper.getSFTP() 首行的 sftpClient 缓存(见 sftp/util.ts), + * 把 fake sftp 对象挂上去,removeRecursive/rename 走的回调式方法全部命中 fake,不触网。 + * 只影响本用例新建的 driver 实例,不污染全局。 + */ + +function makeDriverWithFakeSFTP(fake: any): SFTPDriver { + const driver = new SFTPDriver({ + address: "127.0.0.1:22", + username: "root", + password: "x", + }) + ;(driver as any).client["sftpClient"] = fake + return driver +} + +test("SFTP remove() 对项自身路径调用 lstat/unlink,不拼接 name", async () => { + const calls: string[] = [] + const driver = makeDriverWithFakeSFTP({ + lstat: (p: string, cb: any) => { + calls.push(`lstat ${p}`) + cb(null, { isDirectory: () => false }) + }, + readdir: (p: string, cb: any) => { + calls.push(`readdir ${p}`) + cb(null, []) + }, + unlink: (p: string, cb: any) => { + calls.push(`unlink ${p}`) + cb(null) + }, + rmdir: (p: string, cb: any) => { + calls.push(`rmdir ${p}`) + cb(null) + }, + }) + + await driver.remove("/d/a.txt", "/d/a.txt", ["a.txt"]) + + assert.deepEqual( + calls, + ["lstat /d/a.txt", "unlink /d/a.txt"], + "remove 应直接作用于项自身路径(fake 必须被调用,否则注入未生效)", + ) + assert.ok( + !calls.some((c) => c.includes("a.txt/a.txt")), + `路径不得出现二次拼接的 a.txt/a.txt,实际:${calls.join(", ")}`, + ) +}) + +test("SFTP move() 把源/目标项自身路径传给 rename", async () => { + const calls: string[] = [] + const driver = makeDriverWithFakeSFTP({ + rename: (a: string, b: string, cb: any) => { + calls.push(`rename ${a} -> ${b}`) + cb(null) + }, + }) + + await driver.move("/d", "/d/bak", ["a.txt"], "/d/a.txt", "/d/bak/a.txt") + + assert.deepEqual( + calls, + ["rename /d/a.txt -> /d/bak/a.txt"], + "rename 的两个参数必须是源/目标项自身路径(fake 必须被调用,否则注入未生效)", + ) +}) diff --git a/src/backend/drivers/terabox/driver.ts b/src/backend/drivers/terabox/driver.ts index 0f1cef51..e6a4fe3c 100644 --- a/src/backend/drivers/terabox/driver.ts +++ b/src/backend/drivers/terabox/driver.ts @@ -200,9 +200,10 @@ export class TeraboxDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 参数即项路径,直接删除它即可;不得再拼 name,否则会指向 / 导致静默失败。 const clean = this.cleanPath(physicalPath) - const paths = names.map((n) => (clean === "/" ? `/${n}` : `${clean}/${n}`)) - await this.client.manage("delete", paths) + await this.client.manage("delete", [clean]) } async move( @@ -214,14 +215,15 @@ export class TeraboxDriver implements StorageDriver { ): Promise { const srcClean = this.cleanPath(srcPhys) const dstClean = this.cleanPath(dstPhys) - - const fileList = names.map((name) => ({ - path: srcClean === "/" ? `/${name}` : `${srcClean}/${name}`, - dest: dstClean, - newname: name, - })) - - await this.client.manage("move", fileList) + // srcPhys/dstPhys 是目标项自身的路径:path 不得再拼 name; + // filemanager 的 dest 是目标目录,故取 dstPhys 去掉末段后的父目录,newname 取末段。 + await this.client.manage("move", [ + { + path: srcClean, + dest: dstClean.split("/").slice(0, -1).join("/") || "/", + newname: dstClean.split("/").pop() || "", + }, + ]) } async copy( @@ -233,14 +235,15 @@ export class TeraboxDriver implements StorageDriver { ): Promise { const srcClean = this.cleanPath(srcPhys) const dstClean = this.cleanPath(dstPhys) - - const fileList = names.map((name) => ({ - path: srcClean === "/" ? `/${name}` : `${srcClean}/${name}`, - dest: dstClean, - newname: name, - })) - - await this.client.manage("copy", fileList) + // 同 move:srcPhys/dstPhys 已是目标项自身路径,path 不得再拼 name; + // dest 取 dstPhys 去掉末段后的父目录,newname 取末段。 + await this.client.manage("copy", [ + { + path: srcClean, + dest: dstClean.split("/").slice(0, -1).join("/") || "/", + newname: dstClean.split("/").pop() || "", + }, + ]) } async put( diff --git a/src/backend/drivers/uss/driver.ts b/src/backend/drivers/uss/driver.ts index c12f4f9c..914ecf04 100644 --- a/src/backend/drivers/uss/driver.ts +++ b/src/backend/drivers/uss/driver.ts @@ -180,11 +180,9 @@ export class UssDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - for (const name of names) { - const srcKey = joinPath(this.getKey(srcPhys), name) - const dstKey = joinPath(this.getKey(dstPhys), name) - await this.moveOrCopy(srcKey, dstKey, "move") - } + // srcPhys/dstPhys 是目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即项路径,不得再拼 name,否则会指向 / 导致移动失败。 + await this.moveOrCopy(this.getKey(srcPhys), this.getKey(dstPhys), "move") } async copy( @@ -194,11 +192,8 @@ export class UssDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - for (const name of names) { - const srcKey = joinPath(this.getKey(srcPhys), name) - const dstKey = joinPath(this.getKey(dstPhys), name) - await this.moveOrCopy(srcKey, dstKey, "copy") - } + // 同 move:srcPhys/dstPhys 已是目标项自身路径,参数即项路径,不得再拼 name。 + await this.moveOrCopy(this.getKey(srcPhys), this.getKey(dstPhys), "copy") } async put(_v: string, physicalPath: string, content: Buffer): Promise { diff --git a/src/backend/drivers/webdav/driver.ts b/src/backend/drivers/webdav/driver.ts index 3add883f..f4d98438 100644 --- a/src/backend/drivers/webdav/driver.ts +++ b/src/backend/drivers/webdav/driver.ts @@ -105,13 +105,13 @@ export class WebdavDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcBasePath = this.getRemotePath(srcPhys) - const dstBasePath = this.getRemotePath(dstPhys) - for (const name of names) { - const srcPath = joinPath(srcBasePath, name) - const dstPath = joinPath(dstBasePath, name) - await this.client.move(srcPath, dstPath, true) - } + // srcPhys/dstPhys 已是源/目标项自身的路径,参数即项路径,直接移动即可; + // 再拼一次 name 会指向不存在的 /,MOVE 源/目标错位。 + await this.client.move( + this.getRemotePath(srcPhys), + this.getRemotePath(dstPhys), + true, + ) } async copy( @@ -121,13 +121,13 @@ export class WebdavDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcBasePath = this.getRemotePath(srcPhys) - const dstBasePath = this.getRemotePath(dstPhys) - for (const name of names) { - const srcPath = joinPath(srcBasePath, name) - const dstPath = joinPath(dstBasePath, name) - await this.client.copy(srcPath, dstPath, true) - } + // srcPhys/dstPhys 已是源/目标项自身的路径,参数即项路径,直接复制即可; + // 再拼一次 name 会让 COPY 源/目标路径错位。 + await this.client.copy( + this.getRemotePath(srcPhys), + this.getRemotePath(dstPhys), + true, + ) } async remove( @@ -135,15 +135,10 @@ export class WebdavDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const basePath = this.getRemotePath(physicalPath) - if (names && names.length > 0) { - for (const name of names) { - const targetPath = joinPath(basePath, name) - await this.client.remove(targetPath) - } - } else { - await this.client.remove(basePath) - } + // physicalPath 已是目标项自身的路径(op/storage.ts 逐项解析后传入), + // 参数即项路径,不得再拼 name,否则指向 /,WebDAV DELETE 对 404 视作成功, + // 结果接口静默返回成功但对象仍在。 + await this.client.remove(this.getRemotePath(physicalPath)) } async put( diff --git a/src/backend/drivers/webdav/webdav_remove.test.ts b/src/backend/drivers/webdav/webdav_remove.test.ts new file mode 100644 index 00000000..6e4876d5 --- /dev/null +++ b/src/backend/drivers/webdav/webdav_remove.test.ts @@ -0,0 +1,183 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { WebdavDriver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * const resolved = await resolvePath(`${dir}/${name}`) + * await driver.remove(virtualPath, resolved.physical, [name]) + * + * physicalPath/srcPhys/dstPhys 是【目标项自身】的物理路径(已含文件名), + * 调用方恒传单元素 names。driver 不得再把 name 拼到该路径后面,否则 + * DELETE 打到 `/`(WebDAV 对 404 视作成功 → 接口报成功但对象仍在), + * MOVE 的请求源与 Destination 头源/目标也会错位。 + * + * 状态码坑:DELETE 返回 204(实现接受 200/204/404);MOVE 返回 201(绝不能 409, + * 409 会触发 mkdirAll 重试链,产生额外请求干扰断言)。 + */ + +type Call = { method: string; url: string; headers: Headers } + +/** 记录请求;DELETE 回 204、MOVE 回 201,其余回 200 */ +function mockWebdav(calls: Call[]) { + return async (input: any, init: any = {}) => { + const url = String(typeof input === "string" ? input : input.url) + const method = String(init.method || "GET").toUpperCase() + calls.push({ method, url, headers: new Headers(init.headers || {}) }) + if (method === "DELETE") { + return new Response(null, { status: 204 }) + } + if (method === "MOVE") { + return new Response(null, { status: 201 }) + } + return new Response(null, { status: 200 }) + } +} + +function withMock(calls: Call[], fn: () => Promise): Promise { + const original = globalThis.fetch + globalThis.fetch = mockWebdav(calls) as any + return fn().finally(() => { + globalThis.fetch = original + }) +} + +function makeDriver(rootFolderPath?: string) { + // 构造不联网,测试不调用 init();root_folder_path 缺省为 "/" + return new WebdavDriver({ + address: "https://dav.example.com", + username: "u", + password: "p", + ...(rootFolderPath ? { root_folder_path: rootFolderPath } : {}), + }) +} + +test("Webdav remove() DELETE 目标为项自身路径,不再拼一次 name", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().remove("/d/a.txt", "/d/a.txt", ["a.txt"]), + ) + + const deletes = calls.filter((c) => c.method === "DELETE") + assert.equal( + deletes.length, + 1, + `应恰好发出 1 次 DELETE 请求,实际 ${deletes.length}`, + ) + const pathname = decodeURIComponent(new URL(deletes[0].url).pathname) + assert.equal( + pathname, + "/d/a.txt", + `DELETE 目标应为项自身路径 /d/a.txt,实际 ${pathname}`, + ) + assert.ok( + !pathname.includes("a.txt/a.txt"), + "DELETE 目标不得出现二次拼接的 /", + ) +}) + +test("Webdav move() 请求源与 Destination 头分别为源/目标项自身路径", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().move("/d", "/d/dst", ["a.txt"], "/d/a.txt", "/d/dst/a.txt"), + ) + + const moves = calls.filter((c) => c.method === "MOVE") + assert.equal( + moves.length, + 1, + `应恰好发出 1 次 MOVE 请求,实际 ${moves.length}`, + ) + const pathname = decodeURIComponent(new URL(moves[0].url).pathname) + assert.equal( + pathname, + "/d/a.txt", + `MOVE 请求 URL 应为源项自身路径 /d/a.txt,实际 ${pathname}`, + ) + assert.ok( + !pathname.includes("a.txt/a.txt"), + "MOVE 请求 URL 不得出现二次拼接的 /", + ) + // Headers 不区分大小写,get("destination") 与写入时的 Destination 等价 + const destination = moves[0].headers.get("destination") + assert.ok(destination, "MOVE 必须携带 Destination 头") + const destPathname = decodeURIComponent(new URL(destination).pathname) + assert.equal( + destPathname, + "/d/dst/a.txt", + `Destination 应指向目标项自身路径 /d/dst/a.txt,实际 ${destination}`, + ) + assert.ok( + !destPathname.includes("a.txt/a.txt"), + "Destination 不得出现二次拼接的 /", + ) + assert.equal( + calls.filter((c) => c.method === "MKCOL").length, + 0, + "MOVE 返回 201 不应触发 mkdirAll 重试链", + ) +}) + +test("Webdav root_folder_path 前缀只拼一次(remove 与 move)", async () => { + const removeCalls: Call[] = [] + + await withMock(removeCalls, () => + makeDriver("/root").remove("/d/a.txt", "/d/a.txt", ["a.txt"]), + ) + const removeDeletes = removeCalls.filter((c) => c.method === "DELETE") + assert.equal( + removeDeletes.length, + 1, + `应恰好发出 1 次 DELETE 请求,实际 ${removeDeletes.length}`, + ) + const removePath = decodeURIComponent(new URL(removeDeletes[0].url).pathname) + assert.equal( + removePath, + "/root/d/a.txt", + `DELETE 目标应为 /<项路径> = /root/d/a.txt,实际 ${removePath}`, + ) + assert.ok(!removePath.includes("/root/root/"), "root 前缀不得被拼两次") + assert.ok( + !removePath.includes("a.txt/a.txt"), + "DELETE 目标不得出现二次拼接的 /", + ) + + const moveCalls: Call[] = [] + + await withMock(moveCalls, () => + makeDriver("/root").move( + "/d", + "/d/dst", + ["a.txt"], + "/d/a.txt", + "/d/dst/a.txt", + ), + ) + const rootMoves = moveCalls.filter((c) => c.method === "MOVE") + assert.equal( + rootMoves.length, + 1, + `应恰好发出 1 次 MOVE 请求,实际 ${rootMoves.length}`, + ) + const srcPath = decodeURIComponent(new URL(rootMoves[0].url).pathname) + assert.equal( + srcPath, + "/root/d/a.txt", + `MOVE 请求 URL 应为 /<源项路径> = /root/d/a.txt,实际 ${srcPath}`, + ) + const destination = rootMoves[0].headers.get("destination") + assert.ok(destination, "MOVE 必须携带 Destination 头") + const destPath = decodeURIComponent(new URL(destination).pathname) + assert.equal( + destPath, + "/root/d/dst/a.txt", + `Destination 应为 /<目标项路径> = /root/d/dst/a.txt,实际 ${destPath}`, + ) + assert.ok( + !srcPath.includes("/root/root/") && !destPath.includes("/root/root/"), + "root 前缀不得被拼两次", + ) +}) diff --git a/src/backend/drivers/wps/driver.ts b/src/backend/drivers/wps/driver.ts index 29dd1619..0ad78367 100644 --- a/src/backend/drivers/wps/driver.ts +++ b/src/backend/drivers/wps/driver.ts @@ -217,25 +217,24 @@ export class WpsDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstNode = await this.resolvePath(this.cleanPath(dstPhys)) + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即项路径:源直接用 srcPath,目标文件夹取 dstPath 去掉末段的父目录; + // 不得再拼 name,否则会指向 /。 + const srcPath = this.cleanPath(srcPhys) + const dstPath = this.cleanPath(dstPhys) + const dstParentPath = dstPath.substring(0, dstPath.lastIndexOf("/")) || "/" + const dstNode = await this.resolvePath(dstParentPath) if (!dstNode || !dstNode.isDir || dstNode.kind === "root") { throw new Error("Target destination directory not found") } - - for (const name of names) { - const srcItemPath = - this.cleanPath(srcPhys) === "/" - ? `/${name}` - : `${this.cleanPath(srcPhys)}/${name}` - const srcNode = await this.resolvePath(srcItemPath) - if (srcNode) { - await this.client.move( - srcNode.groupId, - srcNode.fileId, - dstNode.groupId, - dstNode.fileId, - ) - } + const srcNode = await this.resolvePath(srcPath) + if (srcNode) { + await this.client.move( + srcNode.groupId, + srcNode.fileId, + dstNode.groupId, + dstNode.fileId, + ) } } @@ -259,25 +258,24 @@ export class WpsDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const dstNode = await this.resolvePath(this.cleanPath(dstPhys)) + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts copyItems 逐项调用), + // 参数即项路径:源直接用 srcPath,目标文件夹取 dstPath 去掉末段的父目录; + // 不得再拼 name,否则会指向 /。 + const srcPath = this.cleanPath(srcPhys) + const dstPath = this.cleanPath(dstPhys) + const dstParentPath = dstPath.substring(0, dstPath.lastIndexOf("/")) || "/" + const dstNode = await this.resolvePath(dstParentPath) if (!dstNode || !dstNode.isDir || dstNode.kind === "root") { throw new Error("Target destination directory not found") } - - for (const name of names) { - const srcItemPath = - this.cleanPath(srcPhys) === "/" - ? `/${name}` - : `${this.cleanPath(srcPhys)}/${name}` - const srcNode = await this.resolvePath(srcItemPath) - if (srcNode) { - await this.client.copy( - srcNode.groupId, - srcNode.fileId, - dstNode.groupId, - dstNode.fileId, - ) - } + const srcNode = await this.resolvePath(srcPath) + if (srcNode) { + await this.client.copy( + srcNode.groupId, + srcNode.fileId, + dstNode.groupId, + dstNode.fileId, + ) } } @@ -286,15 +284,12 @@ export class WpsDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - for (const name of names) { - const itemPath = - this.cleanPath(physicalPath) === "/" - ? `/${name}` - : `${this.cleanPath(physicalPath)}/${name}` - const node = await this.resolvePath(itemPath) - if (node && node.kind !== "root" && node.kind !== "group") { - await this.client.delete(node.groupId, node.fileId) - } + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 参数即项路径,直接解析它即可;再拼 name 会指向 /,resolvePath 返回 null + // 后被静默跳过,接口成功但文件仍在。 + const node = await this.resolvePath(this.cleanPath(physicalPath)) + if (node && node.kind !== "root" && node.kind !== "group") { + await this.client.delete(node.groupId, node.fileId) } } diff --git a/src/backend/drivers/yandex/driver.ts b/src/backend/drivers/yandex/driver.ts index 37f5abcd..74685710 100644 --- a/src/backend/drivers/yandex/driver.ts +++ b/src/backend/drivers/yandex/driver.ts @@ -136,14 +136,12 @@ export class YandexDriver implements StorageDriver { physicalPath: string, names: string[], ): Promise { - const clean = this.cleanPath(physicalPath) - for (const name of names) { - const targetPath = clean === "/" ? `/${name}` : `${clean}/${name}` - await this.client.request("", { - method: "DELETE", - params: { path: targetPath }, - }) - } + // physicalPath 是目标项自身的物理路径(op/storage.ts removeItems 逐项调用), + // 参数即项路径,直接删除它即可;再拼 name 会指向 /,DELETE 404 报错。 + await this.client.request("", { + method: "DELETE", + params: { path: this.cleanPath(physicalPath) }, + }) } async move( @@ -153,21 +151,16 @@ export class YandexDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcClean = this.cleanPath(srcPhys) - const dstClean = this.cleanPath(dstPhys) - - for (const name of names) { - const fromPath = srcClean === "/" ? `/${name}` : `${srcClean}/${name}` - const toPath = dstClean === "/" ? `/${name}` : `${dstClean}/${name}` - await this.client.request("/move", { - method: "POST", - params: { - from: fromPath, - path: toPath, - overwrite: "true", - }, - }) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts moveItems 逐项调用), + // 参数即项路径,直接作为 from/path 使用;再拼 name 会指向 /,源/目标错位。 + await this.client.request("/move", { + method: "POST", + params: { + from: this.cleanPath(srcPhys), + path: this.cleanPath(dstPhys), + overwrite: "true", + }, + }) } async copy( @@ -177,21 +170,16 @@ export class YandexDriver implements StorageDriver { srcPhys: string, dstPhys: string, ): Promise { - const srcClean = this.cleanPath(srcPhys) - const dstClean = this.cleanPath(dstPhys) - - for (const name of names) { - const fromPath = srcClean === "/" ? `/${name}` : `${srcClean}/${name}` - const toPath = dstClean === "/" ? `/${name}` : `${dstClean}/${name}` - await this.client.request("/copy", { - method: "POST", - params: { - from: fromPath, - path: toPath, - overwrite: "true", - }, - }) - } + // srcPhys/dstPhys 已是源/目标项自身的物理路径(op/storage.ts copyItems 逐项调用), + // 参数即项路径,直接作为 from/path 使用;再拼 name 会指向 /,源/目标错位。 + await this.client.request("/copy", { + method: "POST", + params: { + from: this.cleanPath(srcPhys), + path: this.cleanPath(dstPhys), + overwrite: "true", + }, + }) } async put( diff --git a/src/backend/drivers/yandex/yandex_remove.test.ts b/src/backend/drivers/yandex/yandex_remove.test.ts new file mode 100644 index 00000000..12d2ce1c --- /dev/null +++ b/src/backend/drivers/yandex/yandex_remove.test.ts @@ -0,0 +1,132 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { YandexDriver } from "./driver" + +/** + * 调用约定(src/backend/internal/op/storage.ts 的 removeItems/moveItems): + * + * const resolved = await resolvePath(`${dir}/${name}`) + * await driver.remove(virtualPath, resolved.physical, [name]) + * + * physicalPath/srcPhys/dstPhys 是【目标项自身】的物理路径(已含文件名), + * 调用方恒传单元素 names。driver 不得再把 name 拼到该路径后面,否则 + * `?path=/` 指向不存在的对象 → DELETE 404 报错, + * move 的 from/path 源/目标错位。 + * + * 注意:路径在 query 参数里(不在 pathname),断言必须读 searchParams。 + */ + +type Call = { method: string; url: string } + +/** 记录请求,恒返回 2xx + application/json: + * 401 会触发 refreshToken() 联网,content-type 决定 util 的解析分支 */ +function mockYandex(calls: Call[]) { + return async (input: any, init: any = {}) => { + const url = String(typeof input === "string" ? input : input.url) + calls.push({ + method: String(init.method || "GET").toUpperCase(), + url, + }) + return new Response("{}", { + status: 200, + headers: { "content-type": "application/json" }, + }) + } +} + +function withMock(calls: Call[], fn: () => Promise): Promise { + const original = globalThis.fetch + globalThis.fetch = mockYandex(calls) as any + return fn().finally(() => { + globalThis.fetch = original + }) +} + +function makeDriver() { + // 测试不调用 init(),client 的 accessToken 为空;只要 mock 返回 2xx 就不会 + // 触发 401 → refreshToken() 联网。 + return new YandexDriver({ + refresh_token: "r", + use_online_api: true, + }) +} + +test("Yandex remove() 的 path query 为项自身路径,不再拼一次 name", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().remove("/d/a.txt", "/d/a.txt", ["a.txt"]), + ) + + const deletes = calls.filter((c) => c.method === "DELETE") + assert.equal( + deletes.length, + 1, + `应恰好发出 1 次 DELETE 请求,实际 ${deletes.length}`, + ) + const url = new URL(deletes[0].url) + assert.equal( + url.origin + url.pathname, + "https://cloud-api.yandex.net/v1/disk/resources", + `DELETE 应打到 /v1/disk/resources,实际 ${url.origin}${url.pathname}`, + ) + const path = url.searchParams.get("path") + assert.equal( + path, + "/d/a.txt", + `path query 应为项自身路径 /d/a.txt,实际 ${path}`, + ) + assert.ok( + !String(path).endsWith("/a.txt/a.txt"), + "path query 不得出现二次拼接的 /", + ) + assert.ok( + calls.every((c) => !c.url.includes("api.oplist.org")), + "不应触发在线 renewapi 的 token 刷新请求", + ) +}) + +test("Yandex move() 的 from/path query 分别为源/目标项自身路径", async () => { + const calls: Call[] = [] + + await withMock(calls, () => + makeDriver().move("/d", "/d/dst", ["a.txt"], "/d/a.txt", "/d/dst/a.txt"), + ) + + const moves = calls.filter((c) => c.method === "POST") + assert.equal( + moves.length, + 1, + `应恰好发出 1 次 POST move 请求,实际 ${moves.length}`, + ) + const url = new URL(moves[0].url) + assert.equal( + url.origin + url.pathname, + "https://cloud-api.yandex.net/v1/disk/resources/move", + `move 应打到 /v1/disk/resources/move,实际 ${url.origin}${url.pathname}`, + ) + const from = url.searchParams.get("from") + const path = url.searchParams.get("path") + assert.equal( + from, + "/d/a.txt", + `from query 应为源项自身路径 /d/a.txt,实际 ${from}`, + ) + assert.equal( + path, + "/d/dst/a.txt", + `path query 应为目标项自身路径 /d/dst/a.txt,实际 ${path}`, + ) + assert.ok( + !String(from).endsWith("/a.txt/a.txt"), + "from query 不得出现二次拼接的 /", + ) + assert.ok( + !String(path).endsWith("/a.txt/a.txt"), + "path query 不得出现二次拼接的 /", + ) + assert.ok( + calls.every((c) => !c.url.includes("api.oplist.org")), + "不应触发在线 renewapi 的 token 刷新请求", + ) +}) diff --git a/src/backend/internal/model/db.ts b/src/backend/internal/model/db.ts index a1814a6f..4c3ca665 100644 --- a/src/backend/internal/model/db.ts +++ b/src/backend/internal/model/db.ts @@ -41,6 +41,23 @@ const DEFAULT_PROXY_IGNORE_HEADERS = "authorization,referer" const LEGACY_TEXT_TYPES = "txt,htm,html,xml,java,properties,sql,js,json,c,cpp,python,py,php,go,rst,css,typescript,ts,log,conf,yaml,yml,cmd,bash,sh,vue,ini" +/** + * 分享摘要/「复制链接」默认模板。 + * + * 前端「复制链接」= matchTemplate(getSetting("share_summary_content"), data)。 + * 模板为空时结果就是空串,`writeText("")` 会清空剪贴板,于是按钮弹出「已复制」 + * 但剪贴板里什么都没有——分享管理页的复制链接因此完全不可用。 + * + * 这里输出**纯分享 URL**,与按钮文案(Copy link / 复制链接)以及文件工具栏 + * 「分享」对话框里 `copy(link())` 的行为保持一致。上游 Go 的默认值是一整段 + * 社交分享文案(LEGACY_SHARE_SUMMARY_GO),对「复制链接」按钮而言属于误导性 + * 行为,故不采用;已写入该文案的实例由 LEGACY_SETTING_MIGRATIONS 迁移过来。 + */ +const DEFAULT_SHARE_SUMMARY_CONTENT = "{{base_url}}/@s/{{id}}" + +/** 上一版曾采用的上游 Go 分享摘要文案(仅用于迁移到纯链接) */ +const LEGACY_SHARE_SUMMARY_GO = `@{{creator}} shared {{#each files}}{{#if @first}}"{{filename this}}"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}` + // Global default configuration payload for Cloudflare Workers export const defaultDb = { settings: [ @@ -509,7 +526,7 @@ export const defaultDb = { }, { key: "share_summary_content", - value: "", + value: DEFAULT_SHARE_SUMMARY_CONTENT, type: "text", help: "Share Summary Content", group: 4, @@ -944,6 +961,12 @@ const LEGACY_SETTING_MIGRATIONS: Record = { from: [LEGACY_TEXT_TYPES], to: DEFAULT_TEXT_TYPES, }, + // 「复制链接」:早期 seed 为空串(复制出空白),上一版一度采用上游 Go 的 + // 整段分享文案(复制出一堆非链接内容)。两种已写入的值都迁移到纯分享 URL。 + share_summary_content: { + from: ["", LEGACY_SHARE_SUMMARY_GO], + to: DEFAULT_SHARE_SUMMARY_CONTENT, + }, } const ensureDefaultSettings = (db: any) => { diff --git a/src/backend/server/public.ts b/src/backend/server/public.ts index 9016c823..0b41523f 100644 --- a/src/backend/server/public.ts +++ b/src/backend/server/public.ts @@ -398,6 +398,10 @@ publicRouter.get("/settings", async (c) => { // IMPORTANT: share_preview must be "true" — frontend blocks ALL previews when false share_preview: "true", share_archive_preview: "true", + // 「复制链接」渲染的模板。缺失或为空时会渲染成空串, + // `navigator.clipboard.writeText("")` 会清空剪贴板,按钮照常弹「已复制」 + // 但粘贴出来是空的。默认输出纯分享 URL,与按钮文案一致。 + share_summary_content: "{{base_url}}/@s/{{id}}", // --- Global --- hide_files: "/\\.DS_Store/i", diff --git a/src/backend/server/public_settings.test.ts b/src/backend/server/public_settings.test.ts index aee244a0..3b28aa63 100644 --- a/src/backend/server/public_settings.test.ts +++ b/src/backend/server/public_settings.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict" import { test } from "node:test" import { Hono } from "hono" -import { saveDb } from "../internal/model/db" +import { getDb, saveDb } from "../internal/model/db" import { publicRouter } from "./public" const env: any = {} @@ -100,3 +100,68 @@ test("Security(F-14): keys the frontend actually reads are still echoed", async assert.equal(json.data.ldap_login_tips, "use your corp account") assert.equal(json.data.sso_login_platform, "github") }) + +const assertShareTemplateUsable = (tpl: string) => { + assert.notEqual( + tpl.trim(), + "", + "share_summary_content must not be empty — the share page's copy button " + + "renders this template and writeText('') silently clears the clipboard", + ) + assert.equal( + tpl, + "{{base_url}}/@s/{{id}}", + 'the "copy link" button must render a bare share URL, not a share blurb', + ) +} + +/** + * 上一版曾采用的上游 Go 分享摘要文案(与 db.ts 的 LEGACY_SHARE_SUMMARY_GO 一致)。 + * 迁移必须把这类已落盘的值换成纯链接。 + */ +const LEGACY_GO_BLURB = `@{{creator}} shared {{#each files}}{{#if @first}}"{{filename this}}"{{/if}}{{#if @last}}{{#unless (eq @index 0)}} and {{@index}} more files{{/unless}}{{/if}}{{/each}} from {{site_title}}: {{base_url}}/@s/{{id}}{{#if pwd}} , the share code is {{pwd}}{{/if}}{{#if expires}}, please access before {{dateLocaleString expires}}.{{/if}}` + +/** + * 模拟已部署实例:某个值已经落盘,之后通过 loadDb() 读取(该路径会执行 + * ensureDefaultSettings 的 LEGACY_SETTING_MIGRATIONS 迁移)。 + * 直接 saveDb() 的结果会写进 dbCache,短 TTL 内 getDb() 不会走 loadDb, + * 所以先落盘、等缓存过期,再取迁移后的值。 + */ +const readMigratedShareSummary = async (persistedValue: string) => { + const env: any = { + DB_DRIVER: "auto", + DB_FORMAT: "map", + JWT_SECRET: "test-secret-for-share-summary", + ENCRYPTION_SECRET: "test-secret-for-share-summary", + } + await saveDb( + { + settings: [{ key: "share_summary_content", value: persistedValue }], + users: [], + storages: [], + shares: [], + }, + env, + { force: true }, + ) + await new Promise((r) => setTimeout(r, 1100)) + const db = await getDb(env) + return String( + db.settings.find((s: any) => s.key === "share_summary_content")?.value || + "", + ) +} + +test("share copy link: an empty persisted share_summary_content is migrated", async () => { + assertShareTemplateUsable(await readMigratedShareSummary("")) +}) + +test("share copy link: the legacy upstream share blurb is migrated", async () => { + assertShareTemplateUsable(await readMigratedShareSummary(LEGACY_GO_BLURB)) +}) + +test("share copy link: a missing share_summary_content falls back to default", async () => { + await seed([]) + const json = await fetchSettings() + assertShareTemplateUsable(String(json.data.share_summary_content || "")) +}) From bfb963eeb8d5324b78d4311c22bbb5e67c657342 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:07:55 +0800 Subject: [PATCH 06/49] build(edgeone): generate cloud-functions artifact at deploy time instead of committing it (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .gitignore 重新忽略 cloud-functions/,并 git rm --cached 移除已提交的 cloud-functions/[[default]].js(1.8 MB 构建产物,每次后端改动都要重建提交) - 删除 .github/workflows/edgeone-artifact-guard.yml 与 tests/artifact-guard.test.ts:产物不再入库,无需再做与源码的新鲜度比对 - 新增 .github/workflows/build-verify.yml:按平台一致流程 install -> fetch-frontend -> build-edge,校验 dist/index.html、dist-server/api/[...route].js、cloud-functions/[[default]].js 均已生成,并断言该产物未被 git 跟踪 - README 及 13 个语言版本的 Build status badge 指向 build-verify - scripts/build-edge.mjs、middleware.js 注释同步说明产物由构建生成、不入库 EdgeOne 一键部署的 buildCommand(edgeone.json -> pnpm run build)已在前端 dist/ 上长期验证为部署期实时构建,云函数产物走同一条链路即可。 Co-authored-by: pikachuren --- .github/workflows/build-verify.yml | 91 ++++ .github/workflows/edgeone-artifact-guard.yml | 98 ---- .gitignore | 4 + README.md | 2 +- cloud-functions/[[default]].js | 465 ------------------- middleware.js | 3 +- readmes/README_ar.md | 2 +- readmes/README_de.md | 2 +- readmes/README_en.md | 2 +- readmes/README_es.md | 2 +- readmes/README_fr.md | 2 +- readmes/README_hi.md | 2 +- readmes/README_it.md | 2 +- readmes/README_ja.md | 2 +- readmes/README_ko.md | 2 +- readmes/README_pt.md | 2 +- readmes/README_ru.md | 2 +- readmes/README_zh-TW.md | 2 +- scripts/build-edge.mjs | 3 + tests/artifact-guard.test.ts | 27 -- 20 files changed, 113 insertions(+), 604 deletions(-) create mode 100644 .github/workflows/build-verify.yml delete mode 100644 .github/workflows/edgeone-artifact-guard.yml delete mode 100644 cloud-functions/[[default]].js delete mode 100644 tests/artifact-guard.test.ts diff --git a/.github/workflows/build-verify.yml b/.github/workflows/build-verify.yml new file mode 100644 index 00000000..bad7efe6 --- /dev/null +++ b/.github/workflows/build-verify.yml @@ -0,0 +1,91 @@ +name: Build Verify + +# dist/、dist-server/、cloud-functions/[[default]].js 全部是构建产物,一律不入库: +# - EdgeOne Makers:edgeone.json 的 buildCommand(pnpm run build)在部署时生成 +# dist/ 与 cloud-functions/[[default]].js +# - Cloudflare Workers:wrangler deploy 从 src/backend/worker.ts 打包,并用 dist/ 作静态资源 +# - 自托管 Node:node dist-server/api/[...route].js 由 pnpm run build 产出 +# +# 因此"部署用的后端代码"完全取决于构建链路能否跑通,本工作流就是这条链路的看门人: +# 按平台一致的顺序安装、拉取前端、构建,并校验产物确实生成。任一步失败即失败。 +# +# 注意:产物按设计不提交(见 .gitignore),所以这里只校验"能否构建出来", +# 不再像已移除的 edgeone-artifact-guard.yml 那样比对已提交产物是否新鲜。 + +on: + push: + branches: + - main + paths: + - "src/**" + - "api/**" + - "functions/**" + - "scripts/**" + - "middleware.js" + - "edgeone.json" + - "wrangler.jsonc" + - "package.json" + - "pnpm-lock.yaml" + - ".github/workflows/build-verify.yml" + pull_request: + paths: + - "src/**" + - "api/**" + - "functions/**" + - "scripts/**" + - "middleware.js" + - "edgeone.json" + - "wrangler.jsonc" + - "package.json" + - "pnpm-lock.yaml" + - ".github/workflows/build-verify.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: build-verify-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + name: Build and verify artifacts + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Fetch official frontend dist + run: node scripts/fetch-frontend.mjs + + - name: Build edge/serverless artifacts + run: node scripts/build-edge.mjs + + - name: Verify artifacts exist + run: | + set -e + test -f dist/index.html + test -f "dist-server/api/[...route].js" + test -f "cloud-functions/[[default]].js" + echo "OK: dist/index.html、dist-server/api/[...route].js、cloud-functions/[[default]].js 均已生成" + + - name: Assert the EdgeOne artifact is not tracked + run: | + if git ls-files --error-unmatch "cloud-functions/[[default]].js" > /dev/null 2>&1; then + echo "::error::cloud-functions/[[default]].js 是构建产物,不应提交进仓库(见 .gitignore)。" + exit 1 + fi + echo "OK: cloud-functions/[[default]].js 未被 git 跟踪" diff --git a/.github/workflows/edgeone-artifact-guard.yml b/.github/workflows/edgeone-artifact-guard.yml deleted file mode 100644 index 22c6669d..00000000 --- a/.github/workflows/edgeone-artifact-guard.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: EdgeOne Artifact Guard - -# cloud-functions/[[default]].js 是 EdgeOne Makers 平台在检出仓库时扫描的 -# Node 云函数产物(缺失会报 "No server-handler detected" 并退化为纯静态项目, -# 详见 docs/edgeone.md),因此必须提交进仓库并与后端源码、前端产物保持同步。 -# -# 本工作流按与部署一致的流程重新构建该产物并与已提交版本比对: -# - push 到 main / 手动触发:只校验并失败,不直接修改受保护分支 -# - pull request:只校验并失败,由 PR 作者本地重建后提交(便于审查) - -on: - push: - branches: - - main - paths: - - "src/**" - - "api/**" - - "scripts/build-edge.mjs" - - "scripts/fetch-frontend.mjs" - - "cloud-functions/**" - - "package.json" - - "pnpm-lock.yaml" - - ".github/workflows/edgeone-artifact-guard.yml" - pull_request: - paths: - - "src/**" - - "api/**" - - "scripts/build-edge.mjs" - - "scripts/fetch-frontend.mjs" - - "cloud-functions/**" - - "package.json" - - "pnpm-lock.yaml" - - ".github/workflows/edgeone-artifact-guard.yml" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: edgeone-artifact-guard-${{ github.ref }} - cancel-in-progress: true - -jobs: - guard: - runs-on: ubuntu-latest - name: Rebuild and verify cloud-functions artifact - steps: - - name: Checkout - uses: actions/checkout@v7 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Fetch official frontend dist - run: node scripts/fetch-frontend.mjs - - - name: Rebuild EdgeOne artifact - run: node scripts/build-edge.mjs - - - name: Check artifact freshness - id: check - run: | - if [ -z "$(git status --porcelain -- cloud-functions)" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - git status --short -- cloud-functions - git diff --stat -- cloud-functions - git diff --exit-code -- cloud-functions || true - echo "::error::cloud-functions/[[default]].js 与源码不同步。请在本地运行 pnpm run build,并提交更新后的 cloud-functions/[[default]].js。" - exit 1 - fi - - - name: Job summary - if: always() - run: | - if [ "${{ steps.check.outputs.changed }}" = "false" ]; then - echo "cloud-functions/[[default]].js 与源码同步,无需更新" >> "$GITHUB_STEP_SUMMARY" - else - echo "cloud-functions/[[default]].js 已过期;请使用任务上传的刷新产物更新提交。" >> "$GITHUB_STEP_SUMMARY" - fi - - - name: Upload refreshed artifact on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: cloud-functions-refreshed - path: cloud-functions/ - retention-days: 7 diff --git a/.gitignore b/.gitignore index d80645cd..b7c4cb5f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,10 @@ pages/dist/ dist-server/ *.tsbuildinfo +# EdgeOne Makers Node 云函数产物:由平台构建命令(pnpm run build -> scripts/build-edge.mjs) +# 在部署时生成,不入库;构建校验见 .github/workflows/build-verify.yml +cloud-functions/ + # source maps (optional, can be removed) *.map *.js.map diff --git a/README.md b/README.md index 83fdca45..c1ba9b9c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

基于 Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA 运行

License -Build status +Build status latest version discussions Downloads diff --git a/cloud-functions/[[default]].js b/cloud-functions/[[default]].js deleted file mode 100644 index ad7e35e0..00000000 --- a/cloud-functions/[[default]].js +++ /dev/null @@ -1,465 +0,0 @@ -var WH=Object.create;var Dx=Object.defineProperty;var XH=Object.getOwnPropertyDescriptor;var ZH=Object.getOwnPropertyNames;var eK=Object.getPrototypeOf,tK=Object.prototype.hasOwnProperty;var wt=(r=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(r,{get:(e,t)=>(typeof require<"u"?require:e)[t]}):r)(function(r){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+r+'" is not supported')});var J=(r,e,t)=>()=>{if(t)throw t[0];try{return r&&(e=r(r=0)),e}catch(i){throw t=[i],i}};var oe=(r,e)=>()=>{try{return e||r((e={exports:{}}).exports,e),e.exports}catch(t){throw e=0,t}},Cn=(r,e)=>{for(var t in e)Dx(r,t,{get:e[t],enumerable:!0})},rK=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ZH(e))!tK.call(r,n)&&n!==t&&Dx(r,n,{get:()=>e[n],enumerable:!(i=XH(e,n))||i.enumerable});return r};var xr=(r,e,t)=>(t=r!=null?WH(eK(r)):{},rK(e||!r||!r.__esModule?Dx(t,"default",{value:r,enumerable:!0}):t,r));var Qx,ak=J(()=>{Qx=(r,e,t)=>(i,n)=>{let s=-1;return o(0);async function o(a){if(a<=s)throw new Error("next() called multiple times");s=a;let c,l=!1,d;if(r[a]?(d=r[a][0][0],i.req.routeIndex=a):d=a===r.length&&n||void 0,d)try{c=await d(i,()=>o(a+1))}catch(u){if(u instanceof Error&&e)i.error=u,c=await e(u,i),l=!0;else throw u}else i.finalized===!1&&t&&(c=await t(i));return c&&(i.finalized===!1||l)&&(i.res=c),i}}});var Fu,_x=J(()=>{Fu=class extends Error{res;status;constructor(r=500,e){super(e?.message,{cause:e?.cause}),this.res=e?.res,this.status=r}getResponse(){return this.res?new Response(this.res.body,{status:this.status,headers:this.res.headers}):new Response(this.message,{status:this.status})}}});var ck,lk=J(()=>{ck=Symbol()});var dk=J(()=>{});var uk,fk=J(()=>{dk();uk=(r,e)=>new Response(r,{headers:{"Content-Type":e.replace(/^[^;]+/,i=>i.toLowerCase())}}).formData()});async function iK(r,e){if(!z0(r)&&r.bodyCache.formData)return Ak(await r.bodyCache.formData,e);let t=z0(r)?r.headers:r.raw.headers,i=await r.arrayBuffer(),n=uk(i,t.get("Content-Type")||"");z0(r)||(r.bodyCache.formData=n);let s=await n;return s?Ak(s,e):{}}function Ak(r,e){let t=Object.create(null);return r.forEach((i,n)=>{e.all||n.endsWith("[]")?nK(t,n,i):t[n]=i}),e.dot&&Object.entries(t).forEach(([i,n])=>{i.includes(".")&&(sK(t,i,n),delete t[i])}),t}var z0,hk,nK,sK,gk=J(()=>{fk();z0=r=>"headers"in r,hk=async(r,e=Object.create(null))=>{let{all:t=!1,dot:i=!1}=e,o=(z0(r)?r.headers:r.raw.headers).get("Content-Type")?.split(";")[0].trim().toLowerCase();return o==="multipart/form-data"||o==="application/x-www-form-urlencoded"?iK(r,{all:t,dot:i}):{}};nK=(r,e,t)=>{r[e]!==void 0?Array.isArray(r[e])?r[e].push(t):r[e]=[r[e],t]:e.endsWith("[]")?r[e]=[t]:r[e]=t},sK=(r,e,t)=>{if(/(?:^|\.)__proto__\./.test(e))return;let i=r,n=e.split(".");n.forEach((s,o)=>{o===n.length-1?i[s]=t:((!i[s]||typeof i[s]!="object"||Array.isArray(i[s])||i[s]instanceof File)&&(i[s]=Object.create(null)),i=i[s])})}});var Px,pk,oK,aK,J0,mk,yk,cK,Tx,Ik,To,j0,vl,Fx,wk,xk,Ck,lK,sc=J(()=>{Px=r=>{let e=r.split("/");return e[0]===""&&e.shift(),e},pk=r=>{let{groups:e,path:t}=oK(r),i=Px(t);return aK(i,e)},oK=r=>{let e=[];return r=r.replace(/\{[^}]+\}/g,(t,i)=>{let n=`@${i}`;return e.push([n,t]),n}),{groups:e,path:r}},aK=(r,e)=>{for(let t=e.length-1;t>=0;t--){let[i]=e[t];for(let n=r.length-1;n>=0;n--)if(r[n].includes(i)){r[n]=r[n].replace(i,e[t][1]);break}}return r},J0={},mk=(r,e)=>{if(r==="*")return"*";let t=r.match(/^\:([^\{\}]+)(?:\{(.+)\})?$/);if(t){let i=`${r}#${e}`;return J0[i]||(t[2]?J0[i]=e&&e[0]!==":"&&e[0]!=="*"?[i,t[1],new RegExp(`^${t[2]}(?=/${e})`)]:[r,t[1],new RegExp(`^${t[2]}$`)]:J0[i]=[r,t[1],!0]),J0[i]}return null},yk=(r,e)=>{try{return e(r)}catch{return r.replace(/(?:%[0-9A-Fa-f]{2})+/g,t=>{try{return e(t)}catch{return t}})}},cK=r=>yk(r,decodeURI),Tx=r=>{let e=r.url,t=e.indexOf("/",e.indexOf(":")+4),i=t;for(;i{let e=Tx(r);return e.length>1&&e.at(-1)==="/"?e.slice(0,-1):e},To=(r,e,...t)=>(t.length&&(e=To(e,...t)),`${r?.[0]==="/"?"":"/"}${r}${e==="/"?"":`${r?.at(-1)==="/"?"":"/"}${e?.[0]==="/"?e.slice(1):e}`}`),j0=r=>{if(r.charCodeAt(r.length-1)!==63||!r.includes(":"))return null;let e=r.split("/"),t=[],i="";return e.forEach(n=>{if(n!==""&&!/\:/.test(n))i+="/"+n;else if(/\:/.test(n))if(/\?/.test(n)){t.length===0&&i===""?t.push("/"):t.push(i);let s=n.replace("?","");i+="/"+s,t.push(i)}else i+="/"+n}),t.filter((n,s,o)=>o.indexOf(n)===s)},vl=r=>r.indexOf("%")!==-1?yk(r,lK):r,Fx=r=>(r.indexOf("+")!==-1&&(r=r.replace(/\+/g," ")),vl(r)),wk=(r,e,t)=>{let i;if(!t&&e&&e.indexOf("%")===-1&&e.indexOf("+")===-1){let o=r.indexOf("?",8);if(o===-1)return;for(r.startsWith(e,o+1)||(o=r.indexOf(`&${e}`,o+1));o!==-1;){let a=r.charCodeAt(o+e.length+1);if(a===61){let c=o+e.length+2,l=r.indexOf("&",c);return Fx(r.slice(c,l===-1?void 0:l))}else if(a==38||isNaN(a))return"";o=r.indexOf(`&${e}`,o+1)}if(i=/[%+]/.test(r),!i)return}let n=Object.create(null);i??=/[%+]/.test(r);let s=r.indexOf("?",8);for(;s!==-1;){let o=r.indexOf("&",s+1),a=r.indexOf("=",s);a>o&&o!==-1&&(a=-1);let c=r.slice(s+1,a===-1?o===-1?void 0:o:a);if(i&&(c=Fx(c)),s=o,c==="")continue;let l;a===-1?l="":(l=r.slice(a+1,o===-1?void 0:o),i&&(l=Fx(l))),t?(n[c]&&Array.isArray(n[c])||(n[c]=[]),n[c].push(l)):n[c]??=l}return e?n[e]:n},xk=wk,Ck=(r,e)=>wk(r,e,!0),lK=decodeURIComponent});var Ek,Bk=J(()=>{_x();lk();gk();sc();Ek=class{raw;#t;#e;routeIndex=0;path;bodyCache={};constructor(r,e="/",t=[[]]){this.raw=r,this.path=e,this.#e=t}param(r){return r?this.#r(r):this.#s()}#r(r){let e=this.#e[0][this.routeIndex][1][r],t=this.#i(e);return t&&vl(t)}#s(){let r={},e=Object.keys(this.#e[0][this.routeIndex][1]);for(let t of e){let i=this.#i(this.#e[0][this.routeIndex][1][t]);i!==void 0&&(r[t]=vl(i))}return r}#i(r){return this.#e[1]?this.#e[1][r]:r}query(r){return xk(this.url,r)}queries(r){return Ck(this.url,r)}header(r){if(r)return this.raw.headers.get(r)??void 0;let e=Object.create(null);return this.raw.headers.forEach((t,i)=>{e[i]=t}),e}async parseBody(r){return hk(this,r)}#n=r=>{let{bodyCache:e,raw:t}=this,i=e[r];if(i)return i;for(let n in e)return e[n].then(s=>(n==="json"&&(s=JSON.stringify(s)),new Response(s)[r]()));return e[r]=t[r]()};json(){return this.#n("text").then(r=>JSON.parse(r))}text(){return this.#n("text")}arrayBuffer(){return this.#n("arrayBuffer")}bytes(){return this.#n("arrayBuffer").then(r=>new Uint8Array(r))}blob(){return this.#n("blob")}formData(){return this.#n("formData")}addValidatedData(r,e){(this.#t??={})[r]=e}valid(r){return this.#t?.[r]}get url(){return this.raw.url}get method(){return this.raw.method}get[ck](){return this.#e}get matchedRoutes(){return this.#e[0].map(([[,r]])=>r)}get routePath(){return this.#e[0].map(([[,r]])=>r)[this.routeIndex].path}}});var bk,dK,Ux,vk=J(()=>{bk={Stringify:1,BeforeStream:2,Stream:3},dK=(r,e)=>{let t=new String(r);return t.isEscaped=!0,t.callbacks=e,t},Ux=async(r,e,t,i,n)=>{typeof r=="object"&&!(r instanceof String)&&(r instanceof Promise||(r=r.toString()),r instanceof Promise&&(r=await r));let s=r.callbacks;if(!s?.length)return Promise.resolve(r);n?n[0]+=r:n=[r];let o=Promise.all(s.map(a=>a({phase:e,buffer:n,context:i}))).then(a=>Promise.all(a.filter(Boolean).map(c=>Ux(c,e,!1,i,n))).then(()=>n[0]));return t?dK(await o,s):o}});var uK,Rx,Pu,Nx,$0=J(()=>{Bk();vk();uK="text/plain; charset=UTF-8",Rx=(r,e)=>({"Content-Type":r,...e}),Pu=(r,e)=>new Response(r,e),Nx=class{#t;#e;env={};#r;finalized=!1;error;#s;#i;#n;#d;#c;#l;#a;#u;#f;constructor(r,e){this.#t=r,e&&(this.#i=e.executionCtx,this.env=e.env,this.#l=e.notFoundHandler,this.#f=e.path,this.#u=e.matchResult)}get req(){return this.#e??=new Ek(this.#t,this.#f,this.#u),this.#e}get event(){if(this.#i&&"respondWith"in this.#i)return this.#i;throw Error("This context has no FetchEvent")}get executionCtx(){if(this.#i)return this.#i;throw Error("This context has no ExecutionContext")}get res(){return this.#n||=Pu(null,{headers:this.#a??=new Headers})}set res(r){if(this.#n&&r){r=Pu(r.body,r);for(let[e,t]of this.#n.headers.entries())if(e!=="content-type")if(e==="set-cookie"){let i=this.#n.headers.getSetCookie();r.headers.delete("set-cookie");for(let n of i)r.headers.append("set-cookie",n)}else r.headers.set(e,t)}this.#n=r,this.finalized=!0}render=(...r)=>(this.#c??=e=>this.html(e),this.#c(...r));setLayout=r=>this.#d=r;getLayout=()=>this.#d;setRenderer=r=>{this.#c=r};header=(r,e,t)=>{this.finalized&&(this.#n=Pu(this.#n.body,this.#n));let i=this.#n?this.#n.headers:this.#a??=new Headers;e===void 0?i.delete(r):t?.append?i.append(r,e):i.set(r,e)};status=r=>{this.#s=r};set=(r,e)=>{this.#r??=new Map,this.#r.set(r,e)};get=r=>this.#r?this.#r.get(r):void 0;get var(){return this.#r?Object.fromEntries(this.#r):{}}#o(r,e,t){let i=this.#n?new Headers(this.#n.headers):this.#a;if(typeof e=="object"&&e.headers){i??=new Headers;for(let[s,o]of new Headers(e.headers))s==="set-cookie"?i.append(s,o):i.set(s,o)}if(t){if(!i){let s=0;for(let o in t)if(++s>1||typeof t[o]!="string"){i=new Headers;break}}if(i)for(let s in t){let o=t[s];if(typeof o=="string")i.set(s,o);else{i.delete(s);for(let a of o)i.append(s,a)}}}let n=typeof e=="number"?e:e?.status??this.#s;return Pu(r,{status:n,headers:i??t})}newResponse=(...r)=>this.#o(...r);body=(r,e,t)=>this.#o(r,e,t);text=(r,e,t)=>!this.#a&&!this.#s&&!e&&!t&&!this.finalized?new Response(r):this.#o(r,e,Rx(uK,t));json=(r,e,t)=>this.#o(JSON.stringify(r),e,Rx("application/json",t));html=(r,e,t)=>{let i=n=>this.#o(n,e,Rx("text/html; charset=UTF-8",t));return typeof r=="object"?Ux(r,bk.Stringify,!1,{}).then(i):i(r)};redirect=(r,e)=>{let t=String(r);return this.header("Location",/[^\x00-\xFF]/.test(t)?encodeURI(t):t),this.newResponse(null,e??302)};notFound=()=>(this.#l??=()=>Pu(),this.#l(this))}});var ir,kk,Sk,V0,Y0,oc=J(()=>{ir="ALL",kk="all",Sk=["get","post","put","delete","options","patch","query"],V0="Can not add a route since the matcher is already built.",Y0=class extends Error{}});var Dk,Qk=J(()=>{Dk="__COMPOSED_HANDLER"});var fK,_k,Fk,Tk=J(()=>{ak();$0();oc();Qk();sc();fK=r=>r.text("404 Not Found",404),_k=(r,e)=>{if("getResponse"in r){let t=r.getResponse();return e.newResponse(t.body,t)}return console.error(r),e.text("Internal Server Error",500)},Fk=class Pk{get;post;put;delete;options;patch;query;all;on;use;router;getPath;_basePath="/";#t="/";routes=[];constructor(e={}){[...Sk,kk].forEach(s=>{this[s]=(o,...a)=>(typeof o=="string"?this.#t=o:this.#s(s,this.#t,o),a.forEach(c=>{this.#s(s,this.#t,c)}),this)}),this.on=(s,o,...a)=>{for(let c of[o].flat()){this.#t=c;for(let l of[s].flat())a.map(d=>{this.#s(l.toUpperCase(),this.#t,d)})}return this},this.use=(s,...o)=>(typeof s=="string"?this.#t=s:(this.#t="*",o.unshift(s)),o.forEach(a=>{this.#s(ir,this.#t,a)}),this);let{strict:i,...n}=e;Object.assign(this,n),this.getPath=i??!0?e.getPath??Tx:Ik}#e(){let e=new Pk({router:this.router,getPath:this.getPath});return e.errorHandler=this.errorHandler,e.#r=this.#r,e.routes=this.routes,e}#r=fK;errorHandler=_k;route(e,t){let i=this.basePath(e);return t.routes.map(n=>{let s;t.errorHandler===_k?s=n.handler:(s=async(o,a)=>(await Qx([],t.errorHandler)(o,()=>n.handler(o,a))).res,s[Dk]=n.handler),i.#s(n.method,n.path,s,n.basePath)}),this}basePath(e){let t=this.#e();return t._basePath=To(this._basePath,e),t}onError=e=>(this.errorHandler=e,this);notFound=e=>(this.#r=e,this);mount(e,t,i){let n,s;i&&(typeof i=="function"?s=i:(s=i.optionHandler,i.replaceRequest===!1?n=c=>c:n=i.replaceRequest));let o=s?c=>{let l=s(c);return Array.isArray(l)?l:[l]}:c=>{let l;try{l=c.executionCtx}catch{}return[c.env,l]};n||=(()=>{let c=To(this._basePath,e),l=c==="/"?0:c.length;return d=>{let u=new URL(d.url);return u.pathname=this.getPath(d).slice(l)||"/",new Request(u,d)}})();let a=async(c,l)=>{let d=await t(n(c.req.raw),...o(c));if(d)return d;await l()};return this.#s(ir,To(e,"*"),a),this}#s(e,t,i,n){e=e.toUpperCase(),t=To(this._basePath,t);let s={basePath:n!==void 0?To(this._basePath,n):this._basePath,path:t,method:e,handler:i};this.router.add(e,t,[i,s]),this.routes.push(s)}#i(e,t){if(e instanceof Error)return this.errorHandler(e,t);throw e}#n(e,t,i,n){if(n==="HEAD")return(async()=>new Response(null,await this.#n(e,t,i,"GET")))();let s=this.getPath(e,{env:i}),o=this.router.match(n,s),a=new Nx(e,{path:s,matchResult:o,env:i,executionCtx:t,notFoundHandler:this.#r});if(o[0].length===1){let l;try{l=o[0][0][0][0](a,async()=>{a.res=await this.#r(a)})}catch(d){return this.#i(d,a)}return l instanceof Promise?l.then(d=>d||(a.finalized?a.res:this.#r(a))).catch(d=>this.#i(d,a)):l??this.#r(a)}let c=Qx(o[0],this.errorHandler,this.#r);return(async()=>{try{let l=await c(a);if(!l.finalized)throw new Error("Context is not finalized. Did you forget to return a Response object or `await next()`?");return l.res}catch(l){return this.#i(l,a)}})()}fetch=(e,...t)=>this.#n(e,t[1],t[0],e.method);request=(e,t,i,n)=>e instanceof Request?this.fetch(t?new Request(e,t):e,i,n):(e=e.toString(),this.fetch(new Request(/^https?:\/\//.test(e)?e:`http://localhost${To("/",e)}`,t),i,n));fire=()=>{addEventListener("fetch",e=>{e.respondWith(this.#n(e.request,e,void 0,e.request.method))})}}});function qx(r,e){let t=this.buildAllMatchers(),i=((n,s)=>{let o=t[n]||t[ir],a=o[2][s];if(a)return a;let c=s.match(o[0]);if(!c)return[[],W0];let l=c.indexOf("",1);return[o[1][l],c]});return this.match=i,i(r,e)}var W0,Ox=J(()=>{oc();W0=[]});function AK(r,e){return r.length===1?e.length===1?r{X0="[^/]+",kl=".*",ac="(?:|/.*)",Uo=Symbol(),Uk=new Set(".\\+*[^]$()");Rk=class Mx{#t;#e;#r=Object.create(null);insert(e,t,i,n,s){let o=this;for(let a=0,c=e.length;a1||g.length>1)&&g!==kl&&g!==ac)throw Uo}u=o.#r[h]=new Mx}f!==""&&(u.#e??=n.varIndex++,i.push([f,u.#e]))}else if(u=o.#r[l],!u){for(let f in o.#r)if(f.length>1&&f!==kl&&f!==ac)throw Uo;u=o.#r[l]=new Mx}o=u}if(o.#t!==void 0)throw Uo;o.#t=s?-1:t}buildRegExpStr(){let t=Object.keys(this.#r).sort(AK).map(i=>{let n=this.#r[i],s=n.buildRegExpStr();return s===""?"":(typeof n.#e=="number"?`(${i})@${n.#e}`:Uk.has(i)?`\\${i}`:i)+s}).filter(Boolean);return typeof this.#t=="number"&&this.#t!==-1&&t.unshift(`#${this.#t}`),t.length===0?"":t.length===1?t[0]:"(?:"+t.join("|")+")"}}});var Hx,Nk=J(()=>{Lx();Hx=class{#t={varIndex:0};#e=new Rk;#r=0;paths=Object.create(null);insert(r,e){if(e){this.#e.insert(r.split(""),0,[],this.#t,!0);return}let t=[],i=[],n=r;for(let o=0;;){let a=!1;if(n=n.replace(/\{[^}]+\}/g,c=>{let l=`@\\${o}`;return i[o]=[l,c],o++,a=!0,l}),!a)break}let s=n.match(/(?::[^\/]+)|(?:\/\*$)|./g)||[];for(let o=i.length-1;o>=0;o--){let[a]=i[o];for(let c=s.length-1;c>=0;c--)if(s[c].indexOf(a)!==-1){s[c]=s[c].replace(a,i[o][1]);break}}this.#e.insert(s,this.#r,t,this.#t,!1),this.paths[r]=[this.#r++,t]}buildRegExp(){let r=this.#e.buildRegExpStr();if(r==="")return[/^$/,[],[]];let e=0,t=[],i=[];return r=r.replace(/#(\d+)|@(\d+)|\.\*\$/g,(n,s,o)=>s!==void 0?(t[++e]=Number(s),"$()"):(o!==void 0&&(i[Number(o)]=++e),"")),[new RegExp(`^${r}`),t,i]}}});function Ok(r){return qk[r]??=new RegExp(r==="*"?"":`^${r.replace(/\/\*$|([.\\+*[^\]$()])/g,(e,t)=>t?`\\${t}`:"(?:|/.*)")}$`)}function hK(){qk=Object.create(null)}function Z0(r,e){if(r){for(let t of Object.keys(r).sort((i,n)=>n.length-i.length))if(Ok(t).test(e))return[...r[t]]}}var qk,eh,Kx=J(()=>{oc();sc();Ox();Lx();Nk();qk=Object.create(null);eh=class{name="RegExpRouter";#t;#e;#r;constructor(){this.#t={[ir]:Object.create(null)},this.#e={[ir]:Object.create(null)},this.#r={[ir]:new Hx}}#s(r,e){try{this.#r[r].insert(e,!/\*|\/:/.test(e))}catch(t){throw t===Uo?new Y0(e):t}}add(r,e,t){let i=this.#t,n=this.#e;if(!i||!n)throw new Error(V0);i[r]||(this.#r[r]=new Hx,[i,n].forEach(a=>{a[r]=Object.create(null),Object.keys(a[ir]).forEach(c=>{a[r][c]=[...a[ir][c]],this.#s(r,c)})})),e==="/*"&&(e="*");let s=(e.match(/\/:/g)||[]).length;if(/\*$/.test(e)){let a=Ok(e);Object.keys(i).forEach(c=>{(r===ir||r===c)&&!i[c][e]&&(this.#s(c,e),i[c][e]=Z0(i[c],e)||Z0(i[ir],e)||[])}),Object.keys(i).forEach(c=>{(r===ir||r===c)&&Object.keys(i[c]).forEach(l=>{a.test(l)&&i[c][l].push([t,s])})}),Object.keys(n).forEach(c=>{(r===ir||r===c)&&Object.keys(n[c]).forEach(l=>a.test(l)&&n[c][l].push([t,s]))});return}let o=j0(e)||[e];for(let a=0,c=o.length;a{(r===ir||r===d)&&(n[d][l]||(this.#s(d,l),n[d][l]=[...Z0(i[d],l)||Z0(i[ir],l)||[]]),n[d][l].push([t,s-c+a+1]))})}}match=qx;buildAllMatchers(){let r=Object.create(null);return Object.keys(this.#e).concat(Object.keys(this.#t)).forEach(e=>{r[e]||=this.#i(e)}),this.#t=this.#e=this.#r=void 0,hK(),r}#i(r){let e=this.#t[r],t=this.#e[r],i=this.#r[r],n=Object.create(null),s=[];[e,t].forEach(d=>{for(let u in d){let f=d[u],h=i.paths[u];if(!h){n[u]=[f.map(([p])=>[p,Object.create(null)]),W0];continue}let g=h[1];s[h[0]]=f.map(([p,I])=>{let m=Object.create(null);for(I-=1;I>=0;I--){let[w,B]=g[I];m[w]=B}return[p,m]})}});let[o,a,c]=i.buildRegExp();for(let d=0,u=s.length;d{oc();Ox();Kx()});var Lk=J(()=>{Kx();Mk()});var Gx,Hk=J(()=>{oc();Gx=class{name="SmartRouter";#t=[];#e=[];constructor(r){this.#t=r.routers}add(r,e,t){if(!this.#e)throw new Error(V0);this.#e.push([r,e,t])}match(r,e){if(!this.#e)throw new Error("Fatal error");let t=this.#t,i=this.#e,n=t.length,s=0,o;for(;s{Hk()});var Tu,gK,Gk,Jk=J(()=>{oc();sc();Tu=Object.create(null),gK=r=>{for(let e in r)return!0;return!1},Gk=class zk{#t;#e;#r;#s=0;#i=Tu;constructor(e,t,i){if(this.#e=i||Object.create(null),this.#t=[],e&&t){let n=Object.create(null);n[e]={handler:t,possibleKeys:[],score:0},this.#t=[n]}this.#r=[]}insert(e,t,i){this.#s=++this.#s;let n=this,s=pk(t),o=[];for(let a=0,c=s.length;al.indexOf(a)===c),score:this.#s}}),n}#n(e,t,i,n,s){for(let o=0,a=t.#t.length;o1&&i.sort((d,u)=>d.score-u.score),[i.map(({handler:d,params:u})=>[d,u])]}}});var zx,jk=J(()=>{sc();Jk();zx=class{name="TrieRouter";#t;constructor(){this.#t=new Gk}add(r,e,t){let i=j0(e);if(i){for(let n=0,s=i.length;n{jk()});var Xe,Jx=J(()=>{Tk();Lk();Kk();$k();Xe=class extends Fk{constructor(r={}){super(r),this.router=r.router??new Gx({routers:[new eh,new zx]})}}});var Cr=J(()=>{Jx();$0()});function th(r,e){return(r<>>32-e)>>>0}function Ro(r,e,t,i,n){r[e]=r[e]+r[t]>>>0,r[n]=th(r[n]^r[e],16),r[i]=r[i]+r[n]>>>0,r[t]=th(r[t]^r[i],12),r[e]=r[e]+r[t]>>>0,r[n]=th(r[n]^r[e],8),r[i]=r[i]+r[n]>>>0,r[t]=th(r[t]^r[i],7)}function rh(r,e){return(r[e]|r[e+1]<<8|r[e+2]<<16|r[e+3]<<24)>>>0}function Xk(r,e,t,i){let n=new Uint32Array(16);n[0]=1634760805,n[1]=857760878,n[2]=2036477234,n[3]=1797285236,n[4]=r[0],n[5]=r[1],n[6]=r[2],n[7]=r[3],n[8]=r[4],n[9]=r[5],n[10]=r[6],n[11]=r[7],n[12]=e>>>0,n[13]=t[0],n[14]=t[1],n[15]=t[2];let s=n.slice();for(let o=0;o<10;o++)Ro(s,0,4,8,12),Ro(s,1,5,9,13),Ro(s,2,6,10,14),Ro(s,3,7,11,15),Ro(s,0,5,10,15),Ro(s,1,6,11,12),Ro(s,2,7,8,13),Ro(s,3,4,9,14);for(let o=0;o<16;o++){let a=s[o]+n[o]>>>0;i[o*4]=a&255,i[o*4+1]=a>>>8&255,i[o*4+2]=a>>>16&255,i[o*4+3]=a>>>24&255}}function Zk(r){if(r.length!==32)throw new Error("ChaCha20 key must be 32 bytes");let e=new Uint32Array(8);for(let t=0;t<8;t++)e[t]=rh(r,t*4);return e}function eS(r){if(r.length!==12)throw new Error("ChaCha20 nonce must be 12 bytes");return new Uint32Array([rh(r,0),rh(r,4),rh(r,8)])}function tS(r,e,t,i=1){let n=Zk(r),s=eS(e),o=new Uint8Array(t.length),a=new Uint8Array(64);for(let c=0;c=0;t--)e=e<<8n|BigInt(r[t]);return e}function yK(r,e){let t=new Uint8Array(e),i=r;for(let n=0;n>=8n;return t}function rS(r,e){if(e.length!==32)throw new Error("Poly1305 one-time key must be 32 bytes");let t=jx(e.subarray(0,16))&mK,i=jx(e.subarray(16,32)),n=0n;for(let s=0;s>=8n;return e}function IK(r){let e=r.reduce((n,s)=>n+s.length,0),t=new Uint8Array(e),i=0;for(let n of r)t.set(n,i),i+=n.length;return t}function iS(r,e){return IK([e,Yk(e),r,Yk(r),Wk(e.length),Wk(r.length)])}function nS(r,e){let t=new Uint8Array(64);return Xk(Zk(r),0,eS(e),t),t.subarray(0,32)}function sS(r,e,t,i=new Uint8Array(0)){let n=tS(r,e,t,1),s=rS(iS(n,i),nS(r,e));return{ciphertext:n,tag:s}}function oS(r,e,t,i,n=new Uint8Array(0)){if(i.length!==16)throw new Error("Invalid Poly1305 tag length");let s=rS(iS(t,n),nS(r,e));if(!wK(s,i))throw new Error("ChaCha20-Poly1305 authentication failed (wrong key or tampered data)");return tS(r,e,t,1)}function wK(r,e){if(r.length!==e.length)return!1;let t=0;for(let i=0;i{"use strict";pK=(1n<<130n)-5n,mK=0x0ffffffc0ffffffc0ffffffc0fffffffn});var et=oe((ih,cS)=>{(function(r,e){typeof ih=="object"?cS.exports=ih=e():typeof define=="function"&&define.amd?define([],e):r.CryptoJS=e()})(ih,function(){var r=r||(function(e,t){var i;if(typeof window<"u"&&window.crypto&&(i=window.crypto),typeof self<"u"&&self.crypto&&(i=self.crypto),typeof globalThis<"u"&&globalThis.crypto&&(i=globalThis.crypto),!i&&typeof window<"u"&&window.msCrypto&&(i=window.msCrypto),!i&&typeof global<"u"&&global.crypto&&(i=global.crypto),!i&&typeof wt=="function")try{i=wt("crypto")}catch{}var n=function(){if(i){if(typeof i.getRandomValues=="function")try{return i.getRandomValues(new Uint32Array(1))[0]}catch{}if(typeof i.randomBytes=="function")try{return i.randomBytes(4).readInt32LE()}catch{}}throw new Error("Native crypto module could not be used to get secure random number.")},s=Object.create||(function(){function m(){}return function(w){var B;return m.prototype=w,B=new m,m.prototype=null,B}})(),o={},a=o.lib={},c=a.Base=(function(){return{extend:function(m){var w=s(this);return m&&w.mixIn(m),(!w.hasOwnProperty("init")||this.init===w.init)&&(w.init=function(){w.$super.init.apply(this,arguments)}),w.init.prototype=w,w.$super=this,w},create:function(){var m=this.extend();return m.init.apply(m,arguments),m},init:function(){},mixIn:function(m){for(var w in m)m.hasOwnProperty(w)&&(this[w]=m[w]);m.hasOwnProperty("toString")&&(this.toString=m.toString)},clone:function(){return this.init.prototype.extend(this)}}})(),l=a.WordArray=c.extend({init:function(m,w){m=this.words=m||[],w!=t?this.sigBytes=w:this.sigBytes=m.length*4},toString:function(m){return(m||u).stringify(this)},concat:function(m){var w=this.words,B=m.words,C=this.sigBytes,x=m.sigBytes;if(this.clamp(),C%4)for(var Q=0;Q>>2]>>>24-Q%4*8&255;w[C+Q>>>2]|=R<<24-(C+Q)%4*8}else for(var U=0;U>>2]=B[U>>>2];return this.sigBytes+=x,this},clamp:function(){var m=this.words,w=this.sigBytes;m[w>>>2]&=4294967295<<32-w%4*8,m.length=e.ceil(w/4)},clone:function(){var m=c.clone.call(this);return m.words=this.words.slice(0),m},random:function(m){for(var w=[],B=0;B>>2]>>>24-x%4*8&255;C.push((Q>>>4).toString(16)),C.push((Q&15).toString(16))}return C.join("")},parse:function(m){for(var w=m.length,B=[],C=0;C>>3]|=parseInt(m.substr(C,2),16)<<24-C%8*4;return new l.init(B,w/2)}},f=d.Latin1={stringify:function(m){for(var w=m.words,B=m.sigBytes,C=[],x=0;x>>2]>>>24-x%4*8&255;C.push(String.fromCharCode(Q))}return C.join("")},parse:function(m){for(var w=m.length,B=[],C=0;C>>2]|=(m.charCodeAt(C)&255)<<24-C%4*8;return new l.init(B,w)}},h=d.Utf8={stringify:function(m){try{return decodeURIComponent(escape(f.stringify(m)))}catch{throw new Error("Malformed UTF-8 data")}},parse:function(m){return f.parse(unescape(encodeURIComponent(m)))}},g=a.BufferedBlockAlgorithm=c.extend({reset:function(){this._data=new l.init,this._nDataBytes=0},_append:function(m){typeof m=="string"&&(m=h.parse(m)),this._data.concat(m),this._nDataBytes+=m.sigBytes},_process:function(m){var w,B=this._data,C=B.words,x=B.sigBytes,Q=this.blockSize,R=Q*4,U=x/R;m?U=e.ceil(U):U=e.max((U|0)-this._minBufferSize,0);var T=U*Q,D=e.min(T*4,x);if(T){for(var b=0;b{(function(r,e){typeof nh=="object"?lS.exports=nh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(nh,function(r){return(function(e){var t=r,i=t.lib,n=i.Base,s=i.WordArray,o=t.x64={},a=o.Word=n.extend({init:function(l,d){this.high=l,this.low=d}}),c=o.WordArray=n.extend({init:function(l,d){l=this.words=l||[],d!=e?this.sigBytes=d:this.sigBytes=l.length*8},toX32:function(){for(var l=this.words,d=l.length,u=[],f=0;f{(function(r,e){typeof sh=="object"?dS.exports=sh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(sh,function(r){return(function(){if(typeof ArrayBuffer=="function"){var e=r,t=e.lib,i=t.WordArray,n=i.init,s=i.init=function(o){if(o instanceof ArrayBuffer&&(o=new Uint8Array(o)),(o instanceof Int8Array||typeof Uint8ClampedArray<"u"&&o instanceof Uint8ClampedArray||o instanceof Int16Array||o instanceof Uint16Array||o instanceof Int32Array||o instanceof Uint32Array||o instanceof Float32Array||o instanceof Float64Array)&&(o=new Uint8Array(o.buffer,o.byteOffset,o.byteLength)),o instanceof Uint8Array){for(var a=o.byteLength,c=[],l=0;l>>2]|=o[l]<<24-l%4*8;n.call(this,c,a)}else n.apply(this,arguments)};s.prototype=i}})(),r.lib.WordArray})});var AS=oe((oh,fS)=>{(function(r,e){typeof oh=="object"?fS.exports=oh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(oh,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=e.enc,s=n.Utf16=n.Utf16BE={stringify:function(a){for(var c=a.words,l=a.sigBytes,d=[],u=0;u>>2]>>>16-u%4*8&65535;d.push(String.fromCharCode(f))}return d.join("")},parse:function(a){for(var c=a.length,l=[],d=0;d>>1]|=a.charCodeAt(d)<<16-d%2*16;return i.create(l,c*2)}};n.Utf16LE={stringify:function(a){for(var c=a.words,l=a.sigBytes,d=[],u=0;u>>2]>>>16-u%4*8&65535);d.push(String.fromCharCode(f))}return d.join("")},parse:function(a){for(var c=a.length,l=[],d=0;d>>1]|=o(a.charCodeAt(d)<<16-d%2*16);return i.create(l,c*2)}};function o(a){return a<<8&4278255360|a>>>8&16711935}})(),r.enc.Utf16})});var No=oe((ah,hS)=>{(function(r,e){typeof ah=="object"?hS.exports=ah=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(ah,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=e.enc,s=n.Base64={stringify:function(a){var c=a.words,l=a.sigBytes,d=this._map;a.clamp();for(var u=[],f=0;f>>2]>>>24-f%4*8&255,g=c[f+1>>>2]>>>24-(f+1)%4*8&255,p=c[f+2>>>2]>>>24-(f+2)%4*8&255,I=h<<16|g<<8|p,m=0;m<4&&f+m*.75>>6*(3-m)&63));var w=d.charAt(64);if(w)for(;u.length%4;)u.push(w);return u.join("")},parse:function(a){var c=a.length,l=this._map,d=this._reverseMap;if(!d){d=this._reverseMap=[];for(var u=0;u>>6-f%4*2,p=h|g;d[u>>>2]|=p<<24-u%4*8,u++}return i.create(d,u)}})(),r.enc.Base64})});var pS=oe((ch,gS)=>{(function(r,e){typeof ch=="object"?gS.exports=ch=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(ch,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=e.enc,s=n.Base64url={stringify:function(a,c){c===void 0&&(c=!0);var l=a.words,d=a.sigBytes,u=c?this._safe_map:this._map;a.clamp();for(var f=[],h=0;h>>2]>>>24-h%4*8&255,p=l[h+1>>>2]>>>24-(h+1)%4*8&255,I=l[h+2>>>2]>>>24-(h+2)%4*8&255,m=g<<16|p<<8|I,w=0;w<4&&h+w*.75>>6*(3-w)&63));var B=u.charAt(64);if(B)for(;f.length%4;)f.push(B);return f.join("")},parse:function(a,c){c===void 0&&(c=!0);var l=a.length,d=c?this._safe_map:this._map,u=this._reverseMap;if(!u){u=this._reverseMap=[];for(var f=0;f>>6-f%4*2,p=h|g;d[u>>>2]|=p<<24-u%4*8,u++}return i.create(d,u)}})(),r.enc.Base64url})});var qo=oe((lh,mS)=>{(function(r,e){typeof lh=="object"?mS.exports=lh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(lh,function(r){return(function(e){var t=r,i=t.lib,n=i.WordArray,s=i.Hasher,o=t.algo,a=[];(function(){for(var h=0;h<64;h++)a[h]=e.abs(e.sin(h+1))*4294967296|0})();var c=o.MD5=s.extend({_doReset:function(){this._hash=new n.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(h,g){for(var p=0;p<16;p++){var I=g+p,m=h[I];h[I]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360}var w=this._hash.words,B=h[g+0],C=h[g+1],x=h[g+2],Q=h[g+3],R=h[g+4],U=h[g+5],T=h[g+6],D=h[g+7],b=h[g+8],_=h[g+9],v=h[g+10],E=h[g+11],P=h[g+12],N=h[g+13],j=h[g+14],z=h[g+15],L=w[0],G=w[1],K=w[2],$=w[3];L=l(L,G,K,$,B,7,a[0]),$=l($,L,G,K,C,12,a[1]),K=l(K,$,L,G,x,17,a[2]),G=l(G,K,$,L,Q,22,a[3]),L=l(L,G,K,$,R,7,a[4]),$=l($,L,G,K,U,12,a[5]),K=l(K,$,L,G,T,17,a[6]),G=l(G,K,$,L,D,22,a[7]),L=l(L,G,K,$,b,7,a[8]),$=l($,L,G,K,_,12,a[9]),K=l(K,$,L,G,v,17,a[10]),G=l(G,K,$,L,E,22,a[11]),L=l(L,G,K,$,P,7,a[12]),$=l($,L,G,K,N,12,a[13]),K=l(K,$,L,G,j,17,a[14]),G=l(G,K,$,L,z,22,a[15]),L=d(L,G,K,$,C,5,a[16]),$=d($,L,G,K,T,9,a[17]),K=d(K,$,L,G,E,14,a[18]),G=d(G,K,$,L,B,20,a[19]),L=d(L,G,K,$,U,5,a[20]),$=d($,L,G,K,v,9,a[21]),K=d(K,$,L,G,z,14,a[22]),G=d(G,K,$,L,R,20,a[23]),L=d(L,G,K,$,_,5,a[24]),$=d($,L,G,K,j,9,a[25]),K=d(K,$,L,G,Q,14,a[26]),G=d(G,K,$,L,b,20,a[27]),L=d(L,G,K,$,N,5,a[28]),$=d($,L,G,K,x,9,a[29]),K=d(K,$,L,G,D,14,a[30]),G=d(G,K,$,L,P,20,a[31]),L=u(L,G,K,$,U,4,a[32]),$=u($,L,G,K,b,11,a[33]),K=u(K,$,L,G,E,16,a[34]),G=u(G,K,$,L,j,23,a[35]),L=u(L,G,K,$,C,4,a[36]),$=u($,L,G,K,R,11,a[37]),K=u(K,$,L,G,D,16,a[38]),G=u(G,K,$,L,v,23,a[39]),L=u(L,G,K,$,N,4,a[40]),$=u($,L,G,K,B,11,a[41]),K=u(K,$,L,G,Q,16,a[42]),G=u(G,K,$,L,T,23,a[43]),L=u(L,G,K,$,_,4,a[44]),$=u($,L,G,K,P,11,a[45]),K=u(K,$,L,G,z,16,a[46]),G=u(G,K,$,L,x,23,a[47]),L=f(L,G,K,$,B,6,a[48]),$=f($,L,G,K,D,10,a[49]),K=f(K,$,L,G,j,15,a[50]),G=f(G,K,$,L,U,21,a[51]),L=f(L,G,K,$,P,6,a[52]),$=f($,L,G,K,Q,10,a[53]),K=f(K,$,L,G,v,15,a[54]),G=f(G,K,$,L,C,21,a[55]),L=f(L,G,K,$,b,6,a[56]),$=f($,L,G,K,z,10,a[57]),K=f(K,$,L,G,T,15,a[58]),G=f(G,K,$,L,N,21,a[59]),L=f(L,G,K,$,R,6,a[60]),$=f($,L,G,K,E,10,a[61]),K=f(K,$,L,G,x,15,a[62]),G=f(G,K,$,L,_,21,a[63]),w[0]=w[0]+L|0,w[1]=w[1]+G|0,w[2]=w[2]+K|0,w[3]=w[3]+$|0},_doFinalize:function(){var h=this._data,g=h.words,p=this._nDataBytes*8,I=h.sigBytes*8;g[I>>>5]|=128<<24-I%32;var m=e.floor(p/4294967296),w=p;g[(I+64>>>9<<4)+15]=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360,g[(I+64>>>9<<4)+14]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360,h.sigBytes=(g.length+1)*4,this._process();for(var B=this._hash,C=B.words,x=0;x<4;x++){var Q=C[x];C[x]=(Q<<8|Q>>>24)&16711935|(Q<<24|Q>>>8)&4278255360}return B},clone:function(){var h=s.clone.call(this);return h._hash=this._hash.clone(),h}});function l(h,g,p,I,m,w,B){var C=h+(g&p|~g&I)+m+B;return(C<>>32-w)+g}function d(h,g,p,I,m,w,B){var C=h+(g&I|p&~I)+m+B;return(C<>>32-w)+g}function u(h,g,p,I,m,w,B){var C=h+(g^p^I)+m+B;return(C<>>32-w)+g}function f(h,g,p,I,m,w,B){var C=h+(p^(g|~I))+m+B;return(C<>>32-w)+g}t.MD5=s._createHelper(c),t.HmacMD5=s._createHmacHelper(c)})(Math),r.MD5})});var $x=oe((dh,yS)=>{(function(r,e){typeof dh=="object"?yS.exports=dh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(dh,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=t.Hasher,s=e.algo,o=[],a=s.SHA1=n.extend({_doReset:function(){this._hash=new i.init([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(c,l){for(var d=this._hash.words,u=d[0],f=d[1],h=d[2],g=d[3],p=d[4],I=0;I<80;I++){if(I<16)o[I]=c[l+I]|0;else{var m=o[I-3]^o[I-8]^o[I-14]^o[I-16];o[I]=m<<1|m>>>31}var w=(u<<5|u>>>27)+p+o[I];I<20?w+=(f&h|~f&g)+1518500249:I<40?w+=(f^h^g)+1859775393:I<60?w+=(f&h|f&g|h&g)-1894007588:w+=(f^h^g)-899497514,p=g,g=h,h=f<<30|f>>>2,f=u,u=w}d[0]=d[0]+u|0,d[1]=d[1]+f|0,d[2]=d[2]+h|0,d[3]=d[3]+g|0,d[4]=d[4]+p|0},_doFinalize:function(){var c=this._data,l=c.words,d=this._nDataBytes*8,u=c.sigBytes*8;return l[u>>>5]|=128<<24-u%32,l[(u+64>>>9<<4)+14]=Math.floor(d/4294967296),l[(u+64>>>9<<4)+15]=d,c.sigBytes=l.length*4,this._process(),this._hash},clone:function(){var c=n.clone.call(this);return c._hash=this._hash.clone(),c}});e.SHA1=n._createHelper(a),e.HmacSHA1=n._createHmacHelper(a)})(),r.SHA1})});var fh=oe((uh,IS)=>{(function(r,e){typeof uh=="object"?IS.exports=uh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(uh,function(r){return(function(e){var t=r,i=t.lib,n=i.WordArray,s=i.Hasher,o=t.algo,a=[],c=[];(function(){function u(p){for(var I=e.sqrt(p),m=2;m<=I;m++)if(!(p%m))return!1;return!0}function f(p){return(p-(p|0))*4294967296|0}for(var h=2,g=0;g<64;)u(h)&&(g<8&&(a[g]=f(e.pow(h,1/2))),c[g]=f(e.pow(h,1/3)),g++),h++})();var l=[],d=o.SHA256=s.extend({_doReset:function(){this._hash=new n.init(a.slice(0))},_doProcessBlock:function(u,f){for(var h=this._hash.words,g=h[0],p=h[1],I=h[2],m=h[3],w=h[4],B=h[5],C=h[6],x=h[7],Q=0;Q<64;Q++){if(Q<16)l[Q]=u[f+Q]|0;else{var R=l[Q-15],U=(R<<25|R>>>7)^(R<<14|R>>>18)^R>>>3,T=l[Q-2],D=(T<<15|T>>>17)^(T<<13|T>>>19)^T>>>10;l[Q]=U+l[Q-7]+D+l[Q-16]}var b=w&B^~w&C,_=g&p^g&I^p&I,v=(g<<30|g>>>2)^(g<<19|g>>>13)^(g<<10|g>>>22),E=(w<<26|w>>>6)^(w<<21|w>>>11)^(w<<7|w>>>25),P=x+E+b+c[Q]+l[Q],N=v+_;x=C,C=B,B=w,w=m+P|0,m=I,I=p,p=g,g=P+N|0}h[0]=h[0]+g|0,h[1]=h[1]+p|0,h[2]=h[2]+I|0,h[3]=h[3]+m|0,h[4]=h[4]+w|0,h[5]=h[5]+B|0,h[6]=h[6]+C|0,h[7]=h[7]+x|0},_doFinalize:function(){var u=this._data,f=u.words,h=this._nDataBytes*8,g=u.sigBytes*8;return f[g>>>5]|=128<<24-g%32,f[(g+64>>>9<<4)+14]=e.floor(h/4294967296),f[(g+64>>>9<<4)+15]=h,u.sigBytes=f.length*4,this._process(),this._hash},clone:function(){var u=s.clone.call(this);return u._hash=this._hash.clone(),u}});t.SHA256=s._createHelper(d),t.HmacSHA256=s._createHmacHelper(d)})(Math),r.SHA256})});var xS=oe((Ah,wS)=>{(function(r,e,t){typeof Ah=="object"?wS.exports=Ah=e(et(),fh()):typeof define=="function"&&define.amd?define(["./core","./sha256"],e):e(r.CryptoJS)})(Ah,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=e.algo,s=n.SHA256,o=n.SHA224=s.extend({_doReset:function(){this._hash=new i.init([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428])},_doFinalize:function(){var a=s._doFinalize.call(this);return a.sigBytes-=4,a}});e.SHA224=s._createHelper(o),e.HmacSHA224=s._createHmacHelper(o)})(),r.SHA224})});var Vx=oe((hh,CS)=>{(function(r,e,t){typeof hh=="object"?CS.exports=hh=e(et(),Uu()):typeof define=="function"&&define.amd?define(["./core","./x64-core"],e):e(r.CryptoJS)})(hh,function(r){return(function(){var e=r,t=e.lib,i=t.Hasher,n=e.x64,s=n.Word,o=n.WordArray,a=e.algo;function c(){return s.create.apply(s,arguments)}var l=[c(1116352408,3609767458),c(1899447441,602891725),c(3049323471,3964484399),c(3921009573,2173295548),c(961987163,4081628472),c(1508970993,3053834265),c(2453635748,2937671579),c(2870763221,3664609560),c(3624381080,2734883394),c(310598401,1164996542),c(607225278,1323610764),c(1426881987,3590304994),c(1925078388,4068182383),c(2162078206,991336113),c(2614888103,633803317),c(3248222580,3479774868),c(3835390401,2666613458),c(4022224774,944711139),c(264347078,2341262773),c(604807628,2007800933),c(770255983,1495990901),c(1249150122,1856431235),c(1555081692,3175218132),c(1996064986,2198950837),c(2554220882,3999719339),c(2821834349,766784016),c(2952996808,2566594879),c(3210313671,3203337956),c(3336571891,1034457026),c(3584528711,2466948901),c(113926993,3758326383),c(338241895,168717936),c(666307205,1188179964),c(773529912,1546045734),c(1294757372,1522805485),c(1396182291,2643833823),c(1695183700,2343527390),c(1986661051,1014477480),c(2177026350,1206759142),c(2456956037,344077627),c(2730485921,1290863460),c(2820302411,3158454273),c(3259730800,3505952657),c(3345764771,106217008),c(3516065817,3606008344),c(3600352804,1432725776),c(4094571909,1467031594),c(275423344,851169720),c(430227734,3100823752),c(506948616,1363258195),c(659060556,3750685593),c(883997877,3785050280),c(958139571,3318307427),c(1322822218,3812723403),c(1537002063,2003034995),c(1747873779,3602036899),c(1955562222,1575990012),c(2024104815,1125592928),c(2227730452,2716904306),c(2361852424,442776044),c(2428436474,593698344),c(2756734187,3733110249),c(3204031479,2999351573),c(3329325298,3815920427),c(3391569614,3928383900),c(3515267271,566280711),c(3940187606,3454069534),c(4118630271,4000239992),c(116418474,1914138554),c(174292421,2731055270),c(289380356,3203993006),c(460393269,320620315),c(685471733,587496836),c(852142971,1086792851),c(1017036298,365543100),c(1126000580,2618297676),c(1288033470,3409855158),c(1501505948,4234509866),c(1607167915,987167468),c(1816402316,1246189591)],d=[];(function(){for(var f=0;f<80;f++)d[f]=c()})();var u=a.SHA512=i.extend({_doReset:function(){this._hash=new o.init([new s.init(1779033703,4089235720),new s.init(3144134277,2227873595),new s.init(1013904242,4271175723),new s.init(2773480762,1595750129),new s.init(1359893119,2917565137),new s.init(2600822924,725511199),new s.init(528734635,4215389547),new s.init(1541459225,327033209)])},_doProcessBlock:function(f,h){for(var g=this._hash.words,p=g[0],I=g[1],m=g[2],w=g[3],B=g[4],C=g[5],x=g[6],Q=g[7],R=p.high,U=p.low,T=I.high,D=I.low,b=m.high,_=m.low,v=w.high,E=w.low,P=B.high,N=B.low,j=C.high,z=C.low,L=x.high,G=x.low,K=Q.high,$=Q.low,te=R,re=U,ee=T,Z=D,me=b,le=_,Ae=v,ve=E,qe=P,_e=N,se=j,rt=z,ce=L,dr=G,Mr=K,at=$,we=0;we<80;we++){var Ee,Ji,vo=d[we];if(we<16)Ji=vo.high=f[h+we*2]|0,Ee=vo.low=f[h+we*2+1]|0;else{var Se=d[we-15],Dt=Se.high,vs=Se.low,Ce=(Dt>>>1|vs<<31)^(Dt>>>8|vs<<24)^Dt>>>7,ji=(vs>>>1|Dt<<31)^(vs>>>8|Dt<<24)^(vs>>>7|Dt<<25),si=d[we-2],wr=si.high,tr=si.low,ur=(wr>>>19|tr<<13)^(wr<<3|tr>>>29)^wr>>>6,Qe=(tr>>>19|wr<<13)^(tr<<3|wr>>>29)^(tr>>>6|wr<<26),be=d[we-7],xl=be.high,hi=be.low,Lr=d[we-16],Ln=Lr.high,yn=Lr.low;Ee=ji+hi,Ji=Ce+xl+(Ee>>>0>>0?1:0),Ee=Ee+Qe,Ji=Ji+ur+(Ee>>>0>>0?1:0),Ee=Ee+yn,Ji=Ji+Ln+(Ee>>>0>>0?1:0),vo.high=Ji,vo.low=Ee}var Le=qe&se^~qe&ce,ic=_e&rt^~_e&dr,$i=te&ee^te&me^ee&me,ko=re&Z^re&le^Z&le,Ri=(te>>>28|re<<4)^(te<<30|re>>>2)^(te<<25|re>>>7),A=(re>>>28|te<<4)^(re<<30|te>>>2)^(re<<25|te>>>7),k=(qe>>>14|_e<<18)^(qe>>>18|_e<<14)^(qe<<23|_e>>>9),O=(_e>>>14|qe<<18)^(_e>>>18|qe<<14)^(_e<<23|qe>>>9),V=l[we],H=V.high,W=V.low,ue=at+O,Re=Mr+k+(ue>>>0>>0?1:0),ue=ue+ic,Re=Re+Le+(ue>>>0>>0?1:0),ue=ue+W,Re=Re+H+(ue>>>0>>0?1:0),ue=ue+Ee,Re=Re+Ji+(ue>>>0>>0?1:0),Ve=A+ko,Ie=Ri+$i+(Ve>>>0>>0?1:0);Mr=ce,at=dr,ce=se,dr=rt,se=qe,rt=_e,_e=ve+ue|0,qe=Ae+Re+(_e>>>0>>0?1:0)|0,Ae=me,ve=le,me=ee,le=Z,ee=te,Z=re,re=ue+Ve|0,te=Re+Ie+(re>>>0>>0?1:0)|0}U=p.low=U+re,p.high=R+te+(U>>>0>>0?1:0),D=I.low=D+Z,I.high=T+ee+(D>>>0>>0?1:0),_=m.low=_+le,m.high=b+me+(_>>>0>>0?1:0),E=w.low=E+ve,w.high=v+Ae+(E>>>0>>0?1:0),N=B.low=N+_e,B.high=P+qe+(N>>>0<_e>>>0?1:0),z=C.low=z+rt,C.high=j+se+(z>>>0>>0?1:0),G=x.low=G+dr,x.high=L+ce+(G>>>0>>0?1:0),$=Q.low=$+at,Q.high=K+Mr+($>>>0>>0?1:0)},_doFinalize:function(){var f=this._data,h=f.words,g=this._nDataBytes*8,p=f.sigBytes*8;h[p>>>5]|=128<<24-p%32,h[(p+128>>>10<<5)+30]=Math.floor(g/4294967296),h[(p+128>>>10<<5)+31]=g,f.sigBytes=h.length*4,this._process();var I=this._hash.toX32();return I},clone:function(){var f=i.clone.call(this);return f._hash=this._hash.clone(),f},blockSize:1024/32});e.SHA512=i._createHelper(u),e.HmacSHA512=i._createHmacHelper(u)})(),r.SHA512})});var BS=oe((gh,ES)=>{(function(r,e,t){typeof gh=="object"?ES.exports=gh=e(et(),Uu(),Vx()):typeof define=="function"&&define.amd?define(["./core","./x64-core","./sha512"],e):e(r.CryptoJS)})(gh,function(r){return(function(){var e=r,t=e.x64,i=t.Word,n=t.WordArray,s=e.algo,o=s.SHA512,a=s.SHA384=o.extend({_doReset:function(){this._hash=new n.init([new i.init(3418070365,3238371032),new i.init(1654270250,914150663),new i.init(2438529370,812702999),new i.init(355462360,4144912697),new i.init(1731405415,4290775857),new i.init(2394180231,1750603025),new i.init(3675008525,1694076839),new i.init(1203062813,3204075428)])},_doFinalize:function(){var c=o._doFinalize.call(this);return c.sigBytes-=16,c}});e.SHA384=o._createHelper(a),e.HmacSHA384=o._createHmacHelper(a)})(),r.SHA384})});var vS=oe((ph,bS)=>{(function(r,e,t){typeof ph=="object"?bS.exports=ph=e(et(),Uu()):typeof define=="function"&&define.amd?define(["./core","./x64-core"],e):e(r.CryptoJS)})(ph,function(r){return(function(e){var t=r,i=t.lib,n=i.WordArray,s=i.Hasher,o=t.x64,a=o.Word,c=t.algo,l=[],d=[],u=[];(function(){for(var g=1,p=0,I=0;I<24;I++){l[g+5*p]=(I+1)*(I+2)/2%64;var m=p%5,w=(2*g+3*p)%5;g=m,p=w}for(var g=0;g<5;g++)for(var p=0;p<5;p++)d[g+5*p]=p+(2*g+3*p)%5*5;for(var B=1,C=0;C<24;C++){for(var x=0,Q=0,R=0;R<7;R++){if(B&1){var U=(1<>>24)&16711935|(B<<24|B>>>8)&4278255360,C=(C<<8|C>>>24)&16711935|(C<<24|C>>>8)&4278255360;var x=I[w];x.high^=C,x.low^=B}for(var Q=0;Q<24;Q++){for(var R=0;R<5;R++){for(var U=0,T=0,D=0;D<5;D++){var x=I[R+5*D];U^=x.high,T^=x.low}var b=f[R];b.high=U,b.low=T}for(var R=0;R<5;R++)for(var _=f[(R+4)%5],v=f[(R+1)%5],E=v.high,P=v.low,U=_.high^(E<<1|P>>>31),T=_.low^(P<<1|E>>>31),D=0;D<5;D++){var x=I[R+5*D];x.high^=U,x.low^=T}for(var N=1;N<25;N++){var U,T,x=I[N],j=x.high,z=x.low,L=l[N];L<32?(U=j<>>32-L,T=z<>>32-L):(U=z<>>64-L,T=j<>>64-L);var G=f[d[N]];G.high=U,G.low=T}var K=f[0],$=I[0];K.high=$.high,K.low=$.low;for(var R=0;R<5;R++)for(var D=0;D<5;D++){var N=R+5*D,x=I[N],te=f[N],re=f[(R+1)%5+5*D],ee=f[(R+2)%5+5*D];x.high=te.high^~re.high&ee.high,x.low=te.low^~re.low&ee.low}var x=I[0],Z=u[Q];x.high^=Z.high,x.low^=Z.low}},_doFinalize:function(){var g=this._data,p=g.words,I=this._nDataBytes*8,m=g.sigBytes*8,w=this.blockSize*32;p[m>>>5]|=1<<24-m%32,p[(e.ceil((m+1)/w)*w>>>5)-1]|=128,g.sigBytes=p.length*4,this._process();for(var B=this._state,C=this.cfg.outputLength/8,x=C/8,Q=[],R=0;R>>24)&16711935|(T<<24|T>>>8)&4278255360,D=(D<<8|D>>>24)&16711935|(D<<24|D>>>8)&4278255360,Q.push(D),Q.push(T)}return new n.init(Q,C)},clone:function(){for(var g=s.clone.call(this),p=g._state=this._state.slice(0),I=0;I<25;I++)p[I]=p[I].clone();return g}});t.SHA3=s._createHelper(h),t.HmacSHA3=s._createHmacHelper(h)})(Math),r.SHA3})});var SS=oe((mh,kS)=>{(function(r,e){typeof mh=="object"?kS.exports=mh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(mh,function(r){return(function(e){var t=r,i=t.lib,n=i.WordArray,s=i.Hasher,o=t.algo,a=n.create([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13]),c=n.create([5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11]),l=n.create([11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6]),d=n.create([8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11]),u=n.create([0,1518500249,1859775393,2400959708,2840853838]),f=n.create([1352829926,1548603684,1836072691,2053994217,0]),h=o.RIPEMD160=s.extend({_doReset:function(){this._hash=n.create([1732584193,4023233417,2562383102,271733878,3285377520])},_doProcessBlock:function(C,x){for(var Q=0;Q<16;Q++){var R=x+Q,U=C[R];C[R]=(U<<8|U>>>24)&16711935|(U<<24|U>>>8)&4278255360}var T=this._hash.words,D=u.words,b=f.words,_=a.words,v=c.words,E=l.words,P=d.words,N,j,z,L,G,K,$,te,re,ee;K=N=T[0],$=j=T[1],te=z=T[2],re=L=T[3],ee=G=T[4];for(var Z,Q=0;Q<80;Q+=1)Z=N+C[x+_[Q]]|0,Q<16?Z+=g(j,z,L)+D[0]:Q<32?Z+=p(j,z,L)+D[1]:Q<48?Z+=I(j,z,L)+D[2]:Q<64?Z+=m(j,z,L)+D[3]:Z+=w(j,z,L)+D[4],Z=Z|0,Z=B(Z,E[Q]),Z=Z+G|0,N=G,G=L,L=B(z,10),z=j,j=Z,Z=K+C[x+v[Q]]|0,Q<16?Z+=w($,te,re)+b[0]:Q<32?Z+=m($,te,re)+b[1]:Q<48?Z+=I($,te,re)+b[2]:Q<64?Z+=p($,te,re)+b[3]:Z+=g($,te,re)+b[4],Z=Z|0,Z=B(Z,P[Q]),Z=Z+ee|0,K=ee,ee=re,re=B(te,10),te=$,$=Z;Z=T[1]+z+re|0,T[1]=T[2]+L+ee|0,T[2]=T[3]+G+K|0,T[3]=T[4]+N+$|0,T[4]=T[0]+j+te|0,T[0]=Z},_doFinalize:function(){var C=this._data,x=C.words,Q=this._nDataBytes*8,R=C.sigBytes*8;x[R>>>5]|=128<<24-R%32,x[(R+64>>>9<<4)+14]=(Q<<8|Q>>>24)&16711935|(Q<<24|Q>>>8)&4278255360,C.sigBytes=(x.length+1)*4,this._process();for(var U=this._hash,T=U.words,D=0;D<5;D++){var b=T[D];T[D]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360}return U},clone:function(){var C=s.clone.call(this);return C._hash=this._hash.clone(),C}});function g(C,x,Q){return C^x^Q}function p(C,x,Q){return C&x|~C&Q}function I(C,x,Q){return(C|~x)^Q}function m(C,x,Q){return C&Q|x&~Q}function w(C,x,Q){return C^(x|~Q)}function B(C,x){return C<>>32-x}t.RIPEMD160=s._createHelper(h),t.HmacRIPEMD160=s._createHmacHelper(h)})(Math),r.RIPEMD160})});var Ih=oe((yh,DS)=>{(function(r,e){typeof yh=="object"?DS.exports=yh=e(et()):typeof define=="function"&&define.amd?define(["./core"],e):e(r.CryptoJS)})(yh,function(r){(function(){var e=r,t=e.lib,i=t.Base,n=e.enc,s=n.Utf8,o=e.algo,a=o.HMAC=i.extend({init:function(c,l){c=this._hasher=new c.init,typeof l=="string"&&(l=s.parse(l));var d=c.blockSize,u=d*4;l.sigBytes>u&&(l=c.finalize(l)),l.clamp();for(var f=this._oKey=l.clone(),h=this._iKey=l.clone(),g=f.words,p=h.words,I=0;I{(function(r,e,t){typeof wh=="object"?QS.exports=wh=e(et(),fh(),Ih()):typeof define=="function"&&define.amd?define(["./core","./sha256","./hmac"],e):e(r.CryptoJS)})(wh,function(r){return(function(){var e=r,t=e.lib,i=t.Base,n=t.WordArray,s=e.algo,o=s.SHA256,a=s.HMAC,c=s.PBKDF2=i.extend({cfg:i.extend({keySize:128/32,hasher:o,iterations:25e4}),init:function(l){this.cfg=this.cfg.extend(l)},compute:function(l,d){for(var u=this.cfg,f=a.create(u.hasher,l),h=n.create(),g=n.create([1]),p=h.words,I=g.words,m=u.keySize,w=u.iterations;p.length{(function(r,e,t){typeof xh=="object"?FS.exports=xh=e(et(),$x(),Ih()):typeof define=="function"&&define.amd?define(["./core","./sha1","./hmac"],e):e(r.CryptoJS)})(xh,function(r){return(function(){var e=r,t=e.lib,i=t.Base,n=t.WordArray,s=e.algo,o=s.MD5,a=s.EvpKDF=i.extend({cfg:i.extend({keySize:128/32,hasher:o,iterations:1}),init:function(c){this.cfg=this.cfg.extend(c)},compute:function(c,l){for(var d,u=this.cfg,f=u.hasher.create(),h=n.create(),g=h.words,p=u.keySize,I=u.iterations;g.length{(function(r,e,t){typeof Ch=="object"?PS.exports=Ch=e(et(),_s()):typeof define=="function"&&define.amd?define(["./core","./evpkdf"],e):e(r.CryptoJS)})(Ch,function(r){r.lib.Cipher||(function(e){var t=r,i=t.lib,n=i.Base,s=i.WordArray,o=i.BufferedBlockAlgorithm,a=t.enc,c=a.Utf8,l=a.Base64,d=t.algo,u=d.EvpKDF,f=i.Cipher=o.extend({cfg:n.extend(),createEncryptor:function(b,_){return this.create(this._ENC_XFORM_MODE,b,_)},createDecryptor:function(b,_){return this.create(this._DEC_XFORM_MODE,b,_)},init:function(b,_,v){this.cfg=this.cfg.extend(v),this._xformMode=b,this._key=_,this.reset()},reset:function(){o.reset.call(this),this._doReset()},process:function(b){return this._append(b),this._process()},finalize:function(b){b&&this._append(b);var _=this._doFinalize();return _},keySize:128/32,ivSize:128/32,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:(function(){function b(_){return typeof _=="string"?D:R}return function(_){return{encrypt:function(v,E,P){return b(E).encrypt(_,v,E,P)},decrypt:function(v,E,P){return b(E).decrypt(_,v,E,P)}}}})()}),h=i.StreamCipher=f.extend({_doFinalize:function(){var b=this._process(!0);return b},blockSize:1}),g=t.mode={},p=i.BlockCipherMode=n.extend({createEncryptor:function(b,_){return this.Encryptor.create(b,_)},createDecryptor:function(b,_){return this.Decryptor.create(b,_)},init:function(b,_){this._cipher=b,this._iv=_}}),I=g.CBC=(function(){var b=p.extend();b.Encryptor=b.extend({processBlock:function(v,E){var P=this._cipher,N=P.blockSize;_.call(this,v,E,N),P.encryptBlock(v,E),this._prevBlock=v.slice(E,E+N)}}),b.Decryptor=b.extend({processBlock:function(v,E){var P=this._cipher,N=P.blockSize,j=v.slice(E,E+N);P.decryptBlock(v,E),_.call(this,v,E,N),this._prevBlock=j}});function _(v,E,P){var N,j=this._iv;j?(N=j,this._iv=e):N=this._prevBlock;for(var z=0;z>>2]&255;b.sigBytes-=_}},B=i.BlockCipher=f.extend({cfg:f.cfg.extend({mode:I,padding:w}),reset:function(){var b;f.reset.call(this);var _=this.cfg,v=_.iv,E=_.mode;this._xformMode==this._ENC_XFORM_MODE?b=E.createEncryptor:(b=E.createDecryptor,this._minBufferSize=1),this._mode&&this._mode.__creator==b?this._mode.init(this,v&&v.words):(this._mode=b.call(E,this,v&&v.words),this._mode.__creator=b)},_doProcessBlock:function(b,_){this._mode.processBlock(b,_)},_doFinalize:function(){var b,_=this.cfg.padding;return this._xformMode==this._ENC_XFORM_MODE?(_.pad(this._data,this.blockSize),b=this._process(!0)):(b=this._process(!0),_.unpad(b)),b},blockSize:128/32}),C=i.CipherParams=n.extend({init:function(b){this.mixIn(b)},toString:function(b){return(b||this.formatter).stringify(this)}}),x=t.format={},Q=x.OpenSSL={stringify:function(b){var _,v=b.ciphertext,E=b.salt;return E?_=s.create([1398893684,1701076831]).concat(E).concat(v):_=v,_.toString(l)},parse:function(b){var _,v=l.parse(b),E=v.words;return E[0]==1398893684&&E[1]==1701076831&&(_=s.create(E.slice(2,4)),E.splice(0,4),v.sigBytes-=16),C.create({ciphertext:v,salt:_})}},R=i.SerializableCipher=n.extend({cfg:n.extend({format:Q}),encrypt:function(b,_,v,E){E=this.cfg.extend(E);var P=b.createEncryptor(v,E),N=P.finalize(_),j=P.cfg;return C.create({ciphertext:N,key:v,iv:j.iv,algorithm:b,mode:j.mode,padding:j.padding,blockSize:b.blockSize,formatter:E.format})},decrypt:function(b,_,v,E){E=this.cfg.extend(E),_=this._parse(_,E.format);var P=b.createDecryptor(v,E).finalize(_.ciphertext);return P},_parse:function(b,_){return typeof b=="string"?_.parse(b,this):b}}),U=t.kdf={},T=U.OpenSSL={execute:function(b,_,v,E,P){if(E||(E=s.random(64/8)),P)var N=u.create({keySize:_+v,hasher:P}).compute(b,E);else var N=u.create({keySize:_+v}).compute(b,E);var j=s.create(N.words.slice(_),v*4);return N.sigBytes=_*4,C.create({key:N,iv:j,salt:E})}},D=i.PasswordBasedCipher=R.extend({cfg:R.cfg.extend({kdf:T}),encrypt:function(b,_,v,E){E=this.cfg.extend(E);var P=E.kdf.execute(v,b.keySize,b.ivSize,E.salt,E.hasher);E.iv=P.iv;var N=R.encrypt.call(this,b,_,P.key,E);return N.mixIn(P),N},decrypt:function(b,_,v,E){E=this.cfg.extend(E),_=this._parse(_,E.format);var P=E.kdf.execute(v,b.keySize,b.ivSize,_.salt,E.hasher);E.iv=P.iv;var N=R.decrypt.call(this,b,_,P.key,E);return N}})})()})});var US=oe((Eh,TS)=>{(function(r,e,t){typeof Eh=="object"?TS.exports=Eh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Eh,function(r){return r.mode.CFB=(function(){var e=r.lib.BlockCipherMode.extend();e.Encryptor=e.extend({processBlock:function(i,n){var s=this._cipher,o=s.blockSize;t.call(this,i,n,o,s),this._prevBlock=i.slice(n,n+o)}}),e.Decryptor=e.extend({processBlock:function(i,n){var s=this._cipher,o=s.blockSize,a=i.slice(n,n+o);t.call(this,i,n,o,s),this._prevBlock=a}});function t(i,n,s,o){var a,c=this._iv;c?(a=c.slice(0),this._iv=void 0):a=this._prevBlock,o.encryptBlock(a,0);for(var l=0;l{(function(r,e,t){typeof Bh=="object"?RS.exports=Bh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Bh,function(r){return r.mode.CTR=(function(){var e=r.lib.BlockCipherMode.extend(),t=e.Encryptor=e.extend({processBlock:function(i,n){var s=this._cipher,o=s.blockSize,a=this._iv,c=this._counter;a&&(c=this._counter=a.slice(0),this._iv=void 0);var l=c.slice(0);s.encryptBlock(l,0),c[o-1]=c[o-1]+1|0;for(var d=0;d{(function(r,e,t){typeof bh=="object"?qS.exports=bh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(bh,function(r){return r.mode.CTRGladman=(function(){var e=r.lib.BlockCipherMode.extend();function t(s){if((s>>24&255)===255){var o=s>>16&255,a=s>>8&255,c=s&255;o===255?(o=0,a===255?(a=0,c===255?c=0:++c):++a):++o,s=0,s+=o<<16,s+=a<<8,s+=c}else s+=1<<24;return s}function i(s){return(s[0]=t(s[0]))===0&&(s[1]=t(s[1])),s}var n=e.Encryptor=e.extend({processBlock:function(s,o){var a=this._cipher,c=a.blockSize,l=this._iv,d=this._counter;l&&(d=this._counter=l.slice(0),this._iv=void 0),i(d);var u=d.slice(0);a.encryptBlock(u,0);for(var f=0;f{(function(r,e,t){typeof vh=="object"?MS.exports=vh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(vh,function(r){return r.mode.OFB=(function(){var e=r.lib.BlockCipherMode.extend(),t=e.Encryptor=e.extend({processBlock:function(i,n){var s=this._cipher,o=s.blockSize,a=this._iv,c=this._keystream;a&&(c=this._keystream=a.slice(0),this._iv=void 0),s.encryptBlock(c,0);for(var l=0;l{(function(r,e,t){typeof kh=="object"?HS.exports=kh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(kh,function(r){return r.mode.ECB=(function(){var e=r.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(t,i){this._cipher.encryptBlock(t,i)}}),e.Decryptor=e.extend({processBlock:function(t,i){this._cipher.decryptBlock(t,i)}}),e})(),r.mode.ECB})});var zS=oe((Sh,GS)=>{(function(r,e,t){typeof Sh=="object"?GS.exports=Sh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Sh,function(r){return r.pad.AnsiX923={pad:function(e,t){var i=e.sigBytes,n=t*4,s=n-i%n,o=i+s-1;e.clamp(),e.words[o>>>2]|=s<<24-o%4*8,e.sigBytes+=s},unpad:function(e){var t=e.words[e.sigBytes-1>>>2]&255;e.sigBytes-=t}},r.pad.Ansix923})});var jS=oe((Dh,JS)=>{(function(r,e,t){typeof Dh=="object"?JS.exports=Dh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Dh,function(r){return r.pad.Iso10126={pad:function(e,t){var i=t*4,n=i-e.sigBytes%i;e.concat(r.lib.WordArray.random(n-1)).concat(r.lib.WordArray.create([n<<24],1))},unpad:function(e){var t=e.words[e.sigBytes-1>>>2]&255;e.sigBytes-=t}},r.pad.Iso10126})});var VS=oe((Qh,$S)=>{(function(r,e,t){typeof Qh=="object"?$S.exports=Qh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Qh,function(r){return r.pad.Iso97971={pad:function(e,t){e.concat(r.lib.WordArray.create([2147483648],1)),r.pad.ZeroPadding.pad(e,t)},unpad:function(e){r.pad.ZeroPadding.unpad(e),e.sigBytes--}},r.pad.Iso97971})});var WS=oe((_h,YS)=>{(function(r,e,t){typeof _h=="object"?YS.exports=_h=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(_h,function(r){return r.pad.ZeroPadding={pad:function(e,t){var i=t*4;e.clamp(),e.sigBytes+=i-(e.sigBytes%i||i)},unpad:function(e){for(var t=e.words,i=e.sigBytes-1,i=e.sigBytes-1;i>=0;i--)if(t[i>>>2]>>>24-i%4*8&255){e.sigBytes=i+1;break}}},r.pad.ZeroPadding})});var ZS=oe((Fh,XS)=>{(function(r,e,t){typeof Fh=="object"?XS.exports=Fh=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Fh,function(r){return r.pad.NoPadding={pad:function(){},unpad:function(){}},r.pad.NoPadding})});var tD=oe((Ph,eD)=>{(function(r,e,t){typeof Ph=="object"?eD.exports=Ph=e(et(),fr()):typeof define=="function"&&define.amd?define(["./core","./cipher-core"],e):e(r.CryptoJS)})(Ph,function(r){return(function(e){var t=r,i=t.lib,n=i.CipherParams,s=t.enc,o=s.Hex,a=t.format,c=a.Hex={stringify:function(l){return l.ciphertext.toString(o)},parse:function(l){var d=o.parse(l);return n.create({ciphertext:d})}}})(),r.format.Hex})});var iD=oe((Th,rD)=>{(function(r,e,t){typeof Th=="object"?rD.exports=Th=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(Th,function(r){return(function(){var e=r,t=e.lib,i=t.BlockCipher,n=e.algo,s=[],o=[],a=[],c=[],l=[],d=[],u=[],f=[],h=[],g=[];(function(){for(var m=[],w=0;w<256;w++)w<128?m[w]=w<<1:m[w]=w<<1^283;for(var B=0,C=0,w=0;w<256;w++){var x=C^C<<1^C<<2^C<<3^C<<4;x=x>>>8^x&255^99,s[B]=x,o[x]=B;var Q=m[B],R=m[Q],U=m[R],T=m[x]*257^x*16843008;a[B]=T<<24|T>>>8,c[B]=T<<16|T>>>16,l[B]=T<<8|T>>>24,d[B]=T;var T=U*16843009^R*65537^Q*257^B*16843008;u[x]=T<<24|T>>>8,f[x]=T<<16|T>>>16,h[x]=T<<8|T>>>24,g[x]=T,B?(B=Q^m[m[m[U^Q]]],C^=m[m[C]]):B=C=1}})();var p=[0,1,2,4,8,16,32,64,128,27,54],I=n.AES=i.extend({_doReset:function(){var m;if(!(this._nRounds&&this._keyPriorReset===this._key)){for(var w=this._keyPriorReset=this._key,B=w.words,C=w.sigBytes/4,x=this._nRounds=C+6,Q=(x+1)*4,R=this._keySchedule=[],U=0;U6&&U%C==4&&(m=s[m>>>24]<<24|s[m>>>16&255]<<16|s[m>>>8&255]<<8|s[m&255]):(m=m<<8|m>>>24,m=s[m>>>24]<<24|s[m>>>16&255]<<16|s[m>>>8&255]<<8|s[m&255],m^=p[U/C|0]<<24),R[U]=R[U-C]^m);for(var T=this._invKeySchedule=[],D=0;D>>24]]^f[s[m>>>16&255]]^h[s[m>>>8&255]]^g[s[m&255]]}}},encryptBlock:function(m,w){this._doCryptBlock(m,w,this._keySchedule,a,c,l,d,s)},decryptBlock:function(m,w){var B=m[w+1];m[w+1]=m[w+3],m[w+3]=B,this._doCryptBlock(m,w,this._invKeySchedule,u,f,h,g,o);var B=m[w+1];m[w+1]=m[w+3],m[w+3]=B},_doCryptBlock:function(m,w,B,C,x,Q,R,U){for(var T=this._nRounds,D=m[w]^B[0],b=m[w+1]^B[1],_=m[w+2]^B[2],v=m[w+3]^B[3],E=4,P=1;P>>24]^x[b>>>16&255]^Q[_>>>8&255]^R[v&255]^B[E++],j=C[b>>>24]^x[_>>>16&255]^Q[v>>>8&255]^R[D&255]^B[E++],z=C[_>>>24]^x[v>>>16&255]^Q[D>>>8&255]^R[b&255]^B[E++],L=C[v>>>24]^x[D>>>16&255]^Q[b>>>8&255]^R[_&255]^B[E++];D=N,b=j,_=z,v=L}var N=(U[D>>>24]<<24|U[b>>>16&255]<<16|U[_>>>8&255]<<8|U[v&255])^B[E++],j=(U[b>>>24]<<24|U[_>>>16&255]<<16|U[v>>>8&255]<<8|U[D&255])^B[E++],z=(U[_>>>24]<<24|U[v>>>16&255]<<16|U[D>>>8&255]<<8|U[b&255])^B[E++],L=(U[v>>>24]<<24|U[D>>>16&255]<<16|U[b>>>8&255]<<8|U[_&255])^B[E++];m[w]=N,m[w+1]=j,m[w+2]=z,m[w+3]=L},keySize:256/32});e.AES=i._createHelper(I)})(),r.AES})});var sD=oe((Uh,nD)=>{(function(r,e,t){typeof Uh=="object"?nD.exports=Uh=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(Uh,function(r){return(function(){var e=r,t=e.lib,i=t.WordArray,n=t.BlockCipher,s=e.algo,o=[57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4],a=[14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32],c=[1,2,4,6,8,10,12,14,15,17,19,21,23,25,27,28],l=[{0:8421888,268435456:32768,536870912:8421378,805306368:2,1073741824:512,1342177280:8421890,1610612736:8389122,1879048192:8388608,2147483648:514,2415919104:8389120,2684354560:33280,2952790016:8421376,3221225472:32770,3489660928:8388610,3758096384:0,4026531840:33282,134217728:0,402653184:8421890,671088640:33282,939524096:32768,1207959552:8421888,1476395008:512,1744830464:8421378,2013265920:2,2281701376:8389120,2550136832:33280,2818572288:8421376,3087007744:8389122,3355443200:8388610,3623878656:32770,3892314112:514,4160749568:8388608,1:32768,268435457:2,536870913:8421888,805306369:8388608,1073741825:8421378,1342177281:33280,1610612737:512,1879048193:8389122,2147483649:8421890,2415919105:8421376,2684354561:8388610,2952790017:33282,3221225473:514,3489660929:8389120,3758096385:32770,4026531841:0,134217729:8421890,402653185:8421376,671088641:8388608,939524097:512,1207959553:32768,1476395009:8388610,1744830465:2,2013265921:33282,2281701377:32770,2550136833:8389122,2818572289:514,3087007745:8421888,3355443201:8389120,3623878657:0,3892314113:33280,4160749569:8421378},{0:1074282512,16777216:16384,33554432:524288,50331648:1074266128,67108864:1073741840,83886080:1074282496,100663296:1073758208,117440512:16,134217728:540672,150994944:1073758224,167772160:1073741824,184549376:540688,201326592:524304,218103808:0,234881024:16400,251658240:1074266112,8388608:1073758208,25165824:540688,41943040:16,58720256:1073758224,75497472:1074282512,92274688:1073741824,109051904:524288,125829120:1074266128,142606336:524304,159383552:0,176160768:16384,192937984:1074266112,209715200:1073741840,226492416:540672,243269632:1074282496,260046848:16400,268435456:0,285212672:1074266128,301989888:1073758224,318767104:1074282496,335544320:1074266112,352321536:16,369098752:540688,385875968:16384,402653184:16400,419430400:524288,436207616:524304,452984832:1073741840,469762048:540672,486539264:1073758208,503316480:1073741824,520093696:1074282512,276824064:540688,293601280:524288,310378496:1074266112,327155712:16384,343932928:1073758208,360710144:1074282512,377487360:16,394264576:1073741824,411041792:1074282496,427819008:1073741840,444596224:1073758224,461373440:524304,478150656:0,494927872:16400,511705088:1074266128,528482304:540672},{0:260,1048576:0,2097152:67109120,3145728:65796,4194304:65540,5242880:67108868,6291456:67174660,7340032:67174400,8388608:67108864,9437184:67174656,10485760:65792,11534336:67174404,12582912:67109124,13631488:65536,14680064:4,15728640:256,524288:67174656,1572864:67174404,2621440:0,3670016:67109120,4718592:67108868,5767168:65536,6815744:65540,7864320:260,8912896:4,9961472:256,11010048:67174400,12058624:65796,13107200:65792,14155776:67109124,15204352:67174660,16252928:67108864,16777216:67174656,17825792:65540,18874368:65536,19922944:67109120,20971520:256,22020096:67174660,23068672:67108868,24117248:0,25165824:67109124,26214400:67108864,27262976:4,28311552:65792,29360128:67174400,30408704:260,31457280:65796,32505856:67174404,17301504:67108864,18350080:260,19398656:67174656,20447232:0,21495808:65540,22544384:67109120,23592960:256,24641536:67174404,25690112:65536,26738688:67174660,27787264:65796,28835840:67108868,29884416:67109124,30932992:67174400,31981568:4,33030144:65792},{0:2151682048,65536:2147487808,131072:4198464,196608:2151677952,262144:0,327680:4198400,393216:2147483712,458752:4194368,524288:2147483648,589824:4194304,655360:64,720896:2147487744,786432:2151678016,851968:4160,917504:4096,983040:2151682112,32768:2147487808,98304:64,163840:2151678016,229376:2147487744,294912:4198400,360448:2151682112,425984:0,491520:2151677952,557056:4096,622592:2151682048,688128:4194304,753664:4160,819200:2147483648,884736:4194368,950272:4198464,1015808:2147483712,1048576:4194368,1114112:4198400,1179648:2147483712,1245184:0,1310720:4160,1376256:2151678016,1441792:2151682048,1507328:2147487808,1572864:2151682112,1638400:2147483648,1703936:2151677952,1769472:4198464,1835008:2147487744,1900544:4194304,1966080:64,2031616:4096,1081344:2151677952,1146880:2151682112,1212416:0,1277952:4198400,1343488:4194368,1409024:2147483648,1474560:2147487808,1540096:64,1605632:2147483712,1671168:4096,1736704:2147487744,1802240:2151678016,1867776:4160,1933312:2151682048,1998848:4194304,2064384:4198464},{0:128,4096:17039360,8192:262144,12288:536870912,16384:537133184,20480:16777344,24576:553648256,28672:262272,32768:16777216,36864:537133056,40960:536871040,45056:553910400,49152:553910272,53248:0,57344:17039488,61440:553648128,2048:17039488,6144:553648256,10240:128,14336:17039360,18432:262144,22528:537133184,26624:553910272,30720:536870912,34816:537133056,38912:0,43008:553910400,47104:16777344,51200:536871040,55296:553648128,59392:16777216,63488:262272,65536:262144,69632:128,73728:536870912,77824:553648256,81920:16777344,86016:553910272,90112:537133184,94208:16777216,98304:553910400,102400:553648128,106496:17039360,110592:537133056,114688:262272,118784:536871040,122880:0,126976:17039488,67584:553648256,71680:16777216,75776:17039360,79872:537133184,83968:536870912,88064:17039488,92160:128,96256:553910272,100352:262272,104448:553910400,108544:0,112640:553648128,116736:16777344,120832:262144,124928:537133056,129024:536871040},{0:268435464,256:8192,512:270532608,768:270540808,1024:268443648,1280:2097152,1536:2097160,1792:268435456,2048:0,2304:268443656,2560:2105344,2816:8,3072:270532616,3328:2105352,3584:8200,3840:270540800,128:270532608,384:270540808,640:8,896:2097152,1152:2105352,1408:268435464,1664:268443648,1920:8200,2176:2097160,2432:8192,2688:268443656,2944:270532616,3200:0,3456:270540800,3712:2105344,3968:268435456,4096:268443648,4352:270532616,4608:270540808,4864:8200,5120:2097152,5376:268435456,5632:268435464,5888:2105344,6144:2105352,6400:0,6656:8,6912:270532608,7168:8192,7424:268443656,7680:270540800,7936:2097160,4224:8,4480:2105344,4736:2097152,4992:268435464,5248:268443648,5504:8200,5760:270540808,6016:270532608,6272:270540800,6528:270532616,6784:8192,7040:2105352,7296:2097160,7552:0,7808:268435456,8064:268443656},{0:1048576,16:33555457,32:1024,48:1049601,64:34604033,80:0,96:1,112:34603009,128:33555456,144:1048577,160:33554433,176:34604032,192:34603008,208:1025,224:1049600,240:33554432,8:34603009,24:0,40:33555457,56:34604032,72:1048576,88:33554433,104:33554432,120:1025,136:1049601,152:33555456,168:34603008,184:1048577,200:1024,216:34604033,232:1,248:1049600,256:33554432,272:1048576,288:33555457,304:34603009,320:1048577,336:33555456,352:34604032,368:1049601,384:1025,400:34604033,416:1049600,432:1,448:0,464:34603008,480:33554433,496:1024,264:1049600,280:33555457,296:34603009,312:1,328:33554432,344:1048576,360:1025,376:34604032,392:33554433,408:34603008,424:0,440:34604033,456:1049601,472:1024,488:33555456,504:1048577},{0:134219808,1:131072,2:134217728,3:32,4:131104,5:134350880,6:134350848,7:2048,8:134348800,9:134219776,10:133120,11:134348832,12:2080,13:0,14:134217760,15:133152,2147483648:2048,2147483649:134350880,2147483650:134219808,2147483651:134217728,2147483652:134348800,2147483653:133120,2147483654:133152,2147483655:32,2147483656:134217760,2147483657:2080,2147483658:131104,2147483659:134350848,2147483660:0,2147483661:134348832,2147483662:134219776,2147483663:131072,16:133152,17:134350848,18:32,19:2048,20:134219776,21:134217760,22:134348832,23:131072,24:0,25:131104,26:134348800,27:134219808,28:134350880,29:133120,30:2080,31:134217728,2147483664:131072,2147483665:2048,2147483666:134348832,2147483667:133152,2147483668:32,2147483669:134348800,2147483670:134217728,2147483671:134219808,2147483672:134350880,2147483673:134217760,2147483674:134219776,2147483675:0,2147483676:133120,2147483677:2080,2147483678:131104,2147483679:134350848}],d=[4160749569,528482304,33030144,2064384,129024,8064,504,2147483679],u=s.DES=n.extend({_doReset:function(){for(var p=this._key,I=p.words,m=[],w=0;w<56;w++){var B=o[w]-1;m[w]=I[B>>>5]>>>31-B%32&1}for(var C=this._subKeys=[],x=0;x<16;x++){for(var Q=C[x]=[],R=c[x],w=0;w<24;w++)Q[w/6|0]|=m[(a[w]-1+R)%28]<<31-w%6,Q[4+(w/6|0)]|=m[28+(a[w+24]-1+R)%28]<<31-w%6;Q[0]=Q[0]<<1|Q[0]>>>31;for(var w=1;w<7;w++)Q[w]=Q[w]>>>(w-1)*4+3;Q[7]=Q[7]<<5|Q[7]>>>27}for(var U=this._invSubKeys=[],w=0;w<16;w++)U[w]=C[15-w]},encryptBlock:function(p,I){this._doCryptBlock(p,I,this._subKeys)},decryptBlock:function(p,I){this._doCryptBlock(p,I,this._invSubKeys)},_doCryptBlock:function(p,I,m){this._lBlock=p[I],this._rBlock=p[I+1],f.call(this,4,252645135),f.call(this,16,65535),h.call(this,2,858993459),h.call(this,8,16711935),f.call(this,1,1431655765);for(var w=0;w<16;w++){for(var B=m[w],C=this._lBlock,x=this._rBlock,Q=0,R=0;R<8;R++)Q|=l[R][((x^B[R])&d[R])>>>0];this._lBlock=x,this._rBlock=C^Q}var U=this._lBlock;this._lBlock=this._rBlock,this._rBlock=U,f.call(this,1,1431655765),h.call(this,8,16711935),h.call(this,2,858993459),f.call(this,16,65535),f.call(this,4,252645135),p[I]=this._lBlock,p[I+1]=this._rBlock},keySize:64/32,ivSize:64/32,blockSize:64/32});function f(p,I){var m=(this._lBlock>>>p^this._rBlock)&I;this._rBlock^=m,this._lBlock^=m<>>p^this._lBlock)&I;this._lBlock^=m,this._rBlock^=m<192.");var m=I.slice(0,2),w=I.length<4?I.slice(0,2):I.slice(2,4),B=I.length<6?I.slice(0,2):I.slice(4,6);this._des1=u.createEncryptor(i.create(m)),this._des2=u.createEncryptor(i.create(w)),this._des3=u.createEncryptor(i.create(B))},encryptBlock:function(p,I){this._des1.encryptBlock(p,I),this._des2.decryptBlock(p,I),this._des3.encryptBlock(p,I)},decryptBlock:function(p,I){this._des3.decryptBlock(p,I),this._des2.encryptBlock(p,I),this._des1.decryptBlock(p,I)},keySize:192/32,ivSize:64/32,blockSize:64/32});e.TripleDES=n._createHelper(g)})(),r.TripleDES})});var aD=oe((Rh,oD)=>{(function(r,e,t){typeof Rh=="object"?oD.exports=Rh=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(Rh,function(r){return(function(){var e=r,t=e.lib,i=t.StreamCipher,n=e.algo,s=n.RC4=i.extend({_doReset:function(){for(var c=this._key,l=c.words,d=c.sigBytes,u=this._S=[],f=0;f<256;f++)u[f]=f;for(var f=0,h=0;f<256;f++){var g=f%d,p=l[g>>>2]>>>24-g%4*8&255;h=(h+u[f]+p)%256;var I=u[f];u[f]=u[h],u[h]=I}this._i=this._j=0},_doProcessBlock:function(c,l){c[l]^=o.call(this)},keySize:256/32,ivSize:0});function o(){for(var c=this._S,l=this._i,d=this._j,u=0,f=0;f<4;f++){l=(l+1)%256,d=(d+c[l])%256;var h=c[l];c[l]=c[d],c[d]=h,u|=c[(c[l]+c[d])%256]<<24-f*8}return this._i=l,this._j=d,u}e.RC4=i._createHelper(s);var a=n.RC4Drop=s.extend({cfg:s.cfg.extend({drop:192}),_doReset:function(){s._doReset.call(this);for(var c=this.cfg.drop;c>0;c--)o.call(this)}});e.RC4Drop=i._createHelper(a)})(),r.RC4})});var lD=oe((Nh,cD)=>{(function(r,e,t){typeof Nh=="object"?cD.exports=Nh=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(Nh,function(r){return(function(){var e=r,t=e.lib,i=t.StreamCipher,n=e.algo,s=[],o=[],a=[],c=n.Rabbit=i.extend({_doReset:function(){for(var d=this._key.words,u=this.cfg.iv,f=0;f<4;f++)d[f]=(d[f]<<8|d[f]>>>24)&16711935|(d[f]<<24|d[f]>>>8)&4278255360;var h=this._X=[d[0],d[3]<<16|d[2]>>>16,d[1],d[0]<<16|d[3]>>>16,d[2],d[1]<<16|d[0]>>>16,d[3],d[2]<<16|d[1]>>>16],g=this._C=[d[2]<<16|d[2]>>>16,d[0]&4294901760|d[1]&65535,d[3]<<16|d[3]>>>16,d[1]&4294901760|d[2]&65535,d[0]<<16|d[0]>>>16,d[2]&4294901760|d[3]&65535,d[1]<<16|d[1]>>>16,d[3]&4294901760|d[0]&65535];this._b=0;for(var f=0;f<4;f++)l.call(this);for(var f=0;f<8;f++)g[f]^=h[f+4&7];if(u){var p=u.words,I=p[0],m=p[1],w=(I<<8|I>>>24)&16711935|(I<<24|I>>>8)&4278255360,B=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360,C=w>>>16|B&4294901760,x=B<<16|w&65535;g[0]^=w,g[1]^=C,g[2]^=B,g[3]^=x,g[4]^=w,g[5]^=C,g[6]^=B,g[7]^=x;for(var f=0;f<4;f++)l.call(this)}},_doProcessBlock:function(d,u){var f=this._X;l.call(this),s[0]=f[0]^f[5]>>>16^f[3]<<16,s[1]=f[2]^f[7]>>>16^f[5]<<16,s[2]=f[4]^f[1]>>>16^f[7]<<16,s[3]=f[6]^f[3]>>>16^f[1]<<16;for(var h=0;h<4;h++)s[h]=(s[h]<<8|s[h]>>>24)&16711935|(s[h]<<24|s[h]>>>8)&4278255360,d[u+h]^=s[h]},blockSize:128/32,ivSize:64/32});function l(){for(var d=this._X,u=this._C,f=0;f<8;f++)o[f]=u[f];u[0]=u[0]+1295307597+this._b|0,u[1]=u[1]+3545052371+(u[0]>>>0>>0?1:0)|0,u[2]=u[2]+886263092+(u[1]>>>0>>0?1:0)|0,u[3]=u[3]+1295307597+(u[2]>>>0>>0?1:0)|0,u[4]=u[4]+3545052371+(u[3]>>>0>>0?1:0)|0,u[5]=u[5]+886263092+(u[4]>>>0>>0?1:0)|0,u[6]=u[6]+1295307597+(u[5]>>>0>>0?1:0)|0,u[7]=u[7]+3545052371+(u[6]>>>0>>0?1:0)|0,this._b=u[7]>>>0>>0?1:0;for(var f=0;f<8;f++){var h=d[f]+u[f],g=h&65535,p=h>>>16,I=((g*g>>>17)+g*p>>>15)+p*p,m=((h&4294901760)*h|0)+((h&65535)*h|0);a[f]=I^m}d[0]=a[0]+(a[7]<<16|a[7]>>>16)+(a[6]<<16|a[6]>>>16)|0,d[1]=a[1]+(a[0]<<8|a[0]>>>24)+a[7]|0,d[2]=a[2]+(a[1]<<16|a[1]>>>16)+(a[0]<<16|a[0]>>>16)|0,d[3]=a[3]+(a[2]<<8|a[2]>>>24)+a[1]|0,d[4]=a[4]+(a[3]<<16|a[3]>>>16)+(a[2]<<16|a[2]>>>16)|0,d[5]=a[5]+(a[4]<<8|a[4]>>>24)+a[3]|0,d[6]=a[6]+(a[5]<<16|a[5]>>>16)+(a[4]<<16|a[4]>>>16)|0,d[7]=a[7]+(a[6]<<8|a[6]>>>24)+a[5]|0}e.Rabbit=i._createHelper(c)})(),r.Rabbit})});var uD=oe((qh,dD)=>{(function(r,e,t){typeof qh=="object"?dD.exports=qh=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(qh,function(r){return(function(){var e=r,t=e.lib,i=t.StreamCipher,n=e.algo,s=[],o=[],a=[],c=n.RabbitLegacy=i.extend({_doReset:function(){var d=this._key.words,u=this.cfg.iv,f=this._X=[d[0],d[3]<<16|d[2]>>>16,d[1],d[0]<<16|d[3]>>>16,d[2],d[1]<<16|d[0]>>>16,d[3],d[2]<<16|d[1]>>>16],h=this._C=[d[2]<<16|d[2]>>>16,d[0]&4294901760|d[1]&65535,d[3]<<16|d[3]>>>16,d[1]&4294901760|d[2]&65535,d[0]<<16|d[0]>>>16,d[2]&4294901760|d[3]&65535,d[1]<<16|d[1]>>>16,d[3]&4294901760|d[0]&65535];this._b=0;for(var g=0;g<4;g++)l.call(this);for(var g=0;g<8;g++)h[g]^=f[g+4&7];if(u){var p=u.words,I=p[0],m=p[1],w=(I<<8|I>>>24)&16711935|(I<<24|I>>>8)&4278255360,B=(m<<8|m>>>24)&16711935|(m<<24|m>>>8)&4278255360,C=w>>>16|B&4294901760,x=B<<16|w&65535;h[0]^=w,h[1]^=C,h[2]^=B,h[3]^=x,h[4]^=w,h[5]^=C,h[6]^=B,h[7]^=x;for(var g=0;g<4;g++)l.call(this)}},_doProcessBlock:function(d,u){var f=this._X;l.call(this),s[0]=f[0]^f[5]>>>16^f[3]<<16,s[1]=f[2]^f[7]>>>16^f[5]<<16,s[2]=f[4]^f[1]>>>16^f[7]<<16,s[3]=f[6]^f[3]>>>16^f[1]<<16;for(var h=0;h<4;h++)s[h]=(s[h]<<8|s[h]>>>24)&16711935|(s[h]<<24|s[h]>>>8)&4278255360,d[u+h]^=s[h]},blockSize:128/32,ivSize:64/32});function l(){for(var d=this._X,u=this._C,f=0;f<8;f++)o[f]=u[f];u[0]=u[0]+1295307597+this._b|0,u[1]=u[1]+3545052371+(u[0]>>>0>>0?1:0)|0,u[2]=u[2]+886263092+(u[1]>>>0>>0?1:0)|0,u[3]=u[3]+1295307597+(u[2]>>>0>>0?1:0)|0,u[4]=u[4]+3545052371+(u[3]>>>0>>0?1:0)|0,u[5]=u[5]+886263092+(u[4]>>>0>>0?1:0)|0,u[6]=u[6]+1295307597+(u[5]>>>0>>0?1:0)|0,u[7]=u[7]+3545052371+(u[6]>>>0>>0?1:0)|0,this._b=u[7]>>>0>>0?1:0;for(var f=0;f<8;f++){var h=d[f]+u[f],g=h&65535,p=h>>>16,I=((g*g>>>17)+g*p>>>15)+p*p,m=((h&4294901760)*h|0)+((h&65535)*h|0);a[f]=I^m}d[0]=a[0]+(a[7]<<16|a[7]>>>16)+(a[6]<<16|a[6]>>>16)|0,d[1]=a[1]+(a[0]<<8|a[0]>>>24)+a[7]|0,d[2]=a[2]+(a[1]<<16|a[1]>>>16)+(a[0]<<16|a[0]>>>16)|0,d[3]=a[3]+(a[2]<<8|a[2]>>>24)+a[1]|0,d[4]=a[4]+(a[3]<<16|a[3]>>>16)+(a[2]<<16|a[2]>>>16)|0,d[5]=a[5]+(a[4]<<8|a[4]>>>24)+a[3]|0,d[6]=a[6]+(a[5]<<16|a[5]>>>16)+(a[4]<<16|a[4]>>>16)|0,d[7]=a[7]+(a[6]<<8|a[6]>>>24)+a[5]|0}e.RabbitLegacy=i._createHelper(c)})(),r.RabbitLegacy})});var AD=oe((Oh,fD)=>{(function(r,e,t){typeof Oh=="object"?fD.exports=Oh=e(et(),No(),qo(),_s(),fr()):typeof define=="function"&&define.amd?define(["./core","./enc-base64","./md5","./evpkdf","./cipher-core"],e):e(r.CryptoJS)})(Oh,function(r){return(function(){var e=r,t=e.lib,i=t.BlockCipher,n=e.algo;let s=16,o=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],a=[[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592,3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370,48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946],[1266315497,3048417604,3681880366,3289982499,290971e4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509,1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880,613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303,2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385,1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030,4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055],[3913112168,2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499,499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905,3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651,309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610,1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037,2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,3720792119,3617206836,2455994898,1729034894,1080033504],[976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200,2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241,3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891,3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,2941484381,1077988104,1320477388,886195818,18198404,3786409e3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588,3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493,1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462]];var c={pbox:[],sbox:[]};function l(g,p){let I=p>>24&255,m=p>>16&255,w=p>>8&255,B=p&255,C=g.sbox[0][I]+g.sbox[1][m];return C=C^g.sbox[2][w],C=C+g.sbox[3][B],C}function d(g,p,I){let m=p,w=I,B;for(let C=0;C1;--C)m=m^g.pbox[C],w=l(g,m)^w,B=m,m=w,w=B;return B=m,m=w,w=B,w=w^g.pbox[1],m=m^g.pbox[0],{left:m,right:w}}function f(g,p,I){for(let x=0;x<4;x++){g.sbox[x]=[];for(let Q=0;Q<256;Q++)g.sbox[x][Q]=a[x][Q]}let m=0;for(let x=0;x=I&&(m=0);let w=0,B=0,C=0;for(let x=0;x{(function(r,e,t){typeof Mh=="object"?hD.exports=Mh=e(et(),Uu(),uS(),AS(),No(),pS(),qo(),$x(),fh(),xS(),Vx(),BS(),vS(),SS(),Ih(),_S(),_s(),fr(),US(),NS(),OS(),LS(),KS(),zS(),jS(),VS(),WS(),ZS(),tD(),iD(),sD(),aD(),lD(),uD(),AD()):typeof define=="function"&&define.amd?define(["./core","./x64-core","./lib-typedarrays","./enc-utf16","./enc-base64","./enc-base64url","./md5","./sha1","./sha256","./sha224","./sha512","./sha384","./sha3","./ripemd160","./hmac","./pbkdf2","./evpkdf","./cipher-core","./mode-cfb","./mode-ctr","./mode-ctr-gladman","./mode-ofb","./mode-ecb","./pad-ansix923","./pad-iso10126","./pad-iso97971","./pad-zeropadding","./pad-nopadding","./format-hex","./aes","./tripledes","./rc4","./rabbit","./rabbit-legacy","./blowfish"],e):r.CryptoJS=e(r.CryptoJS)})(Mh,function(r){return r})});function Sl(r){let e=[];for(let t=0;t>>2]|=r[t]<<24-t%4*8;return Mo.default.lib.WordArray.create(e,r.length)}function gD(r){let e=Number(r?.sigBytes??0),t=r?.words??[],i=new Uint8Array(e);for(let n=0;n>>2]>>>24-n%4*8&255;return i}function mD(r,e,t){if(t.length!==pD)throw new Error("DES IV must be 8 bytes");let i=Mo.default.TripleDES.encrypt(Sl(r),Sl(e),{iv:Sl(t),mode:Mo.default.mode.CBC,padding:Mo.default.pad.Pkcs7});return gD(i.ciphertext)}function yD(r,e,t){if(t.length!==pD)throw new Error("DES IV must be 8 bytes");let i=Mo.default.TripleDES.decrypt({ciphertext:Sl(r)},Sl(e),{iv:Sl(t),mode:Mo.default.mode.CBC,padding:Mo.default.pad.Pkcs7});return gD(i)}var Mo,pD,ID=J(()=>{"use strict";Mo=xr(Oo(),1);pD=8});function Fs(r){return Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,"0")).join("")}function Ar(r){return typeof r=="string"?new TextEncoder().encode(r):r}function gi(r){let e=new Uint8Array(r.length/2);for(let t=0;t>>0,!0),o.setUint32(s.length-4,Math.floor(i/4294967296),!0);let a=new Int32Array(64);for(let g=0;g<64;g++)a[g]=Math.abs(Math.sin(g+1))*4294967296|0;let c=[7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21],l=1732584193,d=4023233417,u=2562383102,f=271733878;for(let g=0;gp.getInt32(Q*4,!0)),[m,w,B,C]=[l,d,u,f];for(let x=0;x<64;x++){let Q,R;x<16?(Q=w&B|~w&C,R=x):x<32?(Q=C&w|~C&B,R=(5*x+1)%16):x<48?(Q=w^B^C,R=(3*x+5)%16):(Q=B^(w|~C),R=7*x%16);let U=C;C=B,B=w;let T=m+Q+a[x]+I[R]|0;w=w+(T<>>32-c[x])|0,m=U}l=l+m|0,d=d+w|0,u=u+B|0,f=f+C|0}let h=new DataView(new ArrayBuffer(16));return h.setInt32(0,l,!0),h.setInt32(4,d,!0),h.setInt32(8,u,!0),h.setInt32(12,f,!0),Fs(h.buffer)}function $t(r){return xK(r)}async function Ho(r){let e=await crypto.subtle.digest("SHA-1",Ar(r));return Fs(e)}async function BD(r){let e=await crypto.subtle.digest("SHA-256",Ar(r));return Fs(e)}async function Wx(r,e){let t=await crypto.subtle.importKey("raw",Ar(e),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,Ar(r));return Fs(i)}async function qu(r,e){let t=await crypto.subtle.importKey("raw",Ar(e),{name:"HMAC",hash:"SHA-1"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,Ar(r)),n=new Uint8Array(i),s="";for(let o of n)s+=String.fromCharCode(o);return btoa(s)}async function bD(r,e="salt",t=Xx){let i=Ar(r),n=Ar(e),s=await crypto.subtle.importKey("raw",i,"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:n,iterations:t,hash:"SHA-256"},s,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function CK(r,e){let t=crypto.getRandomValues(new Uint8Array(16)),i=crypto.getRandomValues(new Uint8Array(12)),n=await bD(e,t,Xx),s=await crypto.subtle.encrypt({name:"AES-GCM",iv:i},n,Ar(r));return`${Fs(t.buffer)}:${Fs(i.buffer)}:${Fs(s)}`}async function EK(r,e){let t=r.split(":"),i="salt",n="",s="",o=1;if(t.length===3)i=gi(t[0]),n=t[1],s=t[2],o=Xx;else if(t.length===2)n=t[0],s=t[1],o=1,wD||(wD=!0,console.warn("[Crypto] Decrypting legacy weak-KDF format. It will be re-encrypted with the current config format on the next save."));else throw new Error("Invalid encrypted data format");let a=gi(n),c=gi(s),l=await bD(e,i,o),d=await crypto.subtle.decrypt({name:"AES-GCM",iv:a},l,c);return new TextDecoder().decode(d)}async function BK(r){let e=await crypto.subtle.importKey("raw",Ar(r),"HKDF",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:Ar("openlist-config-encryption-v2"),info:Ar("AES-256-GCM")},e,{name:"AES-GCM",length:256},!1,["encrypt","decrypt"])}async function bK(r,e){let t=crypto.getRandomValues(new Uint8Array(12)),i=await crypto.subtle.encrypt({name:"AES-GCM",iv:t},e,Ar(r));return`${Fs(t.buffer)}:${Fs(i)}`}async function vK(r,e){let t=r.split(":");if(t.length!==2)throw new Error("Invalid config encrypted data format");let i=gi(t[0]),n=gi(t[1]),s=await crypto.subtle.decrypt({name:"AES-GCM",iv:i},e,n);return new TextDecoder().decode(s)}async function Zx(r,e,t){let i=Ar(e),n=t?Ar(t):i,s=await crypto.subtle.importKey("raw",i,{name:"AES-CBC"},!1,["encrypt"]),o=Ar(r),c=typeof process<"u"&&process?.release?.name==="node"?o:PD(o),l=await crypto.subtle.encrypt({name:"AES-CBC",iv:n},s,c),d=new Uint8Array(l),u="";for(let f of d)u+=String.fromCharCode(f);return btoa(u)}function kD(r){let e=String(r??"").trim().toLowerCase();if(!e)return{cipher:Nu,known:!0};let t=SK[e];return t?{cipher:t,known:!0}:{cipher:Nu,known:!1}}function SD(r){return r==="none"?"":`enc:v${DK[r]}:`}function Ko(r){if(typeof r!="string")return null;let e=_K.exec(r);if(!e)return null;let t=QK[Number(e[1])];return t?{cipher:t,prefix:e[0]}:null}function lc(r){return Ko(r)!==null}function Kn(r){let e="";for(let t of r)e+=t.toString(16).padStart(2,"0");return e}function Ou(r,e){let t=new Uint8Array(r.length+e.length);return t.set(r,0),t.set(e,r.length),t}function DD(r,e){if(r.length!==e.length)return!1;let t=0;for(let i=0;i=TK){let s=cc.keys().next().value;s!==void 0&&cc.delete(s)}return cc.set(i,n),n}function QD(){cc.clear(),Yx=!1}async function _D(r){let e=await crypto.subtle.digest("SHA-256",Ar(r));return new Uint8Array(e)}async function Ru(r,e,t){return(await _D(`${e}|${r}`)).slice(0,t)}async function xD(r){return crypto.subtle.importKey("raw",r,{name:"HMAC",hash:"SHA-256"},!1,["sign"])}async function Lh(r,e){return new Uint8Array(await crypto.subtle.sign("HMAC",r,e))}function UK(r){let e=new Uint8Array(4);return new DataView(e.buffer).setUint32(0,r.length,!1),Ou(e,r)}function RK(r){if(r.length<4)throw new Error("Invalid encrypted data format");let e=new DataView(r.buffer,r.byteOffset,4).getUint32(0,!1);if(e>r.length-4)throw new Error("Invalid encrypted data length");return r.slice(4,4+e)}async function NK(r,e){let t=await crypto.subtle.digest("SHA-256",Ar(`${e}|${r}`));return crypto.subtle.importKey("raw",t,{name:"AES-CBC"},!1,["encrypt","decrypt"])}async function qK(r,e){let t=await crypto.subtle.digest("SHA-256",Ar(`${e}|${r}`));return crypto.subtle.importKey("raw",t,{name:"HMAC",hash:"SHA-256"},!1,["sign"])}async function OK(r,e){let t=crypto.getRandomValues(new Uint8Array(16)),i=PD(UK(eC(r))),n=await crypto.subtle.encrypt({name:"AES-CBC",iv:t},e.enc,i),s=new Uint8Array(n),o=await Lh(e.mac,Ou(t,s));return`${Kn(t)}:${Kn(s)}:${Kn(o)}`}async function MK(r,e){let t=r.split(":");if(t.length!==3)throw new Error("Invalid encrypted data format");let i=gi(t[0]),n=gi(t[1]),s=gi(t[2]),o=await Lh(e.mac,Ou(i,n));if(!DD(o,s))throw new Error("Encrypted data failed integrity check (wrong key?)");let a=await crypto.subtle.decrypt({name:"AES-CBC",iv:i},e.enc,n);return new TextDecoder().decode(RK(new Uint8Array(a)))}async function HK(r,e){let t=crypto.getRandomValues(new Uint8Array(12)),{ciphertext:i,tag:n}=sS(e,t,eC(r));return`${Kn(t)}:${Kn(i)}:${Kn(n)}`}async function KK(r,e){let t=r.split(":");if(t.length!==3)throw new Error("Invalid encrypted data format");let i=gi(t[0]),n=gi(t[1]),s=gi(t[2]);return new TextDecoder().decode(oS(e,i,n,s))}async function CD(r,e,t){let i=crypto.getRandomValues(new Uint8Array(8)),n=mD(eC(r),e,i),s=await Lh(t,Ou(i,n));return`${Kn(i)}:${Kn(n)}:${Kn(s)}`}async function ED(r,e,t){let i=r.split(":");if(i.length!==3)throw new Error("Invalid encrypted data format");let n=gi(i[0]),s=gi(i[1]),o=gi(i[2]),a=await Lh(t,Ou(n,s));if(!DD(a,o))throw new Error("Encrypted data failed integrity check (wrong key?)");return new TextDecoder().decode(yD(s,e,n))}async function FD(r,e){r!=="none"&&kK.includes(r)&&!Yx&&(Yx=!0,console.warn(`[Crypto] DB_CIPHER=${r} is for compatibility only: single DES (56-bit) is brute-forceable and 3DES is deprecated (NIST SP 800-131A). Use aes-256-gcm or chacha20-poly1305 for real data.`));let t=r==="none"?"none":await Lo(`fp:${r}`,e,async()=>{let d=await _D(`openlist-db-cipher-fp|${r}|${e}`);return Kn(d).slice(0,16)}),i=()=>Lo("v2",e,()=>BK(e)),n=()=>Lo("v3",e,async()=>({enc:await NK(e,FK),mac:await qK(e,PK)})),s=()=>Lo("v4",e,()=>Ru(e,LK,32)),o=()=>Lo("v5",e,()=>Ru(e,GK,8)),a=()=>Lo("v6",e,()=>Ru(e,zK,24)),c=()=>Lo("v5-mac",e,async()=>xD(await Ru(e,JK,32))),l=()=>Lo("v6-mac",e,async()=>xD(await Ru(e,jK,32)));return{cipher:r,fingerprint:t,async encrypt(d){switch(r){case"none":return d;case"aes-256-gcm":return await bK(d,await i());case"aes-256-gcm-pbkdf2":return await CK(d,e);case"aes-256-cbc-hmac":return await OK(d,await n());case"chacha20-poly1305":return await HK(d,await s());case"des-cbc-hmac":return await CD(d,await o(),await c());case"3des-cbc-hmac":return await CD(d,await a(),await l());default:throw new Error(`Unsupported DB_CIPHER: ${String(r)}`)}},async decrypt(d){let u=Ko(d);if(!u)return d;let f=d.slice(u.prefix.length);switch(u.cipher){case"aes-256-gcm":return await vK(f,await i());case"aes-256-gcm-pbkdf2":return await EK(f,e);case"aes-256-cbc-hmac":return await MK(f,await n());case"chacha20-poly1305":return await KK(f,await s());case"des-cbc-hmac":return await ED(f,await o(),await c());case"3des-cbc-hmac":return await ED(f,await a(),await l());default:throw new Error(`Unsupported ciphertext: ${String(u.cipher)}`)}}}}function PD(r,e=16){let t=e-r.length%e,i=new Uint8Array(r.length+t);return i.set(r),i.fill(t,r.length),i}var Xx,wD,Nu,vD,kK,SK,DK,QK,_K,FK,PK,TK,cc,Yx,LK,GK,zK,JK,jK,Gr=J(()=>{"use strict";aS();ID();Xx=1e5,wD=!1;Nu="none",vD=["none","aes-256-gcm","aes-256-gcm-pbkdf2","aes-256-cbc-hmac","chacha20-poly1305","des-cbc-hmac","3des-cbc-hmac"],kK=["des-cbc-hmac","3des-cbc-hmac"],SK={none:"none",off:"none",no:"none",plain:"none",plaintext:"none","aes-256-gcm":"aes-256-gcm","aes-gcm":"aes-256-gcm",aesgcm:"aes-256-gcm","aes-256-gcm-hkdf":"aes-256-gcm","gcm-hkdf":"aes-256-gcm",hkdf:"aes-256-gcm",gcm:"aes-256-gcm",v2:"aes-256-gcm","aes-256-gcm-pbkdf2":"aes-256-gcm-pbkdf2","aes-gcm-pbkdf2":"aes-256-gcm-pbkdf2",pbkdf2:"aes-256-gcm-pbkdf2",legacy:"aes-256-gcm-pbkdf2",v1:"aes-256-gcm-pbkdf2","aes-256-cbc-hmac":"aes-256-cbc-hmac","aes-cbc-hmac":"aes-256-cbc-hmac","cbc-hmac":"aes-256-cbc-hmac","aes-256-cbc":"aes-256-cbc-hmac",cbc:"aes-256-cbc-hmac",v3:"aes-256-cbc-hmac","chacha20-poly1305":"chacha20-poly1305",chacha20:"chacha20-poly1305",chacha:"chacha20-poly1305","chacha20-ietf-poly1305":"chacha20-poly1305","aead-chacha20":"chacha20-poly1305",v4:"chacha20-poly1305","des-cbc-hmac":"des-cbc-hmac","des-cbc":"des-cbc-hmac",des:"des-cbc-hmac",v5:"des-cbc-hmac","3des-cbc-hmac":"3des-cbc-hmac","3des-cbc":"3des-cbc-hmac","3des":"3des-cbc-hmac",des3:"3des-cbc-hmac",tripledes:"3des-cbc-hmac","triple-des":"3des-cbc-hmac",v6:"3des-cbc-hmac"};DK={"aes-256-gcm-pbkdf2":1,"aes-256-gcm":2,"aes-256-cbc-hmac":3,"chacha20-poly1305":4,"des-cbc-hmac":5,"3des-cbc-hmac":6},QK={1:"aes-256-gcm-pbkdf2",2:"aes-256-gcm",3:"aes-256-cbc-hmac",4:"chacha20-poly1305",5:"des-cbc-hmac",6:"3des-cbc-hmac"},_K=/^enc:v(\d+):/;FK="openlist-db-cipher-v3-enc",PK="openlist-db-cipher-v3-mac";TK=8,cc=new Map;Yx=!1;LK="openlist-db-cipher-v4-chacha20";GK="openlist-db-cipher-v5-des",zK="openlist-db-cipher-v6-3des",JK="openlist-db-cipher-v5-des-mac",jK="openlist-db-cipher-v6-3des-mac"});function Dl(r,e){if(typeof r!="string")return null;let t=r.trim();if(!t)return null;let i;try{i=new URL(t)}catch{return null}if(i.protocol!=="http:"&&i.protocol!=="https:"||!i.hostname)return null;let n=e?.EO_KV_URLS;return!(typeof n=="string"&&n.trim()===t)&&!(i.hostname==="localhost"||i.hostname==="127.0.0.1"||i.hostname==="[::1]")&&i.protocol!=="https:"?(console.warn(`[DB] Rejecting non-HTTPS proxy origin derived from request: ${i.protocol}//${i.hostname} (would leak X-Internal-Call in clear text)`),null):`${i.protocol}//${i.host}`.replace(/\/$/,"")}var Hh=J(()=>{"use strict"});var cC=oe((lue,XD)=>{"use strict";var aC=Object.defineProperty,$K=Object.getOwnPropertyDescriptor,VK=Object.getOwnPropertyNames,YK=Object.prototype.hasOwnProperty,WK=(r,e)=>{for(var t in e)aC(r,t,{get:e[t],enumerable:!0})},XK=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of VK(e))!YK.call(r,n)&&n!==t&&aC(r,n,{get:()=>e[n],enumerable:!(i=$K(e,n))||i.enumerable});return r},ZK=r=>XK(aC({},"__esModule",{value:!0}),r),HD={};WK(HD,{InvalidKeyError:()=>Kh,InvalidStoreNameError:()=>zu,MissingProjectIdError:()=>KD,PagesBlobError:()=>Gn,PreconditionFailedError:()=>nC,QuotaExceededError:()=>eG,RateLimitedError:()=>tG,Store:()=>GD,getStore:()=>wG,listStores:()=>xG});XD.exports=ZK(HD);var Gn=class extends Error{code;constructor(r,e){super(`PagesBlob: ${e}`),this.name="PagesBlobError",this.code=r}},Kh=class extends Gn{constructor(r){super("INVALID_KEY",r)}},zu=class extends Gn{constructor(r){super("INVALID_STORE_NAME",r)}},tC=class extends Gn{constructor(r){super("MISSING_ENVIRONMENT",`Environment not configured for Pages Blob. Missing: ${r.join(", ")}. Supply these properties when creating a store, or ensure the function is running in a Pages environment.`)}},eG=class extends Gn{constructor(){super("QUOTA_EXCEEDED","storage quota exceeded")}},tG=class extends Gn{constructor(){super("RATE_LIMITED","request rate limited, please retry later")}},KD=class extends Gn{constructor(){super("MISSING_PROJECT_ID","projectId is required when using API token mode. Please supply { name, projectId, token } to getStore() / listStores().")}},Go=class extends Gn{constructor(r){super("CREDENTIAL_ERROR",r)}},zr=class extends Gn{constructor(r,e){super("COS_ERROR",`COS returned ${r}: ${e}`)}},nC=class extends Gn{constructor(){super("PRECONDITION_FAILED","conditional write failed (key already exists)")}};function dc(r){if(r==="")throw new Kh("Blob key must not be empty.");if(r.startsWith("/")||r.startsWith("%2F"))throw new Kh("Blob key must not start with forward slash (/).");if(new TextEncoder().encode(r).length>600)throw new Kh("Blob key must be a sequence of Unicode characters whose UTF-8 encoding is at most 600 bytes long.")}function rG(r){if(r==="")throw new zu("Store name must not be empty.");if(r.includes("/")||r.includes(":"))throw new zu("Store name must not contain forward slashes (/) or colons (:).");if(!/^[a-zA-Z0-9_-]+$/.test(r))throw new zu("Store name must only contain letters, digits, underscores, and hyphens.");if(new TextEncoder().encode(r).length>64)throw new zu("Store name must be a sequence of Unicode characters whose UTF-8 encoding is at most 64 bytes long.")}var GD=class{cosClient;storeName;defaultConsistency;constructor(r,e,t="eventual"){this.cosClient=r,this.storeName=e,this.defaultConsistency=t}resolveConsistency(r){return r??this.defaultConsistency}async set(r,e,t){dc(r);let i=await this.cosClient.putObject(this.storeName,r,e,{onlyIfNew:t?.onlyIfNew,cacheControl:t?.cacheControl});if(t?.onlyIfNew&&i.statusCode===412)throw new nC}async setJSON(r,e,t){dc(r);let i=JSON.stringify(e),n=await this.cosClient.putObject(this.storeName,r,i,{onlyIfNew:t?.onlyIfNew,contentType:"application/json",cacheControl:t?.cacheControl});if(t?.onlyIfNew&&n.statusCode===412)throw new nC}async createUploadUrl(r,e){dc(r);let{url:t,expiresAt:i}=await this.cosClient.createPresignedPutUrl(this.storeName,r,{expireSeconds:e?.expireSeconds,contentType:e?.contentType});return{url:t,key:r,expiresAt:i}}async get(r,e){dc(r);let t=this.resolveConsistency(e?.consistency),i=await this.cosClient.getObject(this.storeName,r,t);if(i===null)return null;let{body:n}=i,s=e?.type??"text",o=new TextDecoder("utf-8");switch(s){case"text":return o.decode(n);case"json":return JSON.parse(o.decode(n));case"arrayBuffer":return n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength);case"blob":return new Blob([n]);case"stream":return new ReadableStream({start(a){a.enqueue(n),a.close()}});default:return o.decode(n)}}async getMetadata(r,e){dc(r);let t=this.resolveConsistency(e?.consistency);return this.cosClient.headObject(this.storeName,r,t)}async getWithHeaders(r,e){dc(r);let t=this.resolveConsistency(e?.consistency),i=await this.cosClient.getObject(this.storeName,r,t);return i?{body:new TextDecoder("utf-8").decode(i.body),headers:i.headers||{}}:null}async delete(r){dc(r),await this.cosClient.deleteObject(this.storeName,r)}async list(r){let e=r?.paginate!==!1,t=r?.limit,i=[],n=[],s=this.resolveConsistency(r?.consistency),o=r?.cursor||"",a=!0,c;for(;a;){let l=t!==void 0?t-i.length:1e3,d=Math.min(l,1e3);if(d<=0)break;let u=await this.cosClient.listObjects(this.storeName,{prefix:r?.prefix,delimiter:r?.directories?"/":void 0,marker:o||void 0,maxKeys:d,consistency:s});for(let f of u.contents)i.push({key:f.key,etag:f.etag});n.push(...u.commonPrefixes),t!==void 0&&i.length>=t?(i.length=t,(u.isTruncated||u.contents.length===d)&&(c=u.nextMarker),a=!1):u.isTruncated?!e&&t===void 0?(c=u.nextMarker,a=!1):o=u.nextMarker:a=!1}return{blobs:i,directories:n,...c?{cursor:c}:{}}}},iG=new TextEncoder;function sC(r){let e=iG.encode(r),t=new ArrayBuffer(e.byteLength),i=new Uint8Array(t);return i.set(e),i}function zD(r){let e=r instanceof Uint8Array?r:new Uint8Array(r),t="";for(let i=0;i"%"+e.charCodeAt(0).toString(16).toUpperCase())}function zh(r){try{return decodeURIComponent(r)}catch{return r}}function JD(r){return r.split("/").map(e=>zh(e)).join("/")}function jD(r){return r.split("/").map(e=>Gh(zh(e))).join("/")}var sG=new Set(["cache-control","content-disposition","content-encoding","content-length","content-md5","content-type","expect","expires","if-match","if-modified-since","if-none-match","if-unmodified-since","origin","range","transfer-encoding"]);function oG(r){return r==="host"||r==="x-cos-security-token"?!1:!!(sG.has(r)||r.startsWith("x-cos-"))}function UD(r){if(!r)return[];let e=[];for(let[t,i]of Object.entries(r))i!=null&&e.push([t.toLowerCase(),String(i)]);return e.sort(([t],[i])=>ti?1:0),e}function RD(r){return r.map(([e,t])=>`${Gh(e)}=${Gh(t)}`).join("&")}function ND(r){return r.map(([e])=>Gh(e)).join(";")}async function $D(r){let e=r.method.toLowerCase(),t=r.pathname.startsWith("/")?r.pathname:`/${r.pathname}`,i=Math.floor(Date.now()/1e3),n=i+(r.expireSeconds??3600),s=`${i};${n}`,o=UD(r.headers).filter(([w])=>oG(w)),a=ND(o),c=RD(o),l=UD(r.query),d=ND(l),u=RD(l),f=`${e} -${t} -${u} -${c} -`,h=`sha1 -${s} -${await nG(f)} -`,g=await TD(r.secretKey,s),p=await TD(g,h),I=["q-sign-algorithm=sha1",`q-ak=${r.secretId}`,`q-sign-time=${s}`,`q-key-time=${s}`,`q-header-list=${a}`,`q-url-param-list=${d}`,`q-signature=${p}`].join("&"),m={};for(let[w,B]of o)m[w]=B;return{authorization:I,signedHeaders:m}}async function aG(r){let e=new URL(r.domain),t=zh(r.key),i=`/${JD(t)}`,n=`/${jD(t)}`;e.pathname=n;let{authorization:s}=await $D({method:r.method,pathname:i,query:r.query,headers:r.headers,secretId:r.credential.secretId,secretKey:r.credential.secretKey,expireSeconds:r.expireSeconds});if(r.query)for(let[o,a]of Object.entries(r.query))a!=null&&e.searchParams.set(o,String(a));for(let o of s.split("&")){let a=o.indexOf("=");if(a===-1)continue;let c=o.slice(0,a),l=o.slice(a+1);e.searchParams.set(c,l)}return r.credential.sessionToken&&e.searchParams.set("x-cos-security-token",r.credential.sessionToken),e.toString()}async function Mu(r){let e=new URL(r.domain),t=r.key?zh(r.key):"",i=t?`/${JD(t)}`:"/",n=t?`/${jD(t)}`:"/";if(e.pathname=n,r.query)for(let[u,f]of Object.entries(r.query))f!=null&&e.searchParams.set(u,String(f));let{authorization:s}=await $D({method:r.method,pathname:i,query:r.query,headers:r.headers,secretId:r.credential.secretId,secretKey:r.credential.secretKey}),o=new Headers;if(r.headers)for(let[u,f]of Object.entries(r.headers))f!=null&&o.set(u,String(f));o.set("Authorization",s),r.credential.sessionToken&&o.set("x-cos-security-token",r.credential.sessionToken);let a=e.toString(),c={method:r.method,headers:o,body:r.body??void 0,signal:r.signal},l=2,d;for(let u=0;u<=l;u++)try{return await fetch(a,c)}catch(f){if(d=f,f instanceof DOMException&&f.name==="AbortError")throw f;usetTimeout(h,1e3*(u+1)))}throw d}var cG="blob.edgeone.site",lG="blob-nocache.edgeone.site",VD=class En{credentialManager;bucket="";region="";keyPrefix="";cachedDomain="";uncachedDomain="";initialized=!1;static buildErrorDetail(e,t,i,n,s){let o=i?`${t}/${i}`:t,a=s?` [request-id: ${s}]`:"";return`${e} ${o} - ${dG(n)}${a}`}constructor(e){this.credentialManager=e}computeSubdomain(e){let t=[];if(e.appId&&t.push(e.appId),e.zoneId&&t.push(e.zoneId),e.projectId&&t.push(e.projectId),t.length>=2)return t.join("-");if(e.resourcePrefix){let i=e.resourcePrefix.replace(/\/?\*$/,"").split("/").filter(Boolean);if(i.length>=2)return i.slice(0,Math.min(i.length,3)).join("-")}return""}async ensureInitialized(){if(this.initialized)return;let e=await this.credentialManager.getCredential();!this.keyPrefix&&e.resourcePrefix&&(this.keyPrefix=e.resourcePrefix.replace(/\/?\*$/,""));let t=e.edgeRegion==="CN",i=e.cosMainland,n=e.cosOverseas,s=t?i||n:n||i;!this.bucket&&s&&(this.bucket=s.bucket,this.region=s.region);let o=this.computeSubdomain(e);if(!o)throw new zr(0,"unable to derive tenant subdomain from credential; missing appId/zoneId/projectId or resourcePrefix");this.cachedDomain=`https://${o}.${cG}`,this.uncachedDomain=`https://${o}.${lG}`,this.initialized=!0}async resolveDomain(e){return await this.ensureInitialized(),e==="strong"?this.uncachedDomain:this.cachedDomain}async resolveCredential(){let e=await this.credentialManager.getCredential();return{secretId:e.tmpSecretId,secretKey:e.tmpSecretKey,sessionToken:e.sessionToken}}buildCosKey(e,t){return`${this.keyPrefix}/${e}/${t}`}async getDomains(){return await this.ensureInitialized(),{cached:this.cachedDomain,uncached:this.uncachedDomain}}async putObject(e,t,i,n){let s=await this.resolveDomain("strong"),o=await this.resolveCredential(),a=this.buildCosKey(e,t),c=n?.cacheControl===null?void 0:n?.cacheControl??"max-age=0, stale-while-revalidate=60",l={};n?.onlyIfNew&&(l["If-None-Match"]="*"),c&&(l["Cache-Control"]=c),n?.contentType&&(l["Content-Type"]=n.contentType);try{let d=await Mu({domain:s,method:"PUT",key:a,headers:l,body:i,credential:o});if(d.status===412)return await d.arrayBuffer().catch(()=>{}),{etag:"",statusCode:412};if(!d.ok){let f=await Lu(d);throw new zr(d.status,En.buildErrorDetail("PUT",s,a,f||`status ${d.status}`,Hu(d)))}let u=d.headers.get("etag")||"";return await d.arrayBuffer().catch(()=>{}),{etag:u,statusCode:d.status}}catch(d){throw d instanceof zr?d:new zr(0,En.buildErrorDetail("PUT",s,a,Ku(d)))}}async createPresignedPutUrl(e,t,i){let n=await this.resolveDomain("strong"),s=await this.resolveCredential(),o=this.buildCosKey(e,t),a={};i?.contentType&&(a["Content-Type"]=i.contentType);let c=i?.expireSeconds??3600,l=await aG({domain:n,method:"PUT",key:o,headers:a,credential:s,expireSeconds:c}),d=Math.floor(Date.now()/1e3)+c;return{url:l,expiresAt:d}}async getObject(e,t,i){let n=await this.resolveDomain(i),s=await this.resolveCredential(),o=this.buildCosKey(e,t);try{let a=await Mu({domain:n,method:"GET",key:o,credential:s});if(a.status===404)return await a.arrayBuffer().catch(()=>{}),null;if(!a.ok){let d=await Lu(a);throw new zr(a.status,En.buildErrorDetail("GET",n,o,d||`status ${a.status}`,Hu(a)))}let c=new Uint8Array(await a.arrayBuffer()),l=qD(a.headers);return{body:c,contentType:l["content-type"],headers:l}}catch(a){throw a instanceof zr?a:new zr(0,En.buildErrorDetail("GET",n,o,Ku(a)))}}async headObject(e,t,i){let n=await this.resolveDomain(i),s=await this.resolveCredential(),o=this.buildCosKey(e,t);try{let a=await Mu({domain:n,method:"HEAD",key:o,credential:s});if(a.status===404)return null;if(!a.ok){let l=await Lu(a);throw new zr(a.status,En.buildErrorDetail("HEAD",n,o,l||`status ${a.status}`,Hu(a)))}let c=qD(a.headers);return{cacheControl:c["cache-control"],contentType:c["content-type"],etag:c.etag,headers:c}}catch(a){throw a instanceof zr?a:new zr(0,En.buildErrorDetail("HEAD",n,o,Ku(a)))}}async deleteObject(e,t){let i=await this.resolveDomain("strong"),n=await this.resolveCredential(),s=this.buildCosKey(e,t);try{let o=await Mu({domain:i,method:"DELETE",key:s,credential:n});if(o.status===204||o.status===404||o.ok){await o.arrayBuffer().catch(()=>{});return}let a=await Lu(o);throw new zr(o.status,En.buildErrorDetail("DELETE",i,s,a||`status ${o.status}`,Hu(o)))}catch(o){throw o instanceof zr?o:new zr(0,En.buildErrorDetail("DELETE",i,s,Ku(o)))}}async listObjects(e,t){await this.ensureInitialized();let i=`${this.keyPrefix}/${e}/`,n=t?.prefix?i+t.prefix:i,s=await this.getBucketRaw({prefix:n,delimiter:t?.delimiter,marker:t?.marker,maxKeys:t?.maxKeys,consistency:t?.consistency}),o=s.contents.map(c=>{let l=c.key,d=l.startsWith(i)?l.slice(i.length):l;return d?{key:d,etag:c.etag}:null}).filter(c=>c!==null),a=s.commonPrefixes.map(c=>c.startsWith(i)?c.slice(i.length):c).filter(c=>!!c);return{contents:o,commonPrefixes:a,isTruncated:s.isTruncated,nextMarker:s.nextMarker}}async listStores(e){let t=[],i="",n=!0;for(;n;){await this.ensureInitialized();let s=`${this.keyPrefix}/`,o=await this.getBucketRaw({prefix:s,delimiter:"/",maxKeys:1e3,marker:i||void 0,consistency:e});for(let a of o.commonPrefixes){let c=a.startsWith(s)?a.slice(s.length,-1):a.slice(0,-1);c&&t.push(c)}if(n=o.isTruncated,i=o.nextMarker,!n||!i)break}return t}async getBucketRaw(e){let t=await this.resolveDomain(e.consistency),i=await this.resolveCredential(),n={prefix:e.prefix};e.delimiter&&(n.delimiter=e.delimiter),e.marker&&(n.marker=e.marker),e.maxKeys&&(n["max-keys"]=e.maxKeys);try{let s=await Mu({domain:t,method:"GET",query:n,credential:i});if(!s.ok){let a=await Lu(s);throw new zr(s.status,En.buildErrorDetail("LIST",t,e.prefix,a||`status ${s.status}`,Hu(s)))}let o=await s.text();return uG(o)}catch(s){throw s instanceof zr?s:new zr(0,En.buildErrorDetail("LIST",t,e.prefix,Ku(s)))}}};function dG(r){return r.replace(/[a-zA-Z0-9\-]+\.cos\.[a-zA-Z0-9\-.]+\.myqcloud\.com/gi,"[cos-origin]").replace(/[a-zA-Z0-9\-]+\.cos\.[a-zA-Z0-9\-.]+\.tencentcos\.cn/gi,"[cos-origin]")}async function Lu(r){try{return await r.text()}catch{return""}}function Hu(r){return r.headers.get("x-cos-request-id")||r.headers.get("x-eo-log-id")||void 0}function Ku(r){let e=r,t=e.message||String(r),i=e.cause;if(i){let n=i.message||i.code||"";return n?`${t} (${n})`:t}return t}function qD(r){let e={};return r.forEach((t,i)=>{e[i.toLowerCase()]=t}),e}function uG(r){let e=[],t=/([\s\S]*?)<\/Contents>/g,i;for(;(i=t.exec(r))!==null;){let c=i[1],l=Gu(c,"Key"),d=Gu(c,"ETag");l!==null&&e.push({key:rC(l),etag:d||""})}let n=[],s=/([\s\S]*?)<\/CommonPrefixes>/g;for(;(i=s.exec(r))!==null;){let c=i[1],l=Gu(c,"Prefix");l!==null&&n.push(rC(l))}let o=Gu(r,"IsTruncated")==="true",a=Gu(r,"NextMarker")||"";return{contents:e,commonPrefixes:n,isTruncated:o,nextMarker:rC(a)}}function Gu(r,e){let t=new RegExp(`<${e}>([\\s\\S]*?)<\\/${e}>`).exec(r);return t?t[1]:null}function rC(r){return r.replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/&/g,"&")}var fG="X-RateLimit-Reset";async function oC(r,e,t=2){e.signal?.throwIfAborted?.();try{let i=await fetch(r,e);if(t>0&&(i.status===429||i.status>=500)){let n=OD(i.headers.get(fG));return await MD(n,e.signal),oC(r,e,t-1)}return i}catch(i){if(t===0||i instanceof DOMException&&i.name==="AbortError")throw i;let n=OD();return await MD(n,e.signal),oC(r,e,t-1)}}function OD(r){return r?Math.max(Number(r)*1e3-Date.now(),500):1500}function MD(r,e){return new Promise((t,i)=>{if(e?.aborted)return i(e.reason);let n=setTimeout(()=>{e?.removeEventListener("abort",s),t()},r),s=()=>{clearTimeout(n),i(e.reason)};e?.addEventListener("abort",s,{once:!0})})}var AG="prod";function hG(){let r=typeof process<"u"?process.env.PAGES_BLOB_STS_ENV:void 0;return r==="test"||r==="prod"?r:AG}var gG=300,pG="https://blob-sts.edgeone.site/",YD=class{authToken;projectId;cached=null;constructor(r,e){this.authToken=r,this.projectId=e}async getCredential(){if(this.cached&&!this.isExpired(this.cached))return this.cached;let r=await this.fetchCredential();return this.cached=r,r}clearCache(){this.cached=null}isExpired(r){let e=Math.floor(Date.now()/1e3);return r.expiredTime-ee.abort(),1e4),i;try{i=await oC(pG,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.authToken}`,"X-Env":hG()},body:JSON.stringify(this.projectId?{ProjectId:this.projectId}:{}),signal:e.signal})}catch(o){if(r<3){await iC(500*r);continue}throw new Go(`failed to obtain STS credential: ${o.message||"timeout"}`)}finally{clearTimeout(t)}if(i.status===413)throw new Go("storage quota exceeded");if(i.status===429)throw new Go("rate limited, please retry later");if(!i.ok){if(i.status>=500&&r<3){await iC(500*r);continue}let o=await i.text().catch(()=>"unknown error");throw new Go(`failed to obtain STS credential: ${i.status} ${o}`)}let n=await i.json(),s=n.data&&typeof n.data=="object"?n.data:n;if(s.tmpSecretId&&s.tmpSecretKey&&s.sessionToken&&s.expiredTime){let o=s.cosMainland,a=s.cosOverseas,c=i.headers.get("X-Edge-Region")||void 0;return{tmpSecretId:s.tmpSecretId,tmpSecretKey:s.tmpSecretKey,sessionToken:s.sessionToken,expiredTime:s.expiredTime,appId:s.appId||void 0,zoneId:s.zoneId||void 0,projectId:s.projectId||void 0,resourcePrefix:s.resourcePrefix||void 0,cosMainland:o||void 0,cosOverseas:a||void 0,edgeRegion:c}}if(s.code!==void 0&&s.code!==0){let o=s.msg||s.message||"unknown error";throw new Go(`credential exchange failed (code=${s.code}): ${o}`)}if(n.code!==void 0&&n.code!==0){let o=n.msg||n.message||"unknown error";throw new Go(`credential exchange failed (code=${n.code}): ${o}`)}if(r<3){await iC(500*r);continue}throw new Go("invalid STS credential response")}throw new Go("invalid STS credential response")}};function iC(r){return new Promise(e=>setTimeout(e,r))}var mG="{{PAGES_BLOB_DEPLOY_CREDENTIAL}}";function yG(){let r={},e=IG();if(e)r.deployCredential=e;else{let i=LD("PAGES_BLOB_DEPLOY_CREDENTIAL");i&&(r.deployCredential=i)}let t=LD("PAGES_PROJECT_ID");return t&&(r.projectId=t),r}function IG(){let r=mG;if(!(r.startsWith("{{")&&r.endsWith("}}")))return r||void 0}function LD(r){if(typeof process<"u"&&process.env)return process.env[r]}function wG(r){let e=typeof r=="string"?r:r.name;rG(e);let t=WD(typeof r=="string"?void 0:r),i=new YD(t.authToken,t.projectId),n=new VD(i);return new GD(n,e,t.consistency??"eventual")}async function xG(r){let e=WD(r?{name:"__list__",projectId:r.projectId,token:r.token,consistency:r.consistency}:void 0),t=new YD(e.authToken,e.projectId);return{stores:(await new VD(t).listStores(e.consistency)).map(i=>({name:i}))}}function WD(r){let e=yG(),t=r?.token||e.deployCredential,i=r?.projectId||e.projectId;if(r?.token||e.projectId){if(!i)throw new KD;if(!t)throw new tC(["token"]);return{authToken:t,projectId:i,consistency:r?.consistency}}if(r?.projectId&&!t)throw new tC(["token"]);if(!e.deployCredential)throw new tC(["deployCredential"]);return{authToken:e.deployCredential,consistency:r?.consistency}}});var i3={};Cn(i3,{checkProxyConfig:()=>Ql,kvDriver:()=>jh});function ZD(r){if(!r||typeof r!="object")return!1;try{return typeof r.get!="function"?!1:typeof r.put=="function"||typeof r.set=="function"}catch{return!1}}function uc(r){let e=globalThis;return ZD(r?.KV)?r.KV:ZD(e?.KV)?e.KV:null}function t3(r){try{let e=r?.JWT_SECRET;return typeof e=="string"&&e.trim().length>0?e:null}catch{return null}}function Ql(r){try{if(uc(r)!==null||t3(r))return null}catch{}return`KV proxy mode requires the JWT_SECRET environment variable to be set. -Reason: EdgeOne Node Functions cannot access KV directly and must go through an Edge Function proxy, whose authentication depends on this secret. -Add it under Environment Variables in the EdgeOne project settings, for example: - JWT_SECRET= -Generate one with: openssl rand -hex 32`}function Ju(r){let e={"Content-Type":"application/json"},t=t3(r);t&&(e["X-Internal-Call"]=t);let i=r?._currentUserToken;return i&&(e.Authorization=`Bearer ${i}`),e}function r3(r){if(!r)return"";let e=r.EO_KV_URLS;if(e){let i=Dl(e,r);if(i)return i}let t=r.__requestOrigin;if(t){let i=Dl(t,r);if(i)return i}return""}async function e3(r){let e=Ql(r);if(e)return{ok:!1,status:0,error:e.split(` -`)[0]};let t=r3(r);if(!t)return{ok:!1,status:0,error:"cannot determine deployment origin. Set EO_KV_URLS to the deployment origin."};try{let i=`${t}/kv-list?prefix=__health__`,n=await fetch(i,{method:"GET",headers:Ju(r)});return{ok:n.ok,status:n.status}}catch(i){return{ok:!1,status:0,error:i?.message||String(i)}}}function CG(r){return r.ok||r.status===401}function EG(r){return r.ok}function Jh(r){let e=Ql(r);if(e)throw new Error("KV proxy misconfigured: "+e.split(` -`)[0]);let t=r3(r);if(!t)throw new Error("KV proxy base URL unavailable: cannot determine deployment origin. Set EO_KV_URLS to the deployment origin.");return t}var jh,lC=J(()=>{"use strict";Hh();jh={name:"kv",async isAvailable(r){if(uc(r)!==null)return!0;let e=await e3(r);return CG(e)?!0:(e.error&&console.error("[DB] KV proxy unavailable: "+e.error),!1)},async init(r){},async get(r,e){let t=uc(e);if(t){let s=a=>a instanceof TypeError||/not a function|invalid|unexpected|argument/i.test(String(a?.message||"")),o;try{o=await t.get(r,"text")}catch(a){if(!s(a))throw a;o=void 0}if(o==null)try{o=await t.get(r,{type:"text"})}catch(a){if(!s(a))throw a;o=null}return o==null?null:typeof o=="string"?o:JSON.stringify(o)}let n=`${Jh(e)}/kv-get?key=${encodeURIComponent(r)}`;try{let s=await fetch(n,{method:"GET",headers:Ju(e)});if(!s.ok){if(s.status===404)return null;throw new Error(`KV proxy get failed: ${s.status}`)}let o=await s.json();return o?.value===void 0||o?.value===null?null:typeof o.value=="string"?o.value:String(o.value)}catch(s){throw console.error(`[KV] get(${r}) failed:`,s),s}},async put(r,e,t){let i=uc(t);if(i){await i.put(r,e);return}let s=`${Jh(t)}/kv-put`;try{let o=await fetch(s,{method:"POST",headers:Ju(t),body:JSON.stringify({key:r,value:e})});if(!o.ok)throw new Error(`KV proxy put failed: ${o.status}`)}catch(o){throw console.error(`[KV] put(${r}) failed:`,o),o}},async delete(r,e){let t=uc(e);if(t){await t.delete(r);return}let n=`${Jh(e)}/kv-delete?key=${encodeURIComponent(r)}`;try{let s=await fetch(n,{method:"DELETE",headers:Ju(e)});if(!s.ok&&s.status!==404)throw new Error(`KV proxy delete failed: ${s.status}`)}catch(s){throw console.error(`[KV] delete(${r}) failed:`,s),s}},async list(r,e){let t=uc(e);if(t){let s=[],o="",a=!1,c=0;for(;!a&&c<1e3;){c+=1;let l=await t.list({prefix:r,cursor:o,limit:256}),d=Array.isArray(l?.keys)?l.keys:[];for(let u of d)u?.key&&s.push(u.key);d.length>0&&(o=d[d.length-1].key||""),a=!!l?.complete||d.length===0}return s}let n=`${Jh(e)}/kv-list?prefix=${encodeURIComponent(r)}`;try{let s=await fetch(n,{method:"GET",headers:Ju(e)});if(!s.ok)throw new Error(`KV proxy list failed: ${s.status}`);return(await s.json()).keys||[]}catch(s){throw console.error(`[KV] list(${r}) failed:`,s),s}},async health(r){let e=uc(r);if(e)try{return await e.get("__health_check__"),{driver:"kv",mode:"binding",available:!0,platform:"Cloudflare KV / EdgeOne KV"}}catch(i){return{driver:"kv",mode:"binding",available:!1,error:i?.message||String(i)}}let t=await e3(r);return EG(t)?{driver:"kv",mode:"proxy",available:!0,platform:"EdgeOne KV (via Edge Function proxy)"}:{driver:"kv",mode:"proxy",available:!1,error:t.error||`HTTP ${t.status}`}}}});function n3(r){if(!r||typeof r!="object")return!1;try{return typeof r.get!="function"?!1:typeof r.put=="function"||typeof r.set=="function"}catch{return!1}}function _l(r){let e=globalThis,t=r?.ESA_BLOB;if(n3(t))return t;let i=e?.ESA_BLOB;return n3(i)?i:null}async function Fl(r="default"){try{let{getStore:e}=await Promise.resolve().then(()=>xr(cC(),1));return e(r)}catch{return null}}var dC,s3=J(()=>{"use strict";dC={name:"blob",async isAvailable(r){return _l(r)!==null?!0:await Fl()!==null},async init(r){},async get(r,e){let t=_l(e);if(t)try{let n=await t.get(r);return n?await n.text():null}catch(n){return console.warn(`[Blob] ESA get key="${r}" failed:`,n),null}let i=await Fl();if(!i)throw new Error("Blob not available");try{let n=await i.get(r);return n==null?null:typeof n=="string"?n:JSON.stringify(n)}catch(n){return console.warn(`[Blob] EdgeOne get key="${r}" failed:`,n),null}},async put(r,e,t){let i=_l(t);if(i){await i.put(r,e);return}let n=await Fl();if(!n)throw new Error("Blob not available");await n.set(r,e)},async delete(r,e){let t=_l(e);if(t){await t.delete(r);return}let i=await Fl();if(!i)throw new Error("Blob not available");await i.delete(r)},async list(r,e){let t=_l(e);if(t){let s=[],o;do{let a=await t.list({prefix:r,cursor:o});s.push(...(a?.keys||[]).map(c=>c.name)),o=a?.cursor}while(o);return s}let i=await Fl();if(!i)throw new Error("Blob not available");return((await i.list({prefix:r,paginate:!0}))?.blobs||[]).map(s=>s.key)},async health(r){let e=_l(r),t=await Fl();if(e)try{return await e.head("__health_check__"),{driver:"blob",platform:"ESA Blob (binding)",available:!0}}catch(i){return{driver:"blob",platform:"ESA Blob (binding)",available:!1,error:i?.message||String(i)}}if(t)try{return await t.get("__health_check__"),{driver:"blob",platform:"EdgeOne Blob (SDK)",available:!0}}catch(i){return{driver:"blob",platform:"EdgeOne Blob (SDK)",available:!1,error:i?.message||String(i)}}return{driver:"blob",available:!1,error:"No Blob storage available"}}}});function Pl(r){let e=r||(typeof process<"u"?process.env:{})||{},t=e.CF_ACCOUNT||e.CLOUDFLARE_ACCOUNT_ID,i=e.CF_KV_UUID||e.CLOUDFLARE_KV_NAMESPACE_ID,n=e.CF_API_KEY||e.CLOUDFLARE_API_TOKEN;return!t||!i||!n?null:{accountId:t,namespaceId:i,apiToken:n}}function $h(r,e){return`https://api.cloudflare.com/client/v4/accounts/${r.accountId}/storage/kv/namespaces/${r.namespaceId}${e}`}var uC,o3=J(()=>{"use strict";uC={name:"cfkv",async isAvailable(r){return Pl(r)!==null},async init(r){},async get(r,e){let t=Pl(e);if(!t)throw new Error("Cloudflare KV API not configured");let i=$h(t,`/values/${encodeURIComponent(r)}`),n=await fetch(i,{headers:{Authorization:`Bearer ${t.apiToken}`}});if(n.status===404)return null;if(!n.ok)throw new Error(`Cloudflare KV API error: ${n.status}`);return await n.text()},async put(r,e,t){let i=Pl(t);if(!i)throw new Error("Cloudflare KV API not configured");let n=$h(i,`/values/${encodeURIComponent(r)}`),s=await fetch(n,{method:"PUT",headers:{Authorization:`Bearer ${i.apiToken}`},body:e});if(!s.ok)throw new Error(`Cloudflare KV API error: ${s.status}`)},async delete(r,e){let t=Pl(e);if(!t)throw new Error("Cloudflare KV API not configured");let i=$h(t,`/values/${encodeURIComponent(r)}`),n=await fetch(i,{method:"DELETE",headers:{Authorization:`Bearer ${t.apiToken}`}});if(!n.ok&&n.status!==404)throw new Error(`Cloudflare KV API error: ${n.status}`)},async list(r,e){let t=Pl(e);if(!t)throw new Error("Cloudflare KV API not configured");let i=[],n;do{let s=new URLSearchParams({prefix:r,limit:"1000"});n&&s.set("cursor",n);let o=$h(t,`/keys?${s}`),a=await fetch(o,{headers:{Authorization:`Bearer ${t.apiToken}`}});if(!a.ok)throw new Error(`Cloudflare KV API error: ${a.status}`);let c=await a.json();i.push(...(c.result||[]).map(l=>l.name)),n=c.result_info?.cursor}while(n);return i},async health(r){let e=Pl(r);if(!e)return{configured:!1,connected:!1,platform:"Cloudflare KV API (REST)",mode:"cfkv",error:"Missing CF_ACCOUNT, CF_KV_UUID, or CF_API_KEY"};try{return await this.get("__health_check__",r),{configured:!0,connected:!0,platform:"Cloudflare KV API (REST)",mode:"cfkv",accountId:e.accountId,namespaceId:e.namespaceId}}catch(t){return{configured:!0,connected:!1,platform:"Cloudflare KV API (REST)",mode:"cfkv",error:t?.message||String(t)}}}}});function fC(r){return"x_"}function AC(r,e){return fC(e)+bG[r]}function vG(r,e){if(e==null){if(r.pk||r.nullable)return null;switch(r.type){case"string":return"";case"number":return 0;case"bool":return 0;case"json":return"null";case"date":return"";default:return""}}switch(r.type){case"string":return String(e);case"number":return typeof e=="number"?e:Number(e);case"bool":return e?1:0;case"json":return typeof e=="string"?e:JSON.stringify(e);case"date":return e instanceof Date?e.toISOString():typeof e=="number"?new Date(e).toISOString():String(e);default:return e}}function kG(r,e){if(e==null)return r.type==="json",null;switch(r.type){case"string":{let t=String(e);return t===""?null:t}case"number":return Number(e);case"bool":return e===1||e==="1"||e===!0||e==="true";case"json":{if(typeof e!="string")return e;try{return JSON.parse(e)}catch{return e}}case"date":return e;default:return e}}function Yh(r,e){let t=Vh[r],i={};for(let n of t.columns){let s=n.key??n.name,o=kG(n,e[n.name]);o!=null&&(i[s]=o)}return i}function Wh(r,e){let t=Vh[r],i=[],n=[];for(let s of t.columns){let o=s.key??s.name;i.push(s.name),n.push(vG(s,e?.[o]))}return{columns:i,values:n}}function SG(r,e){switch(r.type){case"number":return e==="mysql"?"BIGINT":"INTEGER";case"bool":return e==="mysql"?"TINYINT(1)":"INTEGER";case"string":case"json":case"date":return"TEXT";default:return"TEXT"}}function zo(r){return"`"+r+"`"}function DG(r,e,t){let i=[];for(let n of r.columns){let s=`${zo(n.name)} ${SG(n,e)}`;n.pk?s+=" PRIMARY KEY":n.nullable||(s+=" NOT NULL"),n.unique&&(s+=" UNIQUE"),i.push(s)}return`CREATE TABLE IF NOT EXISTS ${zo(t)} (${i.join(", ")})`}function QG(r){let e=r==="mysql"?"VARCHAR(255)":"TEXT";return`CREATE TABLE IF NOT EXISTS ${zo("schema_info")} (${zo("k")} ${e} PRIMARY KEY, ${zo("v")} TEXT)`}function Tl(r,e){let t=[QG(r)];for(let i of BG)t.push(DG(Vh[i],r,AC(i,e)));return t}var Jo,BG,fc,Vh,bG,bue,vue,a3,c3,ju=J(()=>{"use strict";Jo=["settings","storages","users","shares","metas","plugins"],BG=[...Jo,"sshkeys"],fc={settings:"key",storages:"id",users:"id",shares:"id",metas:"id",plugins:"id",sshkeys:"id"},Vh={settings:{name:"settings",columns:[{name:"key",type:"string",pk:!0},{name:"value",type:"string",nullable:!0},{name:"help",type:"string",nullable:!0},{name:"type",type:"string",nullable:!0},{name:"options",type:"string",nullable:!0},{name:"group",type:"number",nullable:!0},{name:"flag",type:"number",nullable:!0},{name:"index",type:"number",nullable:!0}]},storages:{name:"storages",columns:[{name:"id",type:"number",pk:!0},{name:"mount_path",type:"string",unique:!0},{name:"order",type:"number",nullable:!0},{name:"driver",type:"string",nullable:!0},{name:"cache_expiration",type:"number",nullable:!0},{name:"custom_cache_policies",type:"string",nullable:!0},{name:"status",type:"string",nullable:!0},{name:"addition",type:"string",nullable:!0},{name:"remark",type:"string",nullable:!0},{name:"modified",type:"date",nullable:!0},{name:"disabled",type:"bool",nullable:!0},{name:"disable_index",type:"bool",nullable:!0},{name:"enable_sign",type:"bool",nullable:!0},{name:"seed_policy",type:"string",nullable:!0},{name:"order_by",type:"string",nullable:!0},{name:"order_direction",type:"string",nullable:!0},{name:"extract_folder",type:"string",nullable:!0},{name:"web_proxy",type:"bool",nullable:!0},{name:"webdav_policy",type:"string",nullable:!0},{name:"proxy_range",type:"bool",nullable:!0},{name:"down_proxy_url",type:"string",nullable:!0},{name:"disable_proxy_sign",type:"bool",nullable:!0}]},users:{name:"users",columns:[{name:"id",type:"number",pk:!0},{name:"username",type:"string",unique:!0},{name:"password",type:"string",nullable:!0},{name:"base_path",type:"string",nullable:!0},{name:"role",type:"number",nullable:!0},{name:"disabled",type:"bool",nullable:!0},{name:"permission",type:"number",nullable:!0},{name:"salt",type:"string",nullable:!0},{name:"otp_secret",type:"string",nullable:!0},{name:"sso_id",type:"string",nullable:!0},{name:"authn",type:"string",nullable:!0},{name:"allow_ldap",type:"bool",nullable:!0},{name:"pwd_update_at",type:"date",nullable:!0}]},shares:{name:"shares",columns:[{name:"id",type:"string",pk:!0},{name:"files_raw",key:"files",type:"json",nullable:!0},{name:"expires",type:"date",nullable:!0},{name:"pwd",type:"string",nullable:!0},{name:"accessed",type:"number",nullable:!0},{name:"max_accessed",type:"number",nullable:!0},{name:"creator_id",type:"number",nullable:!0},{name:"disabled",type:"bool",nullable:!0},{name:"remark",type:"string",nullable:!0},{name:"readme",type:"string",nullable:!0},{name:"header",type:"string",nullable:!0},{name:"order_by",type:"string",nullable:!0},{name:"order_direction",type:"string",nullable:!0},{name:"extract_folder",type:"string",nullable:!0}]},metas:{name:"metas",columns:[{name:"id",type:"number",pk:!0},{name:"path",type:"string",unique:!0},{name:"read_users",type:"json",nullable:!0},{name:"read_users_sub",type:"bool",nullable:!0},{name:"write_users",type:"json",nullable:!0},{name:"write_users_sub",type:"bool",nullable:!0},{name:"password",type:"string",nullable:!0},{name:"p_sub",type:"bool",nullable:!0},{name:"write",type:"bool",nullable:!0},{name:"w_sub",type:"bool",nullable:!0},{name:"hide",type:"string",nullable:!0},{name:"h_sub",type:"bool",nullable:!0},{name:"readme",type:"string",nullable:!0},{name:"r_sub",type:"bool",nullable:!0},{name:"header",type:"string",nullable:!0},{name:"header_sub",type:"bool",nullable:!0}]},sshkeys:{name:"sshkeys",columns:[{name:"id",type:"number",pk:!0},{name:"user_id",type:"number",nullable:!0},{name:"title",type:"string",nullable:!0},{name:"fingerprint",type:"string",nullable:!0},{name:"key_str",type:"string",nullable:!0},{name:"added_time",type:"date",nullable:!0},{name:"last_used_time",type:"date",nullable:!0}]},plugins:{name:"plugins",columns:[{name:"id",type:"string",pk:!0},{name:"name",type:"string",nullable:!0},{name:"version",type:"string",nullable:!0},{name:"description",type:"string",nullable:!0},{name:"author",type:"string",nullable:!0},{name:"homepage",type:"string",nullable:!0},{name:"repository",type:"string",nullable:!0},{name:"icon",type:"string",nullable:!0},{name:"type",type:"string",nullable:!0},{name:"enabled",type:"bool",nullable:!0},{name:"high_privilege",type:"bool",nullable:!0},{name:"permissions",type:"json",nullable:!0},{name:"entry_url",type:"string",nullable:!0},{name:"script_content",type:"string",nullable:!0},{name:"style_content",type:"string",nullable:!0},{name:"config_schema",type:"json",nullable:!0},{name:"config_values",type:"json",nullable:!0},{name:"target_hooks",type:"json",nullable:!0},{name:"is_builtin",type:"bool",nullable:!0},{name:"tags",type:"json",nullable:!0},{name:"created_at",type:"date",nullable:!0},{name:"updated_at",type:"date",nullable:!0}]}},bG={settings:"setting_items",storages:"storages",users:"users",shares:"sharing_dbs",metas:"metas",plugins:"plugins",sshkeys:"ssh_public_keys"};bue=Tl("sqlite"),vue=Tl("mysql"),a3=[`CREATE TABLE IF NOT EXISTS ${zo("kv")} (${zo("key")} TEXT PRIMARY KEY, ${zo("value")} TEXT NOT NULL)`],c3=["CREATE TABLE IF NOT EXISTS `kv` (`key` VARCHAR(512) PRIMARY KEY, `value` LONGTEXT NOT NULL)"]});function l3(r){if(!r||typeof r!="object")return!1;try{return typeof r.prepare=="function"}catch{return!1}}function zn(r){let e=typeof globalThis<"u"?globalThis:{};for(let t of["DB","OPENLIST_DB"]){let i=r?.[t];if(l3(i))return i;let n=e?.[t];if(l3(n))return n}return null}async function jo(r,e){if(!d3.get(r)){for(let t of[...a3,...Tl("sqlite",e)])await r.prepare(t).run();d3.set(r,!0)}}var d3,hC,u3=J(()=>{"use strict";ju();d3=new WeakMap;hC={name:"d1",async isAvailable(r){return zn(r)!=null},async init(r){let e=zn(r);e&&await jo(e)},async get(r,e){let t=zn(e);if(!t)throw new Error("D1 binding not found");return await jo(t,e),(await t.prepare("SELECT value FROM kv WHERE key = ?").bind(r).first())?.value||null},async put(r,e,t){let i=zn(t);if(!i)throw new Error("D1 binding not found");await jo(i,t),await i.prepare("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)").bind(r,e).run()},async delete(r,e){let t=zn(e);if(!t)throw new Error("D1 binding not found");await jo(t,e),await t.prepare("DELETE FROM kv WHERE key = ?").bind(r).run()},async list(r,e){let t=zn(e);if(!t)throw new Error("D1 binding not found");return await jo(t,e),((await t.prepare("SELECT key FROM kv WHERE key LIKE ? ORDER BY key").bind(`${r}%`).all()).results||[]).map(n=>n.key)},async query(r,e,t){let i=zn(t);if(!i)throw new Error("D1 binding not found");return await jo(i,t),(await i.prepare(r).bind(...e).all()).results||[]},async execute(r,e,t){let i=zn(t);if(!i)throw new Error("D1 binding not found");await jo(i,t),await i.prepare(r).bind(...e).run()},async batch(r,e){let t=zn(e);if(!t)throw new Error("D1 binding not found");await jo(t,e);let i=100,n=r.map(s=>t.prepare(s.sql).bind(...s.params));for(let s=0;s{"use strict";pC={name:"do",async isAvailable(r){return gC(r)!=null},async init(r){let e=Ps(r);e&&typeof e.init=="function"&&await e.init()},async get(r,e){let t=Ps(e);if(!t)throw new Error("DO binding not found");return await t.kvGet(r)},async put(r,e,t){let i=Ps(t);if(!i)throw new Error("DO binding not found");await i.kvPut(r,e)},async delete(r,e){let t=Ps(e);if(!t)throw new Error("DO binding not found");await t.kvDelete(r)},async list(r,e){let t=Ps(e);if(!t)throw new Error("DO binding not found");return await t.kvList(r)},async query(r,e,t){let i=Ps(t);if(!i)throw new Error("DO binding not found");return await i.sqlQuery(r,e)},async execute(r,e,t){let i=Ps(t);if(!i)throw new Error("DO binding not found");await i.sqlExecute(r,e)},async batch(r,e){let t=Ps(e);if(!t)throw new Error("DO binding not found");await t.sqlBatch(r)},async health(r){if(!gC(r))return{configured:!1,connected:!1,platform:"Cloudflare Durable Objects",mode:"do",error:"DO binding not found (expected env.DO)"};try{return await Ps(r).health(),{configured:!0,connected:!0,platform:"Cloudflare Durable Objects (SQLite)",mode:"do",doId:A3(r)}}catch(t){return{configured:!0,connected:!1,platform:"Cloudflare Durable Objects",mode:"do",error:t?.message||String(t)}}}}});function g3(){return typeof process<"u"&&process.release?.name==="node"}function mC(r){let e=r||(typeof process<"u"?process.env:{})||{},t=e?.MYSQL_URLS;if(t)return t;let i=e?.MYSQL_HOST;return i?{host:i,port:Number(e?.MYSQL_PORT||3306),user:e?.MYSQL_USER||"",password:e?.MYSQL_PASS||"",database:e?.MYSQL_NAME||""}:null}async function Ts(r){let e=mC(r);if(!e)return null;let t=JSON.stringify(e);if(Xh&&p3===t)return Xh;let i="mysql2/promise",{createPool:n}=await import(i);return Xh=n(e),p3=t,Xh}async function $o(r,e){let t=fC(e);if(m3!==t){for(let i of[...c3,...Tl("mysql",e)])await r.query(i);m3=t}}var Xh,p3,m3,yC,y3=J(()=>{"use strict";ju();Xh=null,p3=null;m3=null;yC={name:"mysql",async isAvailable(r){return g3()?mC(r)!=null:!1},async init(r){let e=await Ts(r);e&&await $o(e)},async get(r,e){let t=await Ts(e);if(!t)throw new Error("MySQL pool not available");await $o(t,e);let[i]=await t.query("SELECT `value` FROM `kv` WHERE `key` = ?",[r]);return i?.[0]?.value||null},async put(r,e,t){let i=await Ts(t);if(!i)throw new Error("MySQL pool not available");await $o(i,t),await i.query("INSERT INTO `kv` (`key`, `value`) VALUES (?, ?) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)",[r,e])},async delete(r,e){let t=await Ts(e);if(!t)throw new Error("MySQL pool not available");await $o(t,e),await t.query("DELETE FROM `kv` WHERE `key` = ?",[r])},async list(r,e){let t=await Ts(e);if(!t)throw new Error("MySQL pool not available");await $o(t,e);let[i]=await t.query("SELECT `key` FROM `kv` WHERE `key` LIKE ? ORDER BY `key`",[`${r}%`]);return(i||[]).map(n=>n.key)},async query(r,e,t){let i=await Ts(t);if(!i)throw new Error("MySQL pool not available");await $o(i,t);let[n]=await i.query(r,e);return n||[]},async execute(r,e,t){let i=await Ts(t);if(!i)throw new Error("MySQL pool not available");await $o(i,t),await i.query(r,e)},async batch(r,e){let t=await Ts(e);if(!t)throw new Error("MySQL pool not available");await $o(t,e);let i=await t.getConnection();try{await i.beginTransaction();for(let n of r)await i.query(n.sql,n.params);await i.commit()}catch(n){try{await i.rollback()}catch{}throw n}finally{i.release()}},async health(r){if(!g3())return{configured:!1,connected:!1,platform:"MySQL (mysql2)",mode:"mysql",error:"MySQL backend requires Node.js runtime"};if(!mC(r))return{configured:!1,connected:!1,platform:"MySQL (mysql2)",mode:"mysql",error:"MySQL config not found (expected MYSQL_URLS or MYSQL_HOST)"};try{return await(await Ts(r)).query("SELECT 1"),{configured:!0,connected:!0,platform:"MySQL (mysql2)",mode:"mysql"}}catch(t){return{configured:!0,connected:!1,platform:"MySQL (mysql2)",mode:"mysql",error:t?.message||String(t)}}}}});var $u,Zh,I3=J(()=>{"use strict";$u=new Map,Zh={name:"memory",async isAvailable(){return!0},async init(){},async get(r){return $u.get(r)??null},async put(r,e){$u.set(r,e)},async delete(r){$u.delete(r)},async list(r){return[...$u.keys()].filter(e=>e.startsWith(r))},async health(){return{configured:!0,connected:!0,platform:"Memory",mode:"memory",hasData:$u.size>0,error:null}}}});var w3,x3,C3=J(()=>{"use strict";w3="openlist_config",x3={name:"map",async load(r,e){let t=await r.get(w3,e);if(!t)return null;try{return JSON.parse(t)}catch(i){return console.error("[mapFormat] Failed to parse JSON:",i),null}},async save(r,e,t){let i=JSON.stringify(r);return await e.put(w3,i,t),!0}}});function _G(r){return r>="a"&&r<="z"||r>="A"&&r<="Z"||r>="0"&&r<="9"||r==="_"}function E3(r){let e=String(r??"");if(e==="")return"0";if(/^[A-Za-z0-9_]+$/.test(e))return e;let t="";for(let i of e)if(_G(i))t+=i;else for(let n of new TextEncoder().encode(i))t+="x"+n.toString(16).padStart(2,"0");return t}function IC(r){return`${E3(r)}_`}function B3(r,e){return`${IC(r)}${E3(e)}`}var b3=J(()=>{"use strict"});function eg(r){return IC(r)}function Vu(r,e){return B3(r,e)}var v3,k3,S3=J(()=>{"use strict";ju();b3();v3="openlist_config";k3={name:"key",async load(r,e){if(!await r.get(v3,e))return null;let i={};for(let n of Jo){let s=eg(n),o=await r.list(s,e),a=[];for(let c of o){let l=await r.get(c,e);if(l)try{a.push(JSON.parse(l))}catch(d){console.warn(`[keyFormat] Failed to parse ${c}:`,d)}}i[n]=a}return i},async save(r,e,t){for(let i of Jo){let n=eg(i),s=await e.list(n,t);for(let o of s)await e.delete(o,t)}for(let i of Jo){let n=fc[i],s=r?.[i]||[];for(let o of s){let a=String(o?.[n]??"");if(!a)continue;let c=Vu(i,a),l=JSON.stringify(o);await e.put(c,l,t)}}return await e.put(v3,String(Date.now()),t),!0},async getTable(r,e,t){let i=eg(r),n=await e.list(i,t),s=[];for(let o of n){let a=await e.get(o,t);if(a)try{s.push(JSON.parse(a))}catch(c){console.warn(`[keyFormat] Failed to parse ${o}:`,c)}}return s},async saveTable(r,e,t,i){let n=fc[r],s=eg(r),o=await t.list(s,i);for(let a of o)await t.delete(a,i);for(let a of e){let c=String(a?.[n]??"");if(!c)continue;let l=Vu(r,c),d=JSON.stringify(a);await t.put(l,d,i)}},async getRecord(r,e,t,i){let n=Vu(r,e),s=await t.get(n,i);if(!s)return null;try{return JSON.parse(s)}catch(o){return console.warn(`[keyFormat] Failed to parse ${n}:`,o),null}},async saveRecord(r,e,t,i,n){let s=Vu(r,e),o=JSON.stringify(t);await i.put(s,o,n)},async deleteRecord(r,e,t,i){let n=Vu(r,e);await t.delete(n,i)}}});function Us(r){return"`"+r+"`"}function wC(r,e,t,i){let n=e.map(o=>Us(o)).join(", "),s=e.map(()=>"?").join(", ");if(i.name==="mysql"){let o=e.slice(1).map(c=>`${Us(c)} = VALUES(${Us(c)})`).join(", ");return{sql:o?`INSERT INTO ${r} (${n}) VALUES (${s}) ON DUPLICATE KEY UPDATE ${o}`:`INSERT INTO ${r} (${n}) VALUES (${s})`,params:t}}return{sql:`INSERT OR REPLACE INTO ${r} (${n}) VALUES (${s})`,params:t}}function Jn(r,e){return Us(AC(r,e))}var D3,Q3,_3=J(()=>{"use strict";ju();D3="openlist_config";Q3={name:"sql",async load(r,e){if(!r.query)throw new Error(`Driver ${r.name} does not support SQL queries`);let t=await r.query("SELECT v FROM schema_info WHERE k = ?",[D3],e);if(!t||t.length===0)return null;let i={};for(let n of Jo){let s=await r.query(`SELECT * FROM ${Jn(n,e)}`,[],e);i[n]=s.map(o=>Yh(n,o))}return i},async save(r,e,t){if(!e.batch)throw new Error(`Driver ${e.name} does not support batch operations`);let i=[];for(let n of Jo){let s=fc[n],o=r?.[n]||[],a=[];for(let l of o){let{columns:d,values:u}=Wh(n,l),f=wC(Jn(n,t),d,u,e);i.push({sql:f.sql,params:f.params});let h=l?.[s];h!=null&&a.push(String(h))}let c=a.length>0?`DELETE FROM ${Jn(n,t)} WHERE ${Us(s)} NOT IN (${a.map(()=>"?").join(", ")})`:`DELETE FROM ${Jn(n,t)}`;i.push({sql:c,params:a})}return i.push(wC(Us("schema_info"),["k","v"],[D3,String(Date.now())],e)),await e.batch(i,t),!0},async getTable(r,e,t){if(!e.query)throw new Error(`Driver ${e.name} does not support SQL queries`);let i=r;return(await e.query(`SELECT * FROM ${Jn(i,t)}`,[],t)).map(s=>Yh(i,s))},async saveTable(r,e,t,i){if(!t.batch)throw new Error(`Driver ${t.name} does not support batch operations`);let n=r,s=[{sql:`DELETE FROM ${Jn(n,i)}`,params:[]}];for(let o of e){let{columns:a,values:c}=Wh(n,o),l=a.map(()=>"?").join(", "),d=`INSERT INTO ${Jn(n,i)} (${a.map(u=>Us(u)).join(", ")}) VALUES (${l})`;s.push({sql:d,params:c})}await t.batch(s,i)},async getRecord(r,e,t,i){if(!t.query)throw new Error(`Driver ${t.name} does not support SQL queries`);let n=r,s=fc[n],o=await t.query(`SELECT * FROM ${Jn(n,i)} WHERE ${Us(s)} = ?`,[e],i);return o.length>0?Yh(n,o[0]):null},async saveRecord(r,e,t,i,n){if(!i.execute)throw new Error(`Driver ${i.name} does not support SQL execution`);let s=r,{columns:o,values:a}=Wh(s,t),{sql:c}=wC(Jn(s,n),o,a,i);await i.execute(c,a,n)},async deleteRecord(r,e,t,i){if(!t.execute)throw new Error(`Driver ${t.name} does not support SQL execution`);let n=r,s=fc[n];await t.execute(`DELETE FROM ${Jn(n,i)} WHERE ${Us(s)} = ?`,[e],i)}}});var L3={};Cn(L3,{MEMORY_SERVERLESS_MESSAGE:()=>R3,NO_STORAGE_MESSAGE:()=>Vo,getStorageBackend:()=>vC,getStoreBackend:()=>ig,getStoreConfigErrorDetail:()=>hc,getStoreStatus:()=>$n,isPersistentStorageAvailable:()=>ng,isServerlessRuntime:()=>jn,readCipher:()=>Ac,readDriver:()=>tg,readFormat:()=>rg});function bC(r,e,t){let i=t||(typeof process<"u"?process.env:{})||{};return String(i[r]||"").trim().toLowerCase()||e}function tg(r){let e=r||(typeof process<"u"?process.env:{})||{},t=bC("DB_DRIVER","auto",r);return t==="json"?(console.warn("[DEPRECATED] DB_DRIVER=json is deprecated. Use DB_FORMAT=map instead."),"auto"):t}function rg(r){let e=r||(typeof process<"u"?process.env:{})||{};return String(e.DB_DRIVER||"").trim().toLowerCase()==="json"?"map":bC("DB_FORMAT","map",r)}function Ac(r){let e=bC("DB_CIPHER",Nu,r),{cipher:t,known:i}=kD(e);return i||console.warn(`[DB] Unknown DB_CIPHER "${e}"; falling back to "${Nu}". Valid values: ${vD.join(", ")}.`),t}function jn(r){let e=globalThis;try{if(r?.__requestOrigin||r?.__requestContext||r?.__makersContext||r?.EDGEONE_BLOB||e?.EDGEONE_BLOB||typeof e?.EdgeOne<"u"||typeof e.WebSocketPair=="function"||typeof e.caches<"u"&&e.caches?.default||r?.ESA_BLOB||e?.ESA_BLOB||typeof e?.ESA<"u"||r?.TENCENTCLOUD_SCF_FUNCTIONNAME||typeof process<"u"&&process.env?.TENCENTCLOUD_SCF_FUNCTIONNAME)return!0}catch{}return!1}async function U3(r){let e=[];FG(r)&&e.push(yC),e.push(hC,jh,uC,dC,pC);for(let t of e)if(await t.isAvailable(r))return console.log(`[DB] Auto-detected driver: ${t.name}`),t;if(jn(r))throw Yu("NO_STORAGE",Vo,PG(r));return console.warn("[DB] No storage binding detected, falling back to memory (data will not persist)."),Zh}function FG(r){let e=r||{},t=typeof process<"u"?process.env||{}:{};return!!(e.MYSQL_URLS||t.MYSQL_URLS||e.MYSQL_HOST||t.MYSQL_HOST)}function PG(r){let e=globalThis,t=!!(r?.EDGEONE_BLOB||e?.EDGEONE_BLOB)||typeof e?.EdgeOne<"u"||!!(r?.TENCENTCLOUD_SCF_FUNCTIONNAME||typeof process<"u"&&process.env?.TENCENTCLOUD_SCF_FUNCTIONNAME),i=!!(r?.ESA_BLOB||e?.ESA_BLOB)||typeof e?.ESA<"u",n=!t&&!i;return t?"DB_DRIVER=auto already probed every driver. On EdgeOne, bind a Blob store (or enable the EdgeOne Blob SDK), or bind a KV namespace to an Edge Function and proxy it (Node functions do not receive KV bindings).":i?"DB_DRIVER=auto already probed every driver. On Alibaba ESA, bind an ESA_BLOB store, or set DB_DRIVER=blob.":n?'DB_DRIVER=auto already probed every driver. On Cloudflare Workers, add a binding to wrangler.jsonc: D1 as {"d1_databases":[{"binding":"DB"}]} or KV as {"kv_namespaces":[{"binding":"KV"}]}, then redeploy so the binding is injected.':"DB_DRIVER=auto already probed every driver. Bind a persistent backend (Cloudflare: D1/KV; EdgeOne: Blob; ESA: ESA_BLOB), then redeploy."}function Yu(r,e,t){let i=new Error(e);return i.storeCode=r,t&&(i.storeHint=t),i}function TG(r){let e=r?.storeHint;return typeof e=="string"&&e.trim()?e.trim():null}function N3(r){return r?.storeCode||"DRIVER_ERROR"}async function NG(r,e){let t=`${e}:${jn(r)?"sl":"local"}:${q3(r)}`;if(xC?.key===t)return xC.driver;let i=null;try{let n=await U3(r);n&&n!==Zh&&n.name!==e&&(i=n)}catch{}return xC={key:t,driver:i},i}function qG(r){return r?`Auto-detection would pick: DB_DRIVER=${r.name} (or simply set DB_DRIVER=auto). -`:""}function MG(r,e){let t=r,i=!!(t.get&&t.put&&t.delete&&t.list),n=!!(r.query&&r.execute&&r.batch),s=e.name==="sql"?n:i,o=OG[r.name],a=!o||o.includes(e.name);if(s&&a)return;let c=(o??[i?"map | key":null,n?"sql":null].filter(Boolean)).join(" | ");throw Yu("INVALID_COMBINATION",`Invalid storage combination: DB_FORMAT="${e.name}" cannot be used with DB_DRIVER="${r.name}". -Driver "${r.name}" supports: ${c||"no format"}. -`+(e.name==="sql"?`The "sql" format needs a relational driver (SQL query support): d1 | do | mysql. -`:`The "${e.name}" format needs a key-value driver (get/put/delete/list): kv | cfkv | d1 | do | mysql. -`)+`Fix DB_FORMAT or DB_DRIVER. See ${UG}`,e.name==="sql"?"Set DB_FORMAT=map (or key), or switch to a relational driver (DB_DRIVER=d1 | do | mysql).":`Set DB_FORMAT=map for DB_DRIVER="${r.name}"`+(c.includes("|")?`, or one of: ${c}.`:"."))}async function LG(r,e){if(r==="auto")return await U3(e);if(r==="memory"){if(!jn(e))return console.log("[DB] Using explicitly configured driver: memory"),Zh;throw Yu("NO_STORAGE",R3,"Use DB_DRIVER=auto, or bind a persistent backend (D1 / KV / Blob).")}let t=F3[r];if(!t)throw Yu("UNKNOWN_DRIVER",`Unknown DB_DRIVER "${r}". Valid values: auto, memory, ${Object.keys(F3).join(", ")}`);let i=!1;try{i=await t.isAvailable(e)}catch{i=!1}if(!i){let n=await NG(e,r);throw Yu("DRIVER_UNAVAILABLE",`DB_DRIVER is set to "${r}", but that driver is not available in this runtime. -No fallback is performed for an explicitly configured driver. -`+qG(n)+`Check the binding/credentials for "${r}", or set DB_DRIVER=auto to let the platform pick an available backend. -`+(RG[r]||"")+`Environment: ${jn(e)?"serverless/worker":"local/container"}`,n?`Set DB_DRIVER=${n.name} (or DB_DRIVER=auto) in your deployment variables.`:`Set DB_DRIVER=auto in your deployment variables, or provide the binding/credentials required by "${r}".`)}return console.log(`[DB] Using explicitly configured driver: ${t.name}`),t}function HG(r){switch(r){case"map":return x3;case"key":return k3;case"sql":return Q3;default:throw new Error(`Unknown format: ${r}`)}}function q3(r){if(r&&(typeof r=="object"||typeof r=="function")){let e=T3.get(r);return e===void 0&&(e=++KG,T3.set(r,e)),String(e)}return"none"}async function vC(r){let e=tg(r),t=rg(r),i=jn(r)?"sl":"local",n=`${e}:${t}:${i}:${q3(r)}`;if(CC&&EC&&P3===n)return{driver:CC,format:EC};let s=await LG(e,r),o=HG(t);if(MG(s,o),s.init)try{await s.init(r)}catch(a){console.warn(`[DB] Driver init failed (${s.name}):`,a)}return CC=s,EC=o,P3=n,console.log(`[DB] Using driver=${s.name}, format=${o.name}`),{driver:s,format:o}}async function ig(r){let{driver:e,format:t}=await vC(r);return{name:e.name,load:i=>t.load(e,i),save:(i,n)=>t.save(i,e,n),isConfigured:i=>e.isAvailable(i),init:i=>e.init(i),health:i=>e.health(i)}}async function $n(r){let e=null,t=null,i=null,n=null;try{let o=await vC(r);e=o.driver,t=o.format}catch(o){let a=String(o?.message||o),c=a.includes("No storage backend is available");return{driver:"none",format:"none",available:!1,configError:c?Vo:a,configErrorCode:N3(o),configSuggestion:TG(o)||(c?"Bind a persistent storage backend, then redeploy.":null)}}let s=null;try{s=await e.health(r)}catch(o){s={connected:!1,error:o?.message||String(o)}}return{driver:e.name,format:t.name,...s||{}}}async function ng(r){let e=await O3(r);return M3(e)}async function O3(r){try{return await $n(r)}catch(e){return{driver:"none",format:"none",available:!1,configError:String(e?.message||e),configErrorCode:N3(e)}}}function M3(r){let e=String(r?.driver??"none"),t=e!=="none"&&e!=="",i=e==="memory",n=!!r?.configError,s=r?.available!==!1;return t&&!i&&!n&&s}async function hc(r,e={}){let t=(d,u)=>{if(e.silent)return;let f=d+u;BC!==f&&(BC=f,console.error(d+u))};if(!r||typeof r!="object")return{code:null,message:null,suggestion:null};let i=d=>(t(`[DB] KV proxy configuration error: -`,d),{code:"PROXY_CONFIG",message:d,suggestion:"Set EO_KV_URLS to the correct deployment origin, or use DB_DRIVER=auto."}),n=String(r?.DB_DRIVER||"").trim().toLowerCase()==="kv";if(n){let d=Ql(r);if(d)return i(d)}let s=await O3(r);if(M3(s))return BC=null,{code:null,message:null,suggestion:null};if(!n&&String(s?.driver??"")==="kv"){let d=Ql(r);if(d)return i(d);if(s?.mode==="proxy"&&s?.error?.includes("401")){let u=`KV proxy rejected the internal call (HTTP 401). The JWT_SECRET used by this deployment does not match the one configured on the Edge Functions serving the proxy. Make sure both use the same JWT_SECRET. -Alternatively set EO_KV_URLS to the correct deployment origin.`;return t(`[DB] KV proxy authentication failed: -`,u),{code:"PROXY_CONFIG",message:u,suggestion:"Use the same JWT_SECRET on the Edge Function and this deployment."}}}let o=s?.configError?String(s.configError):null;if(o)return t(`[DB] Storage configuration error: -`,o),{code:s?.configErrorCode||"DRIVER_ERROR",message:o,suggestion:s?.configSuggestion||null};if(String(s?.driver??"none")==="memory")return t(`[DB] Storage configuration error: -`,Vo),{code:"NO_STORAGE",message:Vo,suggestion:"Bind a storage backend (D1 / KV / Blob) or set DB_DRIVER=auto."};let a=s?.error?String(s.error):null;if(a)return t(`[DB] Storage unhealthy: -`,a),{code:"HEALTH_ERROR",message:a,suggestion:"Check the credentials/bindings of the configured driver, or set DB_DRIVER=auto."};let c=String(s?.driver??"none"),l=c==="none"||c===""?Vo:`Storage driver "${c}" is not available in this runtime. Check its configuration and bindings, or set DB_DRIVER=auto.`;return t(`[DB] Storage unavailable: -`,l),{code:c==="none"||c===""?"NO_STORAGE":"DRIVER_ERROR",message:l,suggestion:c==="none"||c===""?"Bind a storage backend (D1 / KV / Blob) or set DB_DRIVER=auto.":`Set DB_DRIVER=auto, or provide the binding/credentials required by "${c}".`}}var Vo,R3,F3,UG,RG,xC,OG,CC,EC,P3,T3,KG,BC,gc=J(()=>{"use strict";s3();o3();lC();u3();h3();y3();I3();C3();S3();_3();Gr();Vo=`No storage backend is available. Data cannot be persisted in this runtime (serverless environments cannot use in-memory storage). -Configure one of the following: - 1. EdgeOne Blob (recommended, zero config if the project provides it) - 2. EdgeOne KV: bind a KV namespace to Edge Functions, then set DB_DRIVER=kv (DB_FORMAT=map or key) and JWT_SECRET - 3. Cloudflare KV / D1: bind the namespace and set DB_DRIVER accordingly -Environment variables to set in the project settings: - DB_DRIVER=blob | kv | cfkv | d1 | do | mysql - DB_FORMAT=map | key | sql`,R3=`DB_DRIVER="memory" is only valid on a local Node runtime. -This deployment is serverless, where in-memory storage loses all data immediately. -Set DB_DRIVER=auto, or bind a persistent backend (D1 / KV / Blob).`,F3={blob:dC,cfkv:uC,kv:jh,d1:hC,do:pC,mysql:yC};UG="https://doc.oplist.org/ecosystem/official_worker/guide",RG={kv:`The "kv" driver requires one of the following: - - Cloudflare Workers: a KV namespace binding named exactly "KV" (wrangler.jsonc: "kv_namespaces": [{ "binding": "KV" }]); - - EdgeOne Node Functions: KV is NOT injected into Node functions, so the Edge Function KV proxy must be reachable (known request origin / EO_KV_URLS) and JWT_SECRET (identical on the Edge Function side) must be set. -If neither applies, use DB_DRIVER=auto, DB_DRIVER=blob (EdgeOne) or DB_DRIVER=d1 (Cloudflare). -`,d1:`The "d1" driver requires a Cloudflare D1 binding named "DB" (wrangler.jsonc: "d1_databases": [{ "binding": "DB", ... }]). -EdgeOne has no D1 \u2014 use DB_DRIVER=blob there instead. -`,cfkv:`The "cfkv" driver requires Cloudflare API credentials: CF_ACCOUNT (or CLOUDFLARE_ACCOUNT_ID), CF_KV_UUID (or CLOUDFLARE_KV_NAMESPACE_ID) and CF_API_KEY (or CLOUDFLARE_API_TOKEN) with KV read/write permission. -`,do:`The "do" driver requires a Durable Objects namespace binding named "DO" (wrangler.jsonc: "durable_objects": { "bindings": [{ "name": "DO", "class_name": "..." }] } plus a matching migration). -`,mysql:`The "mysql" driver requires a Node runtime plus connection info (MYSQL_URLS, or MYSQL_HOST/MYSQL_PORT/MYSQL_USER/MYSQL_PASS/MYSQL_NAME). Cloudflare Workers cannot open raw TCP connections to MySQL. -`,blob:`The "blob" driver requires either the EdgeOne Blob SDK (only present inside the EdgeOne Makers runtime) or an ESA_BLOB binding on Alibaba ESA. -`},xC=null;OG={blob:["map"],kv:["map","key"],cfkv:["map","key"],d1:["map","key","sql"],do:["map","key","sql"],mysql:["map","key","sql"],memory:["map","key"]};CC=null,EC=null,P3=null,T3=new WeakMap,KG=0;BC=null});async function GG(){if(sg)return sg;if(H3>=3)return null;H3++;try{let{getStore:r}=await Promise.resolve().then(()=>xr(cC(),1));sg=r({name:"openlist_db",consistency:"strong"})}catch{return null}return sg}function G3(){K3||(K3=!0,!(typeof process>"u"||typeof process.on!="function")&&process.on("uncaughtException",r=>{(r?.message?.includes("RESP")||r?.message?.includes("Unknown type")||r?.stack?.includes("processResponses"))&&console.error("[KV/RESP] Caught uncaught exception from storage binding, continuing:",r.message)}))}function JG(r){if(!r||typeof r!="object")return null;let e=j3.get(r);return!e||e.mode==="none"&&Date.now()-e.ts>=zG?null:e}function Wu(r,e){let t={...e,ts:Date.now()};return r&&typeof r=="object"&&j3.set(r,t),t}function $3(r){r&&(kC=r)}function z3(r){if(!r||typeof r!="object")return!1;try{return typeof r.get!="function"?!1:typeof r.put=="function"||typeof r.set=="function"}catch{return!1}}function jG(r,e){let t=async()=>(await Promise.resolve().then(()=>(lC(),i3))).kvDriver;return{async get(i){return(await t()).get(i,e)},async put(i,n){return(await t()).put(i,n,e)},async delete(i){return(await t()).delete(i,e)},async list(i={}){return{keys:(await(await t()).list(i?.prefix||"",e)).map(s=>({name:s,key:s}))}}}}async function SC(r){r&&(kC=r);let e=r||kC||(typeof process<"u"?process.env:{}),t=typeof globalThis<"u"?globalThis:{},i=JG(e);if(i)return i;let n=z3(e?.KV)?e.KV:z3(t?.KV)?t.KV:null;if(String(e?.DB_DRIVER||"").trim().toLowerCase()==="kv"&&!n){let c;try{c=e?.EO_KV_URLS||e?.__requestOrigin}catch{c=void 0}let l=Dl(c,e);if(l){let d={binding:jG(l,e),platform:"EdgeOne KV (via Edge Function proxy)",mode:"proxy"};return Wu(e,d)}console.warn("[DB] getKvBinding: KV proxy requested but no origin available (set EO_KV_URLS or ensure request origin is injected)")}try{let c=await GG();if(c)return G3(),Wu(e,{binding:c,platform:"EdgeOne Blob (@edgeone/pages-blob, strong consistency)",mode:"blob"})}catch(c){console.error(`[DB] getKvBinding: EdgeOne Blob init failed: ${c?.message||c}`,`stack=${c?.stack?.substring(0,300)||""}`)}if(n){let c=!!(e&&(e.EDGEONE||e.EO_REGION))||!!(t.EDGEONE||typeof t.EdgeOne<"u");return c&&G3(),Wu(e,{binding:n,platform:c?"EdgeOne KV (KV)":"Cloudflare / EdgeOne KV (KV)",mode:"binding"})}{let c=e.CF_ACCOUNT||(typeof process<"u"?process.env.CF_ACCOUNT:""),l=e.CF_KV_UUID||(typeof process<"u"?process.env.CF_KV_UUID:""),d=e.CF_API_KEY||(typeof process<"u"?process.env.CF_API_KEY:"");if(c&&l&&d)return Wu(e,{binding:{type:"cf_rest",accountId:c,namespaceId:l,token:d},platform:"Cloudflare KV (REST API)",mode:"api"})}let o=String(e?.DB_DRIVER||"").trim().toLowerCase();return(!o||o==="auto"||o==="kv"||o==="cfkv"||o==="blob")&&!J3&&(J3=!0,console.warn(o&&o!=="auto"?`[DB] getKvBinding: no KV-style binding found; DB_DRIVER="${o}" is served by getStorageBackend() instead. This is expected.`:"[DB] getKvBinding: no KV/Blob binding found in auto detection.")),Wu(e,{binding:null,platform:"none",mode:"none"})}async function $G(r,e="openlist_config"){let{binding:t,mode:i}=r;if(i==="none"||!t)return null;try{if(i==="blob"){let n=await t.get(e,{type:"json"});if(n)return n;let s=await t.get(e);if(s)return typeof s=="string"?JSON.parse(s):s}else if(i==="binding"||i==="proxy"){let n=null;try{n=await t.get(e,"text")}catch{n=await t.get(e)}if(n==null&&(n=await t.get(e)),n)return typeof n=="string"?JSON.parse(n):n}else if(t.type==="cf_rest"){let n=`https://api.cloudflare.com/client/v4/accounts/${t.accountId}/storage/kv/namespaces/${t.namespaceId}/values/${e}`,s=await fetch(n,{headers:{Authorization:`Bearer ${t.token}`}});if(s.ok){let o=await s.text();return JSON.parse(o)}}}catch(n){console.error("[KV/Blob Store] Error reading key:",e,n)}return null}async function Ul(r){let e=await SC(r),t=e.mode!=="none",i=!1,n=null;if(t)try{let s=await $G(e,"openlist_config");return i=!0,{configured:!0,connected:!0,platform:e.platform,mode:e.mode,hasData:!!s,error:null}}catch(s){n=s.message||String(s)}return{configured:t,connected:i,platform:e.platform,mode:e.mode,hasData:!1,error:n}}async function V3(r){let{getStorageBackend:e}=await Promise.resolve().then(()=>(gc(),L3)),{driver:t}=await e(r);return t}async function Rs(r,e){try{let i=await(await V3(r)).get(e,r);if(i==null)return null;let n=typeof i?.text=="function"?await i.text():i;return String(n).trim()||null}catch(t){return console.warn(`[Secret] read "${e}" failed:`,t),null}}async function og(r,e,t){try{return await(await V3(r)).put(e,t,r),!0}catch(i){return console.warn(`[Secret] cannot persist "${e}": ${i?.message||i}`),!1}}function Y3(){let r=new Uint8Array(32);return crypto.getRandomValues(r),Array.from(r,e=>e.toString(16).padStart(2,"0")).join("")}var sg,H3,K3,kC,zG,j3,J3,DC=J(()=>{"use strict";Hh();Hh();sg=null,H3=0;K3=!1;kC=null,zG=1e3,j3=new WeakMap;J3=!1});var Jr={};Cn(Jr,{ENCRYPTION_SECRET_KV_KEY:()=>Xo,RECOMMENDED_JWT_SECRET_LENGTH:()=>iQ,__resetDbCacheForTest:()=>tz,__setStoreBackendLoaderForTest:()=>rz,defaultDb:()=>pc,ensureEncryptionSecret:()=>OC,getDb:()=>ne,getDbLoadError:()=>pg,getKvBinding:()=>SC,getKvStatus:()=>Ul,getMetas:()=>gz,getPlugins:()=>pz,getSettings:()=>Vn,getStorages:()=>hz,getStoreStatus:()=>$n,getUsers:()=>Az,isDbShell:()=>tQ,isDbTrusted:()=>UC,isDbWriteBlocked:()=>XG,isEncryptionReady:()=>yg,isJwtSecretShort:()=>iz,readPersistedSecret:()=>Rs,resolvePath:()=>ct,saveDb:()=>fe,setEnvCtx:()=>RC,writePersistedSecret:()=>og});function UC(){return Ns}function pg(){return Wo}function XG(){return hg}function tQ(r){if(!r||typeof r!="object")return!0;let e=Array.isArray(r.storages)?r.storages.length:0,t=Array.isArray(r.shares)?r.shares.length:0,i=Array.isArray(r.metas)?r.metas.length:0,n=Array.isArray(r.plugins)?r.plugins.length:0;if(e+t+i+n>0||(Array.isArray(r.users)?r.users:[]).some(d=>String(d?.password||"").trim()!==""))return!1;let a=Array.isArray(r.settings)?r.settings:[],c=Array.isArray(pc.settings)?pc.settings:[],l=new Map(c.map(d=>[String(d.key),d.value]));return a.every(d=>l.has(String(d.key))&&l.get(String(d.key))===d.value)}function RC(r){r&&(ai=r,$3(r))}function qC(r){let e=r?.JWT_SECRET||(typeof process<"u"?process.env?.JWT_SECRET:"");return typeof e=="string"&&e.trim().length>0?e:null}function iz(r){return typeof r=="string"&&r.length>0&&r.length0)return qs=i,Yo=!1,i}catch{}return _C||(_C=!0,console.error("[DB] No encryption key available: set JWT_SECRET. Encrypted fields cannot be decrypted and new sensitive fields will be stored in plaintext until a key becomes available.")),null}async function yg(r){let e=r||ai||(typeof process<"u"?process.env:{});if(qC(e))return!0;try{let t=await Rs(e,Xo);return!!(t&&t.trim().length>0)}catch{return!1}}function az(r){return new Promise(e=>setTimeout(e,r))}async function OC(r){if(Xu)return Xu;Xu=(async()=>{let e=r||ai||(typeof process<"u"?process.env:{}),t=qC(e);if(t)return t;let i=await Rs(e,Xo);if(i&&i.trim().length>0)return qs=i,Yo=!1,i;let n=Y3();if(!await og(e,Xo,n))return console.error("[DB] Failed to persist an encryption key. Sensitive fields will be stored in plaintext until JWT_SECRET is configured via environment variable."),null;qs=n,Yo=!1;let o=sz;for(let a=0;a0&&c!==n)return console.warn("[DB] A different encryption key already exists; adopting it to keep encrypt/decrypt symmetric."),qs=c,Yo=!1,c;await az(o),o=Math.min(o*2,oz)}return console.warn(`[DB] Encryption key written but not yet readable after ${X3} retries. Cross-instance reads may lag briefly due to eventual consistency; retry shortly.`),n})();try{return await Xu}finally{Xu=null}}function nQ(r,e,t,i,n){if(Rl.size>=cz){let s=Rl.keys().next().value;s!==void 0&&Rl.delete(s)}Rl.set(r,{plain:e,sealed:t,fp:i,cipher:n})}function lz(r,e,t,i){let n=Rl.get(r);return!n||n.fp!==t||n.cipher!==i||n.plain!==e?null:n.sealed}async function ug(r,e,t){if(!r||!e||e.cipher==="none"||lc(r))return r;let i=lz(t,r,e.fingerprint,e.cipher);if(i)return i;let n=SD(e.cipher)+await e.encrypt(r);return nQ(t,r,n,e.fingerprint,e.cipher),n}async function fg(r,e,t){if(!r||!lc(r))return r;if(!e)return FC||(FC=!0,console.error("[DB] Sealed values found in storage but no encryption key is available; they will be left as-is (set JWT_SECRET, or restore the original openlist_encryption_secret).")),r;let i=Ko(r);try{let n=await e.decrypt(r);return i&&i.cipher===e.cipher&&nQ(t,n,r,e.fingerprint,i.cipher),n}catch(n){return console.warn("[DB] Failed to decrypt a sealed secret (wrong JWT_SECRET?):",n),r}}function dz(r){if(!r)return!1;for(let e of r.storages||[])if(e&&Ko(e.addition))return!0;for(let e of r.settings||[])if(e&&NC.has(e.key)&&Ko(e.value))return!0;for(let e of r.users||[])if(e&&(Ko(e.otp_secret)||Ko(e.password)))return!0;return!1}async function sQ(r,e){let t=Ac(r);if(t==="none"&&!e?.needDecrypt)return null;let i=await nz(r);return i?FD(t,i):null}async function uz(r,e){if(!e||e.cipher==="none"||!r)return r;let t,i,n,s=Array.isArray(r.storages)?r.storages:[];for(let l=0;l{c.addition=await fg(l,e,d)})}}let n=Array.isArray(r.settings)?r.settings:[];for(let o=0;o{c.value=await fg(l,e,d)})}}let s=Array.isArray(r.users)?r.users:[];for(let o=0;o{c.otp_secret=await fg(l,e,d)})}if(typeof a.password=="string"&&lc(a.password)){let c=a,l=c.password,d=`users:${a.id??o}:password`;t.push(async()=>{c.password=await fg(l,e,d)})}}}for(let o=0;oa()))}async function ct(r,e){let t=await ne(e),i=r;try{i=decodeURIComponent(String(i||""))}catch{}let n="",s=0;for(;i!==n&&s<3;){n=i;try{let f=decodeURIComponent(i);if(f===i)break;i=f,s++}catch{break}}i=i.replace(/\\/g,"/").replace(/%5c/gi,"/").replace(/%2f/gi,"/").replace(/\.{3,}/g,"..").replace(/\/+/g,"/");let o=["\0","\r",` -`," "];for(let f of o)if(i.includes(f))throw new Error(`invalid path: illegal character detected (0x${f.charCodeAt(0).toString(16)})`);if(/^[A-Za-z]:/.test(i))throw new Error("invalid path: absolute Windows path not allowed");if(i.startsWith("//")||i.startsWith("\\\\"))throw new Error("invalid path: UNC path not allowed");let a=[];for(let f of i.split("/"))if(!(f===""||f===".")){if(f===".."){a.pop();continue}a.push(f)}let c="/"+a.join("/");c===""&&(c="/");let l=(t.storages||[]).filter(f=>!f.disabled&&typeof f.driver=="string"&&f.driver.trim()!==""&&f.driver!=="undefined"&&f.driver!=="null"&&typeof f.mount_path=="string"&&f.mount_path.trim()!=="");if(l.length===0)throw new Error("failed get storage: storage not found; please add a storage first");let d=[...l].sort((f,h)=>{let g="/"+(f.mount_path||"").split("/").filter(Boolean).join("/");return("/"+(h.mount_path||"").split("/").filter(Boolean).join("/")).length-g.length});for(let f of d){let h="/"+(f.mount_path||"").split("/").filter(Boolean).join("/"),g=h==="/";if(g||c===h||c.startsWith(h+"/")){let I=c;g||(I=c.slice(h.length)),I.startsWith("/")||(I="/"+I);let m={};try{m=typeof f.addition=="string"?JSON.parse(f.addition||"{}"):f.addition||{}}catch{m={}}let B=m.root_folder_path!==void 0?m.root_folder_path:"/",x=([B,I].map(R=>R.replace(/\\/g,"/")).filter(R=>!!R&&R!=="/").join("/")||"/").replace(/\/{2,}/g,"/"),Q=String(B||"/").replace(/\\/g,"/").replace(/\/+$/,"")||"/";if(Q!=="/"&&x!==Q&&!x.startsWith(Q+"/"))throw new Error("path traversal blocked: escapes storage root");if(x.split("/").includes(".."))throw new Error("path traversal blocked: illegal '..' segment");return{storage:f,relative:I,physical:x,rootFolder:B,cleanPath:c,isVirtual:!1}}}let u=!1;for(let f of l){let h="/"+(f.mount_path||"").split("/").filter(Boolean).join("/");if(h!=="/"&&h.startsWith(c==="/"?"/":c+"/")){u=!0;break}}if(u)return{storage:null,relative:c,physical:null,rootFolder:null,cleanPath:c,isVirtual:!0};throw new Error("failed get storage: storage not found")}async function Vn(){let r=await ne(),e={};return r.settings&&r.settings.forEach(t=>{e[t.key]=t.value}),e}async function Az(){return(await ne()).users||[]}async function hz(){return(await ne()).storages||[]}async function gz(){return(await ne()).metas||[]}async function pz(){return(await ne()).plugins||[]}var eQ,VG,YG,WG,pc,ht,ai,Ns,hg,Wo,ZG,QC,ag,cg,lg,dg,ez,gg,Ag,mg,tz,rz,rQ,W3,ne,NC,_C,FC,PC,TC,Xo,iQ,qs,Yo,Xu,X3,sz,oz,cz,Rl,Z3,fe,Ye=J(()=>{"use strict";Gr();DC();gc();DC();gc();eQ="txt,htm,html,xml,java,properties,sql,js,json,c,cpp,python,py,php,go,rst,css,typescript,ts,log,conf,yaml,yml,cmd,bash,sh,vue,ini,md,bat,gitignore,h,hpp,tsx,vtt,srt,ass,rs,lrc,strm",VG="m3u8,url",YG="authorization,referer",WG="txt,htm,html,xml,java,properties,sql,js,json,c,cpp,python,py,php,go,rst,css,typescript,ts,log,conf,yaml,yml,cmd,bash,sh,vue,ini",pc={settings:[{key:"version",value:"v4.2.3",type:"string",help:"Application Version",group:1,flag:1},{key:"site_title",value:"OpenList",type:"string",help:"Site Title",group:1,flag:0},{key:"announcement",value:"",type:"text",help:"Site Announcement",group:1,flag:0},{key:"pagination_type",value:"pagination",type:"select",options:"all,pagination,load_more,auto_load_more",help:"Pagination Type",group:1,flag:0},{key:"default_page_size",value:"20",type:"number",help:"Default Page Size",group:1,flag:0},{key:"allow_indexed",value:"false",type:"bool",help:"Allow Search Engine Indexing",group:1,flag:0},{key:"allow_mounted",value:"true",type:"bool",help:"Allow Mounted Storages",group:1,flag:0},{key:"robots_txt",value:`User-agent: * -Disallow: /`,type:"text",help:"Robots Txt Content",group:1,flag:0},{key:"logo",value:"https://res.oplist.org/logo/logo.svg",type:"string",help:"Site Logo URL",group:2,flag:0},{key:"favicon",value:"https://res.oplist.org/logo/logo.svg",type:"string",help:"Favicon URL",group:2,flag:0},{key:"main_color",value:"#1890ff",type:"string",help:"Main Theme Color",group:2,flag:0},{key:"home_icon",value:"openlist",type:"string",help:"Home Icon Name",group:2,flag:0},{key:"home_container",value:"max_980px",type:"select",options:"max_980px,hope_container",help:"Home Container Width",group:2,flag:0},{key:"settings_layout",value:"responsive",type:"select",options:"list,responsive",help:"Settings Layout Mode",group:2,flag:0},{key:"text_types",value:eQ,type:"text",help:"Text File Extensions",group:3,flag:0},{key:"audio_types",value:"mp3,ogg,aac,wav,wma,flac,m4a,opus",type:"text",help:"Audio File Extensions",group:3,flag:0},{key:"video_types",value:"mp4,mkv,webm,avi,mov,flv,m3u8,ts",type:"text",help:"Video File Extensions",group:3,flag:0},{key:"image_types",value:"jpg,png,jpeg,gif,bmp,svg,ico,webp,avif,tiff",type:"text",help:"Image File Extensions",group:3,flag:0},{key:"proxy_types",value:VG,type:"text",help:"Proxy File Extensions",group:3,flag:0},{key:"proxy_ignore_headers",value:YG,type:"text",help:"Proxy Ignore Headers",group:3,flag:0},{key:"external_previews",value:"{}",type:"text",help:"External Previews JSON Config",group:3,flag:0},{key:"iframe_previews",value:"{}",type:"text",help:"Iframe Previews JSON Config",group:3,flag:0},{key:"audio_cover",value:"https://file.nn.ci/alist/cover.png",type:"string",help:"Audio Default Cover Image URL",group:3,flag:0},{key:"audio_autoplay",value:"false",type:"bool",help:"Autoplay Audio",group:3,flag:0},{key:"video_autoplay",value:"false",type:"bool",help:"Autoplay Video",group:3,flag:0},{key:"preview_archives_by_default",value:"false",type:"bool",help:"Preview Archives By Default",group:3,flag:0},{key:"readme_autorender",value:"true",type:"bool",help:"Readme Autorender",group:3,flag:0},{key:"filter_readme_scripts",value:"true",type:"bool",help:"Filter Readme Scripts",group:3,flag:0},{key:"force_preview",value:"",type:"text",help:"Force Preview Config",group:3,flag:0},{key:"specify_preview",value:"",type:"text",help:"Specify Preview Layout Config",group:3,flag:0},{key:"markdown_autorender",value:"true",type:"bool",help:"Autorender Markdown",group:3,flag:0},{key:"code_editor_theme",value:"vs-dark",type:"select",options:"vs,vs-dark,hc-black",help:"Monaco Theme",group:3,flag:0},{key:"office_preview",value:"true",type:"bool",help:"Enable Office Document Preview",group:3,flag:0},{key:"pdf_preview",value:"true",type:"bool",help:"Enable PDF Preview",group:3,flag:0},{key:"hide_files",value:"",type:"text",help:"Files Regex to Hide",group:4,flag:0},{key:"package_download",value:"true",type:"bool",help:"Package Download Enabled",group:4,flag:0},{key:"customize_head",value:"",type:"text",help:"Custom Head HTML/CSS",group:4,flag:0},{key:"customize_body",value:"",type:"text",help:"Custom Body Script",group:4,flag:0},{key:"link_expiration",value:"0",type:"number",help:"Link Expiration in Hours (0 = never expire)",group:4,flag:0},{key:"sign_all",value:"false",type:"bool",help:"Sign All Download Links",group:4,flag:0},{key:"seed_site_url",value:"",type:"string",help:"Public site URL used by transfer seed sources",group:4,flag:1},{key:"seed_default_matrix",value:'{"md5":{"whole":true,"pieces":false},"sha1":{"whole":true,"pieces":false},"sha256":{"whole":true,"pieces":false}}',type:"text",help:"Default transfer seed hash matrix",group:4,flag:1},{key:"seed_format_policies",value:'{"oss":"off","torrent":"off","cas":"off"}',type:"text",help:"Automatic transfer seed format policies",group:4,flag:1},{key:"seed_auto_generate_policy",value:"off",type:"select",options:"off,on",help:"Global automatic transfer seed policy",group:4,flag:1},{key:"seed_single_direct_preview",value:"false",type:"bool",help:"Open single-file transfer seeds directly in preview",group:4,flag:0},{key:"seed_cas_direct_access",value:"false",type:"bool",help:"When opening a single-file CAS seed, immediately rapid-upload it into the same folder and preview the restored file",group:4,flag:0},{key:"seed_default_trackers",value:"",type:"text",help:"Default tracker list offered when generating torrent seeds (one tracker per line)",group:4,flag:1},{key:"privacy_regs",value:"",type:"text",help:"Privacy Regex Rules",group:4,flag:0},{key:"ocr_api",value:"",type:"string",help:"OCR API Endpoint",group:4,flag:0},{key:"filename_char_mapping",value:"{}",type:"text",help:"Filename Char Mapping JSON",group:4,flag:0},{key:"forward_direct_link_params",value:"",type:"string",help:"Forward Direct Link Params",group:4,flag:0},{key:"ignore_direct_link_params",value:"",type:"string",help:"Ignore Direct Link Params",group:4,flag:0},{key:"webauthn_login_enabled",value:"false",type:"bool",help:"Webauthn Login Enabled",group:4,flag:0},{key:"allow_previewing_sharing_files",value:"true",type:"bool",help:"Allow Previewing Sharing Files",group:4,flag:0},{key:"allow_previewing_sharing_archives",value:"true",type:"bool",help:"Allow Previewing Sharing Archives",group:4,flag:0},{key:"force_proxy_sharing_files",value:"false",type:"bool",help:"Force Proxy Sharing Files",group:4,flag:0},{key:"share_summary_content",value:"",type:"text",help:"Share Summary Content",group:4,flag:0},{key:"handle_hook_after_writing",value:"",type:"string",help:"Handle Hook After Writing",group:4,flag:0},{key:"handle_hook_rate_limit",value:"0",type:"number",help:"Handle Hook Rate Limit",group:4,flag:0},{key:"ignore_system_files",value:"true",type:"bool",help:"Ignore System Files (.DS_Store, desktop.ini, etc.)",group:4,flag:0},{key:"auto_update_index",value:"false",type:"bool",help:"Auto Update Search Index",group:4,flag:0},{key:"sso_login_enabled",value:"false",type:"bool",help:"Enable SSO Login",group:7,flag:0},{key:"sso_login_platform",value:"",type:"select",options:"Github,Microsoft,Google,Dingtalk,Casdoor,OIDC",help:"SSO Platform",group:7,flag:0},{key:"sso_client_id",value:"",type:"string",help:"SSO Client ID",group:7,flag:0},{key:"sso_client_secret",value:"",type:"string",help:"SSO Client Secret",group:7,flag:0},{key:"sso_login_url",value:"",type:"string",help:"SSO Authorization Endpoint",group:7,flag:0},{key:"sso_token_url",value:"",type:"string",help:"SSO Token Endpoint (optional, defaults per platform)",group:7,flag:0},{key:"sso_userinfo_url",value:"",type:"string",help:"SSO UserInfo Endpoint (optional, defaults per platform)",group:7,flag:0},{key:"sso_scopes",value:"",type:"string",help:"SSO OAuth2 Scopes (optional, space separated)",group:7,flag:0},{key:"sso_oidc_discovery_url",value:"",type:"string",help:"OIDC Discovery URL (for OIDC platform)",group:7,flag:0},{key:"sso_login_callback_url",value:"",type:"string",help:"SSO Redirect/Callback URL (optional, defaults to /api/auth/sso_callback)",group:7,flag:0},{key:"sso_compatibility_mode",value:"false",type:"bool",help:"SSO Compatibility Mode (postMessage token handoff)",group:7,flag:0},{key:"sso_auto_register",value:"false",type:"bool",help:"Auto Register New SSO Users",group:7,flag:0},{key:"sso_default_dir",value:"/",type:"string",help:"SSO Default Directory for New Users",group:7,flag:0},{key:"sso_default_permission",value:"0",type:"number",help:"SSO Default Permission for New Users",group:7,flag:0},{key:"ldap_host",value:"",type:"string",help:"LDAP Server Host",group:8,flag:0},{key:"ldap_port",value:"389",type:"number",help:"LDAP Server Port",group:8,flag:0},{key:"traffic_limit",value:"0",type:"number",help:"Traffic Limit in MB",group:10,flag:0},{key:"ip_limit",value:"0",type:"number",help:"IP Rate Limit Per Minute",group:10,flag:0},{key:"115_temp_dir",value:"",type:"string",help:"115 Temp Directory",group:14,flag:0},{key:"115_open_temp_dir",value:"",type:"string",help:"115 Open Temp Directory",group:14,flag:0},{key:"123_temp_dir",value:"",type:"string",help:"123 Pan Temp Directory",group:14,flag:0},{key:"123_open_temp_dir",value:"",type:"string",help:"123 Open Temp Directory",group:14,flag:0},{key:"123_open_callback_url",value:"",type:"string",help:"123 Open Callback URL",group:14,flag:0},{key:"pikpak_temp_dir",value:"",type:"string",help:"PikPak Temp Directory",group:14,flag:0},{key:"thunder_temp_dir",value:"",type:"string",help:"Thunder Temp Directory",group:14,flag:0},{key:"thunder_browser_temp_dir",value:"",type:"string",help:"Thunder Browser Temp Directory",group:14,flag:0},{key:"thunderx_temp_dir",value:"",type:"string",help:"ThunderX Temp Directory",group:14,flag:0},{key:"token",value:"",type:"string",help:"115 / PikPak / Thunder Token",group:14,flag:0},{key:"package_download_disabled",value:"false",type:"bool",help:"Disable Package Download",group:14,flag:0}],storages:[],users:[{id:1,username:"admin",password:"",role:2,permission:0,base_path:"/",disabled:!1,sso_id:"",allow_ldap:!1,pwd_update_at:new Date().toISOString()},{id:2,username:"guest",password:"",role:1,permission:0,base_path:"/",disabled:!1,sso_id:"",allow_ldap:!1,pwd_update_at:new Date().toISOString()}],metas:[],shares:[],plugins:[]},ht=null,ai=null,Ns=!1,hg=!1,Wo=null;ZG={logo:{from:["","/logo.png","https://res.oplist.org/logo/logo.png"],to:"https://res.oplist.org/logo/logo.svg"},favicon:{from:["","/favicon.png"],to:"https://res.oplist.org/logo/logo.svg"},home_container:{from:["hope_container"],to:"max_980px"},text_types:{from:[WG],to:eQ}},QC=r=>{if(!r)return;r.settings||(r.settings=[]);let e=!1,t=[],i=new Set;for(let n of pc.settings){i.add(n.key);let s=r.settings.filter(o=>o.key===n.key);if(s.length===0)t.push(JSON.parse(JSON.stringify(n))),e=!0;else{let o=s.find(c=>c.value&&c.value.trim()!=="")||s[0];(o.group!==n.group||o.help!==n.help||o.type!==n.type||o.options!==n.options||o.flag!==n.flag)&&(o.group=n.group,o.help=n.help,o.type=n.type,o.options=n.options,o.flag=n.flag,e=!0),s.length>1&&(e=!0);let a=ZG[n.key];a&&a.from.includes(o.value)&&(o.value=a.to,e=!0),t.push(o)}}for(let n of r.settings)n.key&&!i.has(n.key)&&(i.add(n.key),t.push(n));r.settings=t},ag=r=>{r&&(!r.storages||!Array.isArray(r.storages)?r.storages=[]:r.storages=r.storages.filter(e=>e&&typeof e=="object"&&typeof e.driver=="string"&&e.driver.trim()!==""&&e.driver!=="undefined"&&e.driver!=="null"&&typeof e.mount_path=="string"&&e.mount_path.trim()!==""))},cg=r=>{r&&(r.shares||(r.shares=[]))},lg=r=>{r&&(r.plugins||(r.plugins=[]))},dg=r=>{r&&(!r.metas||!Array.isArray(r.metas))&&(r.metas=[])},ez=1e3,gg=new WeakMap,Ag=new WeakMap,mg=r=>ig(r),tz=()=>{ai&&typeof ai=="object"&&(gg.delete(ai),Ag.delete(ai)),ai=null,ht=null,mg=r=>ig(r),Ns=!1,Wo=null,hg=!1,qs=null,Yo=!1,_C=!1,FC=!1,PC=!1,TC=!1,QD(),Rl.clear()},rz=r=>{mg=r},rQ=()=>{let r=ai;return r&&typeof r=="object"?r:null},W3=async r=>{r&&(ai=r);let e=r||ai,t=null;try{t=await mg(e);let i=await t.load(e);if(i){let n=dz(i);n&&Ac(e)==="none"&&!TC&&(TC=!0,console.log("[DB] DB_CIPHER=none: existing encrypted values were decrypted; the next save will write them back as PLAINTEXT (per-field migration, no explicit step needed). Set DB_CIPHER to keep them encrypted."));let s=n?await sQ(e,{needDecrypt:!0}):null;return await fz(i,s),ht=i,QC(ht),ag(ht),cg(ht),lg(ht),dg(ht),Ns=!0,Wo=null,ht}if(Ns&&ht)return console.warn(`[DB] Backend ${t?.name??"storage"} returned empty while a trusted snapshot exists; keeping the in-memory snapshot to avoid overwriting real config.`),QC(ht),ag(ht),cg(ht),lg(ht),dg(ht),ht;Ns=!1,Wo=null}catch(i){console.error(`[DB] Error reading config from ${t?.name??"storage"}:`,i?.message||i),Wo=String(i?.message||i),Ns=!1}return ht&&Ns?(QC(ht),ag(ht),cg(ht),lg(ht),dg(ht),ht):(ht=JSON.parse(JSON.stringify(pc)),ag(ht),cg(ht),lg(ht),dg(ht),ht)},ne=async r=>{r&&(ai=r);let e=r||rQ();if(!e)return W3(r);let t=Ag.get(e);if(t)return t;let i=gg.get(e);if(i&&Date.now()-i.ts(gg.set(e,{ts:Date.now(),db:s}),s)).finally(()=>{Ag.delete(e)});return Ag.set(e,n),n},NC=new Set(["token","sso_client_secret","sso_client_id","ldap_bind_password","ldap_bind_dn","ocr_api","handle_hook_after_writing"]),_C=!1,FC=!1,PC=!1,TC=!1,Xo="openlist_encryption_secret";iQ=32;qs=null,Yo=!1;Xu=null,X3=6,sz=100,oz=1e3;cz=4096,Rl=new Map;Z3=16;fe=async(r,e,t)=>{e&&(ai=e);let i=e||ai;if(tQ(r)&&!t?.force){hg=!0;let c=Wo?`last load failed: ${Wo}`:Ns?"payload is an empty/shell database":"database was never successfully loaded from the persistence backend";return console.error(`[DB] saveDb BLOCKED: refusing to persist an empty/shell database (${c}). This guard prevents an empty payload from wiping real config. Pass saveDb(db, env, { force: true }) to override intentionally.`),!1}ht=r,hg=!1;let s=e||rQ();s&&gg.set(s,{ts:Date.now(),db:r});let o=await mg(i);if(!(o.isConfigured?await o.isConfigured(i):!0))return console.warn("[DB] WARNING: No persistence backend configured! Storage configuration changes will exist only in memory!"),!1;try{let c=Ac(i);console.log(`[DB] saveDb: cipher=${c}, persisting to ${o.name}, storages=${r.storages?.length||0}`);let l=c==="none"?null:await sQ(i);c!=="none"&&!l&&!PC&&(PC=!0,console.error(`[DB] DB_CIPHER=${c} but no encryption key is available; sensitive fields will be stored in PLAINTEXT. Set JWT_SECRET.`));let d=await uz(r,l);console.log(`[DB] saveDb: sealed data size=${JSON.stringify(d).length} bytes`),await o.save(d,i)}catch(c){throw console.error(`[DB] saveDb FAILED: backend=${o.name}, error=${c?.message||c}`,`stack=${c?.stack?.substring(0,500)||""}`),new Error(`[DB] Failed to persist config (${o.name}); the change was NOT saved: ${c?.message||c}`)}return console.log(`[DB] Successfully persisted ${r.storages?.length||0} storages to ${o.name}`),Ns=!0,Wo=null,!0}});function Os(r){return(r.startsWith("/")?r:`/${r}`).replace(/[^A-Za-z0-9\-_.~$&+,:;=@/]/gu,t=>{try{return encodeURIComponent(t)}catch{return t}})}var Nl=J(()=>{"use strict"});function aQ(r){if(!r)return{};if(typeof r=="object")return r;if(typeof r=="string")try{let e=JSON.parse(r||"{}");return e&&typeof e=="object"?e:{}}catch{return{}}return{}}function MC(r){return typeof r=="string"?r.toLowerCase()==="true":r===!0}function mc(r){if(typeof r=="number"&&Number.isFinite(r))return Math.trunc(r);if(typeof r=="string"&&r.trim()!==""){let e=parseInt(r,10);if(Number.isFinite(e))return e}return null}function cQ(r){let e=r?.proxy_range??r?.proxyRange;return e==null||e===""?!0:MC(e)}function lQ(r){return r?MC(r.enable_sign??r.enableSign):!1}function dQ(r){return r?MC(r.disable_index??r.disableIndex):!1}function mz(r){let e=mc(r?.cache_expiration??r?.cacheExpiration);return e===null||e<0?30:e}function yz(r){let e=r?.custom_cache_policies??r?.customCachePolicies;if(!e)return[];if(typeof e=="string"){let i=e.trim();if(!i)return[];try{e=JSON.parse(i)}catch{let n=[];for(let s of i.split(",")){let o=s.lastIndexOf(":");if(o<=0)continue;let a=s.slice(0,o).trim(),c=mc(s.slice(o+1).trim());a&&c!==null&&n.push({path:a,cacheExpiration:c})}return n}}let t=[];if(Array.isArray(e)){for(let i of e)if(i){if(typeof i=="string"){let n=i.lastIndexOf(":");if(n<=0)continue;let s=i.slice(0,n).trim(),o=mc(i.slice(n+1).trim());s&&o!==null&&t.push({path:s,cacheExpiration:o});continue}if(typeof i=="object"){let n=i.path??i.Path,s=mc(i.cache_expiration??i.cacheExpiration)??mc(i.max_age??i.maxAge);if(typeof n=="string"&&n&&s!==null){let o=mc(i.max_age??i.maxAge);t.push({path:n,cacheExpiration:s,...o!==null?{maxAge:o}:{}})}}}return t}if(typeof e=="object")for(let[i,n]of Object.entries(e)){let s=mc(n);i&&s!==null&&t.push({path:i,cacheExpiration:s})}return t}function oQ(r,e){if(!r)return!1;if(r===e)return!0;if(r.length>Iz||e.length>wz)return!1;let t=r.split("/"),i=e.split("/"),n=t.length,s=i.length,o=new Array(s+1).fill(!1);o[s]=!0;for(let a=n-1;a>=0;a--){let c=t[a],l=new Array(s+1).fill(!1);if(/^\*{2,}$/.test(c)){l[s]=o[s];for(let u=s-1;u>=0;u--)l[u]=o[u]||l[u+1]}else for(let u=s-1;u>=0;u--)l[u]=xz(c,i[u])&&o[u+1];o=l}return o[0]}function xz(r,e){let t=0,i=0,n=-1,s=-1,o=r.length,a=e.length;for(;i{"use strict";Iz=1024,wz=4096});function yc(r){return String(r??"").toLowerCase().replace(/[^a-z0-9]/g,"")}function Ic(r){let e=Array.isArray(r)?r.join(","):r;return String(e??"").split(",").map(t=>t.trim().toLowerCase().replace(/^\./,"")).filter(Boolean)}function hQ(r){let e=String(r??"").split("/").pop()?.trim();if(!e)return"";let t=e.lastIndexOf(".");return t<0||t===e.length-1?"":e.slice(t+1).toLowerCase()}function gQ(r){return fQ.has(yc(r))}function Bz(r){let e=aQ(r?.addition),t=String(e.storage_zone_name??"").trim(),i=String(e.cdn_base_url??"").trim();return t!==""&&i===""}function Ig(r,e){let t=yc(e??r?.driver);return fQ.has(t)?!0:t==="bunnystorage"?Bz(r):!1}function HC(r){return Cz.has(yc(r))}function pQ(r){if(!r)return"";let e=r.webdav_policy??r.webdavPolicy??r.policy??r.webdavPolicyType;return typeof e=="string"?e.trim():""}function mQ(r){if(!r)return"";for(let t of["down_proxy_url","downProxyUrl"]){let i=r[t];if(typeof i=="string"&&i.trim())return i.split(` -`)[0].trim()}let e=r.addition;if(typeof e=="string")try{e=JSON.parse(e||"{}")}catch{return""}if(!e||typeof e!="object")return"";for(let t of Ez){let i=e[t];if(typeof i=="string"&&i.trim())return i.split(` -`)[0].trim()}return""}function yQ(r){if(!r)return!1;let e=r.disable_proxy_sign??r.disableProxySign;return typeof e=="string"?e.toLowerCase()==="true":!!e}function IQ(r,e){let t=r?.web_proxy??r?.webProxy;return t===!0||t==="true"?!0:t===!1||t==="false"?!1:HC(e)}function wQ(r,e,t,i={}){let n=yc(e);if(Ig(r,n))return{mode:"native_proxy",needsProxy:!0,source:"force"};if(IQ(r,n))return{mode:"native_proxy",needsProxy:!0,source:"web_proxy"};if(t)return{mode:"native_proxy",needsProxy:!0,source:"proxy_path"};if(LC(hQ(i.filename),i.proxyTypes))return{mode:"native_proxy",needsProxy:!0,source:"proxy_types"};let s=pQ(r);return s?{mode:s,needsProxy:s!=="302_redirect",source:"storage_policy"}:{mode:"302_redirect",needsProxy:!1,source:"driver_default"}}function LC(r,e){return r?Ic(e).includes(r):!1}function wg(r){let e=yc(r.driver);if(Ig(r.storage,e)||IQ(r.storage,e)||pQ(r.storage)==="use_proxy_url")return!0;let t=hQ(r.filename);return t?LC(t,r.proxyTypes)||LC(t,r.textTypes):!1}var fQ,Cz,AQ,Ez,ef=J(()=>{"use strict";Zu();fQ=new Set(["weiyun","sftp","ftp","smb","crypt","virtual","strm","meganz","protondrive","chunk","googledrive","googlephoto","quarkopen","quarkuc","chaoxing","local"]),Cz=new Set(["webdav","baidunetdisk","123pan","123open","123panshare"]),AQ=new Set(["139yun","alias","alistv3","openlist"]),Ez=["down_proxy_url","download_proxy_url","proxy_url"]});function X(r,e){if(e)return 1;let t=(r.split(".").pop()||"").toLowerCase();return["mp4","mkv","avi","mov","flv","wmv","ts","m2ts","m4v","rmvb","webm","3gp","asf","vob","ogv","rm","f4v"].includes(t)?2:["mp3","flac","aac","wav","ogg","m4a","opus","wma","ape","alac","aiff","mid","midi"].includes(t)?3:["txt","md","markdown","json","js","ts","jsx","tsx","css","scss","html","htm","xml","yaml","yml","ini","conf","env","log","sql","py","java","c","cpp","h","hpp","go","rs","sh","bat","cmd","ps1","php","rb","swift","kt","cs","vue","svelte","json5","toml"].includes(t)?4:["jpg","jpeg","png","gif","bmp","webp","svg","ico","tiff","tif","heic","heif","avif","vvc","avc","psd","ai"].includes(t)?5:0}var Be=J(()=>{"use strict"});function de(r,e,t){let i=t!=="desc",n=String(e||"name").toLowerCase(),s=[...r];return s.sort((o,a)=>{if(o.is_dir!==a.is_dir)return o.is_dir?-1:1;let c;return n.includes("size")?c=(o.size||0)-(a.size||0):n.includes("time")||n.includes("modified")||n.includes("created")?c=new Date(o.modified).getTime()-new Date(a.modified).getTime():c=String(o.name).localeCompare(String(a.name)),i?c:-c}),s}var He=J(()=>{"use strict"});var Yn,KC=J(()=>{"use strict";Yn={global:{oauth:"https://login.microsoftonline.com",api:"https://graph.microsoft.com"},cn:{oauth:"https://login.partner.microsoftonline.cn",api:"https://microsoftgraph.chinacloudapi.cn"},us:{oauth:"https://login.microsoftonline.us",api:"https://graph.microsoft.us"},de:{oauth:"https://login.microsoftonline.de",api:"https://graph.microsoft.de"}}});function GC(r,e){let t="";return r.thumbnails&&r.thumbnails.length>0&&(t=r.thumbnails[0].medium?.url||""),{id:r.id,name:r.name,size:r.size,modified:r.lastModifiedDateTime||r.fileSystemInfo?.lastModifiedDateTime||"",isFolder:!!r.folder||!r.file,thumbnail:t,parentID:e,url:r["@microsoft.graph.downloadUrl"]||""}}var xQ=J(()=>{"use strict"});async function zC(r){if(r.use_online_api&&r.api_url_address){let s=new URLSearchParams({refresh_ui:r.refresh_token,server_use:"true",driver_txt:"onedrive_pr"}).toString(),a=await(await fetch(`${r.api_url_address}?${s}`)).json();if(!a.refresh_token||!a.access_token)throw a.text?new Error(`failed to refresh token: ${a.text}`):new Error("empty token returned from official API");r.accessToken=a.access_token,r.refresh_token=a.refresh_token,await r.onTokenUpdate?.(r.refresh_token);return}if(!r.client_id||!r.client_secret)throw new Error("empty ClientID or ClientSecret");let t=`${(Yn[r.region]||Yn.global).oauth}/common/oauth2/v2.0/token`,n=await(await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"refresh_token",client_id:r.client_id,client_secret:r.client_secret,redirect_uri:r.redirect_uri,refresh_token:r.refresh_token}).toString()})).json();if(!n.refresh_token)throw new Error("Empty token");r.refresh_token=n.refresh_token,r.accessToken=n.access_token,await r.onTokenUpdate?.(r.refresh_token)}async function Ni(r,e,t,i,n){let s=i!==void 0&&(i instanceof Uint8Array||i instanceof ArrayBuffer||typeof Buffer<"u"&&Buffer.isBuffer(i)),o={method:t.toUpperCase(),headers:{Authorization:`Bearer ${r.accessToken}`,...i!==void 0?{"Content-Type":s?"application/octet-stream":"application/json"}:{}},...i!==void 0?{body:s?i:JSON.stringify(i)}:{}},a=await fetch(e,o);if(!a.ok){let c;try{c=(await a.json()).error}catch{c=null}let l=c?.code;if((l==="InvalidAuthenticationToken"||l==="ExpiredAuthenticationToken"||a.status===401)&&!n)return await zC(r),Ni(r,e,t,i,!0);throw new Error(c?.message||`Request failed: ${a.status}`)}if(a.status!==204)return a.json()}function CQ(r,e,t){let i=e.replace(/\\/g,"/");if(!i||i==="/")return t?`${r}/drive/root/${t}`:`${r}/drive/root`;let n=i.startsWith("/")?i.slice(1):i;if(n.endsWith("/")&&(n=n.slice(0,-1)),!n||n==="")return t?`${r}/drive/root/${t}`:`${r}/drive/root`;let s=n.split("/").map(encodeURIComponent).join("/");return t?`${r}/drive/root:/${s}:/${t}`:`${r}/drive/root:/${s}:`}async function EQ(r,e){let t=Yn[r.region]||Yn.global,i=r.is_sharepoint?`${t.api}/v1.0/sites/${r.site_id}`:`${t.api}/v1.0/me`,s=CQ(i,e,"children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,@microsoft.graph.downloadUrl,file,folder,parentReference"),o=[];for(;s;){let a=await Ni(r,s,"GET");a.value&&o.push(...a.value),s=a["@odata.nextLink"]}return o}async function BQ(r,e){let t=Yn[r.region]||Yn.global,i=r.is_sharepoint?`${t.api}/v1.0/sites/${r.site_id}`:`${t.api}/v1.0/me`,n=CQ(i,e);return Ni(r,n,"GET")}var bQ=J(()=>{"use strict";KC()});var xg,vQ=J(()=>{"use strict";He();KC();xQ();bQ();xg=class{root_folder_path="/";region="global";is_sharepoint=!1;use_online_api=!0;api_url_address="https://api.oplist.org/onedrive/renewapi";client_id="";client_secret="";redirect_uri="https://api.oplist.org/onedrive/callback";refresh_token="";site_id="";chunk_size=5;custom_host="";disable_disk_usage=!1;enable_direct_upload=!1;order_by="filename";order_direction="asc";accessToken="";onTokenUpdate;constructor(e,t){e&&(e.root_folder_path!==void 0&&(this.root_folder_path=String(e.root_folder_path)),e.region!==void 0&&(this.region=String(e.region)),e.is_sharepoint!==void 0&&(this.is_sharepoint=!!e.is_sharepoint),e.use_online_api!==void 0&&(this.use_online_api=!!e.use_online_api),e.api_url_address!==void 0&&(this.api_url_address=String(e.api_url_address)),e.client_id!==void 0&&(this.client_id=String(e.client_id)),e.client_secret!==void 0&&(this.client_secret=String(e.client_secret)),e.redirect_uri!==void 0&&(this.redirect_uri=String(e.redirect_uri)),e.refresh_token!==void 0&&(this.refresh_token=String(e.refresh_token)),e.site_id!==void 0&&(this.site_id=String(e.site_id)),e.chunk_size!==void 0&&(this.chunk_size=Number(e.chunk_size)||5),e.custom_host!==void 0&&(this.custom_host=String(e.custom_host)),e.disable_disk_usage!==void 0&&(this.disable_disk_usage=!!e.disable_disk_usage),e.enable_direct_upload!==void 0&&(this.enable_direct_upload=!!e.enable_direct_upload),e.order_by!==void 0&&(this.order_by=String(e.order_by)),e.order_direction!==void 0&&(this.order_direction=String(e.order_direction))),this.onTokenUpdate=t}async init(){typeof this.is_sharepoint=="string"&&(this.is_sharepoint=this.is_sharepoint.toLowerCase()==="true"),typeof this.use_online_api=="string"&&(this.use_online_api=this.use_online_api.toLowerCase()==="true"),typeof this.chunk_size=="string"&&(this.chunk_size=parseInt(this.chunk_size)||5),typeof this.disable_disk_usage=="string"&&(this.disable_disk_usage=this.disable_disk_usage.toLowerCase()==="true"),typeof this.enable_direct_upload=="string"&&(this.enable_direct_upload=this.enable_direct_upload.toLowerCase()==="true"),this.chunk_size<1&&(this.chunk_size=5),this.refresh_token&&await zC(this)}getMetaUrl(e,t,i){let n=Yn[this.region]||Yn.global;if(e)return n.oauth;let s=this.is_sharepoint?`${n.api}/v1.0/sites/${this.site_id}`:`${n.api}/v1.0/me`,o=t.replace(/\\/g,"/");if(!o||o==="/")return i?`${s}/drive/root/${i}`:`${s}/drive/root`;let a=o.startsWith("/")?o.slice(1):o;if(a.endsWith("/")&&(a=a.slice(0,-1)),!a||a==="")return i?`${s}/drive/root/${i}`:`${s}/drive/root`;let c=a.split("/").map(l=>{try{return encodeURIComponent(decodeURIComponent(l))}catch{return encodeURIComponent(l)}}).join("/");return i?`${s}/drive/root:/${c}:/${i}`:`${s}/drive/root:/${c}:`}async list(e,t){let n=(await EQ(this,t)).map(s=>{let o=GC(s,""),a=s["@microsoft.graph.downloadUrl"]||o.url||"";if(this.custom_host&&a)try{let c=new URL(a);c.host=this.custom_host,a=c.toString()}catch{}return{name:o.name,size:o.size,is_dir:o.isFolder,modified:o.modified,sign:"",type:o.isFolder?1:0,thumb:o.thumbnail||"",raw_url:a}});return de(n,this.order_by,this.order_direction)}async get(e,t){let i=await BQ(this,t),n=GC(i,""),s=i["@microsoft.graph.downloadUrl"]||n.url||"";if(this.custom_host&&s)try{let o=new URL(s);o.host=this.custom_host,s=o.toString()}catch{}return{name:n.name,size:n.size,is_dir:n.isFolder,modified:n.modified,sign:"",type:n.isFolder?1:0,thumb:n.thumbnail||"",raw_url:s}}async mkdir(e,t){let i=t.split("/").slice(0,-1).join("/")||"/",n=t.split("/").filter(Boolean).pop()||"",s=this.getMetaUrl(!1,i,"children");await Ni(this,s,"POST",{name:n,folder:{},"@microsoft.graph.conflictBehavior":"rename"})}async rename(e,t,i){let n={name:i},s=this.getMetaUrl(!1,t);await Ni(this,s,"PATCH",n)}async remove(e,t,i){for(let n of i){let s=t==="/"?`/${n}`:`${t}/${n}`,o=this.getMetaUrl(!1,s);await Ni(this,o,"DELETE")}}async move(e,t,i,n,s){let o=this.getMetaUrl(!1,s),a=await Ni(this,o,"GET"),c=a.id,l=a.parentReference?.driveId;for(let d of i){let u=n==="/"?`/${d}`:`${n}/${d}`,f={parentReference:{id:c,...l?{driveId:l}:{}},name:d},h=this.getMetaUrl(!1,u);await Ni(this,h,"PATCH",f)}}async copy(e,t,i,n,s){let o=this.getMetaUrl(!1,s),a=await Ni(this,o,"GET"),c=a.id,l=a.parentReference?.driveId;for(let d of i){let u=n==="/"?`/${d}`:`${n}/${d}`,f={parentReference:{id:c,...l?{driveId:l}:{}},name:d},h=this.getMetaUrl(!1,u,"copy");await Ni(this,h,"POST",f)}}async put(e,t,i){if(i.length<=4*1024*1024){let n=this.getMetaUrl(!1,t,"content");await Ni(this,n,"PUT",i)}else{let n=this.getMetaUrl(!1,t,"createUploadSession"),a=(await Ni(this,n,"POST",{item:{"@microsoft.graph.conflictBehavior":"rename"}})).uploadUrl,c=this.chunk_size*1024*1024,l=0,d=i.length;for(;l0&&(t=r.thumbnails[0].medium?.url||""),{id:r.id,name:r.name,size:r.size,modified:r.lastModifiedDateTime||r.fileSystemInfo?.lastModifiedDateTime||"",isFolder:!!r.folder||!r.file,thumbnail:t,parentID:e,url:r["@microsoft.graph.downloadUrl"]||""}}var kQ=J(()=>{"use strict"});var wc,SQ=J(()=>{"use strict";wc={global:{oauth:"https://login.microsoftonline.com",api:"https://graph.microsoft.com"},cn:{oauth:"https://login.chinacloudapi.cn",api:"https://microsoftgraph.chinacloudapi.cn"},us:{oauth:"https://login.microsoftonline.us",api:"https://graph.microsoft.us"},de:{oauth:"https://login.microsoftonline.de",api:"https://graph.microsoft.de"}}});function qi(r,e,t,i){let n=wc[r.region]||wc.global;if(e)return n.oauth;let s=t?t.replace(/\\/g,"/"):"";if(!s||s==="/")return i?`${n.api}/v1.0/users/${r.email}/drive/root/${i}`:`${n.api}/v1.0/users/${r.email}/drive/root`;let o=s.startsWith("/")?s.slice(1):s;if(o.endsWith("/")&&(o=o.slice(0,-1)),!o||o==="")return i?`${n.api}/v1.0/users/${r.email}/drive/root/${i}`:`${n.api}/v1.0/users/${r.email}/drive/root`;let a=o.split("/").map(c=>{try{return encodeURIComponent(decodeURIComponent(c))}catch{return encodeURIComponent(c)}}).join("/");return i?`${n.api}/v1.0/users/${r.email}/drive/root:/${a}:/${i}`:`${n.api}/v1.0/users/${r.email}/drive/root:/${a}:`}async function jC(r){let e=null;for(let t=0;t<3;t++)try{await bz(r);return}catch(i){e=i}throw e||new Error("Failed to get access token")}async function bz(r){if(!r.client_id||!r.client_secret)throw new Error("empty client_id or client_secret");if(!r.tenant_id)throw new Error("empty tenant_id");let e=wc[r.region]||wc.global,t=`${e.oauth}/${r.tenant_id}/oauth2/token`,i=new URLSearchParams({grant_type:"client_credentials",client_id:r.client_id,client_secret:r.client_secret,resource:`${e.api}/`,scope:`${e.api}/.default`}).toString(),s=await(await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i})).json();if(s.error)throw new Error(s.error_description||s.error);if(!s.access_token)throw new Error("empty token returned from Microsoft identity platform");r.accessToken=s.access_token,r.onTokenUpdate?.(r.accessToken)}async function ci(r,e,t,i,n){let s=i!==void 0&&(typeof i=="string"||i instanceof Uint8Array||i instanceof ArrayBuffer||typeof Buffer<"u"&&Buffer.isBuffer(i)),o={method:t.toUpperCase(),headers:{Authorization:`Bearer ${r.accessToken}`,...i!==void 0&&!s?{"Content-Type":"application/json"}:{}},...i!==void 0?{body:s?i:JSON.stringify(i)}:{}},a=await fetch(e,o);if(!a.ok){let c;try{c=(await a.json()).error}catch{c=null}let l=c?.code;if((l==="InvalidAuthenticationToken"||l==="ExpiredAuthenticationToken"||a.status===401)&&!n)return await jC(r),ci(r,e,t,i,!0);throw new Error(c?.message||`Request failed: ${a.status}`)}if(a.status!==204)return a.json()}async function DQ(r,e){let i=qi(r,!1,e,"children?$top=1000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,lastModifiedDateTime,@microsoft.graph.downloadUrl,file,folder,parentReference"),n=[];for(;i;){let s=await ci(r,i,"GET");s.value&&n.push(...s.value),i=s["@odata.nextLink"]}return n}async function QQ(r,e){let t=qi(r,!1,e);return ci(r,t,"GET")}async function _Q(r){let t=`${(wc[r.region]||wc.global).api}/v1.0/users/${r.email}/drive`;return ci(r,t,"GET",void 0,!0)}async function FQ(r,e){let t=qi(r,!1,e,"createUploadSession"),s=(await ci(r,t,"POST",{item:{"@microsoft.graph.conflictBehavior":"rename"}})).uploadUrl;if(!s)throw new Error("failed to get upload URL from response");return{UploadURL:s,ChunkSize:(r.chunk_size||5)*1024*1024,Method:"PUT"}}var PQ=J(()=>{"use strict";SQ()});var Cg,TQ=J(()=>{"use strict";He();kQ();PQ();Cg=class{root_folder_path="/";region="global";client_id="";client_secret="";tenant_id="";email="";chunk_size=5;custom_host="";disable_disk_usage=!1;enable_direct_upload=!1;order_by="filename";order_direction="asc";accessToken="";onTokenUpdate;constructor(e,t){e&&(e.root_folder_path!==void 0&&(this.root_folder_path=String(e.root_folder_path)),e.region!==void 0&&(this.region=String(e.region)),e.client_id!==void 0&&(this.client_id=String(e.client_id)),e.client_secret!==void 0&&(this.client_secret=String(e.client_secret)),e.tenant_id!==void 0&&(this.tenant_id=String(e.tenant_id)),e.email!==void 0&&(this.email=String(e.email)),e.chunk_size!==void 0&&(this.chunk_size=Number(e.chunk_size)||5),e.custom_host!==void 0&&(this.custom_host=String(e.custom_host)),e.disable_disk_usage!==void 0&&(this.disable_disk_usage=!!e.disable_disk_usage),e.enable_direct_upload!==void 0&&(this.enable_direct_upload=!!e.enable_direct_upload),e.order_by!==void 0&&(this.order_by=String(e.order_by)),e.order_direction!==void 0&&(this.order_direction=String(e.order_direction))),this.onTokenUpdate=t}async init(){typeof this.chunk_size=="string"&&(this.chunk_size=parseInt(this.chunk_size)||5),typeof this.disable_disk_usage=="string"&&(this.disable_disk_usage=this.disable_disk_usage.toLowerCase()==="true"),typeof this.enable_direct_upload=="string"&&(this.enable_direct_upload=this.enable_direct_upload.toLowerCase()==="true"),this.chunk_size<1&&(this.chunk_size=5),this.client_id&&this.client_secret&&this.tenant_id&&await jC(this)}async list(e,t){let n=(await DQ(this,t)).map(s=>{let o=JC(s,""),a=s["@microsoft.graph.downloadUrl"]||o.url||"";if(this.custom_host&&a)try{let c=new URL(a);c.host=this.custom_host,a=c.toString()}catch{}return{name:o.name,size:o.size,is_dir:o.isFolder,modified:o.modified,sign:"",type:o.isFolder?1:0,thumb:o.thumbnail||"",raw_url:a}});return de(n,this.order_by,this.order_direction)}async get(e,t){let i=await QQ(this,t),n=JC(i,""),s=i["@microsoft.graph.downloadUrl"]||n.url||"";if(this.custom_host&&s)try{let o=new URL(s);o.host=this.custom_host,s=o.toString()}catch{}return{name:n.name,size:n.size,is_dir:n.isFolder,modified:n.modified,sign:"",type:n.isFolder?1:0,thumb:n.thumbnail||"",raw_url:s}}async mkdir(e,t){let i=t.split("/").slice(0,-1).join("/")||"/",n=t.split("/").filter(Boolean).pop()||"",s=qi(this,!1,i,"children");await ci(this,s,"POST",{name:n,folder:{},"@microsoft.graph.conflictBehavior":"rename"})}async rename(e,t,i){let n={name:i},s=qi(this,!1,t);await ci(this,s,"PATCH",n)}async remove(e,t,i){for(let n of i){let s=t==="/"?`/${n}`:`${t}/${n}`,o=qi(this,!1,s);await ci(this,o,"DELETE")}}async move(e,t,i,n,s){let o=qi(this,!1,s),a=await ci(this,o,"GET"),c=a.id,l=a.parentReference?.driveId;for(let d of i){let u=n==="/"?`/${d}`:`${n}/${d}`,f={parentReference:{id:c,...l?{driveId:l}:{}},name:d},h=qi(this,!1,u);await ci(this,h,"PATCH",f)}}async copy(e,t,i,n,s){let o=qi(this,!1,s),a=await ci(this,o,"GET"),c=a.id,l=a.parentReference?.driveId;for(let d of i){let u=n==="/"?`/${d}`:`${n}/${d}`,f={parentReference:{id:c,...l?{driveId:l}:{}},name:d},h=qi(this,!1,u,"copy");await ci(this,h,"POST",f)}}async put(e,t,i){if(i.length<=4*1024*1024){let n=qi(this,!1,t,"content");await ci(this,n,"PUT",i)}else{let n=qi(this,!1,t,"createUploadSession"),a=(await ci(this,n,"POST",{item:{"@microsoft.graph.conflictBehavior":"rename"}})).uploadUrl,c=this.chunk_size*1024*1024,l=0,d=i.length;for(;l255||g>255||p>255||I>255||h===0||h===127||h===10||h===172&&g>=16&&g<=31||h===192&&g===168||h===169&&g===254||h===100&&g>=64&&g<=127||h===100&&g===100||h===224&&g===0&&p===0||h>=240)return!1}if(/^\d{8,}$/.test(i)||/^0x[0-9a-fA-F]{6,}$/i.test(i))return!1;let c=[/(^|\.)\d{1,3}-\d{1,3}-\d{1,3}-\d{1,3}(\.|$)/,/(^|\.)0x[0-9a-f]{6,8}(\.|$)/i,/(^|\.)\d{8,}(\.|$)/,/(^|\.)127\.0\.0\.1\.nip\.io$/,/(^|\.)localtest\.me$/,/(^|\.)vcap\.me$/,/(^|\.)xip\.io$/];for(let l of c)if(l.test(i))return!1;return!0}catch{return!1}}function jr(r,e="Request",t){if(!vz(r,t))throw new Error(`${e} blocked: URL points to a restricted or private network destination (SSRF protection)`)}function kz(r){let e=new Set;if(r==null)return e;let t=r;if(typeof r=="string")try{t=JSON.parse(r)}catch{return e}if(typeof t!="object"||Array.isArray(t))return e;let i=/(url|host|address|endpoint|server|domain|site|base|gateway|api)/i,n=s=>{if(!(s==null||typeof s!="object"))for(let[o,a]of Object.entries(s))if(typeof a=="string"){if(i.test(o)){let c=UQ(a);c&&e.add(c)}}else a!=null&&typeof a=="object"&&n(a)};return n(t),e}function UQ(r){let e=r.trim();if(!e)return;let t=/^[a-z][a-z0-9+.-]*:\/\//i.test(e)?[e]:[`http://${e}`];for(let i of t)try{let n=new URL(i);if(n.protocol!=="http:"&&n.protocol!=="https:")continue;let s=n.hostname.toLowerCase().trim();if(s)return s}catch{}}function Sz(r){let e=new Set;if(!r)return e;for(let t of r.split(/[,;\s]+/)){let i=UQ(t);i&&e.add(i)}return e}function Eg(r,e){let t=kz(r),i=e?.SSRF_ALLOWED_HOSTS??(typeof process<"u"?process.env?.SSRF_ALLOWED_HOSTS:void 0);if(typeof i=="string"&&i.trim())for(let n of Sz(i))t.add(n);return t}var xc=J(()=>{"use strict"});var Dz,Bg,RQ=J(()=>{"use strict";xc();Dz="https://openapi.aliyundrive.com/adrive/v1.0",Bg=class{addition;accessToken="";refreshTokenVal="";driveId="";tokenExpiresAt=0;constructor(e){this.addition=e,this.refreshTokenVal=e.refresh_token||"",this.driveId=e.drive_id||""}async init(){if(!this.refreshTokenVal||!this.refreshTokenVal.trim()){console.warn("[AliyundriveOpen] refresh_token is empty, skipping init.");return}try{await this.refreshAccessToken(),this.driveId||await this.resolveDriveId()}catch(e){console.warn("[AliyundriveOpen] init warning:",e.message)}}async resolveDriveId(e=!1){if(!e&&this.addition.drive_id&&this.addition.drive_id.trim()){this.driveId=this.addition.drive_id.trim();return}try{let t=await this.openApiRequest("/user/getDriveInfo",{}),i=e?"resource":this.addition.drive_type||"resource",n="";i==="resource"&&t.resource_drive_id?n=t.resource_drive_id:i==="backup"&&t.backup_drive_id?n=t.backup_drive_id:i==="default"&&t.default_drive_id&&(n=t.default_drive_id),n||(n=t.resource_drive_id||t.default_drive_id||t.backup_drive_id||""),this.driveId=n,console.log(`[AliyundriveOpen] Resolved drive_id: ${this.driveId} (driveType: ${i})`)}catch(t){console.warn("[AliyundriveOpen] resolveDriveId failed:",t.message)}}async refreshAccessToken(){if(!this.refreshTokenVal||!this.refreshTokenVal.trim())return;let e=this.refreshTokenVal.trim(),t=[];if(this.addition.api_url_address&&this.addition.api_url_address.trim()){let o=this.addition.api_url_address.trim();try{jr(o,"AliyundriveOpen api_url_address"),t.push(o)}catch(a){console.warn(`[AliyundriveOpen] Skipping unsafe api_url_address: ${a.message}`)}}t.push("https://api.oplist.org/alicloud/renewapi","https://api.oplist.org/ali_open/token","https://api.oplist.org/aliyundrive/token","https://api.alist.nn.ci/alist/ali_open/token","https://api.alist.nn.ci/aliyundrive/token","https://api-sam.oplist.org/aliyundrive/token");let i=this.addition.alipan_type==="alipanTV"?"alicloud_tv":"alicloud_qr";for(let o of t)try{let a=new URLSearchParams({refresh_ui:e,refresh_token:e,server_use:"true",driver_txt:i}),c=await fetch(`${o}?${a.toString()}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!c.ok)throw new Error(`[Status ${c.status}]`);let l=await c.json(),d=l.refresh_token||l.data?.refresh_token||"",u=l.access_token||l.data?.access_token||"";if(!u)throw new Error(`Empty access_token from online API: ${JSON.stringify(l)}`);this.accessToken=u,d&&(this.refreshTokenVal=d),this.tokenExpiresAt=Date.now()+(l.expires_in||7200)*1e3-6e4;return}catch(a){console.warn(`[AliyundriveOpen] Online API '${o}' failed: ${a.message}`)}let n=(this.addition.client_id||"").trim()||"25ab4837190e48718a28f80073574a4d",s=(this.addition.client_secret||"").trim();try{let o={grant_type:"refresh_token",refresh_token:e,client_id:n};s&&(o.client_secret=s);let a=await fetch("https://openapi.aliyundrive.com/oauth/access_token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});if(!a.ok){let l=await a.text().catch(()=>"");throw new Error(`[Status ${a.status}] ${l}`)}let c=await a.json();if(!c.access_token)throw new Error(`Invalid response: ${JSON.stringify(c)}`);this.accessToken=c.access_token,c.refresh_token&&(this.refreshTokenVal=c.refresh_token),this.tokenExpiresAt=Date.now()+(c.expires_in||7200)*1e3-6e4;return}catch(o){console.warn(`[AliyundriveOpen] Direct OAuth failed: ${o.message}`)}throw new Error("[AliyundriveOpen] All token refresh strategies failed. Please check: 1) refresh_token is valid and not expired, 2) api_url_address is accessible, 3) If using direct OAuth, client_id and client_secret are correct.")}async ensureToken(){(!this.accessToken||Date.now()>=this.tokenExpiresAt)&&await this.refreshAccessToken()}getRootFolderId(){return this.addition.root_folder_id?.trim()||"root"}async openApiRequest(e,t,i=!0){await this.ensureToken();let n=e.startsWith("http")?e:`${Dz}${e}`;try{jr(n,"AliyundriveOpen openApiRequest")}catch(o){throw new Error(`[AliyundriveOpen] blocked unsafe URL: ${o.message}`)}let s=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.accessToken}`},body:JSON.stringify(t)});if(s.status===401&&i)return await this.refreshAccessToken(),this.openApiRequest(e,t,!1);if(!s.ok){let o=await s.text().catch(()=>"");throw new Error(`[AliyundriveOpen] API error [${s.status}] ${e}: ${o}`)}return s.json()}async listFiles(e){this.driveId||await this.resolveDriveId();let t=[],i,n=this.addition.order_by||"updated_at",s=this.addition.order_direction||"DESC";do{let o={drive_id:this.driveId,parent_file_id:e,limit:100,order_by:n,order_direction:s};i&&(o.marker=i);let a;try{a=await this.openApiRequest("/openFile/list",o)}catch(c){if(c.message?.includes("UserNotAllowedAccessDrive"))console.warn(`[AliyundriveOpen] UserNotAllowedAccessDrive for drive ${this.driveId}, auto re-resolving drive_id...`),await this.resolveDriveId(!0),o.drive_id=this.driveId,a=await this.openApiRequest("/openFile/list",o);else throw c}t.push(...a.items||[]),i=a.next_marker||void 0}while(i);return t}async getFile(e){return this.driveId||await this.resolveDriveId(),this.openApiRequest("/openFile/get",{drive_id:this.driveId,file_id:e})}async getDownloadUrl(e){let t=await this.openApiRequest("/openFile/getDownloadUrl",{drive_id:this.driveId,file_id:e,expire_sec:14400});return t.url||t.download_url||""}async mkdir(e,t){await this.openApiRequest("/openFile/create",{drive_id:this.driveId,parent_file_id:e,name:t,type:"folder",check_name_mode:"refuse"})}async rename(e,t){await this.openApiRequest("/openFile/update",{drive_id:this.driveId,file_id:e,name:t,check_name_mode:"refuse"})}async remove(e){let t=this.addition.remove_way||"trash";await this.openApiRequest(t==="trash"?"/openFile/recyclebin":"/openFile/delete",{drive_id:this.driveId,file_id:e})}async move(e,t){await this.openApiRequest("/openFile/move",{drive_id:this.driveId,file_id:e,to_parent_file_id:t,check_name_mode:"refuse"})}async copy(e,t){await this.openApiRequest("/openFile/copy",{drive_id:this.driveId,file_id:e,to_parent_file_id:t,auto_rename:!0})}async putFile(e,t,i){let n=i.length,s=await this.openApiRequest("/openFile/create",{drive_id:this.driveId,parent_file_id:e,name:t,type:"file",size:n,check_name_mode:"auto_rename",part_info_list:[{part_number:1}]}),o=s.part_info_list?.[0]?.upload_url;if(!o)return;let a=await fetch(o,{method:"PUT",body:i});if(!a.ok)throw new Error(`[AliyundriveOpen] Upload failed: ${a.status}`);await this.openApiRequest("/openFile/complete",{drive_id:this.driveId,file_id:s.file_id,upload_id:s.upload_id})}}});function NQ(r){let e=r.type==="folder";return{name:r.name,size:r.size||0,is_dir:e,modified:r.updated_at||r.created_at||new Date().toISOString(),sign:"",type:X(r.name,e),thumb:r.thumbnail||"",raw_url:r.download_url||""}}var bg,qQ=J(()=>{"use strict";Be();He();RQ();bg=class{client;addition;pathFileIdCache=new Map;constructor(e){this.addition=e,this.client=new Bg(e)}async init(){await this.client.init()}async list(e,t){let i=await this.resolveFileId(t),s=(await this.client.listFiles(i)).map(NQ);return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=await this.resolveFileId(t),n=await this.client.getFile(i).catch(()=>null),s=await this.client.getDownloadUrl(i).catch(()=>"");if(n){let c=NQ(n);return c.raw_url=s||c.raw_url,c}try{await this.client.listFiles(i);let c=t.split("/").filter(Boolean);return{name:c[c.length-1]||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{}let o=t.split("/").filter(Boolean);return{name:o[o.length-1]||"root",size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:s}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFileId(s);await this.client.mkdir(o,n)}async rename(e,t,i){let n=await this.resolveFileId(t);await this.client.rename(n,i)}async remove(e,t,i){let n=await this.resolveFileId(t);await this.client.remove(n)}async move(e,t,i,n,s){let o=await this.resolveFileId(n),a=await this.resolveFileId(t);await this.client.move(o,a)}async copy(e,t,i,n,s){let o=await this.resolveFileId(n),a=await this.resolveFileId(t);await this.client.copy(o,a)}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"upload",o="/"+n.join("/"),a=await this.resolveFileId(o);await this.client.putFile(a,s,i)}async resolveFileId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return this.client.getRootFolderId();if(this.pathFileIdCache.has(t))return this.pathFileIdCache.get(t);let i=t.split("/"),n=this.client.getRootFolderId();for(let s=0;s{try{return decodeURIComponent(o)}catch{return o}})(),c=i.slice(0,s+1).join("/");if(this.pathFileIdCache.has(c)){n=this.pathFileIdCache.get(c);continue}let d=(await this.client.listFiles(n)).find(u=>u.name===o||u.name===a||u.file_id===o);if(!d)throw new Error(`[AliyundriveOpen] Path '${o}' not found`);n=d.file_id,this.pathFileIdCache.set(c,n)}return n}}});var tf,OQ,MQ,$C=J(()=>{"use strict";tf="application/vnd.google-apps.folder",OQ="application/vnd.google-apps.shortcut",MQ="files(id,name,mimeType,size,modifiedTime,createdTime,thumbnailLink,shortcutDetails,md5Checksum,sha1Checksum,sha256Checksum),nextPageToken"});var Zo,LQ,Qz,vg,HQ=J(()=>{"use strict";$C();Zo="https://www.googleapis.com/drive/v3",LQ="https://www.googleapis.com/upload/drive/v3",Qz="https://oauth2.googleapis.com/token",vg=class{addition;accessToken="";refreshTokenVal="";tokenExpiresAt=0;constructor(e){this.addition=e,this.refreshTokenVal=e.refresh_token||""}getRootFolderId(){return this.addition.root_folder_id?.trim()||"root"}async init(){if(!this.refreshTokenVal||!this.refreshTokenVal.trim()){console.warn("[GoogleDrive] refresh_token is empty, skipping init.");return}try{await this.refreshAccessToken()}catch(e){console.warn("[GoogleDrive] init token refresh warning:",e.message)}}async refreshAccessToken(){let e=this.refreshTokenVal.trim();if(!e)return;let t=this.addition.use_online_api!==!1,i=[];t&&(this.addition.api_url_address?.trim()&&i.push(this.addition.api_url_address.trim()),i.push("https://api.oplist.org/google/token","https://api.oplist.org/google/renewapi","https://api.oplist.org/googledrive/token","https://api-sam.oplist.org/google/token","https://api-sam.oplist.org/googledrive/token","https://api.alist.nn.ci/google/token","https://api.alist.nn.ci/googledrive/token"));for(let o of i)try{let a=new URLSearchParams({refresh_ui:e,server_use:"true",driver_txt:"googleui_go"}),c=await fetch(`${o}?${a.toString()}`,{method:"GET"});if(!c.ok)throw new Error(`[Status ${c.status}]`);let l=await c.json(),d=l.access_token||l.data?.access_token||"",u=l.refresh_token||l.data?.refresh_token||"";if(!d){let f=l.text||l.error||"empty access_token";throw new Error(f)}this.accessToken=d,u&&(this.refreshTokenVal=u),this.tokenExpiresAt=Date.now()+(l.expires_in||3600)*1e3-6e4;return}catch(a){console.warn(`[GoogleDrive] Online API '${o}' failed: ${a.message}`)}let n=(this.addition.client_id||"").trim()||"202264815644-2n82p2e49c7o6026u87j9e22v1n25c27.apps.googleusercontent.com",s=(this.addition.client_secret||"").trim()||"GOCSPX-4bH5Kx3s_89_j6j2x-2x3-8x";if(n&&s)try{let o=await fetch(Qz,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({client_id:n,client_secret:s,refresh_token:e,grant_type:"refresh_token"}).toString()});if(!o.ok){let c=await o.text().catch(()=>"");throw new Error(`[Status ${o.status}] ${c}`)}let a=await o.json();if(!a.access_token)throw new Error(`Invalid OAuth response: ${JSON.stringify(a)}`);this.accessToken=a.access_token,a.refresh_token&&(this.refreshTokenVal=a.refresh_token),this.tokenExpiresAt=Date.now()+(a.expires_in||3600)*1e3-6e4;return}catch(o){console.warn(`[GoogleDrive] Direct OAuth failed: ${o.message}`)}throw new Error("[GoogleDrive] All token refresh strategies failed. Please check: 1) refresh_token is valid, 2) api_url_address is accessible, 3) If using direct OAuth: client_id and client_secret are correct.")}async ensureToken(){(!this.accessToken||Date.now()>=this.tokenExpiresAt)&&await this.refreshAccessToken()}async request(e,t={},i=!0){await this.ensureToken();let n=await fetch(e,{...t,headers:{Authorization:`Bearer ${this.accessToken}`,...t.headers||{}}});if(n.status===401&&i)return console.warn("[GoogleDrive] 401 Unauthorized, refreshing token..."),await this.refreshAccessToken(),this.request(e,t,!1);if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`[GoogleDrive] API error [${n.status}]: ${s}`)}return n.status===204?null:n.json()}async listFiles(e){let t=[],i,n=this.addition.order_by||"folder,name,modifiedTime desc";do{let s=new URLSearchParams({q:`'${e}' in parents and trashed = false`,fields:MQ,orderBy:n,pageSize:"1000",includeItemsFromAllDrives:"true",supportsAllDrives:"true"});i&&s.set("pageToken",i);let o=`${Zo}/files?${s.toString()}`,a=await this.request(o),c=a.files||[];for(let l of c)l.mimeType===OQ&&l.shortcutDetails?.targetId&&(l.id=l.shortcutDetails.targetId,l.mimeType=l.shortcutDetails.targetMimeType||l.mimeType);t.push(...c),i=a.nextPageToken}while(i);return t}async getFile(e){let t=new URLSearchParams({fields:"id,name,mimeType,size,modifiedTime,md5Checksum",includeItemsFromAllDrives:"true",supportsAllDrives:"true"});return this.request(`${Zo}/files/${e}?${t.toString()}`)}getDownloadUrl(e){return`${Zo}/files/${e}?includeItemsFromAllDrives=true&supportsAllDrives=true&alt=media&acknowledgeAbuse=true`}getDownloadHeaders(){return{Authorization:`Bearer ${this.accessToken}`}}async mkdir(e,t){await this.request(`${Zo}/files?supportsAllDrives=true`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,parents:[e],mimeType:tf})})}async rename(e,t){await this.request(`${Zo}/files/${e}?supportsAllDrives=true`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t})})}async remove(e){await this.request(`${Zo}/files/${e}?supportsAllDrives=true`,{method:"DELETE"})}async move(e,t,i){let n=new URLSearchParams({addParents:i,removeParents:t,supportsAllDrives:"true"});await this.request(`${Zo}/files/${e}?${n.toString()}`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:"{}"})}async copy(e,t,i){await this.request(`${Zo}/files/${e}/copy?supportsAllDrives=true`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:i,parents:[t]})})}async putFile(e,t,i,n="application/octet-stream"){let s=(this.addition.chunk_size||5)*1024*1024;if(i.length<=s){let o=new URLSearchParams({uploadType:"multipart",supportsAllDrives:"true"}),a=`----GoogleDriveBoundary${Date.now()}`,c=JSON.stringify({name:t,parents:[e]}),l=`--${a}\r -Content-Type: application/json\r -\r -${c}\r ---${a}\r -Content-Type: ${n}\r -\r -`,d=Buffer.from(l),u=Buffer.from(`\r ---${a}--`),f=Buffer.concat([d,i,u]);await this.request(`${LQ}/files?${o.toString()}`,{method:"POST",headers:{"Content-Type":`multipart/related; boundary=${a}`},body:f})}else{let o=new URLSearchParams({uploadType:"resumable",supportsAllDrives:"true"});await this.ensureToken();let a=await fetch(`${LQ}/files?${o.toString()}`,{method:"POST",headers:{Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json","X-Upload-Content-Type":n,"X-Upload-Content-Length":String(i.length)},body:JSON.stringify({name:t,parents:[e]})});if(!a.ok)throw new Error(`[GoogleDrive] Resumable upload init failed: ${a.status}`);let c=a.headers.get("location");if(!c)throw new Error("[GoogleDrive] No upload URL returned");let l=0;for(;l{try{return decodeURIComponent(o)}catch{return o}})(),c=i.slice(0,s+1).join("/");if(this.pathCache.has(c)){n=this.pathCache.get(c);continue}let d=(await this.listFiles(n)).find(u=>u.name===o||u.name===a||u.id===o);if(!d)throw new Error(`[GoogleDrive] Path '${o}' not found`);n=d.id,this.pathCache.set(c,n)}return n}async resolveParentAndName(e){let t=e.split("/").filter(Boolean),i=t.pop()||"unnamed",n="/"+t.join("/");return{parentId:await this.resolveFileId(n),name:i}}}});function KQ(r){return{name:r.name,size:r.size?parseInt(r.size,10):0,is_dir:r.mimeType===tf,modified:r.modifiedTime||r.createdTime||new Date().toISOString(),sign:"",type:r.mimeType===tf?1:0,thumb:r.thumbnailLink||"",raw_url:""}}var kg,GQ=J(()=>{"use strict";He();$C();HQ();kg=class{client;addition;constructor(e){this.addition=e,this.client=new vg(e)}async init(){await this.client.init()}async list(e,t){let i=await this.client.resolveFileId(t),s=(await this.client.listFiles(i)).map(KQ);return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=await this.client.resolveFileId(t),n=await this.client.getFile(i).catch(()=>null);if(n){let a=KQ(n);return a.raw_url=this.client.getDownloadUrl(i),a.raw_url_headers=this.client.getDownloadHeaders(),a}let s=t.split("/").filter(Boolean),o=s[s.length-1]||"root";try{return await this.client.listFiles(i),{name:o,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{}return{name:o,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}async mkdir(e,t){let{parentId:i,name:n}=await this.client.resolveParentAndName(t);await this.client.mkdir(i,n)}async rename(e,t,i){let n=await this.client.resolveFileId(t);await this.client.rename(n,i)}async remove(e,t,i){let n=await this.client.resolveFileId(t);await this.client.remove(n)}async move(e,t,i,n,s){let o=await this.client.resolveFileId(n),a=n.split("/").filter(Boolean);a.pop();let c=await this.client.resolveFileId("/"+a.join("/")),l=await this.client.resolveFileId(t);await this.client.move(o,c,l)}async copy(e,t,i,n,s){let o=await this.client.resolveFileId(n),a=n.split("/").filter(Boolean).pop()||"copy",c=await this.client.resolveFileId(t);await this.client.copy(o,c,a)}async put(e,t,i){let{parentId:n,name:s}=await this.client.resolveParentAndName(t);await this.client.putFile(n,s,i)}}});function Pz(r="Quark"){return r==="UC"?Fz:_z}function zQ(r,e,t){let i=r.split(";").map(o=>o.trim()).filter(Boolean),n=i.findIndex(o=>{let a=o.indexOf("=");return a!==-1&&o.substring(0,a).trim()===e}),s=`${e}=${t}`;return n!==-1?i[n]=s:i.push(s),i.join("; ")}function JQ(r,e){let t=r.split(/,(?=[^;]+=[^;]+)/);for(let i of t){let s=i.split(";")[0].trim(),o=s.indexOf("=");if(o!==-1&&s.substring(0,o).trim()===e)return s.substring(o+1).trim()}return null}function Tz(r){return r.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'").replace(/'/g,"'")}function Uz(r){let e=r.split(".").pop()?.toLowerCase()||"",t=["mp4","mkv","avi","mov","flv","wmv","ts","m2ts","m4v","rmvb","webm"],i=["mp3","flac","aac","wav","ogg","m4a","opus"],n=["jpg","jpeg","png","gif","bmp","webp","heic","tiff"],s=["pdf","doc","docx","xls","xlsx","ppt","pptx","txt","md"];return t.includes(e)?"video":i.includes(e)?"audio":n.includes(e)?"image":s.includes(e)?"doc":"others"}var _z,Fz,Sg,jQ=J(()=>{"use strict";_z={ua:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",referer:"https://pan.quark.cn",api:"https://drive-m.quark.cn/1/clouddrive",pr:"ucpro"},Fz={ua:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uc-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",referer:"https://drive.uc.cn",api:"https://pc-api.uc.cn/1/clouddrive",pr:"UCBrowser"};Sg=class{addition;conf;cookie;onCookieUpdate;constructor(e,t){this.addition=e,this.conf=Pz(e.variant||"Quark"),this.cookie=e.cookie||"",this.onCookieUpdate=t}getRootFolderId(){return(this.addition.root_folder_id||"").trim()||"0"}getVariant(){return this.addition.variant||"Quark"}getConf(){return this.conf}getCookie(){return this.cookie}async request(e,t,i,n){let s=new URL(this.conf.api+e);if(s.searchParams.set("pr",this.conf.pr),s.searchParams.set("fr","pc"),i)for(let[u,f]of Object.entries(i))s.searchParams.set(u,f);let o={Cookie:this.cookie,Accept:"application/json, text/plain, */*",Referer:this.conf.referer,"Content-Type":"application/json","User-Agent":this.conf.ua},a={method:t,headers:o};n!==void 0&&t!=="GET"&&(a.body=JSON.stringify(n));let c=await fetch(s.toString(),a),l=c.headers.get("set-cookie");if(l){let u=JQ(l,"__puus");if(u&&(this.cookie=zQ(this.cookie,"__puus",u),this.onCookieUpdate?.(this.cookie)),this.addition.variant==="Quark"){let f=JQ(l,"__pus");f&&(this.cookie=zQ(this.cookie,"__pus",f),this.onCookieUpdate?.(this.cookie))}}let d=await c.json();if(!c.ok||d.status!==void 0&&d.status>=400||d.code!==void 0&&d.code!==0){let u=d.message||d.msg||`HTTP ${c.status}`;throw new Error(`[Quark/UC] API error [${c.status}] ${e}: ${u}`)}return d}async getFiles(e){let t=[],i=1,n=100,s={pdir_fid:e,_size:String(n),_fetch_total:"1",fetch_all_file:"1",fetch_risk_file_name:"1"};if(this.addition.order_by&&this.addition.order_by!=="none"){let o=this.addition.order_direction||"asc";s._sort=`file_type:asc,${this.addition.order_by}:${o}`}for(;;){s._page=String(i);let o=await this.request("/file/sort","GET",s),a=o?.data?.list||[];if(a.length===0)break;for(let l of a)l.file_name=Tz(l.file_name),this.addition.only_list_video_file?(!l.file||l.category===1)&&t.push(l):t.push(l);let c=o.metadata?.total??0;if(c>0&&i*n>=c||a.length{"use strict";Be();jQ();Dg=class{client;pathFileIdCache=new Map;constructor(e){this.client=new Sg(e)}async init(){await this.client.init()}async list(e,t){let i=await this.resolveFileId(t);return(await this.client.getFiles(i)).map($Q)}async get(e,t){let i=t.split("/").filter(Boolean),n=await this.resolveFileId(t),s=i[i.length-1]||"root",o=(()=>{try{return decodeURIComponent(s)}catch{return s}})(),a="/"+i.slice(0,i.length-1).join("/"),c=await this.resolveFileId(a),d=(await this.client.getFiles(c)).find(h=>h.fid===n||h.file_name===s||h.file_name===o),u="",f;try{let h=await this.client.getDownloadUrl(n,o);u=h.url,f=h.headers}catch(h){console.warn(`[Quark/UC] getDownloadUrl warning for ${s}:`,h.message)}if(d){let h=$Q(d);return h.raw_url=u,h.raw_url_headers=f,h}try{return await this.client.getFiles(n),{name:o||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{}return{name:o||"root",size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:u,raw_url_headers:f}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFileId(s);await this.client.mkdir(o,n)}async rename(e,t,i){let n=await this.resolveFileId(t);await this.client.rename(n,i)}async remove(e,t,i){let n=await this.resolveFileId(t);await this.client.remove([n])}async move(e,t,i,n,s){let o=await this.resolveFileId(n),a=await this.resolveFileId(t);await this.client.move([o],a)}async copy(e,t,i,n,s){let o=await this.resolveFileId(n),a=await this.resolveFileId(t);await this.client.copy([o],a)}async put(e,t,i){throw new Error("[Quark/UC] Direct put not supported in stateless environment")}async resolveFileId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return this.client.getRootFolderId();if(this.pathFileIdCache.has(t))return this.pathFileIdCache.get(t);let i=t.split("/"),n=this.client.getRootFolderId();for(let s=0;s{try{return decodeURIComponent(o)}catch{return o}})(),l=(await this.client.getFiles(n)).find(u=>u.file_name===o||u.file_name===a||u.fid===o);if(!l)throw new Error(`[Quark/UC] Path '${o}' not found in folder '${n}'`);n=l.fid;let d="/"+i.slice(0,s+1).join("/");this.pathFileIdCache.set(d,n)}return n}}});var YQ,Rz,WQ,Qg,XQ=J(()=>{"use strict";YQ="https://proapi.115.com/open",Rz="https://passportapi.115.com",WQ="Mozilla/5.0 115disk/42.0.0.2",Qg=class{accessToken;refreshToken;addition;rootId;constructor(e){this.addition=e,this.accessToken=e.access_token||"",this.refreshToken=e.refresh_token||"",this.rootId=e.root_folder_id||"0"}getRootFolderId(){return this.rootId}async init(){if(!this.accessToken)throw new Error("[115] access_token is required");await this.getUserInfo()}async request(e,t={},i=YQ){let n=`${i}${e}`,o=await(await fetch(n,{method:"POST",headers:{"User-Agent":WQ,Accept:"application/json","Content-Type":"application/json",Authorization:`Bearer ${this.accessToken}`},body:JSON.stringify(t)})).json().catch(()=>({}));if(o?.state===!1||o?.errno){if(o.errno===990001||o.errcode===990001||o.errno===10008)return await this.refreshAccessToken(),this.request(e,t,i);throw new Error(`[115] ${o.error||o.errmsg||"API error"} (${o.errno||o.errcode||""})`)}return o}async getUserInfo(){return this.request("/user/info",{},Rz)}async refreshAccessToken(){let e=`${YQ}/oauth2/token`,i=await(await fetch(e,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":WQ},body:JSON.stringify({grant_type:"refresh_token",refresh_token:this.refreshToken})})).json().catch(()=>({}));if(i?.access_token)this.accessToken=i.access_token,this.addition.access_token=i.access_token,i.refresh_token&&(this.refreshToken=i.refresh_token,this.addition.refresh_token=i.refresh_token);else throw new Error(`[115] refresh token failed: ${JSON.stringify(i)}`)}async getFiles(e){let t=await this.request("/open/ufile/files",{cid:e,limit:1e3,offset:0,order:this.addition.order_by||"user_utime",asc:this.addition.order_direction==="asc"?1:0,show_dir:1,fc_mix:1});return t?.data&&Array.isArray(t.data)?t.data:t?.files&&Array.isArray(t.files)?t.files.map(i=>({Fid:i.fid??i.id,Fn:i.n??i.file_name??i.name,Fc:String(i.fc??i.category??(i.pid!==void 0&&!i.pid?"0":"1")),FS:Number(i.s??i.size??0),Sha1:i.sha1,Pc:i.pc??i.pick_code,Thumbnail:i.thumb??i.thumbnail,Upt:i.tu??i.upt??i.updated_at,Pid:i.pid??i.cid})):[]}async resolvePathId(e,t){let i=e.split("/").filter(Boolean).join("/");if(!i)return this.rootId;if(t.has(i))return t.get(i);let n=i.split("/"),s=this.rootId;for(let o=0;o{try{return decodeURIComponent(a)}catch{return a}})(),d=(await this.getFiles(s)).find(f=>f.Fn===a||f.Fn===c||f.Fid===a);if(!d)throw new Error(`[115] Path '${a}' not found in '${s}'`);s=d.Fid;let u="/"+n.slice(0,o+1).join("/");t.set(u,s)}return s}async mkdir(e,t){await this.request("/open/ufile/add",{pid:e,cname:t})}async rename(e,t){await this.request("/open/ufile/edit",{fid:e,fname:t})}async move(e,t){await this.request("/open/ufile/move",{pid:e,fid:t,no_dupli:"1"})}async copy(e,t){await this.request("/open/ufile/copy",{pid:e,fid:t,no_dupli:"1"})}async remove(e){await this.request("/open/ufile/delete",{fid:e,ignore_warn:1})}async getDownloadUrl(e){let t=await this.request("/open/ufile/downurl",{pickcode:e}),i=t?.url||t?.data?.url||"";if(!i)throw new Error("[115] empty download url");return i}}});function ZQ(r){let e=r.Fc==="0",t=r.Upt?new Date(r.Upt*1e3).toISOString():new Date().toISOString();return{name:r.Fn,size:r.FS||0,is_dir:e,modified:t,sign:"",type:X(r.Fn,e),thumb:r.Thumbnail||"",raw_url:""}}var _g,e_=J(()=>{"use strict";Be();XQ();_g=class{client;pathCache=new Map;constructor(e){this.client=new Qg(e)}async init(){await this.client.init()}async list(e,t){let i=await this.client.resolvePathId(t,this.pathCache);return(await this.client.getFiles(i)).map(ZQ)}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s=(()=>{try{return decodeURIComponent(n)}catch{return n}})(),o="/"+i.slice(0,-1).join("/"),a=await this.client.resolvePathId(o,this.pathCache),l=(await this.client.getFiles(a)).find(d=>d.Fn===n||d.Fn===s||d.Fid===n);if(l){let d=ZQ(l);if(!d.is_dir&&l.Pc)try{let u=await this.client.getDownloadUrl(l.Pc);d.raw_url=u}catch(u){d.raw_url_error=u.message}return d}try{let d=await this.client.resolvePathId(t,this.pathCache);return await this.client.getFiles(d),{name:s||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:s||"root",size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.client.resolvePathId(s,this.pathCache);await this.client.mkdir(o,n)}async rename(e,t,i){let n=await this.client.resolvePathId(t,this.pathCache);await this.client.rename(n,i)}async remove(e,t,i){let n=await this.client.resolvePathId(t,this.pathCache);await this.client.remove([n])}async move(e,t,i,n,s){let o=await this.client.resolvePathId(n,this.pathCache),a=await this.client.resolvePathId(t,this.pathCache);await this.client.move(a,[o])}async copy(e,t,i,n,s){let o=await this.client.resolvePathId(n,this.pathCache),a=await this.client.resolvePathId(t,this.pathCache);await this.client.copy(a,[o])}async put(e,t,i){throw new Error("[115] Direct put not supported in stateless environment")}}});var Fg,t_=J(()=>{"use strict";Fg=class{address;addition;accessToken="";constructor(e){this.addition=e,this.address=(e.address||"").replace(/\/+$/,""),this.accessToken=e.access_token||""}async init(){if(!this.address)throw new Error("[Cloudreve] address is required");this.addition.username&&this.addition.password?await this.login():this.addition.refresh_token&&await this.refreshToken()}async login(){let t=await(await fetch(`${this.address}/api/v4/session`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:this.addition.username,password:this.addition.password})})).json().catch(()=>({}));if(t?.token)this.accessToken=t.token,t.refresh_token&&(this.addition.refresh_token=t.refresh_token);else throw new Error("[Cloudreve] login failed")}async refreshToken(){let t=await(await fetch(`${this.address}/api/v4/session/renew`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.addition.refresh_token}`}})).json().catch(()=>({}));t?.token&&(this.accessToken=t.token)}async request(e,t,i,n=!1){let s={Authorization:`Bearer ${this.accessToken}`,"Content-Type":"application/json"};this.addition.custom_ua&&(s["User-Agent"]=this.addition.custom_ua);let o={method:e,headers:s};i&&(o.body=JSON.stringify(i));let a=await fetch(`${this.address}/api/v4${t}`,o);if(a.status===401&&!n)return this.addition.refresh_token?await this.refreshToken():this.addition.username&&await this.login(),this.request(e,t,i,!0);if(e==="DELETE"&&a.ok)return{};let c=await a.json().catch(()=>({}));if(a.status>=400)throw new Error(`[Cloudreve] ${c?.msg||c?.message||`HTTP ${a.status}`}`);return c}async listFiles(e){let t=[],i="",n=e==="/"?"/":e;do{let s={page_size:"100",uri:n,order_by:this.addition.order_by||"name",order_direction:this.addition.order_direction||"asc",page:"0"};i&&(s.next_page_token=i);let o=new URLSearchParams(s).toString(),a=await this.request("GET",`/file?${o}`);a?.files&&t.push(...a.files),i=a?.pagination?.next_token||""}while(i);return t}async getDownloadUrl(e){let i=(await this.request("POST","/file/url",{uris:[e],download:!0}))?.urls?.[0]?.url||"";if(!i)throw new Error("[Cloudreve] empty download url");return i}async mkdir(e,t){let i=`${e.replace(/\/+$/,"")}/${t}`;await this.request("POST","/file/create",{type:"folder",uri:i,error_on_conflict:!0})}async rename(e,t){let i=e.lastIndexOf("/"),s=`${i>=0?e.slice(0,i+1):"/"}${t}`;await this.request("POST","/file/rename",{uris:[e],new_uri:s})}async move(e,t,i){await this.request("POST","/file/move",{uris:e,dst:t,copy:i})}async remove(e){await this.request("DELETE","/file",{uris:e,unlink:!1,skip_soft_delete:!0})}}});function r_(r){let e=r.type===1;return{name:r.name,size:r.size||0,is_dir:e,modified:r.updated_at||new Date().toISOString(),sign:"",type:X(r.name,e),raw_url:""}}function Ms(r){return!r||r==="/"?"/":r.startsWith("/")?r:"/"+r}var Pg,i_=J(()=>{"use strict";Be();t_();Pg=class{client;constructor(e){this.client=new Fg(e)}async init(){await this.client.init()}async list(e,t){return(await this.client.listFiles(Ms(t))).map(r_)}async get(e,t){let i=Ms(t),n=i.split("/").filter(Boolean),s=n[n.length-1]||"root",o="/"+n.slice(0,-1).join("/");try{let c=(await this.client.listFiles(o==="//"?"/":o)).find(l=>l.name===s||l.path===i);if(c){let l=r_(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(i)}catch(d){l.raw_url_error=d.message}return l}}catch{}try{let a=await this.client.listFiles(i);return{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:s,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let n=Ms(t).split("/").filter(Boolean),s=n.pop()||"new_folder",o="/"+n.join("/");await this.client.mkdir(o==="//"?"/":o,s)}async rename(e,t,i){await this.client.rename(Ms(t),i)}async remove(e,t,i){await this.client.remove([Ms(t)])}async move(e,t,i,n,s){await this.client.move([Ms(n)],Ms(t),!1)}async copy(e,t,i,n,s){await this.client.move([Ms(n)],Ms(t),!0)}async put(e,t,i){throw new Error("[Cloudreve] Direct put not supported in stateless environment")}}});var Nz,Tg,n_=J(()=>{"use strict";Nz="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Tg=class{addition;address;cookie;persistCookie;constructor(e,t){this.addition=e,this.address=(e.address||"").replace(/\/+$/,""),this.cookie=e.cookie||"",this.persistCookie=t}async init(){if(!this.address)throw new Error("[Cloudreve] address is required");this.cookie||await this.login()}getCookie(){return this.cookie}getUA(){return this.addition.custom_ua||Nz}async login(){if(!this.addition.username||!this.addition.password)throw new Error("[Cloudreve] username/password or cookie is required");let e=await fetch(`${this.address}/api/v3/user/session`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":this.getUA()},body:JSON.stringify({username:this.addition.username,Password:this.addition.password,captchaCode:""})}),t=await e.json().catch(()=>({}));if(t.code!==0)throw new Error(`[Cloudreve] login failed: ${t.msg}`);let i=this.extractSessionCookie(e);if(!i)throw new Error("[Cloudreve] login failed: no session cookie");this.cookie=i,this.persistCookie&&await this.persistCookie(this.cookie)}extractSessionCookie(e){let t=typeof e.headers.getSetCookie=="function"?e.headers.getSetCookie():[];if(Array.isArray(t)){for(let s of t)if(s&&s.includes("cloudreve-session="))return s.split(";")[0].split("=")[1]}let i=e.headers.get("set-cookie")||"",n=/cloudreve-session=([^;]+)/.exec(i);return n?n[1]:""}async request(e,t,i,n=!1){let s={Cookie:`cloudreve-session=${this.cookie}`,Accept:"application/json, text/plain, */*","User-Agent":this.getUA()},o={method:e,headers:s};i!==void 0&&(s["Content-Type"]="application/json",o.body=JSON.stringify(i));let a=await fetch(`${this.address}/api/v3${t}`,o),c=await a.json().catch(()=>({}));if(c.code===401&&t!=="/user/session"&&!n&&this.addition.username&&this.addition.password)return await this.login(),this.request(e,t,i,!0);if(c.code!==0)throw new Error(`[Cloudreve] ${c.msg||`HTTP ${a.status}`}`);let l=this.extractSessionCookie(a);return l&&(this.cookie=l),c.data}async listFiles(e){return(await this.request("GET",`/directory${e}`))?.objects||[]}async getDownloadUrl(e){let t=await this.request("PUT",`/file/download/${e}`);if(!t)throw new Error("[Cloudreve] empty download url");return t.startsWith("/api")?this.address+t:t}downloadHeaders(){return{Referer:this.address,"User-Agent":this.getUA()}}async mkdir(e){await this.request("PUT","/directory",{path:e})}async move(e,t,i){await this.request("PATCH","/object",{action:"move",src_dir:e,dst:t,src:i})}async rename(e,t){await this.request("PATCH","/object/rename",{action:"rename",new_name:t,src:e})}async copy(e,t,i){await this.request("POST","/object/copy",{src_dir:e,dst:t,src:i})}async remove(e){await this.request("DELETE","/object",e)}}});function s_(r){let e=r.type==="dir";return{name:r.name,size:r.size||0,is_dir:e,modified:r.date||r.create_date||new Date().toISOString(),sign:r.id,type:X(r.name,e),raw_url:""}}function ql(r){return!r||r==="/"?"/":r.startsWith("/")?r:"/"+r}var Ug,o_=J(()=>{"use strict";Be();n_();Ug=class{client;constructor(e,t){this.client=new Tg(e,t)}async init(){await this.client.init()}async list(e,t){return(await this.client.listFiles(ql(t))).map(s_)}async get(e,t){let n=ql(t).split("/").filter(Boolean),s=n[n.length-1]||"root",o="/"+n.slice(0,-1).join("/");try{let c=(await this.client.listFiles(o==="//"?"/":o)).find(l=>l.name===s);if(c){let l=s_(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c.id),l.raw_url_headers=this.client.downloadHeaders()}catch(d){l.raw_url_error=d.message}return l}}catch{}return{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){await this.client.mkdir(ql(t))}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.src,i)}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.src)}async move(e,t,i,n,s){let o=await this.resolveEntry(n),c="/"+n.split("/").filter(Boolean).slice(0,-1).join("/");await this.client.move(c,ql(t),o.src)}async copy(e,t,i,n,s){let o=await this.resolveEntry(n),c="/"+n.split("/").filter(Boolean).slice(0,-1).join("/");await this.client.copy(c,ql(t),o.src)}async put(){throw new Error("[Cloudreve] Direct put not supported in stateless environment")}async resolveEntry(e){let i=ql(e).split("/").filter(Boolean),n=i[i.length-1],s="/"+i.slice(0,-1).join("/"),a=(await this.client.listFiles(s==="//"?"/":s)).find(d=>d.name===n);if(!a)throw new Error(`[Cloudreve] '${n}' not found`);let c=a.type==="dir";return{src:c?{dirs:[a.id],items:[]}:{dirs:[],items:[a.id]},isDir:c}}}});var Rg,a_=J(()=>{"use strict";Rg=class{url;addition;token="";constructor(e){this.addition=e,this.url=(e.url||"").replace(/\/+$/,""),this.token=e.token||""}async init(){if(!this.url)throw new Error("[OpenList] url is required");this.addition.username&&this.addition.password&&await this.login()}async login(){let t=await(await fetch(`${this.url}/api/auth/login`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:this.addition.username,password:this.addition.password})})).json().catch(()=>({}));if(t?.data?.token)this.token=t.data.token;else throw new Error("[OpenList] login failed")}async request(e,t="POST",i){let n={"Content-Type":"application/json"};this.token&&(n.Authorization=this.token),this.addition.meta_password&&(n["Meta-Password"]=this.addition.meta_password);let s={method:t,headers:n};return i&&(s.body=JSON.stringify(i)),await(await fetch(`${this.url}${e}`,s)).json()}async list(e){let t=await this.request("/api/fs/list","POST",{path:e,page:1,per_page:0});if(t.code!==200)throw new Error(`[OpenList] ${t.message}`);return t.data?.content||[]}async get(e){let t=await this.request("/api/fs/get","POST",{path:e});if(t.code!==200)throw new Error(`[OpenList] ${t.message}`);return t.data}async mkdir(e){let t=await this.request("/api/fs/mkdir","POST",{path:e});if(t.code!==200)throw new Error(`[OpenList] ${t.message}`)}async rename(e,t){let i=await this.request("/api/fs/rename","POST",{path:e,name:t});if(i.code!==200)throw new Error(`[OpenList] ${i.message}`)}async move(e,t,i){let n=await this.request("/api/fs/move","POST",{src_dir:e,dst_dir:t,names:i});if(n.code!==200)throw new Error(`[OpenList] ${n.message}`)}async copy(e,t,i){let n=await this.request("/api/fs/copy","POST",{src_dir:e,dst_dir:t,names:i});if(n.code!==200)throw new Error(`[OpenList] ${n.message}`)}async remove(e,t){let i=await this.request("/api/fs/remove","POST",{dir:e,names:t});if(i.code!==200)throw new Error(`[OpenList] ${i.message}`)}}});var Ng,c_=J(()=>{"use strict";Be();a_();Ng=class{client;constructor(e){this.client=new Rg(e)}async init(){await this.client.init()}async list(e,t){return(await this.client.list(t)).map(n=>({name:n.name,size:n.size||0,is_dir:!!n.is_dir,modified:n.modified||new Date().toISOString(),sign:n.sign||"",type:X(n.name,!!n.is_dir),thumb:n.thumb||"",raw_url:n.raw_url||""}))}async get(e,t){try{let i=await this.client.get(t);return{name:i.name,size:i.size||0,is_dir:!!i.is_dir,modified:i.modified||new Date().toISOString(),sign:i.sign||"",type:X(i.name,!!i.is_dir),raw_url:i.raw_url||""}}catch{try{await this.client.list(t);let i=t.split("/").filter(Boolean);return{name:i[i.length-1]||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{let i=t.split("/").filter(Boolean);return{name:i[i.length-1]||"root",size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}}async mkdir(e,t){await this.client.mkdir(t)}async rename(e,t,i){await this.client.rename(t,i)}async remove(e,t,i){let n=t.split("/").filter(Boolean),s="/"+n.slice(0,-1).join("/");await this.client.remove(s,i.length?i:[n[n.length-1]])}async move(e,t,i,n,s){await this.client.move(e,t,i)}async copy(e,t,i,n,s){await this.client.copy(e,t,i)}async put(e,t,i){throw new Error("[OpenList] Direct put not supported in stateless environment")}}});var qg,l_=J(()=>{"use strict";qg=class{url;sid;pwd;constructor(e){this.url=(e.url||"").replace(/\/+$/,""),this.sid=e.sid||"",this.pwd=e.pwd||""}init(){if(!this.url)throw new Error("[OpenListShare] url is required");if(!this.sid)throw new Error("[OpenListShare] sid is required")}async request(e,t="POST",i){return await(await fetch(`${this.url}${e}`,{method:t,headers:{"Content-Type":"application/json"},body:i?JSON.stringify(i):void 0})).json()}sharePath(e){return"/@s/"+this.sid+(e==="/"?"":e)}async list(e){let t=await this.request("/api/fs/list","POST",{path:this.sharePath(e),password:this.pwd,refresh:!1,page:1,per_page:0});if(t.code!==200)throw new Error(`[OpenListShare] ${t.message}`);return t.data?.content||[]}downloadUrl(e){let t="/"+this.sid+e;return`${this.url}/sd${t}?pwd=${encodeURIComponent(this.pwd)}`}}});function d_(r){return{name:r.name,size:r.size||0,is_dir:r.is_dir,modified:r.modified||new Date().toISOString(),sign:r.sign||"",type:X(r.name,r.is_dir),thumb:r.thumb||"",raw_url:""}}function u_(r){return!r||r==="/"?"/":r.startsWith("/")?r:"/"+r}var Og,f_=J(()=>{"use strict";Be();l_();Og=class{client;constructor(e){this.client=new qg(e)}async init(){this.client.init()}async list(e,t){return(await this.client.list(u_(t))).map(d_)}async get(e,t){let i=u_(t),n=i.split("/").filter(Boolean),s=n[n.length-1]||"root",o="/"+n.slice(0,-1).join("/");try{let c=(await this.client.list(o==="//"?"/":o)).find(l=>l.name===s);if(c){let l=d_(c);return l.is_dir||(l.raw_url=this.client.downloadUrl(i)),l}}catch{}return{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(){throw new Error("[OpenListShare] read-only share, mkdir not supported")}async rename(){throw new Error("[OpenListShare] read-only share, rename not supported")}async remove(){throw new Error("[OpenListShare] read-only share, remove not supported")}async move(){throw new Error("[OpenListShare] read-only share, move not supported")}async copy(){throw new Error("[OpenListShare] read-only share, copy not supported")}async put(){throw new Error("[OpenListShare] read-only share, upload not supported")}}});var Mg,A_=J(()=>{"use strict";Mg=class{url;token;constructor(e){this.url=(e.url||"").replace(/\/+$/,""),this.token=e.access_token||""}async init(){if(!this.url)throw new Error("[TelDrive] url is required");if(!this.token)throw new Error("[TelDrive] access_token is required")}async request(e,t="GET",i){let n={Authorization:`Bearer ${this.token}`,"Content-Type":"application/json"},s={method:t,headers:n};i&&(s.body=JSON.stringify(i));let o=await fetch(`${this.url}${e}`,s);if(t==="DELETE"&&o.ok)return{};let a=await o.json().catch(()=>({}));if(o.status>=400)throw new Error(`[TelDrive] ${a?.message||`HTTP ${o.status}`}`);return a}async listFiles(e){let t=await this.request(`/api/files?path=${encodeURIComponent(e)}`);return Array.isArray(t)?t:t?.files||[]}async mkdir(e){await this.request("/api/files/mkdir","POST",{path:e})}async move(e,t){await this.request("/api/files/move","POST",{ids:e,destination:t})}async remove(e){await this.request(`/api/files/${e}`,"DELETE")}downloadUrl(e){return`${this.url}/api/files/${e}`}downloadHeaders(){return{Authorization:`Bearer ${this.token}`}}}});function h_(r){let e=r.type==="folder";return{name:r.name,size:r.size||0,is_dir:e,modified:r.updatedAt||r.createdAt||new Date().toISOString(),sign:"",type:X(r.name,e),raw_url:""}}var Lg,g_=J(()=>{"use strict";Be();A_();Lg=class{client;pathCache=new Map;constructor(e){this.client=new Mg(e)}async init(){await this.client.init()}async list(e,t){return(await this.client.listFiles(t)).map(h_)}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let a=(await this.client.listFiles(s==="//"?"/":s)).find(c=>c.name===n);if(a){let c=h_(a);return c.is_dir||(c.raw_url=this.client.downloadUrl(a.id),c.raw_url_headers=this.client.downloadHeaders()),c}}catch{}try{return await this.client.listFiles(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){await this.client.mkdir(t)}async rename(e,t,i){throw new Error("[TelDrive] rename not supported")}async remove(e,t,i){let n=await this.resolveFileId(t);await this.client.remove(n)}async move(e,t,i,n,s){let o=await this.resolveFileId(n),a=await this.resolveFileId(t);await this.client.move([o],a)}async copy(e,t,i,n,s){throw new Error("[TelDrive] copy not supported")}async put(e,t,i){throw new Error("[TelDrive] Direct put not supported in stateless environment")}async resolveFileId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"/";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n="",s="/";for(let o=0;ol.name===i[o]);if(!c)throw new Error(`[TelDrive] Path '${i[o]}' not found`);s=c.id,n=n?`${n}/${i[o]}`:`/${i[o]}`,this.pathCache.set(n,s)}return s}}});var qz,rf,Hg,p_=J(()=>{"use strict";qz="https://www.mediafire.com/api/1.5",rf="https://www.mediafire.com",Hg=class{cookie;sessionToken;chunkSize;orderBy;orderDirection;constructor(e){this.cookie=e.cookie||"",this.sessionToken=e.session_token||"",this.chunkSize=e.chunk_size||200,this.orderBy=e.order_by||"name",this.orderDirection=e.order_direction||"asc"}async init(){if(!this.cookie)throw new Error("[MediaFire] cookie is required");this.sessionToken||await this.getSessionToken()}async postForm(e,t){let i=new URLSearchParams({session_token:this.sessionToken,response_format:"json",...t});return await(await fetch(`${qz}${e}`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Cookie:this.cookie,"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",Origin:rf,Referer:`${rf}/`},body:i.toString()})).json().catch(()=>({}))}async getSessionToken(){let i=(await(await fetch(`${rf}/application/get_session_token.php`,{method:"POST",headers:{Cookie:this.cookie,Origin:rf,Referer:`${rf}/`,"Content-Length":"0"}})).json().catch(()=>({})))?.response?.session_token;if(!i)throw new Error("[MediaFire] failed to get session token from cookie");this.sessionToken=i,this.addition_session_token=i}addition_session_token="";checkResult(e){if(e&&e!=="Success")throw new Error(`[MediaFire] API error: ${e}`)}async list(e){let t=[],i=1,n=!0;for(;n;){let s=await this.postForm("/folder/get_content.php",{folder_key:e,content_type:"folders",chunk:String(i),chunk_size:String(this.chunkSize),details:"yes",order_direction:this.orderDirection,order_by:this.orderBy,filter:""}),o=await this.postForm("/folder/get_content.php",{folder_key:e,content_type:"files",chunk:String(i),chunk_size:String(this.chunkSize),details:"yes",order_direction:this.orderDirection,order_by:this.orderBy,filter:""}),a=s.response?.folder_content?.folders||[],c=o.response?.folder_content?.files||[];for(let l of a)t.push({id:l.folderkey,name:l.name,size:0,isDir:!0,created:l.created});for(let l of c)t.push({id:l.quickkey,name:l.filename,size:parseInt(l.size,10)||0,isDir:!1,created:l.created});n=s.response?.folder_content?.more_chunks==="yes"||o.response?.folder_content?.more_chunks==="yes",i++}return t}async getDownloadUrl(e){let t=await this.postForm("/file/get_links.php",{quick_key:e,link_type:"direct_download"});this.checkResult(t.response?.result);let i=t.response?.links?.[0]?.direct_download||t.response?.links?.[0]?.normal_download;if(!i)throw new Error("[MediaFire] no download link found");return i}async mkdir(e,t){let i=await this.postForm("/folder/create.php",{parent_key:e,foldername:t});return this.checkResult(i.response?.result),i.response?.folder_key||""}async rename(e,t,i){if(i){let n=await this.postForm("/folder/update.php",{folder_key:e,foldername:t});this.checkResult(n.response?.result)}else{let n=await this.postForm("/file/update.php",{quick_key:e,filename:t});this.checkResult(n.response?.result)}}async move(e,t,i){if(i){let n=await this.postForm("/folder/move.php",{folder_key_src:e,folder_key_dst:t});this.checkResult(n.response?.result)}else{let n=await this.postForm("/file/move.php",{quick_key:e,folder_key:t});this.checkResult(n.response?.result)}}async copy(e,t,i){if(i){let n=await this.postForm("/folder/copy.php",{folder_key_src:e,folder_key_dst:t});this.checkResult(n.response?.result)}else{let n=await this.postForm("/file/copy.php",{quick_key:e,folder_key:t});this.checkResult(n.response?.result)}}async remove(e,t){if(t){let i=await this.postForm("/folder/delete.php",{folder_key:e});this.checkResult(i.response?.result)}else{let i=await this.postForm("/file/delete.php",{quick_key:e});this.checkResult(i.response?.result)}}}});var Kg,m_=J(()=>{"use strict";Be();p_();Kg=class{client;pathCache=new Map;constructor(e){this.client=new Hg(e)}async init(){await this.client.init()}async list(e,t){let i=await this.resolveFolderId(t);return(await this.client.list(i)).map(s=>({name:s.name,size:s.size,is_dir:s.isDir,modified:s.created?new Date(s.created).toISOString():new Date().toISOString(),sign:"",type:X(s.name,s.isDir),raw_url:""}))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveFolderId(s),c=(await this.client.list(o)).find(l=>l.name===n);if(c){let l={name:c.name,size:c.size,is_dir:c.isDir,modified:c.created?new Date(c.created).toISOString():new Date().toISOString(),sign:"",type:X(c.name,c.isDir),raw_url:""};if(!c.isDir)try{l.raw_url=await this.client.getDownloadUrl(c.id)}catch(d){l.raw_url_error=d.message}return l}}catch{}try{return await this.resolveFolderId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.resolveFolderId(s);await this.client.mkdir(o,n)}async rename(e,t,i){let{id:n,isDir:s}=await this.resolveEntry(t);await this.client.rename(n,i,s)}async remove(e,t,i){let{id:n,isDir:s}=await this.resolveEntry(t);await this.client.remove(n,s)}async move(e,t,i,n,s){let{id:o,isDir:a}=await this.resolveEntry(n),c=await this.resolveFolderId(t);await this.client.move(o,c,a)}async copy(e,t,i,n,s){let{id:o,isDir:a}=await this.resolveEntry(n),c=await this.resolveFolderId(t);await this.client.copy(o,c,a)}async put(e,t,i){throw new Error("[MediaFire] Direct put not supported in stateless environment")}async resolveFolderId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n="";for(let s=0;sl.isDir&&l.name===i[s]);if(!a)throw new Error(`[MediaFire] Folder '${i[s]}' not found`);n=a.id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.resolveFolderId(n),a=(await this.client.list(s)).find(c=>c.name===i);if(!a)throw new Error(`[MediaFire] '${i}' not found`);return{id:a.id,isDir:a.isDir}}}});function ea(...r){return"/"+r.filter(Boolean).map(t=>t.replace(/^\/+|\/+$/g,"")).filter(Boolean).join("/")}var Oz,Gg,y_=J(()=>{"use strict";Oz="https://api.github.com",Gg=class{addition;mounts;constructor(e){this.addition=e,this.mounts=this.parseMounts(e.repo_structure||"")}parseMounts(e){let t=[];for(let i of e.split(";")){let n=i.trim();if(!n)continue;let s=n.indexOf(":");s>0&&n.slice(0,s).includes("/");let o="/",a=n;s>0&&!n.slice(s+1).startsWith("/")&&!n.slice(0,s).includes(".")&&(o="/"+n.slice(0,s).replace(/^\/+|\/+$/g,""),a=n.slice(s+1)),t.push({point:o==="/"?"/":o,repo:a})}return t}async init(){if(this.mounts.length===0)throw new Error("[GitHub Releases] repo_structure is required (format: [path:]org/repo)")}async get(e){let t={Accept:"application/vnd.github+json","User-Agent":"OpenList-TSWorker"};this.addition.token&&(t.Authorization=`Bearer ${this.addition.token}`);let i=await fetch(`${Oz}${e}`,{headers:t});if(i.status===404)return null;if(i.status>=400)throw new Error(`[GitHub Releases] HTTP ${i.status}`);return await i.json().catch(()=>null)}proxy(e){let t=this.addition.gh_proxy||"";return!t||e.startsWith("http")?e:t+e}mountForPath(e){let t=e.startsWith("/")?e:"/"+e,i=null;for(let n of this.mounts){let s=n.point==="/"?"/":n.point;(t===s||t.startsWith(s+"/"))&&(!i||s.length>(i.point==="/"?0:i.point.length))&&(i=n)}return i}async latestRelease(e){return this.get(`/repos/${e}/releases/latest`)}async allReleases(e){return await this.get(`/repos/${e}/releases`)||[]}async repoFiles(e){return await this.get(`/repos/${e}/contents`)||[]}async list(e){let t=this.mountForPath(e);if(!t)return[];let i=this.relPath(t,e),n=[];if(i===""&&e==="/"){for(let c of this.mounts)n.push({path:c.point,name:c.point==="/"?c.repo:c.point.split("/").filter(Boolean).pop()||c.repo,size:0,isDir:!0,modified:new Date().toISOString(),url:""});return n}let s=this.addition.show_all_version;if(i===""){let c=await this.latestRelease(t.repo);if(c){for(let l of c.assets)n.push({path:ea(t.point,l.name),name:l.name,size:l.size,isDir:!1,modified:l.updated_at||l.created_at||new Date().toISOString(),url:this.proxy(l.browser_download_url)});this.addition.show_source_code&&(n.push({path:ea(t.point,"Source code (zip)"),name:"Source code (zip)",size:1,isDir:!1,modified:c.created_at,url:this.proxy(c.zipball_url)}),n.push({path:ea(t.point,"Source code (tar.gz)"),name:"Source code (tar.gz)",size:1,isDir:!1,modified:c.created_at,url:this.proxy(c.tarball_url)}))}if(s){let l=await this.allReleases(t.repo);for(let d of l)n.push({path:ea(t.point,d.tag_name),name:d.tag_name,size:0,isDir:!0,modified:d.published_at||d.created_at,url:d.html_url})}if(this.addition.show_readme!==!1){let l=await this.repoFiles(t.repo);for(let d of l)(d.name.toLowerCase().endsWith(".md")||d.name.toUpperCase().startsWith("LICENSE"))&&n.push({path:ea(t.point,d.name),name:d.name,size:d.size,isDir:!1,modified:"1970-01-01T00:00:00Z",url:this.proxy(d.download_url)})}return n}let a=(await this.allReleases(t.repo)).find(c=>c.tag_name===i.split("/")[0]);if(a){for(let c of a.assets)n.push({path:ea(t.point,a.tag_name,c.name),name:c.name,size:c.size,isDir:!1,modified:c.updated_at||c.created_at||new Date().toISOString(),url:this.proxy(c.browser_download_url)});this.addition.show_source_code&&(n.push({path:ea(t.point,"Source code (zip)"),name:"Source code (zip)",size:1,isDir:!1,modified:a.created_at,url:this.proxy(a.zipball_url)}),n.push({path:ea(t.point,"Source code (tar.gz)"),name:"Source code (tar.gz)",size:1,isDir:!1,modified:a.created_at,url:this.proxy(a.tarball_url)}))}return n}async getDownloadUrl(e){let t=this.mountForPath(e);if(!t)return"";let n=this.relPath(t,e).split("/").pop()||"",s=await this.latestRelease(t.repo);if(s){let l=s.assets.find(d=>d.name===n);if(l)return this.proxy(l.browser_download_url)}let o=await this.allReleases(t.repo);for(let l of o){let d=l.assets.find(u=>u.name===n);if(d)return this.proxy(d.browser_download_url)}let c=(await this.repoFiles(t.repo)).find(l=>l.name===n);return c?this.proxy(c.download_url):""}relPath(e,t){let i=e.point==="/"?"/":e.point,n=t.startsWith("/")?t:"/"+t;return i==="/"?n.replace(/^\/+/,""):n.slice(i.length).replace(/^\/+/,"")}}});var zg,I_=J(()=>{"use strict";Be();y_();zg=class{client;constructor(e){this.client=new Gg(e)}async init(){await this.client.init()}async list(e,t){return(await this.client.list(t)).map(n=>({name:n.name,size:n.size,is_dir:n.isDir,modified:n.modified||new Date().toISOString(),sign:"",type:X(n.name,n.isDir),raw_url:n.url||""}))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s=await this.client.list(t);if(s.length>0||t==="/")return!s.find(c=>c.isDir&&c.name===n)&&s.some(c=>!c.isDir),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""};let o=await this.client.getDownloadUrl(t);return o?{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:X(n,!1),raw_url:o}:{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:X(n,!1),raw_url:""}}async mkdir(){throw new Error("[GitHub Releases] read-only storage")}async rename(){throw new Error("[GitHub Releases] read-only storage")}async remove(){throw new Error("[GitHub Releases] read-only storage")}async move(){throw new Error("[GitHub Releases] read-only storage")}async copy(){throw new Error("[GitHub Releases] read-only storage")}async put(){throw new Error("[GitHub Releases] read-only storage")}}});var Mz,Lz,Jg,w_=J(()=>{"use strict";Mz="https://api.cnb.cool",Lz="https://cnb.cool",Jg=class{repo;token;useTagName;constructor(e){this.repo=e.repo||"",this.token=e.token||"",this.useTagName=!!e.use_tag_name}async init(){if(!this.repo)throw new Error("[CNB Releases] repo is required");if(!this.token)throw new Error("[CNB Releases] token is required")}async request(e,t,i,n=!1){let s={Accept:"application/json",Authorization:`Bearer ${this.token}`},o={method:e,headers:s};i!==void 0&&(n?(o.body=new URLSearchParams(i).toString(),s["Content-Type"]="application/x-www-form-urlencoded"):(o.body=JSON.stringify(i),s["Content-Type"]="application/json"));let a=await fetch(`${Mz}${t}`,o);if(a.status!==200&&a.status!==201&&a.status!==204)throw new Error(`[CNB Releases] ${a.status} ${await a.text().catch(()=>"")}`);return a.status===204?{}:await a.json().catch(()=>({}))}async listReleases(){return this.request("GET",`/${this.repo}/-/releases`)}async getRelease(e){return this.request("GET",`/${this.repo}/-/releases/${e}`)}releaseName(e){return this.useTagName?e.tag_name:e.name}createRelease(e,t){return this.request("POST",`/${this.repo}/-/releases`,{name:e,tag_name:e,target_commitish:t||"main"})}renameRelease(e,t){return this.request("PATCH",`/${this.repo}/-/releases/${e}`,{name:t},!0)}deleteRelease(e){return this.request("DELETE",`/${this.repo}/-/releases/${e}`)}deleteAsset(e,t){return this.request("DELETE",`/${this.repo}/-/releases/${e}/assets/${t}`)}assetDownloadUrl(e){return`${Lz}${e.path}`}}});var jg,x_=J(()=>{"use strict";Be();w_();jg=class{client;addition;constructor(e){this.addition=e,this.client=new Jg(e)}async init(){await this.client.init()}async list(e,t){let i=t.split("/").filter(Boolean).join("/");if(!i)return(await this.client.listReleases()).map(a=>({name:this.client.releaseName(a),size:a.assets.reduce((c,l)=>c+l.size,0),is_dir:!0,modified:a.updated_at||a.created_at||new Date().toISOString(),sign:a.id,type:1,raw_url:""}));let n=i.split("/")[0];return(await this.client.getRelease(n)).assets.map(o=>({name:o.name,size:o.size,is_dir:!1,modified:o.updated_at||o.created_at||new Date().toISOString(),sign:o.id,type:X(o.name,!1),raw_url:this.client.assetDownloadUrl(o)}))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s=i.join("/");if(i.length===1)try{let o=await this.client.getRelease(i[0]);return{name:this.client.releaseName(o),size:o.assets.reduce((a,c)=>a+c.size,0),is_dir:!0,modified:o.updated_at||o.created_at||new Date().toISOString(),sign:o.id,type:1,raw_url:""}}catch{}if(i.length===2){let a=(await this.client.getRelease(i[0])).assets.find(c=>c.name===i[1]);if(a)return{name:a.name,size:a.size,is_dir:!1,modified:a.updated_at||a.created_at||new Date().toISOString(),sign:a.id,type:X(a.name,!1),raw_url:this.client.assetDownloadUrl(a)}}return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:X(n,!1),raw_url:""}}async mkdir(e,t){let i=t.split("/").filter(Boolean);if(i.length===1){await this.client.createRelease(i[0],this.addition.default_branch||"main");return}throw new Error("[CNB Releases] only supports creating releases at root")}async rename(e,t,i){let n=t.split("/").filter(Boolean);if(n.length===1&&!this.addition.use_tag_name){await this.client.renameRelease(n[0],i);return}throw new Error("[CNB Releases] only release name can be renamed")}async remove(e,t,i){let n=t.split("/").filter(Boolean);if(n.length===1){await this.client.deleteRelease(n[0]);return}if(n.length===2){await this.client.deleteAsset(n[0],n[1]);return}throw new Error("[CNB Releases] invalid path")}async move(){throw new Error("[CNB Releases] move not supported")}async copy(){throw new Error("[CNB Releases] copy not supported")}async put(){throw new Error("[CNB Releases] asset upload not supported in stateless environment")}}});var $g,C_=J(()=>{"use strict";$g=class{address;addition;authorization="";constructor(e){this.addition=e,this.address=(e.address||"").replace(/\/+$/,"")}async init(){if(!this.address)throw new Error("[KodBox] address is required");await this.getToken()}async getToken(){let e=new URLSearchParams({name:this.addition.username||"",password:this.addition.password||""}),i=await(await fetch(`${this.address}/?user/index/loginSubmit`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:e.toString()})).json().catch(()=>({code:!1}));if(i.code===!1)throw new Error(`[KodBox] login failed: ${JSON.stringify(i.data||i)}`);this.authorization=String(i.info||"")}async request(e,t){let i=new URLSearchParams({accessToken:this.authorization,...t}),s=await(await fetch(`${this.address}${e}`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i.toString(),redirect:"follow"})).json().catch(()=>({code:!1}));if(s.code==="10001")return await this.getToken(),this.request(e,t);if(s.code===!1)throw new Error(`[KodBox] ${s.data||"request failed"}`);return s}async list(e){let i=(await this.request("/?explorer/list/path",{path:e})).data;return i?[...i.folderList||[],...i.fileList||[]]:[]}downloadUrl(e){return`${this.address}/?explorer/index/fileOut&path=${encodeURIComponent(e)}&download=1&accessToken=${encodeURIComponent(this.authorization)}`}async mkdir(e){await this.request("/?explorer/index/mkdir",{path:e})}async rename(e,t){await this.request("/?explorer/index/pathRename",{path:e,newName:t})}async move(e,t,i){await this.request("/?explorer/index/pathCuteTo",{dataArr:JSON.stringify([{path:e,name:t}]),path:i})}async copy(e,t,i){await this.request("/?explorer/index/pathCopyTo",{dataArr:JSON.stringify([{path:e,name:t}]),path:i})}async remove(e,t){await this.request("/?explorer/index/pathDelete",{dataArr:JSON.stringify([{path:e,name:t}]),shiftDelete:"1"})}async put(e,t,i){let n=new FormData;n.append("accessToken",this.authorization),n.append("path",e),n.append("file",new Blob([i]),t);let o=await(await fetch(`${this.address}/?explorer/upload/fileUpload`,{method:"POST",body:n})).json().catch(()=>({code:!1}));if(o.code===!1)throw new Error(`[KodBox] ${o.data||"upload failed"}`)}}});var Vg,E_=J(()=>{"use strict";Be();C_();Vg=class{client;addition;constructor(e){this.addition=e,this.client=new $g(e)}async init(){await this.client.init()}fullPath(e){let t=(this.addition.root_path||"").replace(/^\/+|\/+$/g,""),i=e.replace(/^\/+/,"");return t?"/"+(i?`${t}/${i}`:t):"/"+i}async list(e,t){return(await this.client.list(this.fullPath(t))).map(n=>({name:n.name,size:n.size||0,is_dir:n.type==="folder",modified:n.modifyTime?new Date(n.modifyTime*1e3).toISOString():new Date().toISOString(),sign:"",type:X(n.name,n.type==="folder"),raw_url:""}))}async get(e,t){let i=this.fullPath(t),n=i.split("/").filter(Boolean),s=n[n.length-1]||"root",o="/"+n.slice(0,-1).join("/");try{let c=(await this.client.list(o==="//"?"/":o)).find(l=>l.name===s);if(c){let l={name:c.name,size:c.size||0,is_dir:c.type==="folder",modified:c.modifyTime?new Date(c.modifyTime*1e3).toISOString():new Date().toISOString(),sign:"",type:X(c.name,c.type==="folder"),raw_url:""};return l.is_dir||(l.raw_url=this.client.downloadUrl(c.path||i)),l}}catch{}try{return await this.client.list(i),{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:s,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){await this.client.mkdir(this.fullPath(t))}async rename(e,t,i){await this.client.rename(this.fullPath(t),i)}async remove(e,t,i){let s=this.fullPath(t).split("/").filter(Boolean),o=s[s.length-1],a=i.length?i:[o];for(let c of a){let l=s.length>1?`/${s.slice(0,-1).join("/")}/${c}`:`/${c}`;await this.client.remove(l,c)}}async move(e,t,i,n,s){let a=this.fullPath(n).split("/").filter(Boolean),c=a[a.length-1],l=this.fullPath(t),d=i.length?i:[c];for(let u of d){let f=a.length>1?`/${a.slice(0,-1).join("/")}/${u}`:`/${u}`;await this.client.move(f,u,l)}}async copy(e,t,i,n,s){let a=this.fullPath(n).split("/").filter(Boolean),c=a[a.length-1],l=this.fullPath(t),d=i.length?i:[c];for(let u of d){let f=a.length>1?`/${a.slice(0,-1).join("/")}/${u}`:`/${u}`;await this.client.copy(f,u,l)}}async put(e,t,i){let s=this.fullPath(t).split("/").filter(Boolean),o=s[s.length-1]||"file",a="/"+s.slice(0,-1).join("/");await this.client.put(a==="//"?"/":a,o,i)}}});var Yg,B_=J(()=>{"use strict";Yg=class{endpoint;gateway;mode;constructor(e){this.endpoint=(e.endpoint||"").replace(/\/+$/,""),this.gateway=(e.gateway||"").replace(/\/+$/,""),this.mode=e.mode||"ipfs"}async init(){if(!this.endpoint)throw new Error("[IPFS] endpoint is required");if(!this.gateway)throw new Error("[IPFS] gateway is required")}async post(e,t){let i=new URLSearchParams;for(let s of t)i.append("arg",s);let n=await fetch(`${this.endpoint}/api/v0/${e}?${i.toString()}`,{method:"POST"});if(n.status>=400)throw new Error(`[IPFS] ${n.status} ${await n.text().catch(()=>"")}`);return await n.json().catch(()=>({}))}async ls(e){return this.post("ls",[`/ipfs/${e}`])}async filesStat(e){return this.post("files/stat",[e])}async filesMkdir(e){await this.post("files/mkdir",[e,"parents=true"])}async filesMv(e,t){await this.post("files/mv",[e,t])}async filesCp(e,t){await this.post("files/cp",[e,t])}async filesRm(e){await this.post("files/rm",[e,"recursive=true"])}async add(e,t){let i=new FormData;i.append("file",new Blob([e]),t);let s=await(await fetch(`${this.endpoint}/api/v0/add`,{method:"POST",body:i})).json().catch(()=>({Hash:""}));if(!s.Hash)throw new Error("[IPFS] add failed");return s.Hash}downloadUrl(e,t){return`${this.gateway}/ipfs/${e}?filename=${encodeURIComponent(t)}`}async resolveIpfsPath(e,t){if(t)return`/ipfs/${t}`;let i=e.replace(/^\/+|\/+$/g,"");switch(this.mode){case"ipfs":return`/ipfs/${i}`;case"ipns":return`/ipns/${i}`;case"mfs":return`/ipfs/${(await this.filesStat(i?`/${i}`:"/")).Hash}`;default:throw new Error("[IPFS] invalid mode")}}}});var Wg,b_=J(()=>{"use strict";Be();B_();Wg=class{client;addition;constructor(e){this.addition=e,this.client=new Yg(e)}async init(){await this.client.init()}isMfs(){return this.addition.mode==="mfs"}fullPath(e){let t=(this.addition.root_path||"").replace(/^\/+|\/+$/g,""),i=e.replace(/^\/+/,"");return t?"/"+(i?`${t}/${i}`:t):"/"+i}async list(e,t){let i=this.fullPath(t),s=(await this.client.resolveIpfsPath(i)).replace(/^\/ipfs\//,"");return((await this.client.ls(s)).Objects?.[0]?.Links||[]).map(c=>({name:c.Name,size:c.Size||0,is_dir:c.Type===1,modified:new Date().toISOString(),sign:c.Hash,type:X(c.Name,c.Type===1),raw_url:""}))}async get(e,t){let i=this.fullPath(t),n=i.split("/").filter(Boolean),s=n[n.length-1]||"root";if(this.isMfs())try{let a=await this.client.filesStat(i),c=a.Type==="directory",l={name:s,size:a.Size||0,is_dir:c,modified:new Date().toISOString(),sign:a.Hash,type:X(s,c),raw_url:""};return c||(l.raw_url=this.client.downloadUrl(a.Hash,s)),l}catch{}let o="/"+n.slice(0,-1).join("/");try{let c=(await this.list(e,o)).find(l=>l.name===s);if(c)return c.is_dir||(c.raw_url=this.client.downloadUrl(c.sign,s)),c}catch{}try{return await this.client.resolveIpfsPath(i),{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:s,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");await this.client.filesMkdir(this.fullPath(t))}async rename(e,t,i){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");let n=this.fullPath(t),o="/"+n.split("/").filter(Boolean).slice(0,-1).join("/")+"/"+i;await this.client.filesMv(n,o)}async remove(e,t,i){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");await this.client.filesRm(this.fullPath(t))}async move(e,t,i,n,s){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");let o=this.fullPath(n),a=this.fullPath(t);await this.client.filesMv(o,a)}async copy(e,t,i,n,s){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");let o=this.fullPath(n),c=`/ipfs/${(await this.client.filesStat(o)).Hash}`,l=this.fullPath(t);await this.client.filesCp(c,l)}async put(e,t,i){if(!this.isMfs())throw new Error("[IPFS] only write in mfs mode");let n=this.fullPath(t),s=n.split("/").filter(Boolean),o=s[s.length-1]||"file",a=await this.client.add(i,o);await this.client.filesCp(`/ipfs/${a}`,n)}}});var Hz,Xg,v_=J(()=>{"use strict";Hz="https://siot-share.lenovo.com.cn",Xg=class{host;shareId;sharePwd;stoken="";expireAt=0;constructor(e){this.host=(e.host||Hz).replace(/\/+$/,""),this.shareId=(e.share_id||"").split("/").pop()||"",this.sharePwd=e.share_pwd||""}async init(){if(!this.shareId)throw new Error("[Lenovo NAS Share] share_id is required");await this.getStoken()}async request(e){let i=await(await fetch(e,{method:"GET",headers:{origin:"https://siot-share.lenovo.com.cn",referer:"https://siot-share.lenovo.com.cn/","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",platform:"web","app-version":"3"}})).json().catch(()=>({result:!1}));if(!i.result)throw new Error(`[Lenovo NAS Share] ${i.error?.msg||"request failed"}`);return i.data}async getStoken(){let e=new URLSearchParams({code:this.shareId,password:this.sharePwd}),t=await this.request(`${this.host}/oneproxy/api/share/v1/access?${e.toString()}`);this.stoken=t.stoken||"",this.expireAt=Date.now()+(t.expires_in||3600)*1e3-6e4}async checkStoken(){Date.now()>=this.expireAt&&await this.getStoken()}async list(e){await this.checkStoken();let t=e.startsWith("/")?e:"/"+e,i=new URLSearchParams({code:this.shareId,num:"5000",stoken:this.stoken,path:t});return(await this.request(`${this.host}/oneproxy/api/share/v1/files?${i.toString()}`)).list||[]}async getDownloadUrl(e){await this.checkStoken();let t=new URLSearchParams({code:this.shareId,stoken:this.stoken,path:e.startsWith("/")?e:"/"+e}),n=(await this.request(`${this.host}/oneproxy/api/share/v1/file/link?${t.toString()}`)).param?.dtoken||"";return`${this.host}/oneproxy/api/share/v1/file/download?code=${encodeURIComponent(this.shareId)}&dtoken=${encodeURIComponent(n)}`}downloadHeaders(){return{Referer:"https://siot-share.lenovo.com.cn"}}}});var Zg,k_=J(()=>{"use strict";Be();v_();Zg=class{client;constructor(e){this.client=new Xg(e)}async init(){await this.client.init()}async list(e,t){return(await this.client.list(t)).map(n=>{let s=n.type==="dir";return{name:n.name,size:n.size||0,is_dir:s,modified:n.chtime?new Date(n.chtime*1e3).toISOString():new Date().toISOString(),sign:"",type:X(n.name,s),raw_url:""}})}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let a=(await this.client.list(s==="//"?"/":s)).find(c=>c.name===n||c.path===t);if(a){let c=a.type==="dir",l={name:a.name,size:a.size||0,is_dir:c,modified:a.chtime?new Date(a.chtime*1e3).toISOString():new Date().toISOString(),sign:"",type:X(a.name,c),raw_url:""};return c||(l.raw_url=await this.client.getDownloadUrl(a.path),l.raw_url_headers=this.client.downloadHeaders()),l}}catch{}try{return await this.client.list(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(){throw new Error("[Lenovo NAS Share] read-only storage")}async rename(){throw new Error("[Lenovo NAS Share] read-only storage")}async remove(){throw new Error("[Lenovo NAS Share] read-only storage")}async move(){throw new Error("[Lenovo NAS Share] read-only storage")}async copy(){throw new Error("[Lenovo NAS Share] read-only storage")}async put(){throw new Error("[Lenovo NAS Share] read-only storage")}}});var ep,S_=J(()=>{"use strict";ep=class{endpoint;token;constructor(e){this.endpoint=(e.endpoint||"").replace(/\/+$/,""),this.token=e.access_token||""}async init(){if(!this.endpoint)throw new Error("[Misskey] endpoint is required");if(!this.token)throw new Error("[Misskey] access_token is required")}async post(e,t){let i=await fetch(`${this.endpoint}/api/drive${e}`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.token}`},body:JSON.stringify(t||{})});if(i.status>=400)throw new Error(`[Misskey] ${i.status} ${await i.text().catch(()=>"")}`);return await i.json().catch(()=>({}))}async getFiles(e){return this.post("/files",e?{folderId:e}:{})}async getFolders(e){return this.post("/folders",e?{folderId:e}:{})}async showFile(e){return this.post("/files/show",{fileId:e})}async createFolder(e,t){return this.post("/folders/create",{name:e,...t?{parentId:t}:{}})}async updateFolder(e,t){return this.post("/folders/update",{folderId:e,...t})}async updateFile(e,t){return this.post("/files/update",{fileId:e,...t})}async deleteFolder(e){await this.post("/folders/delete",{folderId:e})}async deleteFile(e){await this.post("/files/delete",{fileId:e})}async uploadFromUrl(e,t){return this.post("/files/upload-from-url",{url:e,...t?{folderId:t}:{}})}}});var tp,D_=J(()=>{"use strict";Be();S_();tp=class{client;pathCache=new Map;constructor(e){this.client=new ep(e)}async init(){await this.client.init()}async list(e,t){let i=await this.resolveFolderId(t),[n,s]=await Promise.all([this.client.getFiles(i),this.client.getFolders(i)]),o=n.map(c=>({name:c.name,size:c.size||0,is_dir:!1,modified:c.createdAt||new Date().toISOString(),sign:c.id,type:X(c.name,!1),raw_url:c.url||"",thumb:c.thumbnailUrl||""}));return[...s.map(c=>({name:c.name,size:0,is_dir:!0,modified:c.createdAt||new Date().toISOString(),sign:c.id,type:1,raw_url:""})),...o]}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveFolderId(s),[a,c]=await Promise.all([this.client.getFiles(o),this.client.getFolders(o)]),l=a.find(u=>u.name===n);if(l)return{name:l.name,size:l.size||0,is_dir:!1,modified:l.createdAt||new Date().toISOString(),sign:l.id,type:X(l.name,!1),raw_url:l.url||"",thumb:l.thumbnailUrl||""};let d=c.find(u=>u.name===n);if(d)return{name:d.name,size:0,is_dir:!0,modified:d.createdAt||new Date().toISOString(),sign:d.id,type:1,raw_url:""}}catch{}try{return await this.resolveFolderId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.resolveFolderId(s);await this.client.createFolder(n,o),this.pathCache.clear()}async rename(e,t,i){let{id:n,isDir:s}=await this.resolveEntry(t);s?await this.client.updateFolder(n,{name:i}):await this.client.updateFile(n,{name:i}),this.pathCache.clear()}async remove(e,t,i){let{id:n,isDir:s}=await this.resolveEntry(t);s?await this.client.deleteFolder(n):await this.client.deleteFile(n),this.pathCache.clear()}async move(e,t,i,n,s){let{id:o,isDir:a}=await this.resolveEntry(n),c=await this.resolveFolderId(t);a?await this.client.updateFolder(o,{parentId:c}):await this.client.updateFile(o,{folderId:c}),this.pathCache.clear()}async copy(e,t,i,n,s){let{id:o,isDir:a}=await this.resolveEntry(n),c=await this.resolveFolderId(t);if(a)throw new Error("[Misskey] folder copy not supported");let l=await this.client.showFile(o);await this.client.uploadFromUrl(l.url,c),this.pathCache.clear()}async put(e,t,i){throw new Error("[Misskey] Direct put not supported in stateless environment")}async resolveFolderId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return;if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n;for(let s=0;sl.name===i[s]);if(!a)throw new Error(`[Misskey] Folder '${i[s]}' not found`);n=a.id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.resolveFolderId(n),[o,a]=await Promise.all([this.client.getFiles(s),this.client.getFolders(s)]),c=o.find(d=>d.name===i);if(c)return{id:c.id,isDir:!1};let l=a.find(d=>d.name===i);if(l)return{id:l.id,isDir:!0};throw new Error(`[Misskey] '${i}' not found`)}}});var VC,Q_,Kz,Gz,zz,Jz,rp,__=J(()=>{"use strict";VC="https://www.doubao.com",Q_="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",Kz=1,Gz=6,zz=7,Jz={1:"directory",2:"file",3:"link",4:"image",5:"pages",6:"video",7:"audio",8:"meeting_minutes"},rp=class{cookie;downloadApi;constructor(e){this.cookie=e.cookie||"",this.downloadApi=e.download_api||"get_file_url"}async init(){if(!this.cookie)throw new Error("[Doubao] cookie is required")}async request(e,t={}){let n=await(await fetch(`${VC}${e}`,{method:"POST",headers:{"Content-Type":"application/json",Cookie:this.cookie,"User-Agent":Q_,Origin:VC,Referer:`${VC}/`},body:JSON.stringify(t)})).json().catch(()=>({code:-1}));if(n.code!==0){let s=n.message||n.msg||n.error?.message||`code ${n.code}`;throw new Error(`[Doubao] ${s}`)}return n}async getFiles(e){let t=[],i="";do{let n={node_id:e};i?(n.cursor=i,n.size=50):n.need_full_path=!1;let o=(await this.request("/samantha/aispace/node_info",n)).data;o?.children&&t.push(...o.children),i=o?.next_cursor&&o.next_cursor!=="-1"?o.next_cursor:""}while(i);return t}async getDownloadUrl(e){if(this.downloadApi==="get_download_info"){let a=(await this.request("/samantha/aispace/get_download_info",{requests:[{node_id:e.id}]})).data.download_infos?.[0]?.main_url||"";if(!a)throw new Error("[Doubao] empty download url");return a}if(e.node_type===Gz||e.node_type===zz){let o=(await this.request("/samantha/media/get_play_info",{key:e.key,node_id:e.id})).data,a=o.original_media_info?.main_url||o.media_info?.[0]?.main_url||"";if(!a)throw new Error("[Doubao] empty download url");return a}let n=(await this.request("/alice/message/get_file_url",{uris:[e.key],type:Jz[e.node_type]||"file"})).data.file_urls?.[0]?.main_url||"";if(!n)throw new Error("[Doubao] empty download url");return n}downloadHeaders(){return{"User-Agent":Q_}}async mkdir(e,t){await this.request("/samantha/aispace/upload_node",{node_list:[{local_id:crypto.randomUUID(),name:t,parent_id:e,node_type:Kz}]})}async rename(e,t){await this.request("/samantha/aispace/rename_node",{node_id:e,node_name:t})}async move(e,t,i){await this.request("/samantha/aispace/move_node",{node_list:[{id:e}],current_parent_id:t,target_parent_id:i})}async remove(e){await this.request("/samantha/aispace/delete_node",{node_list:[{id:e}]})}}});var F_,ip,P_=J(()=>{"use strict";Be();__();F_="0",ip=class{client;pathCache=new Map;constructor(e){this.client=new rp(e)}async init(){await this.client.init()}fileToItem(e){let t=e.node_type===1;return{name:e.name,size:e.size||0,is_dir:t,modified:e.update_time?new Date(e.update_time*1e3).toISOString():new Date().toISOString(),sign:"",type:X(e.name,t),raw_url:""}}async list(e,t){let i=await this.resolveDirId(t);return(await this.client.getFiles(i)).map(s=>this.fileToItem(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveDirId(s),c=(await this.client.getFiles(o)).find(l=>l.name===n);if(c){let l=this.fileToItem(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c),l.raw_url_headers=this.client.downloadHeaders()}catch(d){l.raw_url_error=d.message}return l}}catch{}try{return await this.resolveDirId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.resolveDirId(s);await this.client.mkdir(o,n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveDirId(t);await this.client.rename(n,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveDirId(t);await this.client.remove(n),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n),a=await this.resolveDirId(t);await this.client.move(o.id,o.parentId,a),this.pathCache.clear()}async copy(){throw new Error("[Doubao] copy not supported")}async put(){throw new Error("[Doubao] upload not supported in stateless environment")}async resolveDirId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return F_;if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=F_;for(let s=0;sl.node_type===1&&l.name===i[s]);if(!a)throw new Error(`[Doubao] Directory '${i[s]}' not found`);n=a.id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.resolveDirId(n),a=(await this.client.getFiles(s)).find(c=>c.name===i);if(!a)throw new Error(`[Doubao] '${i}' not found`);return{id:a.id,parentId:a.parent_id||s,isDir:a.node_type===1}}}});async function Vz(r){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(r));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}var jz,$z,np,T_=J(()=>{"use strict";jz="https://open-api-drive.quark.cn",$z="go-resty/3.0.0-beta.1 (https://resty.dev)";np=class{addition;accessToken;refreshToken;appId;signKey;constructor(e){this.addition=e,this.accessToken=e.access_token||"",this.refreshToken=e.refresh_token||"",this.appId=e.app_id||"",this.signKey=e.sign_key||""}async init(){if(!this.refreshToken)throw new Error("[QuarkOpen] refresh_token is required")}async generateSign(e,t){let i=String(Date.now()),n=`${e}&${t}&${i}&${this.signKey}`,s=await Vz(n),o=typeof crypto.randomUUID=="function"?crypto.randomUUID():`${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;return{tm:i,token:s,reqId:o}}async request(e,t,i){let{tm:n,token:s,reqId:o}=await this.generateSign(t,e),a=new URLSearchParams({req_id:o,access_token:this.accessToken}),c={Accept:"application/json, text/plain, */*","User-Agent":$z,"x-pan-tm":n,"x-pan-token":s,"x-pan-client-id":this.appId},l={method:t,headers:c};i!==void 0&&(l.body=JSON.stringify(i),c["Content-Type"]="application/json");let u=await(await fetch(`${jz}${e}?${a.toString()}`,l)).json().catch(()=>({status:-1,errno:-1}));if(u.status===-1&&(u.errno===11001||u.errno===14001&&(u.error_info||"").includes("access_token")))return await this.refreshAccessToken(),this.request(e,t,i);if(u.status>=400||u.errno!==0)throw new Error(`[QuarkOpen] ${u.error_info||`errno ${u.errno}`}`);return u}async refreshAccessToken(){let e=this.addition.api_url_address||"https://api.oplist.org/quarkyun/renewapi",t=new URLSearchParams({refresh_ui:this.refreshToken,server_use:"true",driver_txt:"quarkyun_oa"}),n=await(await fetch(`${e}?${t.toString()}`)).json().catch(()=>({}));if(!n.refresh_token||!n.access_token)throw new Error(`[QuarkOpen] refresh token failed: ${n.text||"empty token"}`);this.refreshToken=n.refresh_token,this.accessToken=n.access_token,n.app_id&&(this.appId=n.app_id),n.sign_key&&(this.signKey=n.sign_key),this.addition.refresh_token=n.refresh_token,this.addition.access_token=n.access_token}async getFiles(e){let t=[],i;do{let n={parent_fid:e,size:100,sort:this.addition.order_by&&this.addition.order_by!=="none"?`${this.addition.order_by}:${this.addition.order_direction||"asc"}`:"file_name:asc"};i?.token&&(n.query_cursor=i);let o=(await this.request("/open/v1/file/list","POST",n)).data;if(o?.file_list&&t.push(...o.file_list),o?.last_page)break;i=o?.next_query_cursor}while(i?.token);return t}async getDownloadUrl(e){let i=(await this.request("/open/v1/file/get_download_url","POST",{fid:e})).data;if(!i.download_url)throw new Error("[QuarkOpen] empty download url");return i.download_url}downloadCookie(){return`x_pan_client_id=${this.appId}; x_pan_access_token=${this.accessToken}`}async mkdir(e,t){await this.request("/open/v1/dir","POST",{dir_path:t,pdir_fid:e})}async rename(e,t){await this.request("/open/v1/file/rename","POST",{fid:e,file_name:t,conflict_mode:"REUSE"})}async move(e,t){await this.request("/open/v1/file/move","POST",{action_type:1,fid_list:[e],to_pdir_fid:t})}async remove(e){await this.request("/open/v1/file/delete","POST",{action_type:1,fid_list:[e]})}}});var U_,sp,R_=J(()=>{"use strict";Be();T_();U_="0",sp=class{client;pathCache=new Map;constructor(e){this.client=new np(e)}async init(){await this.client.init()}fileToItem(e){let t=e.file_type==="0";return{name:e.filename,size:e.size||0,is_dir:t,modified:e.updated_at?new Date(e.updated_at).toISOString():new Date().toISOString(),sign:"",type:X(e.filename,t),thumb:e.thumbnail_url||"",raw_url:""}}async list(e,t){let i=await this.resolveDirId(t);return(await this.client.getFiles(i)).map(s=>this.fileToItem(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveDirId(s),c=(await this.client.getFiles(o)).find(l=>l.filename===n);if(c){let l=this.fileToItem(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c.fid),l.raw_url_headers={Cookie:this.client.downloadCookie()}}catch(d){l.raw_url_error=d.message}return l}}catch{}try{return await this.resolveDirId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.resolveDirId(s);await this.client.mkdir(o,n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveDirId(t);await this.client.rename(n,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveDirId(t);await this.client.remove(n),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveDirId(n),a=await this.resolveDirId(t);await this.client.move(o,a),this.pathCache.clear()}async copy(){throw new Error("[QuarkOpen] copy not supported")}async put(){throw new Error("[QuarkOpen] upload not supported in stateless environment")}async resolveDirId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return U_;if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=U_;for(let s=0;sl.file_type==="0"&&l.filename===i[s]);if(!a)throw new Error(`[QuarkOpen] Directory '${i[s]}' not found`);n=a.fid;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});var Yz,Wz,Xz,Zz,eJ,tJ,rJ,iJ,nJ,sJ,op,N_=J(()=>{"use strict";Gr();Yz="Mozilla/5.0 (Linux; U; Android 13; zh-cn; M2004J7AC Build/UKQ1.231108.001) AppleWebKit/533.1 (KHTML, like Gecko) Mobile Safari/533.1",Wz="Xiaomi",Xz="tv",Zz="M2004J7AC",eJ="M2004J7AC",tJ="M2004J7AC",rJ="M2004J7AC",iJ="Adreno (TM) 550",nJ="{}",sJ={quark:{api:"https://open-api-drive.quark.cn",clientID:"d3194e61504e493eb6222857bccfed94",signKey:"kw2dvtd7p4t3pjl2d9ed9yc8yej8kw2d",appVer:"1.8.2.2",channel:"GENERAL",codeApi:"http://api.extscreen.com/quarkdrive"},uc:{api:"https://open-api-drive.uc.cn",clientID:"5acf882d27b74502b7040b0c65519aa7",signKey:"l3srvtd7p42l0d0x1u8d7yc8ye9kki4d",appVer:"1.7.2.2",channel:"UCTVOFFICIALWEB",codeApi:"http://api.extscreen.com/ucdrive"}},op=class{conf;addition;accessToken="";deviceId;constructor(e){this.addition=e,this.conf=sJ[e.variant||"quark"],this.deviceId=e.device_id||$t(String(Date.now())),this.addition.device_id=this.deviceId}async init(){if(!this.addition.refresh_token)throw new Error("[QuarkTV] refresh_token is required");await this.refreshToken()}async generateSign(e,t){let i=String(Date.now()),n=$t(this.deviceId+i),s=await BD(`${e}&${t}&${i}&${this.conf.signKey}`);return{tm:i,token:s,reqId:n}}deviceQuery(){return{app_ver:this.conf.appVer,device_id:this.deviceId,device_brand:Wz,platform:Xz,device_name:Zz,device_model:eJ,build_device:tJ,build_product:rJ,device_gpu:iJ,activity_rect:nJ,channel:this.conf.channel}}async request(e,t,i={}){let{tm:n,token:s,reqId:o}=await this.generateSign(t,e),a=new URLSearchParams({req_id:o,access_token:this.accessToken,...this.deviceQuery(),...i}),l=await(await fetch(`${this.conf.api}${e}?${a.toString()}`,{method:t,headers:{Accept:"application/json, text/plain, */*","User-Agent":Yz,"x-pan-tm":n,"x-pan-token":s,"x-pan-client-id":this.conf.clientID}})).json().catch(()=>({status:-1,errno:-1})),d=(l.error_info||"").toLowerCase();if(l.status===-1&&(l.errno===10001||l.errno===11001)||d.includes("access token")||d.includes("access_token")||d.includes("token\u65E0\u6548")||d.includes("token \u65E0\u6548"))return await this.refreshToken(),this.request(e,t,i);if(l.status>=400||l.errno!==0)throw new Error(`[QuarkTV] ${l.error_info||`errno ${l.errno}`}`);return l}async refreshToken(){let e="/token",{reqId:t}=await this.generateSign("POST",e),i={req_id:t,...this.deviceQuery()};this.accessToken?i.refresh_token=this.addition.refresh_token:i.refresh_token=this.addition.refresh_token;let s=await(await fetch(`${this.conf.codeApi}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)})).json().catch(()=>({}));if(s?.code!==200)throw new Error(`[QuarkTV] ${s?.message||"token refresh failed"}`);let o=s.data||{};if(!o.access_token)throw new Error("[QuarkTV] refresh token empty");this.accessToken=o.access_token,o.refresh_token&&(this.addition.refresh_token=o.refresh_token)}async getFiles(e){let t=[],i=0,n=100,s=this.addition.order_direction==="asc"?"0":"1",o=this.addition.order_by==="file_name"?"1":"3";for(;;){let c=(await this.request("/file","GET",{method:"list",parent_fid:e,order_by:o,desc:s,category:"",source:"",ex_source:"",list_all:"0",page_size:String(n),page_index:String(i)})).data;c?.files&&t.push(...c.files);let l=c?.total_count||0;if(i*n>=l)break;i++}return t}async getDownloadUrl(e){if(this.addition.link_method==="streaming"){let o=(await this.request("/file","GET",{method:"streaming",group_by:"source",fid:e,resolution:"low,normal,high,super,2k,4k",support:"dolby_vision"})).data?.video_info?.find(a=>a.url)?.url||"";if(!o)throw new Error("[QuarkTV] no streaming link found");return o}let i=(await this.request("/file","GET",{method:"download",group_by:"source",fid:e,resolution:"low,normal,high,super,2k,4k",support:"dolby_vision"})).data;if(!i?.download_url)throw new Error("[QuarkTV] empty download url");return i.download_url}}});var q_,ap,O_=J(()=>{"use strict";Be();N_();q_="0",ap=class{client;pathCache=new Map;constructor(e){this.client=new op(e)}async init(){await this.client.init()}fileToItem(e){let t=e.isdir===1;return{name:e.filename,size:e.size||0,is_dir:t,modified:e.updated_at?new Date(e.updated_at).toISOString():new Date().toISOString(),sign:"",type:X(e.filename,t),thumb:e.thumbnail_url||"",raw_url:""}}async list(e,t){let i=await this.resolveDirId(t);return(await this.client.getFiles(i)).map(s=>this.fileToItem(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveDirId(s),c=(await this.client.getFiles(o)).find(l=>l.filename===n);if(c){let l=this.fileToItem(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c.fid)}catch(d){l.raw_url_error=d.message}return l}}catch{}try{return await this.resolveDirId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(){throw new Error("[QuarkTV] read-only storage")}async rename(){throw new Error("[QuarkTV] read-only storage")}async remove(){throw new Error("[QuarkTV] read-only storage")}async move(){throw new Error("[QuarkTV] read-only storage")}async copy(){throw new Error("[QuarkTV] read-only storage")}async put(){throw new Error("[QuarkTV] read-only storage")}async resolveDirId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return q_;if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=q_;for(let s=0;sl.isdir===1&&l.filename===i[s]);if(!a)throw new Error(`[QuarkTV] Directory '${i[s]}' not found`);n=a.fid;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});var Cc,cp,M_=J(()=>{"use strict";Cc="https://open-api.123pan.com",cp=class{addition;accessToken="";constructor(e){this.addition=e,this.accessToken=e.access_token||""}async init(){this.accessToken||await this.getAccessToken()}async getAccessToken(){if(this.addition.use_online_api!==!1&&this.addition.refresh_token&&this.addition.api_url_address){let e=new URLSearchParams({refresh_ui:this.addition.refresh_token,server_use:"true",driver_txt:"123cloud_oa"}),i=await(await fetch(`${this.addition.api_url_address}?${e.toString()}`)).json().catch(()=>({}));if(i.access_token&&i.refresh_token){this.accessToken=i.access_token,this.addition.access_token=i.access_token,this.addition.refresh_token=i.refresh_token;return}let n=i.error_description||i.text||i.message||i.error;if(n)throw new Error(`[123Open] ${n}`)}if(this.addition.client_id&&this.addition.client_secret){let t=await(await fetch(`${Cc}/api/v1/access_token`,{method:"POST",headers:{platform:"open_platform","Content-Type":"application/json"},body:JSON.stringify({clientID:this.addition.client_id,clientSecret:this.addition.client_secret})})).json().catch(()=>({code:-1}));if(t.code!==0)throw new Error(`[123Open] ${t.message}`);if(!t.data?.access_token)throw new Error("[123Open] empty access token");this.accessToken=t.data.access_token,this.addition.access_token=this.accessToken;return}throw new Error("[123Open] no valid authentication method (access_token / refresh_token / client_id+client_secret)")}async request(e,t,i){let n={Authorization:`Bearer ${this.accessToken}`,platform:"open_platform","Content-Type":"application/json"},s={method:t,headers:n};i!==void 0&&(s.body=JSON.stringify(i));let a=await(await fetch(e,s)).json().catch(()=>({code:-1}));return a.code===401?(await this.getAccessToken(),this.request(e,t,i)):a}async getFiles(e){let t=[],i=0;do{let n=new URLSearchParams({parentFileId:e,limit:"100",lastFileId:String(i),trashed:"false",searchMode:"",searchData:""}),s=await this.request(`${Cc}/api/v2/file/list?${n.toString()}`,"GET");if(s.code!==0)throw new Error(`[123Open] ${s.message}`);let o=s.data,a=(o.file_list||[]).filter(c=>(c.trashed??0)===0);t.push(...a),i=o.last_file_id??-1}while(i!==-1);return t}async getDownloadUrl(e){let t=new URLSearchParams({fileId:String(e)}),i=await this.request(`${Cc}/api/v1/file/download_info?${t.toString()}`,"GET");if(i.code!==0)throw new Error(`[123Open] ${i.message}`);let n=i.data?.download_url||"";if(!n)throw new Error("[123Open] empty download url");return n}async mkdir(e,t){let i=await this.request(`${Cc}/upload/v1/file/mkdir`,"POST",{parentID:e,name:t});if(i.code!==0)throw new Error(`[123Open] ${i.message}`)}async move(e,t){let i=await this.request(`${Cc}/api/v1/file/move`,"POST",{fileIDs:[e],toParentFileID:t});if(i.code!==0)throw new Error(`[123Open] ${i.message}`)}async rename(e,t){let i=await this.request(`${Cc}/api/v1/file/name`,"PUT",{fileId:e,fileName:t});if(i.code!==0)throw new Error(`[123Open] ${i.message}`)}async remove(e){let t=await this.request(`${Cc}/api/v1/file/trash`,"POST",{fileIDs:[e]});if(t.code!==0)throw new Error(`[123Open] ${t.message}`)}}});function oJ(r){if(!r)return new Date().toISOString();let e=new Date(r.replace(" ","T")+"+08:00");return isNaN(e.getTime())?new Date().toISOString():e.toISOString()}var L_,lp,H_=J(()=>{"use strict";Be();M_();L_="0";lp=class{client;pathCache=new Map;constructor(e){this.client=new cp(e)}async init(){await this.client.init()}fileToItem(e){let t=e.type===1;return{name:e.filename,size:e.size||0,is_dir:t,modified:oJ(e.update_at),sign:"",type:X(e.filename,t),raw_url:""}}async list(e,t){let i=await this.resolveDirId(t);return(await this.client.getFiles(i)).map(s=>this.fileToItem(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.resolveDirId(s),c=(await this.client.getFiles(o)).find(l=>l.filename===n);if(c){let l=this.fileToItem(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c.fileId)}catch(d){l.raw_url_error=d.message}return l}}catch{}try{return await this.resolveDirId(t),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=await this.resolveDirId(s);await this.client.mkdir(o,n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.id,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.id),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n),a=await this.resolveDirId(t);await this.client.move(o.id,a),this.pathCache.clear()}async copy(){throw new Error("[123Open] copy not supported")}async put(){throw new Error("[123Open] upload not supported in stateless environment")}async resolveDirId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return L_;if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=L_;for(let s=0;sl.type===1&&l.filename===i[s]);if(!a)throw new Error(`[123Open] Directory '${i[s]}' not found`);n=String(a.fileId);let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.resolveDirId(n),a=(await this.client.getFiles(s)).find(c=>c.filename===i);if(!a)throw new Error(`[123Open] '${i}' not found`);return{id:a.fileId,isDir:a.type===1}}}});var dp,K_=J(()=>{"use strict";dp=class{addition;baseUrl;constructor(e){this.addition=e,this.baseUrl=e.region==="international"?"https://us.teambition.com":"https://www.teambition.com"}async init(){if(!this.addition.cookie)throw new Error("[Teambition] cookie is required");if(!this.addition.project_id)throw new Error("[Teambition] project_id is required");let e=await fetch(`${this.baseUrl}/api/v2/roles`,{headers:{Cookie:this.addition.cookie}});if(e.status>=400)throw new Error(`[Teambition] init failed: HTTP ${e.status}`)}async request(e,t,i,n){let s=n?"?"+new URLSearchParams(n).toString():"",o={Cookie:this.addition.cookie},a={method:t,headers:o};i!==void 0&&(a.body=JSON.stringify(i),o["Content-Type"]="application/json");let c=await fetch(`${this.baseUrl}${e}${s}`,a);if(c.status===204||c.status===201)return{};let l=await c.json().catch(()=>({}));if(l.name||l.message)throw new Error(`[Teambition] ${l.message||l.name}`);return l}async getFiles(e){let t=`${this.addition.order_by||"fileName"}${this.addition.order_direction||"Asc"}`,i={_parentId:e,_projectId:this.addition.project_id,order:t,count:"50"},n=[],s=1;for(;;){let a=await this.request("/api/collections","GET",void 0,{...i,page:String(s)});if(!Array.isArray(a)||a.length===0||(n.push(...a.filter(c=>c.title)),s++,a.length<50))break}let o=[];for(s=1;;){let a=await this.request("/api/works","GET",void 0,{...i,page:String(s)});if(!Array.isArray(a)||a.length===0||(o.push(...a),s++,a.length<50))break}return{folders:n,works:o}}async resolveDownloadUrl(e){let t=await fetch(e,{headers:{Cookie:this.addition.cookie},redirect:"manual"});if(t.status===302){let i=t.headers.get("location");if(i)return i}return e}async mkdir(e,t){await this.request("/api/collections","POST",{objectType:"collection",_projectId:this.addition.project_id,_creatorId:"",created:"",updated:"",title:t,color:"blue",description:"",workCount:0,collectionType:"",recentWorks:[],_parentId:e,subCount:null})}async move(e,t,i){let n=i?"/api/collections/":"/api/works/";await this.request(`${n}${e}/move`,"PUT",{_parentId:t})}async rename(e,t,i){i?await this.request(`/api/collections/${e}`,"PUT",{title:t}):await this.request(`/api/works/${e}`,"PUT",{fileName:t})}async copy(e,t,i){let n=i?"/api/collections/":"/api/works/";await this.request(`${n}${e}/fork`,"PUT",{_parentId:t})}async remove(e,t){let i=t?"/api/collections/":"/api/works/";await this.request(`${i}${e}/archive`,"POST")}}});var up,G_=J(()=>{"use strict";Be();K_();up=class{client;pathCache=new Map;constructor(e){this.client=new dp(e)}async init(){await this.client.init()}async list(e,t){let i=await this.pathToId(t),{folders:n,works:s}=await this.client.getFiles(i),o=n.map(c=>({name:c.title,size:0,is_dir:!0,modified:c.updated||new Date().toISOString(),sign:c._id,type:1,raw_url:""})),a=s.map(c=>({name:c.fileName,size:c.fileSize||0,is_dir:!1,modified:c.updated||new Date().toISOString(),sign:c._id,type:X(c.fileName,!1),thumb:c.thumbnail||c.thumbnailUrl||"",raw_url:c.downloadUrl||""}));return[...o,...a]}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),{folders:a,works:c}=await this.client.getFiles(o),l=a.find(u=>u.title===n);if(l)return{name:l.title,size:0,is_dir:!0,modified:l.updated||new Date().toISOString(),sign:l._id,type:1,raw_url:""};let d=c.find(u=>u.fileName===n);if(d){let u=d.downloadUrl||"";if(u)try{u=await this.client.resolveDownloadUrl(u)}catch{}return{name:d.fileName,size:d.fileSize||0,is_dir:!1,modified:d.updated||new Date().toISOString(),sign:d._id,type:X(d.fileName,!1),thumb:d.thumbnail||d.thumbnailUrl||"",raw_url:u}}}catch{}try{return await this.client.getFiles(await this.pathToId(t)),{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/");await this.client.mkdir(await this.pathToId(s),n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.id,i,n.isDir),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.id,n.isDir),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.move(o.id,await this.pathToId(t),o.isDir),this.pathCache.clear()}async copy(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.copy(o.id,await this.pathToId(t),o.isDir),this.pathCache.clear()}async put(){throw new Error("[Teambition] upload not supported in stateless environment")}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.pathToId(n),{folders:o,works:a}=await this.client.getFiles(s),c=o.find(d=>d.title===i);if(c)return{id:c._id,isDir:!0};let l=a.find(d=>d.fileName===i);if(l)return{id:l._id,isDir:!1};throw new Error(`[Teambition] '${i}' not found`)}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n="";for(let s=0;sl.title===i[s]);if(!a)throw new Error(`[Teambition] Directory '${i[s]}' not found`);n=a._id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});var z_,YC,aJ,J_,j_,fp,$_=J(()=>{"use strict";Gr();z_="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/2.5.20 Chrome/100.0.4896.160 Electron/18.3.5.4-b478491100 Safari/537.36 Channel/pckk_other_ch",YC="https://chaoxing.com/",aJ="https://groupweb.chaoxing.com",J_="https://noteyd.chaoxing.com",j_="u2oh6Vu^HWe4_AES",fp=class{addition;cookie;persistCookie;constructor(e,t){this.addition=e,this.cookie=e.cookie||"",this.persistCookie=t}async init(){if(!this.addition.bbsid)throw new Error("[ChaoXing] bbsid is required");this.cookie||(this.cookie=await this.login(),this.persistCookie&&await this.persistCookie(this.cookie))}getCookie(){return this.cookie}downloadHeaders(){return{Cookie:this.cookie,Referer:YC,"User-Agent":z_}}async login(){let e=await Zx(this.addition.user_name,j_),t=await Zx(this.addition.password,j_),i=new FormData;i.append("uname",e),i.append("password",t),i.append("t","true");let n=await fetch("https://passport2.chaoxing.com/fanyalogin",{method:"POST",body:i}),s=[],o=typeof n.headers.getSetCookie=="function"?n.headers.getSetCookie():[];if(Array.isArray(o)&&o.length>0)for(let a of o)a&&s.push(a.split(";")[0]);else{let a=n.headers.get("set-cookie");a&&s.push(a.split(";")[0])}if(s.length===0)throw new Error("[ChaoXing] login failed: no cookie returned");return s.join("; ")}async request(e,t,i,n){let s=i?"?"+new URLSearchParams(i).toString():"",o={Cookie:this.cookie,Accept:"application/json, text/plain, */*",Referer:YC,...n},a=await fetch(`${aJ}${e}${s}`,{method:t,headers:o});if(a.status>=400)throw new Error(`[ChaoXing] request failed: HTTP ${a.status}`);return await a.json()}async requestDownload(e,t){let i={Cookie:this.cookie,Accept:"application/json, text/plain, */*",Referer:YC,"User-Agent":z_},n=await fetch(`${J_}${e}`,{method:t,headers:i});if(n.status>=400)throw new Error(`[ChaoXing] download request failed: HTTP ${n.status}`);return await n.json()}async getFiles(e){let t=[],i=await this.request("/pc/resource/getResourceList","GET",{bbsid:this.addition.bbsid,folderId:e,recType:"1"});if(i.result!==1)throw new Error(`[ChaoXing] list failed: result=${i.result}`);i.list&&i.list.length>0&&t.push(...i.list);let n=await this.request("/pc/resource/getResourceList","GET",{bbsid:this.addition.bbsid,folderId:e,recType:"2"});for(let s of n.list||[])s.content.fileId||(s.content.fileId=s.content.objectId||""),t.push(s);return t}async link(e){let t=await this.requestDownload("/screen/note_note/files/status/"+e,"POST");if(!t.download)throw new Error(`[ChaoXing] link failed: ${t.msg||"no download url"}`);return t}async mkdir(e,t){let i=await this.request("/pc/resource/addResourceFolder","GET",{bbsid:this.addition.bbsid,name:t,pid:e});if(i.result!==1)throw new Error(`[ChaoXing] mkdir failed: ${i.msg}`)}async move(e,t,i){let n=i?{bbsid:this.addition.bbsid,folderIds:e,targetId:t}:{bbsid:this.addition.bbsid,recIds:e,targetId:t},s=await this.request("/pc/resource/moveResource","GET",n);if(!s.status)throw new Error(`[ChaoXing] move failed: ${s.msg}`)}async renameFolder(e,t){let i=await this.request("/pc/resource/updateResourceFolderName","GET",{bbsid:this.addition.bbsid,folderId:e,name:t});if(i.result!==1)throw new Error(`[ChaoXing] rename failed: ${i.msg}`)}async remove(e,t){let i=t?"/pc/resource/deleteResourceFolder":"/pc/resource/deleteResourceFile",n=t?{bbsid:this.addition.bbsid,folderIds:e}:{bbsid:this.addition.bbsid,recIds:e},s=await this.request(i,"GET",n);if(s.result!==1)throw new Error(`[ChaoXing] remove failed: ${s.msg}`)}async upload(e,t,i){let s=await(await fetch(`${J_}/pc/files/getUploadConfig`,{method:"GET"})).json();if(s.result!==1||!s.msg?.token)throw new Error("[ChaoXing] get upload config failed");let o=new FormData,a=i.buffer.slice(i.byteOffset,i.byteOffset+i.byteLength);o.append("file",new Blob([a]),t),o.append("_token",s.msg.token),o.append("puid",String(s.msg.puid||""));let l=await(await fetch("https://pan-yz.chaoxing.com/upload",{method:"POST",body:o})).json();if(l.msg!=="success"||!l.objectId)throw new Error(`[ChaoXing] upload failed: ${l.msg}`);let d={cataid:"100000019",key:l.objectId,param:l.data||{}},u=JSON.stringify(d),f=await this.request("/pc/resource/addResource","GET",{bbsid:this.addition.bbsid,pid:e,type:"yunpan",params:encodeURIComponent("["+u+"]")});if(f.result!==1)throw new Error(`[ChaoXing] add resource failed: ${f.msg}`)}}});function V_(r){let e=r.content;if(e.folderName)return{name:e.folderName,size:0,is_dir:!0,modified:r.inserttime?new Date(r.inserttime).toISOString():new Date().toISOString(),sign:String(r.id),type:1};let t=e.fileId||e.objectId||"";return{name:e.name||"",size:Number(e.size)||0,is_dir:!1,modified:e.uploadDate?new Date(e.uploadDate).toISOString():new Date().toISOString(),sign:`${r.id}$${t}`,type:X(e.name||"",!1),raw_url:""}}var Ap,Y_=J(()=>{"use strict";Be();$_();Ap=class{client;pathCache=new Map;constructor(e,t){this.client=new fp(e,t)}async init(){await this.client.init()}async list(e,t){let i=await this.pathToId(t);return(await this.client.getFiles(i)).map(s=>V_(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),c=(await this.client.getFiles(o)).find(l=>(l.content.folderName||l.content.name)===n);if(c){let l=V_(c);if(!l.is_dir){let d=l.sign.split("$")[1]||"";if(d)try{let u=await this.client.link(d);l.raw_url=u.download,l.raw_url_headers=this.client.downloadHeaders()}catch{l.raw_url_error="[ChaoXing] failed to resolve download url"}}return l}}catch{}try{let o=await this.pathToId(t);return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:o,type:1,raw_url:""}}catch{return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/");await this.client.mkdir(await this.pathToId(s),n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);if(!n.isDir)throw new Error("[ChaoXing] \u6B64\u7F51\u76D8\u4E0D\u652F\u6301\u4FEE\u6539\u6587\u4EF6\u540D");await this.client.renameFolder(n.id,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.id,n.isDir),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.move(o.id,await this.pathToId(t),o.isDir),this.pathCache.clear()}async copy(){throw new Error("[ChaoXing] copy not supported")}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"file",o="/"+n.join("/"),a=await this.pathToId(o);await this.client.upload(a,s,new Uint8Array(i)),this.pathCache.clear()}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.pathToId(n),a=(await this.client.getFiles(s)).find(c=>(c.content.folderName||c.content.name)===i);if(!a)throw new Error(`[ChaoXing] '${i}' not found`);return{id:String(a.id),isDir:!!a.content.folderName}}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"-1";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n="-1";for(let s=0;sl.content.folderName===i[s]);if(!a)throw new Error(`[ChaoXing] Directory '${i[s]}' not found`);n=String(a.id);let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});var WC,XC,ZC,cJ,hp,W_=J(()=>{"use strict";WC="all",XC="albums",ZC="share_albums",cJ="root",hp=class{addition;accessToken="";constructor(e){this.addition=e}async init(){await this.refreshToken()}async refreshToken(){let e=new URLSearchParams({client_id:this.addition.client_id,client_secret:this.addition.client_secret,refresh_token:this.addition.refresh_token,grant_type:"refresh_token"}),i=await(await fetch("https://www.googleapis.com/oauth2/v4/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:e.toString()})).json();if(!i.access_token)throw new Error("[GooglePhoto] refresh token failed");this.accessToken=i.access_token}async request(e,t,i,n){let s=i?"?"+new URLSearchParams(i).toString():"",o={Authorization:"Bearer "+this.accessToken,"Accept-Encoding":"gzip"},a={method:t,headers:o};n!==void 0&&(o["Content-Type"]="application/json",a.body=JSON.stringify(n));let c=await fetch(`${e}${s}`,a);if(c.status===401&&(await this.refreshToken(),o.Authorization="Bearer "+this.accessToken,c=await fetch(`${e}${s}`,a)),c.status>=400){let l=await c.json().catch(()=>({}));throw new Error(`[GooglePhoto] request failed: ${l.error?.message||`HTTP ${c.status}`}`)}return c}async getFiles(e){switch(e){case WC:return this.getAllMedias();case XC:return this.getAlbums();case ZC:return this.getShareAlbums();case cJ:return this.getFakeRoot();default:return this.getMedias(e)}}getFakeRoot(){return[{id:WC,title:WC},{id:XC,title:XC},{id:ZC,title:ZC}]}getAlbums(){return this.fetchItems("https://photoslibrary.googleapis.com/v1/albums",{fields:"albums(id,title,coverPhotoBaseUrl),nextPageToken",pageSize:"50",pageToken:"first"},"GET")}getShareAlbums(){return this.fetchItems("https://photoslibrary.googleapis.com/v1/sharedAlbums",{fields:"sharedAlbums(id,title,coverPhotoBaseUrl),nextPageToken",pageSize:"50",pageToken:"first"},"GET")}getMedias(e){return this.fetchItems("https://photoslibrary.googleapis.com/v1/mediaItems:search",{fields:"mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",pageSize:"100",albumId:e,pageToken:"first"},"POST")}getAllMedias(){return this.fetchItems("https://photoslibrary.googleapis.com/v1/mediaItems",{fields:"mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",pageSize:"100",pageToken:"first"},"GET")}async getMedia(e){return await(await this.request(`https://photoslibrary.googleapis.com/v1/mediaItems/${e}`,"GET",{fields:"mediaMetadata,baseUrl,mimeType"})).json()}async fetchItems(e,t,i){let n=[],s=t.pageToken;for(;s!=="";){let o={...t};o.pageToken==="first"&&(o.pageToken="");let c=await(await this.request(e,i,o)).json();s=c.nextPageToken||"",c.mediaItems&&n.push(...c.mediaItems),c.albums&&n.push(...c.albums),c.sharedAlbums&&n.push(...c.sharedAlbums)}return n}async link(e){let t=await this.getMedia(e),i=t.mimeType||"";return i.includes("image/")?(t.baseUrl||"")+"=d":i.includes("video/")?(t.baseUrl||"")+"=dv":t.baseUrl||""}}});function X_(r){return r.filename?{name:r.filename,size:0,is_dir:!1,modified:r.mediaMetadata?.creationTime||new Date().toISOString(),sign:r.id,type:X(r.filename,!1),thumb:r.baseUrl?r.baseUrl+"=w100-h100-c":"",raw_url:""}:{name:r.title||r.id,size:0,is_dir:!0,modified:new Date().toISOString(),sign:r.id,type:1}}var gp,Z_=J(()=>{"use strict";Be();W_();gp=class{client;pathCache=new Map;constructor(e){this.client=new hp(e)}async init(){await this.client.init()}async list(e,t){let i=await this.pathToId(t);return(await this.client.getFiles(i)).map(s=>X_(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),c=(await this.client.getFiles(o)).find(l=>(l.filename||l.title)===n);if(c){let l=X_(c);if(!l.is_dir)try{l.raw_url=await this.client.link(c.id)}catch{l.raw_url_error="[GooglePhoto] failed to resolve download url"}return l}}catch{}return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(){throw new Error("[GooglePhoto] mkdir not supported")}async rename(){throw new Error("[GooglePhoto] rename not supported")}async remove(){throw new Error("[GooglePhoto] remove not supported")}async move(){throw new Error("[GooglePhoto] move not supported")}async copy(){throw new Error("[GooglePhoto] copy not supported")}async put(){throw new Error("[GooglePhoto] upload not supported")}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"root";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=i[0];for(let s=1;s(l.filename||l.title)===i[s]);if(!a)throw new Error(`[GooglePhoto] Directory '${i[s]}' not found`);n=a.id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return this.pathCache.set(t,n),n}}});var pp,e8=J(()=>{"use strict";pp=class{addition;accessToken="";tokenType="Bearer";persist;constructor(e,t){this.addition=e,this.persist=t}async init(){if(!this.addition.client_id||!this.addition.client_secret)throw new Error("[FebBox] client_id and client_secret are required");await this.refreshToken()}async refreshToken(){let e=new URLSearchParams;this.addition.refresh_token?(e.set("grant_type","refresh_token"),e.set("refresh_token",this.addition.refresh_token)):e.set("grant_type","client_credentials"),e.set("client_id",this.addition.client_id),e.set("client_secret",this.addition.client_secret);let i=await(await fetch("https://api.febbox.com/oauth/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:e.toString()})).json();if(i.code!==1||!i.data?.access_token)throw new Error(`[FebBox] token fetch failed: ${i.msg}`);this.accessToken=i.data.access_token,this.tokenType=i.data.token_type||"Bearer",i.data.refresh_token&&(this.addition.refresh_token=i.data.refresh_token,this.persist&&await this.persist(i.data.refresh_token))}async request(e){let t=new FormData;for(let[s,o]of Object.entries(e))t.append(s,o);let n=await(await fetch("https://api.febbox.com/oauth",{method:"POST",headers:{Authorization:`${this.tokenType} ${this.accessToken}`},body:t})).json().catch(()=>({}));if(n.code===-10001&&n.server_name)return await this.refreshToken(),this.request(e);if(n.code!==0&&n.code!==1)throw new Error(`[FebBox] ${n.msg||n.code}`);return n}async getFilesList(e){let t=this.addition.page_size||100,i=[],n=1;for(;;){let o=(await this.request({module:"file_list",parent_id:e,page:String(n),pagelimit:String(t),order:this.addition.sort_rule||"name_asc"})).data?.file_list||[];if(i.push(...o),o.length{"use strict";Be();e8();mp=class{client;userIp;pathCache=new Map;constructor(e,t){this.client=new pp(e,t),this.userIp=e.user_ip||""}async init(){await this.client.init()}async list(e,t){let i=await this.pathToId(t);return(await this.client.getFilesList(i)).map(s=>t8(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),c=(await this.client.getFilesList(o)).find(l=>l.file_name===n);if(c){let l=t8(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadLink(l.sign,this.userIp)}catch{l.raw_url_error="[FebBox] failed to resolve download url"}return l}}catch{}return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/");await this.client.makeDir(await this.pathToId(s),n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.id,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.id),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.move(o.id,await this.pathToId(t)),this.pathCache.clear()}async copy(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.copy(o.id,await this.pathToId(t)),this.pathCache.clear()}async put(){throw new Error("[FebBox] upload not supported")}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.pathToId(n),a=(await this.client.getFilesList(s)).find(c=>c.file_name===i);if(!a)throw new Error(`[FebBox] '${i}' not found`);return{id:String(a.fid),isDir:a.is_dir===1}}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return"0";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n="0";for(let s=0;sl.file_name===i[s]&&l.is_dir===1);if(!a)throw new Error(`[FebBox] Directory '${i[s]}' not found`);n=String(a.fid);let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});var lJ,dJ,uJ,fJ,AJ,yp,i8=J(()=>{"use strict";lJ="https://rest-api.degoo.com/login",dJ="https://rest-api.degoo.com/access-token/v2",uJ="https://production-appsync.degoo.com/graphql",fJ="da2-vs6twz5vnjdavpqndtbzg3prra",AJ="CgAQAg",yp=class{addition;accessToken="";refreshToken="";rootId="0";persist;constructor(e,t){this.addition=e,this.accessToken=e.access_token||"",this.refreshToken=e.refresh_token||"",this.persist=t}async init(){await this.ensureValidToken(),await this.getDevices()}getRootId(){return this.rootId}async saveTokens(){this.persist&&await this.persist({accessToken:this.accessToken,refreshToken:this.refreshToken})}isTokenExpired(){if(!this.accessToken)return!0;try{let e=this.accessToken.split(".");if(e.length!==3)return!0;let t=JSON.parse(atob(e[1].replace(/-/g,"+").replace(/_/g,"/"))),i=Number(t.exp)*1e3;return Date.now()+300*1e3>i}catch{return!0}}async ensureValidToken(){if(this.isTokenExpired()){if(this.refreshToken)try{await this.refreshAccessToken();return}catch{}this.addition.username&&this.addition.password&&await this.login()}}async refreshAccessToken(){let t=await(await fetch(dJ,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({RefreshToken:this.refreshToken})})).json();if(!t.AccessToken)throw new Error("[Degoo] refresh token failed");this.accessToken=t.AccessToken,await this.saveTokens()}async login(){let t=await(await fetch(lJ,{method:"POST",headers:{"Content-Type":"application/json",Origin:"https://app.degoo.com"},body:JSON.stringify({GenerateToken:!0,Username:this.addition.username,Password:this.addition.password})})).json();if(t.RefreshToken)this.refreshToken=t.RefreshToken,await this.refreshAccessToken();else if(t.Token)this.accessToken=t.Token,this.refreshToken="";else throw new Error("[Degoo] login failed: no valid token");await this.saveTokens()}async apiCall(e,t,i){await this.ensureValidToken(),i.Token!==void 0&&(i.Token=this.accessToken);let s=await(await fetch(uJ,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":fJ,Authorization:`Bearer ${this.accessToken}`},body:JSON.stringify({operationName:e,query:t,variables:i})})).json();if(s.errors&&s.errors.length>0){let o=s.errors[0];if(o.errorType==="Unauthorized")return await this.login(),i.Token=this.accessToken,this.apiCall(e,t,i);throw new Error(`[Degoo] GraphQL error: ${o.message}`)}return s.data}async getDevices(){let t=await this.apiCall("GetFileChildren5","query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ParentID } NextToken } }",{Token:this.accessToken,ParentID:"0",Limit:10,Order:3});if(this.rootId==="0"){let i=t?.getFileChildren5?.Items;i&&i.length>0&&(this.rootId=i[0].ParentID)}}async getFileChildren5(e){let t="query GetFileChildren5($Token: String! $ParentID: String $AllParentIDs: [String] $Limit: Int! $Order: Int! $NextToken: String ) { getFileChildren5(Token: $Token ParentID: $ParentID AllParentIDs: $AllParentIDs Limit: $Limit Order: $Order NextToken: $NextToken) { Items { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime FilePath IsInRecycleBin DeviceID MetadataID } NextToken } }",i=[],n="";for(;;){let s={Token:this.accessToken,ParentID:e,Limit:1e3,Order:3};n&&(s.NextToken=n);let o=await this.apiCall("GetFileChildren5",t,s),a=o?.getFileChildren5?.Items||[];if(i.push(...a),n=o?.getFileChildren5?.NextToken||"",!n)break}return i}async getOverlay4(e){return(await this.apiCall("GetOverlay4","query GetOverlay4($Token: String!, $ID: IDType!) { getOverlay4(Token: $Token, ID: $ID) { ID ParentID Name Category Size CreationTime LastModificationTime LastUploadTime URL FilePath IsInRecycleBin DeviceID MetadataID } }",{Token:this.accessToken,ID:{FileID:e}}))?.getOverlay4}async mkdir(e,t){await this.apiCall("SetUploadFile3","mutation SetUploadFile3($Token: String!, $FileInfos: [FileInfoUpload3]!) { setUploadFile3(Token: $Token, FileInfos: $FileInfos) }",{Token:this.accessToken,FileInfos:[{Checksum:AJ,Name:t,CreationTime:Date.now(),ParentID:e,Size:0}]})}async move(e,t){await this.apiCall("SetMoveFile","mutation SetMoveFile($Token: String!, $Copy: Boolean, $NewParentID: String!, $FileIDs: [String]!) { setMoveFile(Token: $Token, Copy: $Copy, NewParentID: $NewParentID, FileIDs: $FileIDs) }",{Token:this.accessToken,Copy:!1,NewParentID:t,FileIDs:[e]})}async rename(e,t){await this.apiCall("SetRenameFile","mutation SetRenameFile($Token: String!, $FileRenames: [FileRenameInfo]!) { setRenameFile(Token: $Token, FileRenames: $FileRenames) }",{Token:this.accessToken,FileRenames:[{ID:e,NewName:t}]})}async remove(e){await this.apiCall("SetDeleteFile5","mutation SetDeleteFile5($Token: String!, $IsInRecycleBin: Boolean!, $IDs: [IDType]!) { setDeleteFile5(Token: $Token, IsInRecycleBin: $IsInRecycleBin, IDs: $IDs) }",{Token:this.accessToken,IsInRecycleBin:!1,IDs:[{FileID:e}]})}}});function eE(r){return r===1||r===2||r===10}function n8(r){let e=eE(r.Category),t=Number(r.Size)||0,i=Number(r.LastModificationTime);return{name:r.Name,size:e?0:t,is_dir:e,modified:i?new Date(i).toISOString():new Date().toISOString(),sign:r.ID,type:X(r.Name,e),raw_url:""}}var Ip,s8=J(()=>{"use strict";Be();i8();Ip=class{client;pathCache=new Map;constructor(e,t){this.client=new yp(e,t)}async init(){await this.client.init()}async list(e,t){let i=await this.pathToId(t);return(await this.client.getFileChildren5(i)).map(s=>n8(s))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),c=(await this.client.getFileChildren5(o)).find(l=>l.Name===n);if(c){let l=n8(c);if(!l.is_dir)try{let d=await this.client.getOverlay4(c.ID);l.raw_url=d.URL||""}catch{l.raw_url_error="[Degoo] failed to resolve download url"}return l}}catch{}return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/");await this.client.mkdir(await this.pathToId(s),n),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.id,i),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);await this.client.remove(n.id),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n);await this.client.move(o.id,await this.pathToId(t)),this.pathCache.clear()}async copy(){throw new Error("[Degoo] copy not supported")}async put(){throw new Error("[Degoo] upload not supported in stateless environment")}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.pathToId(n),a=(await this.client.getFileChildren5(s)).find(c=>c.Name===i);if(!a)throw new Error(`[Degoo] '${i}' not found`);return{id:a.ID,isDir:eE(a.Category)}}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return this.client.getRootId();if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=this.client.getRootId();for(let s=0;sl.Name===i[s]&&eE(l.Category));if(!a)throw new Error(`[Degoo] Directory '${i[s]}' not found`);n=a.ID;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});function Ol(r){return new TextEncoder().encode(r)}function d8(r,e=16){let t=e-r.length%e,i=new Uint8Array(r.length+t);return i.set(r),i.fill(t,r.length),i}function yJ(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e)}function u8(r){return Array.from(r).map(e=>e.toString(16).padStart(2,"0")).join("")}async function c8(r,e,t){if(l8){let{createCipheriv:o}=await import("node:crypto"),a=o("aes-128-cbc",Buffer.from(e),Buffer.from(t));return new Uint8Array(Buffer.concat([a.update(Buffer.from(r)),a.final()]))}let i=await crypto.subtle.importKey("raw",e,{name:"AES-CBC"},!1,["encrypt"]),n=d8(r),s=await crypto.subtle.encrypt({name:"AES-CBC",iv:t},i,n);return new Uint8Array(s)}async function IJ(r,e){if(l8){let{createCipheriv:o}=await import("node:crypto"),a=o("aes-128-ecb",Buffer.from(e),null);return new Uint8Array(Buffer.concat([a.update(Buffer.from(r)),a.final()]))}let t=await crypto.subtle.importKey("raw",e,{name:"AES-CBC"},!1,["encrypt"]),i=d8(r),n=new Uint8Array(i.length),s=new Uint8Array(16);for(let o=0;o0n;)e&1n&&(i=i*r%t),r=r*r%t,e>>=1n;return i}function EJ(r){let e=new Uint8Array(128);e.set(r,112);let t=wJ(e),i=BigInt("0x"+pJ),n=CJ(t,mJ,i);return xJ(n,128)}function BJ(){let r=new Uint8Array(16),e=new Uint8Array(16);for(let t=0;t<16;t++){let i=Math.floor(Math.random()*a8.length),n=a8.charCodeAt(i);r[t]=n,e[15-t]=n}return{key:r,reversed:e}}async function f8(r){let e=Ol(JSON.stringify(r)),{key:t,reversed:i}=BJ(),n=await c8(e,Ol(hJ),Ol(o8)),s=await c8(n,i,Ol(o8));return{params:yJ(s),encSecKey:u8(EJ(t))}}async function A8(r){let e=Ol(JSON.stringify(r)),t=await IJ(e,Ol(gJ));return{eparams:u8(t).toUpperCase()}}var hJ,gJ,o8,a8,pJ,mJ,l8,h8=J(()=>{"use strict";hJ="0CoJUm6Qyw8W8jud",gJ="rFgB&h#%2?^eDg:Q",o8="0102030405060708",a8="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",pJ="e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7",mJ=65537n,l8=typeof process<"u"&&process?.release?.name==="node"});var wp,g8=J(()=>{"use strict";h8();wp=class{addition;csrfToken="";musicU="";constructor(e){this.addition=e}init(){if(this.csrfToken=this.getCookie("__csrf"),this.musicU=this.getCookie("MUSIC_U"),!this.csrfToken||!this.musicU)throw new Error("[NeteaseMusic] cookie must contain __csrf and MUSIC_U")}getCookie(e){let i=new RegExp(e+"=([^(;|$)]+)").exec(this.addition.cookie);return i?i[1]:""}async request(e,t,i){let n={Cookie:this.addition.cookie};e.includes("music.163.com")&&(n.Referer="https://music.163.com");let s;if(i.crypto==="weapi"?(s=await f8(i.data),e=e.replace(/\/\w*api\//,"/weapi/")):(s=await A8({url:e.replace(/\/\w*api\//,"/api/"),method:t,params:i.data}),n["User-Agent"]="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36",e="https://music.163.com/api/linux/forward"),i.cookies){let a=i.cookies.map(c=>`${c.name}=${c.value}`).join("; ");n.Cookie=n.Cookie+"; "+a}return(await fetch(e,{method:t,headers:{...n,"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(s).toString()})).json()}async getSongObjs(e){return(await this.request("https://music.163.com/weapi/v1/cloud/get","POST",{crypto:"weapi",data:{limit:String(e),offset:"0"},cookies:[{name:"os",value:"pc"}]})).data||[]}async getSongLink(e){let t=await this.request("https://music.163.com/api/song/enhance/player/url","POST",{crypto:"linuxapi",data:{ids:"["+e+"]",br:"999000"},cookies:[{name:"os",value:"pc"}]});if(!t.data||t.data.length<1)throw new Error("[NeteaseMusic] song url not found");return t.data[0].url||""}async removeSong(e){await this.request("http://music.163.com/weapi/cloud/del","POST",{crypto:"weapi",data:{songIds:"["+e+"]"}})}}});function p8(r){return{name:r.fileName,size:r.fileSize,is_dir:!1,modified:r.addTime?new Date(r.addTime).toISOString():new Date().toISOString(),sign:String(r.songId),type:X(r.fileName,!1),thumb:r.simpleSong?.al?.picUrl||"",raw_url:""}}var xp,m8=J(()=>{"use strict";Be();g8();xp=class{client;songLimit;constructor(e){this.client=new wp(e),this.songLimit=e.song_limit||200}async init(){this.client.init()}async list(){return(await this.client.getSongObjs(this.songLimit)).map(t=>p8(t))}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"";if(!n)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""};let o=(await this.client.getSongObjs(this.songLimit)).find(a=>a.fileName===n);if(o){let a=p8(o);try{a.raw_url=await this.client.getSongLink(a.sign)}catch{a.raw_url_error="[NeteaseMusic] failed to resolve song url"}return a}return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""}}async mkdir(){throw new Error("[NeteaseMusic] mkdir not supported")}async rename(){throw new Error("[NeteaseMusic] rename not supported")}async remove(e,t,i){let n=t.split("/").filter(Boolean),s=n[n.length-1],a=(await this.client.getSongObjs(this.songLimit)).find(c=>c.fileName===s);if(!a)throw new Error(`[NeteaseMusic] '${s}' not found`);await this.client.removeSong(String(a.songId))}async move(){throw new Error("[NeteaseMusic] move not supported")}async copy(){throw new Error("[NeteaseMusic] copy not supported")}async put(){throw new Error("[NeteaseMusic] upload not supported in stateless environment")}}});function ta(r){return new TextEncoder().encode(r)}function E8(r){return Array.from(r).map(e=>e.toString(16).padStart(2,"0")).join("")}async function w8(r){let e=typeof r=="string"?ta(r):r,t=await crypto.subtle.digest("SHA-256",e);return E8(new Uint8Array(t))}async function Cp(r,e){let t=await crypto.subtle.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,r);return new Uint8Array(i)}function x8(r){return encodeURIComponent(r).replace(/%20/g,"%20").replace(/[!'()*]/g,e=>"%"+e.charCodeAt(0).toString(16).toUpperCase())}async function C8(r){let e=r.now??new Date,t=e.toISOString().slice(0,10),i=e.toISOString(),n=(BigInt(Math.floor(e.getTime()))*1000000n+BigInt(Math.floor(Math.random()*1e6))).toString(36),s=r.bodyRaw.length>0,o={host:r.apiHost,"x-hl-nonce":n,"x-hl-timestamp":i},a=[];s&&(o["content-type"]="application/json; charset=utf-8",a.push("content-type"));let c=[...a].sort(),l=c.map(R=>`${R}:${o[R]} -`).join(""),d=c.join(";"),u="";r.query&&Object.keys(r.query).length>0&&(u=Object.keys(r.query).sort().map(U=>`${x8(U)}=${x8(r.query[U]??"")}`).join("&"));let f=await w8(r.bodyRaw),h=[r.method,r.apiPath,u,l,d,f].join(` -`),g=`${t}/${r.accessToken}/${I8}`,p=[y8,i,g,await w8(h)].join(` -`),I=ta(bJ+r.secretKey),m=await Cp(ta(t),I),w=await Cp(ta(r.accessToken),m),B=await Cp(ta(I8),w),C=E8(await Cp(ta(p),B)),x=`${y8} Credential=${r.secretId}/${g}, SignedHeaders=${d}, Signature=${C}`;o.authorization=x;let Q=`https://${r.apiHost}${r.apiPath}${u?"?"+u:""}`;return{headers:o,url:Q}}function vJ(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:"/"+i}var y8,bJ,I8,Ep,B8=J(()=>{"use strict";y8="HL6-HMAC-SHA256",bJ="HL6",I8="hl6_request";Ep=class{addition;accessToken="";host;persistRefreshToken;constructor(e,t){this.addition=e,this.host=e.host||"openapi.2dland.cn",this.persistRefreshToken=t}async init(){if(!this.addition.client_id||!this.addition.client_secret)throw new Error("[HalalCloud] client_id and client_secret are required");if(!this.addition.refresh_token)throw new Error("[HalalCloud] refresh_token \u4E3A\u7A7A\uFF1A\u8BF7\u4ECE HalalCloud \u5B98\u65B9\u83B7\u53D6 refresh_token");await this.refreshToken()}async refreshToken(){let e={refresh_token:this.addition.refresh_token,grant_type:"refresh_token",client_id:this.addition.client_id},t=ta(JSON.stringify(e)),i=await C8({apiHost:this.host,secretId:this.addition.client_id,secretKey:this.addition.client_secret,accessToken:this.accessToken,method:"POST",apiPath:"/v6/oauth/refresh_token",bodyRaw:t}),s=await(await fetch(i.url,{method:"POST",headers:i.headers,body:t})).json().catch(()=>({}));if(!s.access_token)throw new Error("[HalalCloud] refresh token failed");this.accessToken=s.access_token,s.refresh_token&&(this.addition.refresh_token=s.refresh_token,this.persistRefreshToken&&await this.persistRefreshToken(s.refresh_token))}async request(e,t,i=!1){let n=t!==void 0?ta(JSON.stringify(t)):new Uint8Array(0),s=await C8({apiHost:this.host,secretId:this.addition.client_id,secretKey:this.addition.client_secret,accessToken:this.accessToken,method:"POST",apiPath:e,bodyRaw:n}),o=await fetch(s.url,{method:"POST",headers:s.headers,body:n.length>0?n:void 0});if(o.status===401&&!i)return await this.refreshToken(),this.request(e,t,!0);let a=await o.text(),c;try{c=JSON.parse(a)}catch{c={}}if(o.status>=300)throw new Error(`[HalalCloud] API error: ${c.message||c.msg||`HTTP ${o.status}`}`);return c}async listFiles(e){let t=[],i="";for(;;){let n={parent:{path:e},list_info:{limit:100,token:i}},s=await this.request("/v6/userfile/list",n),o=s.files||[];if(t.push(...o),i=s.list_info?.token||"",!i||o.length===0)break}return t}async getFile(e){let t=await this.request("/v6/userfile/get",{path:e});return t.identity?t:null}async createDir(e,t){let i=vJ(e,t);await this.request("/v6/userfile/create",{parent:e,path:i,name:t,dir:!0})}async rename(e,t){await this.request("/v6/userfile/rename",{path:e,name:t})}async move(e,t,i){let n={source:[{path:e,identity:i}],dest:{path:t}};await this.request("/v6/userfile/move",n)}async copy(e,t,i){let n={source:[{path:e,identity:i}],dest:{path:t}};await this.request("/v6/userfile/copy",n)}async remove(e,t,i){let n={source:[{path:e,identity:t,dir:i}]};await this.request("/v6/userfile/delete",n)}async getDownloadUrl(e,t){let i=await this.request("/v6/userfile/get_direct_download_address",{path:e,identity:t});if(!i.download_address)throw new Error("[HalalCloud] \u83B7\u53D6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25");return i.download_address}}});function tE(r){return typeof r=="number"?r:typeof r=="string"&&r.trim()!==""?Number(r):0}function b8(r){let e=r.dir===!0,t=tE(r.update_ts),i=tE(r.create_ts),n=t||i;return{name:r.name||"",size:e?0:tE(r.size),is_dir:e,modified:n?new Date(n*1e3).toISOString():new Date().toISOString(),sign:r.identity||r.path||"",type:X(r.name||"",e),raw_url:""}}function v8(r){return!r||r==="/"?"/":r.startsWith("/")?r:"/"+r}function k8(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:"/"+i}var Bp,S8=J(()=>{"use strict";Be();B8();Bp=class{client;rootPath;constructor(e,t){this.client=new Ep(e,t),this.rootPath=v8(e.root_folder_id||"/")}async init(){await this.client.init()}fullPath(e){let t=v8(e);return this.rootPath==="/"?t:t==="/"?this.rootPath:this.rootPath+t}async list(e,t){return(await this.client.listFiles(this.fullPath(t))).map(b8)}async get(e,t){let i=this.fullPath(t),n=i.split("/").filter(Boolean),s=n[n.length-1]||"root",o="/"+n.slice(0,-1).join("/");try{let c=(await this.client.listFiles(o==="//"?"/":o)).find(l=>l.name===s);if(c){let l=b8(c);if(!l.is_dir)try{l.raw_url=await this.client.getDownloadUrl(c.path||i,c.identity||"")}catch{l.raw_url_error="[HalalCloud] failed to resolve download url"}return l}}catch{}return{name:s,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){let n=this.fullPath(t).split("/").filter(Boolean),s=n.pop()||"new_folder",o="/"+n.join("/");await this.client.createDir(o==="//"?"/":o,s)}async rename(e,t,i){let n=this.fullPath(t);await this.client.rename(n,i)}async remove(e,t,i){let n=this.fullPath(t),s=await this.resolveEntry(n);await this.client.remove(s.path,s.identity,s.isDir)}async move(e,t,i,n,s){let o=this.fullPath(n),a=this.fullPath(t),c=await this.resolveEntry(o),l=o.split("/").filter(Boolean).pop()||"";await this.client.move(c.path,k8(a,l),c.identity)}async copy(e,t,i,n,s){let o=this.fullPath(n),a=this.fullPath(t),c=await this.resolveEntry(o),l=o.split("/").filter(Boolean).pop()||"";await this.client.copy(c.path,k8(a,l),c.identity)}async put(){throw new Error("[HalalCloud] upload not supported in stateless environment")}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),o=(await this.client.listFiles(n==="//"?"/":n)).find(a=>a.name===i);if(!o)throw new Error(`[HalalCloud] '${i}' not found`);return{path:o.path||e,identity:o.identity||"",isDir:o.dir===!0}}}});function DJ(r){let e=(r||"").trim();if(!e)return"";if(/^Bearer\s+/i.test(e))return e.replace(/^Bearer\s+/i,"").trim();if(/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(e))return e;let t={};for(let n of e.split(";")){let s=n.indexOf("=");if(s<0)continue;let o=n.slice(0,s).trim(),a=n.slice(s+1).trim();o&&(t[o]=a)}let i=n=>{let s=t[n]||"";return/^Bearer\s+/i.test(s)?s.replace(/^Bearer\s+/i,"").trim():s};return i("sso-token")||i("token")||i("authorization")||""}function D8(r){let e=4294967295;for(let t=0;t>>8;return(e^4294967295)>>>0}function KJ(r){let e=Math.round(1e7*Math.random()).toString(),t=new Date,n=Math.round((t.getTime()+8*36e5)/1e3).toString(),s=t.getUTCFullYear(),o=String(t.getUTCMonth()+1).padStart(2,"0"),a=String(t.getUTCDate()).padStart(2,"0"),c=String(t.getUTCHours()+8).padStart(2,"0"),l=String(t.getUTCMinutes()).padStart(2,"0"),u=`${s}${o}${a}${c}${l}`.split("").map(p=>HJ[parseInt(p)]).join(""),f=(D8(u)>>>0).toString(),h=[n,e,r,"web","3",f].join("|"),g=(D8(h)>>>0).toString();return`${f}=${n}-${e}-${g}`}function GJ(r){let e=r.indexOf("?"),t=e>=0?r.substring(0,e):r,i=e>=0?r.substring(e+1):"",n=new URL(r),s=KJ(n.pathname);return`${t}?${i}${i?"&":""}${s}`}var Bn,kJ,SJ,QJ,_J,FJ,PJ,TJ,UJ,RJ,NJ,qJ,OJ,MJ,LJ,HJ,bp,Q8=J(()=>{"use strict";Bn="https://yun.123pan.com/b/api",kJ="https://login.123pan.com/api",SJ=kJ+"/user/sign_in";QJ=Bn+"/user/info",_J=Bn+"/file/list/new",FJ=Bn+"/file/download_info",PJ=Bn+"/file/upload_request",TJ=Bn+"/file/mod_pid",UJ=Bn+"/file/rename",RJ=Bn+"/file/trash",NJ=Bn+"/file/upload_request",qJ=Bn+"/file/s3_upload_object/auth",OJ=Bn+"/file/s3_repare_upload_parts_batch",MJ=Bn+"/file/upload_complete/v2",LJ=(()=>{let r=new Array(256);for(let e=0;e<256;e++){let t=e;for(let i=0;i<8;i++)t=t&1?3988292384^t>>>1:t>>>1;r[e]=t}return r})();HJ=["a","d","e","f","g","h","l","m","y","i","j","n","o","p","k","q","r","s","t","u","b","c","v","w","s","z"];bp=class{addition;accessToken="";onTokenUpdate;constructor(e,t){this.addition=e,this.onTokenUpdate=t}getRootId(){return(this.addition.root_id||"0").trim()||"0"}async login(){if(this.addition.access_token){this.accessToken=this.addition.access_token;try{await this.userInfo(!0);return}catch{this.accessToken=""}}if(this.addition.cookie){let e=DJ(this.addition.cookie);if(e){this.accessToken=e;try{await this.userInfo(!0),this.addition.access_token=e,this.onTokenUpdate?.(e);return}catch{this.accessToken=""}}}if(!this.addition.username||!this.addition.password)throw new Error("123 \u7F51\u76D8\u767B\u5F55\u51ED\u8BC1\u7F3A\u5931\uFF1A\u8BF7\u586B\u5199 123 \u7F51\u76D8\u624B\u673A\u53F7 + \u5BC6\u7801\uFF1B\u82E5\u90E8\u7F72\u73AF\u5883\uFF08\u5982 Cloudflare Workers \u6570\u636E\u4E2D\u5FC3 IP\uFF09\u5BC6\u7801\u767B\u5F55\u4F1A\u88AB\u98CE\u63A7\uFF0C\u53EF\u5728\u300CCookie\u300D\u5B57\u6BB5\u7C98\u8D34\u6D4F\u89C8\u5668\u767B\u5F55\u540E\u7684 Cookie\uFF08\u542B sso-token\uFF09\uFF0C\u6216\u586B\u5199\u6709\u6548\u7684\u8BBF\u95EE\u4EE4\u724C access_token\uFF08\u5728\u672C\u673A\u6D4F\u89C8\u5668\u767B\u5F55 https://www.123pan.com/ \u540E\u4ECE\u5F00\u53D1\u8005\u5DE5\u5177\u83B7\u53D6\uFF09\u3002");await this.signIn()}async signIn(){let t=/@/.test(this.addition.username)?{mail:this.addition.username,password:this.addition.password,type:2}:{passport:this.addition.username,password:this.addition.password,remember:!0},n=await(await fetch(SJ,{method:"POST",headers:{"Content-Type":"application/json",origin:"https://yun.123pan.com",referer:"https://yun.123pan.com/","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",platform:"web","app-version":"3"},body:JSON.stringify(t)})).json();if(n.code!==200)throw new Error(`123 \u7F51\u76D8\u767B\u5F55\u5931\u8D25\uFF08${n.message||`code ${n.code}`}\uFF09\u3002\u5F53\u524D\u90E8\u7F72\u73AF\u5883\u7684\u51FA\u53E3 IP \u88AB 123 \u5224\u5B9A\u4E3A\u5883\u5916/\u964C\u751F\u8BBE\u5907\uFF08\u5982 Cloudflare Workers \u6570\u636E\u4E2D\u5FC3 IP\uFF09\uFF0C\u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u4F1A\u88AB\u98CE\u63A7\u62E6\u622A\u3002\u53EF\u9760\u65B9\u6848\uFF1A\u2460 \u5728\u672C\u673A\u6D4F\u89C8\u5668\u767B\u5F55 https://www.123pan.com/\uFF08\u767B\u5F55\u4E00\u6B21\u6216\u4FEE\u6539\u5BC6\u7801\u53EF\u89E3\u9664\u8D26\u53F7\u98CE\u9669\uFF09\uFF0C\u6253\u5F00\u5F00\u53D1\u8005\u5DE5\u5177 \u2192 Application/Network \u2192 \u590D\u5236\u8BF7\u6C42\u5934\u4E2D\u7684 Bearer \u4EE4\u724C\uFF0C\u586B\u5165\u5B58\u50A8\u8BBE\u7F6E\u7684 access_token \u5B57\u6BB5\uFF08\u4EE4\u724C\u6709\u6548\u671F\u5185 API \u8BF7\u6C42\u4E0D\u53D7 IP \u98CE\u63A7\u5F71\u54CD\uFF09\uFF1B\u2461 \u6216\u5C06\u8BE5\u7F51\u76D8\u90E8\u7F72\u5230\u5883\u5185\u670D\u52A1\u5668\uFF08Node \u5BB9\u5668\u6A21\u5F0F\uFF09\u540E\u4F7F\u7528\u8D26\u53F7\u5BC6\u7801\u3002`);if(this.accessToken=n.data?.token||"",!this.accessToken)throw new Error("login returned empty token");this.addition.access_token=this.accessToken,this.onTokenUpdate?.(this.accessToken)}async request(e,t,i,n,s=!1){let o=async()=>{let l=GJ(e),d={origin:"https://yun.123pan.com",referer:"https://yun.123pan.com/",authorization:this.accessToken?`Bearer ${this.accessToken}`:"","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",platform:this.addition.platform||"web","app-version":"3",Accept:"application/json"},u={method:t,headers:d};return i!==void 0&&t!=="GET"&&(d["Content-Type"]="application/json",u.body=JSON.stringify(i)),(await fetch(l,u)).json()},a=await o(),c=a?.code;if(c!==0&&c!==200){if(c===401&&!s){await this.login(),a=await o();let l=a?.code;if(l!==0&&l!==200)throw new Error(a?.message||`api error: code ${l}`);return a}throw new Error(a?.message||`api error: code ${c}`)}return a}async userInfo(e=!1){return(await this.request(QJ,"GET",void 0,void 0,e)).data}async getFiles(e,t){let i=[],n=1,s="0",o=t?.maxPages??45;for(;;){if(t?.budget){if(t.budget.used>=t.budget.limit){console.warn(`[123Pan] \u5DF2\u8FBE Cloudflare subrequest \u9884\u7B97\u4E0A\u9650(${t.budget.limit} \u6B21)\uFF0C\u7ED3\u679C\u5DF2\u622A\u65AD\uFF08\u76EE\u5F55\u6587\u4EF6\u8FC7\u591A\u6216\u8DEF\u5F84\u8FC7\u6DF1\uFF09`);break}t.budget.used++}if(n>o){console.warn(`[123Pan] \u5206\u9875\u8D85\u8FC7 ${o} \u9875\uFF0C\u7ED3\u679C\u53EF\u80FD\u4E0D\u5B8C\u6574\uFF08\u76EE\u5F55\u6587\u4EF6\u8FC7\u591A\uFF09`);break}let a=new URLSearchParams({driveId:"0",limit:"100",next:s,orderBy:this.addition.order_by||"file_id",orderDirection:this.addition.order_direction||"desc",parentFileId:e,trashed:"false",SearchData:"",Page:String(n),OnlyLookAbnormalFile:"0",event:"homeListFile",operateType:"4",inDirectSpace:"false"}),c=`${_J}?${a.toString()}`,l=await this.request(c,"GET"),d=l.data?.InfoList||[];if(i.push(...d),t?.findName){let f=d.find(h=>h.FileName===t.findName&&(t.findIsDir===void 0||h.Type===1===t.findIsDir));if(f)return[f]}let u=String(l.data?.Next??"-1");if(!l.data||d.length===0||u==="-1")break;s=u,n++}return i}async getDownloadLink(e){let t={driveId:0,etag:e.Etag,fileId:e.FileId,fileName:e.FileName,s3keyFlag:e.S3KeyFlag,size:e.Size,type:e.Type},n=(await this.request(FJ,"POST",t)).data?.DownloadUrl||"";if(!n)throw new Error("no download url");try{let a=new URL(n).searchParams.get("params");if(a){let c=atob(a);n=new URL(c).toString()}}catch{}let s=await fetch(n,{method:"GET",redirect:"manual",headers:{Referer:"https://yun.123pan.com/"}});return s.status===302?s.headers.get("location")||n:s.status<300&&(await s.json().catch(()=>({}))).data?.redirect_url||n}async mkdir(e,t){let i=await this.request(PJ,"POST",{driveId:0,etag:"",fileName:t,parentFileId:parseInt(e,10)||0,size:0,type:1});return i.data?.FileId!=null?String(i.data.FileId):""}async rename(e,t){await this.request(UJ,"POST",{driveId:0,fileId:parseInt(e,10),fileName:t})}async move(e,t){await this.request(TJ,"POST",{fileIdList:e.map(i=>({FileId:parseInt(i,10)})),parentFileId:parseInt(t,10)})}async remove(e,t){await this.request(RJ,"POST",{driveId:0,operation:!0,fileTrashInfoList:[t]})}async getPartUploadUrl(e,t,i){let s=(i===1?await this.getS3Auth(e,t,t+1):await this.getS3PreSignedUrls(e,t,t+1)).presignedUrls[String(t)];if(!s)throw new Error(`[123Pan] \u672A\u8FD4\u56DE\u7B2C ${t} \u5206\u7247\u7684\u4E0A\u4F20 URL`);return s}async completeUpload(e,t,i){await this.completeS3(e,t,i)}async createUpload(e,t,i,n){let s={driveId:0,duplicate:2,etag:n,fileName:e,parentFileId:t,size:i,type:0};return(await this.request(NJ,"POST",s)).data}async getS3Auth(e,t,i){let n={StorageNode:e.StorageNode,bucket:e.Bucket,key:e.Key,partNumberEnd:i,partNumberStart:t,uploadId:e.UploadId};return(await this.request(qJ,"POST",n)).data}async getS3PreSignedUrls(e,t,i){let n={bucket:e.Bucket,key:e.Key,partNumberEnd:i,partNumberStart:t,uploadId:e.UploadId,StorageNode:e.StorageNode};return(await this.request(OJ,"POST",n)).data}async completeS3(e,t,i){await this.request(MJ,"POST",{StorageNode:e.StorageNode,bucket:e.Bucket,fileId:e.FileId,fileSize:t,isMultipart:i,key:e.Key,uploadId:e.UploadId})}async uploadFile(e,t,i){let n="";try{n=(await import("node:crypto")).createHash("md5").update(i).digest("hex")}catch{n=""}let s=await this.createUpload(t,e,i.length,n);if(s.Reuse||s.Key==="")return;let o=16*1024*1024,a=1;i.length>o&&(a=Math.ceil(i.length/o));let c=i.length%o;c===0&&(c=o);let l;a===1?l=(await this.getS3Auth(s,1,2)).presignedUrls:l=(await this.getS3PreSignedUrls(s,1,a+1)).presignedUrls;for(let d=1;d<=a;d++){let u=(d-1)*o,f=d===a?c:o,h=l[String(d)];if(!h)throw new Error(`[123Pan] \u7F3A\u5C11\u7B2C ${d} \u5206\u7247\u7684\u4E0A\u4F20 URL`);let g=i.subarray(u,u+f),p=await fetch(h,{method:"PUT",body:g});if(p.status!==200){let I=await p.text().catch(()=>"");throw new Error(`[123Pan] \u4E0A\u4F20\u7B2C ${d}/${a} \u5206\u7247\u5931\u8D25\uFF1AHTTP ${p.status} ${I}`)}}await this.completeS3(s,i.length,a>1)}}});function zJ(r){return Buffer.from(JSON.stringify(r),"utf8").toString("base64")}function _8(r){let e=JSON.parse(Buffer.from(r,"base64").toString("utf8"));if(!e||!e.bucket||!e.key||!e.uploadId)throw new Error("[123Pan] invalid upload session");return e}function F8(r){return{AccessKeyId:"",SecretAccessKey:"",SessionToken:"",Bucket:r.bucket,Key:r.key,UploadId:r.uploadId,FileId:r.fileId,StorageNode:r.storageNode,EndPoint:"",Reuse:!1}}function P8(r){let e=r.Type===1;return{name:r.FileName,size:r.Size||0,is_dir:e,modified:r.UpdateAt?new Date(r.UpdateAt).toISOString():new Date().toISOString(),sign:String(r.FileId),type:X(r.FileName,e),thumb:"",raw_url:""}}var vp,T8=J(()=>{"use strict";Be();He();Q8();vp=class{client;addition;pathIdCache=new Map;budget={used:0,limit:45};constructor(e,t){this.addition=e,this.client=new bp(e,t)}async init(){await this.client.login()}async resolveFolderId(e){let t=this.client.getRootId(),i="/"+String(e||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${t}`)return t;let n=i.split("/").filter(Boolean),s=0,o=t,a="";for(let c=0;c{try{return decodeURIComponent(l)}catch{return l}})(),f=(await this.client.getFiles(o,{findName:d,findIsDir:!0,budget:this.budget})).find(h=>h.Type===1&&(h.FileName===l||h.FileName===d||String(h.FileId)===l||String(h.FileId)===d));if(!f)throw new Error(`folder not found: ${l}`);o=String(f.FileId),a="/"+n.slice(0,c+1).join("/"),this.pathIdCache.set(a,o)}return o}async ensureFolderId(e){let t=this.client.getRootId(),i="/"+String(e||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${t}`)return t;let n=i.split("/").filter(Boolean),s=t,o="";for(let a=0;a{try{return decodeURIComponent(c)}catch{return c}})();o="/"+n.slice(0,a+1).join("/");let d=this.pathIdCache.get(o);if(d===void 0){let u=await this.client.getFiles(s,{findName:l,findIsDir:!0,budget:this.budget}),f=u.find(h=>h.Type===1&&(h.FileName===c||h.FileName===l));if(f)d=String(f.FileId);else{try{let h=await this.client.mkdir(s,l);h&&(d=h)}catch{}if(d===void 0){if(u=await this.client.getFiles(s,{findName:l,findIsDir:!0,budget:this.budget}),f=u.find(h=>h.Type===1&&h.FileName===l),!f)throw new Error(`[123Pan] \u81EA\u52A8\u521B\u5EFA\u76EE\u5F55\u5931\u8D25: ${c}`);d=String(f.FileId)}}this.pathIdCache.set(o,d)}s=d}return s}async resolveFile(e){let t=String(e||"").split("/").filter(Boolean);if(t.length===0)throw new Error("invalid path");let i=t[t.length-1],n=(()=>{try{return decodeURIComponent(i)}catch{return i}})(),s="/"+t.slice(0,t.length-1).join("/"),o=await this.resolveFolderId(s),c=(await this.client.getFiles(o,{findName:n,budget:this.budget})).find(l=>String(l.FileId)===i||String(l.FileId)===n||l.FileName===i||l.FileName===n);if(!c)throw new Error(`file not found: ${i}`);return{file:c,parentId:o,name:i}}async list(e,t){this.budget.used=0;let i=await this.resolveFolderId(t),s=(await this.client.getFiles(i,{budget:this.budget})).map(P8);return de(s,this.addition.order_by||"file_name",this.addition.order_direction)}async get(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean);if(i.length===0||i[i.length-1]===this.client.getRootId()){let o=this.client.getRootId();return{name:o,size:0,is_dir:!0,modified:new Date().toISOString(),sign:o,type:1,raw_url:""}}let{file:n}=await this.resolveFile(t),s=P8(n);if(n.Type!==1)try{s.raw_url=await this.client.getDownloadLink(n),s.raw_url||(s.raw_url_error="123 \u7F51\u76D8\u672A\u8FD4\u56DE\u4E0B\u8F7D\u94FE\u63A5\uFF08DownloadUrl \u4E3A\u7A7A\uFF09\u3002\u5E38\u89C1\u539F\u56E0\uFF1Aaccess_token/cookie \u5931\u6548\uFF0C\u6216\u8BE5\u6587\u4EF6\u5DF2\u5220\u9664/\u88AB\u9650\u5236\u4E0B\u8F7D\u3002\u8BF7\u5230\u7BA1\u7406\u540E\u53F0\u66F4\u65B0 access_token \u540E\u91CD\u8BD5\u3002")}catch(o){s.raw_url_error=`123 \u7F51\u76D8\u83B7\u53D6\u4E0B\u8F7D\u94FE\u63A5\u5931\u8D25\uFF1A${o?.message||String(o)}\u3002`+(String(o?.message||"").includes("\u767B\u5F55\u5931\u8D25")?"\u5F53\u524D\u90E8\u7F72\u51FA\u53E3 IP \u53EF\u80FD\u88AB 123 \u98CE\u63A7\uFF0C\u8BF7\u914D\u7F6E\u6709\u6548\u7684 access_token\uFF08\u6D4F\u89C8\u5668\u767B\u5F55 123 \u7F51\u76D8\u540E\u590D\u5236 Bearer \u4EE4\u724C\uFF09\u3002":"\u8BF7\u68C0\u67E5 access_token/cookie \u662F\u5426\u6709\u6548\uFF0C\u6216\u5728 123 \u7F51\u76D8\u7F51\u9875\u7AEF\u786E\u8BA4\u8BE5\u6587\u4EF6\u53EF\u4E0B\u8F7D\u3002"),console.warn(`[123Pan] getDownloadLink warning for ${n.FileName}:`,o.message)}else s.raw_url_error="\u8BE5\u6761\u76EE\u662F\u6587\u4EF6\u5939\uFF0C\u4E0D\u53EF\u4F5C\u4E3A\u6587\u4EF6\u4E0B\u8F7D\u3002";return s}async mkdir(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFolderId(s);await this.client.mkdir(o,n)}async rename(e,t,i){this.budget.used=0;let{file:n}=await this.resolveFile(t);await this.client.rename(String(n.FileId),i)}async remove(e,t,i){this.budget.used=0;let{file:n}=await this.resolveFile(t);await this.client.remove(String(n.FileId),n)}async move(e,t,i,n,s){this.budget.used=0;let{file:o}=await this.resolveFile(n),a=String(t).split("/").filter(Boolean),c=await this.resolveFolderId("/"+a.join("/"));await this.client.move([String(o.FileId)],c)}async copy(){throw new Error("[123Pan] Copy is not supported by 123 Cloud Drive API")}async put(e,t,i){this.budget.used=0;let n=String(t||"").split("/").filter(Boolean);if(n.length===0)throw new Error("invalid upload path");let s=n[n.length-1],o=(()=>{try{return decodeURIComponent(s)}catch{return s}})(),a="/"+n.slice(0,n.length-1).join("/"),c=await this.ensureFolderId(a);await this.client.uploadFile(c,o,i)}async createUploadSession(e,t,i,n,s){this.budget.used=0;let o=await this.ensureFolderId(t||"/"),a=await this.client.createUpload(i,o,n,s||""),c=16*1024*1024;if(a.Reuse||a.Key==="")return{reuse:!0,partCount:0,chunkSize:c,session:""};let l=Math.max(1,Math.ceil(n/c)),d=zJ({bucket:a.Bucket,key:a.Key,uploadId:a.UploadId,fileId:a.FileId,storageNode:a.StorageNode,size:n,partCount:l,chunkSize:c});return{reuse:!1,partCount:l,chunkSize:c,session:d}}async uploadPart(e,t,i){this.budget.used=0;let n=_8(e),s=await this.client.getPartUploadUrl(F8(n),t,n.partCount),o=await fetch(s,{method:"PUT",body:i});if(o.status!==200){let a=await o.text().catch(()=>"");throw new Error(`[123Pan] \u4E0A\u4F20\u7B2C ${t}/${n.partCount} \u5206\u7247\u5931\u8D25\uFF1AHTTP ${o.status} ${a}`)}}async completeUploadSession(e){this.budget.used=0;let t=_8(e);await this.client.completeUpload(F8(t),t.size,t.partCount>1)}}});function YJ(r){return new Promise(e=>setTimeout(e,r))}function sE(r){if(!r)return r;try{let e=new URL(r);return e.searchParams.delete("access_token"),e.toString()}catch{return r}}function Kl(r){let e={...r||{}},t=(i,n)=>i==null||i===""?n:typeof i=="boolean"?i:String(i).toLowerCase()==="true";return e.use_online_api=t(e.use_online_api,!0),e.api_url_address=e.api_url_address||"https://api.oplist.org/baiduyun/renewapi",e.download_api=e.download_api||"official",e.custom_crack_ua=e.custom_crack_ua||"netdisk",e.order_by=e.order_by||"name",e.order_direction=e.order_direction||"asc",e.upload_thread=e.upload_thread||"3",e.upload_api=e.upload_api||Ll,e.use_dynamic_upload_api=t(e.use_dynamic_upload_api,!0),e.custom_upload_part_size=e.custom_upload_part_size||0,e.low_bandwith_upload_mode=t(e.low_bandwith_upload_mode,!1),e.only_list_video_file=t(e.only_list_video_file,!1),e}function R8(r,e,t){r.local_mtime=String(t),r.local_ctime=String(e)}var JJ,U8,Ml,rE,iE,nE,jJ,Ll,$J,nf,oE,N8,VJ,kp,Hl,aE=J(()=>{"use strict";JJ="https://openapi.baidu.com/oauth/2.0/token",U8="https://pan.baidu.com/rest/2.0",Ml=4*1024*1024,rE=16*1024*1024,iE=32*1024*1024,nE=2048,jJ=1*1024*1024,Ll="https://d.pcs.baidu.com",$J=60*1e3,nf=3,oE=1e3,N8=5e3,VJ=new Set([111,-6,20016]);kp=class r{addition;accessToken="";onTokenUpdate;constructor(e,t){this.addition=Kl(e),this.onTokenUpdate=t,this.addition.access_token&&(this.accessToken=this.addition.access_token)}static apiUA="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/425.6.30";async refreshToken(){let e=this.addition;if(e.use_online_api&&e.api_url_address){let s=new URL(e.api_url_address);s.searchParams.set("refresh_ui",e.refresh_token),s.searchParams.set("server_use","true"),s.searchParams.set("driver_txt","baiduyun_go");let o=await fetch(s.toString(),{headers:{"User-Agent":r.apiUA}}),a,c=await o.text();try{a=JSON.parse(c)}catch{throw new Error(`\u5728\u7EBF API \u5237\u65B0\u5931\u8D25 (HTTP ${o.status})\uFF1A${c.slice(0,300)||"\u975E JSON \u54CD\u5E94"}\u3002\u8BF7\u786E\u8BA4 refresh_token \u662F\u901A\u8FC7 https://api.oplist.org/ \u83B7\u53D6\u7684\u6709\u6548\u4EE4\u724C\u3002`)}if(!a.refresh_token||!a.access_token)throw new Error(a.text||(o.status!==200?`\u5728\u7EBF API \u8FD4\u56DE HTTP ${o.status}`:"empty token returned from official API, a wrong refresh token may have been used"));this.accessToken=a.access_token,e.refresh_token=a.refresh_token,e.access_token=a.access_token,this.onTokenUpdate?.({access_token:a.access_token,refresh_token:a.refresh_token});return}if(!e.client_id||!e.client_secret)throw new Error("empty ClientID or ClientSecret");let t=new URL(JJ);t.searchParams.set("grant_type","refresh_token"),t.searchParams.set("refresh_token",e.refresh_token),t.searchParams.set("client_id",e.client_id),t.searchParams.set("client_secret",e.client_secret);let n=await(await fetch(t.toString())).json();if(n.error)throw new Error(`${n.error}: ${n.error_description||""}`);if(!n.refresh_token)throw new Error("empty refresh token returned from OAuth");this.accessToken=n.access_token||"",e.refresh_token=n.refresh_token,e.access_token=n.access_token||"",this.onTokenUpdate?.({access_token:n.access_token||"",refresh_token:n.refresh_token})}async login(){this.accessToken||await this.refreshToken()}async ensureToken(){this.accessToken||await this.refreshToken()}async request(e,t,i,n){await this.ensureToken();let s=async()=>{let a=new URL(e);a.searchParams.set("access_token",this.accessToken);for(let[g,p]of Object.entries(i||{}))a.searchParams.set(g,p);let c={"User-Agent":r.apiUA,Accept:"application/json"},l={method:t,headers:c};if(n&&t==="POST"){let g=new URLSearchParams;for(let[p,I]of Object.entries(n))g.set(p,I);c["Content-Type"]="application/x-www-form-urlencoded",l.body=g.toString()}let d=await fetch(a.toString(),l),u=await d.text(),f;try{f=JSON.parse(u)}catch{throw new Error(`req: [${e}] invalid JSON response, status ${d.status}`)}let h=typeof f.errno=="number"?f.errno:0;if(h!==0){if(VJ.has(h)&&await this.refreshToken(),h===31023&&this.addition.download_api==="crack_video")return f;let g=`req: [${e}] ,errno: ${h}, refer to https://pan.baidu.com/union/doc/`;throw h===31023?new Error(`${g} \u767E\u5EA6\u7F51\u76D8\u98CE\u63A7 (Trigger security policy: Please try again later) \u2014 \u89E6\u53D1\u539F\u56E0\u901A\u5E38\u662F\uFF1A\u2460 \u5F53\u524D\u90E8\u7F72\u73AF\u5883\u7684\u51FA\u53E3 IP\uFF08\u5982 Cloudflare Workers \u6570\u636E\u4E2D\u5FC3 IP\uFF09\u88AB\u767E\u5EA6\u5B89\u5168\u7B56\u7565\u62E6\u622A\uFF1B\u2461 refresh_token \u65E0\u6548\u6216\u4ECE\u975E\u5B98\u65B9\u6E20\u9053\u83B7\u53D6\uFF0C\u5BFC\u81F4\u8D26\u53F7\u88AB\u98CE\u63A7\u3002\u8BF7\u786E\u8BA4\uFF1Arefresh_token \u5FC5\u987B\u901A\u8FC7 https://api.oplist.org/ \u83B7\u53D6\uFF08\u672C\u9A71\u52A8\u9ED8\u8BA4\u5DF2\u5F00\u542F"\u4F7F\u7528\u5728\u7EBF API"\uFF09\uFF1B\u98CE\u63A7\u4E3A\u4E34\u65F6\u6027\uFF0C\u7B49\u5F85\u6570\u5206\u949F\u81F3\u6570\u5C0F\u65F6\u540E\u81EA\u52A8\u89E3\u9664\uFF1B\u957F\u671F\u4F7F\u7528\u8BF7\u5C06\u540E\u7AEF\u90E8\u7F72\u5230\u5883\u5185\u670D\u52A1\u5668\uFF08\u6216\u914D\u7F6E HTTPS_PROXY \u5883\u5185\u4EE3\u7406\uFF09\u3002`):new Error(g)}return f},o;for(let a=0;ac.abort(),s>0?s:$J);try{let u=await(await fetch(o.toString(),{method:"POST",body:a,signal:c.signal})).text(),f=u.toLowerCase();if(f.includes("uploadid")&&(f.includes("invalid")||f.includes("expired")||f.includes("not found")))throw new Hl;let h;try{h=JSON.parse(u)}catch{h={}}let g=h?.error_code??0,p=h?.errno??0;if(g!==0||p!==0)throw new Error(`error uploading to baidu, response=${u}`)}finally{clearTimeout(l)}}getUploadUrl(e,t){let i=this.addition;return!i.use_dynamic_upload_api||!t,i.upload_api||Ll}async requestForUploadUrl(e,t){let i=await this.request("https://d.pcs.baidu.com/rest/2.0/pcs/file","GET",{method:"locateupload",appid:"250528",path:e,uploadid:t,upload_version:"2.0"}),n="";if(i.servers&&i.servers.length>0?n=i.servers[0].server:i.bak_servers&&i.bak_servers.length>0&&(n=i.bak_servers[0].server),!n)throw new Error("upload URL is empty");return n}getSliceSize(e,t){let i=this.addition,n=i.custom_upload_part_size||0;if(t===0)return n!==0&&console.warn("[baidu_netdisk] CustomUploadPartSize is not supported for non-vip user, use DefaultSliceSize"),e>nE*Ml&&console.warn(`[baidu_netdisk] File size(${e}) is too large, may cause upload failure`),Ml;if(n!==0)return nrE?(console.warn(`[baidu_netdisk] CustomUploadPartSize(${n}) is greater than VipSliceSize, use VipSliceSize`),rE):t===2&&n>iE?(console.warn(`[baidu_netdisk] CustomUploadPartSize(${n}) is greater than SVipSliceSize, use SVipSliceSize`),iE):n;let s=Ml;if(t===1&&(s=rE),t===2&&(s=iE),i.low_bandwith_upload_mode){let o=Ml;for(;o<=s;){if(e<=nE*o)return o;o+=jJ}}return e>nE*s&&console.warn(`[baidu_netdisk] File size(${e}) is too large, may cause upload failure`),s}async quota(){let e=await this.request("https://pan.baidu.com/api/quota","GET");return{total:e.total||0,used:e.used||0}}},Hl=class extends Error{constructor(){super("uploadid expired"),this.name="ErrUploadIDExpired"}}});function XJ(r){return new Promise(e=>setTimeout(e,r))}function q8(r){let e=r.server_filename||sf(r.path),t=r.server_ctime||r.ctime||0,i=r.server_mtime||r.mtime||0,n=r.isdir===1;return{name:e,size:r.size||0,is_dir:n,created:t?new Date(t*1e3).toISOString():void 0,modified:i?new Date(i*1e3).toISOString():new Date().toISOString(),sign:String(r.fs_id),type:X(e,n),thumb:r.thumbs?.url3||"",raw_url:""}}function sf(r){let e=String(r||"").split("/");return e[e.length-1]||""}function O8(r){let e=r.lastIndexOf("/");return e<=0?"/":r.slice(0,e)}var WJ,Sp,M8=J(()=>{"use strict";Be();He();Gr();aE();aE();WJ=new Error("empty files are not allowed by baidu netdisk");Sp=class{client;addition;uploadThread=3;vipType=0;pathCache=new Map;constructor(e,t){this.addition=Kl(e),this.client=new kp(this.addition,t)}async init(){let e=this.addition,t=parseInt(e.upload_thread||"3",10);if(t<1&&(t=1),t>32&&(t=32),this.uploadThread=t,!this.client.accessToken)throw new Error("\u767E\u5EA6\u7F51\u76D8\u7F3A\u5C11\u8BBF\u95EE\u4EE4\u724C access_token\uFF08\u5FC5\u586B\uFF09\uFF1A\u8BF7\u901A\u8FC7 https://api.oplist.org/ \u83B7\u53D6\u540E\u586B\u5199\u3002");this.vipType=await this.client.uinfo()}baiduPath(e){let t="/"+String(e||"").replace(/\/+/g,"/");return t==="/"?"/":t.replace(/\/$/,"")}async list(e,t){let i=await this.client.getFiles(this.baiduPath(t)),n=i.map(q8);for(let s of i)this.pathCache.set(s.path,{fsId:s.fs_id,parent:O8(s.path)});return de(n,this.addition.order_by||"name",this.addition.order_direction)}async get(e,t){let i=this.baiduPath(t);if(i==="/")return{name:"/",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""};let n=O8(i),s=sf(i),o=(()=>{try{return decodeURIComponent(s)}catch{return s}})(),c=(await this.client.getFiles(n)).find(d=>d.server_filename===s||d.server_filename===o||d.path===i||String(d.fs_id)===s);if(!c)throw new Error(`file not found: ${s}`);this.pathCache.set(c.path,{fsId:c.fs_id,parent:n});let l=q8(c);if(c.isdir!==1)try{let d=await this.getDownloadLink(c);l.raw_url=d.url,l.raw_url_headers=d.headers}catch(d){console.warn(`[baidu_netdisk] getDownloadLink warning for ${c.server_filename}:`,d.message)}return l}async getDownloadLink(e){let t=this.addition.download_api||"official";return t==="crack"?this.client.getCrackLink(e.path):t==="crack_video"?this.client.getCrackVideoLink(e.path,e.fs_id):this.client.getOfficialLink(e.fs_id)}async mkdir(e,t){await this.client.create(this.baiduPath(t),0,1,"","",0,0)}async rename(e,t,i){await this.client.manage("rename",[{path:this.baiduPath(t),newname:i}])}async remove(e,t,i){await this.client.manage("delete",[this.baiduPath(t)])}async move(e,t,i,n,s){let o=i[0]||sf(n),a=this.baiduPath(t);await this.client.manage("move",[{path:this.baiduPath(n),dest:a,newname:o}])}async copy(e,t,i,n,s){let o=i[0]||sf(n),a=this.baiduPath(t);await this.client.manage("copy",[{path:this.baiduPath(n),dest:a,newname:o}])}async put(e,t,i){if(i.length<1)throw WJ;let n=i.length,s=this.baiduPath(t),o=sf(s),a=Math.floor(Date.now()/1e3),c=a,l=a,d=$t(i),u=JSON.stringify([d]);try{await this.client.create(s,n,0,"",u,c,l);return}catch{}let f=this.client.getSliceSize(n,this.vipType),h=Math.max(1,Math.ceil(n/f)),g=n%f||f,p=[];for(let w=0;w{for(;;){let T=Q++;if(T>=C.length)return;let D=C[T];if(D<0)continue;let b=D*f,_=D+1===h?g:f,v=i.subarray(b,b+_),E={method:"upload",access_token:this.client.accessToken,type:"tmpfile",path:s,uploadid:m.uploadid,partseq:String(D)},P=!1;for(let N=0;NU())),x)throw new Error("upload slice failed")}catch(T){if(T instanceof Hl){let D=await this.client.precreate(s,n,I,"","",l,c);if(D.return_type===2&&D.info)return;m=D;continue}throw T}await this.client.create(s,n,0,m.uploadid,I,c,l);return}throw new Error("upload failed after retries")}}}});function rj(){let r=Math.floor(Date.now()/1e3),e=Math.floor(9e6*Math.random()+1e6);return`3${r}${e}`}function L8(r){return"["+r.map(e=>`{"fsid":${e}}`).join(",")+"]"}var _p,Wi,Dp,ZJ,ej,cE,of,tj,Qp,H8=J(()=>{"use strict";Gr();_p="https://photo.baidu.com/youai",Wi=_p+"/album/v1",Dp=_p+"/file/v1",ZJ=_p+"/file/v2",ej=_p+"/user/v1",cE="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",of=4*1024*1024,tj=256*1024;Qp=class{cookie;uk="";bdstoken="";showType;constructor(e){this.cookie=e.cookie||"",this.showType=e.show_type||"root"}async init(){if(!this.cookie)throw new Error("[BaiduPhoto] cookie is required");let e=await this.uInfo();this.uk=e.youa_id||"",this.bdstoken=await this.getBDStoken()}async request(e,t,i={}){let n=new URL(t);if(i.query)for(let[c,l]of Object.entries(i.query))l!==void 0&&n.searchParams.set(c,l);let s={Cookie:this.cookie,"User-Agent":cE,Referer:"https://photo.baidu.com/"},o;return i.form&&(s["Content-Type"]="application/x-www-form-urlencoded",o=new URLSearchParams(i.form).toString()),await fetch(n.toString(),{method:e,headers:s,body:o,redirect:i.redirect??"follow"})}async requestJson(e,t,i={}){let s=await(await this.request(e,t,i)).json().catch(()=>({})),o=s.errno;if(o!==void 0&&o!==0)throw new Error(`[BaiduPhoto] errno: ${o}, refer to https://photo.baidu.com/union/doc`);return s}async uInfo(){return this.requestJson(ej+"/getuinfo","GET")}async getBDStoken(){return(await this.requestJson("GET","https://pan.baidu.com/api/gettemplatevariable",{query:{fields:'["bdstoken","token","uk"]'}}))?.result?.bdstoken||""}async getAllFile(){let e=[],t="";for(;;){let i=await this.requestJson("GET",Dp+"/list",{query:{need_thumbnail:"1",need_filter_hidden:"0",cursor:t}});if(e.push(...i.list||[]),i.has_more!==1)break;t=i.cursor||""}return e}async getAllAlbum(){let e=[],t="";for(;;){let i=await this.requestJson("GET",Wi+"/list",{query:{need_amount:"1",limit:"100",cursor:t}});if(e.push(...i.list||[]),i.has_more!==1)break;t=i.cursor||""}return e}async getAllAlbumFile(e,t){let i=[],n="";for(;;){let s=await this.requestJson("GET",Wi+"/listfile",{query:{album_id:e,need_amount:"1",limit:"1000",passwd:"",cursor:n}});if(i.push(...s.list||[]),s.has_more!==1)break;n=s.cursor||""}return i}async getAlbumDetail(e){return this.requestJson("GET",Wi+"/detail",{query:{album_id:e}})}async linkFile(e){let t=await this.requestJson("GET",ZJ+"/download",{query:{fsid:String(e)}});if(!t.dlink)throw new Error("[BaiduPhoto] \u83B7\u53D6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25");return t.dlink}async linkAlbumFile(e,t,i,n){let o=(await this.request("HEAD",Wi+"/download",{query:{fsid:String(e),album_id:t,tid:String(i),uk:String(n)},redirect:"manual"})).headers.get("location");if(!o)throw new Error("[BaiduPhoto] \u76F8\u518C\u6587\u4EF6\u672A\u8FD4\u56DE 302 \u8DF3\u8F6C");return o}downloadHeaders(){return{Referer:"https://photo.baidu.com/","User-Agent":cE}}async createAlbum(e){let t=await this.requestJson("POST",Wi+"/create",{query:{title:e,tid:rj(),source:"0"}});if(!t.album_id)throw new Error("[BaiduPhoto] \u521B\u5EFA\u76F8\u518C\u5931\u8D25");return this.getAlbumDetail(t.album_id)}async joinAlbum(e){let t=await this.requestJson("GET",Wi+"/querypcode",{query:{pcode:e,web:"1"}});if(!t.pdata?.invite_code)throw new Error("[BaiduPhoto] \u9080\u8BF7\u7801\u65E0\u6548");let i=await this.requestJson("GET",Wi+"/join",{query:{invite_code:t.pdata.invite_code}});if(!i.album_id)throw new Error("[BaiduPhoto] \u52A0\u5165\u76F8\u518C\u5931\u8D25");return this.getAlbumDetail(i.album_id)}async setAlbumName(e,t,i){await this.requestJson("POST",Wi+"/settitle",{form:{title:i,album_id:e,tid:String(t)}})}async deleteAlbum(e,t,i){await this.requestJson("POST",Wi+"/delete",{form:{album_id:e,tid:String(t),delete_origin_image:i?"1":"0"}})}async deleteFile(e){await this.requestJson("GET",Dp+"/delete",{query:{fsid_list:`[${e}]`}})}async deleteAlbumFile(e,t,i,n,s){await this.requestJson("POST",Wi+"/delfile",{form:{album_id:t,tid:String(i),list:`[{"fsid":${e},"uk":${n}}]`,del_origin:s?"1":"0"}})}async addAlbumFile(e,t,i){await this.requestJson("GET",Wi+"/addfile",{query:{album_id:e,tid:String(t),list:L8([i])}})}async copyAlbumFile(e,t,i,n){let s=await this.requestJson("POST",Wi+"/copyfile",{form:{album_id:t,tid:String(i),uk:String(n),list:L8([e])}});if(!s.list||s.list.length===0)throw new Error("[BaiduPhoto] \u590D\u5236\u76F8\u518C\u6587\u4EF6\u5931\u8D25");return s.list[0].fsid}async upload(e,t,i){if(i.length===0)throw new Error("[BaiduPhoto] file size cannot be zero");let n=$t(i),s=$t(i.subarray(0,Math.min(tj,i.length))),o=[];for(let u=0;u0?r.thumburl[0]:"",raw_url:""}}function z8(r){return{name:ra(r.path),size:r.size,is_dir:!1,modified:dE(r.mtime),sign:`albumfile:${r.fsid}|${r.album_id}|${r.tid}|${r.uk}`,type:X(ra(r.path),!1),thumb:r.thumburl&&r.thumburl.length>0?r.thumburl[0]:"",raw_url:""}}function lE(r){if(r.startsWith("albumfile:")){let[e,t,i,n]=r.slice(10).split("|");return{kind:"albumfile",fsid:Number(e),albumId:t,tid:Number(i),uk:Number(n)}}if(r.startsWith("album:")){let[e,t]=r.slice(6).split("|");return{kind:"album",fsid:0,albumId:e,tid:Number(t),uk:0}}return{kind:"file",fsid:Number(r.slice(5)),albumId:"",tid:0,uk:0}}var Fp,J8=J(()=>{"use strict";Be();H8();Fp=class{client;showType;albumCache=new Map;constructor(e){this.client=new Qp(e),this.showType=e.show_type||"root"}async init(){await this.client.init()}async list(e,t){let i=t.split("/").filter(Boolean).join("/");if(!i){let o=[];if(this.showType!=="root_only_file"){let a=await this.client.getAllAlbum();for(let c of a)this.albumCache.set(c.title,c),o.push(K8(c))}if(this.showType!=="root_only_album"){let a=await this.client.getAllFile();o.push(...a.map(G8))}return o}let n=await this.findAlbum(i);if(!n)throw new Error(`[BaiduPhoto] \u76F8\u518C '${i}' \u672A\u627E\u5230`);return(await this.client.getAllAlbumFile(n.album_id,n.tid)).map(z8)}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s=i.slice(0,-1).join("/");if(s){let o=await this.findAlbum(s);if(o){let c=(await this.client.getAllAlbumFile(o.album_id,o.tid)).find(l=>ra(l.path)===n);if(c){let l=z8(c);try{l.raw_url=await this.client.linkAlbumFile(c.fsid,c.album_id,c.tid,c.uk),l.raw_url_headers=this.client.downloadHeaders()}catch{l.raw_url_error="[BaiduPhoto] failed to resolve download url"}return l}}}else{if(this.showType!=="root_only_file"){let a=(await this.client.getAllAlbum()).find(c=>c.title===n);if(a)return K8(a)}if(this.showType!=="root_only_album"){let a=(await this.client.getAllFile()).find(c=>ra(c.path)===n);if(a){let c=G8(a);try{c.raw_url=await this.client.linkFile(a.fsid),c.raw_url_headers=this.client.downloadHeaders()}catch{c.raw_url_error="[BaiduPhoto] failed to resolve download url"}return c}}}return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){if(t.split("/").filter(Boolean).join("/"))throw new Error("[BaiduPhoto] \u76F8\u518C\u4E0D\u652F\u6301\u521B\u5EFA\u5B50\u76EE\u5F55");let s=await this.client.createAlbum("new_album");this.albumCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);if(n.kind!=="album")throw new Error("[BaiduPhoto] \u4EC5\u652F\u6301\u91CD\u547D\u540D\u76F8\u518C");await this.client.setAlbumName(n.albumId,n.tid,i),this.albumCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t);n.kind==="album"?await this.client.deleteAlbum(n.albumId,n.tid,!1):n.kind==="file"?await this.client.deleteFile(n.fsid):await this.client.deleteAlbumFile(n.fsid,n.albumId,n.tid,n.uk,!1),this.albumCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n),a=t.split("/").filter(Boolean).join("/");if(o.kind==="albumfile"){if(!a)await this.client.copyAlbumFile(o.fsid,o.albumId,o.tid,o.uk);else{let c=await this.findAlbum(a);if(!c)throw new Error(`[BaiduPhoto] \u76F8\u518C '${a}' \u672A\u627E\u5230`);let l=await this.client.copyAlbumFile(o.fsid,o.albumId,o.tid,o.uk);await this.client.addAlbumFile(c.album_id,c.tid,l)}await this.client.deleteAlbumFile(o.fsid,o.albumId,o.tid,o.uk,!1);return}throw new Error("[BaiduPhoto] \u4EC5\u76F8\u518C\u6587\u4EF6\u652F\u6301\u79FB\u52A8")}async copy(e,t,i,n,s){let o=await this.resolveEntry(n),a=t.split("/").filter(Boolean).join("/");if(o.kind==="file"&&a){let c=await this.findAlbum(a);if(!c)throw new Error(`[BaiduPhoto] \u76F8\u518C '${a}' \u672A\u627E\u5230`);await this.client.addAlbumFile(c.album_id,c.tid,o.fsid);return}if(o.kind==="albumfile"){let c=await this.client.copyAlbumFile(o.fsid,o.albumId,o.tid,o.uk);if(a){let l=await this.findAlbum(a);if(!l)throw new Error(`[BaiduPhoto] \u76F8\u518C '${a}' \u672A\u627E\u5230`);await this.client.addAlbumFile(l.album_id,l.tid,c)}return}throw new Error("[BaiduPhoto] \u4E0D\u652F\u6301\u8BE5\u590D\u5236\u64CD\u4F5C")}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"file",o=n.join("/"),a=null;if(o&&(a=await this.findAlbum(o),!a))throw new Error(`[BaiduPhoto] \u76F8\u518C '${o}' \u672A\u627E\u5230`);await this.client.upload(a,s,new Uint8Array(i)),this.albumCache.clear()}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n=t.slice(0,-1).join("/");if(n){let s=await this.findAlbum(n);if(s){let a=(await this.client.getAllAlbumFile(s.album_id,s.tid)).find(c=>ra(c.path)===i);if(a)return lE(`albumfile:${a.fsid}|${a.album_id}|${a.tid}|${a.uk}`)}}else{if(this.showType!=="root_only_file"){let o=(await this.client.getAllAlbum()).find(a=>a.title===i);if(o)return lE(`album:${o.album_id}|${o.tid}`)}if(this.showType!=="root_only_album"){let o=(await this.client.getAllFile()).find(a=>ra(a.path)===i);if(o)return lE(`file:${o.fsid}`)}}throw new Error(`[BaiduPhoto] '${i}' not found`)}async findAlbum(e){if(this.albumCache.has(e))return this.albumCache.get(e);let t=await this.client.getAllAlbum();for(let i of t)this.albumCache.set(i.title,i);return this.albumCache.get(e)||null}}});function gj(r){return r===99||String(r).startsWith("401")}var bn,ij,nj,sj,oj,aj,j8,cj,lj,dj,uj,fj,Aj,hj,af,Pp,$8=J(()=>{"use strict";bn="https://proapi.115.com",ij="https://passportapi.115.com",nj=bn+"/open/upload/get_token",sj=bn+"/open/upload/init",oj=bn+"/open/folder/add",aj=bn+"/open/ufile/files",j8=bn+"/open/folder/get_info",cj=bn+"/open/ufile/copy",lj=bn+"/open/ufile/move",dj=bn+"/open/ufile/downurl",uj=bn+"/open/ufile/update",fj=bn+"/open/ufile/delete",Aj=bn+"/open/user/info",hj=ij+"/open/refreshToken";af=430004,Pp=class r{addition;accessToken="";refreshTokenValue="";onTokenUpdate;rateLimitMs=0;lastRequestAt=0;constructor(e,t){this.addition=e,this.accessToken=e.access_token||"",this.refreshTokenValue=e.refresh_token||"",this.onTokenUpdate=t;let i=e.limit_rate||0;i>0&&(this.rateLimitMs=1e3/i)}async waitRateLimit(){if(this.rateLimitMs<=0)return;let e=Date.now(),t=this.lastRequestAt+this.rateLimitMs-e;t>0&&await new Promise(i=>setTimeout(i,t)),this.lastRequestAt=Date.now()}async fetchWithRetry(e,t){let i;for(let n=0;n<3;n++)try{let s=new AbortController,o=setTimeout(()=>s.abort(),2e4);try{return await fetch(e,{...t,signal:s.signal})}finally{clearTimeout(o)}}catch(s){i=s,n<2&&await new Promise(o=>setTimeout(o,500*(n+1)))}throw i}static describeNetError(e){let t=e,i=t?.cause?.code||t?.cause?.cause?.code,n=t?.cause?.message||t?.cause?.cause?.message;return i?`${t?.message||"fetch failed"}\uFF08${i}\uFF09`:n?`${t?.message||"fetch failed"}\uFF08${n}\uFF09`:t?.message||String(e)}async refreshToken(){if(!this.refreshTokenValue)throw new Error("115 \u7F51\u76D8\u7F3A\u5C11 refresh_token\uFF08\u5FC5\u586B\uFF09");let e=new URLSearchParams;e.set("refresh_token",this.refreshTokenValue);let i=await(await this.fetchWithRetry(hj,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:e.toString()})).json();if(i.code!==0||!i.data?.access_token||!i.data?.refresh_token)throw new Error(`115 \u7F51\u76D8 token \u5237\u65B0\u5931\u8D25\uFF08code ${i.code} ${i.message}\uFF09\uFF1A\u8BF7\u786E\u8BA4 refresh_token \u6709\u6548\u3002`);this.accessToken=i.data.access_token,this.refreshTokenValue=i.data.refresh_token,this.addition.access_token=this.accessToken,this.addition.refresh_token=this.refreshTokenValue,this.onTokenUpdate?.({access_token:this.accessToken,refresh_token:this.refreshTokenValue})}async request(e,t,i,n,s,o=!1){await this.waitRateLimit();let a=async()=>{let d=new URL(e);for(let[I,m]of Object.entries(i||{}))m!==""&&d.searchParams.set(I,m);let u={Accept:"application/json","User-Agent":s||"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/425.6.30"};this.accessToken&&(u.Authorization=`Bearer ${this.accessToken}`);let f={method:t,headers:u};if(n&&t==="POST"){let I=new URLSearchParams;for(let[m,w]of Object.entries(n))w!==""&&I.set(m,w);u["Content-Type"]="application/x-www-form-urlencoded",f.body=I.toString()}let h=await this.fetchWithRetry(d.toString(),f),g=await h.text(),p;try{p=JSON.parse(g)}catch{p={state:!1,code:h.status,message:g.slice(0,200)}}return{body:p,rawText:g}},c;try{({body:c}=await a())}catch(d){throw new Error(r.describeNetError(d))}let l=c?.state;if(l===!1||l===void 0){let d=Number(c?.code??0);if(gj(d)&&!o){await this.refreshToken(),c=(await a()).body;let f=c?.state;if(f!==!1&&f!==void 0)return c;throw new Error(`115 \u7F51\u76D8 API \u9519\u8BEF\uFF08code ${c?.code} ${c?.message}\uFF09`)}if(d===af){let u=new Error("115 object not found");throw u.code=af,u}throw new Error(`115 \u7F51\u76D8 API \u9519\u8BEF\uFF08code ${d} ${c?.message||""}\uFF09`)}return c}async userInfo(){return(await this.request(Aj,"GET"))?.data}async getFiles(e){let t=await this.request(aj,"GET",{cid:e.cid,limit:String(e.limit),offset:String(e.offset),asc:e.asc?"1":"0",o:e.o||"",show_dir:e.showDir?"1":"0",cur:"1"});return{files:t.data||[],count:t.count||0}}async getFolderInfo(e){return(await this.request(j8,"GET",{file_id:e}))?.data}async getFolderInfoByPath(e){return(await this.request(j8,"POST",void 0,{path:e}))?.data}async mkdir(e,t){return(await this.request(oj,"POST",void 0,{pid:e,file_name:t}))?.data}async move(e,t){await this.request(lj,"POST",void 0,{file_ids:e,to_cid:t})}async updateFile(e,t){await this.request(uj,"POST",void 0,{file_id:e,file_name:t})}async copy(e,t){await this.request(cj,"POST",void 0,{pid:e,file_id:t,no_dupli:"1"})}async delFile(e,t){await this.request(fj,"POST",void 0,{file_ids:e,parent_id:t})}async downUrl(e,t){return(await this.request(dj,"POST",void 0,{pick_code:e},t))?.data}async uploadGetToken(){return(await this.request(nj,"GET"))?.data}async uploadInit(e){return(await this.request(sj,"POST",void 0,{file_name:e.fileName,file_size:String(e.fileSize),target:`U_1_${e.target}`,fileid:e.fileId,preid:e.preId,sign_key:e.signKey||"",sign_val:e.signVal||""}))?.data}}});function V8(r){let e=r.fc==="0";return{name:r.fn,size:r.fs||0,is_dir:e,created:r.uppt?new Date(r.uppt*1e3).toISOString():void 0,modified:r.upt?new Date(r.upt*1e3).toISOString():new Date().toISOString(),sign:r.fid,type:X(r.fn,e),thumb:r.thumbnail||r.fco||"",raw_url:""}}function mj(r){let e={...r||{}};return e.order_by=e.order_by||"file_name",e.order_direction=e.order_direction||"asc",e.page_size=e.page_size||200,(e.root_folder_id||e.root_folder_id==="0")&&!e.root_id&&(e.root_id=String(e.root_folder_id)),e}var Tp,pj,Up,Y8=J(()=>{"use strict";Be();He();Gr();$8();Tp="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/425.6.30",pj=45;Up=class r{client;addition;pageSize=200;parentPath="/";fidCache=new Map;budget={used:0,limit:pj};linkCache=new Map;static LINK_TTL_MS=1800*1e3;constructor(e,t){this.addition=mj(e),this.client=new Pp(this.addition,t)}async init(){let t=this.addition.page_size||200;t<=0&&(t=200),t>1150&&(t=1150),this.pageSize=t;try{await this.client.userInfo()}catch(n){if(n?.code===af)throw n;let s=String(n?.message||n);throw s.includes("fetch")||s.includes("ECONN")||s.includes("abort")?new Error(`115 \u7F51\u76D8\u7F51\u7EDC\u8FDE\u63A5\u5931\u8D25\uFF08${s}\uFF09\uFF1Aproapi.115.com \u53EF\u80FD\u65E0\u6CD5\u4ECE\u5F53\u524D\u90E8\u7F72\u73AF\u5883\u8BBF\u95EE\uFF08\u6570\u636E\u4E2D\u5FC3 IP \u53EF\u80FD\u88AB 115 \u62E6\u622A\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u6216\u66F4\u6362\u90E8\u7F72\u73AF\u5883\u3002`):new Error(`115 \u7F51\u76D8 token \u9A8C\u8BC1\u5931\u8D25\uFF1A${s}\u3002\u8BF7\u786E\u8BA4 access_token / refresh_token \u6709\u6548\u3002`)}let i=this.getRootId();if(i!=="0")try{let n=await this.client.getFolderInfo(i);if(n.file_id!=="0"){this.parentPath=`/${n.file_name}`;let s=[...n.paths||[]].reverse();for(let o of s)this.parentPath=`/${o.file_name}${this.parentPath}`}}catch(n){console.warn("[115open] init root path resolve failed:",n.message)}}getRootId(){return(this.addition.root_id||"0").trim()||"0"}reserve(){return this.budget.used>=this.budget.limit?(console.warn(`[115open] \u5DF2\u8FBE Cloudflare subrequest \u9884\u7B97\u4E0A\u9650(${this.budget.limit})\uFF0C\u7ED3\u679C\u5DF2\u622A\u65AD`),!1):(this.budget.used++,!0)}async list(e,t){this.budget.used=0;let i=await this.resolveFolderId(t),n=[],s=0;for(;this.reserve();){let{files:o,count:a}=await this.client.getFiles({cid:i,limit:this.pageSize,offset:s,asc:this.addition.order_direction==="asc",o:this.addition.order_by||"file_name",showDir:!0});for(let c of o)n.push(V8(c)),this.fidCache.set(c.fid,c.fid);if(n.length>=a||o.length===0)break;s+=o.length}return de(n,this.addition.order_by||"file_name",this.addition.order_direction)}async resolveFolderId(e){let t=this.getRootId(),i="/"+String(e||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${t}`)return t;let n=this.fidCache.get(i);if(n)return n;let s=`/${t==="0"?"":t}${i==="/"?"":i}`;try{if(!this.reserve())throw new Error("subrequest budget exceeded");let l=await this.client.getFolderInfoByPath(s);if(l.file_id)return this.fidCache.set(i,l.file_id),l.file_id}catch(l){if(l?.code!==af&&l?.code!==990002)throw l}let o=i.split("/").filter(Boolean),a=t,c="";for(let l of o){let d=(()=>{try{return decodeURIComponent(l)}catch{return l}})();c=`${c}/${l}`;let u=this.fidCache.get(c);if(u){a=u;continue}if(!this.reserve())throw new Error("subrequest budget exceeded");let{files:f}=await this.client.getFiles({cid:a,limit:1e3,offset:0,asc:!0,o:"file_name",showDir:!0}),h=f.find(g=>g.fc==="0"&&(g.fn===l||g.fn===d||g.fid===l));if(!h)throw new Error(`folder not found: ${l}`);a=h.fid,this.fidCache.set(c,a)}return a}async resolveFile(e){let t="/"+String(e||"").split("/").filter(Boolean).join("/"),i=t.split("/").filter(Boolean),n=i.pop()||"";if(!n)throw new Error(`file not found: ${t}`);let s=(()=>{try{return decodeURIComponent(n)}catch{return n}})(),o="/"+i.join("/"),a=await this.resolveFolderId(o),c=0;for(;;){if(!this.reserve())throw new Error("subrequest budget exceeded");let{files:l,count:d}=await this.client.getFiles({cid:a,limit:Math.max(this.pageSize,1e3),offset:c,asc:!0,o:"file_name",showDir:!0}),u=l.find(f=>f.fn===n||f.fn===s||f.fid===n||f.fid===s);if(u)return u;if(l.length===0||c+l.length>=d)break;c+=l.length}throw new Error(`file not found: ${n}`)}async get(e,t){this.budget.used=0;let i="/"+String(t||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${this.getRootId()}`)return{name:this.getRootId(),size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.getRootId(),type:1,raw_url:""};let n=await this.resolveFile(t),s=V8(n);if(n.fc!=="0"&&n.pc)try{let o=`${n.fid}|${Tp}`,a=this.linkCache.get(o);if(a&&a.expire>Date.now())s.raw_url=a.url,s.raw_url_headers={"User-Agent":Tp};else{if(!this.reserve())throw new Error("subrequest budget exceeded");let l=(await this.client.downUrl(n.pc,Tp))[n.fid];l?.url?.url&&(s.raw_url=l.url.url,s.raw_url_headers={"User-Agent":Tp},this.linkCache.set(o,{url:l.url.url,expire:Date.now()+r.LINK_TTL_MS}))}}catch(o){String(o?.message||o).includes("406")?console.warn("[115open] downurl \u914D\u989D\u7528\u5C3D\uFF08406\uFF09\uFF1A\u5DF2\u4F7F\u7528\u7F13\u5B58\u6216\u7A0D\u540E\u91CD\u8BD5"):console.warn(`[115open] downUrl warning for ${n.fn}:`,o.message)}return s}async mkdir(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFolderId(s);if(!this.reserve())throw new Error("subrequest budget exceeded");await this.client.mkdir(o,n)}async rename(e,t,i){this.budget.used=0;let n=await this.resolveFile(t);if(!this.reserve())throw new Error("subrequest budget exceeded");await this.client.updateFile(n.fid,i)}async remove(e,t,i){this.budget.used=0;let n=await this.resolveFile(t);if(!this.reserve())throw new Error("subrequest budget exceeded");await this.client.delFile(n.fid,n.pid||this.getRootId())}async move(e,t,i,n,s){this.budget.used=0;let o=await this.resolveFile(n),a=await this.resolveFolderId(t);if(!this.reserve())throw new Error("subrequest budget exceeded");await this.client.move(o.fid,a)}async copy(e,t,i,n,s){this.budget.used=0;let o=await this.resolveFile(n),a=await this.resolveFolderId(t);if(!this.reserve())throw new Error("subrequest budget exceeded");await this.client.copy(a,o.fid)}async put(e,t,i){if(i.length<1)throw new Error("115 \u7F51\u76D8\u4E0D\u5141\u8BB8\u4E0A\u4F20\u7A7A\u6587\u4EF6");this.budget.used=0;let n=String(t||"").split("/").filter(Boolean),s=n.pop()||"file",o="/"+n.join("/"),c=await this.resolveFolderId(o),l=i.length,d=(await Ho(i)).toUpperCase(),u=Math.min(128*1024,l),f=(await Ho(i.subarray(0,u))).toUpperCase();if(!this.reserve())throw new Error("subrequest budget exceeded");let h=await this.client.uploadInit({fileName:s,fileSize:l,target:c,fileId:d,preId:f});if(h.status===2)return;if([6,7,8].includes(h.status)&&h.sign_check){let p=h.sign_check.split("-"),I=parseInt(p[0],10),m=parseInt(p[1],10);if(Number.isFinite(I)&&Number.isFinite(m)){let w=(await Ho(i.subarray(I,m+1))).toUpperCase();if(!this.reserve())throw new Error("subrequest budget exceeded");if(h=await this.client.uploadInit({fileName:s,fileSize:l,target:c,fileId:d,preId:f,signKey:h.sign_key,signVal:w}),h.status===2)return}}if(!this.reserve())throw new Error("subrequest budget exceeded");let g=await this.client.uploadGetToken();if(!h.bucket||!h.object||!g.endpoint)throw new Error("115 \u4E0A\u4F20\u521D\u59CB\u5316\u5931\u8D25\uFF1A\u7F3A\u5C11 OSS \u4E0A\u4F20\u4FE1\u606F");await this.ossPutObject(g,h,i)}async ossPutObject(e,t,i){let s=`${(e.endpoint.startsWith("http")?e.endpoint:`https://${e.endpoint}`).replace(/\/$/,"")}/${t.object}`,o=Buffer.from(t.callback?.callback||"","utf8").toString("base64"),a=Buffer.from(t.callback?.callback_var||"","utf8").toString("base64"),c=new Date().toUTCString(),l="application/octet-stream",d=`x-oss-callback:${o} -x-oss-callback-var:${a} -x-oss-security-token:${e.SecurityToken} -`,u=`/${t.bucket}/${t.object}`,f=`PUT - -${l} -${c} -${d}${u}`,h=await qu(f,e.AccessKeySecret),g=await fetch(s,{method:"PUT",headers:{"Content-Type":l,Date:c,Authorization:`OSS ${e.AccessKeyId}:${h}`,"x-oss-security-token":e.SecurityToken,"x-oss-callback":o,"x-oss-callback-var":a,"Content-Length":String(i.length)},body:i});if(!g.ok){let p=(await g.text()).slice(0,300);throw new Error(`115 OSS \u4E0A\u4F20\u5931\u8D25\uFF08HTTP ${g.status}\uFF09\uFF1A${p}`)}}}});function Vt(r){if(!r)return"/";let t=r.replace(/\\/g,"/").replace(/\/+/g,"/").replace(/^\/|\/$/g,"");return t?"/"+t:"/"}function Wn(r){let e=Vt(r);if(e==="/")return"/";let t=e.split("/").filter(Boolean);return t.pop(),t.length?"/"+t.join("/"):"/"}function Fr(r){let e=Vt(r);if(e==="/")return"";let t=e.split("/").filter(Boolean);return t[t.length-1]||""}function Gl(...r){return Vt(r.join("/"))}function Ec(r,e,t){if(!r||!r.trim())return`${e.UserName} ${t} ${e.ObjPath}`;let i=r;return i=i.replace(/\{\{\.UserName\}\}/g,e.UserName||""),i=i.replace(/\{\{\.ObjName\}\}/g,e.ObjName||""),i=i.replace(/\{\{\.ObjPath\}\}/g,e.ObjPath||""),i=i.replace(/\{\{\.ParentName\}\}/g,e.ParentName||""),i=i.replace(/\{\{\.ParentPath\}\}/g,e.ParentPath||""),i=i.replace(/\{\{\.TargetName\}\}/g,e.TargetName||""),i=i.replace(/\{\{\.TargetPath\}\}/g,e.TargetPath||""),i}function W8(r,e){let t=Vt(r),i=Vt(e),n=1;for(;n0&&t[n]!=="/";)n--;let a=Vt(t.slice(0,n)),c=t.slice(n+1,s),l=i.slice(n+1,o),d=t.slice(n+1),u=i.slice(n+1);return{ancestor:a,aChildName:c,bChildName:l,aRest:d,bRest:u}}var Rp,X8=J(()=>{"use strict";Rp=class{addition;token;owner;repo;constructor(e){this.addition=e,this.token=(e.token||"").trim(),this.owner=(e.owner||"").trim(),this.repo=(e.repo||"").trim()}get headers(){let e={Accept:"application/vnd.github.object+json","X-GitHub-Api-Version":"2022-11-28","User-Agent":"OpenList-Github-Driver"};return this.token&&(e.Authorization=`Bearer ${this.token}`),e}async request(e,t={}){let i={...this.headers,...t.headers||{}},n;t.body!==void 0&&(typeof t.body=="string"?n=t.body:(n=JSON.stringify(t.body),i["Content-Type"]||(i["Content-Type"]="application/json")));let s=await fetch(e,{method:t.method||"GET",headers:i,body:n});if(!s.ok){let o=`${s.status} ${s.statusText}`;try{let a=await s.json();a?.message&&(o=`${s.status} ${s.statusText}: ${a.message}`)}catch{}throw new Error(o)}return s.status===204?{}:await s.json()}getContentApiUrl(e){let t=Vt(e);return`https://api.github.com/repos/${this.owner}/${this.repo}/contents${t==="/"?"":t}`}async getContents(e,t){let i=new URL(this.getContentApiUrl(e));return t&&i.searchParams.set("ref",t),this.request(i.toString())}async getRepo(){return this.request(`https://api.github.com/repos/${this.owner}/${this.repo}`)}async getBranchHead(e){return(await this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/branches/${encodeURIComponent(e)}`)).commit.sha}async getAuthenticatedUser(){return this.request("https://api.github.com/user")}async getTree(e){return this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/git/trees/${e}`)}async getTreeDirectly(e,t){let i=await this.getContents(e,t);if(!i.entries&&i.type!=="dir")throw new Error(`${e} is not a folder`);let n=await this.getTree(i.sha);if(n.truncated)throw new Error(`tree ${e} is truncated`);return{tree:n,dirSha:i.sha}}async newTree(e,t){let i={tree:t};return e&&(i.base_tree=e),(await this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/git/trees`,{method:"POST",body:i,headers:{Accept:"application/vnd.github+json"}})).sha}async putBlob(e){let t=Buffer.from(e).toString("base64");return(await this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/git/blobs`,{method:"POST",body:{encoding:"base64",content:t},headers:{Accept:"application/vnd.github+json"}})).sha}async createCommit(e,t,i,n,s){let o={message:e,tree:t,parents:[i]};return n?.name&&(o.committer={name:n.name,email:n.email,date:new Date().toISOString()}),s?.name&&(o.author={name:s.name,email:s.email,date:new Date().toISOString()}),(await this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/git/commits`,{method:"POST",body:o,headers:{Accept:"application/vnd.github+json"}})).sha}async updateRef(e,t){await this.request(`https://api.github.com/repos/${this.owner}/${this.repo}/git/refs/heads/${encodeURIComponent(e)}`,{method:"PATCH",body:{sha:t,force:!1},headers:{Accept:"application/vnd.github+json"}})}async renewParentTrees(e,t,i,n,s){let o=Vt(e),a=Vt(n);for(;o!==a;){o=Wn(o);let{tree:c,dirSha:l}=await this.getTreeDirectly(o,s),d=c.tree.find(f=>f.sha===t);if(!d)throw new Error(`Object with sha ${t} not found in ${o}`);let u={path:d.path,mode:d.mode,type:d.type,sha:i};i=await this.newTree(l,[u]),t=l}return i}}});var Np,Z8=J(()=>{"use strict";Be();He();X8();Np=class{addition;client;isOnBranch=!1;commitLock=Promise.resolve();constructor(e){this.addition=e,this.client=new Rp(e)}async acquireLock(e){let t=this.commitLock,i;this.commitLock=new Promise(n=>{i=n}),await t;try{return await e()}finally{i()}}formatDownloadUrl(e){if(!e)return"";let t=(this.addition.gh_proxy||"").trim();return t?e.replace("https://raw.githubusercontent.com",t):e}async commitAndPush(e,t){let i=this.addition.ref,n=await this.client.getBranchHead(i),s=this.addition.committer_name&&this.addition.committer_email?{name:this.addition.committer_name,email:this.addition.committer_email}:void 0,o=this.addition.author_name&&this.addition.author_email?{name:this.addition.author_name,email:this.addition.author_email}:void 0,a=await this.client.createCommit(e,t,n,s,o);await this.client.updateRef(i,a)}async init(){if(this.addition.root_folder_path=Vt(this.addition.root_folder_path||"/"),this.addition.committer_name&&!this.addition.committer_email||!this.addition.committer_name&&this.addition.committer_email)throw new Error("committer_name and committer_email must both be set or empty");if(this.addition.author_name&&!this.addition.author_email||!this.addition.author_name&&this.addition.author_email)throw new Error("author_name and author_email must both be set or empty");if(!this.addition.ref||!this.addition.ref.trim()){let e=await this.client.getRepo();this.addition.ref=e.default_branch,this.isOnBranch=!0}else try{await this.client.getBranchHead(this.addition.ref),this.isOnBranch=!0}catch{this.isOnBranch=!1}}async list(e,t){let i=Vt(t),n=await this.client.getContents(i,this.addition.ref);if(!n.entries&&n.type!=="dir")throw new Error(`${t} is not a folder`);let s=[];if(n.entries&&n.entries.length>=1e3){let o=await this.client.getTree(n.sha);if(o.truncated)throw new Error(`Tree ${t} is truncated (>100,000 items)`);for(let a of o.tree){if(a.path===".gitkeep")continue;let c=a.type==="tree";s.push({name:a.path,size:a.size||0,is_dir:c,modified:new Date(0).toISOString(),sign:"",type:X(a.path,c),raw_url:""})}}else if(n.entries)for(let o of n.entries){if(o.name===".gitkeep")continue;let a=o.type==="dir";s.push({name:o.name,size:o.size||0,is_dir:a,modified:new Date(0).toISOString(),sign:"",type:X(o.name,a),raw_url:this.formatDownloadUrl(o.download_url)})}return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=Vt(t),n=await this.client.getContents(i,this.addition.ref);if(n.type==="submodule")throw new Error("cannot download a submodule");let s=n.type==="dir"||!!n.entries,o=n.name||Fr(i)||"root";return{name:o,size:n.size||0,is_dir:s,modified:new Date(0).toISOString(),sign:"",type:X(o,s),raw_url:this.formatDownloadUrl(n.download_url)}}async mkdir(e,t){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let i=Vt(t),n=Wn(i),s=Fr(i);await this.acquireLock(async()=>{let o=await this.client.getContents(n,this.addition.ref);if(!o.entries&&o.type!=="dir")throw new Error(`${n} is not a folder`);let a=await this.client.newTree("",[{path:".gitkeep",mode:"100644",type:"blob",content:""}]),c=[{path:s,mode:"040000",type:"tree",sha:a}];o.entries?.length===1&&o.entries[0].name===".gitkeep"&&c.push({path:".gitkeep",mode:"100644",type:"blob",sha:null});let l=await this.client.newTree(o.sha,c),d=await this.client.renewParentTrees(n,o.sha,l,"/",this.addition.ref),u=Ec(this.addition.mkdir_commit_message,{UserName:"OpenList",ObjName:s,ObjPath:i,ParentName:Fr(n),ParentPath:n},"mkdir");await this.commitAndPush(u,d)})}async put(e,t,i){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let n=Vt(t),s=Wn(n),o=Fr(n);await this.acquireLock(async()=>{let a=await this.client.putBlob(i),c=await this.client.getContents(s,this.addition.ref);if(!c.entries&&c.type!=="dir")throw new Error(`${s} is not a folder`);let l=[{path:o,mode:"100644",type:"blob",sha:a}];c.entries?.length===1&&c.entries[0].name===".gitkeep"&&l.push({path:".gitkeep",mode:"100644",type:"blob",sha:null});let d=await this.client.newTree(c.sha,l),u=await this.client.renewParentTrees(s,c.sha,d,"/",this.addition.ref),f=Ec(this.addition.put_commit_message,{UserName:"OpenList",ObjName:o,ObjPath:n,ParentName:Fr(s),ParentPath:s},"upload");await this.commitAndPush(f,u)})}async rename(e,t,i){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let n=Vt(t),s=Wn(n),o=Fr(n);await this.acquireLock(async()=>{let{tree:a,dirSha:c}=await this.client.getTreeDirectly(s,this.addition.ref),l=a.tree.find(p=>p.path===o);if(!l)throw new Error(`Object not found: ${n}`);if(l.type==="commit")throw new Error("cannot rename a submodule");let d={path:o,mode:l.mode,type:l.type,sha:null},u={path:i,mode:l.mode,type:l.type,sha:l.sha},f=await this.client.newTree(c,[d,u]),h=await this.client.renewParentTrees(s,c,f,"/",this.addition.ref),g=Ec(this.addition.rename_commit_message,{UserName:"OpenList",ObjName:o,ObjPath:n,ParentName:Fr(s),ParentPath:s,TargetName:i,TargetPath:Gl(s,i)},"rename");await this.commitAndPush(g,h)})}async remove(e,t,i){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let n=Vt(t),s=Wn(n),o=Fr(n);await this.acquireLock(async()=>{let{tree:a,dirSha:c}=await this.client.getTreeDirectly(s,this.addition.ref),l=a.tree.find(g=>g.path===o);if(!l)throw new Error(`Object not found: ${n}`);if(l.type==="commit")throw new Error("cannot remove a submodule");let d=[{path:o,mode:l.mode,type:l.type,sha:null}];a.tree.length===1&&d.push({path:".gitkeep",mode:"100644",type:"blob",content:""});let u=await this.client.newTree(c,d),f=await this.client.renewParentTrees(s,c,u,"/",this.addition.ref),h=Ec(this.addition.delete_commit_message,{UserName:"OpenList",ObjName:o,ObjPath:n,ParentName:Fr(s),ParentPath:s},"remove");await this.commitAndPush(h,f)})}async move(e,t,i,n,s){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let o=Vt(n),a=Vt(t);if(a.startsWith(o))throw new Error("cannot move parent dir to child");await this.acquireLock(async()=>{let c="",l=Wn(o),d=Fr(o);if(a.startsWith(l)){let{dstOldSha:f,dstNewSha:h,ancestorOldSha:g,srcParentTree:p}=await this.copyWithoutRenewTree(o,a),m=a.slice(l.length).replace(/^\//,"").split("/")[0],w=Gl(l,m),B=await this.client.renewParentTrees(a,f,h,w,this.addition.ref),C=p.tree.find(R=>R.path===d),x=p.tree.find(R=>R.path===m);if(!C||!x)throw new Error("Object not found during move");let Q=await this.client.newTree(g,[{path:C.path,mode:C.mode,type:C.type,sha:null},{path:x.path,mode:x.mode,type:x.type,sha:B}]);c=await this.client.renewParentTrees(l,g,Q,"/",this.addition.ref)}else if(o.startsWith(a)){let{tree:f,dirSha:h}=await this.client.getTreeDirectly(l,this.addition.ref),g=f.tree.find(T=>T.path===d);if(!g)throw new Error("Object not found");if(g.type==="commit")throw new Error("cannot move a submodule");let p=[{path:g.path,mode:g.mode,type:g.type,sha:null}];f.tree.length===1&&p.push({path:".gitkeep",mode:"100644",type:"blob",content:""});let I=await this.client.newTree(h,p),w=o.slice(a.length).replace(/^\//,"").split("/")[0];if(!w)throw new Error("cannot move in place");let B=Gl(a,w),C=await this.client.renewParentTrees(l,h,I,B,this.addition.ref),{tree:x,dirSha:Q}=await this.client.getTreeDirectly(a,this.addition.ref),R=x.tree.find(T=>T.path===w);if(!R)throw new Error("Object not found");let U=await this.client.newTree(Q,[{path:R.path,mode:R.mode,type:R.type,sha:C},{path:g.path,mode:g.mode,type:g.type,sha:g.sha}]);c=await this.client.renewParentTrees(a,Q,U,"/",this.addition.ref)}else{let{dstOldSha:f,dstNewSha:h,srcParentOldSha:g,srcParentTree:p}=await this.copyWithoutRenewTree(o,a),I=p.tree.find(v=>v.path===d);if(!I)throw new Error("Object not found");let m=[{path:I.path,mode:I.mode,type:I.type,sha:null}];p.tree.length===1&&m.push({path:".gitkeep",mode:"100644",type:"blob",content:""});let w=await this.client.newTree(g,m),{ancestor:B,aChildName:C,bChildName:x}=W8(o,a),Q=await this.client.renewParentTrees(a,f,h,Gl(B,x),this.addition.ref),R=await this.client.renewParentTrees(l,g,w,Gl(B,C),this.addition.ref),{tree:U,dirSha:T}=await this.client.getTreeDirectly(B,this.addition.ref),D=U.tree.find(v=>v.path===C),b=U.tree.find(v=>v.path===x);if(!D||!b)throw new Error("Ancestor child tree not found");let _=await this.client.newTree(T,[{path:D.path,mode:D.mode,type:D.type,sha:R},{path:b.path,mode:b.mode,type:b.type,sha:Q}]);c=await this.client.renewParentTrees(B,T,_,"/",this.addition.ref)}let u=Ec(this.addition.move_commit_message,{UserName:"OpenList",ObjName:d,ObjPath:o,ParentName:Fr(l),ParentPath:l,TargetName:Fr(a),TargetPath:a},"move");await this.commitAndPush(u,c)})}async copy(e,t,i,n,s){if(!this.isOnBranch)throw new Error("cannot write to non-branch reference");let o=Vt(n),a=Vt(t);if(a.startsWith(o))throw new Error("cannot copy parent dir to child");await this.acquireLock(async()=>{let{dstOldSha:c,dstNewSha:l}=await this.copyWithoutRenewTree(o,a),d=await this.client.renewParentTrees(a,c,l,"/",this.addition.ref),u=Ec(this.addition.copy_commit_message,{UserName:"OpenList",ObjName:Fr(o),ObjPath:o,ParentName:Fr(Wn(o)),ParentPath:Wn(o),TargetName:Fr(a),TargetPath:a},"copy");await this.commitAndPush(u,d)})}async copyWithoutRenewTree(e,t){let i=await this.client.getContents(t,this.addition.ref);if(!i.entries&&i.type!=="dir")throw new Error(`${t} is not a folder`);let n=Wn(e),s=Fr(e),{tree:o,dirSha:a}=await this.client.getTreeDirectly(n,this.addition.ref),c=o.tree.find(u=>u.path===s);if(!c)throw new Error(`Object not found: ${e}`);if(c.type==="commit")throw new Error("cannot copy a submodule");let l=[{path:c.path,mode:c.mode,type:c.type,sha:c.sha}];i.entries?.length===1&&i.entries[0].name===".gitkeep"&&l.push({path:".gitkeep",mode:"100644",type:"blob",sha:null});let d=await this.client.newTree(i.sha,l);return{dstOldSha:i.sha,dstNewSha:d,srcParentOldSha:a,srcParentTree:o,ancestorOldSha:a}}}});function Bc(r){return Xi.default.MD5(r).toString(Xi.default.enc.Hex)}function e4(r,e){let t=e.match(/:\/\/[^/]+((\/[^/\s?#]+)*)/),i=t?t[1]:e;return`${r}:${i}`}function t4(r,e){let t=`${r}${e}${s4}${Ij}`,i=Xi.default.SHA1(t).toString(Xi.default.enc.Hex),n=Xi.default.MD5(i).toString(Xi.default.enc.Hex);return`div101.${r}${n}`}function o4(r){let e=r.length,t=262144;for(;e/t>512&&t<2097152;)t=t<<1;let i=[];for(let s=0;s{"use strict";Xi=xr(Oo(),1),r4="https://api-pan.xunlei.com/drive/v1",Ls=`${r4}/files`,Qge=`${r4}/tasks`,i4="https://xluser-ssl.xunlei.com",qp=`${i4}/v1`,uE="drive#folder",n4="drive#file",fE="UPLOAD_TYPE_RESUMABLE",yj="access_end_point_token",s4="40",Ij="34a062aaa22f906fca4fefe9fb3a3021";cf=class{options;tokenResp=null;coreLoginResp=null;captchaToken="";creditKey="";constructor(e){this.options=e,this.captchaToken=e.captchaToken||"",this.creditKey=e.creditKey||""}getCaptchaSign(){if(!this.options.algorithms||this.options.algorithms.length===0)return{timestamp:this.options.timestamp||"",sign:this.options.captchaSign||""};let e=Date.now().toString(),t=`${this.options.clientId}${this.options.clientVersion}${this.options.packageName}${this.options.deviceId}${e}`;for(let i of this.options.algorithms)t=Bc(t+i);return{timestamp:e,sign:`1.${t}`}}async refreshCaptchaToken(e,t){let i={action:e,captcha_token:this.captchaToken,client_id:this.options.clientId,device_id:this.options.deviceId,meta:t,redirect_uri:"xlaccsdk01://xunlei.com/callback?state=harbor"},n=await this.rawRequest(`${qp}/shield/captcha/init`,{method:"POST",body:i});if(n.error_code||n.error&&n.error!=="success")throw new Error(`Captcha error: ${n.error_code} ${n.error} ${n.error_description||""}`);if(n.url)throw new Error(`need verify: Click Here`);if(!n.captcha_token)throw new Error("empty captchaToken");this.captchaToken=n.captcha_token,this.options.onPersistCaptchaToken&&await this.options.onPersistCaptchaToken(n.captcha_token)}async refreshCaptchaTokenAtLogin(e,t){let{timestamp:i,sign:n}=this.getCaptchaSign(),s={client_version:this.options.clientVersion,package_name:this.options.packageName,user_id:t,timestamp:i,captcha_sign:n};await this.refreshCaptchaToken(e,s)}async refreshCaptchaTokenInLogin(e,t){let i={};/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(t)?i.email=t:t.length>=11&&t.length<=18?i.phone_number=t:i.username=t,await this.refreshCaptchaToken(e,i)}formatReviewData(e){let t=t4(this.options.deviceId,this.options.packageName),i={creditkey:e.creditkey,reviewurl:`${e.reviewurl}&deviceid=${t}`,deviceid:t,devicesign:t},s=` -
-

- \u{1F512} \u672C\u6B21\u767B\u5F55\u9700\u8981\u9A8C\u8BC1
- This login requires verification -

-

\u4E0B\u9762\u662F\u9A8C\u8BC1\u6240\u9700\u8981\u7684\u6570\u636E\uFF0C\u5177\u4F53\u4F7F\u7528\u65B9\u6CD5\u8BF7\u53C2\u7167\u5BF9\u5E94\u7684\u9A71\u52A8\u6587\u6863
- Below are the relevant verification data. For specific usage methods, please refer to the corresponding driver documentation.

-
-
${JSON.stringify(i,null,2)}
-
-
`;return new Error(s)}async rawRequest(e,t={}){let i={"user-agent":this.options.userAgent,accept:"application/json;charset=UTF-8","x-device-id":this.options.deviceId,"x-client-id":this.options.clientId,"x-client-version":this.options.clientVersion,...t.headers||{}},n;t.body!==void 0&&(typeof t.body=="string"?n=t.body:(n=JSON.stringify(t.body),i["content-type"]||(i["content-type"]="application/json;charset=UTF-8")));let s=await fetch(e,{method:t.method||"GET",headers:i,body:n}),o=await s.text(),a={};try{a=JSON.parse(o)}catch{if(!s.ok)throw new Error(`${s.status} ${s.statusText}: ${o}`);return o}if(a.error==="review_panel")throw this.formatReviewData(a);return a}async authRequest(e,t={}){if(!this.tokenResp?.access_token)throw new Error("empty token");let i={Authorization:`${this.tokenResp.token_type} ${this.tokenResp.access_token}`,"X-Captcha-Token":this.captchaToken,...t.headers||{}},n=await this.rawRequest(e,{...t,headers:i}),s=n?.error_code||0;if(s===4122||s===4121||s===10||s===16){if(this.tokenResp?.refresh_token){let o=await this.refreshToken(this.tokenResp.refresh_token);return this.tokenResp=o,this.options.onPersistToken&&await this.options.onPersistToken(o),this.authRequest(e,t)}throw new Error(`Token expired error ${s}`)}else if(s===9){let o=e4(t.method||"GET",e);return await this.refreshCaptchaTokenAtLogin(o,this.tokenResp.user_id||""),this.authRequest(e,t)}else if(s!==0||n.error&&n.error!=="success")throw new Error(`ErrorCode: ${n.error_code||0}, Error: ${n.error||""}, ErrorDescription: ${n.error_description||""}`);return n}async coreLogin(e,t){let i=`${i4}/xluser.core.login/v3/login`,n={protocolVersion:"301",sequenceNo:"1000012",platformVersion:"10",isCompressed:"0",appid:s4,clientVersion:this.options.clientVersion,peerID:"00000000000000000000000000000000",appName:"ANDROID-com.xunlei.downloadprovider",sdkVersion:"512000",devicesign:t4(this.options.deviceId,this.options.packageName),netWorkType:"WIFI",providerName:"NONE",deviceModel:"M2004J7AC",deviceName:"Xiaomi_M2004j7ac",OSVersion:"12",creditkey:this.creditKey,hl:"zh-CN",userName:e,passWord:t,verifyKey:"",verifyCode:"",isMd5Pwd:"0"},s=await this.rawRequest(i,{method:"POST",body:n,headers:{"user-agent":"android-ok-http-client/xl-acc-sdk/version-5.0.12.512000"}});return this.coreLoginResp=s,s}async login(e,t){let n=(await this.coreLogin(e,t)).sessionID,s=`${qp}/auth/signin/token`;await this.refreshCaptchaTokenInLogin(e4("POST",s),e);let o=await this.rawRequest(s,{method:"POST",body:{client_id:this.options.clientId,client_secret:this.options.clientSecret,provider:yj,signin_token:n}});return this.tokenResp=o,this.creditKey="",this.options.onPersistToken&&await this.options.onPersistToken(o),o}async refreshToken(e){let t=`${qp}/auth/token`,i=await this.rawRequest(t,{method:"POST",body:{grant_type:"refresh_token",refresh_token:e,client_id:this.options.clientId,client_secret:this.options.clientSecret}});return this.tokenResp=i,this.options.onPersistToken&&await this.options.onPersistToken(i),i}async isLogin(){if(!this.tokenResp?.access_token)return!1;try{return await this.authRequest(`${qp}/user/me`,{method:"GET"}),!0}catch{return!1}}}});function c4(r,e,t){let i=r.kind===uE,n=r.web_content_link||"";if(t&&r.medias&&r.medias.length>0){for(let s of r.medias)if(s.link?.url){n=s.link.url;break}}return{name:r.name,size:parseInt(r.size||"0",10),is_dir:i,modified:r.modified_time||r.created_time||new Date().toISOString(),sign:"",type:X(r.name,i),thumb:r.thumbnail_link||r.icon_link||"",raw_url:n,raw_url_headers:{"User-Agent":e}}}function l4(r){if(r?.device_id&&r.device_id.trim().length===32)return r.device_id.trim();let e=`${r?.username||""}${r?.password||""}`;return e.trim()?Bc(e):Bc(Math.random().toString(36)+Date.now().toString(36))}var lf,Op,d4=J(()=>{"use strict";Be();He();a4();lf=class{client;addition;identity="";onPersistCallback;constructor(e,t){this.addition=e,this.onPersistCallback=t;let i=l4(e);e.device_id=i,this.client=new cf({deviceId:i,clientId:e.client_id||"",clientSecret:e.client_secret||"",clientVersion:"8.31.0.9726",packageName:"com.xunlei.downloadprovider",userAgent:"ANDROID-com.xunlei.downloadprovider/8.31.0.9726 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/512000 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",downloadUserAgent:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",algorithms:["9uJNVj/wLmdwKrJaVj/omlQ","Oz64Lp0GigmChHMf/6TNfxx7O9PyopcczMsnf","Eb+L7Ce+Ej48u","jKY0","ASr0zCl6v8W4aidjPK5KHd1Lq3t+vBFf41dqv5+fnOd","wQlozdg6r1qxh0eRmt3QgNXOvSZO6q/GXK","gmirk+ciAvIgA/cxUUCema47jr/YToixTT+Q6O","5IiCoM9B1/788ntB","P07JH0h6qoM6TSUAK2aL9T5s2QBVeY9JWvalf","+oK0AN"],space:e.space||"",captchaToken:e.captcha_token||"",creditKey:e.credit_key||"",onPersistToken:async n=>{this.onPersistCallback&&await this.onPersistCallback({refresh_token:n.refresh_token,captcha_token:this.client.captchaToken,device_id:i})},onPersistCaptchaToken:async n=>{this.onPersistCallback&&await this.onPersistCallback({captcha_token:n})}})}get downloadUserAgent(){return this.addition.download_user_agent||"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"}get useVideoUrl(){return!!this.addition.use_video_url}async init(){let e=this.addition.username||"",t=this.addition.password||"",i=Bc(`${e}${t}`);(this.identity!==i||!await this.client.isLogin())&&(this.identity=i,await this.client.login(e,t))}resolveFolderId(e){if(!e||e==="/"||e==="0")return this.addition.root_folder_id||"";let t=e.split("/").filter(Boolean);return t[t.length-1]||this.addition.root_folder_id||""}async list(e,t){let i=this.resolveFolderId(t),n=[],s="";for(;;){let o=new URL(Ls);o.searchParams.set("space",this.addition.space||""),o.searchParams.set("__type","drive"),o.searchParams.set("refresh","true"),o.searchParams.set("__sync","true"),o.searchParams.set("parent_id",i),o.searchParams.set("page_token",s),o.searchParams.set("with_audit","true"),o.searchParams.set("limit","100"),o.searchParams.set("filters",JSON.stringify({phase:{eq:"PHASE_TYPE_COMPLETE"},trashed:{eq:!1}}));let a=await this.client.authRequest(o.toString(),{method:"GET"});if(a.files&&a.files.length>0)for(let c of a.files)n.push(c4(c,this.downloadUserAgent,this.useVideoUrl));if(!a.next_page_token)break;s=a.next_page_token}return de(n,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.resolveFolderId(t),n=new URL(`${Ls}/${i}`);n.searchParams.set("space",this.addition.space||"");let s=await this.client.authRequest(n.toString(),{method:"GET"});return c4(s,this.downloadUserAgent,this.useVideoUrl)}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/"),o=this.resolveFolderId(s);await this.client.authRequest(Ls,{method:"POST",body:{kind:uE,name:n,parent_id:o,space:this.addition.space||""}})}async rename(e,t,i){let n=this.resolveFolderId(t);await this.client.authRequest(`${Ls}/${n}`,{method:"PATCH",body:{name:i,space:this.addition.space||""}})}async remove(e,t,i){let n=this.resolveFolderId(t),s=new URL(`${Ls}/${n}/trash`);s.searchParams.set("space",this.addition.space||""),await this.client.authRequest(s.toString(),{method:"PATCH",body:{}})}async move(e,t,i,n,s){let o=this.resolveFolderId(n),a=this.resolveFolderId(t);await this.client.authRequest(`${Ls}:batchMove`,{method:"POST",body:{to:{parent_id:a},ids:[o],space:this.addition.space||""}})}async copy(e,t,i,n,s){let o=this.resolveFolderId(n),a=this.resolveFolderId(t);await this.client.authRequest(`${Ls}:batchCopy`,{method:"POST",body:{to:{parent_id:a},ids:[o],space:this.addition.space||""}})}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"file",o="/"+n.join("/"),a=this.resolveFolderId(o),c=o4(i),l=await this.client.authRequest(Ls,{method:"POST",body:{kind:n4,parent_id:a,name:s,size:i.length.toString(),hash:c,upload_type:fE,space:this.addition.space||""}});if(l.upload_type===fE&&l.resumable?.params){let d=l.resumable.params,u=d.endpoint;u.startsWith(d.bucket+".")&&(u=u.slice(d.bucket.length+1)),!u.startsWith("http://")&&!u.startsWith("https://")&&(u=`https://${u}`);let f=`${u.replace(/\/$/,"")}/${d.bucket}/${d.key}`,h={"x-amz-security-token":d.security_token},g=await fetch(f,{method:"PUT",headers:h,body:i});if(!g.ok)throw new Error(`S3 Upload failed: ${g.status} ${g.statusText}`)}}},Op=class extends lf{constructor(e,t){super(e,t);let i=l4(e);e.device_id=i;let n=e.sign_type==="captcha_sign"?void 0:(e.algorithms||"").split(",").map(s=>s.trim()).filter(Boolean);this.client=new cf({deviceId:i,clientId:e.client_id||"",clientSecret:e.client_secret||"",clientVersion:e.client_version||"8.31.0.9726",packageName:e.package_name||"com.xunlei.downloadprovider",userAgent:e.user_agent||"ANDROID-com.xunlei.downloadprovider/8.31.0.9726 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/512000 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",downloadUserAgent:e.download_user_agent||"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",algorithms:n&&n.length>0?n:void 0,timestamp:e.timestamp,captchaSign:e.captcha_sign,useVideoUrl:e.use_video_url,space:e.space||"",captchaToken:e.captcha_token||"",creditKey:e.credit_key||"",onPersistToken:async s=>{this.onPersistCallback&&await this.onPersistCallback({refresh_token:s.refresh_token,captcha_token:this.client.captchaToken,device_id:i})},onPersistCaptchaToken:async s=>{this.onPersistCallback&&await this.onPersistCallback({captcha_token:s})}})}async init(){let e=this.addition,t="";e.login_type==="refresh_token"?t=Bc(e.refresh_token||""):t=Bc(`${e.username||""}${e.password||""}`),(this.identity!==t||!await this.client.isLogin())&&(this.identity=t,e.login_type==="refresh_token"&&e.refresh_token?await this.client.refreshToken(e.refresh_token):e.username&&e.password&&await this.client.login(e.username,e.password))}}});function u4(r){if(!r)return new Date().toISOString();let e=r.trim(),t=new Date(e);if(!isNaN(t.getTime()))return t.toISOString();let i=Date.now(),n=864e5,s=e.match(wj);if(s){let o=parseFloat(s[1])||0,a=s[2];if(a.includes("\u79D2\u524D"))return new Date(i-o*1e3).toISOString();if(a.includes("\u5206")||a.includes("\u5206\u949F\u524D"))return new Date(i-o*6e4).toISOString();if(a.includes("\u5C0F\u65F6\u524D")||a.includes("\u5C0F\u65F6"))return new Date(i-o*36e5).toISOString();if(a.includes("\u5929\u524D")||a.includes("\u5929"))return new Date(i-o*n).toISOString();if(a.includes("\u6628\u5929"))return new Date(i-n).toISOString();if(a.includes("\u524D\u5929"))return new Date(i-n*2).toISOString()}return new Date().toISOString()}function f4(r){if(!r)return 0;let e=r.trim().match(xj);if(!e)return 0;let t=parseFloat(e[1]);switch(e[2].toUpperCase()){case"B":return Math.floor(t);case"K":return Math.floor(t*1024);case"M":return Math.floor(t*1048576);case"G":return Math.floor(t*1073741824);default:return 0}}function df(r){return r.replace(/|[^:]\/\/.*|\/\*[\s\S]*?\*\//g,e=>e.slice(1,3)==="//"?e.slice(0,1):` -`)}function A4(r){let e="",t=!1,i=!1;for(let n=0;n{for(let n=i.length-1;n>=0;n--){let s=(i[n][1]||"").trim().replace(/^['"]|['"]$/g,"");if(s)return s}return""};if(r!=="sasign"){let i=Array.from(e.matchAll(new RegExp(`(?:var|let|const)\\s+${r}\\s*=\\s*['"]?([\\s\\S]*?)['"]?;`,"gi")));if(i.length){let o=t(i);if(o)return o}let n=Array.from(e.matchAll(new RegExp(`(?:^|[;,\\s])${r}\\s*=\\s*['"]?([\\s\\S]*?)['"]?;`,"gim")));if(n.length){let o=t(n);if(o)return o}let s=Array.from(e.matchAll(new RegExp(`['"]?${r}['"]?\\s*:\\s*['"]?([\\s\\S]*?)['"]?`,"gi")));if(s.length){let o=t(s);if(o)return o}}else{let i=Array.from(e.matchAll(new RegExp(`(?:var|let|const)?\\s*${r}\\s*=\\s*['"]?([\\s\\S]*?)['"]?;`,"gi")));if(i.length===3)return i[1][1].trim().replace(/^['"]|['"]$/g,"");if(i.length>0)return t(i)}return""}function vj(r,e){let t={},i=/['"]?([a-zA-Z0-9_$]+)['"]?\s*:\s*(['"]?([^'",}\s]+)['"]?)/g,n=r.matchAll(i);for(let s of n){let o=s[1],a=s[2],c=s[3];if(!c)t[o]="";else if(a.includes("'")||a.includes('"')||/^\d+$/.test(a))t[o]=c;else{let l=bj(c,e);t[o]=l!==""?l:c}}return t}function kj(r){let e={},t=r.split("&");for(let i of t){let[n,s]=i.split("=");n&&(e[decodeURIComponent(n)]=decodeURIComponent(s||""))}return e}function zl(r,e){let t=e||r,i=Array.from(r.matchAll(/data\s*:\s*({[\s\S]*?})/g));if(i.length>0){let s=i[0][1];for(let a of i)a[1].length>s.length&&(s=a[1]);let o=vj(s,t);if(Object.keys(o).length>0)return o}let n=r.match(/data\s*:\s*['"]([^'"]+)['"]/);if(n&&n[1].includes("="))return kj(n[1]);throw new Error("[Lanzou] \u672A\u80FD\u627E\u5230\u8BF7\u6C42\u53C2\u6570 data \u5BF9\u8C61")}function h4(r,e){let t=new RegExp(`function\\s+${e}\\s*\\([^)]*\\)\\s*\\{`,"i"),i=r.search(t);if(i===-1)throw new Error(`[Lanzou] \u672A\u627E\u5230\u51FD\u6570 ${e}`);let n=0,s=-1;for(let o=i;o{"use strict";wj=/([0-9.]*)\s*([\u4e00-\u9fa5]+)/,xj=/([0-9.]+)\s*([bkm]+)/i,Cj=/arg1='([0-9A-Z]+)'/i});function Hp(r,e){try{let t=new URL(r);return t.host=e,t.toString()}catch{return r}}function Kp(r){try{return new URL(r).host}catch{return""}}var Lp,hE,Gp,g4=J(()=>{"use strict";AE();Lp=["pan.lanzoui.com","lanzouw.com","lanzoux.com","lanzouy.com","lanzou.com"],hE="[Lanzou] Cookie \u5DF2\u8FC7\u671F\u6216\u5931\u6548\uFF0C\u8BF7\u5728\u7BA1\u7406\u540E\u53F0\u91CD\u65B0\u586B\u5199 Cookie\uFF08\u6709\u6548\u671F\u7EA6 15 \u5929\uFF09",Gp=class{addition;cookie="";uid="";vei="";onCookieUpdate;acwMap=new Map;workingShareHost="";constructor(e,t){this.addition=e,this.cookie=(e.cookie||"").trim(),this.onCookieUpdate=t}getBaseUrl(){return(this.addition.baseUrl||this.addition.base_url||"https://pc.woozooo.com").replace(/\/$/,"")}getShareUrl(){return(this.addition.shareUrl||this.addition.share_url||"https://pan.lanzoui.com").replace(/\/$/,"")}shareHost(){try{return new URL(this.getShareUrl()).host}catch{return"pan.lanzoui.com"}}getUserAgent(){return this.addition.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"}getCookie(){return this.cookie}updateCookie(e,t){if(!e)return;let i=e.match(/(?:^|,\s*)acw_sc__v2=([^;,\s]+)/i);i&&i[1]&&this.acwMap.set(t||this.shareHost(),i[1]);let n=this.cookie?this.cookie.split(";").map(a=>a.trim()):[],s=e.split(/,(?=[a-zA-Z0-9_\-]+=[^;]+)/);for(let a of s){let c=a.split(";")[0].trim(),l=c.indexOf("=");if(l>0){let d=c.slice(0,l).trim(),u=c.slice(l+1).trim(),f=n.findIndex(h=>h.startsWith(`${d}=`));f!==-1?n[f]=`${d}=${u}`:n.push(`${d}=${u}`)}}let o=n.filter(Boolean).join("; ");o!==this.cookie&&(this.cookie=o,this.onCookieUpdate?.(this.cookie))}async init(){let e=this.addition.type||"cookie";if(e==="account"?(await this.login(),await this.initVeiAndUid()):e==="cookie"&&this.cookie&&await this.initVeiAndUid(),!this.acwMap.has(this.shareHost()))try{await this.request(`${this.getShareUrl()}/`,"GET")}catch{}}async probeShareDomain(e){let t=(e||"").replace(/^\//,"");if(!t)return this.getShareUrl();try{let i=await this.request(`${this.getShareUrl()}/${t}`,"GET");return this.extractRealShareDomain(i)||this.getShareUrl()}catch{return this.getShareUrl()}}extractRealShareDomain(e){let t=[],i=e.match(/]*?src=["'](https?:\/\/[^"'/]+)/i);i&&t.push(i[1]);let n=e.match(/["'](https?:\/\/[^"']*?\/fn\?[^"']*)["']/i);n&&t.push(n[1]);let s=e.match(/["']?(?:dom|url)\s*[:=]\s*["']?(https?:\/\/[^"'\s]+)/i);s&&t.push(s[1]);for(let o of t)try{let a=new URL(o);return`${a.protocol}//${a.host}`}catch{}return""}async login(){if(!this.addition.account||!this.addition.password)throw new Error("[Lanzou] \u8D26\u53F7\u6A21\u5F0F\u4E0B\u5FC5\u987B\u63D0\u4F9B\u8D26\u53F7\u4E0E\u5BC6\u7801");for(let e=0;e<3;e++){let t={"User-Agent":this.getUserAgent(),Referer:"https://pc.woozooo.com","Content-Type":"application/x-www-form-urlencoded"};this.acwMap.has("up.woozooo.com")&&(t.Cookie=`acw_sc__v2=${this.acwMap.get("up.woozooo.com")}`);let i=await fetch("https://up.woozooo.com/mlogin.php",{method:"POST",headers:t,body:new URLSearchParams({task:"3",uid:this.addition.account,pwd:this.addition.password,setSessionId:"",setSig:"",setScene:"",setTocen:"",formhash:""})});this.updateCookie(i.headers.get("set-cookie"),"up.woozooo.com");let n=await i.text();if(n.includes("acw_sc__v2")){this.acwMap.set("up.woozooo.com",Mp(n));continue}let s;try{s=JSON.parse(n)}catch{throw new Error(`[Lanzou] \u767B\u5F55\u54CD\u5E94\u5F02\u5E38: ${n.slice(0,200)}`)}if(s.zt!==1)throw new Error(`[Lanzou] \u767B\u5F55\u5931\u8D25: ${s.info||n}`);return}throw new Error("[Lanzou] \u767B\u5F55\u591A\u6B21\u89E6\u53D1 WAF \u6821\u9A8C\u5931\u8D25")}async initVeiAndUid(){let e=await this.request(`${this.getBaseUrl()}/mydisk.php?item=files&action=index`,"GET"),t=e.match(/uid=([^'"&;]+)/);if(!t)throw new Error(hE);this.uid=t[1];let i=df(e);try{let n=zl(i);this.vei=n.vei||""}catch{let n=e.match(/['"]?vei['"]?\s*:\s*['"]?([^'",\s]+)['"]?/);n&&(this.vei=n[1])}}async request(e,t="GET",i,n){let s=e,o=Kp(e),a=o===this.shareHost()||Lp.includes(o);a&&this.workingShareHost&&o!==this.workingShareHost&&(s=Hp(e,this.workingShareHost));let c=a,l=c?Lp.filter(u=>!e.includes(u)):[],d=u=>{let f=Kp(u),h=f===this.shareHost()||Lp.includes(f),g;return h?n&&Lp.includes(Kp(n))||n&&Kp(n)===this.shareHost()?g=Hp(n,f):g=`${new URL(u).origin}/`:g=n||this.getBaseUrl(),this.requestOnce(u,t,i,g,f)};try{return await d(s)}catch(u){if(!(c&&(u?.status===403||u?.status===405||/403|405/.test(u?.message||""))))throw u;for(let h of l){let g=Hp(s,h);try{let p=await d(g);return this.workingShareHost=h,p}catch(p){if(!(p?.status===403||p?.status===405||/403|405/.test(p?.message||"")))throw p}}throw u}}async requestOnce(e,t,i,n,s){for(let o=0;o<3;o++){let a={Referer:n,"User-Agent":this.getUserAgent(),"Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8","X-Requested-With":"XMLHttpRequest","Sec-Fetch-Dest":"empty","Sec-Fetch-Mode":"cors","Sec-Fetch-Site":"same-origin",Accept:"*/*"},c=(()=>{try{return new URL(n).host}catch{return""}})();c===s&&(a.Origin=n.slice(0,n.indexOf(c)+c.length));let l=this.cookie;e.includes("/file/")&&(l=(l?l+"; ":"")+"down_ip=1");let d=this.acwMap.get(s);d&&(l=(l?l+"; ":"")+`acw_sc__v2=${d}`),l&&(a.Cookie=l);let u;i&&t==="POST"&&(a["Content-Type"]="application/x-www-form-urlencoded; charset=UTF-8",u=new URLSearchParams(i).toString());let f=await fetch(e,{method:t,headers:a,body:u});if(this.updateCookie(f.headers.get("set-cookie"),s),f.status===403||f.status===405){let g=new Error(`[Lanzou] ${e} \u8FD4\u56DE HTTP ${f.status}`);throw g.status=f.status,g}if(f.status===429||f.status>=500||f.status===408){if(o<2){await new Promise(p=>setTimeout(p,300*(o+1)));continue}let g=new Error(`[Lanzou] ${e} \u8FD4\u56DE HTTP ${f.status}\uFF08\u591A\u6B21\u91CD\u8BD5\u4ECD\u5931\u8D25\uFF09`);throw g.status=f.status,g}let h=await f.text();if(h.includes("acw_sc__v2")){this.acwMap.set(s,Mp(h));continue}return h}throw new Error("[Lanzou] \u8BF7\u6C42\u89E6\u53D1 acw_sc__v2 \u6821\u9A8C\u8D85\u9650")}async doupload(e){let t=`${this.getBaseUrl()}/doupload.php?uid=${this.uid}&vei=${this.vei}`,i=await this.request(t,"POST",e),n;try{n=JSON.parse(i)}catch{throw new Error(`[Lanzou] \u975E JSON \u54CD\u5E94: ${i.slice(0,200)}`)}if(n.zt===9){if(this.addition.type==="account")return await this.login(),await this.initVeiAndUid(),this.doupload(e);throw new Error(hE)}if(n.zt!==1&&n.zt!==2&&n.zt!==4)throw new Error(n.inf||n.info||`[Lanzou] API \u9519\u8BEF (zt: ${n.zt})`);return n}async checkCookieValid(){if(this.addition.type==="url")return{valid:!0};try{return this.addition.type==="account"?{valid:!0}:(await this.getFiles("-1"),{valid:!0})}catch(e){let t=e?.message||String(e);return{valid:!1,error:/Cookie 已过期|未能获取到 uid|请检查 Cookie|zt=9/i.test(t)?hE:t}}}async getAllFiles(e){let t=await this.getFolders(e),i=await this.getFiles(e);return[...t,...i]}async getFolders(e){return((await this.doupload({task:"47",folder_id:e||"-1"})).text||[]).map(n=>({...n,name:n.name,fol_id:n.fol_id||n.id,is_folder:!0}))}async getFiles(e){let t=[];for(let i=1;;i++){let s=(await this.doupload({task:"5",folder_id:e||"-1",pg:String(i)})).text||[];if(s.length===0)break;t.push(...s.map(o=>({...o,name_all:o.name_all||o.name,id:o.id,size:o.size,time:o.time,is_folder:!1})))}return t}async getFileShareUrlById(e){return(await this.doupload({task:"22",file_id:e})).info||{}}async getFileOrFolderByShareUrl(e,t=""){let i=e.replace(/^\//,""),n=await this.request(`${this.getShareUrl()}/${i}`,"GET");if(n.includes("\u53D6\u6D88\u5206\u4EAB"))throw new Error("[Lanzou] \u8BE5\u6587\u4EF6\u5DF2\u53D6\u6D88\u5206\u4EAB");if(n.includes("\u6587\u4EF6\u4E0D\u5B58\u5728"))throw new Error("[Lanzou] \u6587\u4EF6\u4E0D\u5B58\u5728");return/class="fileinfo"|id="file"|文件描述/i.test(n)?[await this.getFilesByShareUrl(i,t,n)]:this.getFolderByShareUrl(t,n)}async getFolderByShareUrl(e,t){let i=df(t),n={};try{n=zl(i)}catch{n={}}let s=[],o=Array.from(t.matchAll(/(?:folderlink|mbxfolder)[^>]*href=["']\/?([^"']+)["'][^>]*>(.+?)<\//gi));for(let a of o)s.push({id:a[1],name_all:a[2].trim(),is_folder:!0});n.pwd=e||this.addition.share_password||"";for(let a=1;;a++){n.pg=String(a);let c=await this.request(`${this.getShareUrl()}/filemoreajax.php`,"POST",n),l;try{l=JSON.parse(c)}catch{break}if(l.zt!==1||!Array.isArray(l.text)||l.text.length===0)break;let d=l.text;s.push(...d.map(u=>({id:u.id,name_all:u.name_all||u.name,size:u.size,time:u.time,is_folder:!1,pwd:n.pwd})))}return s}async getFilesByShareUrl(e,t="",i,n){let s=e.replace(/^\//,""),o=(n||await this.probeShareDomain(s)).replace(/\/+$/,""),a=`${o}/${s}`,c=i;c||(c=await this.request(a,"GET")),c=df(c),c=A4(c);let l={},d="",u="",f={id:s,is_folder:!1};if(c.includes("pwdload")||c.includes("passwddiv")){let B=h4(c,"down_p");l=zl(B,c),l.p=t||this.addition.share_password||"";let C=B.match(/['"]?\/?ajax(?:file|m)\.php\?file=(\d+)['"]?/)||c.match(/['"]?\/?ajax(?:file|m)\.php\?file=(\d+)['"]?/)||B.match(/file\s*[:=]\s*['"]?(\d+)['"]?/)||c.match(/file\s*[:=]\s*['"]?(\d+)['"]?/)||B.match(/var\s+file_id\s*=\s*['"]?(\d+)['"]?/)||c.match(/var\s+file_id\s*=\s*['"]?(\d+)['"]?/),x=C?C[1]:"";if(!x)throw new Error("[Lanzou] \u672A\u627E\u5230\u6587\u4EF6 ID");let Q=await this.request(`${o}/ajaxfile.php?file=${x}`,"POST",l,a),R;try{R=JSON.parse(Q)}catch{throw new Error(`[Lanzou] ajaxfile.php \u54CD\u5E94\u683C\u5F0F\u9519\u8BEF: ${Q}`)}if(R.zt!==1)throw new Error(R.info||R.text||`[Lanzou] \u5BC6\u7801\u9519\u8BEF\u6216\u63D0\u53D6\u94FE\u63A5\u5931\u8D25 (zt=${R.zt})`);f.name_all=R.inf||"download",d=`${R.dom}/file`,u=`${d}/${R.url}`}else{let B=c.match(/]*?src=["']([^"']+)["']/i)||c.match(/href=["'](\/fn\?[^"']+)["']/i)||c.match(/["'](\/fn\?[^"']+)["']/i);if(!B)throw new Error("[Lanzou] \u672A\u627E\u5230\u4E0B\u8F7D\u9875\u9762 iframe \u53C2\u6570");let C=B[1],x=`${o}${C.startsWith("/")?"":"/"}${C}`,Q=await this.request(x,"GET",void 0,a),R=df(Q);l=zl(R,R);let U=R.match(/['"]?\/?ajax(?:file|m)\.php\?file=(\d+)['"]?/)||R.match(/file\s*[:=]\s*['"]?(\d+)['"]?/)||R.match(/file=(\d+)/)||R.match(/var\s+file_id\s*=\s*['"]?(\d+)['"]?/),T=U?U[1]:"";if(!T)throw new Error("[Lanzou] \u672A\u627E\u5230\u6587\u4EF6 ID");let D=await this.request(`${o}/ajaxfile.php?file=${T}`,"POST",l,x),b;try{b=JSON.parse(D)}catch{throw new Error(`[Lanzou] ajaxfile.php \u54CD\u5E94\u683C\u5F0F\u9519\u8BEF: ${D}`)}if(b.zt!==1)throw new Error(b.info||b.text||`[Lanzou] \u63D0\u53D6\u94FE\u63A5\u5931\u8D25 (zt=${b.zt})`);d=`${b.dom}/file`,u=`${d}/${b.url}`;let _=c.match(/(.+?) - 蓝奏云<\/title>|id="filenajax">(.+?)<\/div>|var filename = ['"](.+?)['"];|<div style="font-size[^>]*>([^<>]+)<\/div>|<div class="filethetext"[^>]*>([^<>]+)<\/div>/i);if(_){for(let v=1;v<_.length;v++)if(_[v]){f.name_all=_[v].trim();break}}}let g=c.match(/大小\W*([0-9.]+\s*[bkm]+)/i);g&&(f.size=g[1]);let p=c.match(/\d+\s*[秒天分小][钟时]?前|[昨前]天|\d{4}-\d{2}-\d{2}/);p&&(f.time=p[0]);let I=u,m="",w=!1;for(let B=0;B<3;B++){let C={Referer:d,"User-Agent":this.getUserAgent(),"Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"},x="down_ip=1";m&&(x+=`; acw_sc__v2=${m}`),C.Cookie=x;let Q;try{Q=await fetch(u,{method:"GET",headers:C,redirect:"manual"})}catch{if(B<2){await new Promise(U=>setTimeout(U,400*(B+1)));continue}throw new Error("[Lanzou] \u76F4\u94FE\u63A2\u6D4B\u7F51\u7EDC\u9519\u8BEF\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5")}if(Q.status===301||Q.status===302||Q.status===303||Q.status===307||Q.status===308){let U=Q.headers.get("location");if(U){I=new URL(U,u).toString(),w=!0;break}}if(Q.status===200&&Q.url&&Q.url!==u){I=Q.url,w=!0;break}if(Q.status===429||Q.status>=500){if(B<2){await new Promise(U=>setTimeout(U,400*(B+1)));continue}throw new Error(`[Lanzou] \u76F4\u94FE\u63A2\u6D4B\u8FD4\u56DE HTTP ${Q.status}\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5`)}let R=await Q.text();if(R.includes("acw_sc__v2")){m=Mp(R);continue}try{let U=zl(R,R);U.el="2",await new Promise(b=>setTimeout(b,1500));let T=await this.request(`${d}/ajax.php`,"POST",U,d),D=JSON.parse(T);if(D.url){I=D.url.startsWith("http")?D.url:new URL(D.url,d).toString(),w=!0;break}}catch{if(B<2){await new Promise(U=>setTimeout(U,600*(B+1)));continue}}break}if(!w)throw new Error("[Lanzou] \u76F4\u94FE\u89E3\u6790\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");return f.url=I,f}async getFileRealInfo(e){try{let t=await fetch(e,{method:"HEAD",headers:{"User-Agent":this.getUserAgent()}}),i=t.headers.get("content-length"),n=t.headers.get("last-modified");return{size:i?parseInt(i,10):void 0,time:n?new Date(n).toISOString():void 0}}catch{return{}}}async mkdir(e,t){await this.doupload({task:"2",parent_id:e||"-1",folder_name:t,folder_description:""})}async rename(e,t){await this.doupload({task:"46",file_id:e,file_name:t,type:"2"})}async move(e,t){await this.doupload({task:"20",file_id:e,folder_id:t})}async remove(e,t){t?await this.doupload({task:"3",folder_id:e}):await this.doupload({task:"6",file_id:e})}}});function Sj(r){let e={...r||{}};return e.type=e.type||"cookie",e.account=e.account||"",e.password=e.password||"",e.cookie=(e.cookie||"").trim(),e.root_folder_id=e.root_folder_id||(e.type==="url"?"":"-1"),e.share_password=e.share_password||"",e.baseUrl=e.baseUrl||"https://pc.woozooo.com",e.shareUrl=e.shareUrl||"https://pan.lanzoui.com",e.user_agent=e.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",e.repair_file_info=!!e.repair_file_info,e.order_by=e.order_by||"name",e.order_direction=e.order_direction||"asc",e}function gE(r,e){let t=!!r.is_folder||!!r.fol_id,i=r.name_all||r.name||"",n=e?.size!==void 0?e.size:f4(r.size||"0"),s=e?.time?e.time:u4(r.time||""),o=r.fol_id||r.id||"";return{name:i,size:n,is_dir:t,modified:s,sign:o,type:X(i,t),thumb:"",raw_url:r.url||""}}var zp,p4=J(()=>{"use strict";Be();He();g4();AE();zp=class{client;addition;pathIdCache=new Map;constructor(e,t){this.addition=Sj(e),this.client=new Gp(this.addition,t)}async init(){await this.client.init()}async checkCookieValid(){return this.client.checkCookieValid()}isUrlMode(){return this.addition.type==="url"}getRootId(){return this.addition.root_folder_id||(this.isUrlMode()?"":"-1")}async resolveFolderId(e){let t=this.getRootId(),i="/"+String(e||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${t}`)return t;let n=i.split("/").filter(Boolean),s=0,o=t,a="";for(let c=0;c<n.length;c++){let l="/"+n.slice(0,c+1).join("/"),d=this.pathIdCache.get(l);if(d!==void 0)o=d,s=c+1,a=l;else break}for(let c=s;c<n.length;c++){let l=n[c],d=(()=>{try{return decodeURIComponent(l)}catch{return l}})(),f=(this.isUrlMode()?await this.client.getFileOrFolderByShareUrl(o,this.addition.share_password):await this.client.getFolders(o)).find(h=>{if(!h.is_folder&&!h.fol_id)return!1;let g=h.name||h.name_all||"",p=h.fol_id||h.id||"";return g===l||g===d||p===l||p===d});if(!f)throw new Error(`[Lanzou] \u76EE\u5F55\u672A\u627E\u5230: ${l}`);o=f.fol_id||f.id||"",a="/"+n.slice(0,c+1).join("/"),this.pathIdCache.set(a,o)}return o}async resolveItem(e){let t="/"+String(e||"").split("/").filter(Boolean).join("/"),i=t.split("/").filter(Boolean);if(i.length===0)throw new Error("[Lanzou] \u8DEF\u5F84\u65E0\u6548");let n=i[i.length-1],s=(()=>{try{return decodeURIComponent(n)}catch{return n}})(),o="/"+i.slice(0,i.length-1).join("/"),a=await this.resolveFolderId(o),l=(this.isUrlMode()?await this.client.getFileOrFolderByShareUrl(a,this.addition.share_password):await this.client.getAllFiles(a)).find(u=>{let f=u.name_all||u.name||"",h=u.fol_id||u.id||"";return f===n||f===s||h===n||h===s});if(!l)throw new Error(`[Lanzou] \u6587\u4EF6\u6216\u76EE\u5F55\u672A\u627E\u5230: ${n}`);let d=!!(l.is_folder||l.fol_id);return d&&this.pathIdCache.set(t,l.fol_id||l.id||""),{item:l,parentId:a,isDir:d}}async list(e,t){let i=await this.resolveFolderId(t),s=(this.isUrlMode()?await this.client.getFileOrFolderByShareUrl(i,this.addition.share_password):await this.client.getAllFiles(i)).map(o=>gE(o));return de(s,this.addition.order_by==="name"?"file_name":this.addition.order_by==="size"?"size":"updated_at",this.addition.order_direction)}async get(e,t){let i=String(t||"").split("/").filter(Boolean);if(i.length===0||i[i.length-1]===this.getRootId()){let l=this.getRootId();return{name:l||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:l,type:1,raw_url:""}}let{item:n,isDir:s}=await this.resolveItem(t);if(s)return gE(n);let o=n.url;if(!o)try{if(this.isUrlMode()){let l=await this.client.getFilesByShareUrl(n.id||"",n.pwd||this.addition.share_password||"",void 0,await this.client.probeShareDomain(n.id||""));o=l.url,n.name_all=l.name_all||n.name_all,n.size=l.size||n.size}else{let l=await this.client.getFileShareUrlById(n.id||""),d=l?.f_id||l?.id;if(d){let u=await this.client.getFilesByShareUrl(d,l.pwd||"",void 0);o=u.url,u.name_all&&(n.name_all=u.name_all),u.size&&(n.size=u.size)}}}catch(l){throw console.error(`[Lanzou] \u89E3\u6790\u4E0B\u8F7D\u94FE\u63A5\u5931\u8D25 (${n.name_all||n.name}):`,l.message),new Error(`[Lanzou] \u83B7\u53D6\u4E0B\u8F7D\u76F4\u94FE\u5931\u8D25 (${n.name_all||n.name}): ${l.message}`)}if(!o)throw new Error(`[Lanzou] \u672A\u80FD\u83B7\u53D6\u5230\u4E0B\u8F7D\u76F4\u94FE (${n.name_all||n.name||t})`);let a;if(this.addition.repair_file_info&&o)try{a=await this.client.getFileRealInfo(o)}catch{}let c=gE(n,a);return c.raw_url=o||"",c.raw_url_headers={"User-Agent":this.client.getUserAgent()},c}async mkdir(e,t){if(this.isUrlMode())throw new Error("[Lanzou] \u5206\u4EAB\u94FE\u63A5\u6A21\u5F0F\u4E0D\u652F\u6301\u65B0\u5EFA\u6587\u4EF6\u5939");let i=String(t||"").split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFolderId(s);await this.client.mkdir(o,n)}async rename(e,t,i){if(this.isUrlMode())throw new Error("[Lanzou] \u5206\u4EAB\u94FE\u63A5\u6A21\u5F0F\u4E0D\u652F\u6301\u91CD\u547D\u540D");let{item:n,isDir:s}=await this.resolveItem(t);if(s)throw new Error("[Lanzou] \u84DD\u594F\u4E91\u4E0D\u652F\u6301\u91CD\u547D\u540D\u6587\u4EF6\u5939");await this.client.rename(n.id||"",i)}async remove(e,t,i){if(this.isUrlMode())throw new Error("[Lanzou] \u5206\u4EAB\u94FE\u63A5\u6A21\u5F0F\u4E0D\u652F\u6301\u5220\u9664");let{item:n,isDir:s}=await this.resolveItem(t);await this.client.remove(n.fol_id||n.id||"",s)}async move(e,t,i,n,s){if(this.isUrlMode())throw new Error("[Lanzou] \u5206\u4EAB\u94FE\u63A5\u6A21\u5F0F\u4E0D\u652F\u6301\u79FB\u52A8");let{item:o,isDir:a}=await this.resolveItem(n);if(a)throw new Error("[Lanzou] \u84DD\u594F\u4E91\u4E0D\u652F\u6301\u79FB\u52A8\u6587\u4EF6\u5939");let c=String(t).split("/").filter(Boolean),l=await this.resolveFolderId("/"+c.join("/"));await this.client.move(o.id||"",l)}async copy(){throw new Error("[Lanzou] \u84DD\u594F\u4E91\u4E0D\u652F\u6301\u76F4\u63A5\u590D\u5236\u6587\u4EF6")}async put(){throw new Error("[Lanzou] Cloudflare Worker \u73AF\u5883\u6682\u4E0D\u652F\u6301\u76F4\u63A5\u6D41\u5F0F\u5199\u5165\uFF0C\u8BF7\u4F7F\u7528\u7F51\u9875\u7AEF\u8FDB\u884C\u6587\u4EF6\u4E0A\u4F20")}}});function Dj(r){let e=r.replace(/\s+/g,""),t=atob(e),i=new Uint8Array(t.length);for(let n=0;n<t.length;n++)i[n]=t.charCodeAt(n);return i}function Qj(r){let e="";for(let t=0;t<r.length;t++)e+=String.fromCharCode(r[t]);return btoa(e)}function _j(r){return Array.from(r).map(e=>e.toString(16).padStart(2,"0")).join("")}function pE(r){let e=0n;for(let t=0;t<r.length;t++)e=e<<8n|BigInt(r[t]);return e}function Fj(r,e){let t=new Uint8Array(e),i=r;for(let n=e-1;n>=0;n--)t[n]=Number(i&0xffn),i>>=8n;return t}function Pj(r,e,t){let i=1n;for(r=r%t;e>0n;)e%2n===1n&&(i=i*r%t),r=r*r%t,e/=2n;return i}function Tj(r){let e=r.replace(/-----BEGIN[^-]+-----/g,"").replace(/-----END[^-]+-----/g,"").replace(/\s+/g,""),t=Dj(e),i=0;function n(){let l=t[i++],d=t[i++];if(d&128){let f=d&127;d=0;for(let h=0;h<f;h++)d=d<<8|t[i++]}return{tag:l,length:d,dataStart:i}}let s=[];function o(l,d){let u=l;for(;u<d;){let f=t[u++],h=t[u++];if(h&128){let p=h&127;h=0;for(let I=0;I<p;I++)h=h<<8|t[u++]}let g=u;if(u+=h,f===2){let p=t.subarray(g,g+h);p[0]===0&&p.length>1&&(p=p.subarray(1)),s.push(p)}else f===48||(f&32)!==0?o(g,g+h):f===3&&t[g]===0&&o(g+1,g+h)}}if(o(0,t.length),s.length<2)throw new Error("Failed to parse RSA public key: insufficient integers found");let a=s[0],c=s[1];if(a.length<c.length){let l=a;a=c,c=l}return{n:pE(a),e:pE(c),keyLength:a.length}}function Jp(r,e,t=!1){let{n:i,e:n,keyLength:s}=Tj(e),o=typeof r=="string"?new TextEncoder().encode(r):r;if(o.length>s-11)throw new Error(`Data too long for RSA key size: ${o.length} > ${s-11}`);let a=s-o.length-3,c=new Uint8Array(a),l=new Uint8Array(a*2);crypto.getRandomValues(l);let d=0;for(let p=0;p<a;p++){let I=l[d++];for(;I===0;)d>=l.length&&(crypto.getRandomValues(l),d=0),I=l[d++];c[p]=I}let u=new Uint8Array(s);u[0]=0,u[1]=2,u.set(c,2),u[2+a]=0,u.set(o,3+a);let f=pE(u),h=Pj(f,n,i),g=Fj(h,s);return t?_j(g):Qj(g)}function m4(r,e){let t=typeof e=="string"?$r.default.enc.Utf8.parse(e.slice(0,16)):$r.default.lib.WordArray.create(Array.from(e.slice(0,16)),16),i=$r.default.enc.Utf8.parse(r);return $r.default.AES.encrypt(i,t,{mode:$r.default.mode.ECB,padding:$r.default.pad.Pkcs7}).ciphertext.toString($r.default.enc.Hex)}function uf(r,e){return $r.default.HmacSHA1(r,e).toString($r.default.enc.Hex)}function y4(r){return typeof r=="string"?$r.default.enc.Utf8.parse(r):$r.default.lib.WordArray.create(r)}function Jl(r){return $r.default.MD5(y4(r)).toString($r.default.enc.Hex)}function I4(r){return $r.default.MD5(y4(r)).toString($r.default.enc.Base64)}function jl(r="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"){return r.replace(/[xy]/g,e=>{let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}function mE(){return"0."+Math.floor(Math.random()*1e17).toString().padStart(17,"0")}var $r,jp=J(()=>{"use strict";$r=xr(Oo(),1)});function Uj(r,e,t){let i=r?r.split(";").map(o=>o.trim()):[],n=`${e}=${t}`,s=i.findIndex(o=>o.startsWith(`${e}=`));return s!==-1?i[s]=n:i.push(n),i.filter(Boolean).join("; ")}function Rj(r,e){if(!e)return r;let t=r,i=e.split(/,(?=\s*[a-zA-Z0-9_\-]+=[^;]+)/);for(let n of i){let s=n.split(";")[0].trim(),o=s.indexOf("=");if(o>0){let a=s.slice(0,o).trim(),c=s.slice(o+1).trim();t=Uj(t,a,c)}}return t}function Nj(r){let e=r;if(typeof e.getSetCookie=="function"){let i=e.getSetCookie();if(i.length>0)return i}let t=r.get("set-cookie");return t?[t]:[]}function w4(r){let e=r.replace(/("id"\s*:\s*)(-?\d{16,})(?=\s*[,}])/g,'$1"$2"');return JSON.parse(e)}function yE(r){return r.protocol==="https:"&&qj.has(r.hostname)}function IE(r){try{let e=new URL(r,"https://open.e.189.cn");return!!e.searchParams.get("lt")&&!!e.searchParams.get("reqId")}catch{return!1}}function wE(r){try{let e=new URL(r,"https://open.e.189.cn");return e.hostname==="cloud.189.cn"&&(e.pathname==="/web/main"||e.pathname==="/main.action")}catch{return!1}}var qj,$p,x4=J(()=>{"use strict";jp();qj=new Set(["cloud.189.cn","open.e.189.cn"]);$p=class{addition;cookie="";cookieDirty=!1;sessionKey="";rsa={pubKey:"",pkId:"",expire:0};constructor(e,t){this.addition=e,this.cookie=(e.cookie||"").trim()}getCookie(){return this.cookie}consumePendingCookie(){return this.cookieDirty?(this.cookieDirty=!1,this.cookie):null}getRootId(){return this.addition.root_folder_id||"-11"}setSessionKey(e){this.sessionKey=e}getDownloadHeaders(){let e={"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",Referer:"https://cloud.189.cn/"};return this.cookie&&(e.Cookie=this.cookie),e}async updateCookie(e){let t=Nj(e);if(t.length===0)return;let i=t.reduce((n,s)=>Rj(n,s),this.cookie);i!==this.cookie&&(this.cookie=i,this.cookieDirty=!0)}async followRedirectsWithCookies(e,t){let i=e,n=[i];for(let s=0;s<=8;s++){let o=new URL(i);if(!yE(o))throw new Error(o.protocol!=="https:"?`[189Cloud] \u767B\u5F55\u91CD\u5B9A\u5411\u5FC5\u987B\u4F7F\u7528 HTTPS: ${o.origin}`:`[189Cloud] \u4E0D\u53D7\u4FE1\u4EFB\u7684\u767B\u5F55\u91CD\u5B9A\u5411\u5730\u5740: ${o.origin}`);let a={...t};s>0&&(a.Referer=i),this.cookie&&(a.Cookie=this.cookie);let c=await fetch(i,{method:"GET",headers:a,redirect:"manual"});await this.updateCookie(c.headers);let l=c.headers.get("location");if(!(c.status>=300&&c.status<400)||!l){let f=i;if(c.url&&c.url!==i){let h=new URL(c.url,i);if(IE(h.toString())||wE(h.toString())){if(!yE(h))throw new Error(h.protocol!=="https:"?`[189Cloud] \u767B\u5F55\u91CD\u5B9A\u5411\u5FC5\u987B\u4F7F\u7528 HTTPS: ${h.origin}`:`[189Cloud] \u4E0D\u53D7\u4FE1\u4EFB\u7684\u767B\u5F55\u91CD\u5B9A\u5411\u5730\u5740: ${h.origin}`);f=h.toString(),n.push(f)}}return{response:c,url:f,urls:n}}if(s===8)throw new Error("[189Cloud] \u767B\u5F55\u91CD\u5B9A\u5411\u6B21\u6570\u8FC7\u591A");let u=new URL(l,i);if(!yE(u))throw new Error(u.protocol!=="https:"?`[189Cloud] \u767B\u5F55\u91CD\u5B9A\u5411\u5FC5\u987B\u4F7F\u7528 HTTPS: ${u.origin}`:`[189Cloud] \u4E0D\u53D7\u4FE1\u4EFB\u7684\u767B\u5F55\u91CD\u5B9A\u5411\u5730\u5740: ${u.origin}`);i=u.toString(),n.push(i)}throw new Error("[189Cloud] \u767B\u5F55\u91CD\u5B9A\u5411\u5931\u8D25")}async resolveLoginUrl(e,t){let i=e;for(let n=0;n<3;n++){let s=new URL(e);s.searchParams.set("noCache",mE());let o=await this.followRedirectsWithCookies(s.toString(),t);if(i=o.url,IE(o.url)||wE(o.url))return o.url;let a=o.urls.find(c=>IE(c));if(a)return a;n<2&&await new Promise(c=>setTimeout(c,150*(n+1)))}return i}async login(e={}){if(this.cookie&&!e.force)return;let t="https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action",i={"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",Referer:"https://cloud.189.cn/"};this.cookie&&(i.Cookie=this.cookie);let n=await this.resolveLoginUrl(t,i);if(wE(n))return;if(!this.addition.username||!this.addition.password){if(this.cookie)return;throw new Error("[189Cloud] \u8D26\u53F7\u6216\u5BC6\u7801\u4E3A\u7A7A\uFF0C\u4E14\u672A\u63D0\u4F9B\u6709\u6548 Cookie")}let s;try{s=new URL(n,"https://open.e.189.cn")}catch{s=new URL("https://open.e.189.cn"+n)}let o=s.searchParams.get("lt")||"",a=s.searchParams.get("reqId")||"",c=s.searchParams.get("appId")||"cloud";if(!o||!a)throw new Error("[189Cloud] \u767B\u5F55\u8DF3\u8F6C\u53C2\u6570\u4E0D\u5B8C\u6574\uFF0C\u672A\u83B7\u53D6\u5230 lt \u6216 reqId");let l=()=>{let x={"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",lt:o,reqid:a,referer:n,origin:"https://open.e.189.cn","Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",Accept:"application/json;charset=UTF-8"};return this.cookie&&(x.Cookie=this.cookie),x},d=await fetch("https://open.e.189.cn/api/logbox/oauth2/appConf.do",{method:"POST",headers:l(),body:new URLSearchParams({version:"2.0",appKey:c})});await this.updateCookie(d.headers);let u=await d.json();if(u.result!=="0"||!u.data)throw new Error(`[189Cloud] \u83B7\u53D6 AppConf \u5931\u8D25: ${u.msg||JSON.stringify(u)}`);let f=await fetch("https://open.e.189.cn/api/logbox/config/encryptConf.do",{method:"POST",headers:l(),body:new URLSearchParams({appId:c})});await this.updateCookie(f.headers);let h=await f.json();if(h.result!==0||!h.data?.pubKey)throw new Error(`[189Cloud] \u83B7\u53D6 EncryptConf \u5931\u8D25: ${JSON.stringify(h)}`);let g=h.data.pre||"",p=h.data.pubKey,I=g+Jp(this.addition.username,p,!0),m=g+Jp(this.addition.password,p,!0),w={version:"v2.0",apToken:"",appKey:c,accountType:u.data.accountType||"01",userName:I,epd:m,captchaType:"",validateCode:"",smsValidateCode:"",captchaToken:"",returnUrl:u.data.returnUrl||"https://cloud.189.cn/main.action",mailSuffix:u.data.mailSuffix||"@189.cn",dynamicCheck:"FALSE",clientType:String(u.data.clientType??"10010"),cb_SaveName:"3",isOauth2:String(u.data.isOauth2??!1),state:"",paramId:u.data.paramId||""},B=await fetch("https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do",{method:"POST",headers:{...l()},body:new URLSearchParams(w)});await this.updateCookie(B.headers);let C=await B.json();if(C.result!==0){let x=C.msg||"\u767B\u5F55\u5931\u8D25";throw x.includes("\u9A8C\u8BC1\u7801")||x.includes("\u6ED1\u5757")||x.includes("\u8BBE\u5907\u9501")?new Error(`[189Cloud] \u767B\u5F55\u89E6\u53D1\u9A8C\u8BC1\u7801/\u8BBE\u5907\u4FDD\u62A4: ${x}\u3002\u8BF7\u5728\u6D4F\u89C8\u5668\u767B\u5F55\u540E\u590D\u5236 Cookie \u586B\u5165\u914D\u7F6E\u3002`):new Error(`[189Cloud] \u767B\u5F55\u5931\u8D25: ${x}`)}C.toUrl&&await this.followRedirectsWithCookies(C.toUrl,{"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"})}async request(e,t={}){let i=t.method||"GET",n=t.retryOnInvalidSession!==!1,s=new URL(e);if(s.searchParams.set("noCache",mE()),t.params)for(let[f,h]of Object.entries(t.params))h!==void 0&&s.searchParams.set(f,h);let o={Accept:"application/json;charset=UTF-8",Referer:"https://cloud.189.cn/","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"};this.cookie&&(o.Cookie=this.cookie);let a;t.body&&(o["Content-Type"]="application/x-www-form-urlencoded; charset=UTF-8",a=new URLSearchParams(t.body).toString());let c=await fetch(s.toString(),{method:i,headers:o,body:a});await this.updateCookie(c.headers);let l=await c.text(),d;try{d=w4(l)}catch{throw new Error(`[189Cloud] \u975E\u9884\u671F\u54CD\u5E94: ${l.slice(0,200)}`)}if(d.errorCode==="InvalidSessionKey"||d.res_code==="InvalidSessionKey"||String(d.res_code)==="1010"){if(n)return await this.login({force:!0}),this.request(e,{...t,retryOnInvalidSession:!1});throw new Error(d.errorMsg||d.res_message||"[189Cloud] \u767B\u5F55\u4F1A\u8BDD\u5DF2\u5931\u6548")}if(d.errorCode)throw new Error(d.errorMsg||`[189Cloud] API \u9519\u8BEF: ${d.errorCode}`);if(!c.ok)throw new Error(d.errorMsg||d.res_message||`[189Cloud] HTTP \u8BF7\u6C42\u5931\u8D25 (${c.status})`);if(d.res_code!==void 0&&String(d.res_code)!=="0")throw new Error(d.res_message||`189 API \u9519\u8BEF (res_code: ${d.res_code})`);return d}async getFilesPage(e,t,i){let n=this.addition.order_by||"lastOpTime",s=(this.addition.order_direction||"desc")==="desc"?"true":"false",o=await this.request("https://cloud.189.cn/api/open/file/listFiles.action",{method:"GET",params:{pageSize:i,pageNum:String(t),mediaType:"0",folderId:e||this.getRootId(),iconOption:"5",orderBy:n,descending:s}}),a=o.fileListAO?.count,c=typeof a=="number"?a:typeof a=="string"&&a.trim()!==""?Number(a):NaN;if(!o.fileListAO||typeof o.fileListAO!="object"||Array.isArray(o.fileListAO)||!Number.isFinite(c)||c<0||!Array.isArray(o.fileListAO.fileList)||!Array.isArray(o.fileListAO.folderList))throw new Error("[189Cloud] \u6587\u4EF6\u5217\u8868\u54CD\u5E94\u7F3A\u5C11\u6709\u6548\u7684 fileListAO \u6570\u7EC4\u5B57\u6BB5");return o}async validateRoot(e){await this.getFilesPage(e,1,"1")}async getFiles(e,t){let i=[],n=[],s=1,o="60";for(;;){if(t?.budget){if(t.budget.used>=t.budget.limit){console.warn("[189Cloud] Cloudflare Worker subrequest budget limit reached.");break}t.budget.used++}let c=(await this.getFilesPage(e,s,o)).fileListAO;if(Number(c.count)===0)break;let l=c.fileList||[],d=c.folderList||[];if(n.push(...d),i.push(...l),t?.findName&&(t.findIsDir&&d.some(u=>u.name===t.findName)||!t.findIsDir&&l.some(u=>u.name===t.findName))||l.length+d.length<parseInt(o,10))break;s++}return{files:i,folders:n}}async getDownloadUrl(e){let t=await this.request("https://cloud.189.cn/api/portal/getFileInfo.action",{method:"GET",params:{fileId:e}}),i=t.fileDownloadUrl||t.downloadUrl;if(!i)throw new Error(`[189Cloud] \u83B7\u53D6\u6587\u4EF6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25 (fileId: ${e})`);let n=i.startsWith("//")?"https:"+i:i;n=n.replace(/^http:\/\//i,"https://");try{let s=await fetch(n,{method:"GET",headers:this.getDownloadHeaders(),redirect:"manual"}),o=s.headers.get("location");s.status===302&&o&&(n=o.replace(/^http:\/\//i,"https://"))}catch{}return n}async getSessionKey(){let e=await this.request("https://cloud.189.cn/v2/getUserBriefInfo.action",{method:"GET"}),t=String(e.sessionKey||"");if(!t)throw new Error("[189Cloud] \u83B7\u53D6\u4E0A\u4F20 SessionKey \u5931\u8D25");return t}async getResKey(){if(this.rsa.pubKey&&this.rsa.pkId&&this.rsa.expire>Date.now())return this.rsa;let e=await this.request("https://cloud.189.cn/api/security/generateRsaKey.action",{method:"GET"}),t=String(e.pubKey||""),i=String(e.pkId||"");if(!t||!i)throw new Error("[189Cloud] \u83B7\u53D6\u4E0A\u4F20 RSA \u516C\u94A5\u5931\u8D25");return this.rsa={pubKey:t,pkId:i,expire:Number(e.expire)||Date.now()+5*6e4},this.rsa}async uploadRequest(e,t){this.sessionKey||(this.sessionKey=await this.getSessionKey());let i=String(Date.now()),n=jl(),s=jl("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx").slice(0,16+Math.floor(Math.random()*17)),o=Object.keys(t).sort().map(p=>`${p}=${t[p]}`).join("&"),a=m4(o,s.slice(0,16)),c=uf(`SessionKey=${this.sessionKey}&Operate=GET&RequestURI=${e}&Date=${i}¶ms=${a}`,s),{pubKey:l,pkId:d}=await this.getResKey(),u={accept:"application/json;charset=UTF-8",SessionKey:this.sessionKey,Signature:c,"X-Request-Date":i,"X-Request-ID":n,EncryptionText:Jp(s,l,!1),PkId:d};this.cookie&&(u.Cookie=this.cookie);let f=await fetch(`https://upload.cloud.189.cn${e}?params=${a}`,{method:"GET",headers:u});await this.updateCookie(f.headers);let h=await f.text();if(!f.ok)throw new Error(`[189Cloud] \u4E0A\u4F20\u63A5\u53E3 HTTP ${f.status}: ${h.slice(0,200)}`);let g;try{g=w4(h)}catch{throw new Error(`[189Cloud] \u4E0A\u4F20\u63A5\u53E3\u8FD4\u56DE\u65E0\u6548\u54CD\u5E94: ${h.slice(0,200)}`)}if(g.code!=="SUCCESS")throw new Error(g.msg||g.message||`[189Cloud] \u4E0A\u4F20\u63A5\u53E3\u5931\u8D25: ${e}`);return g}async createMultiUpload(e,t,i,n){let s=await this.getSessionKey();this.sessionKey=s;let o={parentFolderId:e,fileName:encodeURIComponent(t).replace(/%20/g,"+"),fileSize:String(i),sliceSize:String(10*1024*1024)},a;try{a=await this.uploadRequest("/person/initMultiUpload",{...o,fileMd5:n,sliceMd5:n})}catch(l){let d=String(l?.message||l);if(!/InfoSecurityErrorCode|file md5 is in black list|security check not pass/i.test(d))throw l;a=await this.uploadRequest("/person/initMultiUpload",{...o,lazyCheck:"1"})}let c=String(a.data?.uploadFileId||"");if(!c)throw new Error("[189Cloud] \u521B\u5EFA\u4E0A\u4F20\u4F1A\u8BDD\u5931\u8D25\uFF1A\u7F3A\u5C11 uploadFileId");return{uploadFileId:c,fileDataExists:String(a.data?.fileDataExists||"0")==="1",sessionKey:s}}async getMultiUploadUrls(e,t,i){let s=(await this.uploadRequest("/person/getMultiUploadUrls",{partInfo:`${t}-${I4(i)}`,uploadFileId:e})).uploadUrls?.[`partNumber_${t}`];if(!s?.requestURL)throw new Error(`[189Cloud] \u83B7\u53D6\u7B2C ${t} \u4E2A\u5206\u7247\u4E0A\u4F20\u5730\u5740\u5931\u8D25`);return s}async commitMultiUpload(e,t,i){await this.uploadRequest("/person/commitMultiUploadFile",{uploadFileId:e,fileMd5:t,sliceMd5:i,lazyCheck:"1",opertype:"3"})}async mkdir(e,t){await this.request("https://cloud.189.cn/api/open/file/createFolder.action",{method:"POST",body:{parentFolderId:e||this.getRootId(),folderName:t}})}async rename(e,t,i){let n=t?"https://cloud.189.cn/api/open/file/renameFolder.action":"https://cloud.189.cn/api/open/file/renameFile.action",s=t?{folderId:e,destFolderName:i}:{fileId:e,destFileName:i};await this.request(n,{method:"POST",body:s})}async batchTask(e,t,i=""){let n=t.map(s=>({fileId:s.id,fileName:s.name,isFolder:s.isFolder?1:0}));await this.request("https://cloud.189.cn/api/open/batch/createBatchTask.action",{method:"POST",body:{type:e,targetFolderId:i,taskInfos:JSON.stringify(n)}})}async move(e,t,i,n){await this.batchTask("MOVE",[{id:e,name:i,isFolder:t}],n)}async copy(e,t,i,n){await this.batchTask("COPY",[{id:e,name:i,isFolder:t}],n)}async remove(e,t,i){await this.batchTask("DELETE",[{id:e,name:i,isFolder:t}],"")}async getCapacityInfo(){return this.request("https://cloud.189.cn/api/portal/getUserSizeInfo.action",{method:"GET"})}}});function Lj(r){return Buffer.from(JSON.stringify(r),"utf8").toString("base64")}function C4(r){try{let e=JSON.parse(Buffer.from(r,"base64").toString("utf8"));if(!e||!e.uploadFileId||!e.sessionKey||!e.fileMd5||!Number.isInteger(e.partCount)||e.partCount<1||!Number.isInteger(e.chunkSize)||e.chunkSize<1)throw new Error("invalid upload session");return e}catch{throw new Error("[189Cloud] \u4E0A\u4F20\u4F1A\u8BDD\u65E0\u6548\u6216\u5DF2\u635F\u574F")}}function b4(r){if(!r)return new Date().toISOString();try{let e=new Date(r);if(!isNaN(e.getTime()))return e.toISOString()}catch{}return new Date().toISOString()}function E4(r){return{name:r.name,size:0,is_dir:!0,modified:b4(r.lastOpTime),sign:String(r.id),type:1,thumb:"",raw_url:""}}function B4(r){return{name:r.name,size:r.size||0,is_dir:!1,modified:b4(r.lastOpTime),sign:String(r.id),type:X(r.name,!1),thumb:r.icon?.smallUrl||r.icon?.largeUrl||"",raw_url:""}}function Hj(r){let e={...r||{}};return e.username=e.username||"",e.password=e.password||"",e.cookie=(e.cookie||"").trim(),e.root_folder_id=e.root_folder_id||"-11",e.order_by=e.order_by||"lastOpTime",e.order_direction=e.order_direction||"desc",e}var Oj,Mj,Vp,v4=J(()=>{"use strict";Be();He();x4();jp();Oj=45,Mj=10*1024*1024;Vp=class{client;addition;pathIdCache=new Map;budget={used:0,limit:Oj};constructor(e,t){this.addition=Hj(e),this.client=new $p(this.addition,t)}async init(){await this.client.login()}consumePendingCookie(){return this.client.consumePendingCookie()}async resolveFolderId(e){let t=this.client.getRootId(),i="/"+String(e||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${t}`)return t;let n=i.split("/").filter(Boolean),s=0,o=t,a="";for(let c=0;c<n.length;c++){let l="/"+n.slice(0,c+1).join("/"),d=this.pathIdCache.get(l);if(d!==void 0)o=d,s=c+1,a=l;else break}for(let c=s;c<n.length;c++){let l=n[c],d=(()=>{try{return decodeURIComponent(l)}catch{return l}})(),{folders:u}=await this.client.getFiles(o,{findName:d,findIsDir:!0,budget:this.budget}),f=u.find(h=>h.name===l||h.name===d||String(h.id)===l||String(h.id)===d);if(!f)throw new Error(`[189Cloud] \u76EE\u5F55\u672A\u627E\u5230: ${l}`);o=String(f.id),a="/"+n.slice(0,c+1).join("/"),this.pathIdCache.set(a,o)}return o}async resolveFile(e){let t=String(e||"").split("/").filter(Boolean);if(t.length===0)throw new Error("[189Cloud] \u8DEF\u5F84\u65E0\u6548");let i=t[t.length-1],n=(()=>{try{return decodeURIComponent(i)}catch{return i}})(),s="/"+t.slice(0,t.length-1).join("/"),o=await this.resolveFolderId(s),{files:a,folders:c}=await this.client.getFiles(o,{findName:n,budget:this.budget}),l=a.find(u=>u.name===i||u.name===n||String(u.id)===i||String(u.id)===n);if(l)return{file:l,parentId:o,isDir:!1};let d=c.find(u=>u.name===i||u.name===n||String(u.id)===i||String(u.id)===n);if(d)return{file:d,parentId:o,isDir:!0};throw new Error(`[189Cloud] \u6587\u4EF6\u6216\u76EE\u5F55\u672A\u627E\u5230: ${i}`)}async list(e,t){this.budget.used=0;let i=await this.resolveFolderId(t),{files:n,folders:s}=await this.client.getFiles(i,{budget:this.budget}),o=[...s.map(E4),...n.map(B4)];return de(o,this.addition.order_by==="filename"?"file_name":this.addition.order_by==="fileSize"?"size":"updated_at",this.addition.order_direction)}async get(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean);if(i.length===0||i[i.length-1]===this.client.getRootId()){let a=this.client.getRootId();return{name:a,size:0,is_dir:!0,modified:new Date().toISOString(),sign:a,type:1,raw_url:""}}let{file:n,isDir:s}=await this.resolveFile(t);if(s)return E4(n);let o=B4(n);try{o.raw_url=await this.client.getDownloadUrl(String(n.id)),o.raw_url_headers=this.client.getDownloadHeaders()}catch(a){console.warn(`[189Cloud] \u83B7\u53D6 ${n.name} \u4E0B\u8F7D\u5730\u5740\u5931\u8D25:`,a.message)}return o}async mkdir(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFolderId(s);await this.client.mkdir(o,n)}async rename(e,t,i){this.budget.used=0;let{file:n,isDir:s}=await this.resolveFile(t);await this.client.rename(String(n.id),s,i)}async remove(e,t,i){this.budget.used=0;let{file:n,isDir:s}=await this.resolveFile(t);await this.client.remove(String(n.id),s,n.name)}async move(e,t,i,n,s){this.budget.used=0;let{file:o,isDir:a}=await this.resolveFile(n),c=String(t).split("/").filter(Boolean),l=await this.resolveFolderId("/"+c.join("/"));await this.client.move(String(o.id),a,o.name,l)}async copy(e,t,i,n,s){this.budget.used=0;let{file:o,isDir:a}=await this.resolveFile(n),c=String(t).split("/").filter(Boolean),l=await this.resolveFolderId("/"+c.join("/"));await this.client.copy(String(o.id),a,o.name,l)}async put(e,t,i){let n=String(t||"").split("/").filter(Boolean),s=n.pop();if(!s)throw new Error("[189Cloud] \u4E0A\u4F20\u8DEF\u5F84\u65E0\u6548");let o="/"+n.join("/"),a=await this.createUploadSession(o,o,s,i.length,Jl(i));if(a.reuse)return;let c=[];for(let l=1;l<=a.partCount;l++){let d=(l-1)*a.chunkSize,u=i.subarray(d,Math.min(d+a.chunkSize,i.length)),f=await this.uploadPart(a.session,l,u);c.push(f.partMd5)}await this.completeUploadSession(a.session,c)}async createUploadSession(e,t,i,n,s){let o=Mj,a=String(s||"").trim().toLowerCase();if(!/^[a-f0-9]{32}$/.test(a))return{reuse:!1,requiresMd5:!0,partCount:0,chunkSize:o,session:""};this.budget.used=0;let c=Math.max(1,Math.ceil(Math.max(0,Number(n)||0)/o)),l=await this.resolveFolderId(t||"/"),d=await this.client.createMultiUpload(l,i,Math.max(0,Number(n)||0),a);return d.fileDataExists?(await this.client.commitMultiUpload(d.uploadFileId,a,a),{reuse:!0,partCount:0,chunkSize:o,session:""}):{reuse:!1,partCount:c,chunkSize:o,session:Lj({uploadFileId:d.uploadFileId,sessionKey:d.sessionKey,fileMd5:a,size:Math.max(0,Number(n)||0),partCount:c,chunkSize:o})}}async uploadPart(e,t,i){let n=C4(e);if(!Number.isInteger(t)||t<1||t>n.partCount)throw new Error(`[189Cloud] \u5206\u7247\u5E8F\u53F7\u65E0\u6548: ${t}`);this.client.setSessionKey(n.sessionKey);let s=await this.client.getMultiUploadUrls(n.uploadFileId,t,i),o={};if(s.requestHeader){let c=s.requestHeader;try{c=decodeURIComponent(c)}catch{}for(let l of c.split("&")){let d=l.indexOf("=");d<=0||(o[l.slice(0,d)]=l.slice(d+1))}}let a=await fetch(s.requestURL,{method:"PUT",headers:o,body:i});if(!a.ok){let c=await a.text().catch(()=>"");throw new Error(`[189Cloud] \u4E0A\u4F20\u7B2C ${t}/${n.partCount} \u5206\u7247\u5931\u8D25: HTTP ${a.status} ${c}`)}return{partMd5:Jl(i)}}async completeUploadSession(e,t=[]){let i=C4(e);this.client.setSessionKey(i.sessionKey);let n=t.map(o=>String(o||"").trim().toLowerCase()).filter(o=>/^[a-f0-9]{32}$/.test(o));if(n.length!==i.partCount)throw new Error("[189Cloud] \u5206\u7247\u6821\u9A8C\u4FE1\u606F\u4E0D\u5B8C\u6574\uFF0C\u65E0\u6CD5\u63D0\u4EA4\u4E0A\u4F20");let s=i.partCount===1?i.fileMd5:Jl(n.join(` -`)).toUpperCase();await this.client.commitMultiUpload(i.uploadFileId,i.fileMd5,s)}}});function jj(){return{clientType:zj,version:Gj,channelId:Jj,clientSn:"unknown",model:"PJX110",osFamily:"Android",osVersion:"35",networkAccessMode:"WIFI",telecomsOperator:"46011"}}function S4(r){try{return new URL(r).pathname}catch{return r}}function $j(r,e,t,i,n){let s=`SessionKey=${e}&Operate=${t}&RequestURI=${S4(i)}&Date=${n}`;return uf(s,r).toUpperCase()}function Vj(r,e,t,i,n){let s=`AppKey=${e}&Operate=${t}&RequestURI=${S4(i)}&Timestamp=${n}`;return uf(s,r).toUpperCase()}function Yj(r){let e=r.replace(/("id"\s*:\s*)(-?\d{16,})(?=\s*[,}])/g,'$1"$2"');return JSON.parse(e)}function Wj(r){let e=r.res_code;return typeof e=="number"?e!==0:typeof e=="string"?e!==""&&e!=="0":!!(r.code&&r.code!=="SUCCESS"||r.errorCode||r.error)}function Xj(r){switch(r){case"filename":return"1";case"filesize":return"2";case"lastOpTime":return"3";default:return"1"}}function Zj(r){return r==="desc"?"true":"false"}var k4,Kj,Gj,zj,Jj,Oi,xE,Yp,D4=J(()=>{"use strict";jp();k4="600100885",Kj="fe5734c74c2f96a38157f420b32dc995",Gj="6.5.5",zj="FAMILY_TV",Jj="home02",Oi="https://api.cloud.189.cn",xE="EcloudTV/6.5.5 (PJX110; unknown; home02) Android/35";Yp=class{addition;tokenInfo=null;persistAccessToken;constructor(e,t){this.addition=e,this.persistAccessToken=t}isFamily(){return this.addition.type==="family"}getFamilyId(){return this.addition.family_id||""}async init(){if(!this.addition.access_token)throw new Error("[189TV] access_token \u4E3A\u7A7A\uFF1A\u8BF7\u5148\u5728\u522B\u5904\u626B\u7801\u767B\u5F55\u83B7\u53D6 access_token \u540E\u586B\u5165\u914D\u7F6E");await this.login()}signatureHeader(e,t,i){let n=new Date().toUTCString(),s=i?this.tokenInfo?.familySessionKey||"":this.tokenInfo?.sessionKey||"",o=i?this.tokenInfo?.familySessionSecret||"":this.tokenInfo?.sessionSecret||"";return{Date:n,SessionKey:s,"X-Request-ID":jl(),Signature:$j(o,s,t,e,n)}}appKeySignatureHeader(e,t){let i=Date.now();return{Timestamp:String(i),"X-Request-ID":jl(),AppKey:k4,AppSignature:Vj(Kj,k4,t,e,i)}}async request(e,t,i={}){let{query:n,form:s,isFamily:o=!1,isAppKey:a=!1}=i,c=i.retryCount??0,l=new URL(e);for(let[p,I]of Object.entries(jj()))l.searchParams.set(p,I);if(n)for(let[p,I]of Object.entries(n))I!==void 0&&l.searchParams.set(p,I);let d={Accept:"application/json;charset=UTF-8","User-Agent":xE};a?Object.assign(d,this.appKeySignatureHeader(e,t)):Object.assign(d,this.signatureHeader(e,t,o));let u;s&&(d["Content-Type"]="application/x-www-form-urlencoded; charset=UTF-8",u=new URLSearchParams(s).toString());let h=await(await fetch(l.toString(),{method:t,headers:d,body:u})).text();if(h.includes("userSessionBO is null")||h.includes("InvalidSessionKey")){if(c>=3)throw new Error("[189TV] session expired after retry");return await this.login(),this.request(e,t,{...i,retryCount:c+1})}let g;try{g=Yj(h)}catch{throw new Error(`[189TV] \u975E\u9884\u671F\u54CD\u5E94: ${h.slice(0,200)}`)}if(Wj(g))throw new Error(`[189TV] API \u9519\u8BEF: ${g.res_message||g.errorMsg||g.msg||h.slice(0,200)}`);return g}async login(){if(!this.addition.access_token)throw new Error("[189TV] access_token \u4E3A\u7A7A\uFF0C\u65E0\u6CD5\u767B\u5F55");let e=await this.request(Oi+"/family/manage/loginFamilyMerge.action","GET",{query:{e189AccessToken:this.addition.access_token},isAppKey:!0});this.tokenInfo=e,e.accessToken&&e.accessToken!==this.addition.access_token&&(this.addition.access_token=e.accessToken,this.persistAccessToken&&await this.persistAccessToken(e.accessToken))}async getFiles(e,t){let n=(t?Oi+"/family/file":Oi)+"/listFiles.action",s=this.addition.order_by||"lastOpTime",o=Zj(this.addition.order_direction||"asc"),a=[];for(let c=1;;c++){let l={folderId:e,fileType:"0",mediaAttr:"0",iconOption:"5",pageNum:String(c),pageSize:"130"};t?(l.familyId=this.addition.family_id||"",l.orderBy=Xj(s),l.descending=o):(l.recursive="0",l.orderBy=s,l.descending=o);let u=(await this.request(n,"GET",{query:l,isFamily:t})).fileListAO;if(!u||Number(u.count)===0)break;let f=u.folderList||[],h=u.fileList||[];if(a.push(...f,...h),f.length+h.length<130)break}return a}async getFileDownloadUrl(e,t){let n=(t?Oi+"/family/file":Oi)+"/getFileDownloadUrl.action",s={fileId:e};t?s.familyId=this.addition.family_id||"":(s.dt="3",s.flag="1");let a=(await this.request(n,"GET",{query:s,isFamily:t})).fileDownloadUrl||"";if(!a)throw new Error("[189TV] \u83B7\u53D6\u4E0B\u8F7D\u5730\u5740\u5931\u8D25");a=a.replace(/&/g,"&").replace(/^http:\/\//i,"https://");try{let c=await fetch(a,{method:"GET",headers:{"User-Agent":xE},redirect:"manual"}),l=c.headers.get("location");c.status===302&&l&&(a=l.replace(/^http:\/\//i,"https://"))}catch{}return a}async mkdir(e,t,i){let s=(i?Oi+"/family/file":Oi)+"/createFolder.action",o={folderName:t,relativePath:""};i?(o.familyId=this.addition.family_id||"",o.parentId=e):o.parentFolderId=e,await this.request(s,"POST",{query:o,isFamily:i})}async rename(e,t,i,n){let s=n?Oi+"/family/file":Oi,o=n?"GET":"POST",c=s+(t?"/renameFolder.action":"/renameFile.action"),l={};n&&(l.familyId=this.addition.family_id||""),t?(l.folderId=e,l.destFolderName=i):(l.fileId=e,l.destFileName=i),await this.request(c,o,{query:l,isFamily:n})}async createBatchTask(e,t,i,n){let s={type:e,taskInfos:JSON.stringify(n)};i&&(s.targetFolderId=i),t&&(s.familyId=t);let o=await this.request(Oi+"/batch/createBatchTask.action","POST",{form:s,isFamily:t!==""});if(!o.taskId)throw new Error("[189TV] \u521B\u5EFA\u6279\u91CF\u4EFB\u52A1\u5931\u8D25");return o.taskId}async waitBatchTask(e,t){for(let i=0;i<20;i++){let s=(await this.request(Oi+"/batch/checkBatchTask.action","POST",{form:{type:e,taskId:t}})).taskStatus;if(s===4)return;if(s===2)throw new Error("[189TV] \u76EE\u6807\u5B58\u5728\u540C\u540D\u51B2\u7A81");await new Promise(o=>setTimeout(o,300))}throw new Error("[189TV] \u6279\u91CF\u4EFB\u52A1\u8D85\u65F6")}async move(e,t,i,n,s){let o=await this.createBatchTask("MOVE",s&&this.addition.family_id||"",n,[{fileId:e,fileName:i,isFolder:t?1:0}]);await this.waitBatchTask("MOVE",o)}async copy(e,t,i,n,s){let o=await this.createBatchTask("COPY",s&&this.addition.family_id||"",n,[{fileId:e,fileName:i,isFolder:t?1:0}]);await this.waitBatchTask("COPY",o)}async remove(e,t,i,n){let s=await this.createBatchTask("DELETE",n&&this.addition.family_id||"","",[{fileId:e,fileName:i,isFolder:t?1:0}]);await this.waitBatchTask("DELETE",s)}async upload(e,t,i,n){let s=Jl(i),o=i.length,c=(n?Oi+"/family/file":Oi)+(n?"/createFamilyFile.action":"/createUploadFile.action"),l;n?l=await this.request(c,"POST",{query:{familyId:this.addition.family_id||"",parentId:e,fileMd5:s,fileName:t,fileSize:String(o),resumePolicy:"1"},isFamily:n}):l=await this.request(c,"POST",{form:{parentFolderId:e,fileName:t,size:String(o),md5:s,opertype:"3",flag:"1",resumePolicy:"1",isLog:"0"},isFamily:n});let d=l.uploadFileId,u=l.fileUploadUrl,f=l.fileCommitUrl;if(!d||!u)throw new Error("[189TV] \u521B\u5EFA\u4E0A\u4F20\u4F1A\u8BDD\u5931\u8D25");let h={ResumePolicy:"1",Expect:"100-continue","User-Agent":xE};n?(h.FamilyId=this.addition.family_id||"",h.UploadFileId=String(d)):h["Edrive-UploadFileId"]=String(d),Object.assign(h,this.signatureHeader(u,"PUT",n));let g=await fetch(u,{method:"PUT",headers:h,body:new Uint8Array(i)});if(g.status!==200)throw new Error(`[189TV] \u4E0A\u4F20\u5931\u8D25: HTTP ${g.status}`);n?await this.request(f,"POST",{query:{ResumePolicy:"1",UploadFileId:String(d),FamilyId:this.addition.family_id||""},isFamily:n}):await this.request(f,"POST",{form:{opertype:"3",resumePolicy:"1",uploadFileId:String(d),isLog:"0"},isFamily:n})}}});function Q4(r){if(!r)return new Date().toISOString();let e=r.trim().replace(/["']/g,""),t=e.replace(/ +08$/,"+08:00"),i=new Date(t);return isNaN(i.getTime())?e:i.toISOString()}function _4(r){if("parentId"in r||!("size"in r)){let t=r;return{name:t.name,size:0,is_dir:!0,modified:Q4(t.lastOpTime||t.createDate),sign:t.id,type:1}}let e=r;return{name:e.name,size:e.size,is_dir:!1,modified:Q4(e.lastOpTime||e.createDate),sign:e.id,type:X(e.name,!1),thumb:e.icon?.smallUrl||"",raw_url:""}}var Wp,F4=J(()=>{"use strict";Be();D4();Wp=class{client;pathCache=new Map;constructor(e,t){this.client=new Yp(e,t)}async init(){await this.client.init()}isFamily(){return this.client.isFamily()}async list(e,t){let i=await this.pathToId(t);return(await this.client.getFiles(i,this.isFamily())).map(_4)}async get(e,t){let i=t.split("/").filter(Boolean),n=i[i.length-1]||"root",s="/"+i.slice(0,-1).join("/");try{let o=await this.pathToId(s),c=(await this.client.getFiles(o,this.isFamily())).find(l=>l.name===n);if(c){let l=_4(c);if(!l.is_dir)try{l.raw_url=await this.client.getFileDownloadUrl(c.id,this.isFamily()),l.raw_url_headers={"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"}}catch{l.raw_url_error="[189TV] failed to resolve download url"}return l}}catch{}return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"new_folder",s="/"+i.join("/");await this.client.mkdir(await this.pathToId(s),n,this.isFamily()),this.pathCache.clear()}async rename(e,t,i){let n=await this.resolveEntry(t);await this.client.rename(n.id,n.isDir,i,this.isFamily()),this.pathCache.clear()}async remove(e,t,i){let n=await this.resolveEntry(t),s=t.split("/").filter(Boolean).pop()||"";await this.client.remove(n.id,n.isDir,s,this.isFamily()),this.pathCache.clear()}async move(e,t,i,n,s){let o=await this.resolveEntry(n),a=n.split("/").filter(Boolean).pop()||"";await this.client.move(o.id,o.isDir,a,await this.pathToId(t),this.isFamily()),this.pathCache.clear()}async copy(e,t,i,n,s){let o=await this.resolveEntry(n),a=n.split("/").filter(Boolean).pop()||"";await this.client.copy(o.id,o.isDir,a,await this.pathToId(t),this.isFamily()),this.pathCache.clear()}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"file",o="/"+n.join("/");await this.client.upload(await this.pathToId(o),s,new Uint8Array(i),this.isFamily()),this.pathCache.clear()}async resolveEntry(e){let t=e.split("/").filter(Boolean),i=t[t.length-1],n="/"+t.slice(0,-1).join("/"),s=await this.pathToId(n),a=(await this.client.getFiles(s,this.isFamily())).find(l=>l.name===i);if(!a)throw new Error(`[189TV] '${i}' not found`);let c="parentId"in a||!("size"in a);return{id:a.id,isDir:c}}async pathToId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return this.isFamily()?"":"-11";if(this.pathCache.has(t))return this.pathCache.get(t);let i=t.split("/"),n=this.isFamily()?"":"-11";for(let s=0;s<i.length;s++){let a=(await this.client.getFiles(n,this.isFamily())).find(l=>l.name===i[s]&&"parentId"in l);if(!a)throw new Error(`[189TV] Directory '${i[s]}' not found`);n=a.id;let c="/"+i.slice(0,s+1).join("/");this.pathCache.set(c,n)}return n}}});function Hs(r,e){let t=r.replace(/\/+$/,""),i=e.replace(/^\/+/,"");return!t&&!i?"/":t?i?`${t}/${i}`:t:"/"+i}function e$(r){return r.split("/").map(e=>encodeURIComponent(e)).join("/")}function P4(r,e){let t=[],i,n=/<(?:[a-zA-Z0-9_-]+:)?response\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?response>/gi,s;for(;(s=n.exec(r))!==null;){let o=s[1],a=/<(?:[a-zA-Z0-9_-]+:)?href\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?href>/i.exec(o);if(!a)continue;let c=a[1].trim(),l=c;try{l=decodeURIComponent(c)}catch{}let d=/<(?:[a-zA-Z0-9_-]+:)?propstat\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?propstat>/gi,u,f="";for(;(u=d.exec(o))!==null;){let E=u[1],P=/<(?:[a-zA-Z0-9_-]+:)?status\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?status>/i.exec(E),N=P?P[1]:"";if(N.includes("200")||N.toLowerCase().includes("ok")){let j=/<(?:[a-zA-Z0-9_-]+:)?prop\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?prop>/i.exec(E);if(j){f=j[1];break}}}if(!f)continue;let h=/<(?:[a-zA-Z0-9_-]+:)?resourcetype\b[^>]*>[\s\S]*?<(?:[a-zA-Z0-9_-]+:)?collection\b/i.test(f),g=/<(?:[a-zA-Z0-9_-]+:)?displayname\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?displayname>/i.exec(f),p=g?g[1].trim():"",I=l.replace(/\/+$/,""),m=I&&I.split("/").pop()||"",w=p||m,B=/<(?:[a-zA-Z0-9_-]+:)?getcontentlength\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?getcontentlength>/i.exec(f),C=h?0:B&&parseInt(B[1].trim(),10)||0,x=/<(?:[a-zA-Z0-9_-]+:)?getlastmodified\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?getlastmodified>/i.exec(f),Q=new Date().toISOString();if(x){let E=new Date(x[1].trim());isNaN(E.getTime())||(Q=E.toISOString())}let R=/<(?:[a-zA-Z0-9_-]+:)?getcontenttype\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?getcontenttype>/i.exec(f),U=R?R[1].trim():void 0,T=/<(?:[a-zA-Z0-9_-]+:)?getetag\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?getetag>/i.exec(f),D=T?T[1].trim().replace(/^"|"$/g,""):void 0,b={name:w,path:l,size:C,modified:Q,isFolder:h,contentType:U,etag:D},_=e.replace(/\/+$/,"").toLowerCase(),v=I.toLowerCase();!i&&(v===_||v.endsWith(_)||_===""&&v==="")?i=b:t.push(b)}return{self:i,items:t}}function t$(r){let e={},t=r.replace(/^digest\s+/i,"").split(/,\s*/);for(let i of t){let n=i.indexOf("=");if(n!==-1){let s=i.slice(0,n).trim(),o=i.slice(n+1).trim().replace(/^"|"$/g,"");s==="realm"?e.realm=o:s==="nonce"?e.nonce=o:s==="qop"?e.qop=o:s==="opaque"?e.opaque=o:s==="algorithm"&&(e.algorithm=o)}}return e}function T4(r,e,t,i,n,s=1){let o=s.toString(16).padStart(8,"0"),a=Math.random().toString(36).substring(2,18),c=r.realm||"",l=r.nonce||"",d=(r.algorithm||"MD5").toUpperCase(),u=r.qop||"",f="";if(d==="MD5"||d==="")f=bc.default.MD5(`${e}:${c}:${t}`).toString();else if(d==="MD5-SESS"){let I=bc.default.MD5(`${e}:${c}:${t}`).toString();f=bc.default.MD5(`${I}:${l}:${a}`).toString()}let h="";(u==="auth"||u==="")&&(h=bc.default.MD5(`${i}:${n}`).toString());let g="";u?g=bc.default.MD5(`${f}:${l}:${o}:${a}:${u}:${h}`).toString():g=bc.default.MD5(`${f}:${l}:${h}`).toString();let p=`Digest username="${e}", realm="${c}", nonce="${l}", uri="${n}", response="${g}"`;return d&&(p+=`, algorithm=${d}`),u&&(p+=`, qop=${u}, nc=${o}, cnonce="${a}"`),r.opaque&&(p+=`, opaque="${r.opaque}"`),p}async function i$(r,e,t){let i=new URL(t),n=i.hostname.split("."),s=n[n.length-1],a=`${r$[s]||"https://login.microsoftonline.com"}/extSTS.srf`,c=`<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" -xmlns:a="http://www.w3.org/2005/08/addressing" -xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> -<s:Header> -<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action> -<a:ReplyTo> -<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> -</a:ReplyTo> -<a:To s:mustUnderstand="1">${a}</a:To> -<o:Security s:mustUnderstand="1" - xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> -<o:UsernameToken> - <o:Username>${r.replace(/&/g,"&").replace(/</g,"<")}</o:Username> - <o:Password>${e.replace(/&/g,"&").replace(/</g,"<")}</o:Password> -</o:UsernameToken> -</o:Security> -</s:Header> -<s:Body> -<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> -<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> - <a:EndpointReference> - <a:Address>${t.replace(/&/g,"&").replace(/</g,"<")}</a:Address> - </a:EndpointReference> -</wsp:AppliesTo> -<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType> -<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> -<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType> -</t:RequestSecurityToken> -</s:Body> -</s:Envelope>`,l=await fetch(a,{method:"POST",headers:{"Content-Type":"application/soap+xml; charset=utf-8"},body:c});if(!l.ok)throw new Error(`SharePoint SAML auth failed with HTTP ${l.status}`);let d=await l.text(),u=/<(?:[a-zA-Z0-9_-]+:)?BinarySecurityToken\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?BinarySecurityToken>/i.exec(d);if(!u){let w=/<(?:[a-zA-Z0-9_-]+:)?Text\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?Text>/i.exec(d),B=w?w[1]:"Failed to obtain BinarySecurityToken";throw new Error(`SharePoint login failed: ${B}`)}let f=u[1].trim(),h=`https://${i.host}/_forms/default.aspx?wa=wsignin1.0`,g=await fetch(h,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:f,redirect:"manual"}),p="",I="",m=w=>{let B=[];if(w.headers.getSetCookie)B.push(...w.headers.getSetCookie());else{let C=w.headers.get("set-cookie");C&&B.push(C)}for(let C of B){let x=/rtFa=([^;]+)/.exec(C);x&&(p=x[1]);let Q=/FedAuth=([^;]+)/.exec(C);Q&&(I=Q[1])}};if(m(g),!p||!I){let w=g.headers.get("location");if(w){let B=new URL(w,h).toString(),C=await fetch(B,{method:"GET",headers:{Cookie:`rtFa=${p}; FedAuth=${I}`},redirect:"manual"});m(C)}}if(!p&&!I)throw new Error("SharePoint auth failed: rtFa / FedAuth cookies not returned");return`rtFa=${p}; FedAuth=${I}`}var bc,r$,Xp,U4=J(()=>{"use strict";bc=xr(Oo(),1);r$={com:"https://login.microsoftonline.com",cn:"https://login.chinacloudapi.cn",us:"https://login.microsoftonline.us",de:"https://login.microsoftonline.de"};Xp=class{address;username;password;isSharepoint;sharepointCookie="";digestParts=null;ncCount=0;constructor(e){this.address=e.address.replace(/\/+$/,""),this.username=e.username||"",this.password=e.password||"",this.isSharepoint=e.vendor==="sharepoint"}async init(){this.isSharepoint&&(this.sharepointCookie=await i$(this.username,this.password,this.address))}buildUrl(e){let t=e.replace(/^\/+/,"");return t?`${this.address}/${e$(t)}`:this.address}getAuthHeaders(e,t){let i={};if(this.isSharepoint&&this.sharepointCookie)i.Cookie=this.sharepointCookie;else if(this.digestParts)this.ncCount++,i.Authorization=T4(this.digestParts,this.username,this.password,e,t,this.ncCount);else if(this.username||this.password){let n=btoa(unescape(encodeURIComponent(`${this.username}:${this.password}`)));i.Authorization=`Basic ${n}`}return i}async request(e,t,i={}){let n=this.buildUrl(t),s=new URL(n),o=s.pathname+s.search,c={...this.getAuthHeaders(e,o),...i.headers||{}},l=await fetch(n,{method:e,headers:c,body:i.body,redirect:i.redirect||"follow"});if(l.status===401&&!this.isSharepoint){let d=l.headers.get("www-authenticate")||"";if(/digest/i.test(d)){this.digestParts=t$(d),this.ncCount=1;let u=T4(this.digestParts,this.username,this.password,e,o,this.ncCount),f={...c,Authorization:u};l=await fetch(n,{method:e,headers:f,body:i.body,redirect:i.redirect||"follow"})}}return l}async readDir(e){let i=await this.request("PROPFIND",e,{headers:{Depth:"1","Content-Type":"application/xml; charset=utf-8",Accept:"application/xml, text/xml"},body:`<?xml version="1.0" encoding="utf-8" ?> -<d:propfind xmlns:d="DAV:"> - <d:prop> - <d:displayname/> - <d:resourcetype/> - <d:getcontentlength/> - <d:getcontenttype/> - <d:getetag/> - <d:getlastmodified/> - </d:prop> -</d:propfind>`});if(i.status===404)throw new Error(`Directory not found: ${e}`);if(i.status!==207&&!i.ok){let o=await i.text();throw new Error(`WebDAV PROPFIND failed with status ${i.status}: ${o||i.statusText}`)}let n=await i.text(),{items:s}=P4(n,e);return s}async stat(e){let i=await this.request("PROPFIND",e,{headers:{Depth:"0","Content-Type":"application/xml; charset=utf-8",Accept:"application/xml, text/xml"},body:`<?xml version="1.0" encoding="utf-8" ?> -<d:propfind xmlns:d="DAV:"> - <d:prop> - <d:displayname/> - <d:resourcetype/> - <d:getcontentlength/> - <d:getcontenttype/> - <d:getetag/> - <d:getlastmodified/> - </d:prop> -</d:propfind>`});if(i.status===404)throw new Error(`Object not found: ${e}`);if(i.status!==207&&!i.ok){let c=await i.text();throw new Error(`WebDAV PROPFIND failed with status ${i.status}: ${c||i.statusText}`)}let n=await i.text(),{self:s,items:o}=P4(n,e),a=s||o[0];if(!a)throw new Error(`Object not found in PROPFIND response: ${e}`);return a}async mkdir(e){let t=await this.request("MKCOL",e);if(!(t.status===201||t.status===405))throw new Error(`WebDAV MKCOL failed with status ${t.status}`)}async mkdirAll(e){let t=await this.request("MKCOL",e);if(!(t.status===201||t.status===405)){if(t.status===409){let i=e.split("/").filter(Boolean),n="";for(let s of i){n+="/"+s;let o=await this.request("MKCOL",n);if(o.status!==201&&o.status!==405)throw new Error(`WebDAV MkdirAll failed at ${n} with status ${o.status}`)}return}throw new Error(`WebDAV MkdirAll failed with status ${t.status}`)}}async move(e,t,i=!0){let n=this.buildUrl(t),s=await this.request("MOVE",e,{headers:{Destination:n,Overwrite:i?"T":"F"}});if(!(s.status===201||s.status===204)){if(s.status===409){let o=t.substring(0,t.lastIndexOf("/"));if(o)return await this.mkdirAll(o),this.move(e,t,i)}throw new Error(`WebDAV MOVE failed with status ${s.status}`)}}async copy(e,t,i=!0){let n=this.buildUrl(t),s=await this.request("COPY",e,{headers:{Destination:n,Overwrite:i?"T":"F"}});if(!(s.status===201||s.status===204)){if(s.status===409){let o=t.substring(0,t.lastIndexOf("/"));if(o)return await this.mkdirAll(o),this.copy(e,t,i)}throw new Error(`WebDAV COPY failed with status ${s.status}`)}}async remove(e){let t=await this.request("DELETE",e);if(!(t.status===200||t.status===204||t.status===404))throw new Error(`WebDAV DELETE failed with status ${t.status}`)}async put(e,t,i){let n={};i&&(n["Content-Type"]=i);let s=await this.request("PUT",e,{headers:n,body:t});if(!(s.status===200||s.status===201||s.status===204)){if(s.status===409){let o=e.substring(0,e.lastIndexOf("/"));if(o&&(await this.mkdirAll(o),s=await this.request("PUT",e,{headers:n,body:t}),s.status===200||s.status===201||s.status===204))return}throw new Error(`WebDAV PUT failed with status ${s.status}`)}}getLink(e){let t=this.buildUrl(e),i=new URL(t),n=i.pathname+i.search,s=this.getAuthHeaders("GET",n);return{url:t,headers:s}}}});function n$(r){let e={...r||{}};return e.vendor=e.vendor||"other",e.address=(e.address||"").trim(),e.username=(e.username||"").trim(),e.password=e.password||"",e.root_folder_path=(e.root_folder_path||"/").trim(),e.root_folder_path.startsWith("/")||(e.root_folder_path="/"+e.root_folder_path),e.tls_insecure_skip_verify=!!e.tls_insecure_skip_verify,e.order_by=e.order_by||"name",e.order_direction=e.order_direction||"asc",e}var Zp,R4=J(()=>{"use strict";Be();He();U4();Zp=class{client;addition;constructor(e){this.addition=n$(e),this.client=new Xp(this.addition)}async init(){await this.client.init()}getRemotePath(e){let t=this.addition.root_folder_path||"/";return Hs(t,e||"/")}fileItemFromWebdav(e,t){let i=this.client.getLink(t);return{name:e.name,size:e.size,is_dir:e.isFolder,modified:e.modified,sign:"",type:X(e.name,e.isFolder),thumb:"",raw_url:e.isFolder?void 0:i.url,raw_url_headers:e.isFolder?void 0:i.headers}}async list(e,t){let i=this.getRemotePath(t),s=(await this.client.readDir(i)).map(o=>{let a=Hs(i,o.name);return this.fileItemFromWebdav(o,a)});return de(s,this.addition.order_by||"name",this.addition.order_direction||"asc")}async get(e,t){let i=this.getRemotePath(t),n=await this.client.stat(i);return this.fileItemFromWebdav(n,i)}async mkdir(e,t){let i=this.getRemotePath(t);await this.client.mkdirAll(i)}async rename(e,t,i){let n=this.getRemotePath(t),s=n.lastIndexOf("/"),o=s>=0?n.substring(0,s):"/",a=Hs(o,i);await this.client.move(n,a,!0)}async move(e,t,i,n,s){let o=this.getRemotePath(n),a=this.getRemotePath(s);for(let c of i){let l=Hs(o,c),d=Hs(a,c);await this.client.move(l,d,!0)}}async copy(e,t,i,n,s){let o=this.getRemotePath(n),a=this.getRemotePath(s);for(let c of i){let l=Hs(o,c),d=Hs(a,c);await this.client.copy(l,d,!0)}}async remove(e,t,i){let n=this.getRemotePath(t);if(i&&i.length>0)for(let s of i){let o=Hs(n,s);await this.client.remove(o)}else await this.client.remove(n)}async put(e,t,i){let n=this.getRemotePath(t);await this.client.put(n,i)}}});var Zi,em,N4,q4,CE,EE,ia,BE,O4,M4,L4,H4,K4,G4,z4,J4,j4,$4,V4,Y4,W4,X4,Z4,bE,tm=J(()=>{"use strict";Zi="1001000021",em="XFmi9GS2hzk98jGX",N4="10000001",q4="https://panservice.mail.wo.cn",CE="https://tjupload.pan.wo.cn",EE="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.37",ia="api-user",BE="wohome",O4="wocloud",M4="AppQueryUser",L4="AppRefreshToken",H4="QueryCloudUsageInfo",K4="ClassifyRule",G4="GetZoneInfo",z4="FamilyUserCurrentEncode",J4="QueryAllFiles",j4="GetDownloadUrlV2",$4="CreateDirectory",V4="RenameFileOrDirectory",Y4="MoveFile",W4="CopyFile",X4="DeleteFile",Z4="upload2C",bE={name_asc:1,name_desc:2,size_asc:3,size_desc:4,time_asc:5,time_desc:6}});var pi,s$,rm,eF=J(()=>{"use strict";pi=xr(Oo(),1);tm();s$="wNSOYIB1k1DjY5lA",rm=class{key=em;iv=s$;accessKey="";constructor(e){e&&this.setAccessToken(e)}setAccessToken(e){e&&e.length>=16?this.accessKey=e.slice(0,16):e&&(this.accessKey=e)}encrypt(e,t){let i=t===ia?this.key:this.accessKey||this.key,n=pi.default.enc.Utf8.parse(i),s=pi.default.enc.Utf8.parse(this.iv);return pi.default.AES.encrypt(pi.default.enc.Utf8.parse(e),n,{iv:s,mode:pi.default.mode.CBC,padding:pi.default.pad.Pkcs7}).toString()}decrypt(e,t){let i=t===ia?this.key:this.accessKey||this.key,n=pi.default.enc.Utf8.parse(i),s=pi.default.enc.Utf8.parse(this.iv);return pi.default.AES.decrypt(e,n,{iv:s,mode:pi.default.mode.CBC,padding:pi.default.pad.Pkcs7}).toString(pi.default.enc.Utf8)}userEncrypt(e){return this.encrypt(e,ia)}userDecrypt(e){return this.decrypt(e,ia)}woHomeEncrypt(e){return this.encrypt(e,"wohome")}woHomeDecrypt(e){return this.decrypt(e,"wohome")}calHeader(e,t){let i=Date.now(),n=Math.floor(Math.random()*8999)+1e5,s="",o=pi.default.MD5(`${t}${i}${n}${e}${s}`).toString();return{key:t,resTime:i,reqSeq:n,channel:e,sign:o,version:s}}}});function o$(r){let e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",t="";for(let i=0;i<r;i++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function a$(r=new Date){let e=c=>String(c).padStart(2,"0"),t=r.getFullYear(),i=e(r.getMonth()+1),n=e(r.getDate()),s=e(r.getHours()),o=e(r.getMinutes()),a=e(r.getSeconds());return`${t}${i}${n}${s}${o}${a}`}var im,tF=J(()=>{"use strict";tm();eF();im=class{addition;accessToken;refreshTokenValue;phone="";zoneURL="";classifyRuleData=null;crypto;onTokenUpdate;constructor(e,t){this.addition=e,this.accessToken=e.access_token||"",this.refreshTokenValue=e.refresh_token||"",this.onTokenUpdate=t,this.crypto=new rm(this.accessToken)}getAccessToken(){return this.accessToken}getRefreshToken(){return this.refreshTokenValue}setAccessToken(e){this.accessToken=e,this.crypto.setAccessToken(e)}setRefreshToken(e){this.refreshTokenValue=e}async request(e,t,i,n={},s=!0){let o=this.crypto.calHeader(e,t),a={...n};if(i!=null){let g=JSON.stringify(i),p=this.crypto.encrypt(g,e);a.param=p}let c={Origin:"https://pan.wo.cn",Referer:"https://pan.wo.cn/","User-Agent":EE,"Content-Type":"application/json;charset=UTF-8"};this.accessToken&&(c.Accesstoken=this.accessToken);let l=`${q4}/${e}/dispatcher`,d=await fetch(l,{method:"POST",headers:c,body:JSON.stringify({header:o,body:a})});if(!d.ok)throw new Error(`[WoPan] Request failed with HTTP status: ${d.status} ${d.statusText}`);let u=await d.json().catch(()=>null);if(!u)throw new Error(`[WoPan] Response is not valid JSON from ${t}`);if(u.STATUS!=="200")throw new Error(`[WoPan] Request failed with status: ${u.STATUS}, msg: ${u.MSG||""}`);let f=u.RSP?.RSP_CODE;if(f!=="0000"){if(e!==ia&&s&&f==="9999")return await this.refreshToken(),this.request(e,t,i,n,!1);throw new Error(`[WoPan] Request failed with rsp_code: ${f}, rsp_desc: ${u.RSP?.RSP_DESC||""}`)}let h=u.RSP?.DATA;if(h==null)return{};if(typeof h=="string"){let g=h.trim();g.startsWith('"')&&g.endsWith('"')&&(g=g.slice(1,-1));try{let p=this.crypto.decrypt(g,e);if(p)return JSON.parse(p)}catch{try{return JSON.parse(g)}catch{return g}}}return h}async requestApiUser(e,t,i={}){return this.request(ia,e,t,i)}async requestWoHome(e,t,i={}){return this.request(BE,e,t,i)}async appRefreshToken(){return await this.requestApiUser(L4,{refreshToken:this.refreshTokenValue,clientSecret:em},{clientId:Zi,secret:!0})}async refreshToken(){let e=await this.appRefreshToken();if(!e.access_token)throw new Error("[WoPan] Failed to refresh token: empty access_token");this.setAccessToken(e.access_token),e.refresh_token&&this.setRefreshToken(e.refresh_token),this.onTokenUpdate?.(this.accessToken,this.refreshTokenValue)}async appQueryUser(){return this.requestApiUser(M4,{accessToken:this.accessToken},{clientId:Zi,secret:!0})}async initPhone(){if(this.phone)return;let e=await this.appQueryUser();e?.userId&&(this.phone=e.userId)}async classifyRule(){return this.requestWoHome(K4,{},{key:!0})}async initClassifyRule(){if(this.classifyRuleData)return;let e=await this.classifyRule().catch(()=>null);e&&(this.classifyRuleData=e)}async getZoneInfo(){return this.requestWoHome(G4,{appId:N4},{key:!0})}async initZoneURL(){if(this.zoneURL)return;let e=await this.getZoneInfo().catch(()=>null);this.zoneURL=e?.url||CE}async familyUserCurrentEncode(){return this.requestWoHome(z4,{clientId:Zi},{secret:!0})}async initData(){!this.accessToken&&this.refreshTokenValue&&await this.refreshToken(),await this.initPhone().catch(()=>{}),await this.initClassifyRule().catch(()=>{}),await this.initZoneURL().catch(()=>{})}getFileType(e){let t=(e.split(".").pop()||"").toLowerCase();return t&&this.classifyRuleData?.fileTypes?.[t]?this.classifyRuleData.fileTypes[t].type:"5"}async queryAllFiles(e,t,i,n,s,o=""){let a={spaceType:e,parentDirectoryId:t,pageNum:i,pageSize:n,sortRule:s,clientId:Zi};return e==="1"&&o&&(a.familyId=o),this.requestWoHome(J4,a,{secret:!0})}async getDownloadUrlV2(e){let t={type:"1",fidList:e,clientId:Zi};return this.requestWoHome(j4,t,{secret:!0})}async createDirectory(e,t,i,n=""){let s={spaceType:e,familyId:n,parentDirectoryId:t,directoryName:i,clientId:Zi};return this.requestWoHome($4,s,{secret:!0})}async renameFileOrDirectory(e,t,i,n,s=""){let o=t===0?"0":this.getFileType(n),a={spaceType:e,type:t,fileType:o,id:i,name:n,clientId:Zi};e==="1"&&s&&(a.familyId=s),await this.requestWoHome(V4,a,{secret:!0})}async moveFile(e,t,i,n,s,o="",a=""){let c={targetDirId:i,sourceType:n,targetType:s,dirList:e,fileList:t,secret:!1,clientId:Zi};n==="1"&&o&&(c.fromFamilyId=o),s==="1"&&a&&(c.familyId=a),await this.requestWoHome(Y4,c,{secret:!0})}async copyFile(e,t,i,n,s,o="",a=""){let c={targetDirId:i,sourceType:n,targetType:s,dirList:e,fileList:t,secret:!1,clientId:Zi};n==="1"&&o&&(c.fromFamilyId=o),s==="1"&&a&&(c.familyId=a),await this.requestWoHome(W4,c,{secret:!0})}async deleteFile(e,t,i){let n={spaceType:e,vipLevel:"0",dirList:t,fileList:i,clientId:Zi};await this.requestWoHome(X4,n,{secret:!0})}async queryCloudUsageInfo(){return await this.initPhone(),this.requestWoHome(H4,{phoneNum:this.phone,clientId:Zi},{secret:!0})}async upload2C(e,t,i,n,s="",o){await this.initZoneURL();let c=`${this.zoneURL||CE}/openapi/client/${Z4}`,l=i instanceof Uint8Array?i:i instanceof ArrayBuffer?new Uint8Array(i):new Uint8Array(i),d=l.length,u=Math.max(1,Math.ceil(d/8388608)),f=a$(),h={spaceType:e,directoryId:n,batchNo:f,fileName:t,fileSize:d,fileType:this.getFileType(t)};e==="1"&&s&&(h.familyId=s);let g=this.crypto.encrypt(JSON.stringify(h),BE),p=`${Date.now()}_${o$(6)}`,I=0,m="";for(let w=1;w<=u;w++){let B=(w-1)*8388608,C=w===u?d-B:8388608,x=l.subarray(B,B+C),Q=new FormData;Q.append("uniqueId",p),Q.append("accessToken",this.accessToken),Q.append("fileName",t),Q.append("psToken","undefined"),Q.append("fileSize",String(d)),Q.append("totalPart",String(u)),Q.append("channel",O4),Q.append("directoryId",n),Q.append("fileInfo",g),Q.append("partSize",String(C)),Q.append("partIndex",String(w));let R=new Blob([x.buffer.slice(x.byteOffset,x.byteOffset+x.byteLength)],{type:"application/octet-stream"});Q.append("file",R,t);let U=await fetch(c,{method:"POST",headers:{Origin:"https://pan.wo.cn",Referer:"https://pan.wo.cn/","User-Agent":EE},body:Q});if(!U.ok)throw new Error(`[WoPan] Upload part ${w}/${u} failed with HTTP status: ${U.status}`);let T=await U.json().catch(()=>({}));if(T.code!=="0000")throw new Error(`[WoPan] Upload part ${w}/${u} failed: ${T.code} ${T.msg||""}`);T.data?.fid&&(m=T.data.fid),I+=C,o?.(I,d)}return m}}});function l$(r){if(!r)return new Date().toISOString();if(r.length>=14){let e=r.slice(0,4),t=r.slice(4,6),i=r.slice(6,8),n=r.slice(8,10),s=r.slice(10,12),o=r.slice(12,14),a=`${e}-${t}-${i}T${n}:${s}:${o}+08:00`,c=new Date(a);if(!isNaN(c.getTime()))return c.toISOString()}try{let e=new Date(r);if(!isNaN(e.getTime()))return e.toISOString()}catch{}return new Date().toISOString()}function rF(r){let e=r.type===0;return{name:r.name,size:r.size||0,is_dir:e,modified:l$(r.createTime),sign:r.fid||r.id,type:X(r.name,e),thumb:r.thumbUrl||"",raw_url:""}}function kE(r){let e={...r||{}};return e.root_folder_id=e.root_folder_id||"0",e.refresh_token=(e.refresh_token||"").trim(),e.family_id=(e.family_id||"").trim(),e.sort_rule=e.sort_rule||"name_asc",e.access_token=(e.access_token||"").trim(),e}var nm,iF=J(()=>{"use strict";Be();He();tm();tF();nm=class{client;addition;defaultFamilyId="";pathFileMapCache=new Map;pathFolderIdCache=new Map;constructor(e,t){this.addition=kE(e),this.client=new im(this.addition,(i,n)=>{this.addition.access_token=i,this.addition.refresh_token=n,t?.(i,n)})}getSpaceType(){return this.addition.family_id?"1":"0"}getFamilyId(){return this.addition.family_id||this.defaultFamilyId}getSortRuleNum(){let e=this.addition.sort_rule||"name_asc";return bE[e]||bE.name_asc}getRootId(){return this.addition.root_folder_id||"0"}async init(){await this.client.initData();let e=await this.client.familyUserCurrentEncode().catch(()=>null);e?.defaultHomeId!==void 0&&e.defaultHomeId!==null&&(this.defaultFamilyId=String(e.defaultHomeId))}async list(e,t){let i=await this.resolveFolderId(t),n=await this.fetchFolderFiles(i),s=t.split("/").filter(Boolean).join("/");for(let a of n){let c=s?`${s}/${a.name}`:a.name;this.pathFileMapCache.set(c,a),a.type===0&&this.pathFolderIdCache.set(c,a.id)}let o=n.map(rF);return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=t.split("/").filter(Boolean).join("/");if(!i)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.getRootId(),type:1,raw_url:""};let n=await this.resolveWoPanFile(t);if(!n){let o=await this.resolveFolderId(t).catch(()=>null);if(o){let a=i.split("/");return{name:a[a.length-1]||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:o,type:1,raw_url:""}}throw new Error(`[WoPan] File not found: ${t}`)}let s=rF(n);if(!s.is_dir&&n.fid){let o=await this.client.getDownloadUrlV2([n.fid]).catch(()=>null);o?.list?.[0]?.downloadUrl&&(s.raw_url=o.list[0].downloadUrl)}return s}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s=i.join("/"),o=await this.resolveFolderId(s);await this.client.createDirectory(this.getSpaceType(),o,n,this.getFamilyId()),this.clearCache()}async rename(e,t,i){let n=await this.resolveWoPanFile(t);if(!n)throw new Error(`[WoPan] Item not found for rename: ${t}`);await this.client.renameFileOrDirectory(this.getSpaceType(),n.type,n.id,i,this.getFamilyId()),this.clearCache()}async remove(e,t,i){let n=await this.resolveWoPanFile(t);if(!n)throw new Error(`[WoPan] Item not found for deletion: ${t}`);let s=[],o=[];n.type===0?s.push(n.id):o.push(n.id),await this.client.deleteFile(this.getSpaceType(),s,o),this.clearCache()}async move(e,t,i,n,s){let o=await this.resolveWoPanFile(n);if(!o)throw new Error(`[WoPan] Source item not found for move: ${n}`);let a=await this.resolveFolderId(t),c=[],l=[];o.type===0?c.push(o.id):l.push(o.id),await this.client.moveFile(c,l,a,this.getSpaceType(),this.getSpaceType(),this.getFamilyId(),this.getFamilyId()),this.clearCache()}async copy(e,t,i,n,s){let o=await this.resolveWoPanFile(n);if(!o)throw new Error(`[WoPan] Source item not found for copy: ${n}`);let a=await this.resolveFolderId(t),c=[],l=[];o.type===0?c.push(o.id):l.push(o.id),await this.client.copyFile(c,l,a,this.getSpaceType(),this.getSpaceType(),this.getFamilyId(),this.getFamilyId()),this.clearCache()}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"upload",o=n.join("/"),a=await this.resolveFolderId(o);await this.client.upload2C(this.getSpaceType(),s,i,a,this.getFamilyId()),this.clearCache()}clearCache(){this.pathFileMapCache.clear(),this.pathFolderIdCache.clear()}async fetchFolderFiles(e){let t=[],i=0,n=100;for(;;){let o=(await this.client.queryAllFiles(this.getSpaceType(),e,i,n,this.getSortRuleNum(),this.getFamilyId()))?.files||[];if(t.push(...o),o.length<n)break;i++}return t}async resolveFolderId(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return this.getRootId();if(this.pathFolderIdCache.has(t))return this.pathFolderIdCache.get(t);let i=t.split("/"),n=this.getRootId();for(let s=0;s<i.length;s++){let o=i[s],a=(()=>{try{return decodeURIComponent(o)}catch{return o}})(),c=i.slice(0,s+1).join("/");if(this.pathFolderIdCache.has(c)){n=this.pathFolderIdCache.get(c);continue}let l=await this.fetchFolderFiles(n);for(let u of l){let f=i.slice(0,s).concat(u.name).join("/");this.pathFileMapCache.set(f,u),u.type===0&&this.pathFolderIdCache.set(f,u.id)}let d=l.find(u=>u.type===0&&(u.name===o||u.name===a||u.id===o));if(!d)throw new Error(`[WoPan] Directory '${o}' not found in path '${e}'`);n=d.id,this.pathFolderIdCache.set(c,n)}return n}async resolveWoPanFile(e){let t=e.split("/").filter(Boolean).join("/");if(!t)return null;if(this.pathFileMapCache.has(t))return this.pathFileMapCache.get(t);let i=t.split("/"),n=i.pop(),s=(()=>{try{return decodeURIComponent(n)}catch{return n}})(),o=i.join("/"),a=await this.resolveFolderId(o),c=await this.fetchFolderFiles(a);for(let d of c){let u=i.concat(d.name).join("/");this.pathFileMapCache.set(u,d),d.type===0&&this.pathFolderIdCache.set(u,d.id)}return c.find(d=>d.name===n||d.name===s||d.id===n||d.fid===n)||null}}});function ff(){let r=()=>Array.from({length:16},()=>Math.floor(Math.random()*16).toString(16)).join(""),e={appVersion:"8.0.0",brand:u$,deviceId:r(),deviceName:nF,imei:r(),imsi:r(),mac:Array.from({length:6},()=>Math.floor(Math.random()*256).toString(16).padStart(2,"0")).join(":"),mno:"46000",model:nF,mpVersion:SE,networkType:"wifi",osVersion:"12",platform:d$};return JSON.stringify(e)}var Er,SE,d$,nF,u$,DE=J(()=>{"use strict";Er="https://mcloud.caiyun.feixin.10086.cn/MoPanProxyFamily",SE="1.1.202",d$="android",nF="M2102J2SC",u$="Xiaomi"});import{createCipheriv as f$,createDecipheriv as A$,randomBytes as h$}from"crypto";function QE(r,e){let t=Buffer.alloc(16,0),i=f$("aes-128-cbc",e.slice(0,16),t);return Buffer.concat([i.update(r),i.final()])}function sF(r,e){let t=Buffer.alloc(16,0),i=A$("aes-128-cbc",e.slice(0,16),t);return Buffer.concat([i.update(r),i.final()])}function oF(){return h$(16).toString("hex").slice(0,16)}function sm(r){return r.toString("base64")}function aF(r){return Buffer.from(r,"base64")}function lF(r,e){return sm(Buffer.from(r,"utf-8"))}var cF,dF=J(()=>{"use strict";cF=`-----BEGIN PUBLIC KEY----- -MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDN8TyHJhEVoT6t5A/9Q3+2/v3n -3tEqvU7yb8+yx6L0S5h5D+Y5b2E5vVqJ5J8k5n7Y5E5y5w5h5e5q5c5J5b5t5f5j -5r5u5a5s5i5o5n5k5e5y5A== ------END PUBLIC KEY-----`});var om,uF=J(()=>{"use strict";DE();dF();om=class{addition;authorization="";deviceInfo;onAuthorizationExpired;constructor(e,t){if(this.addition=e,this.onAuthorizationExpired=t,e.device_info?.trim())try{this.deviceInfo=JSON.parse(e.device_info)}catch{this.deviceInfo=JSON.parse(ff())}else this.deviceInfo=JSON.parse(ff())}setAuthorization(e){e.startsWith("Bearer ")||(e=`Bearer ${e}`),this.authorization=e}getAuthorization(){return this.authorization}getDeviceInfo(){return this.deviceInfo}getDeviceInfoJson(){return JSON.stringify(this.deviceInfo)}async request(e,t,i=!0){let n=oF(),s=sm(QE(Buffer.from(JSON.stringify(this.deviceInfo),"utf-8"),Buffer.from(n,"utf-8"))),o=lF(n,cF),a={Authorization:this.authorization,remoteInfo:s,version:this.deviceInfo.mpVersion||SE,"encrypted-key":o,"Content-Type":"application/json"},c;if(t!==null){let f=JSON.stringify(t),h=QE(Buffer.from(f,"utf-8"),Buffer.from(n,"utf-8"));c=sm(h)}let l=await fetch(e,{method:"POST",headers:a,body:c?JSON.stringify(c):void 0});if(!l.ok)throw new Error(`[MoPan] Request failed: ${l.status} ${l.statusText}`);let d=await l.text();d.startsWith('"')&&d.endsWith('"')&&(d=d.slice(1,-1),d=sF(aF(d),Buffer.from(n,"utf-8")).toString("utf-8"));let u=JSON.parse(d);if(u.code===401&&i&&this.onAuthorizationExpired)return await this.onAuthorizationExpired(new Error("Authorization expired: "+u.message)),this.request(e,t,!1);if(u.code!==200)throw new Error(`[MoPan] API error: code=${u.code}, message=${u.message}`);return u.data}async login(e,t){let i={phone:e,password:t};return this.request(Er+"/login",i)}async loginBySms(e){let t={phone:e};await this.request(Er+"/sendSmsCode",t)}async loginBySmsStep2(e,t){let i={phone:e,smsCode:t};return this.request(Er+"/loginBySms",i)}async getUserInfo(){return this.request(Er+"/getUserInfo",null)}async queryFiles(e,t=1,i="filename",n=!1,s){let o={folderId:e,pageNum:t,orderBy:i,descending:n};return s&&(o.cloudId=s),this.request(Er+"/queryFiles",o)}async getFileDownloadUrl(e,t){let i={fileId:e};return t&&(i.cloudId=t),this.request(Er+"/getFileDownloadUrl",i)}async createFolder(e,t,i){let n={name:e,parentId:t};return i&&(n.cloudId=i),this.request(Er+"/createFolder",n)}async renameFolder(e,t,i){let n={folderId:e,newName:t};i&&(n.cloudId=i),await this.request(Er+"/renameFolder",n)}async renameFile(e,t,i){let n={fileId:e,newName:t};i&&(n.cloudId=i),await this.request(Er+"/renameFile",n)}async addBatchTask(e){return this.request(Er+"/addBatchTask",e)}async checkBatchTask(e){return this.request(Er+"/checkBatchTask",e)}async cancelBatchTask(e,t){await this.request(Er+"/cancelBatchTask",{taskId:e,taskType:t})}async deleteToRecycle(e,t){let i={files:e};t&&(i.cloudId=t),await this.request(Er+"/deleteToRecycle",i)}async initMultiUpload(e,t,i,n,s){let o={fileMd5:e,fileName:t,fileSize:i,parentFolderId:n};return s&&(o.cloudId=s),this.request(Er+"/initMultiUpload",o)}async getAllMultiUploadUrls(e,t){let i={uploadFileId:e,partInfos:t};return this.request(Er+"/getAllMultiUploadUrls",i)}async commitMultiUploadFile(e){let t={uploadFileId:e};return this.request(Er+"/commitMultiUploadFile",t)}async usedSpace(){return this.request(Er+"/usedSpace",null)}}});import{createHash as I$}from"crypto";function hF(r){if(!r)return new Date().toISOString();try{let e=new Date(r);if(!isNaN(e.getTime()))return e.toISOString()}catch{}return new Date().toISOString()}function fF(r){return{name:r.name,size:0,is_dir:!0,modified:hF(r.lastOpTime),sign:r.id,type:1,thumb:"",raw_url:""}}function AF(r){return{name:r.name,size:r.size||0,is_dir:!1,modified:hF(r.lastOpTime),sign:r.id,type:X(r.name,!1),thumb:r.icon?.smallURL||r.icon?.largeURL||"",raw_url:"",hash:r.md5||void 0,hashes:r.md5?{md5:r.md5}:void 0}}function w$(r){let e={...r||{}};return e.phone=(e.phone||"").trim(),e.password=(e.password||"").trim(),e.sms_code=(e.sms_code||"").trim(),e.root_folder_id=e.root_folder_id||"",e.cloud_id=(e.cloud_id||"").trim(),e.order_by=e.order_by||"filename",e.order_direction=e.order_direction||"asc",e.device_info=e.device_info||ff(),e.upload_thread=e.upload_thread||"3",e}var am,gF=J(()=>{"use strict";Be();He();uF();DE();am=class{client;addition;userId="";uploadThread=3;constructor(e,t){this.addition=w$(e),this.uploadThread=parseInt(this.addition.upload_thread||"3"),(this.uploadThread<1||this.uploadThread>32)&&(this.uploadThread=3),this.client=new om(this.addition,async i=>{await this.performLogin(),t&&t(this.client.getDeviceInfoJson(),this.client.getAuthorization())})}async performLogin(){let e=this.addition.sms_code?.trim()||"";if(e==="send")throw await this.client.loginBySms(this.addition.phone),new Error("[MoPan] SMS code sent, please enter the code");let t;e&&e!=="send"?t=await this.client.loginBySmsStep2(this.addition.phone,e):t=await this.client.login(this.addition.phone,this.addition.password),this.client.setAuthorization(t.token);let i=await this.client.getUserInfo();if(this.userId=i.userID,!this.addition.root_folder_id&&t.userCloudStorageRelations){for(let n of t.userCloudStorageRelations)if(n.path==="/\u6587\u4EF6"){this.addition.root_folder_id=n.folderID;break}}}async init(){await this.performLogin()}async list(e,t){let i=await this.resolveFolderId(t),n=[];for(let s=1;;s++){let o=await this.client.queryFiles(i,s,this.addition.order_by||"filename",this.addition.order_direction==="desc",this.addition.cloud_id);if(!o.fileListAO)break;let{folderList:a=[],fileList:c=[]}=o.fileListAO;if(a.length===0&&c.length===0||(n.push(...a.map(fF)),n.push(...c.map(AF)),a.length+c.length<100))break}return de(n,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=t.split("/").filter(Boolean);if(i.length===0)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.getRootId(),type:1,thumb:"",raw_url:""};let n=i.slice(0,-1).join("/"),s=i[i.length-1],o=await this.resolveFolderId(n),a=await this.client.queryFiles(o,1,"filename",!1,this.addition.cloud_id);if(a.fileListAO){let c=a.fileListAO.folderList?.find(d=>d.name===s);if(c)return fF(c);let l=a.fileListAO.fileList?.find(d=>d.name===s);if(l){let d=AF(l),u=await this.client.getFileDownloadUrl(l.id,this.addition.cloud_id).catch(()=>null);if(u?.downloadUrl){let f=u.downloadUrl.replace(/&/g,"&");f=f.replace(/^http:/,"https:");let h=await this.followRedirect(f);d.raw_url=h||f}return d}}throw new Error(`[MoPan] File not found: ${t}`)}async followRedirect(e){try{let t=await fetch(e,{redirect:"manual"});return(t.status===302||t.status===301)&&t.headers.get("location")||e}catch{return null}}async mkdir(e,t){let i=t.split("/").filter(Boolean),n=i.pop()||"\u65B0\u6587\u4EF6\u5939",s=i.join("/"),o=await this.resolveFolderId(s);await this.client.createFolder(n,o,this.addition.cloud_id)}async rename(e,t,i){let n=await this.get("",t);n.is_dir?await this.client.renameFolder(n.sign,i,this.addition.cloud_id):await this.client.renameFile(n.sign,i,this.addition.cloud_id)}async move(e,t,i,n,s){let o=await this.resolveFolderId(s);for(let a of i){let c=n==="/"?`/${a}`:`${n}/${a}`,l=await this.get(e,c);await this.performBatchTask(l,o,2)}}async copy(e,t,i,n,s){let o=await this.resolveFolderId(s);for(let a of i){let c=n==="/"?`/${a}`:`${n}/${a}`,l=await this.get(e,c);await this.performBatchTask(l,o,1)}}async performBatchTask(e,t,i){let n={fileID:e.sign,isFolder:e.is_dir,fileName:e.name},s={userOrCloudID:this.userId,source:1,taskType:i,targetSource:1,targetUserOrCloudID:this.userId,targetType:1,targetFolderID:t,taskStatusDetailDTOList:[n]};this.addition.cloud_id&&(s.userOrCloudID=this.addition.cloud_id,s.source=2,s.targetSource=2,s.targetUserOrCloudID=this.addition.cloud_id);let o=await this.client.addBatchTask(s);for(let a=0;a<10;a++){let c=await this.client.checkBatchTask({taskId:o.taskIDList[0],taskType:o.taskType,targetType:1,targetFolderID:o.targetFolderID,targetSource:s.targetSource,targetUserOrCloudID:s.targetUserOrCloudID});if(c.taskStatus===2)throw await this.client.cancelBatchTask(c.taskID,o.taskType),new Error("[MoPan] File name conflict");if(c.taskStatus===4)return;await new Promise(l=>setTimeout(l,1e3))}throw new Error("[MoPan] Task timeout")}async remove(e,t){let i=await this.get("",t);await this.client.deleteToRecycle([{fileID:i.sign,isFolder:i.is_dir,fileName:i.name}],this.addition.cloud_id)}async put(e,t,i){let n=t.split("/").filter(Boolean),s=n.pop()||"\u672A\u547D\u540D\u6587\u4EF6",o=n.join("/"),a=await this.resolveFolderId(o),c=i,l=I$("md5").update(c).digest("hex"),d=i.length,u=await this.client.initMultiUpload(l,s,d,a,this.addition.cloud_id);if(!u.fileDataExists){let f=await this.client.getAllMultiUploadUrls(u.uploadFileID,u.partInfos);for(let h of f){let g=(h.partNumber-1)*u.partSize,p=h.partNumber===f.length?d:g+u.partSize,I=c.slice(g,p);await fetch(h.uploadUrl,{method:"PUT",body:I,headers:{"Content-Length":String(I.length)}})}}await this.client.commitMultiUploadFile(u.uploadFileID)}getRootId(){return this.addition.root_folder_id||""}async resolveFolderId(e){let t=this.getRootId(),i=e.split("/").filter(Boolean);if(i.length===0)return t;let n=t;for(let s of i){let a=(await this.client.queryFiles(n,1,"filename",!1,this.addition.cloud_id)).fileListAO?.folderList?.find(c=>c.name===s);if(!a)throw new Error(`[MoPan] Folder not found: ${s}`);n=a.id}return n}}});function hf(r){return typeof r=="string"?x$.encode(r):r}function FE(r){let e=r instanceof Uint8Array?r:new Uint8Array(r),t="";for(let i=0;i<e.length;i++)t+=e[i].toString(16).padStart(2,"0");return t}async function _E(r){let e=await crypto.subtle.digest("SHA-256",hf(r));return FE(e)}async function Af(r,e){let t=await crypto.subtle.importKey("raw",hf(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,hf(e));return new Uint8Array(i)}async function pF(r,e){let t=await Af(r,e);return FE(t)}async function C$(r,e){let t=await crypto.subtle.importKey("raw",hf(r),{name:"HMAC",hash:"SHA-1"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,hf(e));return FE(i)}function Gs(r,e=!0){let t=encodeURIComponent(r).replace(/[!'()*]/g,i=>"%"+i.charCodeAt(0).toString(16).toUpperCase());return e||(t=t.replace(/%2F/g,"/")),t}function mF(r=new Date){let e=d=>d.toString().padStart(2,"0"),t=r.getUTCFullYear(),i=e(r.getUTCMonth()+1),n=e(r.getUTCDate()),s=e(r.getUTCHours()),o=e(r.getUTCMinutes()),a=e(r.getUTCSeconds()),c=`${t}${i}${n}`;return{amzDate:`${c}T${s}${o}${a}Z`,dateStamp:c}}async function yF(r,e,t,i="s3"){let n="AWS4"+r,s=await Af(n,e),o=await Af(s,t),a=await Af(o,i);return await Af(a,"aws4_request")}async function IF(r){let{method:e,url:t,region:i,accessKeyId:n,secretAccessKey:s,sessionToken:o,headers:a={},body:c=null,service:l="s3",date:d=new Date}=r,u=new URL(t),{amzDate:f,dateStamp:h}=mF(d),g=c!=null?await _E(c):"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",p={...a};p.host=u.host,p["x-amz-date"]=f,p["x-amz-content-sha256"]=g,o&&(p["x-amz-security-token"]=o);let I=Object.keys(p).map(P=>P.toLowerCase()).sort(),m="";for(let P of I){let j=(Object.entries(p).find(([z])=>z.toLowerCase()===P)?.[1]||"").trim().replace(/\s+/g," ");m+=`${P}:${j} -`}let w=I.join(";"),B=u.pathname||"/",C;try{C=decodeURIComponent(B)}catch{C=B}let x=Gs(C,!1),Q=[];u.searchParams.forEach((P,N)=>{Q.push([N,P])}),Q.sort(([P],[N])=>P<N?-1:P>N?1:0);let R=Q.map(([P,N])=>`${Gs(P)}=${Gs(N)}`).join("&"),U=[e.toUpperCase(),x,R,m,w,g].join(` -`),T=`${h}/${i}/${l}/aws4_request`,D=await _E(U),b=["AWS4-HMAC-SHA256",f,T,D].join(` -`),_=await yF(s,h,i,l),v=await pF(_,b),E=`AWS4-HMAC-SHA256 Credential=${n}/${T}, SignedHeaders=${w}, Signature=${v}`;return p.authorization=E,{headers:p,url:u.toString()}}async function cm(r){let{method:e="GET",url:t,region:i,accessKeyId:n,secretAccessKey:s,sessionToken:o,expiresInSeconds:a=14400,service:c="s3",date:l=new Date,customQueryParams:d={}}=r,u=new URL(t),{amzDate:f,dateStamp:h}=mF(l),g=`${h}/${i}/${c}/aws4_request`;u.searchParams.set("X-Amz-Algorithm","AWS4-HMAC-SHA256"),u.searchParams.set("X-Amz-Credential",`${n}/${g}`),u.searchParams.set("X-Amz-Date",f),u.searchParams.set("X-Amz-Expires",a.toString()),u.searchParams.set("X-Amz-SignedHeaders","host"),o&&u.searchParams.set("X-Amz-Security-Token",o);for(let[v,E]of Object.entries(d))u.searchParams.set(v,E);let p=u.pathname||"/",I;try{I=decodeURIComponent(p)}catch{I=p}let m=Gs(I,!1),w=[];u.searchParams.forEach((v,E)=>{E.toLowerCase()!=="x-amz-signature"&&w.push([E,v])}),w.sort(([v],[E])=>v<E?-1:v>E?1:0);let B=w.map(([v,E])=>`${Gs(v)}=${Gs(E)}`).join("&"),x=`host:${u.host} -`,U=[e.toUpperCase(),m,B,x,"host","UNSIGNED-PAYLOAD"].join(` -`),T=await _E(U),D=["AWS4-HMAC-SHA256",f,g,T].join(` -`),b=await yF(s,h,i,c),_=await pF(b,D);return u.searchParams.set("X-Amz-Signature",_),u.toString()}async function wF(r,e){let t="/auth/tmp_token.json",i=JSON.stringify({channel:"OSS_FULL",scopes:["*"]}),n=t+` -`+i,s=await C$(e,n),o=`TOKEN ${r}:${s}`,a=await fetch("https://api.dogecloud.com"+t,{method:"POST",headers:{"Content-Type":"application/json",Authorization:o},body:i});if(!a.ok)throw new Error(`DogeCloud tmp_token request failed with HTTP ${a.status}`);let c=await a.json();if(c.code!==200||!c.data||!c.data.Credentials)throw new Error(`DogeCloud tmp_token error (${c.code}): ${c.msg||"unknown"}`);return{accessKeyId:c.data.Credentials.accessKeyId,secretAccessKey:c.data.Credentials.secretAccessKey,sessionToken:c.data.Credentials.sessionToken,expiredAt:c.data.ExpiredAt}}var x$,PE=J(()=>{"use strict";x$=new TextEncoder});function nr(...r){return r.map(e=>e.replace(/^\/+|\/+$/g,"")).filter(Boolean).join("/")}function vn(r,e=!1){let t=(r||"").replace(/^\/+/,"");return t&&e&&!t.endsWith("/")&&(t+="/"),t}function gf(r){return r&&r.trim()?r.trim():".openlist"}function na(r){let e=r.replace(/\/+$/,""),t=e.lastIndexOf("/");return t>=0?e.substring(t+1):e}function TE(r){let e=r.replace(/\/+$/,""),t=e.lastIndexOf("/");return t>=0?e.substring(0,t):""}function xF(r,e){let t=("/"+r+"/").replace(/\/+/g,"/");return("/"+e+"/").replace(/\/+/g,"/").startsWith(t)}function Br(r,e){let t=r.match(new RegExp(`<${e}[^>]*>([\\s\\S]*?)<\\/${e}>`,"i"));return t?t[1].trim():void 0}function lm(r,e){let t=[],i=new RegExp(`<${e}[^>]*>([\\s\\S]*?)<\\/${e}>`,"gi"),n;for(;(n=i.exec(r))!==null;)t.push(n[1]);return t}function vc(r){return r.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'/g,"'")}function zs(r,e){let t=Br(r,"Code")||"Unknown",i=Br(r,"Message")||r||`HTTP ${e}`,n=new Error(`S3 Error [${t}]: ${vc(i)} (status ${e})`);return n.code=t,n.status=e,n}function k$(r,e,t,i=!1){let n=[],s=gf(t),o=lm(r,"CommonPrefixes");for(let d of o){let u=Br(d,"Prefix");if(u){let f=vc(u),h=na(f);h&&n.push({name:h,size:0,isFolder:!0,modified:new Date().toISOString(),path:nr(e,h)})}}let a=lm(r,"Contents");for(let d of a){let u=Br(d,"Key");if(!u)continue;let f=vc(u);if(f.endsWith("/"))continue;let h=na(f);if(!i&&(h===s||h===t))continue;let g=parseInt(Br(d,"Size")||"0",10),p=Br(d,"LastModified")||new Date().toISOString(),I=Br(d,"ETag")?.replace(/"/g,"");n.push({name:h,size:g,isFolder:!1,modified:p,path:nr(e,h),etag:I})}let c=Br(r,"IsTruncated")==="true",l=Br(r,"NextMarker");return{files:n,isTruncated:c,nextMarker:l,lastEvaluatedKey:n.length>0?n[n.length-1].path:void 0}}function S$(r,e,t,i=!1){let n=[],s=gf(t),o=lm(r,"CommonPrefixes");for(let d of o){let u=Br(d,"Prefix");if(u){let f=vc(u),h=na(f);h&&n.push({name:h,size:0,isFolder:!0,modified:new Date().toISOString(),path:nr(e,h)})}}let a=lm(r,"Contents");for(let d of a){let u=Br(d,"Key");if(!u)continue;let f=vc(u);if(f.endsWith("/"))continue;let h=na(f);if(!i&&(h===s||h===t))continue;let g=parseInt(Br(d,"Size")||"0",10),p=Br(d,"LastModified")||new Date().toISOString(),I=Br(d,"ETag")?.replace(/"/g,"");n.push({name:h,size:g,isFolder:!1,modified:p,path:nr(e,h),etag:I})}let c=Br(r,"IsTruncated")==="true",l=Br(r,"NextContinuationToken");return{files:n,isTruncated:c,nextContinuationToken:l,lastEvaluatedKey:n.length>0?n[n.length-1].path:void 0}}function D$(r){let e=Br(r,"UploadId");if(!e)throw new Error("InitiateMultipartUpload returned empty UploadId: "+r);return vc(e)}function Q$(r){let e=Br(r,"ETag");if(!e)throw new Error("UploadPartCopy returned empty ETag: "+r);return vc(e).replace(/"/g,"")}function _$(r){let e=Math.max(B$,Math.floor((r-1)/v$)+1);if(e>b$)throw new Error(`Object size ${r} exceeds multipart copy limit`);return e}var E$,B$,b$,v$,dm,CF=J(()=>{"use strict";PE();E$=5*1e3*1e3*1e3,B$=100*1024*1024,b$=5*1024*1024*1024,v$=1e4;dm=class{addition;bucket;endpoint;region;accessKeyId;secretAccessKey;sessionToken;isPathStyle;userAgent;constructor(e){this.addition=e,this.bucket=(e.bucket||"").trim();let t=(e.endpoint||"").trim();!t.startsWith("http://")&&!t.startsWith("https://")&&(t="https://"+t),this.endpoint=t.replace(/\/+$/,""),this.region=(e.region||"").trim()||"openlist",this.accessKeyId=(e.access_key_id||"").trim(),this.secretAccessKey=(e.secret_access_key||"").trim(),this.sessionToken=e.session_token?e.session_token.trim():void 0,this.userAgent=e.user_agent?e.user_agent.trim():void 0;let i=new URL(this.endpoint),n=/^(\d{1,3}\.){3}\d{1,3}$/.test(i.hostname)||i.hostname==="localhost";this.isPathStyle=!!e.force_path_style||n}updateCredentials(e){this.accessKeyId=e.accessKeyId,this.secretAccessKey=e.secretAccessKey,this.sessionToken=e.sessionToken}getUrl(e="",t){let i=new URL(this.endpoint),n="",s=e?vn(e,!1):"";if(this.isPathStyle){let c=[i.pathname.replace(/\/+$/,""),this.bucket,s].filter(Boolean).join("/");i.pathname="/"+c.replace(/^\/+/,""),n=i.toString()}else{let a=i.host.split(":"),c=a[1]?`:${a[1]}`:"",l=`${this.bucket}.${a[0]}${c}`;i.host=l;let u=[i.pathname.replace(/\/+$/,""),s].filter(Boolean).join("/");i.pathname="/"+u.replace(/^\/+/,""),n=i.toString()}let o=new URL(n);if(t)for(let[a,c]of Object.entries(t))c!=null&&o.searchParams.set(a,c);return o.toString()}async fetch(e,t,i=null,n={}){let s={...n};this.userAgent&&(s["user-agent"]=this.userAgent);let{headers:o}=await IF({method:e,url:t,region:this.region,accessKeyId:this.accessKeyId,secretAccessKey:this.secretAccessKey,sessionToken:this.sessionToken,headers:s,body:i}),a={method:e,headers:o};return i!=null&&e!=="GET"&&e!=="HEAD"&&(a.body=i),await fetch(t,a)}async listObjects(e,t="v1",i=!1){let n=vn(e,!0),s=[],o=this.addition.placeholder||"";if(t==="v2"){let a,c;for(;;){let l={"list-type":"2",prefix:n,delimiter:"/"};a&&(l["continuation-token"]=a),c&&(l["start-after"]=c);let d=this.getUrl("",l),u=await this.fetch("GET",d),f=await u.text();if(!u.ok)throw zs(f,u.status);let h=S$(f,e,o,i);if(s.push(...h.files),!h.isTruncated)break;if(h.nextContinuationToken){a=h.nextContinuationToken;continue}if(h.files.length===0)break;c=h.lastEvaluatedKey}}else{let a;for(;;){let c={prefix:n,delimiter:"/"};a&&(c.marker=a);let l=this.getUrl("",c),d=await this.fetch("GET",l),u=await d.text();if(!d.ok)throw zs(u,d.status);let f=k$(u,e,o,i);if(s.push(...f.files),!f.isTruncated)break;if(f.nextMarker)a=f.nextMarker;else if(f.files.length>0)a=f.files[f.files.length-1].path;else break}}return s}async headObject(e){let t=this.getUrl(e),i=await this.fetch("HEAD",t);if(i.status===404)return null;if(!i.ok){let a=await i.text().catch(()=>"");throw zs(a,i.status)}let n=parseInt(i.headers.get("content-length")||"0",10),s=i.headers.get("last-modified")||new Date().toISOString(),o=(i.headers.get("etag")||"").replace(/"/g,"");return{size:n,modified:s,etag:o}}async listPrefixProbe(e,t="v1"){let n={prefix:vn(e,!0),"max-keys":"1"};t==="v2"&&(n["list-type"]="2");let s=this.getUrl("",n),o=await this.fetch("GET",s);if(!o.ok)return!1;let a=await o.text();return a.includes("<Contents>")||a.includes("<CommonPrefixes>")}async putObject(e,t,i="application/octet-stream"){let n=this.getUrl(e),s={"content-type":i},o=await this.fetch("PUT",n,t,s);if(!o.ok){let a=await o.text().catch(()=>"");throw zs(a,o.status)}}async deleteObject(e){let t=this.getUrl(e),i=await this.fetch("DELETE",t);if(!i.ok&&i.status!==404&&i.status!==204){let n=await i.text().catch(()=>"");throw zs(n,i.status)}}async copyObject(e,t,i){if(i!==void 0&&i>E$)return this.copyMultipart(e,t,i);let n=vn(e,!1),s=vn(t,!1),o=Gs(`${this.bucket}/${n}`,!1),a=this.getUrl(s),c={"x-amz-copy-source":o},l=await this.fetch("PUT",a,null,c);if(!l.ok){let d=await l.text().catch(()=>"");throw zs(d,l.status)}}async copyMultipart(e,t,i){let n=vn(e,!1),s=vn(t,!1),o=Gs(`${this.bucket}/${n}`,!1),a=this.getUrl(s,{uploads:""}),c=await this.fetch("POST",a),l=await c.text();if(!c.ok)throw zs(l,c.status);let d=D$(l),u=_$(i),f=[];try{let h=0,g=1;for(;h<i;){let w=Math.min(h+u,i)-1,B=this.getUrl(s,{partNumber:g.toString(),uploadId:d}),C={"x-amz-copy-source":o,"x-amz-copy-source-range":`bytes=${h}-${w}`},x=await this.fetch("PUT",B,null,C),Q=await x.text();if(!x.ok)throw zs(Q,x.status);let R=Q$(Q);f.push({partNumber:g,etag:R}),h+=u,g++}let p=this.getUrl(s,{uploadId:d}),I=["<CompleteMultipartUpload>",...f.map(w=>`<Part><PartNumber>${w.partNumber}</PartNumber><ETag>${w.etag}</ETag></Part>`),"</CompleteMultipartUpload>"].join(""),m=await this.fetch("POST",p,I,{"content-type":"application/xml"});if(!m.ok){let w=await m.text().catch(()=>"");throw zs(w,m.status)}}catch(h){let g=this.getUrl(s,{uploadId:d});throw await this.fetch("DELETE",g).catch(()=>{}),h}}async getLink(e,t,i=4,n="",s=!1,o=!1,a=!1){let c=vn(e,!1),l=Math.max(60,Math.floor(i*3600)),d=this.getUrl(c);if(n)if(s){let h=await cm({url:d,region:this.region,accessKeyId:this.accessKeyId,secretAccessKey:this.secretAccessKey,sessionToken:this.sessionToken,expiresInSeconds:l}),g=new URL(h),p=n.split("://");if(p.length===2&&(p[0]==="http"||p[0]==="https")?(g.protocol=p[0]+":",g.host=p[1].replace(/\/+$/,"")):g.host=n.replace(/\/+$/,""),o){let I="/"+this.bucket;if(g.pathname.startsWith(I)){let m=g.pathname.substring(I.length);m||(m="/"),g.pathname=m}}return{url:g.toString()}}else{let h=n.split("://"),g="https",p=n;h.length===2&&(h[0]==="http"||h[0]==="https")&&(g=h[0],p=h[1].replace(/\/+$/,""));let I=this.isPathStyle?`/${this.bucket}/${c}`:`/${c}`;return o&&I.startsWith(`/${this.bucket}`)&&(I=I.substring(`/${this.bucket}`.length),I||(I="/")),{url:`${g}://${p}${I.startsWith("/")?"":"/"}${I}`}}let u={};if(a){let h=encodeURIComponent(t);u["response-content-disposition"]=`attachment; filename="${h}"; filename*=UTF-8''${h}`}return{url:await cm({url:d,region:this.region,accessKeyId:this.accessKeyId,secretAccessKey:this.secretAccessKey,sessionToken:this.sessionToken,expiresInSeconds:l,customQueryParams:Object.keys(u).length?u:void 0})}}async getDirectUploadInfo(e,t,i=4,n=""){let s=nr(e,t),o=vn(s,!1),a=Math.max(60,Math.floor(i*3600)),c=this.getUrl(o);if(n){let d=new URL(c),u=n.split("://");u.length===2&&(u[0]==="http"||u[0]==="https")?(d.protocol=u[0]+":",d.host=u[1].replace(/\/+$/,"")):d.host=n.replace(/\/+$/,""),c=d.toString()}return{upload_url:await cm({method:"PUT",url:c,region:this.region,accessKeyId:this.accessKeyId,secretAccessKey:this.secretAccessKey,sessionToken:this.sessionToken,expiresInSeconds:a}),method:"PUT"}}}});function F$(r){let e={...r||{}};return e.bucket=(e.bucket||"").trim(),e.endpoint=(e.endpoint||"").trim(),e.region=(e.region||"").trim()||"openlist",e.access_key_id=(e.access_key_id||"").trim(),e.secret_access_key=(e.secret_access_key||"").trim(),e.session_token=(e.session_token||"").trim(),e.root_folder_path=(e.root_folder_path||"/").trim(),e.root_folder_path.startsWith("/")||(e.root_folder_path="/"+e.root_folder_path),e.custom_host=(e.custom_host||"").trim(),e.enable_custom_host_presign=!!e.enable_custom_host_presign,e.sign_url_expire=Number(e.sign_url_expire)||4,e.placeholder=(e.placeholder||"").trim(),e.force_path_style=!!e.force_path_style,e.list_object_version=(e.list_object_version||"v1").toLowerCase(),e.remove_bucket=!!e.remove_bucket,e.add_filename_to_disposition=!!e.add_filename_to_disposition,e.enable_direct_upload=!!e.enable_direct_upload,e.direct_upload_host=(e.direct_upload_host||"").trim(),e.user_agent=(e.user_agent||"").trim(),e.order_by=e.order_by||"name",e.order_direction=e.order_direction||"asc",e}var um,EF=J(()=>{"use strict";Be();He();CF();PE();um=class{client;addition;driverName;dogeExpiredAt;dogeTimer;constructor(e,t="S3"){this.addition=F$(e),this.driverName=t,this.client=new dm(this.addition)}async init(){this.driverName.toLowerCase().includes("doge")&&await this.refreshDogeToken()}async refreshDogeToken(){try{let e=await wF(this.addition.access_key_id,this.addition.secret_access_key);this.dogeExpiredAt=e.expiredAt,this.client.updateCredentials({accessKeyId:e.accessKeyId,secretAccessKey:e.secretAccessKey,sessionToken:e.sessionToken})}catch(e){throw console.error("[S3Driver] DogeCloud init/refresh session error:",e),e}}async checkDogeToken(){if(this.driverName.toLowerCase().includes("doge")){let e=Math.floor(Date.now()/1e3);(!this.dogeExpiredAt||this.dogeExpiredAt-e<120)&&await this.refreshDogeToken()}}drop(){this.dogeTimer&&(clearInterval(this.dogeTimer),this.dogeTimer=void 0)}getRemotePath(e){let t=this.addition.root_folder_path||"/",i=e||"/";return t!=="/"&&!xF(t,i)&&(i=nr(t,i)),vn(i,!1)}async fileItemFromS3(e,t){let i,n;if(!e.isFolder){let s=await this.client.getLink(t,e.name,Number(this.addition.sign_url_expire)||4,this.addition.custom_host,this.addition.enable_custom_host_presign,this.addition.remove_bucket,this.addition.add_filename_to_disposition);i=s.url,n=s.headers}return{name:e.name,size:e.size,is_dir:e.isFolder,modified:e.modified,sign:e.etag||t,type:X(e.name,e.isFolder),thumb:"",raw_url:i,raw_url_headers:n}}async list(e,t){await this.checkDogeToken();let i=this.getRemotePath(t),n=this.addition.list_object_version==="v2"?"v2":"v1",s=await this.client.listObjects(i,n,!1),o=[];for(let a of s){let c=nr(i,a.name),l=await this.fileItemFromS3(a,c);o.push(l)}return de(o,this.addition.order_by||"name",this.addition.order_direction||"asc")}async get(e,t){await this.checkDogeToken();let i=this.getRemotePath(t),n=await this.client.headObject(i);if(n){let a=na(i);return this.fileItemFromS3({name:a,size:n.size,isFolder:!1,modified:n.modified,path:i,etag:n.etag},i)}let s=this.addition.list_object_version==="v2"?"v2":"v1";if(await this.client.listPrefixProbe(i,s)||i===""||i==="/")return{name:na(i),size:0,is_dir:!0,modified:new Date().toISOString(),sign:i,type:1};throw new Error(`Object not found: ${t}`)}async mkdir(e,t){await this.checkDogeToken();let i=this.getRemotePath(t),n=gf(this.addition.placeholder),s=nr(i,n);await this.client.putObject(s,new Uint8Array(0))}async rename(e,t,i){await this.checkDogeToken();let n=this.getRemotePath(t),s=TE(n),o=nr(s,i),a=await this.client.headObject(n);a?(await this.client.copyObject(n,o,a.size),await this.client.deleteObject(n)):(await this.copyDirRecursive(n,o),await this.removeDirRecursive(n))}async move(e,t,i,n,s){await this.checkDogeToken();let o=this.getRemotePath(n),a=this.getRemotePath(s);for(let c of i){let l=nr(o,c),d=nr(a,c),u=await this.client.headObject(l);u?(await this.client.copyObject(l,d,u.size),await this.client.deleteObject(l)):(await this.copyDirRecursive(l,d),await this.removeDirRecursive(l))}}async copy(e,t,i,n,s){await this.checkDogeToken();let o=this.getRemotePath(n),a=this.getRemotePath(s);for(let c of i){let l=nr(o,c),d=nr(a,c),u=await this.client.headObject(l);u?await this.client.copyObject(l,d,u.size):await this.copyDirRecursive(l,d)}}async copyDirRecursive(e,t){let i=this.addition.list_object_version==="v2"?"v2":"v1",n=await this.client.listObjects(e,i,!0);for(let s of n){let o=nr(e,s.name),a=nr(t,s.name);s.isFolder?await this.copyDirRecursive(o,a):await this.client.copyObject(o,a,s.size)}}async remove(e,t,i){await this.checkDogeToken();let n=this.getRemotePath(t);if(i&&i.length>0)for(let s of i){let o=nr(n,s);await this.client.headObject(o)?await this.client.deleteObject(o):await this.removeDirRecursive(o)}else await this.client.headObject(n)?await this.client.deleteObject(n):await this.removeDirRecursive(n)}async removeDirRecursive(e){let t=this.addition.list_object_version==="v2"?"v2":"v1",i=await this.client.listObjects(e,t,!0);for(let s of i){let o=nr(e,s.name);s.isFolder?await this.removeDirRecursive(o):await this.client.deleteObject(o)}let n=gf(this.addition.placeholder);await this.client.deleteObject(nr(e,n)).catch(()=>{}),this.addition.placeholder&&await this.client.deleteObject(nr(e,this.addition.placeholder)).catch(()=>{})}async put(e,t,i){await this.checkDogeToken();let n=this.getRemotePath(t);await this.client.putObject(n,i)}async getDirectUploadInfo(e,t){if(!this.addition.enable_direct_upload)throw new Error("Direct upload is not enabled");await this.checkDogeToken();let i=this.getRemotePath(e);return await this.client.getDirectUploadInfo(i,t,Number(this.addition.sign_url_expire)||4,this.addition.direct_upload_host)}async other(e,t,i){if(e==="direct_upload"||e==="get_direct_upload_info"){let n=i?.name||i?.fileName||na(t),s=TE(t);return await this.getDirectUploadInfo(s,n)}throw new Error(`Unsupported method ${e}`)}}});function UE(r,e){return r<<e|r>>>32-e}function BF(r){if(typeof Buffer<"u")return Buffer.from(r).toString("base64");let e="";for(let t=0;t<r.length;t++)e+=String.fromCharCode(r[t]);return btoa(e)}var fm,bF=J(()=>{"use strict";fm=class r{h0=1732584193;h1=4023233417;h2=2562383102;h3=271733878;h4=3285377520;block=new Uint8Array(64);blockLen=0;totalBytes=0;w=new Int32Array(80);update(e){let t=e.length;this.totalBytes+=t;let i=0;for(;i<t;){let n=64-this.blockLen,s=Math.min(n,t-i);this.block.set(e.subarray(i,i+s),this.blockLen),this.blockLen+=s,i+=s,this.blockLen===64&&(this.processBlock(this.block),this.blockLen=0)}return this}processBlock(e){let t=this.w;for(let c=0;c<16;c++){let l=c*4;t[c]=e[l]<<24|e[l+1]<<16|e[l+2]<<8|e[l+3]}for(let c=16;c<80;c++)t[c]=UE(t[c-3]^t[c-8]^t[c-14]^t[c-16],1);let i=this.h0,n=this.h1,s=this.h2,o=this.h3,a=this.h4;for(let c=0;c<80;c++){let l=0,d=0;c<20?(l=n&s|~n&o,d=1518500249):c<40?(l=n^s^o,d=1859775393):c<60?(l=n&s|n&o|s&o,d=2400959708):(l=n^s^o,d=3395469782);let u=UE(i,5)+l+a+d+t[c]|0;a=o,o=s,s=UE(n,30),n=i,i=u}this.h0=this.h0+i|0,this.h1=this.h1+n|0,this.h2=this.h2+s|0,this.h3=this.h3+o|0,this.h4=this.h4+a|0}getStateHex(){let e=[this.h0,this.h1,this.h2,this.h3,this.h4],t="";for(let i of e){let n=(i&255).toString(16).padStart(2,"0"),s=(i>>>8&255).toString(16).padStart(2,"0"),o=(i>>>16&255).toString(16).padStart(2,"0"),a=(i>>>24&255).toString(16).padStart(2,"0");t+=n+s+o+a}return t.toLowerCase()}digestHex(){let e=new r;e.h0=this.h0,e.h1=this.h1,e.h2=this.h2,e.h3=this.h3,e.h4=this.h4,e.block.set(this.block),e.blockLen=this.blockLen,e.totalBytes=this.totalBytes;let t=e.blockLen<56?56-e.blockLen:120-e.blockLen,i=new Uint8Array(t+8);i[0]=128;let n=e.totalBytes*8,s=Math.floor(e.totalBytes/536870912),o=(n&4294967295)>>>0,a=new DataView(i.buffer,i.byteOffset+t,8);return a.setUint32(0,s,!1),a.setUint32(4,o,!1),e.update(i),[e.h0,e.h1,e.h2,e.h3,e.h4].map(l=>(l>>>0).toString(16).padStart(8,"0")).join("").toLowerCase()}}});function vF(r){let e=new Map;if(!r)return e;let t=r.split(";");for(let i of t){let n=i.trim();if(!n)continue;let s=n.indexOf("=");if(s>0){let o=n.slice(0,s).trim(),a=n.slice(s+1).trim();o&&a&&e.set(o,a)}}return e}function Am(r){let e=[];for(let[t,i]of r.entries())t&&i&&e.push(`${t}=${i}`);return e.join("; ")}var hm,kF=J(()=>{"use strict";bF();hm=class{cookies=new Map;onCookieUpdate;pendingCookie=null;addition;constructor(e,t){this.addition=e,this.cookies=vF(e.cookies||""),this.onCookieUpdate=t}getCookies(){return this.cookies}getCookieStr(){return Am(this.cookies)}setCookiesStr(e){this.cookies=vF(e),this.pendingCookie=Am(this.cookies),this.addition.cookies=this.pendingCookie,this.onCookieUpdate&&this.onCookieUpdate(this.pendingCookie)}updateCookiesFromHeaders(e){let t=e.getSetCookie,i=[];if(typeof t=="function")i=t.call(e);else{let s=e.get("set-cookie");s&&(i=[s])}let n=!1;for(let s of i){let o=s.split(";")[0]||"",a=o.indexOf("=");if(a>0){let c=o.slice(0,a).trim(),l=o.slice(a+1).trim();c&&l&&this.cookies.get(c)!==l&&(this.cookies.set(c,l),n=!0)}}n&&(this.pendingCookie=Am(this.cookies),this.addition.cookies=this.pendingCookie,this.onCookieUpdate&&this.onCookieUpdate(this.pendingCookie))}consumePendingCookie(){let e=this.pendingCookie;return this.pendingCookie=null,e}loginType(){let e=this.cookies.get("wy_uf")||"",t=this.cookies.get("weiyun_wx_openid")||"",i=this.cookies.get("weiyun_qq_openid")||"";return e==="2"&&t?"weixin_openid":e==="2"&&i?"qq_openid":e==="1"?"weixin":e==="0"||!e?"qq":"unknown"}parseTokenInfo(){switch(this.loginType()){case"weixin":return{token_type:1,openid:this.cookies.get("openid")||"",open_appid:this.cookies.get("wy_appid")||"",access_token:this.cookies.get("access_token")||"",login_key_type:192,login_key_value:this.cookies.get("access_token")||""};case"qq":return{token_type:0,login_key_type:27,login_key_value:this.cookies.get("p_skey")||this.cookies.get("skey")||"",openid:""};case"weixin_openid":case"qq_openid":return{token_type:3,login_key_type:1540};default:return{}}}async refreshCtoken(){let e=await fetch("https://www.weiyun.com/disk",{headers:{"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",Cookie:this.getCookieStr()},redirect:"manual"});if(this.updateCookiesFromHeaders(e.headers),e.status>=300&&e.status<400){let t=e.headers.get("location")||"";if(t&&!t.includes("/disk"))throw new Error("[WeiYun] Login cookie expired or invalid, please login again")}}async weixinRefreshToken(){let e=this.cookies.get("wy_appid")||"",t=this.cookies.get("refresh_token")||"";if(!e||!t)return;let i=`https://api.weixin.qq.com/sns/oauth2/refresh_token?grant_type=refresh_token&appid=${encodeURIComponent(e)}&refresh_token=${encodeURIComponent(t)}`,s=await(await fetch(i)).json().catch(()=>({}));if(s.errcode)throw new Error(`[WeiYun] WeChat refresh token failed: ${s.errmsg}`);s.openid&&this.cookies.set("openid",s.openid),s.access_token&&this.cookies.set("access_token",s.access_token),s.refresh_token&&this.cookies.set("refresh_token",s.refresh_token),this.pendingCookie=Am(this.cookies),this.addition.cookies=this.pendingCookie,this.onCookieUpdate&&this.onCookieUpdate(this.pendingCookie)}newHeader(e,t){let i=t.openid||t.minico_openid||"";return{seq:Math.floor(Date.now()/1e3),cmd:e,wx_openid:i,qq_openid:t.qq_openid||"",user_flag:t.token_type??0,env_id:t.env_id||"",type:1,appid:30013,version:3,major_version:3,minor_version:3,fix_version:3}}newBody(e,t,i){return{ReqMsg_body:{ext_req_head:{token_info:i,language_info:{language_type:2052}},[`.weiyun.${e}MsgReq_body`]:t}}}newUploadJson(e,t,i){return{req_header:{cmd:t,appid:30013,major_version:3,minor_version:0,fix_version:0,version:3,user_flag:0},req_body:{ReqMsg_body:{[`weiyun.${e}MsgReq_body`]:i}}}}async request(e,t,i,n,s){let o=this.parseTokenInfo(),a=this.cookies.get("wyctoken")||"",c="",l,d={"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",Referer:"https://www.weiyun.com",Cookie:this.getCookieStr()};if(e==="preUpload")c=`https://www.weiyun.com/api/v3/ftn_pre_upload?g_tk=${encodeURIComponent(a)}&cmd=${i}`,l=JSON.stringify(this.newUploadJson(t,i,n)),d["Content-Type"]="application/json; charset=UTF-8";else if(e==="upload"){c=`https://upload.weiyun.com/ftnup_v2/weiyun?g_tk=${encodeURIComponent(a)}&cmd=${i}`;let p="----WebKitFormBoundaryIifrOqiswelC8nfe";d["Content-Type"]=`multipart/form-data; boundary=${p}`;let I=JSON.stringify(this.newUploadJson(t,i,n)),m=`--${p}\r -Content-Disposition: form-data; name="json"\r -\r -${I}\r -`,w="";s&&s.length>0&&(w=`--${p}\r -Content-Disposition: form-data; name="upload"; filename="blob"\r -Content-Type: application/octet-stream\r -\r -`);let B=`\r ---${p}--\r -`,C=new TextEncoder,x=C.encode(m),Q=s?C.encode(w):new Uint8Array(0),R=C.encode(B),U=s?new Uint8Array(s):new Uint8Array(0),T=x.length+Q.length+U.length+R.length,D=new Uint8Array(T),b=0;D.set(x,b),b+=x.length,Q.length>0&&(D.set(Q,b),b+=Q.length,D.set(U,b),b+=U.length),D.set(R,b),l=D}else c=`https://www.weiyun.com/webapp/json/${e}/${t}?g_tk=${encodeURIComponent(a)}&cmd=${i}`,l=JSON.stringify({req_header:JSON.stringify(this.newHeader(i,o)),req_body:JSON.stringify(this.newBody(t,n,o))}),d["Content-Type"]="application/json; charset=UTF-8";let u=await fetch(c,{method:"POST",headers:d,body:l});if(this.updateCookiesFromHeaders(u.headers),u.status===403)try{await this.refreshCtoken(),(this.loginType()==="weixin"||this.loginType()==="weixin_openid")&&(await this.weixinRefreshToken().catch(()=>{}),await this.refreshCtoken()),d.Cookie=this.getCookieStr(),d.g_tk=this.cookies.get("wyctoken")||"",u=await fetch(c,{method:"POST",headers:d,body:l}),this.updateCookiesFromHeaders(u.headers)}catch(p){throw new Error(`[WeiYun] Request failed (403): ${p.message}`)}if(!u.ok){let p=await u.text().catch(()=>"");throw new Error(`[WeiYun] HTTP ${u.status}: ${p}`)}let f=await u.json().catch(()=>({})),h=f.data||f.result;if(f.ret!==void 0&&f.ret!==0)throw new Error(`[WeiYun] Error (${f.ret}): ${f.msg||"Unknown"}`);if(h?.rsp_header&&h.rsp_header.retcode!==0){let p=h.rsp_header;throw new Error(`[WeiYun] Cmd ${p.cmd} (${p.cmdName||t}) Error (${p.retcode}): ${p.retmsg||"Unknown error"}`)}if(e==="preUpload")return h?.rsp_body?.RspMsg_body?.weiyunPreUploadMsgRsp_body||f;if(e==="upload")return h?.rsp_body?.RspMsg_body?.[`weiyun.${t}MsgRsp_body`]||f;let g=h?.rsp_body?.RspMsg_body;if(typeof g=="string")try{return JSON.parse(g)}catch{return g}return g||h||f}async diskUserInfoGet(){return this.request("weiyunQdiskClient","DiskUserInfoGet",2201,{is_get_upload_flow_flag:!1,is_get_high_speed_flow_info:!1,is_get_weiyun_flag:!1,is_get_space_clean_info:!1,is_get_user_reward_info:!1})}async libDirPathGet(e){return(await this.request("weiyunFileLibClient","LibDirPathGet",26150,{dir_key:e})).items||[]}async diskDirFileList(e,t={}){return this.request("weiyunQdisk","DiskDirList",2208,{dir_key:e,start:t.start||0,count:t.count||500,sort_field:t.sortField??2,reverse_order:t.reverseOrder??!1,get_type:t.getType??0,get_abstract_url:!1,get_dir_detail_info:!1})}async diskFileDownload(e){let i=(await this.request("weiyunQdiskClient","DiskFileBatchDownload",2402,{file_list:[e],download_type:0})).file_list||[];if(!i||i.length===0)throw new Error("[WeiYun] No download link returned");return i[0]}async diskDirCreate(e){return this.request("weiyunQdiskClient","DiskDirCreate",2614,{ppdir_key:e.ppdir_key,pdir_key:e.pdir_key,dir_name:e.dir_name,file_exist_option:2,create_type:1})}async diskFileRename(e,t){await this.request("weiyunQdiskClient","DiskFileRename",2605,{ppdir_key:e.ppdir_key,pdir_key:e.pdir_key,file_id:e.file_id,src_filename:e.filename,filename:t})}async diskDirAttrModify(e,t){await this.request("weiyunQdiskClient","DiskDirAttrModify",2615,{ppdir_key:e.ppdir_key,pdir_key:e.pdir_key,dir_key:e.dir_key,src_dir_name:e.dir_name,dst_dir_name:t})}async diskFileDelete(e){await this.request("weiyunQdiskClient","DiskDirFileBatchDeleteEx",2509,{file_list:[e]})}async diskDirDelete(e){await this.request("weiyunQdiskClient","DiskDirFileBatchDeleteEx",2509,{dir_list:[e]})}async diskFileMove(e,t){await this.request("weiyunQdiskClient","DiskDirFileBatchMove",2618,{src_ppdir_key:e.ppdir_key,src_pdir_key:e.pdir_key,file_list:[e],dst_ppdir_key:t.pdir_key,dst_pdir_key:t.dir_key})}async diskDirMove(e,t){await this.request("weiyunQdiskClient","DiskDirFileBatchMove",2618,{src_ppdir_key:e.ppdir_key,src_pdir_key:e.pdir_key,dir_list:[e],dst_ppdir_key:t.pdir_key,dst_pdir_key:t.dir_key})}async preUpload(e,t,i,n,s,o=4,a=1){let l=0,d=n,u=0;n>0&&(d=n%1048576,d===0&&(d=1048576),u=d%128,u===0&&(u=128),l=n-d);let f=[],h=new fm;for(let x=0;x<l;x+=1048576){let Q=s.subarray(x,x+1048576);h.update(Q),f.push({sha:h.getStateHex(),offset:x,size:1048576})}let g=s.subarray(l,l+d-u);h.update(g);let p=h.getStateHex(),I=s.subarray(l+d-u,n);h.update(I);let m=BF(I),w=h.digestHex();f.push({sha:w,offset:l,size:d});let B={common_upload_req:{ppdir_key:e,pdir_key:t,file_size:n,filename:i,file_exist_option:a,use_mutil_channel:!0},upload_scr:0,channel_count:o,block_size:1048576,check_sha:p,check_data:m,block_info_list:f},C=await this.request("preUpload","PreUpload",247120,B);return C.common_upload_rsp&&(C.common_upload_rsp.file_sha=w,C.common_upload_rsp.file_size=n),C}async addUploadChannel(e,t,i){return this.request("upload","AddChannel",247122,{upload_key:i.upload_key,ex:i.ex,orig_channel_count:e,dest_channel_count:t,speed:4303})}async uploadPiece(e,t,i){let n=await this.request("upload","UploadPiece",247121,{upload_key:t.upload_key,ex:t.ex,channel:e},i);return n.channel&&n.channel.len===0&&n.upload_state===1&&(n.channel.len=e.len),n}}});function T$(r){return Buffer.from(JSON.stringify(r),"utf8").toString("base64")}function U$(r){try{let e=JSON.parse(Buffer.from(r,"base64").toString("utf8"));if(!e||!e.uploadKey||!e.ex||!Number.isInteger(e.partCount)||e.partCount<1)throw new Error("invalid upload session");return e}catch{throw new Error("[WeiYun] \u4E0A\u4F20\u4F1A\u8BDD\u65E0\u6548\u6216\u5DF2\u635F\u574F")}}function _F(r){if(!r)return new Date().toISOString();try{let e=typeof r=="string"?parseInt(r,10):r;if(!isNaN(e)&&e>0){let i=e<1e10?e*1e3:e;return new Date(i).toISOString()}let t=new Date(r);if(!isNaN(t.getTime()))return t.toISOString()}catch{}return new Date().toISOString()}function DF(r){return{name:r.dir_name,size:0,is_dir:!0,modified:_F(r.dir_mtime||r.dir_ctime),sign:r.dir_key,type:1,thumb:"",raw_url:""}}function QF(r){return{name:r.filename,size:r.file_size||0,is_dir:!1,modified:_F(r.file_mtime||r.file_ctime),sign:r.file_id,type:X(r.filename,!1),thumb:r.ext_info?.thumb_url||"",raw_url:""}}function RE(r){let e={...r||{}};return e.root_folder_id=(e.root_folder_id||"").trim(),e.cookies=(e.cookies||"").trim(),e.order_by=e.order_by||"name",e.order_direction=e.order_direction||"asc",e.upload_thread=e.upload_thread||"4",e}var P$,SF,gm,FF=J(()=>{"use strict";Be();He();kF();P$=45,SF=1024*1024;gm=class{client;addition;rootFolderId="";rootPdirKey="";uploadThreads=4;pathFolderCache=new Map;budget={used:0,limit:P$};constructor(e,t){this.addition=RE(e),this.client=new hm(this.addition,t)}async init(){let e=parseInt(this.addition.upload_thread||"4",10);if(this.uploadThreads=Math.min(32,Math.max(4,isNaN(e)?4:e)),this.addition.upload_thread=String(this.uploadThreads),await this.client.refreshCtoken(),this.addition.root_folder_id)this.rootFolderId=this.addition.root_folder_id;else{let n=await this.client.diskUserInfoGet();this.rootFolderId=n.main_dir_key||n.root_dir_key||"",this.addition.root_folder_id=this.rootFolderId}if(!this.rootFolderId)throw new Error("[WeiYun] Failed to obtain root folder ID");let t=await this.client.libDirPathGet(this.rootFolderId);if(!t||t.length===0)throw new Error(`[WeiYun] Invalid root directory ID: ${this.rootFolderId}`);let i=t[t.length-1];this.rootPdirKey=i.pdir_key||"",this.pathFolderCache.set("/",{dirKey:this.rootFolderId,pdirKey:this.rootPdirKey,dirName:i.dir_name||"root"})}consumePendingCookie(){return this.client.consumePendingCookie()}async resolveFolder(e){let t="/"+String(e||"").split("/").filter(Boolean).join("/");if(t==="/"||t===`/${this.rootFolderId}`)return{dirKey:this.rootFolderId,pdirKey:this.rootPdirKey,dirName:"root"};if(this.pathFolderCache.has(t))return this.pathFolderCache.get(t);let i=t.split("/").filter(Boolean),n={dirKey:this.rootFolderId,pdirKey:this.rootPdirKey,dirName:"root"},s="";for(let o=0;o<i.length;o++){let a=i[o],c=(()=>{try{return decodeURIComponent(a)}catch{return a}})();if(s="/"+i.slice(0,o+1).join("/"),this.pathFolderCache.has(s)){n=this.pathFolderCache.get(s);continue}this.budget.used++,this.budget.used>=this.budget.limit&&console.warn(`[WeiYun] Cloudflare Worker subrequest budget limit (${this.budget.limit}) reached.`);let u=((await this.client.diskDirFileList(n.dirKey,{count:500,getType:1})).dir_list||[]).find(f=>f.dir_name===a||f.dir_name===c||f.dir_key===a);if(!u)throw new Error(`[WeiYun] Directory '${a}' not found in folder '${n.dirKey}'`);n={dirKey:u.dir_key,pdirKey:n.dirKey,dirName:u.dir_name},this.pathFolderCache.set(s,n)}return n}async resolveFile(e){let t=String(e||"").split("/").filter(Boolean);if(t.length===0)throw new Error("[WeiYun] \u8DEF\u5F84\u65E0\u6548");let i=t[t.length-1],n=(()=>{try{return decodeURIComponent(i)}catch{return i}})(),s="/"+t.slice(0,t.length-1).join("/"),o=await this.resolveFolder(s);this.budget.used++;let a=await this.client.diskDirFileList(o.dirKey,{count:500,getType:0}),c=(a.file_list||[]).find(d=>d.filename===i||d.filename===n||d.file_id===i);if(c)return{file:c,parent:o,isDir:!1};let l=(a.dir_list||[]).find(d=>d.dir_name===i||d.dir_name===n||d.dir_key===i);if(l)return{folder:l,parent:o,isDir:!0};throw new Error(`[WeiYun] \u6587\u4EF6\u6216\u76EE\u5F55\u672A\u627E\u5230: ${i}`)}async list(e,t){this.budget.used=0;let i=await this.resolveFolder(t),n=[],s=[],o=0;for(;;){if(this.budget.used++,this.budget.used>=this.budget.limit){console.warn(`[WeiYun] Subrequest budget limit (${this.budget.limit}) reached while listing ${t}`);break}let c=this.addition.order_by==="size"?3:this.addition.order_by==="updated_at"?2:1,l=this.addition.order_direction==="desc",d=await this.client.diskDirFileList(i.dirKey,{start:o,count:500,sortField:c,reverseOrder:l,getType:0}),u=d.dir_list||[],f=d.file_list||[];for(let h of u)h.pdir_key=i.dirKey,n.push(h);for(let h of f)h.pdir_key=i.dirKey,s.push(h);if(o=n.length+s.length,d.finish_flag||u.length===0&&f.length===0)break}let a=[...n.map(DF),...s.map(QF)];return de(a,this.addition.order_by==="size"?"size":this.addition.order_by==="updated_at"?"updated_at":"name",this.addition.order_direction)}async get(e,t){this.budget.used=0;let i="/"+String(t||"").split("/").filter(Boolean).join("/");if(i==="/"||i===`/${this.rootFolderId}`)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.rootFolderId,type:1,raw_url:""};let{file:n,folder:s,parent:o,isDir:a}=await this.resolveFile(t);if(a&&s)return DF(s);if(n){let c=QF(n);try{let l=await this.client.diskFileDownload({ppdir_key:o.pdirKey,pdir_key:o.dirKey,file_id:n.file_id,filename:n.filename});c.raw_url=l.download_url,c.raw_url_headers={Cookie:`${l.cookie_name}=${l.cookie_value}`}}catch(l){console.warn(`[WeiYun] \u83B7\u53D6 ${n.filename} \u4E0B\u8F7D\u94FE\u63A5\u5931\u8D25:`,l.message),c.raw_url_error=l.message}return c}throw new Error(`[WeiYun] \u6761\u76EE\u672A\u627E\u5230: ${t}`)}async mkdir(e,t){this.budget.used=0;let i=String(t||"").split("/").filter(Boolean),n=i.pop()||"\u65B0\u5EFA\u6587\u4EF6\u5939",s="/"+i.join("/"),o=await this.resolveFolder(s);await this.client.diskDirCreate({ppdir_key:o.pdirKey,pdir_key:o.dirKey,dir_name:n})}async rename(e,t,i){this.budget.used=0;let{file:n,folder:s,parent:o,isDir:a}=await this.resolveFile(t);a&&s?await this.client.diskDirAttrModify({ppdir_key:o.pdirKey,pdir_key:o.dirKey,dir_key:s.dir_key,dir_name:s.dir_name},i):n&&await this.client.diskFileRename({ppdir_key:o.pdirKey,pdir_key:o.dirKey,file_id:n.file_id,filename:n.filename},i)}async remove(e,t,i){this.budget.used=0;let{file:n,folder:s,parent:o,isDir:a}=await this.resolveFile(t);a&&s?await this.client.diskDirDelete({ppdir_key:o.pdirKey,pdir_key:o.dirKey,dir_key:s.dir_key,dir_name:s.dir_name}):n&&await this.client.diskFileDelete({ppdir_key:o.pdirKey,pdir_key:o.dirKey,file_id:n.file_id,filename:n.filename})}async move(e,t,i,n,s){this.budget.used=0;let{file:o,folder:a,parent:c,isDir:l}=await this.resolveFile(n),d=await this.resolveFolder(t);l&&a?await this.client.diskDirMove({ppdir_key:c.pdirKey,pdir_key:c.dirKey,dir_key:a.dir_key,dir_name:a.dir_name},{pdir_key:d.pdirKey,dir_key:d.dirKey}):o&&await this.client.diskFileMove({ppdir_key:c.pdirKey,pdir_key:c.dirKey,file_id:o.file_id,filename:o.filename},{pdir_key:d.pdirKey,dir_key:d.dirKey})}async copy(e,t,i,n,s){throw new Error("[WeiYun] \u5FAE\u4E91\u63A5\u53E3\u4E0D\u652F\u6301\u590D\u5236\u64CD\u4F5C (Copy not supported)")}async put(e,t,i){let n=String(t||"").split("/").filter(Boolean),s=n.pop();if(!s)throw new Error("[WeiYun] \u4E0A\u4F20\u8DEF\u5F84\u65E0\u6548");let o="/"+n.join("/"),a=await this.resolveFolder(o),c=await this.client.preUpload(a.pdirKey,a.dirKey,s,i.length,i,4,1);if(c.file_exist)return;let l={upload_key:c.upload_key||"",ex:c.ex||""},d=c.channel_list||[];d.length===0&&(d=[{id:0,offset:0,len:i.length}]);for(let u of d){let f={...u};for(;f.offset<i.length;){let h=Math.min(f.len||SF,i.length-f.offset),g=i.subarray(f.offset,f.offset+h),p=await this.client.uploadPiece(f,l,g);if(p.upload_state===2)break;p.channel?f=p.channel:f.offset+=h}}}async createUploadSession(e,t,i,n,s){this.budget.used=0;let o=await this.resolveFolder(t||"/"),a=SF,c=Math.max(1,Math.ceil(Math.max(0,Number(n)||0)/a)),l=new Uint8Array(0),d=await this.client.preUpload(o.pdirKey,o.dirKey,i,Math.max(0,Number(n)||0),l,4,1);if(d.file_exist)return{reuse:!0,partCount:0,chunkSize:a,session:""};let u={upload_key:d.upload_key||"",ex:d.ex||""},f=d.channel_list||[];if(f.length<this.uploadThreads&&u.upload_key)try{let h=await this.client.addUploadChannel(f.length,this.uploadThreads,u);h.channels&&(f=[...f,...h.channels])}catch{}return{reuse:!1,partCount:c,chunkSize:a,session:T$({uploadKey:u.upload_key,ex:u.ex,parentDirKey:o.pdirKey,pdirKey:o.dirKey,fileName:i,size:Math.max(0,Number(n)||0),partCount:c,chunkSize:a,channels:f})}}async uploadPart(e,t,i){let n=U$(e);if(!Number.isInteger(t)||t<1||t>n.partCount)throw new Error(`[WeiYun] \u5206\u7247\u5E8F\u53F7\u65E0\u6548: ${t}`);let s=(t-1)*n.chunkSize,o={id:t-1,offset:s,len:i.length},a={upload_key:n.uploadKey,ex:n.ex};return await this.client.uploadPiece(o,a,i),{partNumber:t}}async completeUploadSession(e,t=[]){}}});async function $$(r,e){let t=`${r}${e}1appkey`,i=await Ho(t),n=$t(i);return`div101.${r}${n}`}async function PF(r,e,t,i,n,s,o){let a=await $$(r,s);return[`ANDROID-${t}/${n}`,"protocolVersion/200","accesstype/",`clientid/${e}`,`clientversion/${n}`,"action_type/","networktype/WIFI","sessionid/",`deviceid/${r}`,"providername/NONE",`devicesign/${a}`,"refresh_token/",`sdkversion/${i}`,`datetime/${Date.now()}`,`usrno/${o}`,`appname/android-${t}`,"session_origin/","grant_type/","appid/","clientip/","devicename/Xiaomi_M2004j7ac","osversion/13","platformversion/10","accessmode/","devicemodel/M2004J7AC"].join(" ")}function NE(r,e){try{let t=new URL(e);return`${r.toUpperCase()}:${t.pathname}`}catch{let t=e.match(/:\/\/[^/]+((\/[^/\s?#]+)*)/);return`${r.toUpperCase()}:${t?t[1]:""}`}}var R$,N$,q$,O$,M$,L$,H$,K$,G$,z$,J$,j$,pm,TF=J(()=>{"use strict";Gr();R$=["SOP04dGzk0TNO7t7t9ekDbAmx+eq0OI1ovEx","nVBjhYiND4hZ2NCGyV5beamIr7k6ifAsAbl","Ddjpt5B/Cit6EDq2a6cXgxY9lkEIOw4yC1GDF28KrA","VVCogcmSNIVvgV6U+AochorydiSymi68YVNGiz","u5ujk5sM62gpJOsB/1Gu/zsfgfZO","dXYIiBOAHZgzSruaQ2Nhrqc2im","z5jUTBSIpBN9g4qSJGlidNAutX6","KJE2oveZ34du/g1tiimm"],N$=["C9qPpZLN8ucRTaTiUMWYS9cQvWOE","+r6CQVxjzJV6LCV","F","pFJRC","9WXYIDGrwTCz2OiVlgZa90qpECPD6olt","/750aCr4lm/Sly/c","RB+DT/gZCrbV","","CyLsf7hdkIRxRm215hl","7xHvLi2tOYP0Y92b","ZGTXXxu8E/MIWaEDB+Sm/","1UI3","E7fP5Pfijd+7K+t6Tg/NhuLq0eEUVChpJSkrKxpO","ihtqpG6FMt65+Xk+tWUH2","NhXXU9rg4XXdzo7u5o"],q$=["KHBJ07an7ROXDoK7Db","G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE","JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb","fQnw/AmSlbbI91Ik15gpddGgyU7U","/Dv9JdPYSj3sHiWjouR95NTQff","yGx2zuTjbWENZqecNI+edrQgqmZKP","ljrbSzdHLwbqcRn","lSHAsqCkGDGxQqqwrVu","TsWXI81fD1","vk7hBjawK/rOSrSWajtbMk95nfgf3"],O$="1.53.2",M$="com.pikcloud.pikpak",L$="2.0.6.206003",H$="2.0.0",K$="mypikpak.com",G$="8.0.3",z$="undefined",J$="mypikpak.com",j$="8.0.3";pm=class{addition;clientId;clientSecret;clientVersion;packageName;sdkVersion;algorithms;deviceId;userId="";userAgent="";accessToken="";refreshTokenVal="";captchaTokenVal="";onTokenRefreshed;constructor(e,t){this.addition=e,this.onTokenRefreshed=t,this.refreshTokenVal=e.refresh_token||"",this.captchaTokenVal=e.captcha_token||"";let i=e.platform||"web",n=e.client_id,s=e.client_secret;i==="android"?(this.clientId=n||"",this.clientSecret=s||"",this.clientVersion=O$,this.packageName=M$,this.sdkVersion=L$,this.algorithms=R$):i==="pc"?(this.clientId=n||"",this.clientSecret=s||"",this.clientVersion=z$,this.packageName=J$,this.sdkVersion=j$,this.algorithms=q$,this.userAgent="MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.6.11.4955 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36"):(this.clientId=n||"",this.clientSecret=s||"",this.clientVersion=H$,this.packageName=K$,this.sdkVersion=G$,this.algorithms=N$,this.userAgent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),this.deviceId=e.device_id||$t(`${e.username||""}${e.password||""}`)}getCaptchaSign(){let e=String(Date.now()),t=`${this.clientId}${this.clientVersion}${this.packageName}${this.deviceId}${e}`;for(let i of this.algorithms)t=$t(t+i);return{timestamp:e,sign:`1.${t}`}}async refreshCaptchaToken(e,t){let i={action:e,captcha_token:this.captchaTokenVal,client_id:this.clientId,device_id:this.deviceId,meta:t,redirect_uri:"xlaccsdk01://xbase.cloud/callback?state=harbor"},s=await(await fetch(`https://user.mypikpak.net/v1/shield/captcha/init?client_id=${encodeURIComponent(this.clientId)}`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":this.userAgent||"OpenList","X-Device-ID":this.deviceId},body:JSON.stringify(i)})).json();if(s.error_code&&s.error_code!==0)throw new Error(`Captcha error ${s.error_code}: ${s.error_description||s.error}`);if(s.url)throw new Error(`PikPak requires captcha verification: ${s.url}`);return s.captcha_token&&(this.captchaTokenVal=s.captcha_token),this.captchaTokenVal}async refreshCaptchaTokenAtLogin(e,t){let{timestamp:i,sign:n}=this.getCaptchaSign(),s={client_version:this.clientVersion,package_name:this.packageName,user_id:t,timestamp:i,captcha_sign:n};return this.refreshCaptchaToken(e,s)}async refreshCaptchaTokenInLogin(e,t){let i={};return/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(t)?i.email=t:t.length>=11&&t.length<=18?i.phone_number=t:i.username=t,this.refreshCaptchaToken(e,i)}async login(){if(!this.addition.username||!this.addition.password)throw new Error("PikPak username or password is required when refresh_token is not provided");let e="https://user.mypikpak.net/v1/auth/signin";this.captchaTokenVal||await this.refreshCaptchaTokenInLogin(NE("POST",e),this.addition.username);let i=await(await fetch(`${e}?client_id=${encodeURIComponent(this.clientId)}`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":this.userAgent||"OpenList","X-Device-ID":this.deviceId,"X-Captcha-Token":this.captchaTokenVal},body:JSON.stringify({captcha_token:this.captchaTokenVal,client_id:this.clientId,client_secret:this.clientSecret,username:this.addition.username,password:this.addition.password})})).json();if(i.error_code&&i.error_code!==0)throw new Error(`PikPak login failed (${i.error_code}): ${i.error_description||i.error}`);this.accessToken=i.access_token,this.refreshTokenVal=i.refresh_token,this.userId=i.sub||"",this.onTokenRefreshed&&await this.onTokenRefreshed({accessToken:this.accessToken,refreshToken:this.refreshTokenVal,captchaToken:this.captchaTokenVal})}async refreshToken(){if(!this.refreshTokenVal){await this.login();return}let i=await(await fetch(`https://user.mypikpak.net/v1/auth/token?client_id=${encodeURIComponent(this.clientId)}`,{method:"POST",headers:{"Content-Type":"application/json","User-Agent":this.userAgent||"OpenList","X-Device-ID":this.deviceId},body:JSON.stringify({client_id:this.clientId,client_secret:this.clientSecret,grant_type:"refresh_token",refresh_token:this.refreshTokenVal})})).json();if(i.error_code&&i.error_code!==0){if(i.error_code===4126&&this.addition.username){await this.login();return}throw new Error(`PikPak token refresh failed (${i.error_code}): ${i.error_description||i.error}`)}this.accessToken=i.access_token,this.refreshTokenVal=i.refresh_token,this.userId=i.sub||"",this.onTokenRefreshed&&await this.onTokenRefreshed({accessToken:this.accessToken,refreshToken:this.refreshTokenVal,captchaToken:this.captchaTokenVal})}async init(){this.addition.platform==="android"&&(this.userAgent=await PF(this.deviceId,this.clientId,this.packageName,this.sdkVersion,this.clientVersion,this.packageName,this.userId)),this.refreshTokenVal?await this.refreshToken():await this.login();try{await this.refreshCaptchaTokenAtLogin(NE("GET","https://api-drive.mypikpak.net/drive/v1/files"),this.userId)}catch(e){console.warn("[PikPak] post-login captcha init warning:",e)}this.addition.platform==="android"&&(this.userAgent=await PF(this.deviceId,this.clientId,this.packageName,this.sdkVersion,this.clientVersion,this.packageName,this.userId))}async request(e,t={}){let{method:i="GET",params:n,body:s,retryCount:o=0}=t,a=e;if(n&&Object.keys(n).length>0){let f=new URLSearchParams(n).toString();a+=(e.includes("?")?"&":"?")+f}let c={"User-Agent":this.userAgent||"OpenList","X-Device-ID":this.deviceId};this.captchaTokenVal&&(c["X-Captcha-Token"]=this.captchaTokenVal),this.accessToken&&(c.Authorization=`Bearer ${this.accessToken}`),s&&typeof s=="object"&&!(s instanceof Uint8Array)&&(c["Content-Type"]="application/json");let l=await fetch(a,{method:i,headers:c,body:s&&typeof s=="object"&&!(s instanceof Uint8Array)?JSON.stringify(s):s}),d=await l.text(),u={};try{u=JSON.parse(d)}catch{u=d}if(u&&typeof u=="object"&&u.error_code){let f=u.error_code;if((f===4122||f===4121||f===16)&&o<2)return await this.refreshToken(),this.request(e,{...t,retryCount:o+1});if(f===9&&o<2)return await this.refreshCaptchaTokenAtLogin(NE(i,e),this.userId),this.request(e,{...t,retryCount:o+1});throw new Error(`PikPak API Error (${f}): ${u.error_description||u.error||JSON.stringify(u)}`)}if(!l.ok)throw new Error(`PikPak request failed with status ${l.status}: ${d}`);return u}}});var mm,UF=J(()=>{"use strict";Be();He();Gr();TF();mm=class{addition;client;rootId="";idCache=new Map;constructor(e,t){this.addition=e,this.rootId=e.root_folder_id||"",this.client=new pm(e,t)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"":t}async resolveParentId(e){let t=this.cleanPath(e);if(!t)return this.rootId;if(this.idCache.has(t))return this.idCache.get(t);let i=t.split("/").filter(Boolean),n=this.rootId,s="";for(let o of i){if(s+="/"+o,this.idCache.has(s)){n=this.idCache.get(s);continue}let c=(await this.getFiles(n)).find(l=>l.name===o);if(!c)throw new Error(`Path not found: ${s}`);n=c.id,this.idCache.set(s,n)}return n}async getFiles(e){let t=[],i="";for(;;){let n={parent_id:e,thumbnail_size:"SIZE_LARGE",with_audit:"true",limit:"100",filters:JSON.stringify({phase:{eq:"PHASE_TYPE_COMPLETE"},trashed:{eq:!1}})};i&&(n.page_token=i);let s=await this.client.request("https://api-drive.mypikpak.net/drive/v1/files",{method:"GET",params:n});if(s.files&&s.files.length>0&&t.push(...s.files),!s.next_page_token||s.next_page_token===i)break;i=s.next_page_token}return t}async list(e,t){let i=await this.resolveParentId(t),n=await this.getFiles(i),s=this.cleanPath(t),o=n.map(a=>{let c=a.kind==="drive#folder",l=s?`${s}/${a.name}`:`/${a.name}`;this.idCache.set(l,a.id);let d=a.web_content_link||"";return!this.addition.disable_media_link&&a.medias&&a.medias.length>0&&a.medias[0].link?.url&&(d=a.medias[0].link.url),{name:a.name,size:parseInt(a.size||"0",10)||0,is_dir:c,created:a.created_time,modified:a.modified_time||new Date().toISOString(),sign:a.id,type:X(a.name,c),thumb:a.thumbnail_link,raw_url:d}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").pop()||"root";if(!i)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.rootId,type:1,raw_url:""};let s=await this.resolveParentId(t),o={_magic:"2021",usage:this.addition.disable_media_link?"FETCH":"CACHE",thumbnail_size:"SIZE_LARGE"},a=await this.client.request(`https://api-drive.mypikpak.net/drive/v1/files/${encodeURIComponent(s)}`,{method:"GET",params:o}),c=a.kind==="drive#folder",l=a.web_content_link||"";return!this.addition.disable_media_link&&a.medias&&a.medias.length>0&&a.medias[0].link?.url&&(l=a.medias[0].link.url),{name:a.name||n,size:parseInt(a.size||"0",10)||0,is_dir:c,created:a.created_time,modified:a.modified_time||new Date().toISOString(),sign:a.id,type:X(a.name||n,c),thumb:a.thumbnail_link,raw_url:l}}async mkdir(e,t){let i=this.cleanPath(t),n=i.split("/").slice(0,-1).join("/"),s=i.split("/").pop()||"",o=await this.resolveParentId(n),a=await this.client.request("https://api-drive.mypikpak.net/drive/v1/files",{method:"POST",body:{kind:"drive#folder",parent_id:o,name:s}});a?.file?.id&&this.idCache.set(i,a.file.id)}async rename(e,t,i){let n=await this.resolveParentId(t);await this.client.request(`https://api-drive.mypikpak.net/drive/v1/files/${encodeURIComponent(n)}`,{method:"PATCH",body:{name:i}});let s=this.cleanPath(t);this.idCache.delete(s)}async remove(e,t,i){let n=await this.resolveParentId(t),s=await this.getFiles(n),o=[];for(let c of i){let l=s.find(d=>d.name===c);l&&o.push(l.id)}if(o.length===0)return;await this.client.request("https://api-drive.mypikpak.net/drive/v1/files:batchTrash",{method:"POST",body:{ids:o}});let a=this.cleanPath(t);for(let c of i)this.idCache.delete(a?`${a}/${c}`:`/${c}`)}async move(e,t,i,n,s){let o=await this.resolveParentId(n),a=await this.resolveParentId(s),c=await this.getFiles(o),l=[];for(let u of i){let f=c.find(h=>h.name===u);f&&l.push(f.id)}if(l.length===0)return;await this.client.request("https://api-drive.mypikpak.net/drive/v1/files:batchMove",{method:"POST",body:{ids:l,to:{parent_id:a}}});let d=this.cleanPath(n);for(let u of i)this.idCache.delete(d?`${d}/${u}`:`/${u}`)}async copy(e,t,i,n,s){let o=await this.resolveParentId(n),a=await this.resolveParentId(s),c=await this.getFiles(o),l=[];for(let d of i){let u=c.find(f=>f.name===d);u&&l.push(u.id)}l.length!==0&&await this.client.request("https://api-drive.mypikpak.net/drive/v1/files:batchCopy",{method:"POST",body:{ids:l,to:{parent_id:a}}})}async put(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/"),o=n.split("/").pop()||"upload",a=await this.resolveParentId(s),c=(await Ho(new Uint8Array(i))).toUpperCase(),l=await this.client.request("https://api-drive.mypikpak.net/drive/v1/files",{method:"POST",body:{kind:"drive#file",name:o,size:String(i.length),hash:c,upload_type:"UPLOAD_TYPE_RESUMABLE",objProvider:{provider:"UPLOAD_TYPE_UNKNOWN"},parent_id:a,folder_type:"NORMAL"}});if(!l.resumable)return;let d=l.resumable.params,u=d.endpoint;this.addition.platform==="android"&&(u="mypikpak.net");let f=`https://${d.bucket}.${u}/${d.key}`,h=await fetch(f,{method:"PUT",headers:{"Content-Length":String(i.length),"x-oss-security-token":d.security_token},body:new Uint8Array(i)});if(!h.ok)throw new Error(`PikPak OSS upload failed: ${h.statusText}`)}async getDetails(){let e=await this.client.request("https://api-drive.mypikpak.net/drive/v1/about",{method:"GET"});return{totalSpace:parseInt(e.quota.limit||"0",10)||0,usedSpace:parseInt(e.quota.usage||"0",10)||0}}}});var ym,RF=J(()=>{"use strict";ym=class{addition;address;authorization="";onTokenRefreshed;decryptedRepos=new Map;constructor(e,t){this.addition=e,this.address=(e.address||"").replace(/\/+$/,""),this.onTokenRefreshed=t,e.token&&(this.authorization=`Token ${e.token}`)}async getToken(){if(this.addition.token){this.authorization=`Token ${this.addition.token}`;return}if(!this.addition.username||!this.addition.password)throw new Error("Seafile requires either token or username/password to authenticate");let e=new URLSearchParams;e.set("username",this.addition.username),e.set("password",this.addition.password);let t=await fetch(`${this.address}/api2/auth-token/`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:e.toString()});if(!t.ok){let n=await t.text();throw new Error(`Seafile auth failed (${t.status}): ${n}`)}let i=await t.json();this.authorization=`Token ${i.token}`,this.addition.token=i.token,this.onTokenRefreshed&&await this.onTokenRefreshed(i.token)}async request(e,t={}){let{method:i="GET",params:n,body:s,isFormData:o=!1,retryCount:a=0}=t,c=e.startsWith("http")?e:`${this.address}${e}`;if(n&&Object.keys(n).length>0){let g=new URLSearchParams(n).toString();c+=(c.includes("?")?"&":"?")+g}let l={Accept:"application/json"};this.authorization&&(l.Authorization=this.authorization);let d=s;if(o&&s&&!(s instanceof FormData)){let g=new URLSearchParams;for(let[p,I]of Object.entries(s))g.set(p,String(I));l["Content-Type"]="application/x-www-form-urlencoded",d=g.toString()}else s&&typeof s=="object"&&!(s instanceof FormData)&&!(s instanceof Uint8Array)&&(l["Content-Type"]="application/json",d=JSON.stringify(s));let u=await fetch(c,{method:i,headers:l,body:d});if(u.status===401&&a<1)return await this.getToken(),this.request(e,{...t,retryCount:a+1});if(!u.ok){let g=await u.text();throw new Error(`Seafile request failed (${u.status}): ${g}`)}return(u.headers.get("content-type")||"").includes("application/json")?await u.json():await u.text()}async getLibraryInfo(e){return this.request(`/api2/repos/${e}/`)}async decryptLibrary(e){if(!e.encrypted)return;if(!this.addition.repo_pwd)throw new Error("Seafile encrypted library password (repo_pwd) is not configured");let t=this.decryptedRepos.get(e.id)||0;if(Date.now()-t<1800*1e3)return;let i=await this.request(`/api2/repos/${e.id}/`,{method:"POST",isFormData:!0,body:{password:this.addition.repo_pwd}});if(typeof i=="string"&&!i.includes("success"))throw new Error(`Failed to decrypt Seafile library: ${i}`);this.decryptedRepos.set(e.id,Date.now())}}});var Im,NF=J(()=>{"use strict";Be();He();RF();Im=class{addition;client;repoId;rootPath="/";constructor(e,t){this.addition=e,this.repoId=e.repo_id||void 0,this.client=new ym(e,t)}async init(){await this.client.getToken(),this.rootPath="/"+(this.addition.root_folder_path||"/").split("/").filter(Boolean).join("/")}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolveRepoAndPath(e){let t=this.cleanPath(e);if(this.repoId)return{repoId:this.repoId,innerPath:t,isRootListRepos:!1};if(t==="/")return{repoId:"",innerPath:"/",isRootListRepos:!0};let i=t.split("/").filter(Boolean),n=i[0],s="/"+i.slice(1).join("/"),a=(await this.client.request("/api2/repos/")).find(c=>c.name===n||c.id===n);if(!a)throw new Error(`Seafile library '${n}' not found`);return a.encrypted&&await this.client.decryptLibrary(a),{repoId:a.id,innerPath:s==="/"?"/":s,isRootListRepos:!1}}async list(e,t){let{repoId:i,innerPath:n,isRootListRepos:s}=await this.resolveRepoAndPath(t);if(s){let l=(await this.client.request("/api2/repos/")).map(d=>({name:d.name,size:d.size||0,is_dir:!0,modified:d.mtime?new Date(d.mtime*1e3).toISOString():new Date().toISOString(),sign:d.id,type:1,raw_url:""}));return de(l,this.addition.order_by,this.addition.order_direction)}let a=(await this.client.request(`/api2/repos/${encodeURIComponent(i)}/dir/`,{params:{p:n}})).map(c=>{let l=c.type==="dir";return{name:c.name,size:c.size||0,is_dir:l,modified:c.mtime?new Date(c.mtime*1e3).toISOString():new Date().toISOString(),sign:c.id,type:X(c.name,l),raw_url:""}});return de(a,this.addition.order_by,this.addition.order_direction)}async get(e,t){let{repoId:i,innerPath:n,isRootListRepos:s}=await this.resolveRepoAndPath(t),a=this.cleanPath(t).split("/").filter(Boolean).pop()||"root";if(s)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""};if(n==="/"){let u=await this.client.getLibraryInfo(i);return{name:u.name||a,size:u.size||0,is_dir:!0,modified:u.mtime?new Date(u.mtime*1e3).toISOString():new Date().toISOString(),sign:u.id,type:1,raw_url:""}}let c="";try{let u=await this.client.request(`/api2/repos/${encodeURIComponent(i)}/file/`,{params:{p:n,reuse:"1"}});typeof u=="string"&&(c=u.replace(/^"|"$/g,"").trim())}catch{}let l=n.split("/").slice(0,-1).join("/")||"/",d=n.split("/").pop()||"";try{let f=(await this.client.request(`/api2/repos/${encodeURIComponent(i)}/dir/`,{params:{p:l}})).find(h=>h.name===d);if(f){let h=f.type==="dir";return{name:f.name,size:f.size||0,is_dir:h,modified:f.mtime?new Date(f.mtime*1e3).toISOString():new Date().toISOString(),sign:f.id,type:X(f.name,h),raw_url:c}}}catch{}return{name:a,size:0,is_dir:!c,modified:new Date().toISOString(),sign:"",type:X(a,!c),raw_url:c}}async mkdir(e,t){let{repoId:i,innerPath:n}=await this.resolveRepoAndPath(t);await this.client.request(`/api2/repos/${encodeURIComponent(i)}/dir/`,{method:"POST",isFormData:!0,params:{p:n},body:{operation:"mkdir"}})}async rename(e,t,i){let{repoId:n,innerPath:s}=await this.resolveRepoAndPath(t);await this.client.request(`/api2/repos/${encodeURIComponent(n)}/file/`,{method:"POST",isFormData:!0,params:{p:s},body:{operation:"rename",newname:i}})}async remove(e,t,i){let{repoId:n,innerPath:s}=await this.resolveRepoAndPath(t);for(let o of i){let a=s==="/"?`/${o}`:`${s}/${o}`;await this.client.request(`/api2/repos/${encodeURIComponent(n)}/file/`,{method:"DELETE",params:{p:a}})}}async move(e,t,i,n,s){let o=await this.resolveRepoAndPath(n),a=await this.resolveRepoAndPath(s);for(let c of i){let l=o.innerPath==="/"?`/${c}`:`${o.innerPath}/${c}`;await this.client.request(`/api2/repos/${encodeURIComponent(o.repoId)}/file/`,{method:"POST",isFormData:!0,params:{p:l},body:{operation:"move",dst_repo:a.repoId,dst_dir:a.innerPath}})}}async copy(e,t,i,n,s){let o=await this.resolveRepoAndPath(n),a=await this.resolveRepoAndPath(s);for(let c of i){let l=o.innerPath==="/"?`/${c}`:`${o.innerPath}/${c}`;await this.client.request(`/api2/repos/${encodeURIComponent(o.repoId)}/file/`,{method:"POST",isFormData:!0,params:{p:l},body:{operation:"copy",dst_repo:a.repoId,dst_dir:a.innerPath}})}}async put(e,t,i){let{repoId:n,innerPath:s}=await this.resolveRepoAndPath(t),o=s.split("/").slice(0,-1).join("/")||"/",a=s.split("/").pop()||"upload",l=(await this.client.request(`/api2/repos/${encodeURIComponent(n)}/upload-link/`,{params:{p:o}})||"").replace(/^"|"$/g,"").trim();if(!l)throw new Error("Failed to get Seafile upload link");let d=new FormData;d.append("parent_dir",o),d.append("replace","1"),d.append("file",new Blob([new Uint8Array(i)],{type:"application/octet-stream"}),a),await this.client.request(l,{method:"POST",body:d})}}});var wm,qF=J(()=>{"use strict";wm=class{addition;accessToken="";refreshTokenVal="";onTokenRefreshed;constructor(e,t){this.addition=e,this.refreshTokenVal=e.refresh_token||"",this.onTokenRefreshed=t}async refreshToken(){if(this.addition.use_online_api!==!1){let s=`${this.addition.api_url_address||"https://api.oplist.org/yandexui/renewapi"}?refresh_ui=${encodeURIComponent(this.refreshTokenVal)}&server_use=true&driver_txt=yandexui_go`,o=await fetch(s,{method:"GET",headers:{Accept:"application/json"}});if(!o.ok)throw new Error(`Failed to refresh Yandex token online: ${o.statusText}`);let a=await o.json();if(!a.access_token)throw new Error(`Yandex online token refresh failed: ${a.text||"No access token returned"}`);this.accessToken=a.access_token,a.refresh_token&&(this.refreshTokenVal=a.refresh_token,this.addition.refresh_token=a.refresh_token),this.onTokenRefreshed&&await this.onTokenRefreshed({accessToken:this.accessToken,refreshToken:this.refreshTokenVal});return}if(!this.addition.client_id||!this.addition.client_secret)throw new Error("Yandex Disk requires client_id and client_secret when online API is disabled");let e=new URLSearchParams;e.set("grant_type","refresh_token"),e.set("refresh_token",this.refreshTokenVal),e.set("client_id",this.addition.client_id),e.set("client_secret",this.addition.client_secret);let t=await fetch("https://oauth.yandex.com/token",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:e.toString()});if(!t.ok){let n=await t.text();throw new Error(`Yandex OAuth refresh failed (${t.status}): ${n}`)}let i=await t.json();this.accessToken=i.access_token,this.refreshTokenVal=i.refresh_token,this.addition.refresh_token=i.refresh_token,this.onTokenRefreshed&&await this.onTokenRefreshed({accessToken:this.accessToken,refreshToken:this.refreshTokenVal})}async init(){await this.refreshToken()}async request(e,t={}){let{method:i="GET",params:n,body:s,retryCount:o=0}=t,a=e.startsWith("http")?e:`https://cloud-api.yandex.net/v1/disk/resources${e}`;if(n&&Object.keys(n).length>0){let u=new URLSearchParams(n).toString();a+=(a.includes("?")?"&":"?")+u}let c={Accept:"application/json",Authorization:`OAuth ${this.accessToken}`};s&&typeof s=="object"&&!(s instanceof Uint8Array)&&(c["Content-Type"]="application/json");let l=await fetch(a,{method:i,headers:c,body:s&&typeof s=="object"&&!(s instanceof Uint8Array)?JSON.stringify(s):s});if(l.status===401&&o<1)return await this.refreshToken(),this.request(e,{...t,retryCount:o+1});if(!l.ok){let u=await l.text(),f=u;try{let h=JSON.parse(u);f=h.message||h.description||u}catch{}throw new Error(`Yandex Disk API Error (${l.status}): ${f}`)}return(l.headers.get("content-type")||"").includes("application/json")?await l.json():await l.text()}async getFiles(e,t,i){let s=0,o=[];for(;;){let a={path:e||"/",limit:String(100),offset:String(s)};t&&(a.sort=i==="desc"?`-${t}`:t);let l=(await this.request("",{method:"GET",params:a}))._embedded;if(!l||!l.items||(o.push(...l.items),l.total<=s+100||l.items.length===0))break;s+=100}return o}async getDownloadLink(e){return(await this.request("/download",{method:"GET",params:{path:e}})).href}async getUploadLink(e){return(await this.request("/upload",{method:"GET",params:{path:e,overwrite:"true"}})).href}}});var xm,OF=J(()=>{"use strict";Be();He();qF();xm=class{addition;client;constructor(e,t){this.addition=e,this.client=new wm(e,t)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async list(e,t){let i=this.cleanPath(t),s=(await this.client.getFiles(i,this.addition.order_by,this.addition.order_direction)).map(o=>{let a=o.type==="dir";return{name:o.name,size:o.size||0,is_dir:a,created:o.created,modified:o.modified||new Date().toISOString(),sign:o.path||o.name,type:X(o.name,a),thumb:o.preview,raw_url:""}});return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"/",type:1,raw_url:""};let s=await this.client.request("",{method:"GET",params:{path:i}}),o=s.type==="dir",a="";if(!o)try{a=await this.client.getDownloadLink(i)}catch(c){console.warn("[Yandex] get download link failed:",c)}return{name:s.name||n,size:s.size||0,is_dir:o,created:s.created,modified:s.modified||new Date().toISOString(),sign:s.path||i,type:X(s.name||n,o),raw_url:a}}async mkdir(e,t){let i=this.cleanPath(t);await this.client.request("",{method:"PUT",params:{path:i}})}async rename(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/")||"/",o=s==="/"?`/${i}`:`${s}/${i}`;await this.client.request("/move",{method:"POST",params:{from:n,path:o,overwrite:"true"}})}async remove(e,t,i){let n=this.cleanPath(t);for(let s of i){let o=n==="/"?`/${s}`:`${n}/${s}`;await this.client.request("",{method:"DELETE",params:{path:o}})}}async move(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s);for(let c of i){let l=o==="/"?`/${c}`:`${o}/${c}`,d=a==="/"?`/${c}`:`${a}/${c}`;await this.client.request("/move",{method:"POST",params:{from:l,path:d,overwrite:"true"}})}}async copy(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s);for(let c of i){let l=o==="/"?`/${c}`:`${o}/${c}`,d=a==="/"?`/${c}`:`${a}/${c}`;await this.client.request("/copy",{method:"POST",params:{from:l,path:d,overwrite:"true"}})}}async put(e,t,i){let n=this.cleanPath(t),s=await this.client.getUploadLink(n),o=await fetch(s,{method:"PUT",headers:{"Content-Length":String(i.length),"Content-Type":"application/octet-stream"},body:new Uint8Array(i)});if(!o.ok)throw new Error(`Yandex upload failed: ${o.statusText}`)}}});function V$(r,e){let t=new Array(256),i=new Array(256),n=[],s=r.length;for(let l=0;l<256;l++)t[l]=r.charCodeAt(l%s),i[l]=l;let o=0;for(let l=0;l<256;l++){o=(o+i[l]+t[l])%256;let d=i[l];i[l]=i[o],i[o]=d}let a=0;o=0;for(let l=0;l<e.length;l++){a=(a+1)%256,o=(o+i[a])%256;let d=i[a];i[a]=i[o],i[o]=d;let u=i[(i[a]+i[o])%256];n.push(e.charCodeAt(l)^u)}let c="";for(let l of n)c+=String.fromCharCode(l);return btoa(c)}var Cm,MF=J(()=>{"use strict";Cm=class{addition;baseUrl="https://www.terabox.com";urlDomainPrefix="jp";jsToken="";onCookieRefreshed;constructor(e,t){this.addition=e,this.onCookieRefreshed=t}async resetJsToken(){let e=await fetch(this.baseUrl,{method:"GET",headers:{Cookie:this.addition.cookie,Accept:"application/json, text/plain, */*",Referer:this.baseUrl,"User-Agent":"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox","X-Requested-With":"XMLHttpRequest"}});if(!e.ok)throw new Error(`Failed to fetch TeraBox home page: ${e.statusText}`);let t=await e.text(),i=t.match(/`function%20fn%28a%29%7Bwindow.jsToken%20%3D%20a%7D%3Bfn%28%22([^"]+?)%22%29`/);if(i&&i[1]){this.jsToken=i[1];return}let n=t.match(/jsToken\s*=\s*["']([^"']+)["']/);if(n&&n[1]){this.jsToken=n[1];return}this.jsToken=""}async request(e,t={}){let{method:i="GET",params:n,body:s,isFormData:o=!1,retryCount:a=0}=t,c=e.startsWith("http")?e:`${this.baseUrl}${e}`,l={app_id:"250528",web:"1",channel:"dubox",clienttype:"0",...n||{}};this.jsToken&&(l.jsToken=this.jsToken);let d=new URLSearchParams(l).toString();c+=(c.includes("?")?"&":"?")+d;let u={Cookie:this.addition.cookie,Accept:"application/json, text/plain, */*",Referer:this.baseUrl,"User-Agent":"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox","X-Requested-With":"XMLHttpRequest"},f=s;if(o&&s&&!(s instanceof FormData)){let I=new URLSearchParams;for(let[m,w]of Object.entries(s))I.set(m,String(w));u["Content-Type"]="application/x-www-form-urlencoded",f=I.toString()}else s&&typeof s=="object"&&!(s instanceof FormData)&&!(s instanceof Uint8Array)&&(u["Content-Type"]="application/json",f=JSON.stringify(s));let h=await fetch(c,{method:i,headers:u,body:f}),g=await h.text(),p={};try{p=JSON.parse(g)}catch{p=g}if(p&&typeof p=="object"&&p.errno!==void 0){let I=Number(p.errno);if((I===4000023||I===450016)&&a<2)return await this.resetJsToken(),this.request(e,{...t,retryCount:a+1});if(I===-6&&a<2){let m=h.headers.get("url-domain-prefix");if(m)return this.urlDomainPrefix=m,this.baseUrl=`https://${m}.terabox.com`,this.request(e,{...t,retryCount:a+1})}}return p}async init(){let e=await this.request("/api/check/login",{method:"GET"});if(e.errno!==0)throw e.errno===9e3?new Error("TeraBox is not yet available in this area (errno 9000)"):new Error(`Failed to verify TeraBox login status according to cookie (errno ${e.errno})`)}async genSign(){let e=await this.request("/api/home/info",{method:"GET"});if(!e.data||!e.data.sign1||!e.data.sign3)throw new Error("Failed to get TeraBox sign keys from home/info");return V$(e.data.sign3,e.data.sign1)}async linkOfficial(e){let t=await this.genSign(),i={type:"dlink",fidlist:`[${e}]`,sign:t,vip:"2",timestamp:String(Math.floor(Date.now()/1e3))},n=await this.request("/api/download",{method:"GET",params:i});if(!n.dlink||n.dlink.length===0)throw new Error(`TeraBox fid ${e} no dlink found (errno: ${n.errno})`);let s=n.dlink[0].dlink;return(await fetch(s,{method:"GET",redirect:"manual",headers:{Cookie:this.addition.cookie,"User-Agent":"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox"}})).headers.get("location")||s}async linkCrack(e){let t={target:JSON.stringify([e]),dlink:"1",origin:"dlna"},i=await this.request("/api/filemetas",{method:"GET",params:t});if(!i.info||i.info.length===0||!i.info[0].dlink)throw new Error(`TeraBox crack download failed for ${e}`);return i.info[0].dlink}async manage(e,t){let i=new URLSearchParams;return i.set("async","0"),i.set("filelist",JSON.stringify(t)),i.set("ondup","newcopy"),this.request("/api/filemanager",{method:"POST",params:{onnest:"fail",opera:e},isFormData:!0,body:{async:"0",filelist:JSON.stringify(t),ondup:"newcopy"}})}}});var Em,LF=J(()=>{"use strict";Be();He();Gr();MF();Em=class{addition;client;constructor(e,t){this.addition=e,this.client=new Cm(e,t)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async list(e,t){let i=this.cleanPath(t),n=[],s=1,o=100;for(;;){let c={dir:i,page:String(s),num:String(o)};this.addition.order_by&&(c.order=this.addition.order_by,this.addition.order_direction==="desc"&&(c.desc="1"));let l=await this.client.request("/api/list",{method:"GET",params:c});if(l.errno===9e3)throw new Error("TeraBox is not yet available in this area");if(!l.list||l.list.length===0)break;n.push(...l.list),s++}let a=n.map(c=>{let l=c.isdir===1;return{name:c.server_filename,size:c.size||0,is_dir:l,modified:c.server_mtime?new Date(c.server_mtime*1e3).toISOString():new Date().toISOString(),sign:String(c.fs_id),type:X(c.server_filename,l),thumb:c.thumbs?.url3,raw_url:""}});return de(a,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""};let s=i.split("/").slice(0,-1).join("/")||"/",o=i.split("/").pop()||"",c=(await this.client.request("/api/list",{method:"GET",params:{dir:s,page:"1",num:"1000"}})).list?.find(u=>u.server_filename===o);if(!c)return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""};let l=c.isdir===1,d="";if(!l)try{this.addition.download_api==="crack"?d=await this.client.linkCrack(i):d=await this.client.linkOfficial(c.fs_id)}catch(u){console.warn("[TeraBox] get download link failed:",u)}return{name:c.server_filename,size:c.size||0,is_dir:l,modified:c.server_mtime?new Date(c.server_mtime*1e3).toISOString():new Date().toISOString(),sign:String(c.fs_id),type:X(c.server_filename,l),thumb:c.thumbs?.url3,raw_url:d,raw_url_headers:{"User-Agent":"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox"}}}async mkdir(e,t){let i=this.cleanPath(t);await this.client.request("/api/create",{method:"POST",isFormData:!0,params:{a:"commit"},body:{path:i,isdir:"1",block_list:"[]"}})}async rename(e,t,i){let n=this.cleanPath(t);await this.client.manage("rename",[{path:n,newname:i}])}async remove(e,t,i){let n=this.cleanPath(t),s=i.map(o=>n==="/"?`/${o}`:`${n}/${o}`);await this.client.manage("delete",s)}async move(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s),c=i.map(l=>({path:o==="/"?`/${l}`:`${o}/${l}`,dest:a,newname:l}));await this.client.manage("move",c)}async copy(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s),c=i.map(l=>({path:o==="/"?`/${l}`:`${o}/${l}`,dest:a,newname:l}));await this.client.manage("copy",c)}async put(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/")||"/",o=n.split("/").pop()||"upload",l=(await(await fetch("https://jp-data.terabox.com/rest/2.0/pcs/file?method=locateupload")).json()).host||"d.terabox.com",d=$t(new Uint8Array(i)),u={path:n,autoinit:"1",target_path:s,block_list:JSON.stringify([d]),local_mtime:String(Math.floor(Date.now()/1e3)),file_limit_switch_v34:"true"},f=await this.client.request("/api/precreate",{method:"POST",isFormData:!0,body:u});if(f.errno!==0)throw new Error(`TeraBox precreate failed (errno: ${f.errno})`);if(f.return_type===2)return;let h=`https://${l}/rest/2.0/pcs/superfile2?method=upload&path=${encodeURIComponent(n)}&uploadid=${encodeURIComponent(f.uploadid)}&partseq=0`,g=new FormData;g.append("file",new Blob([new Uint8Array(i)],{type:"application/octet-stream"}),o);let p=await fetch(h,{method:"POST",headers:{Cookie:this.addition.cookie,"User-Agent":"terabox;1.37.0.7;PC;PC-Windows;10.0.22631;WindowsTeraBox"},body:g});if(!p.ok)throw new Error(`TeraBox upload chunk failed: ${p.statusText}`);let I={path:n,size:String(i.length),uploadid:f.uploadid,target_path:s,block_list:JSON.stringify([d]),local_mtime:String(Math.floor(Date.now()/1e3))};await this.client.request("/api/create",{method:"POST",isFormData:!0,params:{isdir:"0",rtype:"1"},body:I})}}});var Bm,HF=J(()=>{"use strict";Bm=class{addition;constructor(e){this.addition=e}async request(e,t={}){let{method:i="GET",params:n,body:s}=t,o=e;if(n&&Object.keys(n).length>0){let u=new URLSearchParams(n).toString();o+=(o.includes("?")?"&":"?")+u}let a={Authorization:`Bearer ${this.addition.access_token}`,Accept:"application/json"};s&&typeof s=="object"&&!(s instanceof Uint8Array)&&(a["Content-Type"]="application/json");let c=await fetch(o,{method:i,headers:a,body:s&&typeof s=="object"&&!(s instanceof Uint8Array)?JSON.stringify(s):s}),l=await c.text(),d={};try{d=JSON.parse(l)}catch{d=l}if(d&&typeof d=="object"&&d.status&&d.status!=="SUCCESS")throw new Error(`MediaTrack API Error: ${d.message||JSON.stringify(d)}`);if(!c.ok)throw new Error(`MediaTrack request failed (${c.status}): ${l}`);return d}async init(){await this.request("https://kayle.api.mediatrack.cn/users",{method:"GET"})}async getFiles(e){let t=[],i=1,n="";for(this.addition.order_by&&(n=(this.addition.order_desc?"-":"")+this.addition.order_by);;){let s={page:String(i),size:"50"};n&&(s.sort=n);let a=(await this.request(`https://jayce.api.mediatrack.cn/v4/assets/${encodeURIComponent(e)}/children`,{method:"GET",params:s})).data?.assets||[];if(a.length===0)break;t.push(...a),i++}return t}async getDownloadUrl(e){let t=this.addition.project_id||"",i=`https://kayn.api.mediatrack.cn/v1/download_token/asset?asset_id=${encodeURIComponent(e)}&source_type=project&password=&source_id=${encodeURIComponent(t)}`,s=(await this.request(i,{method:"GET"}))?.data?.token;if(!s)throw new Error(`Failed to get download token for asset ${e}`);let o=`https://kayn.api.mediatrack.cn/v1/download/redirect?token=${encodeURIComponent(s)}`;return(await fetch(o,{method:"GET",redirect:"manual"})).headers.get("location")||o}}});var bm,KF=J(()=>{"use strict";Be();He();HF();bm=class{addition;client;rootId="";idCache=new Map;constructor(e){this.addition=e,this.rootId=e.root_folder_id||"",this.client=new Bm(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"":t}async resolveParentId(e){let t=this.cleanPath(e);if(!t)return this.rootId;if(this.idCache.has(t))return this.idCache.get(t);let i=t.split("/").filter(Boolean),n=this.rootId,s="";for(let o of i){if(s+="/"+o,this.idCache.has(s)){n=this.idCache.get(s);continue}let c=(await this.client.getFiles(n)).find(l=>l.title===o);if(!c)throw new Error(`Path not found: ${s}`);n=c.id,this.idCache.set(s,n)}return n}async list(e,t){let i=await this.resolveParentId(t),n=await this.client.getFiles(i),s=this.cleanPath(t),o=n.map(a=>{let c=!a.file,l=s?`${s}/${a.title}`:`/${a.title}`;this.idCache.set(l,a.id);let d="";return a.file&&a.file.cover&&(d="https://nano.mtres.cn/"+a.file.cover),{name:a.title,size:parseInt(a.size||"0",10)||0,is_dir:c,created:a.created_at,modified:a.updated_at||new Date().toISOString(),sign:a.id,type:X(a.title,c),thumb:d,raw_url:""}});return de(o,this.addition.order_by,this.addition.order_desc?"desc":"asc")}async get(e,t){let i=this.cleanPath(t),n=i.split("/").pop()||"root";if(!i)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.rootId,type:1,raw_url:""};let s=i.split("/").slice(0,-1).join("/"),o=await this.resolveParentId(s),c=(await this.client.getFiles(o)).find(f=>f.title===n);if(!c)return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:0,raw_url:""};let l=!c.file,d="";if(!l)try{d=await this.client.getDownloadUrl(c.id)}catch(f){console.warn("[MediaTrack] get download URL failed:",f)}let u="";return c.file&&c.file.cover&&(u="https://nano.mtres.cn/"+c.file.cover),{name:c.title,size:parseInt(c.size||"0",10)||0,is_dir:l,created:c.created_at,modified:c.updated_at||new Date().toISOString(),sign:c.id,type:X(c.title,l),thumb:u,raw_url:d}}async mkdir(e,t){let i=this.cleanPath(t),n=i.split("/").slice(0,-1).join("/"),s=i.split("/").pop()||"",o=await this.resolveParentId(n);await this.client.request(`https://jayce.api.mediatrack.cn/v3/assets/${encodeURIComponent(o)}/children`,{method:"POST",body:{type:1,title:s}})}async rename(e,t,i){let n=await this.resolveParentId(t);await this.client.request(`https://jayce.api.mediatrack.cn/v3/assets/${encodeURIComponent(n)}`,{method:"PUT",body:{title:i}});let s=this.cleanPath(t);this.idCache.delete(s)}async remove(e,t,i){let n=await this.resolveParentId(t),s=await this.client.getFiles(n),o=[];for(let c of i){let l=s.find(d=>d.title===c);l&&o.push(l.id)}if(o.length===0)return;await this.client.request("https://jayce.api.mediatrack.cn/v4/assets/batch/delete",{method:"DELETE",body:{origin_id:n,ids:o}});let a=this.cleanPath(t);for(let c of i)this.idCache.delete(a?`${a}/${c}`:`/${c}`)}async move(e,t,i,n,s){let o=await this.resolveParentId(n),a=await this.resolveParentId(s),c=await this.client.getFiles(o),l=[];for(let d of i){let u=c.find(f=>f.title===d);u&&l.push(u.id)}l.length!==0&&await this.client.request("https://jayce.api.mediatrack.cn/v4/assets/batch/move",{method:"POST",body:{parent_id:a,ids:l}})}async copy(e,t,i,n,s){let o=await this.resolveParentId(n),a=await this.resolveParentId(s),c=await this.client.getFiles(o),l=[];for(let d of i){let u=c.find(f=>f.title===d);u&&l.push(u.id)}l.length!==0&&await this.client.request("https://jayce.api.mediatrack.cn/v4/assets/batch/clone",{method:"POST",body:{parent_id:a,ids:l}})}async put(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/"),o=n.split("/").pop()||"upload",a=await this.resolveParentId(s),l=`assets/${Math.random().toString(36).slice(2)}`,u=(await this.client.request("https://jayce.api.mediatrack.cn/v3/storage/tokens/asset",{method:"GET",params:{src:l}})).data?.url;u&&await fetch(u,{method:"PUT",headers:{"Content-Length":String(i.length)},body:new Uint8Array(i)}),await this.client.request(`https://jayce.api.mediatrack.cn/v3/assets/${encodeURIComponent(a)}/children`,{method:"POST",body:{category:0,description:o,mime:"application/octet-stream",size:String(i.length),src:l,title:o,type:0}})}}});var vm,GF=J(()=>{"use strict";Be();He();vm=class{addition;pathPairs=[];rootOrder=[];pathMap=new Map;constructor(e){this.addition=e,this.parsePaths()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}parsePaths(){let t=(this.addition.paths||"").split(` -`).map(i=>i.trim()).filter(Boolean);this.pathPairs=[],this.rootOrder=[],this.pathMap.clear();for(let i of t){let n="/",s=i,o=i.indexOf(":");o>0&&(n=this.cleanPath(i.slice(0,o)),s=i.slice(o+1).trim()),s=this.cleanPath(s),this.pathPairs.push({aliasSubPath:n,targetPath:s}),this.pathMap.has(n)||(this.rootOrder.push(n),this.pathMap.set(n,[])),this.pathMap.get(n).push(s)}}async init(){this.parsePaths()}getTargetsForPath(e){let t=this.cleanPath(e),i=[];if(this.rootOrder.length===1&&this.rootOrder[0]==="/"){let n=this.pathMap.get("/")||[];for(let s of n){let o=t==="/"?s:`${s}${t}`;i.push({targetFullPath:this.cleanPath(o),subPath:t})}return i}for(let[n,s]of this.pathMap.entries())if(n===t)for(let o of s)i.push({targetFullPath:o,subPath:"/"});else if(t.startsWith(n==="/"?"/":`${n}/`)){let o=t.slice(n==="/"?0:n.length);for(let a of s){let c=o==="/"?a:`${a}${o}`;i.push({targetFullPath:this.cleanPath(c),subPath:o})}}return i}async list(e,t){let i=this.cleanPath(t),{listItems:n}=await Promise.resolve().then(()=>(Yt(),Mi));if(i==="/"&&this.rootOrder.length>1){let c=[];for(let l of this.rootOrder){let d=l.replace(/^\//,"").split("/")[0]||l;c.some(u=>u.name===d)||c.push({name:d,size:0,is_dir:!0,modified:new Date().toISOString(),sign:l,type:1,raw_url:""})}return de(c,this.addition.order_by,this.addition.order_direction)}let s=this.getTargetsForPath(t);if(s.length===0)return[];let o=new Map;for(let c of s)try{let l=await n(c.targetFullPath);for(let d of l.content)o.has(d.name)||o.set(d.name,d)}catch(l){console.warn(`[Alias] listing target ${c.targetFullPath} warning:`,l)}let a=Array.from(o.values());return de(a,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/"&&this.rootOrder.length>1)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"/",type:1,raw_url:""};let s=this.getTargetsForPath(t);if(s.length===0)return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:X(n,!1),raw_url:""};let{getItem:o}=await Promise.resolve().then(()=>(Yt(),Mi));for(let a of s)try{let c=await o(a.targetFullPath);if(c?.item)return{...c.item,raw_url:c.item.raw_url||c.rawUrl}}catch{}return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",type:X(n,!1),raw_url:""}}async mkdir(e,t){let i=this.getTargetsForPath(t);if(i.length===0)throw new Error(`[Alias] no target found for path ${t}`);let{makeDirectory:n}=await Promise.resolve().then(()=>(Yt(),Mi));await n(i[0].targetFullPath)}async rename(e,t,i){let n=this.getTargetsForPath(t);if(n.length===0)throw new Error(`[Alias] no target found for path ${t}`);let{renameItem:s}=await Promise.resolve().then(()=>(Yt(),Mi));await s(n[0].targetFullPath,i)}async remove(e,t,i){let n=this.getTargetsForPath(t);if(n.length===0)return;let{removeItems:s}=await Promise.resolve().then(()=>(Yt(),Mi));await s(n[0].targetFullPath,i)}async move(e,t,i,n,s){let o=this.getTargetsForPath(n),a=this.getTargetsForPath(s);if(o.length===0||a.length===0)throw new Error("[Alias] cannot resolve source or destination path");let{moveItems:c}=await Promise.resolve().then(()=>(Yt(),Mi));await c(o[0].targetFullPath,a[0].targetFullPath,i)}async copy(e,t,i,n,s){let o=this.getTargetsForPath(n),a=this.getTargetsForPath(s);if(o.length===0||a.length===0)throw new Error("[Alias] cannot resolve source or destination path");let{copyItems:c}=await Promise.resolve().then(()=>(Yt(),Mi));await c(o[0].targetFullPath,a[0].targetFullPath,i)}async put(e,t,i){let n=this.getTargetsForPath(t);if(n.length===0)throw new Error(`[Alias] no target found for upload path ${t}`);let{putItem:s}=await Promise.resolve().then(()=>(Yt(),Mi));await s(n[0].targetFullPath,i)}}});var km,zF=J(()=>{"use strict";km=class{addition;onTokenRefreshed;base="https://api.dropboxapi.com";contentBase="https://content.dropboxapi.com";constructor(e,t){this.addition=e,this.onTokenRefreshed=t}async refreshToken(){if(this.addition.use_online_api&&this.addition.api_url_address){let e=new URL(this.addition.api_url_address);e.searchParams.set("refresh_ui",this.addition.refresh_token),e.searchParams.set("server_use","true"),e.searchParams.set("driver_txt","dropboxs_go");let t=await fetch(e.toString());if(!t.ok)throw new Error(`Failed to refresh token: HTTP ${t.status}`);let i=await t.json();if(!i.access_token)throw new Error(i.text||"Empty token returned from renew API");this.addition.access_token=i.access_token,i.refresh_token&&(this.addition.refresh_token=i.refresh_token)}else{let e=`${this.base}/oauth2/token`,t=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.addition.refresh_token,client_id:this.addition.client_id||"",client_secret:this.addition.client_secret||""}),i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:t.toString()});if(!i.ok){let s=await i.text();throw new Error(`Failed to refresh token: HTTP ${i.status} - ${s}`)}let n=await i.json();this.addition.access_token=n.access_token,n.refresh_token&&(this.addition.refresh_token=n.refresh_token)}this.onTokenRefreshed&&await this.onTokenRefreshed({accessToken:this.addition.access_token||"",refreshToken:this.addition.refresh_token||""})}async request(e,t={},i=!0){this.addition.access_token||await this.refreshToken();let s=`${t.isContentApi?this.contentBase:this.base}${e}`,o={Authorization:`Bearer ${this.addition.access_token}`,...t.customHeaders};this.addition.root_namespace_id&&(o["Dropbox-API-Path-Root"]=JSON.stringify({".tag":"root",root:this.addition.root_namespace_id}));let a;t.body!==void 0&&(typeof t.body=="object"&&!(t.body instanceof Uint8Array)&&!(t.body instanceof ArrayBuffer)&&!(t.body instanceof ReadableStream)?(o["Content-Type"]="application/json",a=JSON.stringify(t.body)):a=t.body);let c=await fetch(s,{method:t.method||"POST",headers:o,body:a});if(!c.ok){let d=await c.text(),u=c.status===401||d.includes("expired_access_token")||d.includes("invalid_access_token");if(i&&u)return await this.refreshToken(),this.request(e,t,!1);throw new Error(`Dropbox API error (${c.status}): ${d}`)}return(c.headers.get("content-type")||"").includes("application/json")?await c.json():await c.text()}async init(){if(!this.addition.access_token&&this.addition.refresh_token&&await this.refreshToken(),!this.addition.root_namespace_id)try{let e=await this.getCurrentAccount();e.root_info?.root_namespace_id&&(this.addition.root_namespace_id=e.root_info.root_namespace_id)}catch(e){console.warn("[Dropbox] failed to get root namespace ID:",e)}}async getCurrentAccount(){return this.request("/2/users/get_current_account",{method:"POST",body:null})}async getSpaceUsage(){return this.request("/2/users/get_space_usage",{method:"POST",body:null})}async getFiles(e){let t=e==="/"||e===""?"":e,i=await this.request("/2/files/list_folder",{method:"POST",body:{path:t,recursive:!1,include_deleted:!1,include_has_explicit_shared_members:!1,include_mounted_folders:!0,include_non_downloadable_files:!1,limit:2e3}}),n=[...i.entries];for(;i.has_more;)i=await this.request("/2/files/list_folder/continue",{method:"POST",body:{cursor:i.cursor}}),n.push(...i.entries);return n}async getTemporaryLink(e){return(await this.request("/2/files/get_temporary_link",{method:"POST",body:{path:e}})).link}async createFolder(e){await this.request("/2/files/create_folder_v2",{method:"POST",body:{path:e,autorename:!1}})}async move(e,t){await this.request("/2/files/move_v2",{method:"POST",body:{from_path:e,to_path:t,autorename:!1,allow_ownership_transfer:!1,allow_shared_folder:!1}})}async copy(e,t){await this.request("/2/files/copy_v2",{method:"POST",body:{from_path:e,to_path:t,autorename:!1,allow_ownership_transfer:!1,allow_shared_folder:!1}})}async delete(e){await this.request("/2/files/delete_v2",{method:"POST",body:{path:e}})}}});var Sm,JF=J(()=>{"use strict";Be();He();zF();Sm=class{addition;client;constructor(e,t){this.addition=e,this.client=new km(e,t)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"":t}async list(e,t){let i=this.cleanPath(t),s=(await this.client.getFiles(i)).map(o=>{let a=o[".tag"]==="folder",c=o.path_display||o.path_lower||`/${o.name}`;return{name:o.name,size:o.size||0,is_dir:a,modified:o.server_modified||o.client_modified||new Date().toISOString(),sign:o.id||c,type:X(o.name,a),raw_url:""}});return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(!i)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"/",type:1,raw_url:""};try{let s=await this.client.getTemporaryLink(i);return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:i,type:X(n,!1),raw_url:s}}catch{return{name:n,size:0,is_dir:!0,modified:new Date().toISOString(),sign:i,type:1,raw_url:""}}}async link(e,t){let i=this.cleanPath(t);return{url:await this.client.getTemporaryLink(i)}}async mkdir(e,t){let i=this.cleanPath(t);await this.client.createFolder(i)}async rename(e,t,i){let n=this.cleanPath(t),s=n.substring(0,n.lastIndexOf("/")),o=s?`${s}/${i}`:`/${i}`;await this.client.move(n,o)}async remove(e,t,i){let n=this.cleanPath(t);for(let s of i){let o=n===""?`/${s}`:`${n}/${s}`;await this.client.delete(o)}}async move(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s);for(let c of i){let l=o===""?`/${c}`:`${o}/${c}`,d=a===""?`/${c}`:`${a}/${c}`;await this.client.move(l,d)}}async copy(e,t,i,n,s){let o=this.cleanPath(n),a=this.cleanPath(s);for(let c of i){let l=o===""?`/${c}`:`${o}/${c}`,d=a===""?`/${c}`:`${a}/${c}`;await this.client.copy(l,d)}}async put(e,t,i){let n=this.cleanPath(t);await this.client.request("/2/files/upload",{method:"POST",isContentApi:!0,customHeaders:{"Dropbox-API-Arg":JSON.stringify({path:n,mode:"overwrite",autorename:!1,mute:!1,strict_conflict:!1}),"Content-Type":"application/octet-stream"},body:i})}async getDetails(){try{let e=await this.client.getSpaceUsage();return{total_space:e.allocation?.allocated||0,used_space:e.used||0}}catch{return{}}}}});var qE,jF,Dm,$F=J(()=>{"use strict";qE="https://365.kdocs.cn",jF="https://drive.wps.cn",Dm=class{addition;loginState;constructor(e){this.addition=e}isPersonal(){return this.loginState?.is_company_account!==void 0?!this.loginState.is_company_account:(this.addition.mode||"Personal")==="Personal"}driveHost(){return this.isPersonal()?jF:qE}drivePrefix(){return this.isPersonal()?"":"/3rd/drive"}driveUrl(e){return`${this.driveHost()}${this.drivePrefix()}${e}`}getUA(){return this.addition.custom_ua||"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"}async request(e,t={}){let i=new URL(e);if(t.params)for(let[c,l]of Object.entries(t.params))i.searchParams.set(c,l);let n={Cookie:this.addition.cookie,Accept:"application/json","User-Agent":this.getUA(),Origin:this.driveHost(),...t.headers},s;t.body!==void 0&&(typeof t.body=="object"?(n["Content-Type"]="application/json",s=JSON.stringify(t.body)):s=t.body);let o=await fetch(i.toString(),{method:t.method||"GET",headers:n,body:s});if(!o.ok){let c=await o.text();throw new Error(`WPS API error (${o.status}): ${c}`)}if((o.headers.get("content-type")||"").includes("application/json")){let c=await o.json();if(c.result&&c.result!=="ok"&&c.result!=="success")throw new Error(`WPS API error: ${c.result} - ${c.msg||""}`);return c}return await o.text()}async init(){if(!this.addition.cookie)throw new Error("WPS cookie is required");let e=await this.request("https://account.kdocs.cn/api/v3/islogin",{method:"GET"});this.loginState=e}async getGroups(){if(this.isPersonal())return((await this.request(this.driveUrl("/api/v3/groups"))).groups||[]).map(s=>({group_id:s.id,id:s.id,name:s.name}));let e=this.loginState?.companyid||0,t=`${qE}/3rd/plus/groups/v1/companies/${e}/users/self/groups/private`;return(await this.request(t)).groups||[]}async getFiles(e,t){let i=[],n=0;for(let s=0;s<50;s++){let o=this.driveUrl(`/api/v5/groups/${e}/files`),a=await this.request(o,{params:{parentid:String(t),offset:String(n)}});if(a.files&&a.files.length>0&&i.push(...a.files),a.next_offset===-1||a.next_offset===void 0)break;n=a.next_offset}return i}async getDownloadUrl(e,t){let i=this.driveUrl(`/api/v5/groups/${e}/files/${t}/download?support_checksums=sha1`),n=await this.request(i);if(!n.url)throw new Error("Empty download url received from WPS");return n.url}async createFolder(e,t,i){await this.request(this.driveUrl("/api/v5/files/folder"),{method:"POST",body:{groupid:e,parentid:t,name:i}})}async rename(e,t,i){await this.request(this.driveUrl(`/api/v3/groups/${e}/files/${t}`),{method:"PUT",body:{fname:i}})}async move(e,t,i,n){await this.request(this.driveUrl(`/api/v3/groups/${e}/files/batch/move`),{method:"POST",body:{fileids:[t],target_groupid:i,target_parentid:n}})}async copy(e,t,i,n){await this.request(this.driveUrl(`/api/v3/groups/${e}/files/batch/copy`),{method:"POST",body:{fileids:[t],groupid:e,target_groupid:i,target_parentid:n,duplicated_name_model:1}})}async delete(e,t){await this.request(this.driveUrl(`/api/v3/groups/${e}/files/batch/delete`),{method:"POST",body:{fileids:[t]}})}async getStorageDetails(){if(this.isPersonal()){let s=`${jF}/api/v3/spaces`,o=await this.request(s);return{total:o.total,used:o.used}}let e=this.loginState?.companyid||0,t=`${qE}/3rd/plussvr/compose/v1/u/companies/batch/service-space?comp_ids=${e}`,n=((await this.request(t)).info||[]).find(s=>s.id===e);return n?{total:n.space_total,used:n.space_used}:{}}}});var Qm,VF=J(()=>{"use strict";Be();He();$F();Qm=class{addition;client;constructor(e){this.addition=e,this.client=new Dm(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolvePath(e){let t=this.cleanPath(e);if(t==="/")return{kind:"root",groupId:0,fileId:0,name:"root",isDir:!0,size:0,modified:new Date().toISOString()};let i=t.split("/").filter(Boolean),s=(await this.client.getGroups()).find(l=>l.name===i[0]);if(!s)return null;let o=s.group_id||s.id||0;if(i.length===1)return{kind:"group",groupId:o,fileId:0,name:s.name,isDir:!0,size:0,modified:new Date().toISOString()};let a=0,c=null;for(let l=1;l<i.length;l++){let d=i[l],f=(await this.client.getFiles(o,a)).find(g=>g.fname===d);if(!f)return null;let h=f.ftype==="folder";c={kind:h?"folder":"file",groupId:o,fileId:f.id,name:f.fname,isDir:h,size:f.fsize||0,modified:f.mtime?new Date(f.mtime*1e3).toISOString():new Date().toISOString()},a=f.id}return c}async list(e,t){let i=this.cleanPath(t),n=await this.resolvePath(i);if(!n||!n.isDir)return[];if(n.kind==="root"){let c=(await this.client.getGroups()).map(l=>({name:l.name,size:0,is_dir:!0,modified:new Date().toISOString(),sign:String(l.group_id||l.id),type:1,raw_url:""}));return de(c,this.addition.order_by,this.addition.order_direction)}let o=(await this.client.getFiles(n.groupId,n.fileId)).map(a=>{let c=a.ftype==="folder";return{name:a.fname,size:a.fsize||0,is_dir:c,modified:a.mtime?new Date(a.mtime*1e3).toISOString():new Date().toISOString(),sign:String(a.id),type:X(a.fname,c),raw_url:""}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=await this.resolvePath(i);if(!n)throw new Error(`Path not found: ${i}`);let s="";if(!n.isDir&&n.kind==="file")try{s=await this.client.getDownloadUrl(n.groupId,n.fileId)}catch(o){console.warn("[WPS] failed to get download url in get():",o)}return{name:n.name,size:n.size,is_dir:n.isDir,modified:n.modified,sign:String(n.fileId||n.groupId),type:X(n.name,n.isDir),raw_url:s}}async link(e,t){let i=this.cleanPath(t),n=await this.resolvePath(i);if(!n||n.isDir||n.kind!=="file")throw new Error(`Cannot get link for non-file: ${i}`);return{url:await this.client.getDownloadUrl(n.groupId,n.fileId),headers:{"User-Agent":this.client.getUA(),Referer:this.client.driveHost()}}}async mkdir(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=await this.resolvePath(n);if(!o||!o.isDir||o.kind==="root")throw new Error("Cannot create folder directly in root (groups are read-only)");await this.client.createFolder(o.groupId,o.fileId,s)}async move(e,t,i,n,s){let o=await this.resolvePath(this.cleanPath(s));if(!o||!o.isDir||o.kind==="root")throw new Error("Target destination directory not found");for(let a of i){let c=this.cleanPath(n)==="/"?`/${a}`:`${this.cleanPath(n)}/${a}`,l=await this.resolvePath(c);l&&await this.client.move(l.groupId,l.fileId,o.groupId,o.fileId)}}async rename(e,t,i){let n=await this.resolvePath(this.cleanPath(t));if(!n)throw new Error("Source node not found");await this.client.rename(n.groupId,n.fileId,i)}async copy(e,t,i,n,s){let o=await this.resolvePath(this.cleanPath(s));if(!o||!o.isDir||o.kind==="root")throw new Error("Target destination directory not found");for(let a of i){let c=this.cleanPath(n)==="/"?`/${a}`:`${this.cleanPath(n)}/${a}`,l=await this.resolvePath(c);l&&await this.client.copy(l.groupId,l.fileId,o.groupId,o.fileId)}}async remove(e,t,i){for(let n of i){let s=this.cleanPath(t)==="/"?`/${n}`:`${this.cleanPath(t)}/${n}`,o=await this.resolvePath(s);o&&o.kind!=="root"&&o.kind!=="group"&&await this.client.delete(o.groupId,o.fileId)}}async put(e,t,i){let n=this.cleanPath(t),s=n.substring(0,n.lastIndexOf("/"))||"/",o=n.substring(n.lastIndexOf("/")+1),a=await this.resolvePath(s);if(!a||!a.isDir)throw new Error(`Parent directory not found: ${s}`);console.warn(`[WPS] direct small upload for ${o}`)}async getDetails(){try{let e=await this.client.getStorageDetails();return{total_space:e.total,used_space:e.used}}catch{return{}}}}});function Y$(r){let e=encodeURIComponent(r);return e=e.replace(/\+/g,"%20"),e=e.replace(/!/g,"%21"),e=e.replace(/'/g,"%27"),e=e.replace(/\(/g,"%28"),e=e.replace(/\)/g,"%29"),e=e.replace(/\*/g,"%2A"),e}function OE(r){return kc.default.MD5(r).toString(kc.default.enc.Hex)}function W$(r,e,t){let n=Y$(r).split("").sort().join(""),s=kc.default.enc.Utf8.parse(n),o=kc.default.enc.Base64.stringify(s),a=OE(o)+OE(`${e}:${t}`);return OE(a).toUpperCase()}function X$(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let i=0;i<r;i++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function Z$(r){let e=t=>String(t).padStart(2,"0");return`${r.getFullYear()}-${e(r.getMonth()+1)}-${e(r.getDate())} ${e(r.getHours())}:${e(r.getMinutes())}:${e(r.getSeconds())}`}var kc,_m,YF=J(()=>{"use strict";kc=xr(Oo(),1);_m=class{addition;personalHost="https://yun.139.com";familyHost="https://yun.139.com";groupHost="https://yun.139.com";account="";constructor(e){this.addition=e,this.extractAccount()}extractAccount(){if(this.addition.authorization)try{let e=this.getAuthString(),i=kc.default.enc.Base64.parse(e).toString(kc.default.enc.Utf8).split(":");i.length>=2&&(this.account=i[1])}catch{}}getAuthString(){let e=(this.addition.authorization||"").trim();return e.startsWith("Basic ")&&(e=e.slice(6).trim()),e}isPersonalNew(){return!this.addition.type||this.addition.type==="personal_new"}isFamily(){return this.addition.type==="family"}isGroup(){return this.addition.type==="group"}getHost(){return this.isFamily()?this.familyHost:this.isGroup()?this.groupHost:this.personalHost}async request(e,t){let i=Z$(new Date),n=X$(16),s=JSON.stringify(t||{}),o=W$(s,i,n),a;e.startsWith("http://")||e.startsWith("https://")?a=e:e.startsWith("/orchestration/")?a=`https://yun.139.com${e}`:a=`${this.getHost()}${e}`;let c=this.isFamily()?"2":"1",l={Accept:"application/json, text/plain, */*","Content-Type":"application/json","CMS-DEVICE":"default",Authorization:`Basic ${this.getAuthString()}`,Caller:"web","Mcloud-Channel":"1000101","Mcloud-Client":"10701","Mcloud-Route":"001","mcloud-channel":"1000101","mcloud-client":"10701","mcloud-sign":`${i},${n},${o}`,"mcloud-version":"7.14.0",Origin:"https://yun.139.com",Referer:"https://yun.139.com/w/","x-DeviceInfo":"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||","x-huawei-channelSrc":"10000034","x-inner-ntwk":"2","x-m4c-caller":"PC","x-m4c-src":"10002","x-SvcType":c,"Inner-Hcy-Router-Https":"1","X-Yun-Api-Version":"v1","X-Yun-App-Channel":"10000034","X-Yun-Channel-Source":"10000034","X-Yun-Client-Info":"||9|7.14.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||","X-Yun-Module-Type":"100","X-Yun-Svc-Type":"1"},d=await fetch(a,{method:"POST",headers:l,body:s});if(!d.ok){let f=await d.text();throw new Error(`139 Cloud API error (${d.status}): ${f}`)}let u=await d.json();if(u.success===!1&&u.message)throw new Error(`139 Cloud API error: ${u.message}`);return u}async init(){if(!this.addition.authorization)throw new Error("139 Cloud Authorization is required");try{let e=await this.request("https://user-njs.yun.139.com/user/route/qryRoutePolicy",{userInfo:{userType:1,accountType:1,accountName:this.account},modAddrType:1});if(e.data?.routePolicyList)for(let t of e.data.routePolicyList)t.modName==="personal"&&t.httpsUrl?this.personalHost=t.httpsUrl:t.modName==="group"&&t.httpsUrl?this.groupHost=t.httpsUrl:t.modName==="family"&&t.httpsUrl&&(this.familyHost=t.httpsUrl)}catch(e){console.warn("[139] queryRoutePolicy warning, fallback to default host:",e)}}async listFiles(e=""){if(this.isPersonalNew()){let t="",i=[],n=e||this.addition.root_folder_id||"/";do{let a=await this.request("/file/list",{parentFileId:n,pageInfo:{pageCursor:t,pageSize:100},orderBy:"updated_at",orderDirection:"DESC",imageThumbnailStyleList:["Small","Large"]}),c=a.data?.items||[];i.push(...c),t=a.data?.nextPageCursor||""}while(t);let s=i.filter(a=>a.type==="folder").map(a=>({catalogID:a.fileId,catalogName:a.name,updateTime:a.updatedAt}));return{files:i.filter(a=>a.type!=="folder").map(a=>({contentID:a.fileId,contentName:a.name,contentSize:a.size,updateTime:a.updatedAt,createTime:a.createdAt,thumbnailURL:a.thumbnailUrls?.[0]?.url})),folders:s}}return this.getDisk(e)}async getDisk(e=""){let i=(await this.request("/orchestration/personalCloud/catalog/v1.0/getDisk",{catalogID:e||"",sortDirection:1,filterType:0,catalogSortType:0,contentSortType:0,startNumber:1,endNumber:5e3,commonAccountInfo:{account:this.account,accountType:1}})).data?.getDiskResult;return{files:i?.fileList||[],folders:i?.catalogList||[]}}async getDownloadUrl(e){if(this.isPersonalNew()){let n=await this.request("/file/getDownloadUrl",{fileId:e}),s=(n.data?.cdnSwitch?n.data?.cdnUrl:null)||n.data?.url||n.data?.cdnUrl;if(!s)throw new Error("Empty download URL received from 139 Cloud");return s}let t=await this.request("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",{contentID:e,commonAccountInfo:{account:this.account,accountType:1}}),i=t.data?.downloadURL||t.data?.url;if(!i)throw new Error("Empty download URL received from 139 Cloud");return i}async createCatalog(e,t){return this.isPersonalNew()?(await this.request("/file/create",{parentFileId:e||this.addition.root_folder_id||"/",name:t,description:"",type:"folder",fileRenameMode:"force_rename"})).data?.fileId||"":(await this.request("/orchestration/personalCloud/catalog/v1.0/createCatalog",{parentCatalogID:e||"",catalogName:t,commonAccountInfo:{account:this.account,accountType:1}})).data?.catalogID||""}async deleteFile(e){if(this.isPersonalNew()){await this.request("/file/delete",{fileIds:[e]});return}await this.request("/orchestration/personalCloud/catalog/v1.0/deleteContent",{contentID:e,commonAccountInfo:{account:this.account,accountType:1}})}async deleteCatalog(e){if(this.isPersonalNew()){await this.request("/file/delete",{fileIds:[e]});return}await this.request("/orchestration/personalCloud/catalog/v1.0/deleteCatalog",{catalogID:e,commonAccountInfo:{account:this.account,accountType:1}})}async rename(e,t){if(this.isPersonalNew()){await this.request("/file/update",{fileId:e,name:t,description:""});return}await this.request("/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo",{catalogID:e,catalogName:t,commonAccountInfo:{account:this.account,accountType:1}})}async getStorageDetails(){try{let e=await this.request("/orchestration/personalCloud/catalog/v1.0/getUserDomainInfo",{commonAccountInfo:{account:this.account,accountType:1}});return{total:e.data?.totalSize,used:e.data?.usedSize}}catch{return{}}}}});var Fm,WF=J(()=>{"use strict";Be();He();YF();Fm=class{addition;client;constructor(e){this.addition=e,this.client=new _m(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}getRootId(){return this.addition.root_folder_id?this.addition.root_folder_id:this.client.isPersonalNew()?"/":""}async resolveCatalogId(e){let t=this.cleanPath(e);if(t==="/")return this.getRootId();let i=t.split("/").filter(Boolean),n=this.getRootId();for(let s of i){let a=(await this.client.listFiles(n)).folders.find(c=>c.catalogName===s);if(a)n=a.catalogID;else break}return n}async list(e,t){let i=this.cleanPath(t),n=await this.resolveCatalogId(i),s=await this.client.listFiles(n),o=s.folders.map(l=>({name:l.catalogName,size:0,is_dir:!0,modified:l.updateTime||new Date().toISOString(),sign:l.catalogID,type:1,raw_url:""})),a=s.files.map(l=>{let d=typeof l.contentSize=="number"?l.contentSize:parseInt(String(l.contentSize||"0"),10),u=l.contentName||"file";return{name:u,size:isNaN(d)?0:d,is_dir:!1,modified:l.updateTime||l.createTime||new Date().toISOString(),sign:l.contentID||u,type:X(u,!1),thumb:l.thumbnailURL||l.bigThumbnailURL,raw_url:""}}),c=[...o,...a];return de(c,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.getRootId(),type:1,raw_url:""};let s=i.substring(0,i.lastIndexOf("/"))||"/",o=await this.resolveCatalogId(s),a=await this.client.listFiles(o),c=a.folders.find(d=>d.catalogName===n);if(c)return{name:c.catalogName,size:0,is_dir:!0,modified:c.updateTime||new Date().toISOString(),sign:c.catalogID,type:1,raw_url:""};let l=a.files.find(d=>d.contentName===n);if(l){let d=typeof l.contentSize=="number"?l.contentSize:parseInt(String(l.contentSize||"0"),10),u="";if(l.contentID)try{u=await this.client.getDownloadUrl(l.contentID)}catch(f){console.warn("[139] failed to get download url in get():",f)}return{name:l.contentName||n,size:isNaN(d)?0:d,is_dir:!1,modified:l.updateTime||l.createTime||new Date().toISOString(),sign:l.contentID||n,type:X(n,!1),thumb:l.thumbnailURL||l.bigThumbnailURL,raw_url:u}}throw new Error(`Item not found: ${i}`)}async link(e,t){let i=await this.get(e,t);if(i.is_dir)throw new Error(`Cannot get link for folder: ${t}`);return{url:await this.client.getDownloadUrl(i.sign),headers:{Referer:"https://yun.139.com/","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}}}async mkdir(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=await this.resolveCatalogId(n);await this.client.createCatalog(o,s)}async rename(e,t,i){let n=await this.get(e,t);await this.client.rename(n.sign,i)}async remove(e,t,i){let n=this.cleanPath(t),s=await this.resolveCatalogId(n),o=await this.client.listFiles(s);for(let a of i){let c=o.folders.find(l=>l.catalogName===a);if(c)await this.client.deleteCatalog(c.catalogID);else{let l=o.files.find(d=>d.contentName===a);l&&l.contentID&&await this.client.deleteFile(l.contentID)}}}async move(e,t,i,n,s){console.warn(`[139] move from ${n} to ${s}`)}async copy(e,t,i,n,s){console.warn(`[139] copy from ${n} to ${s}`)}async put(e,t,i){console.warn(`[139] put for ${t}`)}async getDetails(){try{let e=await this.client.getStorageDetails();return{total_space:e.total,used_space:e.used}}catch{return{}}}}});function eV(r){let e=r.replace(/-/g,"+").replace(/_/g,"/");for(;e.length%4!==0;)e+="=";return Sc.default.enc.Base64.parse(e)}function tV(r){return Sc.default.enc.Base64.stringify(r).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function rV(r){return Sc.default.enc.Utf8.parse(r)}var Sc,Pm,XF=J(()=>{"use strict";Sc=xr(Oo(),1);Pm=class{addition;sid="";seq=Math.floor(Math.random()*268435456);masterKey=null;nodes=new Map;rootId="";constructor(e){this.addition=e}nextSeq(){return this.seq=(this.seq+1)%268435456,this.seq}async request(e){let i=`https://g.api.mega.co.nz/cs?id=${this.nextSeq()}`;this.sid&&(i+=`&sid=${this.sid}`);let n=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok)throw new Error(`Mega API HTTP error: ${n.status}`);let s=await n.json();if(typeof s=="number"&&s<0)throw new Error(`Mega API error code: ${s}`);return s}async init(){if(!this.addition.email||!this.addition.password)throw new Error("Mega email and password are required");let e=Sc.default.enc.Base64.stringify(Sc.default.enc.Utf8.parse(this.addition.email.toLowerCase())).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),i=(await this.request([{a:"us",user:this.addition.email.toLowerCase(),uh:e}]))[0];if(typeof i=="number"&&i<0)throw new Error(`Mega authentication failed: ${i}`);this.sid=i.sid||"",await this.fetchNodes()}async fetchNodes(){if(!this.sid)return;let t=(await this.request([{a:"f",c:1}]))[0];if(t?.f&&Array.isArray(t.f)){this.nodes.clear();for(let i of t.f){let n=i.t===1||i.t===2||i.t===3||i.t===4;i.t===2&&(this.rootId=i.h);let s="unnamed";if(i.a)try{let a=this.decryptAttributes(i.a,i.k);a?.n&&(s=a.n)}catch{s=i.h}let o={id:i.h,parent_id:i.p,name:s,size:i.s||0,is_dir:n,modified:i.ts?new Date(i.ts*1e3).toISOString():new Date().toISOString(),type:i.t,key:i.k};this.nodes.set(i.h,o)}}}decryptAttributes(e,t){try{let i=eV(e),n=Sc.default.enc.Utf8.stringify(i);if(n.startsWith("MEGA{")){let s=n.substring(4);return JSON.parse(s)}}catch{}return null}getChildren(e){let t=e||this.rootId,i=[];for(let n of this.nodes.values())n.parent_id===t&&i.push(n);return i}getNode(e){return this.nodes.get(e)}getRootId(){return this.rootId}async getDownloadLink(e){let i=(await this.request([{a:"g",g:1,n:e}]))[0];if(i?.g)return i.g;throw new Error(`Failed to get download URL for node ${e}`)}async createFolder(e,t){let i=t||this.rootId,n=JSON.stringify({n:e}),s=tV(rV(`MEGA${n}`)),o=await this.request([{a:"p",t:i,n:[{h:"xxxxxxxx",t:1,a:s,k:"dummy_key"}]}]);return await this.fetchNodes(),o[0]?.f?.[0]?.h||""}async deleteNode(e){await this.request([{a:"d",n:e}]),this.nodes.delete(e)}async moveNode(e,t){await this.request([{a:"m",n:e,t}]);let i=this.nodes.get(e);i&&(i.parent_id=t)}async getQuota(){let t=(await this.request([{a:"uq",strg:1}]))[0];return{total:t?.mstrg||0,used:t?.cstrg||0}}}});var Tm,ZF=J(()=>{"use strict";Be();He();XF();Tm=class{addition;client;constructor(e){this.addition=e,this.client=new Pm(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}resolveNodeByPath(e){let t=this.cleanPath(e);if(t==="/")return{id:this.client.getRootId(),name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),type:2};let i=t.split("/").filter(Boolean),n=this.client.getRootId(),s=null;for(let o of i){let c=this.client.getChildren(n).find(l=>l.name===o);if(!c)return null;s=c,n=c.id}return s}async list(e,t){let i=this.cleanPath(t),n=this.resolveNodeByPath(i);if(!n||!n.is_dir)return[];let o=this.client.getChildren(n.id).map(a=>({name:a.name,size:a.size||0,is_dir:a.is_dir,modified:a.modified||new Date().toISOString(),sign:a.id,type:X(a.name,a.is_dir),raw_url:""}));return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=this.resolveNodeByPath(i);if(!n)throw new Error(`Node not found: ${i}`);let s="";if(!n.is_dir)try{s=await this.client.getDownloadLink(n.id)}catch(o){console.warn("[Mega] failed to get download link in get():",o)}return{name:n.name,size:n.size,is_dir:n.is_dir,modified:n.modified,sign:n.id,type:X(n.name,n.is_dir),raw_url:s}}async link(e,t){let i=this.cleanPath(t),n=this.resolveNodeByPath(i);if(!n||n.is_dir)throw new Error(`Cannot get link for non-file: ${i}`);return{url:await this.client.getDownloadLink(n.id)}}async mkdir(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=this.resolveNodeByPath(n);if(!o||!o.is_dir)throw new Error(`Parent folder not found: ${n}`);await this.client.createFolder(s,o.id)}async rename(e,t,i){if(!this.resolveNodeByPath(this.cleanPath(t)))throw new Error("Node not found");console.warn(`[Mega] rename ${t} to ${i}`)}async move(e,t,i,n,s){let o=this.resolveNodeByPath(this.cleanPath(s));if(!o||!o.is_dir)throw new Error("Destination folder not found");for(let a of i){let c=this.cleanPath(n)==="/"?`/${a}`:`${this.cleanPath(n)}/${a}`,l=this.resolveNodeByPath(c);l&&await this.client.moveNode(l.id,o.id)}}async copy(e,t,i,n,s){console.warn(`[Mega] copy not supported natively from ${n} to ${s}`)}async remove(e,t,i){for(let n of i){let s=this.cleanPath(t)==="/"?`/${n}`:`${this.cleanPath(t)}/${n}`,o=this.resolveNodeByPath(s);o&&await this.client.deleteNode(o.id)}}async put(e,t,i){console.warn(`[Mega] put for ${t}`)}async getDetails(){try{let e=await this.client.getQuota();return{total_space:e.total,used_space:e.used}}catch{return{}}}}});var Um,eP=J(()=>{"use strict";Um=class{addition;constructor(e){this.addition=e}async init(){if(!this.addition.share_code||!this.addition.receive_code)throw new Error("115 Share: share_code and receive_code are required")}async getFiles(e="0"){let t=this.addition.page_size||1e3,i=[],n=0,s=0;do{let o=new URL("https://webapi.115.com/share/snap");o.searchParams.set("share_code",this.addition.share_code),o.searchParams.set("receive_code",this.addition.receive_code),o.searchParams.set("cid",e||"0"),o.searchParams.set("limit",String(t)),o.searchParams.set("offset",String(n));let a={"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",Referer:"https://115.com/"};this.addition.cookie&&(a.Cookie=this.addition.cookie);let c=await fetch(o.toString(),{headers:a});if(!c.ok)throw new Error(`115 Share API error: HTTP ${c.status}`);let l=await c.json();if(!l.state)throw new Error(`115 Share API error: ${l.msg||"failed"}`);let d=l.data?.list||[];if(i.push(...d),s=l.data?.count||0,n+=d.length,d.length===0)break}while(n<s);return i}async getDownloadUrl(e){let t=new URL("https://proapi.115.com/app/share/downurl"),i=new URLSearchParams({share_code:this.addition.share_code,receive_code:this.addition.receive_code,file_id:e}),n={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36","Content-Type":"application/x-www-form-urlencoded"};this.addition.cookie&&(n.Cookie=this.addition.cookie);let s=await fetch(t.toString(),{method:"POST",headers:n,body:i.toString()});if(!s.ok)throw new Error(`115 Share download error: HTTP ${s.status}`);let o=await s.json(),a=o.data?.url?.url||o.data?.url;if(!a)throw new Error(o.msg||"Empty download URL received from 115 Share");return a}}});var Rm,tP=J(()=>{"use strict";Be();He();eP();Rm=class{addition;client;constructor(e){this.addition=e,this.client=new Um(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolveCid(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id||"0";let i=t.split("/").filter(Boolean),n=this.addition.root_folder_id||"0";for(let s of i){let a=(await this.client.getFiles(n)).find(c=>c.is_file===0&&c.file_name===s);if(a&&a.category_id)n=a.category_id;else break}return n}async list(e,t){let i=this.cleanPath(t),n=await this.resolveCid(i),o=(await this.client.getFiles(n)).map(a=>{let c=a.is_file===0,l=typeof a.file_size=="number"?a.file_size:parseInt(String(a.file_size||"0"),10),d=typeof a.user_utime=="number"?a.user_utime:parseInt(String(a.user_utime||"0"),10),u=a.file_name||"file";return{name:u,size:c||isNaN(l)?0:l,is_dir:c,modified:d>0?new Date(d*1e3).toISOString():new Date().toISOString(),sign:c?a.category_id||"":a.file_id||"",type:X(u,c),thumb:a.thumb_url,raw_url:""}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"0",type:1,raw_url:""};let s=i.substring(0,i.lastIndexOf("/"))||"/",o=await this.resolveCid(s),c=(await this.client.getFiles(o)).find(h=>h.file_name===n);if(!c)throw new Error(`File not found: ${i}`);let l=c.is_file===0,d=typeof c.file_size=="number"?c.file_size:parseInt(String(c.file_size||"0"),10),u=typeof c.user_utime=="number"?c.user_utime:parseInt(String(c.user_utime||"0"),10),f="";if(!l&&c.file_id)try{f=await this.client.getDownloadUrl(c.file_id)}catch(h){console.warn("[115Share] failed to get download url:",h)}return{name:c.file_name||n,size:l||isNaN(d)?0:d,is_dir:l,modified:u>0?new Date(u*1e3).toISOString():new Date().toISOString(),sign:l?c.category_id||"":c.file_id||"",type:X(c.file_name||n,l),thumb:c.thumb_url,raw_url:f}}async link(e,t){let i=await this.get(e,t);if(i.is_dir)throw new Error(`Cannot get link for folder: ${t}`);return{url:await this.client.getDownloadUrl(i.sign),headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",Referer:"https://115.com/"}}}async mkdir(e,t){throw new Error("115 Share is read-only")}async rename(e,t,i){throw new Error("115 Share is read-only")}async remove(e,t,i){throw new Error("115 Share is read-only")}async move(e,t,i,n,s){throw new Error("115 Share is read-only")}async copy(e,t,i,n,s){throw new Error("115 Share is read-only")}async put(e,t,i){throw new Error("115 Share is read-only")}}});function rP(r){let e=typeof r=="string"?new TextEncoder().encode(r):r,t=-1;for(let i=0;i<e.length;i++)t=t>>>8^iV[(t^e[i])&255];return(t^-1)>>>0}function nV(r,e="web",t="3"){let i=["a","d","e","f","g","h","l","m","y","i","j","n","o","p","k","q","r","s","t","u","b","c","v","w","s","z"],n=String(Math.round(1e7*Math.random())),s=new Date,o=String(Math.floor(s.getTime()/1e3)),a=h=>String(h).padStart(2,"0"),c=`${s.getFullYear()}${a(s.getMonth()+1)}${a(s.getDate())}${a(s.getHours())}${a(s.getMinutes())}`,l=[];for(let h=0;h<c.length;h++){let g=c.charCodeAt(h)-48;l.push(i[g]||"a")}let d=String(rP(l.join(""))),u=[o,n,r,e,t,d].join("|"),f=String(rP(u));return[d,[o,n,f].join("-")]}function sV(r){let e=new URL(r),[t,i]=nV(e.pathname,"web","3");return e.searchParams.set(t,i),e.toString()}var iV,Nm,iP=J(()=>{"use strict";iV=(()=>{let r=new Uint32Array(256);for(let e=0;e<256;e++){let t=e;for(let i=0;i<8;i++)t=t&1?3988292384^t>>>1:t>>>1;r[e]=t}return r})();Nm=class{addition;mainApi="https://yun.123pan.com/b/api";constructor(e){this.addition=e}async init(){if(!this.addition.sharekey)throw new Error("123Pan Share: sharekey is required")}async request(e,t={}){let i=new URL(e);if(t.params)for(let[l,d]of Object.entries(t.params))i.searchParams.set(l,d);let n=sV(i.toString()),s={Origin:"https://yun.123pan.com",Referer:"https://yun.123pan.com/","User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) openlist-client",Platform:"web","App-Version":"3"};this.addition.accesstoken&&(s.Authorization=`Bearer ${this.addition.accesstoken}`);let o;t.body&&(s["Content-Type"]="application/json",o=JSON.stringify(t.body));let a=await fetch(n,{method:t.method||"GET",headers:s,body:o});if(!a.ok)throw new Error(`123Pan Share API error: HTTP ${a.status}`);let c=await a.json();if(c.code!==0&&c.code!==200)throw new Error(c.message||`123Pan Share API error (${c.code})`);return c}async getFiles(e="0"){let t=[],i=1;for(;;){let n=await this.request(`${this.mainApi}/share/get`,{method:"GET",params:{limit:"100",next:"0",orderBy:"file_id",orderDirection:"desc",parentFileId:e||"0",Page:String(i),shareKey:this.addition.sharekey,SharePwd:this.addition.sharepassword||""}}),s=n.data?.InfoList||[];if(t.push(...s),i++,s.length===0||n.data?.Next==="-1"||n.data?.Next===-1)break}return t}async getDownloadUrl(e){let i=(await this.request(`${this.mainApi}/share/download/info`,{method:"POST",body:{shareKey:this.addition.sharekey,SharePwd:this.addition.sharepassword||"",etag:e.Etag||"",fileId:e.FileId,s3keyFlag:e.S3KeyFlag||"",size:e.Size}})).data?.DownloadURL||"";if(!i)throw new Error("Failed to obtain download URL from 123Pan Share");try{let s=new URL(i).searchParams.get("params");s&&(i=atob(s))}catch{}return i}}});var qm,nP=J(()=>{"use strict";Be();He();iP();qm=class{addition;client;constructor(e){this.addition=e,this.client=new Nm(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolveParentId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id||"0";let i=t.split("/").filter(Boolean),n=this.addition.root_folder_id||"0";for(let s of i){let a=(await this.client.getFiles(n)).find(c=>c.Type===1&&c.FileName===s);if(a)n=String(a.FileId);else break}return n}async list(e,t){let i=this.cleanPath(t),n=await this.resolveParentId(i),o=(await this.client.getFiles(n)).map(a=>{let c=a.Type===1;return{name:a.FileName,size:c?0:a.Size||0,is_dir:c,modified:a.UpdateAt||a.CreateAt||new Date().toISOString(),sign:String(a.FileId),type:X(a.FileName,c),raw_url:""}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"0",type:1,raw_url:""};let s=i.substring(0,i.lastIndexOf("/"))||"/",o=await this.resolveParentId(s),c=(await this.client.getFiles(o)).find(u=>u.FileName===n);if(!c)throw new Error(`File not found: ${i}`);let l=c.Type===1,d="";if(!l)try{d=await this.client.getDownloadUrl(c)}catch(u){console.warn("[123Share] failed to get download url in get():",u)}return{name:c.FileName,size:l?0:c.Size||0,is_dir:l,modified:c.UpdateAt||c.CreateAt||new Date().toISOString(),sign:String(c.FileId),type:X(c.FileName,l),raw_url:d}}async link(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=await this.resolveParentId(n),c=(await this.client.getFiles(o)).find(d=>d.FileName===s);if(!c||c.Type===1)throw new Error(`Cannot get link for: ${t}`);return{url:await this.client.getDownloadUrl(c),headers:{Referer:"https://yun.123pan.com/"}}}async mkdir(e,t){throw new Error("123Pan Share is read-only")}async rename(e,t,i){throw new Error("123Pan Share is read-only")}async remove(e,t,i){throw new Error("123Pan Share is read-only")}async move(e,t,i,n,s){throw new Error("123Pan Share is read-only")}async copy(e,t,i,n,s){throw new Error("123Pan Share is read-only")}async put(e,t,i){throw new Error("123Pan Share is read-only")}}});var Om,sP=J(()=>{"use strict";Om=class{addition;shareToken="";driveId="";constructor(e){this.addition=e}async getShareToken(){if(!this.addition.share_id)throw new Error("Aliyundrive Share: share_id is required");let e={share_id:this.addition.share_id};this.addition.share_pwd&&(e.share_pwd=this.addition.share_pwd);let t=await fetch("https://api.alipan.com/v2/share_link/get_share_token",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!t.ok)throw new Error(`Aliyundrive Share token error: HTTP ${t.status}`);let i=await t.json();if(i.code)throw new Error(i.message||`Aliyundrive Share error: ${i.code}`);return this.shareToken=i.share_token||"",this.shareToken}async init(){await this.getShareToken()}async getFiles(e="root"){this.shareToken||await this.getShareToken();let t=[],i="";do{let n={image_thumbnail_process:"image/resize,w_160/format,jpeg",image_url_process:"image/resize,w_1920/format,jpeg",limit:200,order_by:this.addition.order_by||"name",order_direction:this.addition.order_direction||"ASC",parent_file_id:e||"root",share_id:this.addition.share_id,video_thumbnail_process:"video/snapshot,t_1000,f_jpg,ar_auto,w_300",marker:i||void 0},s=await fetch("https://api.alipan.com/adrive/v3/file/list",{method:"POST",headers:{"Content-Type":"application/json","x-share-token":this.shareToken,"X-Canary":"client=web,app=share,version=v2.3.1"},body:JSON.stringify(n)});if(!s.ok)throw new Error(`Aliyundrive Share list error: HTTP ${s.status}`);let o=await s.json();if(o.code==="ShareLinkTokenInvalid"||o.code==="AccessTokenInvalid")return await this.getShareToken(),this.getFiles(e);if(o.code)throw new Error(o.message||`Aliyundrive list error (${o.code})`);let a=o.items||[];t.push(...a),i=o.next_marker||"",a.length>0&&!this.driveId&&a[0].drive_id&&(this.driveId=a[0].drive_id)}while(i);return t}async getDownloadUrl(e){this.shareToken||await this.getShareToken();let t={share_id:this.addition.share_id,file_id:e,drive_id:this.driveId||void 0,expire_sec:14400},i=await fetch("https://api.alipan.com/v2/file/get_share_link_download_url",{method:"POST",headers:{"Content-Type":"application/json","x-share-token":this.shareToken,"X-Canary":"client=web,app=share,version=v2.3.1"},body:JSON.stringify(t)});if(!i.ok)throw new Error(`Aliyundrive download URL error: HTTP ${i.status}`);let n=await i.json();if(n.code==="ShareLinkTokenInvalid")return await this.getShareToken(),this.getDownloadUrl(e);let s=n.download_url||n.url;if(!s)throw new Error("Failed to get download URL from Aliyundrive Share");return s}}});var Mm,oP=J(()=>{"use strict";Be();He();sP();Mm=class{addition;client;constructor(e){this.addition=e,this.client=new Om(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolveFileId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id||"root";let i=t.split("/").filter(Boolean),n=this.addition.root_folder_id||"root";for(let s of i){let a=(await this.client.getFiles(n)).find(c=>c.type==="folder"&&c.name===s);if(a)n=a.file_id;else break}return n}async list(e,t){let i=this.cleanPath(t),n=await this.resolveFileId(i),o=(await this.client.getFiles(n)).map(a=>{let c=a.type==="folder";return{name:a.name,size:c?0:a.size||0,is_dir:c,modified:a.updated_at||a.created_at||new Date().toISOString(),sign:a.file_id,type:X(a.name,c),thumb:a.thumbnail,raw_url:""}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"root",type:1,raw_url:""};let s=i.substring(0,i.lastIndexOf("/"))||"/",o=await this.resolveFileId(s),c=(await this.client.getFiles(o)).find(u=>u.name===n);if(!c)throw new Error(`File not found: ${i}`);let l=c.type==="folder",d="";if(!l)try{d=await this.client.getDownloadUrl(c.file_id)}catch(u){console.warn("[AliyundriveShare] failed to get download url in get():",u)}return{name:c.name,size:l?0:c.size||0,is_dir:l,modified:c.updated_at||c.created_at||new Date().toISOString(),sign:c.file_id,type:X(c.name,l),thumb:c.thumbnail,raw_url:d}}async link(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=await this.resolveFileId(n),c=(await this.client.getFiles(o)).find(d=>d.name===s);if(!c||c.type==="folder")throw new Error(`Cannot get link for: ${t}`);return{url:await this.client.getDownloadUrl(c.file_id),headers:{Referer:"https://www.aliyundrive.com/","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}}}async mkdir(e,t){throw new Error("Aliyundrive Share is read-only")}async rename(e,t,i){throw new Error("Aliyundrive Share is read-only")}async remove(e,t,i){throw new Error("Aliyundrive Share is read-only")}async move(e,t,i,n,s){throw new Error("Aliyundrive Share is read-only")}async copy(e,t,i,n,s){throw new Error("Aliyundrive Share is read-only")}async put(e,t,i){throw new Error("Aliyundrive Share is read-only")}}});var Lm,aP=J(()=>{"use strict";Lm=class{addition;finalUrl="";cookie="";constructor(e){this.addition=e}async init(){if(!this.addition.url)throw new Error("OneDrive Sharelink URL is required");try{let e=await fetch(this.addition.url,{method:"HEAD",redirect:"follow"});this.finalUrl=e.url||this.addition.url}catch{this.finalUrl=this.addition.url}}async getFiles(e="/"){this.finalUrl||await this.init();let t=new URL(this.finalUrl);if(t.pathname.includes("/:u:/")||t.pathname.includes("/:v:/")||t.pathname.includes("/:b:/"))return[{id:"root_file",name:decodeURIComponent(t.pathname.split("/").pop()||"file"),size:0,is_folder:!1,modified:new Date().toISOString(),download_url:this.getDirectDownloadLink(this.finalUrl)}];try{let i=`${t.origin}${t.pathname}/_api/web/GetListUsingPath(DecodedUrl=@a1)/renderListDataAsStream`,n=await fetch(i,{method:"POST",headers:{Accept:"application/json;odata=nometadata","Content-Type":"application/json;odata=verbose"},body:JSON.stringify({parameters:{RenderOptions:5707,AllowMultipleValueFilterForTaxonomyFields:!0,AddRequiredFields:!0}})});if(n.ok)return((await n.json()).ListData?.Row||[]).map(a=>{let c=String(a.FSObjType)==="1",l=parseInt(a.File_x0020_Size||"0",10);return{id:a.UniqueId,name:a.FileLeafRef,size:c||isNaN(l)?0:l,is_folder:c,modified:a["Modified."]||new Date().toISOString(),download_url:a["@content.downloadUrl"],sp_item_url:a[".spItemUrl"]}})}catch{}return[]}getDirectDownloadLink(e){let t=new URL(e);return t.searchParams.set("download","1"),t.toString()}}});var Hm,cP=J(()=>{"use strict";Be();He();aP();Hm=class{addition;client;constructor(e){this.addition=e,this.client=new Lm(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async list(e,t){let i=this.cleanPath(t),s=(await this.client.getFiles(i)).map(o=>({name:o.name,size:o.size||0,is_dir:o.is_folder,modified:o.modified||new Date().toISOString(),sign:o.id||o.name,type:X(o.name,o.is_folder),raw_url:o.download_url||""}));return de(s,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"root",type:1,raw_url:""};let o=(await this.client.getFiles("/")).find(a=>a.name===n);if(!o)throw new Error(`File not found: ${i}`);return{name:o.name,size:o.size||0,is_dir:o.is_folder,modified:o.modified||new Date().toISOString(),sign:o.id||o.name,type:X(o.name,o.is_folder),raw_url:o.download_url||this.client.getDirectDownloadLink(this.addition.url)}}async link(e,t){let i=await this.get(e,t);if(i.is_dir)throw new Error(`Cannot get link for folder: ${t}`);return{url:i.raw_url||this.client.getDirectDownloadLink(this.addition.url)}}async mkdir(e,t){throw new Error("OneDrive Sharelink is read-only")}async rename(e,t,i){throw new Error("OneDrive Sharelink is read-only")}async remove(e,t,i){throw new Error("OneDrive Sharelink is read-only")}async move(e,t,i,n,s){throw new Error("OneDrive Sharelink is read-only")}async copy(e,t,i,n,s){throw new Error("OneDrive Sharelink is read-only")}async put(e,t,i){throw new Error("OneDrive Sharelink is read-only")}}});var Km,lP=J(()=>{"use strict";Km=class{addition;passCodeToken="";deviceId="";constructor(e){this.addition=e,this.deviceId=e.device_id||this.generateDeviceId()}generateDeviceId(){return"web_"+Math.random().toString(36).substring(2,15)}async init(){if(!this.addition.share_id)throw new Error("PikPak Share: share_id is required")}async getFiles(e=""){let t=[],i="";do{let n=new URL("https://api-drive.mypikpak.com/drive/v1/share");n.searchParams.set("share_id",this.addition.share_id),this.addition.share_pwd&&n.searchParams.set("pass_code",this.addition.share_pwd),e&&n.searchParams.set("parent_id",e),i&&n.searchParams.set("page_token",i),n.searchParams.set("thumbnail_size","SIZE_LARGE");let s={"X-Client-ID":"YUMx5nI8ZU8Ap8pm","X-Device-ID":this.deviceId,"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"};this.passCodeToken&&(s["X-Share-Token"]=this.passCodeToken);let o=await fetch(n.toString(),{headers:s});if(!o.ok)throw new Error(`PikPak Share error: HTTP ${o.status}`);let a=await o.json();a.pass_code_token&&(this.passCodeToken=a.pass_code_token);let c=a.files||[];if(t.push(...c),i=a.next_page_token||"",c.length===0)break}while(i);return t}async getDownloadUrl(e){if(e.web_content_link)return e.web_content_link;if(e.medias&&e.medias.length>0&&e.medias[0].link?.url)return e.medias[0].link.url;throw new Error("Download URL not found in PikPak share item")}}});var Gm,dP=J(()=>{"use strict";Be();He();lP();Gm=class{addition;client;constructor(e){this.addition=e,this.client=new Km(e)}async init(){await this.client.init()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async resolveParentId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id||"";let i=t.split("/").filter(Boolean),n=this.addition.root_folder_id||"";for(let s of i){let a=(await this.client.getFiles(n)).find(c=>c.kind==="drive#folder"&&c.name===s);if(a)n=a.id;else break}return n}async list(e,t){let i=this.cleanPath(t),n=await this.resolveParentId(i),o=(await this.client.getFiles(n)).map(a=>{let c=a.kind==="drive#folder",l=typeof a.size=="number"?a.size:parseInt(String(a.size||"0"),10);return{name:a.name,size:c||isNaN(l)?0:l,is_dir:c,modified:a.modified_time||new Date().toISOString(),sign:a.id,type:X(a.name,c),thumb:a.thumbnail_link,raw_url:""}});return de(o,this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"root",type:1,raw_url:""};let s=i.substring(0,i.lastIndexOf("/"))||"/",o=await this.resolveParentId(s),c=(await this.client.getFiles(o)).find(f=>f.name===n);if(!c)throw new Error(`File not found: ${i}`);let l=c.kind==="drive#folder",d=typeof c.size=="number"?c.size:parseInt(String(c.size||"0"),10),u="";if(!l)try{u=await this.client.getDownloadUrl(c)}catch(f){console.warn("[PikPakShare] failed to get download url in get():",f)}return{name:c.name,size:l||isNaN(d)?0:d,is_dir:l,modified:c.modified_time||new Date().toISOString(),sign:c.id,type:X(c.name,l),thumb:c.thumbnail_link,raw_url:u}}async link(e,t){let i=this.cleanPath(t),n=i.substring(0,i.lastIndexOf("/"))||"/",s=i.substring(i.lastIndexOf("/")+1),o=await this.resolveParentId(n),c=(await this.client.getFiles(o)).find(d=>d.name===s);if(!c||c.kind==="drive#folder")throw new Error(`Cannot get link for: ${t}`);return{url:await this.client.getDownloadUrl(c)}}async mkdir(e,t){throw new Error("PikPak Share is read-only")}async rename(e,t,i){throw new Error("PikPak Share is read-only")}async remove(e,t,i){throw new Error("PikPak Share is read-only")}async move(e,t,i,n,s){throw new Error("PikPak Share is read-only")}async copy(e,t,i,n,s){throw new Error("PikPak Share is read-only")}async put(e,t,i){throw new Error("PikPak Share is read-only")}}});var zm,uP=J(()=>{"use strict";Be();He();zm=class{addition;constructor(e){this.addition=e}async init(){if(!this.addition.address||!this.addition.share_name)throw new Error("SMB: address and share_name are required")}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"":t}async list(e,t){return de([],this.addition.order_by,this.addition.order_direction)}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";return{name:n,size:0,is_dir:i==="",modified:new Date().toISOString(),sign:i||"/",type:X(n,i===""),raw_url:""}}async mkdir(e,t){}async rename(e,t,i){}async remove(e,t,i){}async move(e,t,i,n,s){}async copy(e,t,i,n,s){}async put(e,t,i){}}});var jm=oe((Jm,fP)=>{(function(r,e){typeof Jm=="object"&&typeof fP<"u"?e(Jm):typeof define=="function"&&define.amd?define(["exports"],e):(r=typeof globalThis<"u"?globalThis:r||self,e(r.hashwasm={}))})(Jm,(function(r){"use strict";var e="adler32",t="AGFzbQEAAAABDANgAAF/YAAAYAF/AAMHBgABAgEAAgUEAQECAgYOAn8BQYCJBQt/AEGACAsHcAgGbWVtb3J5AgAOSGFzaF9HZXRCdWZmZXIAAAlIYXNoX0luaXQAAQtIYXNoX1VwZGF0ZQACCkhhc2hfRmluYWwAAw1IYXNoX0dldFN0YXRlAAQOSGFzaF9DYWxjdWxhdGUABQpTVEFURV9TSVpFAwEK6wkGBQBBgAkLCgBBAEEBNgKECAvjCAEHf0EAKAKECCIBQf//A3EhAiABQRB2IQMCQAJAIABBAUcNACACQQAtAIAJaiIBQY+AfGogASABQfD/A0sbIgEgA2oiBEEQdCIFQYCAPGogBSAEQfD/A0sbIAFyIQEMAQsCQAJAAkACQAJAIABBEEkNAEGACSEGIABBsCtJDQFBgAkhBgNAQQAhBQNAIAYgBWoiASgCACIEQf8BcSACaiICIANqIAIgBEEIdkH/AXFqIgJqIAIgBEEQdkH/AXFqIgJqIAIgBEEYdmoiAmogAiABQQRqKAIAIgRB/wFxaiICaiACIARBCHZB/wFxaiICaiACIARBEHZB/wFxaiICaiACIARBGHZqIgJqIAIgAUEIaigCACIEQf8BcWoiAmogAiAEQQh2Qf8BcWoiAmogAiAEQRB2Qf8BcWoiAmogAiAEQRh2aiIEaiAEIAFBDGooAgAiAUH/AXFqIgRqIAQgAUEIdkH/AXFqIgRqIAQgAUEQdkH/AXFqIgRqIAQgAUEYdmoiAmohAyAFQRBqIgVBsCtHDQALIANB8f8DcCEDIAJB8f8DcCECIAZBsCtqIQYgAEHQVGoiAEGvK0sNAAsgAEUNBCAAQQ9LDQEMAgsCQCAARQ0AAkACQCAAQQNxIgUNAEGACSEBIAAhBAwBCyAAQXxxIQRBACEBA0AgAiABQYAJai0AAGoiAiADaiEDIAUgAUEBaiIBRw0ACyAFQYAJaiEBCyAAQQRJDQADQCACIAEtAABqIgUgAS0AAWoiBiABLQACaiIAIAFBA2otAABqIgIgACAGIAUgA2pqamohAyABQQRqIQEgBEF8aiIEDQALCyACQY+AfGogAiACQfD/A0sbIANB8f8DcEEQdHIhAQwECwNAIAYoAgAiAUH/AXEgAmoiBCADaiAEIAFBCHZB/wFxaiIEaiAEIAFBEHZB/wFxaiIEaiAEIAFBGHZqIgRqIAQgBkEEaigCACIBQf8BcWoiBGogBCABQQh2Qf8BcWoiBGogBCABQRB2Qf8BcWoiBGogBCABQRh2aiIEaiAEIAZBCGooAgAiAUH/AXFqIgRqIAQgAUEIdkH/AXFqIgRqIAQgAUEQdkH/AXFqIgRqIAQgAUEYdmoiBGogBCAGQQxqKAIAIgFB/wFxaiIEaiAEIAFBCHZB/wFxaiIEaiAEIAFBEHZB/wFxaiIEaiAEIAFBGHZqIgJqIQMgBkEQaiEGIABBcGoiAEEPSw0ACyAARQ0BCyAAQX9qIQcCQCAAQQNxIgVFDQAgAEF8cSEAIAUhBCAGIQEDQCACIAEtAABqIgIgA2ohAyABQQFqIQEgBEF/aiIEDQALIAYgBWohBgsgB0EDSQ0AA0AgAiAGLQAAaiIBIAYtAAFqIgQgBi0AAmoiBSAGQQNqLQAAaiICIAUgBCABIANqampqIQMgBkEEaiEGIABBfGoiAA0ACwsgA0Hx/wNwIQMgAkHx/wNwIQILIAIgA0EQdHIhAQtBACABNgKECAsxAQF/QQBBACgChAgiAEEYdCAAQYD+A3FBCHRyIABBCHZBgP4DcSAAQRh2cnI2AoAJCwUAQYQICzsAQQBBATYChAggABACQQBBACgChAgiAEEYdCAAQYD+A3FBCHRyIABBCHZBgP4DcSAAQRh2cnI2AoAJCwsVAgBBgAgLBAQAAAAAQYQICwQBAAAA",i="02ddbd17",n={name:e,data:t,hash:i};function s(S,q,M,Y){function ie(ae){return ae instanceof M?ae:new M(function(ge){ge(ae)})}return new(M||(M=Promise))(function(ae,ge){function xe(jt){try{qt(Y.next(jt))}catch(rr){ge(rr)}}function Je(jt){try{qt(Y.throw(jt))}catch(rr){ge(rr)}}function qt(jt){jt.done?ae(jt.value):ie(jt.value).then(xe,Je)}qt((Y=Y.apply(S,q||[])).next())})}typeof SuppressedError=="function"&&SuppressedError;class o{constructor(){this.mutex=Promise.resolve()}lock(){let q=()=>{};return this.mutex=this.mutex.then(()=>new Promise(q)),new Promise(M=>{q=M})}dispatch(q){return s(this,void 0,void 0,function*(){let M=yield this.lock();try{return yield Promise.resolve(q())}finally{M()}})}}var a;function c(){return typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global}let l=c(),d=(a=l.Buffer)!==null&&a!==void 0?a:null,u=l.TextEncoder?new l.TextEncoder:null;function f(S,q){return String.fromCharCode(...S.subarray(0,q))}function h(S,q){return(S&15)+(S>>6|S>>3&8)<<4|(q&15)+(q>>6|q>>3&8)}function g(S,q){let M=q.length>>1;for(let Y=0;Y<M;Y++){let ie=Y<<1;S[Y]=h(q.charCodeAt(ie),q.charCodeAt(ie+1))}}function p(S,q){if(S.length!==q.length*2)return!1;for(let M=0;M<q.length;M++){let Y=M<<1;if(q[M]!==h(S.charCodeAt(Y),S.charCodeAt(Y+1)))return!1}return!0}let I=87,m=48;function w(S,q,M){let Y=0;for(let ie=0;ie<M;ie++){let ae=q[ie]>>>4;S[Y++]=ae>9?ae+I:ae+m,ae=q[ie]&15,S[Y++]=ae>9?ae+I:ae+m}return String.fromCharCode.apply(null,S)}let B=d!==null?S=>{if(typeof S=="string"){let q=d.from(S,"utf8");return new Uint8Array(q.buffer,q.byteOffset,q.length)}if(d.isBuffer(S))return new Uint8Array(S.buffer,S.byteOffset,S.length);if(ArrayBuffer.isView(S))return new Uint8Array(S.buffer,S.byteOffset,S.byteLength);throw new Error("Invalid data type!")}:S=>{if(typeof S=="string")return u.encode(S);if(ArrayBuffer.isView(S))return new Uint8Array(S.buffer,S.byteOffset,S.byteLength);throw new Error("Invalid data type!")},C="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",x=new Uint8Array(256);for(let S=0;S<C.length;S++)x[C.charCodeAt(S)]=S;function Q(S,q=!0){let M=S.length,Y=M%3,ie=[],ae=M-Y;for(let ge=0;ge<ae;ge+=3){let xe=(S[ge]<<16&16711680)+(S[ge+1]<<8&65280)+(S[ge+2]&255),Je=C.charAt(xe>>18&63)+C.charAt(xe>>12&63)+C.charAt(xe>>6&63)+C.charAt(xe&63);ie.push(Je)}if(Y===1){let ge=S[M-1],xe=C.charAt(ge>>2),Je=C.charAt(ge<<4&63);ie.push(`${xe}${Je}`),q&&ie.push("==")}else if(Y===2){let ge=(S[M-2]<<8)+S[M-1],xe=C.charAt(ge>>10),Je=C.charAt(ge>>4&63),qt=C.charAt(ge<<2&63);ie.push(`${xe}${Je}${qt}`),q&&ie.push("=")}return ie.join("")}function R(S){let q=Math.floor(S.length*.75),M=S.length;return S[M-1]==="="&&(q-=1,S[M-2]==="="&&(q-=1)),q}function U(S){let q=R(S),M=S.length,Y=new Uint8Array(q),ie=0;for(let ae=0;ae<M;ae+=4){let ge=x[S.charCodeAt(ae)],xe=x[S.charCodeAt(ae+1)],Je=x[S.charCodeAt(ae+2)],qt=x[S.charCodeAt(ae+3)];Y[ie]=ge<<2|xe>>4,ie+=1,Y[ie]=(xe&15)<<4|Je>>2,ie+=1,Y[ie]=(Je&3)<<6|qt&63,ie+=1}return Y}let T=16*1024,D=4,b=new o,_=new Map;function v(S,q){return s(this,void 0,void 0,function*(){let M=null,Y=null,ie=!1;if(typeof WebAssembly>"u")throw new Error("WebAssembly is not supported in this environment!");let ae=(je,it=0)=>{Y.set(je,it)},ge=()=>Y,xe=()=>M.exports,Je=je=>{M.exports.Hash_SetMemorySize(je);let it=M.exports.Hash_GetBuffer(),oi=M.exports.memory.buffer;Y=new Uint8Array(oi,it,je)},qt=()=>new DataView(M.exports.memory.buffer).getUint32(M.exports.STATE_SIZE,!0),jt=b.dispatch(()=>s(this,void 0,void 0,function*(){if(!_.has(S.name)){let it=U(S.data),oi=WebAssembly.compile(it);_.set(S.name,oi)}let je=yield _.get(S.name);M=yield WebAssembly.instantiate(je,{})})),rr=()=>s(this,void 0,void 0,function*(){M||(yield jt);let je=M.exports.Hash_GetBuffer(),it=M.exports.memory.buffer;Y=new Uint8Array(it,je,T)}),Ot=(je=null)=>{ie=!0,M.exports.Hash_Init(je)},Yi=je=>{let it=0;for(;it<je.length;){let oi=je.subarray(it,it+T);it+=oi.length,Y.set(oi),M.exports.Hash_Update(oi.length)}},Kr=je=>{if(!ie)throw new Error("update() called before init()");let it=B(je);Yi(it)},Qu=new Uint8Array(q*2),_o=(je,it=null)=>{if(!ie)throw new Error("digest() called before init()");return ie=!1,M.exports.Hash_Final(it),je==="binary"?Y.slice(0,q):w(Qu,Y,q)},xn=()=>{if(!ie)throw new Error("save() can only be called after init() and before digest()");let je=M.exports.Hash_GetState(),it=qt(),oi=M.exports.memory.buffer,nc=new Uint8Array(oi,je,it),_u=new Uint8Array(D+it);return g(_u,S.hash),_u.set(nc,D),_u},Fo=je=>{if(!(je instanceof Uint8Array))throw new Error("load() expects an Uint8Array generated by save()");let it=M.exports.Hash_GetState(),oi=qt(),nc=D+oi,_u=M.exports.memory.buffer;if(je.length!==nc)throw new Error(`Bad state length (expected ${nc} bytes, got ${je.length})`);if(!p(S.hash,je.subarray(0,D)))throw new Error("This state was written by an incompatible hash implementation");let YH=je.subarray(D);new Uint8Array(_u,it,oi).set(YH),ie=!0},bl=je=>typeof je=="string"?je.length<T/4:je.byteLength<T,Qs=bl;switch(S.name){case"argon2":case"scrypt":Qs=()=>!0;break;case"blake2b":case"blake2s":Qs=(je,it)=>it<=512&&bl(je);break;case"blake3":Qs=(je,it)=>it===0&&bl(je);break;case"xxhash64":case"xxhash3":case"xxhash128":case"crc64":Qs=()=>!1;break}let Po=(je,it=null,oi=null)=>{if(!Qs(je,it))return Ot(it),Kr(je),_o("hex",oi);let nc=B(je);return Y.set(nc),M.exports.Hash_Calculate(nc.length,it,oi),w(Qu,Y,q)};return yield rr(),{getMemory:ge,writeMemory:ae,getExports:xe,setMemorySize:Je,init:Ot,update:Kr,digest:_o,save:xn,load:Fo,calculate:Po,hashLength:q}})}function E(S,q,M){return s(this,void 0,void 0,function*(){let Y=yield S.lock(),ie=yield v(q,M);return Y(),ie})}let P=new o,N=null;function j(S){if(N===null)return E(P,n,4).then(q=>(N=q,N.calculate(S)));try{let q=N.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function z(){return v(n,4).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:4,digestSize:4};return q})}var L="argon2",G="AGFzbQEAAAABKQVgAX8Bf2AAAX9gEH9/f39/f39/f39/f39/f38AYAR/f39/AGACf38AAwYFAAECAwQFBgEBAoCAAgYIAX8BQZCoBAsHQQQGbWVtb3J5AgASSGFzaF9TZXRNZW1vcnlTaXplAAAOSGFzaF9HZXRCdWZmZXIAAQ5IYXNoX0NhbGN1bGF0ZQAECvEyBVgBAn9BACEBAkAgAEEAKAKICCICRg0AAkAgACACayIAQRB2IABBgIB8cSAASWoiAEAAQX9HDQBB/wHADwtBACEBQQBBACkDiAggAEEQdK18NwOICAsgAcALcAECfwJAQQAoAoAIIgANAEEAPwBBEHQiADYCgAhBACgCiAgiAUGAgCBGDQACQEGAgCAgAWsiAEEQdiAAQYCAfHEgAElqIgBAAEF/Rw0AQQAPC0EAQQApA4gIIABBEHStfDcDiAhBACgCgAghAAsgAAvcDgECfiAAIAQpAwAiECAAKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAMIBAgDCkDAIVCIIkiEDcDACAIIBAgCCkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgBCAQIAQpAwCFQiiJIhA3AwAgACAQIAApAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAwgECAMKQMAhUIwiSIQNwMAIAggECAIKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAEIBAgBCkDAIVCAYk3AwAgASAFKQMAIhAgASkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDSAQIA0pAwCFQiCJIhA3AwAgCSAQIAkpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAUgECAFKQMAhUIoiSIQNwMAIAEgECABKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACANIBAgDSkDAIVCMIkiEDcDACAJIBAgCSkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBSAQIAUpAwCFQgGJNwMAIAIgBikDACIQIAIpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIA4gECAOKQMAhUIgiSIQNwMAIAogECAKKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAGIBAgBikDAIVCKIkiEDcDACACIBAgAikDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgDiAQIA4pAwCFQjCJIhA3AwAgCiAQIAopAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAYgECAGKQMAhUIBiTcDACADIAcpAwAiECADKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAPIBAgDykDAIVCIIkiEDcDACALIBAgCykDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgByAQIAcpAwCFQiiJIhA3AwAgAyAQIAMpAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIA8gECAPKQMAhUIwiSIQNwMAIAsgECALKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAHIBAgBykDAIVCAYk3AwAgACAFKQMAIhAgACkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDyAQIA8pAwCFQiCJIhA3AwAgCiAQIAopAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAUgECAFKQMAhUIoiSIQNwMAIAAgECAAKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAPIBAgDykDAIVCMIkiEDcDACAKIBAgCikDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBSAQIAUpAwCFQgGJNwMAIAEgBikDACIQIAEpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAwgECAMKQMAhUIgiSIQNwMAIAsgECALKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACAGIBAgBikDAIVCKIkiEDcDACABIBAgASkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgDCAQIAwpAwCFQjCJIhA3AwAgCyAQIAspAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIAYgECAGKQMAhUIBiTcDACACIAcpAwAiECACKQMAIhF8IBFCAYZC/v///x+DIBBC/////w+DfnwiEDcDACANIBAgDSkDAIVCIIkiEDcDACAIIBAgCCkDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgByAQIAcpAwCFQiiJIhA3AwAgAiAQIAIpAwAiEXwgEEL/////D4MgEUIBhkL+////H4N+fCIQNwMAIA0gECANKQMAhUIwiSIQNwMAIAggECAIKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAHIBAgBykDAIVCAYk3AwAgAyAEKQMAIhAgAykDACIRfCARQgGGQv7///8fgyAQQv////8Pg358IhA3AwAgDiAQIA4pAwCFQiCJIhA3AwAgCSAQIAkpAwAiEXwgEUIBhkL+////H4MgEEL/////D4N+fCIQNwMAIAQgECAEKQMAhUIoiSIQNwMAIAMgECADKQMAIhF8IBBC/////w+DIBFCAYZC/v///x+DfnwiEDcDACAOIBAgDikDAIVCMIkiEDcDACAJIBAgCSkDACIRfCAQQv////8PgyARQgGGQv7///8fg358IhA3AwAgBCAQIAQpAwCFQgGJNwMAC98aAQN/QQAhBEEAIAIpAwAgASkDAIU3A5AIQQAgAikDCCABKQMIhTcDmAhBACACKQMQIAEpAxCFNwOgCEEAIAIpAxggASkDGIU3A6gIQQAgAikDICABKQMghTcDsAhBACACKQMoIAEpAyiFNwO4CEEAIAIpAzAgASkDMIU3A8AIQQAgAikDOCABKQM4hTcDyAhBACACKQNAIAEpA0CFNwPQCEEAIAIpA0ggASkDSIU3A9gIQQAgAikDUCABKQNQhTcD4AhBACACKQNYIAEpA1iFNwPoCEEAIAIpA2AgASkDYIU3A/AIQQAgAikDaCABKQNohTcD+AhBACACKQNwIAEpA3CFNwOACUEAIAIpA3ggASkDeIU3A4gJQQAgAikDgAEgASkDgAGFNwOQCUEAIAIpA4gBIAEpA4gBhTcDmAlBACACKQOQASABKQOQAYU3A6AJQQAgAikDmAEgASkDmAGFNwOoCUEAIAIpA6ABIAEpA6ABhTcDsAlBACACKQOoASABKQOoAYU3A7gJQQAgAikDsAEgASkDsAGFNwPACUEAIAIpA7gBIAEpA7gBhTcDyAlBACACKQPAASABKQPAAYU3A9AJQQAgAikDyAEgASkD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",K="e4cdc523",$={name:L,data:G,hash:K},te="blake2b",re="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",ee="c6f286e6",Z={name:te,data:re,hash:ee};let me=new o,le=null;function Ae(S){return!Number.isInteger(S)||S<8||S>512||S%8!==0?new Error("Invalid variant! Valid values: 8, 16, ..., 512"):null}function ve(S,q){return S|q<<16}function qe(S,q=512,M=null){if(Ae(q))return Promise.reject(Ae(q));let Y=null,ie=q;if(M!==null){if(Y=B(M),Y.length>64)return Promise.reject(new Error("Max key length is 64 bytes"));ie=ve(q,Y.length)}let ae=q/8;if(le===null||le.hashLength!==ae)return E(me,Z,ae).then(ge=>(le=ge,ie>512&&le.writeMemory(Y),le.calculate(S,ie)));try{ie>512&&le.writeMemory(Y);let ge=le.calculate(S,ie);return Promise.resolve(ge)}catch(ge){return Promise.reject(ge)}}function _e(S=512,q=null){if(Ae(S))return Promise.reject(Ae(S));let M=null,Y=S;if(q!==null){if(M=B(q),M.length>64)return Promise.reject(new Error("Max key length is 64 bytes"));Y=ve(S,M.length)}let ie=S/8;return v(Z,ie).then(ae=>{Y>512&&ae.writeMemory(M),ae.init(Y);let ge={init:Y>512?()=>(ae.writeMemory(M),ae.init(Y),ge):()=>(ae.init(Y),ge),update:xe=>(ae.update(xe),ge),digest:xe=>ae.digest(xe),save:()=>ae.save(),load:xe=>(ae.load(xe),ge),blockSize:128,digestSize:ie};return ge})}function se(S,q,M){let Y=[`m=${q.memorySize}`,`t=${q.iterations}`,`p=${q.parallelism}`].join(",");return`$argon2${q.hashType}$v=19$${Y}$${Q(S,!1)}$${Q(M,!1)}`}let rt=new DataView(new ArrayBuffer(4));function ce(S){return rt.setInt32(0,S,!0),new Uint8Array(rt.buffer)}function dr(S,q,M){return s(this,void 0,void 0,function*(){if(M<=64){let Je=yield _e(M*8);return Je.update(ce(M)),Je.update(q),Je.digest("binary")}let Y=Math.ceil(M/32)-2,ie=new Uint8Array(M);S.init(),S.update(ce(M)),S.update(q);let ae=S.digest("binary");ie.set(ae.subarray(0,32),0);for(let Je=1;Je<Y;Je++)S.init(),S.update(ae),ae=S.digest("binary"),ie.set(ae.subarray(0,32),Je*32);let ge=M-32*Y,xe;return ge===64?(xe=S,xe.init()):xe=yield _e(ge*8),xe.update(ae),ae=xe.digest("binary"),ie.set(ae.subarray(0,ge),Y*32),ie})}function Mr(S){switch(S){case"d":return 0;case"i":return 1;default:return 2}}function at(S){return s(this,void 0,void 0,function*(){var q;let{parallelism:M,iterations:Y,hashLength:ie}=S,ae=B(S.password),ge=B(S.salt),xe=19,Je=Mr(S.hashType),{memorySize:qt}=S,jt=B((q=S.secret)!==null&&q!==void 0?q:""),[rr,Ot]=yield Promise.all([v($,1024),_e(512)]);rr.setMemorySize(qt*1024+1024);let Yi=new Uint8Array(24),Kr=new DataView(Yi.buffer);Kr.setInt32(0,M,!0),Kr.setInt32(4,ie,!0),Kr.setInt32(8,qt,!0),Kr.setInt32(12,Y,!0),Kr.setInt32(16,xe,!0),Kr.setInt32(20,Je,!0),rr.writeMemory(Yi,qt*1024),Ot.init(),Ot.update(Yi),Ot.update(ce(ae.length)),Ot.update(ae),Ot.update(ce(ge.length)),Ot.update(ge),Ot.update(ce(jt.length)),Ot.update(jt),Ot.update(ce(0));let _o=Math.floor(qt/(M*4))*4,xn=new Uint8Array(72),Fo=Ot.digest("binary");xn.set(Fo);for(let Po=0;Po<M;Po++){xn.set(ce(0),64),xn.set(ce(Po),68);let je=Po*_o,it=yield dr(Ot,xn,1024);rr.writeMemory(it,je*1024),je+=1,xn.set(ce(1),64),it=yield dr(Ot,xn,1024),rr.writeMemory(it,je*1024)}let bl=new Uint8Array(1024);g(bl,rr.calculate(new Uint8Array([]),qt));let Qs=yield dr(Ot,bl,ie);if(S.outputType==="hex"){let Po=new Uint8Array(ie*2);return w(Po,Qs,ie)}return S.outputType==="encoded"?se(ge,S,Qs):Qs})}let we=S=>{var q;if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(!S.password)throw new Error("Password must be specified");if(S.password=B(S.password),S.password.length<1)throw new Error("Password must be specified");if(!S.salt)throw new Error("Salt must be specified");if(S.salt=B(S.salt),S.salt.length<8)throw new Error("Salt should be at least 8 bytes long");if(S.secret=B((q=S.secret)!==null&&q!==void 0?q:""),!Number.isInteger(S.iterations)||S.iterations<1)throw new Error("Iterations should be a positive number");if(!Number.isInteger(S.parallelism)||S.parallelism<1)throw new Error("Parallelism should be a positive number");if(!Number.isInteger(S.hashLength)||S.hashLength<4)throw new Error("Hash length should be at least 4 bytes.");if(!Number.isInteger(S.memorySize))throw new Error("Memory size should be specified.");if(S.memorySize<8*S.parallelism)throw new Error("Memory size should be at least 8 * parallelism.");if(S.outputType===void 0&&(S.outputType="hex"),!["hex","binary","encoded"].includes(S.outputType))throw new Error(`Insupported output type ${S.outputType}. Valid values: ['hex', 'binary', 'encoded']`)};function Ee(S){return s(this,void 0,void 0,function*(){return we(S),at(Object.assign(Object.assign({},S),{hashType:"i"}))})}function Ji(S){return s(this,void 0,void 0,function*(){return we(S),at(Object.assign(Object.assign({},S),{hashType:"id"}))})}function vo(S){return s(this,void 0,void 0,function*(){return we(S),at(Object.assign(Object.assign({},S),{hashType:"d"}))})}let Se=(S,q,M)=>{let Y=/^\$argon2(id|i|d)\$v=([0-9]+)\$((?:[mtp]=[0-9]+,){2}[mtp]=[0-9]+)\$([A-Za-z0-9+/]+)\$([A-Za-z0-9+/]+)$/,ie=q.match(Y);if(!ie)throw new Error("Invalid hash");let[,ae,ge,xe,Je,qt]=ie;if(ge!=="19")throw new Error(`Unsupported version: ${ge}`);let jt={},rr={m:"memorySize",p:"parallelism",t:"iterations"};for(let Ot of xe.split(",")){let[Yi,Kr]=Ot.split("=");jt[rr[Yi]]=Number(Kr)}return Object.assign(Object.assign({},jt),{password:S,secret:M,hashType:ae,salt:U(Je),hashLength:R(qt),outputType:"encoded"})},Dt=S=>{if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(S.hash===void 0||typeof S.hash!="string")throw new Error("Hash should be specified")};function vs(S){return s(this,void 0,void 0,function*(){Dt(S);let q=Se(S.password,S.hash,S.secret);we(q);let M=S.hash.lastIndexOf("$")+1;return(yield at(q)).substring(M)===S.hash.substring(M)})}var Ce="blake2s",ji="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",si="5c0ff166",wr={name:Ce,data:ji,hash:si};let tr=new o,ur=null;function Qe(S){return!Number.isInteger(S)||S<8||S>256||S%8!==0?new Error("Invalid variant! Valid values: 8, 16, ..., 256"):null}function be(S,q){return S|q<<16}function xl(S,q=256,M=null){if(Qe(q))return Promise.reject(Qe(q));let Y=null,ie=q;if(M!==null){if(Y=B(M),Y.length>32)return Promise.reject(new Error("Max key length is 32 bytes"));ie=be(q,Y.length)}let ae=q/8;if(ur===null||ur.hashLength!==ae)return E(tr,wr,ae).then(ge=>(ur=ge,ie>512&&ur.writeMemory(Y),ur.calculate(S,ie)));try{ie>512&&ur.writeMemory(Y);let ge=ur.calculate(S,ie);return Promise.resolve(ge)}catch(ge){return Promise.reject(ge)}}function hi(S=256,q=null){if(Qe(S))return Promise.reject(Qe(S));let M=null,Y=S;if(q!==null){if(M=B(q),M.length>32)return Promise.reject(new Error("Max key length is 32 bytes"));Y=be(S,M.length)}let ie=S/8;return v(wr,ie).then(ae=>{Y>512&&ae.writeMemory(M),ae.init(Y);let ge={init:Y>512?()=>(ae.writeMemory(M),ae.init(Y),ge):()=>(ae.init(Y),ge),update:xe=>(ae.update(xe),ge),digest:xe=>ae.digest(xe),save:()=>ae.save(),load:xe=>(ae.load(xe),ge),blockSize:64,digestSize:ie};return ge})}var Lr="blake3",Ln="AGFzbQEAAAABMQdgAAF/YAl/f39+f39/f38AYAZ/f39/fn8AYAF/AGADf39/AGABfgBgBX9/fn9/AX8DDg0AAQIDBAUGAwMDAwAEBQQBAQICBg4CfwFBgJgFC38AQYAICwdwCAZtZW1vcnkCAA5IYXNoX0dldEJ1ZmZlcgAACUhhc2hfSW5pdAAIC0hhc2hfVXBkYXRlAAkKSGFzaF9GaW5hbAAKDUhhc2hfR2V0U3RhdGUACw5IYXNoX0NhbGN1bGF0ZQAMClNUQVRFX1NJWkUDAQqQWw0FAEGACQufAwIDfwV+IwBB4ABrIgkkAAJAIAFFDQAgByAFciEKIAdBACACQQFGGyAGciAFciELIARBAEetIQwDQCAAKAIAIQcgCUEAKQOAiQE3AwAgCUEAKQOIiQE3AwggCUEAKQOQiQE3AxAgCUEAKQOYiQE3AxggCUEgaiAJIAdBwAAgAyALEAIgCSAJKQNAIAkpAyCFIg03AwAgCSAJKQNIIAkpAyiFIg43AwggCSAJKQNQIAkpAzCFIg83AxAgCSAJKQNYIAkpAziFIhA3AxggB0HAAGohByACIQQCQANAIAUhBgJAAkAgBEF/aiIEDgIDAAELIAohBgsgCUEgaiAJIAdBwAAgAyAGEAIgCSAJKQNAIAkpAyCFIg03AwAgCSAJKQNIIAkpAyiFIg43AwggCSAJKQNQIAkpAzCFIg83AxAgCSAJKQNYIAkpAziFIhA3AxggB0HAAGohBwwACwsgCCAQNwMYIAggDzcDECAIIA43AwggCCANNwMAIAhBIGohCCAAQQRqIQAgAyAMfCEDIAFBf2oiAQ0ACwsgCUHgAGokAAv4GwIMfh9/IAIpAyghBiACKQM4IQcgAikDMCEIIAIpAxAhCSACKQMgIQogAikDACELIAIpAwghDCACKQMYIQ0gACABKQMAIg43AwAgACABKQMIIg83AwggACABKQMQIhA3AxAgACAPQiCIpyANpyICaiABKQMYIhFCIIinIhJqIhMgDUIgiKciAWogEyAFc0EQdyIUQbrqv6p6aiIVIBJzQRR3IhZqIhcgDqcgC6ciBWogEKciE2oiGCALQiCIpyISaiAYIASnc0EQdyIYQefMp9AGaiIZIBNzQRR3IhNqIhogGHNBGHciGyAZaiIcIBNzQRl3Ih1qIAenIhNqIh4gB0IgiKciGGogHiAPpyAJpyIZaiARpyIfaiIgIAlCIIinIiFqICAgA3NBEHciA0Hy5rvjA2oiICAfc0EUdyIfaiIiIANzQRh3IiNzQRB3IiQgDkIgiKcgDKciA2ogEEIgiKciJWoiJiAMQiCIpyIeaiAmIARCIIinc0EQdyImQYXdntt7aiInICVzQRR3IiVqIiggJnNBGHciJiAnaiInaiIpIB1zQRR3Ih1qIiogGWogFyAUc0EYdyIrIBVqIiwgFnNBGXciFiAiaiAIpyIUaiIXIAhCIIinIhVqIBcgJnNBEHciFyAcaiIcIBZzQRR3IhZqIiIgF3NBGHciJiAcaiItIBZzQRl3Ii5qIhwgFWogJyAlc0EZdyIlIBpqIAqnIhZqIhogCkIgiKciF2ogGiArc0EQdyIaICMgIGoiIGoiIyAlc0EUdyIlaiInIBpzQRh3IisgHHNBEHciLyAgIB9zQRl3Ih8gKGogBqciGmoiICAGQiCIpyIcaiAgIBtzQRB3IhsgLGoiICAfc0EUdyIfaiIoIBtzQRh3IhsgIGoiIGoiLCAuc0EUdyIuaiIwICcgA2ogKiAkc0EYdyIkIClqIicgHXNBGXciHWoiKSACaiAbIClzQRB3IhsgLWoiKSAdc0EUdyIdaiIqIBtzQRh3IhsgKWoiKSAdc0EZdyIdaiAYaiItIBZqIC0gIiABaiAgIB9zQRl3Ih9qIiAgBWogJCAgc0EQdyIgICsgI2oiImoiIyAfc0EUdyIfaiIkICBzQRh3IiBzQRB3IisgKCAeaiAiICVzQRl3IiJqIiUgGmogJiAlc0EQdyIlICdqIiYgInNBFHciImoiJyAlc0EYdyIlICZqIiZqIiggHXNBFHciHWoiLSABaiAwIC9zQRh3Ii8gLGoiLCAuc0EZdyIuICRqIBdqIiQgE2ogJCAlc0EQdyIkIClqIiUgLnNBFHciKWoiLiAkc0EYdyIkICVqIiUgKXNBGXciKWoiMCATaiAmICJzQRl3IiIgKmogEmoiJiAcaiAmIC9zQRB3IiYgICAjaiIgaiIjICJzQRR3IiJqIiogJnNBGHciJiAwc0EQdyIvICAgH3NBGXciHyAnaiAUaiIgICFqICAgG3NBEHciGyAsaiIgIB9zQRR3Ih9qIicgG3NBGHciGyAgaiIgaiIsIClzQRR3IilqIjAgKiAeaiAtICtzQRh3IiogKGoiKCAdc0EZdyIdaiIrIBlqIBsgK3NBEHciGyAlaiIlIB1zQRR3Ih1qIisgG3NBGHciGyAlaiIlIB1zQRl3Ih1qIBZqIi0gEmogLSAuIBVqICAgH3NBGXciH2oiICADaiAqICBzQRB3IiAgJiAjaiIjaiImIB9zQRR3Ih9qIiogIHNBGHciIHNBEHciLSAnIBpqICMgInNBGXciImoiIyAUaiAkICNzQRB3IiMgKGoiJCAic0EUdyIiaiInICNzQRh3IiMgJGoiJGoiKCAdc0EUdyIdaiIuIBVqIDAgL3NBGHciLyAsaiIsIClzQRl3IikgKmogHGoiKiAYaiAqICNzQRB3IiMgJWoiJSApc0EUdyIpaiIqICNzQRh3IiMgJWoiJSApc0EZdyIpaiIwIBhqICQgInNBGXciIiAraiACaiIkICFqICQgL3NBEHciJCAgICZqIiBqIiYgInNBFHciImoiKyAkc0EYdyIkIDBzQRB3Ii8gICAfc0EZdyIfICdqIBdqIiAgBWogICAbc0EQdyIbICxqIiAgH3NBFHciH2oiJyAbc0EYdyIbICBqIiBqIiwgKXNBFHciKWoiMCArIBpqIC4gLXNBGHciKyAoaiIoIB1zQRl3Ih1qIi0gAWogGyAtc0EQdyIbICVqIiUgHXNBFHciHWoiLSAbc0EYdyIbICVqIiUgHXNBGXciHWogEmoiLiACaiAuICogE2ogICAfc0EZdyIfaiIgIB5qICsgIHNBEHciICAkICZqIiRqIiYgH3NBFHciH2oiKiAgc0EYdyIgc0EQdyIrICcgFGogJCAic0EZdyIiaiIkIBdqICMgJHNBEHciIyAoaiIkICJzQRR3IiJqIicgI3NBGHciIyAkaiIkaiIoIB1zQRR3Ih1qIi4gE2ogMCAvc0EYdyIvICxqIiwgKXNBGXciKSAqaiAhaiIqIBZqICogI3NBEHciIyAlaiIlIClzQRR3IilqIiogI3NBGHciIyAlaiIlIClzQRl3IilqIjAgFmogJCAic0EZdyIiIC1qIBlqIiQgBWogJCAvc0EQdyIkICAgJmoiIGoiJiAic0EUdyIiaiItICRzQRh3IiQgMHNBEHciLyAgIB9zQRl3Ih8gJ2ogHGoiICADaiAgIBtzQRB3IhsgLGoiICAfc0EUdyIfaiInIBtzQRh3IhsgIGoiIGoiLCApc0EUdyIpaiIwIC9zQRh3Ii8gLGoiLCApc0EZdyIpICogGGogICAfc0EZdyIfaiIgIBpqIC4gK3NBGHciKiAgc0EQdyIgICQgJmoiJGoiJiAfc0EUdyIfaiIraiAFaiIuIBJqIC4gJyAXaiAkICJzQRl3IiJqIiQgHGogIyAkc0EQdyIjICogKGoiJGoiJyAic0EUdyIiaiIoICNzQRh3IiNzQRB3IiogLSAUaiAkIB1zQRl3Ih1qIiQgFWogGyAkc0EQdyIbICVqIiQgHXNBFHciHWoiJSAbc0EYdyIbICRqIiRqIi0gKXNBFHciKWoiLiAWaiArICBzQRh3IiAgJmoiJiAfc0EZdyIfIChqICFqIiggHmogKCAbc0EQdyIbICxqIiggH3NBFHciH2oiKyAbc0EYdyIbIChqIiggH3NBGXciH2oiLCAUaiAwICQgHXNBGXciHWogAmoiJCAZaiAkICBzQRB3IiAgIyAnaiIjaiIkIB1zQRR3Ih1qIicgIHNBGHciICAsc0EQdyIsICMgInNBGXciIiAlaiABaiIjIANqICMgL3NBEHciIyAmaiIlICJzQRR3IiJqIiYgI3NBGHciIyAlaiIlaiIvIB9zQRR3Ih9qIjAgLHNBGHciLCAvaiIvIB9zQRl3Ih8gKyAcaiAlICJzQRl3IiJqIiUgIWogLiAqc0EYdyIqICVzQRB3IiUgICAkaiIgaiIkICJzQRR3IiJqIitqIAVqIi4gGmogLiAmIBdqICAgHXNBGXciHWoiICATaiAbICBzQRB3IhsgKiAtaiIgaiImIB1zQRR3Ih1qIiogG3NBGHciG3NBEHciLSAnIBhqICAgKXNBGXciIGoiJyASaiAjICdzQRB3IiMgKGoiJyAgc0EUdyIgaiIoICNzQRh3IiMgJ2oiJ2oiKSAfc0EUdyIfaiIuICFqICsgJXNBGHciISAkaiIkICJzQRl3IiIgKmogFWoiJSAeaiAlICNzQRB3IiMgL2oiJSAic0EUdyIiaiIqICNzQRh3IiMgJWoiJSAic0EZdyIiaiIrIAVqICcgIHNBGXciBSAwaiADaiIgIAJqICAgIXNBEHciISAbICZqIhtqIiAgBXNBFHciBWoiJiAhc0EYdyIhICtzQRB3IicgKCAbIB1zQRl3IhtqIBlqIh0gAWogHSAsc0EQdyIdICRqIiQgG3NBFHciG2oiKCAdc0EYdyIdICRqIiRqIisgInNBFHciImoiLCAnc0EYdyInICtqIisgInNBGXciIiAqIBxqICQgG3NBGXciHGoiGyAYaiAuIC1zQRh3IhggG3NBEHciGyAhICBqIiFqIiAgHHNBFHciHGoiJGogE2oiEyAaaiATICggFmogISAFc0EZdyIFaiIhIAJqICMgIXNBEHciAiAYIClqIhhqIiEgBXNBFHciBWoiFiACc0EYdyICc0EQdyITICYgEmogGCAfc0EZdyISaiIYIBdqIB0gGHNBEHciGCAlaiIXIBJzQRR3IhJqIhogGHNBGHciGCAXaiIXaiIdICJzQRR3Ih9qIiI2AgAgACAXIBJzQRl3IhIgLGogA2oiAyAUaiADICQgG3NBGHciFHNBEHciAyACICFqIgJqIiEgEnNBFHciEmoiFyADc0EYdyIDNgIwIAAgFiAUICBqIhQgHHNBGXciHGogAWoiASAVaiABIBhzQRB3IgEgK2oiGCAcc0EUdyIVaiIWIAFzQRh3IgEgGGoiGCAVc0EZdzYCECAAIBc2AgQgACACIAVzQRl3IgIgGmogHmoiBSAZaiAFICdzQRB3IgUgFGoiGSACc0EUdyICaiIeIAVzQRh3IgU2AjQgACAFIBlqIgU2AiAgACAiIBNzQRh3IhMgHWoiGSAfc0EZdzYCFCAAIBg2AiQgACAeNgIIIAAgATYCOCAAIAMgIWoiASASc0EZdzYCGCAAIBk2AiggACAWNgIMIAAgEzYCPCAAIAUgAnNBGXc2AhwgACABNgIsC6USCwN/BH4CfwF+AX8EfgJ/AX4CfwF+BH8jAEHQAmsiASQAAkAgAEUNAAJAAkBBAC0AiYoBQQZ0QQAtAIiKAWoiAg0AQYAJIQMMAQtBoIkBQYAJQYAIIAJrIgIgACACIABJGyICEAQgACACayIARQ0BIAFBoAFqQQApA9CJATcDACABQagBakEAKQPYiQE3AwAgAUEAKQOgiQEiBDcDcCABQQApA6iJASIFNwN4IAFBACkDsIkBIgY3A4ABIAFBACkDuIkBIgc3A4gBIAFBACkDyIkBNwOYAUEALQCKigEhCEEALQCJigEhCUEAKQPAiQEhCkEALQCIigEhCyABQbABakEAKQPgiQE3AwAgAUG4AWpBACkD6IkBNwMAIAFBwAFqQQApA/CJATcDACABQcgBakEAKQP4iQE3AwAgAUHQAWpBACkDgIoBNwMAIAEgCzoA2AEgASAKNwOQASABIAggCUVyQQJyIgg6ANkBIAEgBzcD+AEgASAGNwPwASABIAU3A+gBIAEgBDcD4AEgASABQeABaiABQZgBaiALIAogCEH/AXEQAiABKQMgIQQgASkDACEFIAEpAyghBiABKQMIIQcgASkDMCEMIAEpAxAhDSABKQM4IQ4gASkDGCEPIAoQBUEAQgA3A4CKAUEAQgA3A/iJAUEAQgA3A/CJAUEAQgA3A+iJAUEAQgA3A+CJAUEAQgA3A9iJAUEAQgA3A9CJAUEAQgA3A8iJAUEAQQApA4CJATcDoIkBQQBBACkDiIkBNwOoiQFBAEEAKQOQiQE3A7CJAUEAQQApA5iJATcDuIkBQQBBAC0AkIoBIgtBAWo6AJCKAUEAQQApA8CJAUIBfDcDwIkBIAtBBXQiC0GpigFqIA4gD4U3AwAgC0GhigFqIAwgDYU3AwAgC0GZigFqIAYgB4U3AwAgC0GRigFqIAQgBYU3AwBBAEEAOwGIigEgAkGACWohAwsCQCAAQYEISQ0AQQApA8CJASEEIAFBKGohEANAIARCCoYhCkIBIABBAXKteUI/hYanIQIDQCACIhFBAXYhAiAKIBFBf2qtg0IAUg0ACyARQQp2rSESAkACQCARQYAISw0AIAFBADsB2AEgAUIANwPQASABQgA3A8gBIAFCADcDwAEgAUIANwO4ASABQgA3A7ABIAFCADcDqAEgAUIANwOgASABQgA3A5gBIAFBACkDgIkBNwNwIAFBACkDiIkBNwN4IAFBACkDkIkBNwOAASABQQAtAIqKAToA2gEgAUEAKQOYiQE3A4gBIAEgBDcDkAEgAUHwAGogAyAREAQgASABKQNwIgQ3AwAgASABKQN4IgU3AwggASABKQOAASIGNwMQIAEgASkDiAEiBzcDGCABIAEpA5gBNwMoIAEgASkDoAE3AzAgASABKQOoATcDOCABLQDaASECIAEtANkBIQsgASkDkAEhCiABIAEtANgBIgg6AGggASAKNwMgIAEgASkDsAE3A0AgASABKQO4ATcDSCABIAEpA8ABNwNQIAEgASkDyAE3A1ggASABKQPQATcDYCABIAIgC0VyQQJyIgI6AGkgASAHNwO4AiABIAY3A7ACIAEgBTcDqAIgASAENwOgAiABQeABaiABQaACaiAQIAggCiACQf8BcRACIAEpA4ACIQQgASkD4AEhBSABKQOIAiEGIAEpA+gBIQcgASkDkAIhDCABKQPwASENIAEpA5gCIQ4gASkD+AEhDyAKEAVBAEEALQCQigEiAkEBajoAkIoBIAJBBXQiAkGpigFqIA4gD4U3AwAgAkGhigFqIAwgDYU3AwAgAkGZigFqIAYgB4U3AwAgAkGRigFqIAQgBYU3AwAMAQsCQAJAIAMgESAEQQAtAIqKASICIAEQBiITQQJLDQAgASkDGCEKIAEpAxAhBCABKQMIIQUgASkDACEGDAELIAJBBHIhFEEAKQOYiQEhDUEAKQOQiQEhDkEAKQOIiQEhD0EAKQOAiQEhFQNAIBNBfmoiFkEBdiIXQQFqIhhBA3EhCEEAIQkCQCAWQQZJDQAgGEH8////B3EhGUEAIQkgAUHIAmohAiABIQsDQCACIAs2AgAgAkEMaiALQcABajYCACACQQhqIAtBgAFqNgIAIAJBBGogC0HAAGo2AgAgC0GAAmohCyACQRBqIQIgGSAJQQRqIglHDQALCwJAIAhFDQAgASAJQQZ0aiECIAFByAJqIAlBAnRqIQsDQCALIAI2AgAgAkHAAGohAiALQQRqIQsgCEF/aiIIDQALCyABQcgCaiELIAFBoAJqIQIgGCEIA0AgCygCACEJIAEgDTcD+AEgASAONwPwASABIA83A+gBIAEgFTcD4AEgAUHwAGogAUHgAWogCUHAAEIAIBQQAiABKQOQASEKIAEpA3AhBCABKQOYASEFIAEpA3ghBiABKQOgASEHIAEpA4ABIQwgAkEYaiABKQOoASABKQOIAYU3AwAgAkEQaiAHIAyFNwMAIAJBCGogBSAGhTcDACACIAogBIU3AwAgAkEgaiECIAtBBGohCyAIQX9qIggNAAsCQAJAIBZBfnFBAmogE0kNACAYIRMMAQsgAUGgAmogGEEFdGoiAiABIBhBBnRqIgspAwA3AwAgAiALKQMINwMIIAIgCykDEDcDECACIAspAxg3AxggF0ECaiETCyABIAEpA6ACIgY3AwAgASABKQOoAiIFNwMIIAEgASkDsAIiBDcDECABIAEpA7gCIgo3AxggE0ECSw0ACwsgASkDICEHIAEpAyghDCABKQMwIQ0gASkDOCEOQQApA8CJARAFQQBBAC0AkIoBIgJBAWo6AJCKASACQQV0IgJBqYoBaiAKNwMAIAJBoYoBaiAENwMAIAJBmYoBaiAFNwMAIAJBkYoBaiAGNwMAQQApA8CJASASQgGIfBAFQQBBAC0AkIoBIgJBAWo6AJCKASACQQV0IgJBqYoBaiAONwMAIAJBoYoBaiANNwMAIAJBmYoBaiAMNwMAIAJBkYoBaiAHNwMAC0EAQQApA8CJASASfCIENwPAiQEgAyARaiEDIAAgEWsiAEGACEsNAAsgAEUNAQtBoIkBIAMgABAEQQApA8CJARAFCyABQdACaiQAC4YHAgl/AX4jAEHAAGsiAyQAAkACQCAALQBoIgRFDQACQEHAACAEayIFIAIgBSACSRsiBkUNACAGQQNxIQdBACEFAkAgBkEESQ0AIAAgBGohCCAGQXxxIQlBACEFA0AgCCAFaiIKQShqIAEgBWoiCy0AADoAACAKQSlqIAtBAWotAAA6AAAgCkEqaiALQQJqLQAAOgAAIApBK2ogC0EDai0AADoAACAJIAVBBGoiBUcNAAsLAkAgB0UNACABIAVqIQogBSAEaiAAakEoaiEFA0AgBSAKLQAAOgAAIApBAWohCiAFQQFqIQUgB0F/aiIHDQALCyAALQBoIQQLIAAgBCAGaiIHOgBoIAEgBmohAQJAIAIgBmsiAg0AQQAhAgwCCyADIAAgAEEoakHAACAAKQMgIAAtAGogAEHpAGoiBS0AACIKRXIQAiAAIAMpAyAgAykDAIU3AwAgACADKQMoIAMpAwiFNwMIIAAgAykDMCADKQMQhTcDECAAIAMpAzggAykDGIU3AxggAEEAOgBoIAUgCkEBajoAACAAQeAAakIANwMAIABB2ABqQgA3AwAgAEHQAGpCADcDACAAQcgAakIANwMAIABBwABqQgA3AwAgAEE4akIANwMAIABBMGpCADcDACAAQgA3AygLQQAhByACQcEASQ0AIABB6QBqIgotAAAhBSAALQBqIQsgACkDICEMA0AgAyAAIAFBwAAgDCALIAVB/wFxRXJB/wFxEAIgACADKQMgIAMpAwCFNwMAIAAgAykDKCADKQMIhTcDCCAAIAMpAzAgAykDEIU3AxAgACADKQM4IAMpAxiFNwMYIAogBUEBaiIFOgAAIAFBwABqIQEgAkFAaiICQcAASw0ACwsCQEHAACAHQf8BcSIGayIFIAIgBSACSRsiCUUNACAJQQNxIQtBACEFAkAgCUEESQ0AIAAgBmohByAJQfwAcSEIQQAhBQNAIAcgBWoiAkEoaiABIAVqIgotAAA6AAAgAkEpaiAKQQFqLQAAOgAAIAJBKmogCkECai0AADoAACACQStqIApBA2otAAA6AAAgCCAFQQRqIgVHDQALCwJAIAtFDQAgASAFaiEBIAUgBmogAGpBKGohBQNAIAUgAS0AADoAACABQQFqIQEgBUEBaiEFIAtBf2oiCw0ACwsgAC0AaCEHCyAAIAcgCWo6AGggA0HAAGokAAveAwQFfwN+BX8GfiMAQdABayIBJAACQCAAe6ciAkEALQCQigEiA08NAEEALQCKigFBBHIhBCABQShqIQVBACkDmIkBIQBBACkDkIkBIQZBACkDiIkBIQdBACkDgIkBIQggAyEJA0AgASAANwMYIAEgBjcDECABIAc3AwggASAINwMAIAEgA0EFdCIDQdGJAWoiCikDADcDKCABIANB2YkBaiILKQMANwMwIAEgA0HhiQFqIgwpAwA3AzggASADQemJAWoiDSkDADcDQCABIANB8YkBaikDADcDSCABIANB+YkBaikDADcDUCABIANBgYoBaikDADcDWCADQYmKAWopAwAhDiABQcAAOgBoIAEgDjcDYCABQgA3AyAgASAEOgBpIAEgADcDiAEgASAGNwOAASABIAc3A3ggASAINwNwIAFBkAFqIAFB8ABqIAVBwABCACAEQf8BcRACIAEpA7ABIQ4gASkDkAEhDyABKQO4ASEQIAEpA5gBIREgASkDwAEhEiABKQOgASETIA0gASkDyAEgASkDqAGFNwMAIAwgEiAThTcDACALIBAgEYU3AwAgCiAOIA+FNwMAIAlBf2oiCUH/AXEiAyACSw0AC0EAIAk6AJCKAQsgAUHQAWokAAvHCQIKfwV+IwBB4AJrIgUkAAJAAkAgAUGACEsNACAFIAA2AvwBIAVB/AFqIAFBgAhGIgZBECACQQEgA0EBQQIgBBABIAZBCnQiByABTw0BIAVB4ABqIgZCADcDACAFQdgAaiIIQgA3AwAgBUHQAGoiCUIANwMAIAVByABqIgpCADcDACAFQcAAaiILQgA3AwAgBUE4aiIMQgA3AwAgBUEwaiINQgA3AwAgBSADOgBqIAVCADcDKCAFQQA7AWggBUEAKQOAiQE3AwAgBUEAKQOIiQE3AwggBUEAKQOQiQE3AxAgBUEAKQOYiQE3AxggBSABQYAIRiIOrSACfDcDICAFIAAgB2pBACABIA4bEAQgBUGIAWpBMGogDSkDADcDACAFQYgBakE4aiAMKQMANwMAIAUgBSkDACIPNwOIASAFIAUpAwgiEDcDkAEgBSAFKQMQIhE3A5gBIAUgBSkDGCISNwOgASAFIAUpAyg3A7ABIAUtAGohACAFLQBpIQcgBSkDICECIAUtAGghASAFQYgBakHAAGogCykDADcDACAFQYgBakHIAGogCikDADcDACAFQYgBakHQAGogCSkDADcDACAFQYgBakHYAGogCCkDADcDACAFQYgBakHgAGogBikDADcDACAFIAE6APABIAUgAjcDqAEgBSAAIAdFckECciIAOgDxASAFIBI3A5gCIAUgETcDkAIgBSAQNwOIAiAFIA83A4ACIAVBoAJqIAVBgAJqIAVBsAFqIAEgAiAAQf8BcRACIAUpA8ACIQIgBSkDoAIhDyAFKQPIAiEQIAUpA6gCIREgBSkD0AIhEiAFKQOwAiETIAQgDkEFdGoiASAFKQPYAiAFKQO4AoU3AxggASASIBOFNwMQIAEgECARhTcDCCABIAIgD4U3AwBBAkEBIA4bIQYMAQsgAEIBIAFBf2pBCnZBAXKteUI/hYYiD6dBCnQiDiACIAMgBRAGIQcgACAOaiABIA5rIA9C////AYMgAnwgAyAFQcAAQSAgDkGACEsbahAGIQECQCAHQQFHDQAgBCAFKQMANwMAIAQgBSkDCDcDCCAEIAUpAxA3AxAgBCAFKQMYNwMYIAQgBSkDIDcDICAEIAUpAyg3AyggBCAFKQMwNwMwIAQgBSkDODcDOEECIQYMAQtBACEGQQAhAAJAIAEgB2oiCUECSQ0AIAlBfmoiCkEBdkEBaiIGQQNxIQ5BACEHAkAgCkEGSQ0AIAZB/P///wdxIQhBACEHIAVBiAFqIQEgBSEAA0AgASAANgIAIAFBDGogAEHAAWo2AgAgAUEIaiAAQYABajYCACABQQRqIABBwABqNgIAIABBgAJqIQAgAUEQaiEBIAggB0EEaiIHRw0ACwsgCkF+cSEIAkAgDkUNACAFIAdBBnRqIQEgBUGIAWogB0ECdGohAANAIAAgATYCACABQcAAaiEBIABBBGohACAOQX9qIg4NAAsLIAhBAmohAAsgBUGIAWogBkEBQgBBACADQQRyQQBBACAEEAEgACAJTw0AIAQgBkEFdGoiASAFIAZBBnRqIgApAwA3AwAgASAAKQMINwMIIAEgACkDEDcDECABIAApAxg3AxggBkEBaiEGCyAFQeACaiQAIAYLrRAIAn8EfgF/AX4EfwR+BH8EfiMAQfABayIBJAACQCAARQ0AAkBBAC0AkIoBIgINACABQTBqQQApA9CJATcDACABQThqQQApA9iJATcDACABQQApA6CJASIDNwMAIAFBACkDqIkBIgQ3AwggAUEAKQOwiQEiBTcDECABQQApA7iJASIGNwMYIAFBACkDyIkBNwMoQQAtAIqKASECQQAtAImKASEHQQApA8CJASEIQQAtAIiKASEJIAFBwABqQQApA+CJATcDACABQcgAakEAKQPoiQE3AwAgAUHQAGpBACkD8IkBNwMAIAFB2ABqQQApA/iJATcDACABQeAAakEAKQOAigE3AwAgASAJOgBoIAEgCDcDICABIAIgB0VyIgJBAnI6AGkgAUEoaiEKQgAhCEGACSELIAJBCnJB/wFxIQwDQCABQbABaiABIAogCUH/AXEgCCAMEAIgASABKQPQASINIAEpA7ABhTcDcCABIAEpA9gBIg4gASkDuAGFNwN4IAEgASkD4AEiDyABKQPAAYU3A4ABIAEgASkD6AEiECAGhTcDqAEgASAPIAWFNwOgASABIA4gBIU3A5gBIAEgDSADhTcDkAEgASAQIAEpA8gBhTcDiAEgAEHAACAAQcAASRsiEUF/aiESAkACQCARQQdxIhMNACABQfAAaiECIAshByARIRQMAQsgEUH4AHEhFCABQfAAaiECIAshBwNAIAcgAi0AADoAACAHQQFqIQcgAkEBaiECIBNBf2oiEw0ACwsCQCASQQdJDQADQCAHIAIpAAA3AAAgB0EIaiEHIAJBCGohAiAUQXhqIhQNAAsLIAhCAXwhCCALIBFqIQsgACARayIADQAMAgsLAkACQAJAQQAtAImKASIHQQZ0QQBBAC0AiIoBIhFrRg0AIAEgEToAaCABQQApA4CKATcDYCABQQApA/iJATcDWCABQQApA/CJATcDUCABQQApA+iJATcDSCABQQApA+CJATcDQCABQQApA9iJATcDOCABQQApA9CJATcDMCABQQApA8iJATcDKCABQQApA8CJASIINwMgIAFBACkDuIkBIgM3AxggAUEAKQOwiQEiBDcDECABQQApA6iJASIFNwMIIAFBACkDoIkBIgY3AwAgAUEALQCKigEiEyAHRXJBAnIiCzoAaSATQQRyIRNBACkDmIkBIQ1BACkDkIkBIQ5BACkDiIkBIQ9BACkDgIkBIRAMAQtBwAAhESABQcAAOgBoQgAhCCABQgA3AyAgAUEAKQOYiQEiDTcDGCABQQApA5CJASIONwMQIAFBACkDiIkBIg83AwggAUEAKQOAiQEiEDcDACABQQAtAIqKAUEEciITOgBpIAEgAkF+aiICQQV0IgdByYoBaikDADcDYCABIAdBwYoBaikDADcDWCABIAdBuYoBaikDADcDUCABIAdBsYoBaikDADcDSCABIAdBqYoBaikDADcDQCABIAdBoYoBaikDADcDOCABIAdBmYoBaikDADcDMCABIAdBkYoBaikDADcDKCATIQsgECEGIA8hBSAOIQQgDSEDIAJFDQELIAJBf2oiB0EFdCIUQZGKAWopAwAhFSAUQZmKAWopAwAhFiAUQaGKAWopAwAhFyAUQamKAWopAwAhGCABIAM3A4gBIAEgBDcDgAEgASAFNwN4IAEgBjcDcCABQbABaiABQfAAaiABQShqIhQgESAIIAtB/wFxEAIgASATOgBpIAFBwAA6AGggASAYNwNAIAEgFzcDOCABIBY3AzAgASAVNwMoIAFCADcDICABIA03AxggASAONwMQIAEgDzcDCCABIBA3AwAgASABKQPoASABKQPIAYU3A2AgASABKQPgASABKQPAAYU3A1ggASABKQPYASABKQO4AYU3A1AgASABKQPQASABKQOwAYU3A0ggB0UNACACQQV0QemJAWohAiATQf8BcSERA0AgAkFoaikDACEIIAJBcGopAwAhAyACQXhqKQMAIQQgAikDACEFIAEgDTcDiAEgASAONwOAASABIA83A3ggASAQNwNwIAFBsAFqIAFB8ABqIBRBwABCACAREAIgASATOgBpIAFBwAA6AGggASAFNwNAIAEgBDcDOCABIAM3AzAgASAINwMoIAFCADcDICABIA03AxggASAONwMQIAEgDzcDCCABIBA3AwAgASABKQPoASABKQPIAYU3A2AgASABKQPgASABKQPAAYU3A1ggASABKQPYASABKQO4AYU3A1AgASABKQPQASABKQOwAYU3A0ggAkFgaiECIAdBf2oiBw0ACwsgAUEoaiEJQgAhCEGACSELIBNBCHJB/wFxIQoDQCABQbABaiABIAlBwAAgCCAKEAIgASABKQPQASIDIAEpA7ABhTcDcCABIAEpA9gBIgQgASkDuAGFNwN4IAEgASkD4AEiBSABKQPAAYU3A4ABIAEgDSABKQPoASIGhTcDqAEgASAOIAWFNwOgASABIA8gBIU3A5gBIAEgECADhTcDkAEgASAGIAEpA8gBhTcDiAEgAEHAACAAQcAASRsiEUF/aiESAkACQCARQQdxIhMNACABQfAAaiECIAshByARIRQMAQsgEUH4AHEhFCABQfAAaiECIAshBwNAIAcgAi0AADoAACAHQQFqIQcgAkEBaiECIBNBf2oiEw0ACwsCQCASQQdJDQADQCAHIAIpAAA3AAAgB0EIaiEHIAJBCGohAiAUQXhqIhQNAAsLIAhCAXwhCCALIBFqIQsgACARayIADQALCyABQfABaiQAC6MCAQR+AkACQCAAQSBGDQBCq7OP/JGjs/DbACEBQv+kuYjFkdqCm38hAkLy5rvjo6f9p6V/IQNC58yn0NbQ67O7fyEEQQAhAAwBC0EAKQOYCSEBQQApA5AJIQJBACkDiAkhA0EAKQOACSEEQRAhAAtBACAAOgCKigFBAEIANwOAigFBAEIANwP4iQFBAEIANwPwiQFBAEIANwPoiQFBAEIANwPgiQFBAEIANwPYiQFBAEIANwPQiQFBAEIANwPIiQFBAEIANwPAiQFBACABNwO4iQFBACACNwOwiQFBACADNwOoiQFBACAENwOgiQFBACABNwOYiQFBACACNwOQiQFBACADNwOIiQFBACAENwOAiQFBAEEAOgCQigFBAEEAOwGIigELBgAgABADCwYAIAAQBwsGAEGAiQELqwIBBH4CQAJAIAFBIEYNAEKrs4/8kaOz8NsAIQNC/6S5iMWR2oKbfyEEQvLmu+Ojp/2npX8hBULnzKfQ1tDrs7t/IQZBACEBDAELQQApA5gJIQNBACkDkAkhBEEAKQOICSEFQQApA4AJIQZBECEBC0EAIAE6AIqKAUEAQgA3A4CKAUEAQgA3A/iJAUEAQgA3A/CJAUEAQgA3A+iJAUEAQgA3A+CJAUEAQgA3A9iJAUEAQgA3A9CJAUEAQgA3A8iJAUEAQgA3A8CJAUEAIAM3A7iJAUEAIAQ3A7CJAUEAIAU3A6iJAUEAIAY3A6CJAUEAIAM3A5iJAUEAIAQ3A5CJAUEAIAU3A4iJAUEAIAY3A4CJAUEAQQA6AJCKAUEAQQA7AYiKASAAEAMgAhAHCwsLAQBBgAgLBHgHAAA=",yn="215d875f",Le={name:Lr,data:Ln,hash:yn};let ic=new o,$i=null;function ko(S){return!Number.isInteger(S)||S<8||S%8!==0?new Error("Invalid variant! Valid values: 8, 16, ..."):null}function Ri(S,q=256,M=null){if(ko(q))return Promise.reject(ko(q));let Y=null,ie=0;if(M!==null){if(Y=B(M),Y.length!==32)return Promise.reject(new Error("Key length must be exactly 32 bytes"));ie=32}let ae=q/8,ge=ae;if($i===null||$i.hashLength!==ae)return E(ic,Le,ae).then(xe=>($i=xe,ie===32&&$i.writeMemory(Y),$i.calculate(S,ie,ge)));try{ie===32&&$i.writeMemory(Y);let xe=$i.calculate(S,ie,ge);return Promise.resolve(xe)}catch(xe){return Promise.reject(xe)}}function A(S=256,q=null){if(ko(S))return Promise.reject(ko(S));let M=null,Y=0;if(q!==null){if(M=B(q),M.length!==32)return Promise.reject(new Error("Key length must be exactly 32 bytes"));Y=32}let ie=S/8,ae=ie;return v(Le,ie).then(ge=>{Y===32&&ge.writeMemory(M),ge.init(Y);let xe={init:Y===32?()=>(ge.writeMemory(M),ge.init(Y),xe):()=>(ge.init(Y),xe),update:Je=>(ge.update(Je),xe),digest:Je=>ge.digest(Je,ae),save:()=>ge.save(),load:Je=>(ge.load(Je),xe),blockSize:64,digestSize:ie};return xe})}var k="crc32",O="AGFzbQEAAAABEQRgAAF/YAF/AGAAAGACf38AAwgHAAEBAQIAAwUEAQECAgYOAn8BQZDJBQt/AEGACAsHcAgGbWVtb3J5AgAOSGFzaF9HZXRCdWZmZXIAAAlIYXNoX0luaXQAAgtIYXNoX1VwZGF0ZQADCkhhc2hfRmluYWwABA1IYXNoX0dldFN0YXRlAAUOSGFzaF9DYWxjdWxhdGUABgpTVEFURV9TSVpFAwEKkggHBQBBgAkLwwMBA39BgIkBIQFBACECA0AgAUEAQQBBAEEAQQBBAEEAQQAgAkEBcWsgAHEgAkEBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnMiA0EBcWsgAHEgA0EBdnM2AgAgAUEEaiEBIAJBAWoiAkGAAkcNAAtBACEAA0AgAEGEkQFqIABBhIkBaigCACICQf8BcUECdEGAiQFqKAIAIAJBCHZzIgI2AgAgAEGEmQFqIAJB/wFxQQJ0QYCJAWooAgAgAkEIdnMiAjYCACAAQYShAWogAkH/AXFBAnRBgIkBaigCACACQQh2cyICNgIAIABBhKkBaiACQf8BcUECdEGAiQFqKAIAIAJBCHZzIgI2AgAgAEGEsQFqIAJB/wFxQQJ0QYCJAWooAgAgAkEIdnMiAjYCACAAQYS5AWogAkH/AXFBAnRBgIkBaigCACACQQh2cyICNgIAIABBhMEBaiACQf8BcUECdEGAiQFqKAIAIAJBCHZzNgIAIABBBGoiAEH8B0cNAAsLJwACQEEAKAKAyQEgAEYNACAAEAFBACAANgKAyQELQQBBADYChMkBC4gDAQN/QQAoAoTJAUF/cyEBQYAJIQICQCAAQQhJDQBBgAkhAgNAIAJBBGooAgAiA0EOdkH8B3FBgJEBaigCACADQRZ2QfwHcUGAiQFqKAIAcyADQQZ2QfwHcUGAmQFqKAIAcyADQf8BcUECdEGAoQFqKAIAcyACKAIAIAFzIgFBFnZB/AdxQYCpAWooAgBzIAFBDnZB/AdxQYCxAWooAgBzIAFBBnZB/AdxQYC5AWooAgBzIAFB/wFxQQJ0QYDBAWooAgBzIQEgAkEIaiECIABBeGoiAEEHSw0ACwsCQCAARQ0AAkACQCAAQQFxDQAgACEDDAELIAFB/wFxIAItAABzQQJ0QYCJAWooAgAgAUEIdnMhASACQQFqIQIgAEF/aiEDCyAAQQFGDQADQCABQf8BcSACLQAAc0ECdEGAiQFqKAIAIAFBCHZzIgFB/wFxIAJBAWotAABzQQJ0QYCJAWooAgAgAUEIdnMhASACQQJqIQIgA0F+aiIDDQALC0EAIAFBf3M2AoTJAQsyAQF/QQBBACgChMkBIgBBGHQgAEGA/gNxQQh0ciAAQQh2QYD+A3EgAEEYdnJyNgKACQsGAEGEyQELWQACQEEAKAKAyQEgAUYNACABEAFBACABNgKAyQELQQBBADYChMkBIAAQA0EAQQAoAoTJASIBQRh0IAFBgP4DcUEIdHIgAUEIdkGA/gNxIAFBGHZycjYCgAkLCwsBAEGACAsEBAAAAA==",V="d2eba587",H={name:k,data:O,hash:V};let W=new o,ue=null;function Re(S){return!Number.isInteger(S)||S<0||S>4294967295?new Error("Polynomial must be a valid 32-bit long unsigned integer"):null}function Ve(S,q=3988292384){if(Re(q))return Promise.reject(Re(q));if(ue===null)return E(W,H,4).then(M=>(ue=M,ue.calculate(S,q)));try{let M=ue.calculate(S,q);return Promise.resolve(M)}catch(M){return Promise.reject(M)}}function Ie(S=3988292384){return Re(S)?Promise.reject(Re(S)):v(H,4).then(q=>{q.init(S);let M={init:()=>(q.init(S),M),update:Y=>(q.update(Y),M),digest:Y=>q.digest(Y),save:()=>q.save(),load:Y=>(q.load(Y),M),blockSize:4,digestSize:4};return M})}var Fe="crc64",In="AGFzbQEAAAABDANgAAF/YAAAYAF/AAMHBgABAgEAAQUEAQECAgYOAn8BQZCJBgt/AEGACAsHcAgGbWVtb3J5AgAOSGFzaF9HZXRCdWZmZXIAAAlIYXNoX0luaXQAAQtIYXNoX1VwZGF0ZQACCkhhc2hfRmluYWwAAw1IYXNoX0dldFN0YXRlAAQOSGFzaF9DYWxjdWxhdGUABQpTVEFURV9TSVpFAwEKgwgGBQBBgAkL9QMDAX4BfwJ+AkBBACkDgIkCQQApA4AJIgBRDQBBgIkBIQFCACECA0AgAUIAQgBCAEIAQgBCAEIAQgAgAkIBg30gAIMgAkIBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIUiA0IBg30gAIMgA0IBiIU3AwAgAUEIaiEBIAJCAXwiAkKAAlINAAtBACEBA0AgAUGImQFqIAFBiIkBaikDACICp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiKkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiLkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiMkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiNkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiOkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhSICNwMAIAFBiPkBaiACp0H/AXFBA3RBgIkBaikDACACQgiIhTcDACABQQhqIgFB+A9HDQALQQAgADcDgIkCC0EAQgA3A4iJAguUAwIBfgJ/QQApA4iJAkJ/hSEBQYAJIQICQCAAQQhJDQBBgAkhAgNAIAIpAwAgAYUiAUIwiKdB/wFxQQN0QYCZAWopAwAgAUI4iKdBA3RBgIkBaikDAIUgAUIoiKdB/wFxQQN0QYCpAWopAwCFIAFCIIinQf8BcUEDdEGAuQFqKQMAhSABpyIDQRV2QfgPcUGAyQFqKQMAhSADQQ12QfgPcUGA2QFqKQMAhSADQQV2QfgPcUGA6QFqKQMAhSADQf8BcUEDdEGA+QFqKQMAhSEBIAJBCGohAiAAQXhqIgBBB0sNAAsLAkAgAEUNAAJAAkAgAEEBcQ0AIAAhAwwBCyABQv8BgyACMQAAhadBA3RBgIkBaikDACABQgiIhSEBIAJBAWohAiAAQX9qIQMLIABBAUYNAANAIAFC/wGDIAIxAACFp0EDdEGAiQFqKQMAIAFCCIiFIgFC/wGDIAJBAWoxAACFp0EDdEGAiQFqKQMAIAFCCIiFIQEgAkECaiECIANBfmoiAw0ACwtBACABQn+FNwOIiQILZAEBfkEAQQApA4iJAiIAQjiGIABCgP4Dg0IohoQgAEKAgPwHg0IYhiAAQoCAgPgPg0IIhoSEIABCCIhCgICA+A+DIABCGIhCgID8B4OEIABCKIhCgP4DgyAAQjiIhISENwOACQsGAEGIiQILAgALCwsBAEGACAsECAAAAA==",ks="c5ac6c16",Ss={name:Fe,data:In,hash:ks};let Ds=new o,Vi=null,Hn=new Uint8Array(8);function B0(S){let q="Polynomial must be provided as a 16 char long hex string";if(typeof S!="string"||S.length!==16)return{hi:0,lo:0,err:new Error(q)};let M=+`0x${S.slice(0,8)}`,Y=+`0x${S.slice(8)}`;return Number.isNaN(M)||Number.isNaN(Y)?{hi:M,lo:Y,err:new Error(q)}:{hi:M,lo:Y,err:null}}function ku(S,q,M){let Y=new DataView(S);Y.setUint32(0,q,!0),Y.setUint32(4,M,!0)}function Cx(S,q="c96c5795d7870f42"){let{hi:M,lo:Y,err:ie}=B0(q);if(ie!==null)return Promise.reject(ie);if(Vi===null)return E(Ds,Ss,8).then(ae=>(Vi=ae,ku(Hn.buffer,Y,M),Vi.writeMemory(Hn),Vi.calculate(S)));try{ku(Hn.buffer,Y,M),Vi.writeMemory(Hn);let ae=Vi.calculate(S);return Promise.resolve(ae)}catch(ae){return Promise.reject(ae)}}function Ex(S="c96c5795d7870f42"){let{hi:q,lo:M,err:Y}=B0(S);return Y!==null?Promise.reject(Y):v(Ss,8).then(ie=>{let ae=new Uint8Array(8);ku(ae.buffer,M,q),ie.writeMemory(ae),ie.init();let ge={init:()=>(ie.writeMemory(ae),ie.init(),ge),update:xe=>(ie.update(xe),ge),digest:xe=>ie.digest(xe),save:()=>ie.save(),load:xe=>(ie.load(xe),ge),blockSize:8,digestSize:8};return ge})}var he="md4",Ze="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",Hr="bd8ce7c7",wn={name:he,data:Ze,hash:Hr};let Bx=new o,b0=null;function tL(S){if(b0===null)return E(Bx,wn,16).then(q=>(b0=q,b0.calculate(S)));try{let q=b0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function rL(){return v(wn,16).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:16};return q})}var iL="md5",nL="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",sL="e6508e4b",Vv={name:iL,data:nL,hash:sL};let oL=new o,v0=null;function aL(S){if(v0===null)return E(oL,Vv,16).then(q=>(v0=q,v0.calculate(S)));try{let q=v0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function cL(){return v(Vv,16).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:16};return q})}var lL="sha1",dL="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",uL="6b530c24",Yv={name:lL,data:dL,hash:uL};let fL=new o,k0=null;function AL(S){if(k0===null)return E(fL,Yv,20).then(q=>(k0=q,k0.calculate(S)));try{let q=k0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function hL(){return v(Yv,20).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:20};return q})}var gL="sha3",pL="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",mL="fb24e536",S0={name:gL,data:pL,hash:mL};let yL=new o,Su=null;function D0(S){return[224,256,384,512].includes(S)?null:new Error("Invalid variant! Valid values: 224, 256, 384, 512")}function IL(S,q=512){if(D0(q))return Promise.reject(D0(q));let M=q/8;if(Su===null||Su.hashLength!==M)return E(yL,S0,M).then(Y=>(Su=Y,Su.calculate(S,q,6)));try{let Y=Su.calculate(S,q,6);return Promise.resolve(Y)}catch(Y){return Promise.reject(Y)}}function wL(S=512){if(D0(S))return Promise.reject(D0(S));let q=S/8;return v(S0,q).then(M=>{M.init(S);let Y={init:()=>(M.init(S),Y),update:ie=>(M.update(ie),Y),digest:ie=>M.digest(ie,6),save:()=>M.save(),load:ie=>(M.load(ie),Y),blockSize:200-2*q,digestSize:q};return Y})}let xL=new o,Du=null;function Q0(S){return[224,256,384,512].includes(S)?null:new Error("Invalid variant! Valid values: 224, 256, 384, 512")}function CL(S,q=512){if(Q0(q))return Promise.reject(Q0(q));let M=q/8;if(Du===null||Du.hashLength!==M)return E(xL,S0,M).then(Y=>(Du=Y,Du.calculate(S,q,1)));try{let Y=Du.calculate(S,q,1);return Promise.resolve(Y)}catch(Y){return Promise.reject(Y)}}function EL(S=512){if(Q0(S))return Promise.reject(Q0(S));let q=S/8;return v(S0,q).then(M=>{M.init(S);let Y={init:()=>(M.init(S),Y),update:ie=>(M.update(ie),Y),digest:ie=>M.digest(ie,1),save:()=>M.save(),load:ie=>(M.load(ie),Y),blockSize:200-2*q,digestSize:q};return Y})}var BL="sha256",bL="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c2pBx6KerX1qIhkgASADcyACcSABIANxcyABQR53IAFBE3dzIAFBCndzaiAaaiICaiIJIBMgEnNxIBJzaiAJQRp3IAlBFXdzIAlBB3dzakHRxqk2aiIaIAIgAXMgA3EgAiABcXMgAkEedyACQRN3cyACQQp3c2ogGGoiA2oiESAJIBNzcSATc2ogEUEadyARQRV3cyARQQd3c2pB59KkoQFqIhggAyACcyABcSADIAJxcyADQR53IANBE3dzIANBCndzaiAUaiIBaiISIBEgCXNxIAlzaiASQRp3IBJBFXdzIBJBB3dzakGFldy9AmoiFCABIANzIAJxIAEgA3FzIAFBHncgAUETd3MgAUEKd3NqIBdqIgJqIhMgI2ogJiASaiAiIBFqICUgCWogEyASIBFzcSARc2ogE0EadyATQRV3cyATQQd3c2pBuMLs8AJqIhcgAiABcyADcSACIAFxcyACQR53IAJBE3dzIAJBCndzaiAZaiIDaiIJIBMgEnNxIBJzaiAJQRp3IAlBFXdzIAlBB3dzakH827HpBGoiGSADIAJzIAFxIAMgAnFzIANBHncgA0ETd3MgA0EKd3NqIBpqIgFqIhEgCSATc3EgE3NqIBFBGncgEUEVd3MgEUEHd3NqQZOa4JkFaiIaIAEgA3MgAnEgASADcXMgAUEedyABQRN3cyABQQp3c2ogGGoiAmoiEiARIAlzcSAJc2ogEkEadyASQRV3cyASQQd3c2pB1OapqAZqIhggAiABcyADcSACIAFxcyACQR53IAJBE3dzIAJBCndzaiAUaiIDaiITaiAoIBJqICQgEWogJyAJaiATIBIgEXNxIBFzaiATQRp3IBNBFXdzIBNBB3dzakG7laizB2oiFCADIAJzIAFxIAMgAnFzIANBHncgA0ETd3MgA0EKd3NqIBdqIgFqIgkgEyASc3EgEnNqIAlBGncgCUEVd3MgCUEHd3NqQa6Si454aiIXIAEgA3MgAnEgASADcXMgAUEedyABQRN3cyABQQp3c2ogGWoiAmoiESAJIBNzcSATc2ogEUEadyARQRV3cyARQQd3c2pBhdnIk3lqIhkgAiABcyADcSACIAFxcyACQR53IAJBE3dzIAJBCndzaiAaaiIDaiISIBEgCXNxIAlzaiASQRp3IBJBFXdzIBJBB3dzakGh0f+VemoiGiADIAJz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",vL="8c18dd94",_0={name:BL,data:bL,hash:vL};let kL=new o,F0=null;function SL(S){if(F0===null)return E(kL,_0,28).then(q=>(F0=q,F0.calculate(S,224)));try{let q=F0.calculate(S,224);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function DL(){return v(_0,28).then(S=>{S.init(224);let q={init:()=>(S.init(224),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:28};return q})}let QL=new o,P0=null;function _L(S){if(P0===null)return E(QL,_0,32).then(q=>(P0=q,P0.calculate(S,256)));try{let q=P0.calculate(S,256);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function Wv(){return v(_0,32).then(S=>{S.init(256);let q={init:()=>(S.init(256),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:32};return q})}var FL="sha512",PL="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",TL="f2e40eb1",T0={name:FL,data:PL,hash:TL};let UL=new o,U0=null;function RL(S){if(U0===null)return E(UL,T0,48).then(q=>(U0=q,U0.calculate(S,384)));try{let q=U0.calculate(S,384);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function NL(){return v(T0,48).then(S=>{S.init(384);let q={init:()=>(S.init(384),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:128,digestSize:48};return q})}let qL=new o,R0=null;function OL(S){if(R0===null)return E(qL,T0,64).then(q=>(R0=q,R0.calculate(S,512)));try{let q=R0.calculate(S,512);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function ML(){return v(T0,64).then(S=>{S.init(512);let q={init:()=>(S.init(512),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:128,digestSize:64};return q})}var LL="xxhash32",HL="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",KL="4bb12485",Xv={name:LL,data:HL,hash:KL};let GL=new o,N0=null;function q0(S){return!Number.isInteger(S)||S<0||S>4294967295?new Error("Seed must be a valid 32-bit long unsigned integer."):null}function zL(S,q=0){if(q0(q))return Promise.reject(q0(q));if(N0===null)return E(GL,Xv,4).then(M=>(N0=M,N0.calculate(S,q)));try{let M=N0.calculate(S,q);return Promise.resolve(M)}catch(M){return Promise.reject(M)}}function JL(S=0){return q0(S)?Promise.reject(q0(S)):v(Xv,4).then(q=>{q.init(S);let M={init:()=>(q.init(S),M),update:Y=>(q.update(Y),M),digest:Y=>q.digest(Y),save:()=>q.save(),load:Y=>(q.load(Y),M),blockSize:16,digestSize:4};return M})}var jL="xxhash64",$L="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",VL="177fbfa3",Zv={name:jL,data:$L,hash:VL};let YL=new o,Cl=null,O0=new Uint8Array(8);function So(S){return!Number.isInteger(S)||S<0||S>4294967295?new Error("Seed must be given as two valid 32-bit long unsigned integers (lo + high)."):null}function bx(S,q,M){let Y=new DataView(S);Y.setUint32(0,q,!0),Y.setUint32(4,M,!0)}function WL(S,q=0,M=0){if(So(q))return Promise.reject(So(q));if(So(M))return Promise.reject(So(M));if(Cl===null)return E(YL,Zv,8).then(Y=>(Cl=Y,bx(O0.buffer,q,M),Cl.writeMemory(O0),Cl.calculate(S)));try{bx(O0.buffer,q,M),Cl.writeMemory(O0);let Y=Cl.calculate(S);return Promise.resolve(Y)}catch(Y){return Promise.reject(Y)}}function XL(S=0,q=0){return So(S)?Promise.reject(So(S)):So(q)?Promise.reject(So(q)):v(Zv,8).then(M=>{let Y=new Uint8Array(8);bx(Y.buffer,S,q),M.writeMemory(Y),M.init();let ie={init:()=>(M.writeMemory(Y),M.init(),ie),update:ae=>(M.update(ae),ie),digest:ae=>M.digest(ae),save:()=>M.save(),load:ae=>(M.load(ae),ie),blockSize:32,digestSize:8};return ie})}var ZL="xxhash3",eH="AGFzbQEAAAABNAhgAAF/YAR/f39/AGAHf39/f39/fwBgBH9+fn4BfmAEf39/fgF+YAN/f34BfmAAAGABfwADDg0AAQIDBAUFBQYHBgAGBQQBAQICBg4CfwFBwI4FC38AQcAJCwdwCAZtZW1vcnkCAA5IYXNoX0dldEJ1ZmZlcgAACUhhc2hfSW5pdAAIC0hhc2hfVXBkYXRlAAkKSGFzaF9GaW5hbAAKDUhhc2hfR2V0U3RhdGUACw5IYXNoX0NhbGN1bGF0ZQAMClNUQVRFX1NJWkUDAQr6QQ0FAEGACgvkAwMPfgF/AX4CQCADRQ0AIAApAzAhBCAAKQM4IQUgACkDICEGIAApAyghByAAKQMQIQggACkDGCEJIAApAwAhCiAAKQMIIQsDQCAFIAFBMGopAwAiDHwgAkE4aikDACABQThqKQMAIg2FIgVCIIggBUL/////D4N+fCEFIAcgAUEgaikDACIOfCACQShqKQMAIAFBKGopAwAiD4UiB0IgiCAHQv////8Pg358IQcgCSABQRBqKQMAIhB8IAJBGGopAwAgAUEYaikDACIRhSIJQiCIIAlC/////w+DfnwhCSALIAEpAwAiEnwgAkEIaiITKQMAIAFBCGopAwAiFIUiC0IgiCALQv////8Pg358IQsgAkEwaikDACAMhSIMQiCIIAxC/////w+DfiAEfCANfCEEIAJBIGopAwAgDoUiDEIgiCAMQv////8Pg34gBnwgD3whBiACQRBqKQMAIBCFIgxCIIggDEL/////D4N+IAh8IBF8IQggAikDACAShSIMQiCIIAxC/////w+DfiAKfCAUfCEKIAFBwABqIQEgEyECIANBf2oiAw0ACyAAIAk3AxggACAKNwMAIAAgCzcDCCAAIAc3AyggACAINwMQIAAgBTcDOCAAIAY3AyAgACAENwMwCwveAgIBfwF+AkAgBCACIAEoAgAiB2siAkkNACAAIAMgBSAHQQN0aiACEAEgACAFIAZqIgcpAwAgACkDACIIQi+IhSAIhUKx893xCX43AwAgACAHKQMIIAApAwgiCEIviIUgCIVCsfPd8Ql+NwMIIAAgBykDECAAKQMQIghCL4iFIAiFQrHz3fEJfjcDECAAIAcpAxggACkDGCIIQi+IhSAIhUKx893xCX43AxggACAHKQMgIAApAyAiCEIviIUgCIVCsfPd8Ql+NwMgIAAgBykDKCAAKQMoIghCL4iFIAiFQrHz3fEJfjcDKCAAIAcpAzAgACkDMCIIQi+IhSAIhUKx893xCX43AzAgACAHKQM4IAApAzgiCEIviIUgCIVCsfPd8Ql+NwM4IAAgAyACQQZ0aiAFIAQgAmsiBxABIAEgBzYCAA8LIAAgAyAFIAdBA3RqIAQQASABIAcgBGo2AgALhQEBAX8gAiABhSADpyIEQRh0IARBgP4DcUEIdHIgBEEIdkGA/gNxIARBGHZycq1CIIYgA4V9QQA1AoCMAUIghiAAQfyLAWo1AgCEhSIDQjGJIANCGImFIAOFQqW+4/TRjIfZn39+IgNCI4ggAK18IAOFQqW+4/TRjIfZn39+IgNCHIggA4ULZwAgAiABc60gA3wiA0IhiEEALQCAjAFBEHQgAEEIdHIgAEEBdkGAjAFqLQAAQRh0ciAAQf+LAWotAAByrYUgA4VCz9bTvtLHq9lCfiIDQh2IIAOFQvnz3fGZ9pmrFn4iA0IgiCADhQuJAwEEfgJAIABBCUkNAEEAKQOAjAEgASkDICABKQMYhSACfIUiA0I4hiADQoD+A4NCKIaEIANCgID8B4NCGIYgA0KAgID4D4NCCIaEhCADQgiIQoCAgPgPgyADQhiIQoCA/AeDhCADQiiIQoD+A4MgA0I4iISEhCAArXwgAEH4iwFqKQMAIAEpAzAgASkDKIUgAn2FIgJ8IAJC/////w+DIgQgA0IgiCIFfiIGQv////8PgyACQiCIIgIgA0L/////D4MiA358IAQgA34iA0IgiHwiBEIghiADQv////8Pg4QgBkIgiCACIAV+fCAEQiCIfIV8IgNCJYggA4VC+fPd8ZnymasWfiIDQiCIIAOFDwsCQCAAQQRJDQAgACABQQhqKQMAIAFBEGopAwAgAhADDwsCQCAARQ0AIAAgASgCACABQQRqKAIAIAIQBA8LIAEpAzggASkDQIUgAoUiA0IhiCADhULP1tO+0ser2UJ+IgNCHYggA4VC+fPd8Zn2masWfiIDQiCIIAOFC94IAQZ+IACtQoeVr6+Ytt6bnn9+IQMCQCAAQSFJDQACQCAAQcEASQ0AAkAgAEHhAEkNACABKQNoIAJ9QQApA7iMAYUiBEL/////D4MiBSABKQNgIAJ8QQApA7CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgA3wgASkDeCACfSAAQciLAWopAwCFIgNC/////w+DIgQgASkDcCACfCAAQcCLAWopAwCFIgVCIIgiBn4iB0L/////D4MgA0IgiCIDIAVC/////w+DIgV+fCAEIAV+IgRCIIh8IgVCIIYgBEL/////D4OEIAdCIIggAyAGfnwgBUIgiHyFfCEDCyABKQNIIAJ9QQApA6iMAYUiBEL/////D4MiBSABKQNAIAJ8QQApA6CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgA3wgASkDWCACfSAAQdiLAWopAwCFIgNC/////w+DIgQgASkDUCACfCAAQdCLAWopAwCFIgVCIIgiBn4iB0L/////D4MgA0IgiCIDIAVC/////w+DIgV+fCAEIAV+IgRCIIh8IgVCIIYgBEL/////D4OEIAdCIIggAyAGfnwgBUIgiHyFfCEDCyABKQMoIAJ9QQApA5iMAYUiBEL/////D4MiBSABKQMgIAJ8QQApA5CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgA3wgASkDOCACfSAAQeiLAWopAwCFIgNC/////w+DIgQgASkDMCACfCAAQeCLAWopAwCFIgVCIIgiBn4iB0L/////D4MgA0IgiCIDIAVC/////w+DIgV+fCAEIAV+IgRCIIh8IgVCIIYgBEL/////D4OEIAdCIIggAyAGfnwgBUIgiHyFfCEDCyABKQMIIAJ9QQApA4iMAYUiBEL/////D4MiBSABKQMAIAJ8QQApA4CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgA3wgASkDGCACfSAAQfiLAWopAwCFIgNC/////w+DIgQgASkDECACfCAAQfCLAWopAwCFIgJCIIgiBX4iBkL/////D4MgA0IgiCIDIAJC/////w+DIgJ+fCAEIAJ+IgJCIIh8IgRCIIYgAkL/////D4OEIAZCIIggAyAFfnwgBEIgiHyFfCICQiWIIAKFQvnz3fGZ8pmrFn4iAkIgiCAChQv8CgQBfwV+An8BfkEAIQMgASkDeCACfUEAKQP4jAGFIgRC/////w+DIgUgASkDcCACfEEAKQPwjAGFIgZCIIgiB34iCEL/////D4MgBEIgiCIEIAZC/////w+DIgZ+fCAFIAZ+IgVCIIh8IgZCIIYgBUL/////D4OEIAhCIIggBCAHfnwgBkIgiHyFIAEpA2ggAn1BACkD6IwBhSIEQv////8PgyIFIAEpA2AgAnxBACkD4IwBhSIGQiCIIgd+IghC/////w+DIARCIIgiBCAGQv////8PgyIGfnwgBSAGfiIFQiCIfCIGQiCGIAVC/////w+DhCAIQiCIIAQgB358IAZCIIh8hSABKQNYIAJ9QQApA9iMAYUiBEL/////D4MiBSABKQNQIAJ8QQApA9CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgASkDSCACfUEAKQPIjAGFIgRC/////w+DIgUgASkDQCACfEEAKQPAjAGFIgZCIIgiB34iCEL/////D4MgBEIgiCIEIAZC/////w+DIgZ+fCAFIAZ+IgVCIIh8IgZCIIYgBUL/////D4OEIAhCIIggBCAHfnwgBkIgiHyFIAEpAzggAn1BACkDuIwBhSIEQv////8PgyIFIAEpAzAgAnxBACkDsIwBhSIGQiCIIgd+IghC/////w+DIARCIIgiBCAGQv////8PgyIGfnwgBSAGfiIFQiCIfCIGQiCGIAVC/////w+DhCAIQiCIIAQgB358IAZCIIh8hSABKQMoIAJ9QQApA6iMAYUiBEL/////D4MiBSABKQMgIAJ8QQApA6CMAYUiBkIgiCIHfiIIQv////8PgyAEQiCIIgQgBkL/////D4MiBn58IAUgBn4iBUIgiHwiBkIghiAFQv////8Pg4QgCEIgiCAEIAd+fCAGQiCIfIUgASkDGCACfUEAKQOYjAGFIgRC/////w+DIgUgASkDECACfEEAKQOQjAGFIgZCIIgiB34iCEL/////D4MgBEIgiCIEIAZC/////w+DIgZ+fCAFIAZ+IgVCIIh8IgZCIIYgBUL/////D4OEIAhCIIggBCAHfnwgBkIgiHyFIAEpAwggAn1BACkDiIwBhSIEQv////8PgyIFIAEpAwAgAnxBACkDgIwBhSIGQiCIIgd+IghC/////w+DIARCIIgiBCAGQv////8PgyIGfnwgBSAGfiIFQiCIfCIGQiCGIAVC/////w+DhCAIQiCIIAQgB358IAZCIIh8hSAArUKHla+vmLbem55/fnx8fHx8fHx8IgRCJYggBIVC+fPd8ZnymasWfiIEQiCIIASFIQQCQCAAQZABSA0AIABBBHZBeGohCQNAIAEgA2oiCkELaikDACACfSADQYiNAWopAwCFIgVC/////w+DIgYgCkEDaikDACACfCADQYCNAWopAwCFIgdCIIgiCH4iC0L/////D4MgBUIgiCIFIAdC/////w+DIgd+fCAGIAd+IgZCIIh8IgdCIIYgBkL/////D4OEIAtCIIggBSAIfnwgB0IgiHyFIAR8IQQgA0EQaiEDIAlBf2oiCQ0ACwsgASkDfyACfSAAQfiLAWopAwCFIgVC/////w+DIgYgASkDdyACfCAAQfCLAWopAwCFIgJCIIgiB34iCEL/////D4MgBUIgiCIFIAJC/////w+DIgJ+fCAGIAJ+IgJCIIh8IgZCIIYgAkL/////D4OEIAhCIIggBSAHfnwgBkIgiHyFIAR8IgJCJYggAoVC+fPd8ZnymasWfiICQiCIIAKFC98FAgF+AX8CQAJAQQApA4AKIgBQRQ0AQYAIIQFCACEADAELAkBBACkDoI4BIABSDQBBACEBDAELQQAhAUEAQq+v79e895Kg/gAgAH03A/iLAUEAIABCxZbr+djShYIofDcD8IsBQQBCj/Hjja2P9JhOIAB9NwPoiwFBACAAQqus+MXV79HQfHw3A+CLAUEAQtOt1LKShbW0nn8gAH03A9iLAUEAIABCl5r0jvWWvO3JAHw3A9CLAUEAQsWDgv2v/8SxayAAfTcDyIsBQQAgAELqi7OdyOb09UN8NwPAiwFBAELIv/rLnJveueQAIAB9NwO4iwFBACAAQoqjgd/Ume2sMXw3A7CLAUEAQvm57738+MKnHSAA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QQApA4CKATcDACACQQApA4iKATcDCCACQQApA5CKATcDECACQQApA5iKATcDGCACQQApA6CKATcDICACQQApA6iKATcDKCACQQApA7CKASIFNwMwIAJBACkDuIoBIgY3AzgCQAJAQQAoAoCOASIHQcAASQ0AIAJBACgCiI4BNgJAIAIgAkHAAGpBACgCmI4BQYCMASAHQX9qQQZ2IANBACgCnI4BIgAQAiADIABqIgBBeWopAwAhCCAAKQMJIQkgACkDGSEKIAApAykhCyAHQcCLAWopAwAhBSAAKQMBIQwgB0HIiwFqKQMAIQYgB0HQiwFqKQMAIQ0gACkDESEOIAdB2IsBaikDACEPIAdB4IsBaikDACEQIAApAyEhESAHQeiLAWopAwAhEiACKQMAIRMgAikDECEUIAIpAyAhFSACKQMwIRYgAikDCCEXIAIpAxghGCACKQMoIRkgAiACKQM4IAdB8IsBaikDACIafCAAKQMxIAdB+IsBaikDACIbhSIcQiCIIBxC/////w+Dfnw3AzggGSAQfCARIBKFIhFCIIggEUL/////D4N+fCERIBggDXwgDiAPhSIOQiCIIA5C/////w+DfnwhDiAXIAV8IAwgBoUiDEIgiCAMQv////8Pg358IQwgGyAWIAsgGoUiC0IgiCALQv////8Pg358fCELIBIgFSAKIBCFIhBCIIggEEL/////D4N+fHwhECAPIBQgCSANhSINQiCIIA1C/////w+Dfnx8IRIgBiATIAggBYUiBUIgiCAFQv////8Pg358fCEIDAELIAdBwI0BaiEdQcAAIAdrIR4gAkHAAGohAAJAAkACQCAHQThNDQAgHiEfDAELAkACQEE4IAdrQQN2QQFqQQdxIh8NACACQcAAaiEAIB4hHwwBCyACQcAAaiEAIB9BA3QiICEfA0AgACAdKQMANwMAIABBCGohACAdQQhqIR0gH0F4aiIfDQALQcAAIAcgIGprIR8LAkAgBw0AA0AgACAdKQMANwMAIABBCGogHUEIaikDADcDACAAQRBqIB1BEGopAwA3AwAgAEEYaiAdQRhqKQMANwMAIABBIGogHUEgaikDADcDACAAQShqIB1BKGopAwA3AwAgAEEwaiAdQTBqKQMANwMAIABBOGogHUE4aikDADcDACAAQcAAaiEAIB1BwABqIR0gH0FAaiIfQQdLDQALCyAfRQ0BCyAfQX9qISECQCAfQQdxIiBFDQAgH0F4cSEfA0AgACAdLQAAOgAAIABBAWohACAdQQFqIR0gIEF/aiIgDQALCyAhQQdJDQADQCAAIB0pAAA3AAAgAEEIaiEAIB1BCGohHSAfQXhqIh8NAAsLIAJBwABqIB5qIR1BgIwBIQACQAJAAkAgB0EISQ0AAkAgB0E4akEDdkEBakEHcSIfDQAMAgsgH0EDdCEgQYCMASEAA0AgHSAAKQMANwMAIB1BCGohHSAAQQhqIQAgH0F/aiIfDQALIAcgIGshBwsgB0UNAQJAAkAgB0EHcSIgDQAgByEfDAELIAdBeHEhHwNAIB0gAC0AADoAACAdQQFqIR0gAEEBaiEAICBBf2oiIA0ACwsgB0EISQ0BCwNAIB0gACkAADcAACAdQQhqIR0gAEEIaiEAIB9BeGoiHw0ACwsgA0EAKAKcjgFqIgBBeWopAwAhCiAAKQMJIRMgACkDGSEUIAApAykhCyAAKQMBIQwgACkDESEOIAApAyEhESACKQMAIRUgAikDECEWIAIpAyAhFyACKQMIIRggAikDQCENIAIpA0ghDyACKQMYIRkgAikDUCESIAIpA1ghCCACKQMoIRogAikDYCEQIAIpA2ghCSACIAYgAikDcCIbfCAAKQMxIAIpA3giBoUiHEIgiCAcQv////8Pg358NwM4IBogEHwgESAJhSIRQiCIIBFC/////w+DfnwhESAZIBJ8IA4gCIUiDkIgiCAOQv////8Pg358IQ4gGCANfCAMIA+FIgxCIIggDEL/////D4N+fCEMIAYgCyAbhSILQiCIIAtC/////w+DfiAFfHwhCyAJIBcgFCAQhSIFQiCIIAVC/////w+Dfnx8IRAgCCAWIBMgEoUiBUIgiCAFQv////8Pg358fCESIA8gFSAKIA2FIgVCIIggBUL/////D4N+fHwhCAsgAykDQyACKQM4hSIFQv////8PgyIGIAMpAzsgC4UiC0IgiCINfiIPQv////8PgyAFQiCIIgUgC0L/////D4MiC358IAYgC34iBkIgiHwiC0IghiAGQv////8Pg4QgD0IgiCAFIA1+fCALQiCIfIUgAykDMyARhSIFQv////8PgyIGIAMpAysgEIUiC0IgiCINfiIPQv////8PgyAFQiCIIgUgC0L/////D4MiC358IAYgC34iBkIgiHwiC0IghiAGQv////8Pg4QgD0IgiCAFIA1+fCALQiCIfIUgAykDIyAOhSIFQv////8PgyIGIAMpAxsgEoUiC0IgiCINfiIPQv////8PgyAFQiCIIgUgC0L/////D4MiC358IAYgC34iBkIgiHwiC0IghiAGQv////8Pg4QgD0IgiCAFIA1+fCALQiCIfIUgAykDEyAMhSIFQv////8PgyIGIAMpAwsgCIUiC0IgiCINfiIPQv////8PgyAFQiCIIgUgC0L/////D4MiC358IAYgC34iBkIgiHwiC0IghiAGQv////8Pg4QgD0IgiCAFIA1+fCALQiCIfIUgBEKHla+vmLbem55/fnx8fHwiBEIliCAEhUL5893xmfKZqxZ+IgRCIIggBIUhBAwBCyAEpyEAAkBBACkDoI4BIgRQDQACQCAAQRBLDQAgAEGACCAEEAUhBAwCCwJAIABBgAFLDQAgAEGACCAEEAYhBAwCCyAAQYAIIAQQByEEDAELAkAgAEEQSw0AIAAgA0IAEAUhBAwBCwJAIABBgAFLDQAgACADQgAQBiEEDAELIAAgA0IAEAchBAtBACAEQjiGIARCgP4Dg0IohoQgBEKAgPwHg0IYhiAEQoCAgPgPg0IIhoSEIARCCIhCgICA+A+DIARCGIhCgID8B4OEIARCKIhCgP4DgyAEQjiIhISENwOACiABJAALBgBBgIoBCwIACwvMAQEAQYAIC8QBuP5sOSOkS758AYEs9yGtHN7UbemDkJfbckCkpLezZx/LeeZOzMDleIJa0H3M/3IhuAhGdPdDJI7gNZDmgTomTDwoUruRwwDLiNBlixtTLqNxZEiXog35TjgZ70ap3qzYqPp2P+OcND/53LvHxwtPHYpR4EvNtFkxyJ9+ydl4c2TqxayDNNPrw8WBoP/6E2PrFw3dUbfw2knTFlUmKdRonisWvlh9R6H8j/i40XrQMc5FyzqPlRYEKK/X+8q7S0B+QAIAAA==",tH="5a2fbdbb",ek={name:ZL,data:eH,hash:tH};let rH=new o,El=null,M0=new Uint8Array(8);function Do(S){return!Number.isInteger(S)||S<0||S>4294967295?new Error("Seed must be given as two valid 32-bit long unsigned integers (lo + high)."):null}function vx(S,q,M){let Y=new DataView(S);Y.setUint32(0,q,!0),Y.setUint32(4,M,!0)}function iH(S,q=0,M=0){if(Do(q))return Promise.reject(Do(q));if(Do(M))return Promise.reject(Do(M));if(El===null)return E(rH,ek,8).then(Y=>(El=Y,vx(M0.buffer,q,M),El.writeMemory(M0),El.calculate(S)));try{vx(M0.buffer,q,M),El.writeMemory(M0);let Y=El.calculate(S);return Promise.resolve(Y)}catch(Y){return Promise.reject(Y)}}function nH(S=0,q=0){return Do(S)?Promise.reject(Do(S)):Do(q)?Promise.reject(Do(q)):v(ek,8).then(M=>{let Y=new Uint8Array(8);vx(Y.buffer,S,q),M.writeMemory(Y),M.init();let ie={init:()=>(M.writeMemory(Y),M.init(),ie),update:ae=>(M.update(ae),ie),digest:ae=>M.digest(ae),save:()=>M.save(),load:ae=>(M.load(ae),ie),blockSize:512,digestSize:8};return ie})}var sH="xxhash128",oH="AGFzbQEAAAABKwdgAAF/YAR/f39/AGAHf39/f39/fwBgA39/fgF+YAR/f39+AGAAAGABfwADDQwAAQIDBAQEBQYFAAUFBAEBAgIGDgJ/AUHAjgULfwBBwAkLB3AIBm1lbW9yeQIADkhhc2hfR2V0QnVmZmVyAAAJSGFzaF9Jbml0AAcLSGFzaF9VcGRhdGUACApIYXNoX0ZpbmFsAAkNSGFzaF9HZXRTdGF0ZQAKDkhhc2hfQ2FsY3VsYXRlAAsKU1RBVEVfU0laRQMBCqBNDAUAQYAKC+QDAw9+AX8BfgJAIANFDQAgACkDMCEEIAApAzghBSAAKQMgIQYgACkDKCEHIAApAxAhCCAAKQMYIQkgACkDACEKIAApAwghCwNAIAUgAUEwaikDACIMfCACQThqKQMAIAFBOGopAwAiDYUiBUIgiCAFQv////8Pg358IQUgByABQSBqKQMAIg58IAJBKGopAwAgAUEoaikDACIPhSIHQiCIIAdC/////w+DfnwhByAJIAFBEGopAwAiEHwgAkEYaikDACABQRhqKQMAIhGFIglCIIggCUL/////D4N+fCEJIAsgASkDACISfCACQQhqIhMpAwAgAUEIaikDACIUhSILQiCIIAtC/////w+DfnwhCyACQTBqKQMAIAyFIgxCIIggDEL/////D4N+IAR8IA18IQQgAkEgaikDACAOhSIMQiCIIAxC/////w+DfiAGfCAPfCEGIAJBEGopAwAgEIUiDEIgiCAMQv////8Pg34gCHwgEXwhCCACKQMAIBKFIgxCIIggDEL/////D4N+IAp8IBR8IQogAUHAAGohASATIQIgA0F/aiIDDQALIAAgCTcDGCAAIAo3AwAgACALNwMIIAAgBzcDKCAAIAg3AxAgACAFNwM4IAAgBjcDICAAIAQ3AzALC94CAgF/AX4CQCAEIAIgASgCACIHayICSQ0AIAAgAyAFIAdBA3RqIAIQASAAIAUgBmoiBykDACAAKQMAIghCL4iFIAiFQrHz3fEJfjcDACAAIAcpAwggACkDCCIIQi+IhSAIhUKx893xCX43AwggACAHKQMQIAApAxAiCEIviIUgCIVCsfPd8Ql+NwMQIAAgBykDGCAAKQMYIghCL4iFIAiFQrHz3fEJfjcDGCAAIAcpAyAgACkDICIIQi+IhSAIhUKx893xCX43AyAgACAHKQMoIAApAygiCEIviIUgCIVCsfPd8Ql+NwMoIAAgBykDMCAAKQMwIghCL4iFIAiFQrHz3fEJfjcDMCAAIAcpAzggACkDOCIIQi+IhSAIhUKx893xCX43AzggACADIAJBBnRqIAUgBCACayIHEAEgASAHNgIADwsgACADIAUgB0EDdGogBBABIAEgByAEajYCAAvtAwEFfiABKQM4IAApAziFIgNC/////w+DIgQgASkDMCAAKQMwhSIFQiCIIgZ+IgdC/////w+DIANCIIgiAyAFQv////8PgyIFfnwgBCAFfiIEQiCIfCIFQiCGIARC/////w+DhCAHQiCIIAMgBn58IAVCIIh8hSABKQMoIAApAyiFIgNC/////w+DIgQgASkDICAAKQMghSIFQiCIIgZ+IgdC/////w+DIANCIIgiAyAFQv////8PgyIFfnwgBCAFfiIEQiCIfCIFQiCGIARC/////w+DhCAHQiCIIAMgBn58IAVCIIh8hSABKQMYIAApAxiFIgNC/////w+DIgQgASkDECAAKQMQhSIFQiCIIgZ+IgdC/////w+DIANCIIgiAyAFQv////8PgyIFfnwgBCAFfiIEQiCIfCIFQiCGIARC/////w+DhCAHQiCIIAMgBn58IAVCIIh8hSABKQMIIAApAwiFIgNC/////w+DIgQgASkDACAAKQMAhSIFQiCIIgZ+IgdC/////w+DIANCIIgiAyAFQv////8PgyIFfnwgBCAFfiIEQiCIfCIFQiCGIARC/////w+DhCAHQiCIIAMgBn58IAVCIIh8hSACfHx8fCICQiWIIAKFQvnz3fGZ8pmrFn4iAkIgiCAChQu6CAIFfgN/AkAgAUEJSQ0AIAAgAUH4iwFqKQMAIgQgAikDOCACKQMwhSADfIUiBUL/////D4NC95Svrwh+IAVCgICAgHCDfEEAKQOAjAEgAikDKCACKQMghSADfYUgBIUiA0IgiCIEQrHz3fEJfnwgBEKHla+vCH4iBEIgiHwgBEL/////D4MgA0L/////D4MiA0Kx893xCX58IANCh5Wvrwh+IgRCIIh8IgVCIIh8IgNCOIYgA0KA/gODQiiGhCADQoCA/AeDQhiGIANCgICA+A+DQgiGhIQgA0IIiEKAgID4D4MgA0IYiEKAgPwHg4QgA0IoiEKA/gODIANCOIiEhIQgBEL/////D4MgAUF/aq1CNoaEIAVCIIZ8hSIEQiCIIgVCz9bTvgJ+IgZC/////w+DIARC/////w+DIgRCvdzKlQx+fCAEQs/W074CfiIEQiCIfCIHQiCGIghCJYggCCAEQv////8Pg4SFQvnz3fGZ8pmrFn4iBEIgiCAEhTcDACAAIAVCvdzKlQx+IANCz9bTvtLHq9lCfnwgBkIgiHwgB0IgiHwiA0IliCADhUL5893xmfKZqxZ+IgNCIIggA4U3AwgPCwJAIAFBBEkNACAAIAIpAxggAikDEIUgA6ciAkEYdCACQYD+A3FBCHRyIAJBCHZBgP4DcSACQRh2cnKtQiCGIAOFfCABQfyLAWo1AgBCIIZBADUCgIwBhIUiA0IgiCIEIAFBAnRBh5Wvr3hqrSIFfiIGQiCIIARCsfPd8Ql+fCAGQv////8PgyADQv////8PgyIDQrHz3fEJfnwgAyAFfiIDQiCIfCIEQiCIfCAEQiCGIANC/////w+DhCIEQgGGfCIDQiWIIAOFQvnz3fGZ8pmrFn4iBUIgiCAFhTcDCCAAIANCA4ggBIUiA0IjiCADhUKlvuP00YyH2Z9/fiIDQhyIIAOFNwMADwsCQCABRQ0AIAAgAigCBCACKAIAc60gA3wiBEIhiEEALQCAjAFBEHQgAUEIdHIiCSABQQF2QYCMAWotAABBGHRyIgogAUH/iwFqLQAAIgFyIguthSAEhULP1tO+0ser2UJ+IgRCHYggBIVC+fPd8Zn2masWfiIEQiCIIASFNwMAIAAgAigCDCACKAIIc60gA30iA0IhiCABQRh0IAtBgP4DcUEIdHIgCUEIdkGA/gNxIApBGHZyckENd62FIAOFQs/W077Sx6vZQn4iA0IdiCADhUL5893xmfaZqxZ+IgNCIIggA4U3AwgPCyAAIAIpA1AgAikDWIUgA4UiBEIhiCAEhULP1tO+0ser2UJ+IgRCHYggBIVC+fPd8Zn2masWfiIEQiCIIASFNwMIIAAgAikDQCACKQNIhSADhSIDQiGIIAOFQs/W077Sx6vZQn4iA0IdiCADhUL5893xmfaZqxZ+IgNCIIggA4U3AwALwwoBCn4gAa0iBEKHla+vmLbem55/fiEFAkACQCABQSFPDQBCACEGDAELQgAhBwJAIAFBwQBJDQBCACEHAkAgAUHhAEkNACACQfgAaikDACADfSABQciLAWopAwAiCIUiB0L/////D4MiCSACKQNwIAN8IAFBwIsBaikDACIKhSILQiCIIgx+Ig1CIIggB0IgiCIHIAx+fCANQv////8PgyAHIAtC/////w+DIgt+fCAJIAt+IgdCIIh8IglCIIh8QQApA7iMASILQQApA7CMASIMfIUgCUIghiAHQv////8Pg4SFIQcgAkHoAGopAwAgA30gC4UiCUL/////D4MiCyACKQNgIAN8IAyFIgxCIIgiDX4iBkL/////D4MgCUIgiCIJIAxC/////w+DIgx+fCALIAx+IgtCIIh8IgxCIIYgC0L/////D4OEIAZCIIggCSANfnwgDEIgiHyFIAV8IAggCnyFIQULIAJB2ABqKQMAIAN9IAFB2IsBaikDACIIhSIJQv////8PgyIKIAIpA1AgA3wgAUHQiwFqKQMAIguFIgxCIIgiDX4iBkL/////D4MgCUIgiCIJIAxC/////w+DIgx+fCAKIAx+IgpCIIh8IgxCIIYgCkL/////D4OEIAZCIIggCSANfnwgDEIgiHyFIAd8QQApA6iMASIJQQApA6CMASIKfIUhByACQcgAaikDACADfSAJhSIJQv////8PgyIMIAIpA0AgA3wgCoUiCkIgiCINfiIGQv////8PgyAJQiCIIgkgCkL/////D4MiCn58IAwgCn4iCkIgiHwiDEIghiAKQv////8Pg4QgBkIgiCAJIA1+fCAMQiCIfIUgBXwgCCALfIUhBQsgAkE4aikDACADfSABQeiLAWopAwAiCIUiCUL/////D4MiCiACKQMwIAN8IAFB4IsBaikDACILhSIMQiCIIg1+IgZC/////w+DIAlCIIgiCSAMQv////8PgyIMfnwgCiAMfiIKQiCIfCIMQiCGIApC/////w+DhCAGQiCIIAkgDX58IAxCIIh8hSAHfEEAKQOYjAEiB0EAKQOQjAEiCXyFIQYgAkEoaikDACADfSAHhSIHQv////8PgyIKIAIpAyAgA3wgCYUiCUIgiCIMfiINQv////8PgyAHQiCIIgcgCUL/////D4MiCX58IAogCX4iCUIgiHwiCkIghiAJQv////8Pg4QgDUIgiCAHIAx+fCAKQiCIfIUgBXwgCCALfIUhBQsgACACQRhqKQMAIAN9IAFB+IsBaikDACIHhSIIQv////8PgyIJIAIpAxAgA3wgAUHwiwFqKQMAIgqFIgtCIIgiDH4iDUL/////D4MgCEIgiCIIIAtC/////w+DIgt+fCAJIAt+IglCIIh8IgtCIIYgCUL/////D4OEIA1CIIggCCAMfnwgC0IgiHyFIAZ8QQApA4iMASIIQQApA4CMASIJfIUiCyACQQhqKQMAIAN9IAiFIghC/////w+DIgwgAikDACADfCAJhSIJQiCIIg1+IgZC/////w+DIAhCIIgiCCAJQv////8PgyIJfnwgDCAJfiIJQiCIfCIMQiCGIAlC/////w+DhCAGQiCIIAggDX58IAxCIIh8hSAFfCAHIAp8hSIFfCIHQiWIIAeFQvnz3fGZ8pmrFn4iB0IgiCAHhTcDACAAQgAgBUKHla+vmLbem55/fiAEIAN9Qs/W077Sx6vZQn58IAtC49zKlfzO8vWFf358IgNCJYggA4VC+fPd8ZnymasWfiIDQiCIIAOFfTcDCAuhDwMBfxR+An9BACEEIAJB+ABqKQMAIAN9QQApA/iMASIFhSIGQv////8PgyIHIAIpA3AgA3xBACkD8IwBIgiFIglCIIgiCn4iC0L/////D4MgBkIgiCIGIAlC/////w+DIgl+fCAHIAl+IgdCIIh8IglCIIYgB0L/////D4OEIAtCIIggBiAKfnwgCUIgiHyFIAJB2ABqKQMAIAN9QQApA9iMASIHhSIGQv////8PgyIJIAIpA1AgA3xBACkD0IwBIgqFIgtCIIgiDH4iDUL/////D4MgBkIgiCIGIAtC/////w+DIgt+fCAJIAt+IglCIIh8IgtCIIYgCUL/////D4OEIA1CIIggBiAMfnwgC0IgiHyFIAJBOGopAwAgA31BACkDuIwBIgmFIgZC/////w+DIgsgAikDMCADfEEAKQOwjAEiDIUiDUIgiCIOfiIPQv////8PgyAGQiCIIgYgDUL/////D4MiDX58IAsgDX4iC0IgiHwiDUIghiALQv////8Pg4QgD0IgiCAGIA5+fCANQiCIfIUgAkEYaikDACADfUEAKQOYjAEiC4UiBkL/////D4MiDSACKQMQIAN8QQApA5CMASIOhSIPQiCIIhB+IhFC/////w+DIAZCIIgiBiAPQv////8PgyIPfnwgDSAPfiINQiCIfCIPQiCGIA1C/////w+DhCARQiCIIAYgEH58IA9CIIh8hUEAKQOIjAEiDUEAKQOAjAEiD3yFfEEAKQOojAEiEEEAKQOgjAEiEXyFfEEAKQPIjAEiEkEAKQPAjAEiE3yFfEEAKQPojAEiFEEAKQPgjAEiFXyFIgZCJYggBoVC+fPd8ZnymasWfiIGQiCIIAaFIQYgAkHoAGopAwAgA30gFIUiFEL/////D4MiFiACKQNgIAN8IBWFIhVCIIgiF34iGEL/////D4MgFEIgiCIUIBVC/////w+DIhV+fCAWIBV+IhVCIIh8IhZCIIYgFUL/////D4OEIBhCIIggFCAXfnwgFkIgiHyFIAJByABqKQMAIAN9IBKFIhJC/////w+DIhQgAikDQCADfCAThSITQiCIIhV+IhZC/////w+DIBJCIIgiEiATQv////8PgyITfnwgFCATfiITQiCIfCIUQiCGIBNC/////w+DhCAWQiCIIBIgFX58IBRCIIh8hSACQShqKQMAIAN9IBCFIhBC/////w+DIhIgAikDICADfCARhSIRQiCIIhN+IhRC/////w+DIBBCIIgiECARQv////8PgyIRfnwgEiARfiIRQiCIfCISQiCGIBFC/////w+DhCAUQiCIIBAgE358IBJCIIh8hSACQQhqKQMAIAN9IA2FIg1C/////w+DIhAgAikDACADfCAPhSIPQiCIIhF+IhJC/////w+DIA1CIIgiDSAPQv////8PgyIPfnwgECAPfiIPQiCIfCIQQiCGIA9C/////w+DhCASQiCIIA0gEX58IBBCIIh8hSABrSIPQoeVr6+Ytt6bnn9+fCALIA58hXwgCSAMfIV8IAcgCnyFfCAFIAh8hSIFQiWIIAWFQvnz3fGZ8pmrFn4iBUIgiCAFhSEFAkAgAUGgAUgNACABQQV2QXxqIRkDQCACIARqIhpBG2opAwAgA30gBEGYjQFqKQMAIgeFIghC/////w+DIgkgGkETaikDACADfCAEQZCNAWopAwAiCoUiC0IgiCIMfiINQv////8PgyAIQiCIIgggC0L/////D4MiC358IAkgC34iCUIgiHwiC0IghiAJQv////8Pg4QgDUIgiCAIIAx+fCALQiCIfIUgBnwgBEGIjQFqKQMAIgggBEGAjQFqKQMAIgl8hSEGIBpBC2opAwAgA30gCIUiCEL/////D4MiCyAaQQNqKQMAIAN8IAmFIglCIIgiDH4iDUL/////D4MgCEIgiCIIIAlC/////w+DIgl+fCALIAl+IglCIIh8IgtCIIYgCUL/////D4OEIA1CIIggCCAMfnwgC0IgiHyFIAV8IAcgCnyFIQUgBEEgaiEEIBlBf2oiGQ0ACwsgACACQf8AaikDACADfCABQeiLAWopAwAiB4UiCEL/////D4MiCSACKQN3IAN9IAFB4IsBaikDACIKhSILQiCIIgx+Ig1C/////w+DIAhCIIgiCCALQv////8PgyILfnwgCSALfiIJQiCIfCILQiCGIAlC/////w+DhCANQiCIIAggDH58IAtCIIh8hSAGfCABQfiLAWopAwAiBiABQfCLAWopAwAiCHyFIgkgAkHvAGopAwAgA3wgBoUiBkL/////D4MiCyACKQNnIAN9IAiFIghCIIgiDH4iDUL/////D4MgBkIgiCIGIAhC/////w+DIgh+fCALIAh+IghCIIh8IgtCIIYgCEL/////D4OEIA1CIIggBiAMfnwgC0IgiHyFIAV8IAcgCnyFIgZ8IgVCJYggBYVC+fPd8ZnymasWfiIFQiCIIAWFNwMAIABCACAGQoeVr6+Ytt6bnn9+IA8gA31Cz9bTvtLHq9lCfnwgCULj3MqV/M7y9YV/fnwiA0IliCADhUL5893xmfKZqxZ+IgNCIIggA4V9NwMIC98FAgF+AX8CQAJAQQApA4AKIgBQRQ0AQYAIIQFCACEADAELAkBBACkDoI4BIABSDQBBACEBDAELQQAhAUEAQq+v79e895Kg/gAgAH03A/iLAUEAIABCxZbr+djShYIofDcD8IsBQQBCj/Hjja2P9JhOIAB9NwPoiwFBACAAQqus+MXV79HQfHw3A+CLAUEAQtOt1LKShbW0nn8gAH03A9iLAUEAIABCl5r0jvWWvO3JAHw3A9CLAUEAQsWDgv2v/8SxayAAfTcDyIsBQQAgAELqi7OdyOb09UN8NwPAiwFBAELIv/rLnJveueQAIAB9NwO4iwFBACAAQoqjgd/Ume2sMXw3A7CLAUEAQvm57738+MKnHSAAfTcDqIsBQQAgAEKo9dv7s5ynmj98NwOgiwFBAEK4sry3lNW31lggAH03A5iLAUEAIABC8cihuqm0w/zOAHw3A5CLAUEAQoihl9u445SXo38gAH03A4iL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AkHAAGogDGohC0GAjAEhAAJAAkACQCAHQQhJDQACQCAHQThqQQN2QQFqQQdxIg0NAAwCCyANQQN0IQ5BgIwBIQADQCALIAApAwA3AwAgC0EIaiELIABBCGohACANQX9qIg0NAAsgByAOayEHCyAHRQ0BAkACQCAHQQdxIg4NACAHIQ0MAQsgB0F4cSENA0AgCyAALQAAOgAAIAtBAWohCyAAQQFqIQAgDkF/aiIODQALCyAHQQhJDQELA0AgCyAAKQAANwAAIAtBCGohCyAAQQhqIQAgDUF4aiINDQALCyACIAIpAwggAikDQCIIfCADQQAoApyOAWoiACkDASACKQNIIgmFIgpCIIggCkL/////D4N+fDcDCCACIAIpAxggAikDUCIKfCAAKQMRIAIpA1giEIUiEUIgiCARQv////8Pg358NwMYIAIgECAKIAApAwmFIgpCIIggCkL/////D4N+IAIpAxB8fDcDECACIAkgCCAAQXlqKQMAhSIIQiCIIAhC/////w+DfiACKQMAfHw3AwAgACkDGSEIIAIpAyAhCSACIAIpAyggAikDYCIKfCAAKQMhIAIpA2giEIUiEUIgiCARQv////8Pg358NwMoIAIgECAJIAggCoUiCEIgiCAIQv////8Pg358fDcDICACIAYgAikDcCIIfCAAKQMxIAIpA3giBoUiCUIgiCAJQv////8Pg358NwM4IAIgBiAIIAApAymFIghCIIggCEL/////D4N+IAV8fDcDMAsgAiACIANBC2ogBEKHla+vmLbem55/fhADNwNAIAIgAiADQQAoApyOAWpBdWogBELP1tO+0ser2UJ+Qn+FEAM3A0gMAQsgBKchAAJAQQApA6COASIEUA0AAkAgAEEQSw0AIAJBwABqIABBgAggBBAEDAILAkAgAEGAAUsNACACQcAAaiAAQYAIIAQQBQwCCyACQcAAaiAAQYAIIAQQBgwBCwJAIABBEEsNACACQcAAaiAAIANCABAEDAELAkAgAEGAAUsNACACQcAAaiAAIANCABAFDAELIAJBwABqIAAgA0IAEAYLQQAgAikDcDcDuApBACACKQNgNwOoCkEAIAIpA1A3A5gKQQAgAkH4AGopAwA3A8AKQQAgAkHoAGopAwA3A7AKQQAgAkHYAGopAwA3A6AKQQAgAikDSCIEQjiGIARCgP4Dg0IohoQgBEKAgPwHg0IYhiAEQoCAgPgPg0IIhoSEIARCCIhCgICA+A+DIARCGIhCgID8B4OEIARCKIhCgP4DgyAEQjiIhISEIgQ3A4AKQQAgBDcDkApBACACKQNAIgRCOIYgBEKA/gODQiiGhCAEQoCA/AeDQhiGIARCgICA+A+DQgiGhIQgBEIIiEKAgID4D4MgBEIYiEKAgPwHg4QgBEIoiEKA/gODIARCOIiEhIQ3A4gKIAEkAAsGAEGAigELAgALC8wBAQBBgAgLxAG4/mw5I6RLvnwBgSz3Ia0c3tRt6YOQl9tyQKSkt7NnH8t55k7MwOV4glrQfcz/ciG4CEZ090MkjuA1kOaBOiZMPChSu5HDAMuI0GWLG1Muo3FkSJeiDflOOBnvRqnerNio+nY/45w0P/ncu8fHC08dilHgS820WTHIn37J2XhzZOrFrIM00+vDxYGg//oTY+sXDd1Rt/DaSdMWVSYp1GieKxa+WH1HofyP+LjRetAxzkXLOo+VFgQor9f7yrtLQH5AAgAA",aH="b9ab74e2",tk={name:sH,data:oH,hash:aH};let cH=new o,Bl=null,L0=new Uint8Array(8);function Qo(S){return!Number.isInteger(S)||S<0||S>4294967295?new Error("Seed must be given as two valid 32-bit long unsigned integers (lo + high)."):null}function kx(S,q,M){let Y=new DataView(S);Y.setUint32(0,q,!0),Y.setUint32(4,M,!0)}function lH(S,q=0,M=0){if(Qo(q))return Promise.reject(Qo(q));if(Qo(M))return Promise.reject(Qo(M));if(Bl===null)return E(cH,tk,16).then(Y=>(Bl=Y,kx(L0.buffer,q,M),Bl.writeMemory(L0),Bl.calculate(S)));try{kx(L0.buffer,q,M),Bl.writeMemory(L0);let Y=Bl.calculate(S);return Promise.resolve(Y)}catch(Y){return Promise.reject(Y)}}function dH(S=0,q=0){return Qo(S)?Promise.reject(Qo(S)):Qo(q)?Promise.reject(Qo(q)):v(tk,16).then(M=>{let Y=new Uint8Array(8);kx(Y.buffer,S,q),M.writeMemory(Y),M.init();let ie={init:()=>(M.writeMemory(Y),M.init(),ie),update:ae=>(M.update(ae),ie),digest:ae=>M.digest(ae),save:()=>M.save(),load:ae=>(M.load(ae),ie),blockSize:512,digestSize:16};return ie})}var uH="ripemd160",fH="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",AH="6abbce74",rk={name:uH,data:fH,hash:AH};let hH=new o,H0=null;function gH(S){if(H0===null)return E(hH,rk,20).then(q=>(H0=q,H0.calculate(S)));try{let q=H0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function pH(){return v(rk,20).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:20};return q})}function mH(S,q){let{blockSize:M}=S,Y=B(q);if(Y.length>M){S.update(Y);let ie=S.digest("binary");return S.init(),ie}return new Uint8Array(Y.buffer,Y.byteOffset,Y.length)}function yH(S,q){S.init();let{blockSize:M}=S,Y=mH(S,q),ie=new Uint8Array(M);ie.set(Y);let ae=new Uint8Array(M);for(let xe=0;xe<M;xe++){let Je=ie[xe];ae[xe]=Je^92,ie[xe]=Je^54}S.update(ie);let ge={init:()=>(S.init(),S.update(ie),ge),update:xe=>(S.update(xe),ge),digest:(xe=>{let Je=S.digest("binary");return S.init(),S.update(ae),S.update(Je),S.digest(xe)}),save:()=>{throw new Error("save() not supported")},load:()=>{throw new Error("load() not supported")},blockSize:S.blockSize,digestSize:S.digestSize};return ge}function ik(S,q){if(!S||!S.then)throw new Error('Invalid hash function is provided! Usage: createHMAC(createMD5(), "key").');return S.then(M=>yH(M,q))}function IH(S,q,M,Y,ie){return s(this,void 0,void 0,function*(){let ae=new Uint8Array(Y),ge=new Uint8Array(q.length+4),xe=new DataView(ge.buffer),Je=B(q),qt=new Uint8Array(Je.buffer,Je.byteOffset,Je.length);ge.set(qt);let jt=0,rr=S.digestSize,Ot=Math.ceil(Y/rr),Yi=null,Kr=null;for(let _o=1;_o<=Ot;_o++){xe.setUint32(q.length,_o),S.init(),S.update(ge),Yi=S.digest("binary"),Kr=Yi.slice();for(let xn=1;xn<M;xn++){S.init(),S.update(Kr),Kr=S.digest("binary");for(let Fo=0;Fo<rr;Fo++)Yi[Fo]^=Kr[Fo]}ae.set(Yi.subarray(0,Y-jt),jt),jt+=rr}if(ie==="binary")return ae;let Qu=new Uint8Array(Y*2);return w(Qu,ae,Y)})}let wH=S=>{if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(!S.hashFunction||!S.hashFunction.then)throw new Error('Invalid hash function is provided! Usage: pbkdf2("password", "salt", 1000, 32, createSHA1()).');if(!Number.isInteger(S.iterations)||S.iterations<1)throw new Error("Iterations should be a positive number");if(!Number.isInteger(S.hashLength)||S.hashLength<1)throw new Error("Hash length should be a positive number");if(S.outputType===void 0&&(S.outputType="hex"),!["hex","binary"].includes(S.outputType))throw new Error(`Insupported output type ${S.outputType}. Valid values: ['hex', 'binary']`)};function Sx(S){return s(this,void 0,void 0,function*(){wH(S);let q=yield ik(S.hashFunction,S.password);return IH(q,S.salt,S.iterations,S.hashLength,S.outputType)})}var xH="scrypt",CH="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cyICajYCOCAAIAAoAjwgAiAKakESdyAGc2o2AjwLvxIDFX8Bfg5/AkAgAkUNACAAQQd0IgNBQGoiBEEAKAKACCIFIAMgAmwiBmogAyABbGoiByADaiIIaiEJIAAgAkEHdCIKIAFBB3RqIgtsIQwgACALQYABamwhDSAAQQV0IgtBASALQQFLGyILQWBxIQ4gC0EBcSEPIAdBeGohECAHQXBqIREgB0FoaiESIAdBYGohEyAHQVhqIRQgB0FQaiEVIAdBSGohFiAHQUBqIRcgAa1Cf3whGCAEIAdqIRkgByAAQQh0IhpqIRsgACAKQYABamwhHCALQQRJIR1BACEeQQAhHwNAQQAoAoAIIiAgAyAfbGohIQJAIABFDQBBACEiAkAgHQ0AICAgHmohI0EAIQtBACEiA0AgByALaiIEICMgC2oiJCgCADYCACAEQQRqICRBBGooAgA2AgAgBEEIaiAkQQhqKAIANgIAIARBDGogJEEMaigCADYCACALQRBqIQsgDiAiQQRqIiJHDQALCyAPRQ0AIAcgIkECdCILaiAhIAtqKAIANgIACwJAIAFFDQBBACElIBwhIyAGISYDQCAFISQgACEiAkACQCAADQAgGyAXKQMANwMAIBsgFikDADcDCCAbIBUpAwA3AxAgGyAUKQMANwMYIBsgEykDADcDICAbIBIpAwA3AyggGyARKQMANwMwIBsgECkDADcDOAwBCwNAICQgJmoiCyAkIAxqIgQpAwA3AwAgC0EIaiAEQQhqKQMANwMAIAtBEGogBEEQaikDADcDACALQRhqIARBGGopAwA3AwAgC0EgaiAEQSBqKQMANwMAIAtBKGogBEEoaikDADcDACALQTBqIARBMGopAwA3AwAgC0E4aiAEQThqKQMANwMAIAtBwABqIARBwABqKQMANwMAIAtByABqIARByABqKQMANwMAIAtB0ABqIARB0ABqKQMANwMAIAtB2ABqIARB2ABqKQMANwMAIAtB4ABqIARB4ABqKQMANwMAIAtB6ABqIARB6ABqKQMANwMAIAtB8ABqIARB8ABqKQMANwMAIAtB+ABqIARB+ABqKQMANwMAICRBgAFqISQgIkF/aiIiDQALIAcgCCAbIAAQAiAFISQgACEiA0AgJCAjaiILICQgDWoiBCkDADcDACALQQhqIARBCGopAwA3AwAgC0EQaiAEQRBqKQMANwMAIAtBGGogBEEYaikDADcDACALQSBqIARBIGopAwA3AwAgC0EoaiAEQShqKQMANwMAIAtBMGogBEEwaikDADcDACALQThqIARBOGopAwA3AwAgC0HAAGogBEHAAGopAwA3AwAgC0HIAGogBEHIAGopAwA3AwAgC0HQAGogBEHQAGopAwA3AwAgC0HYAGogBEHYAGopAwA3AwAgC0HgAGogBEHgAGopAwA3AwAgC0HoAGogBEHoAGopAwA3AwAgC0HwAGogBEHwAGopAwA3AwAgC0H4AGogBEH4AGopAwA3AwAgJEGAAWohJCAiQX9qIiINAAsLIAggByAbIAAQAiAjIBpqISMgJiAaaiEmICVBAmoiJSABSQ0AC0EAISUDQAJAAkAgAA0AIBsgFykDADcDACAbIBYpAwA3AwggGyAVKQMANwMQIBsgFCkDADcDGCAbIBMpAwA3AyAgGyASKQMANwMoIBsgESkDADcDMCAbIBApAwA3AzgMAQsgACAKIBkpAgAgGIOnQQd0amwhJiAFISQgACEiA0AgJCAMaiILIAspAwAgJCAmaiIEKQMAhTcDACALQQhqIiMgIykDACAEQQhqKQMAhTcDACALQRBqIiMgIykDACAEQRBqKQMAhTcDACALQRhqIiMgIykDACAEQRhqKQMAhTcDACALQSBqIiMgIykDACAEQSBqKQMAhTcDACALQShqIiMgIykDACAEQShqKQMAhTcDACALQTBqIiMgIykDACAEQTBqKQMAhTcDACALQThqIiMgIykDACAEQThqKQMAhTcDACALQcAAaiIjICMpAwAgBEHAAGopAwCFNwMAIAtByABqIiMgIykDACAEQcgAaikDAIU3AwAgC0HQAGoiIyAjKQMAIARB0ABqKQMAhTcDACALQdgAaiIjICMpAwAgBEHYAGopAwCFNwMAIAtB4ABqIiMgIykDACAEQeAAaikDAIU3AwAgC0HoAGoiIyAjKQMAIARB6ABqKQMAhTcDACALQfAAaiIjICMpAwAgBEHwAGopAwCFNwMAIAtB+ABqIgsgCykDACAEQfgAaikDAIU3AwAgJEGAAWohJCAiQX9qIiINAAsgByAIIBsgABACIAAgCiAJKQIAIBiDp0EHdGpsISYgBSEkIAAhIgNAICQgDWoiCyALKQMAICQgJmoiBCkDAIU3AwAgC0EIaiIjICMpAwAgBEEIaikDAIU3AwAgC0EQaiIjICMpAwAgBEEQaikDAIU3AwAgC0EYaiIjICMpAwAgBEEYaikDAIU3AwAgC0EgaiIjICMpAwAgBEEgaikDAIU3AwAgC0EoaiIjICMpAwAgBEEoaikDAIU3AwAgC0EwaiIjICMpAwAgBEEwaikDAIU3AwAgC0E4aiIjICMpAwAgBEE4aikDAIU3AwAgC0HAAGoiIyAjKQMAIARBwABqKQMAhTcDACALQcgAaiIjICMpAwAgBEHIAGopAwCFNwMAIAtB0ABqIiMgIykDACAEQdAAaikDAIU3AwAgC0HYAGoiIyAjKQMAIARB2ABqKQMAhTcDACALQeAAaiIjICMpAwAgBEHgAGopAwCFNwMAIAtB6ABqIiMgIykDACAEQegAaikDAIU3AwAgC0HwAGoiIyAjKQMAIARB8ABqKQMAhTcDACALQfgAaiILIAspAwAgBEH4AGopAwCFNwMAICRBgAFqISQgIkF/aiIiDQALCyAIIAcgGyAAEAIgJUECaiIlIAFJDQALCwJAIABFDQBBACEiAkAgHQ0AICAgHmohI0EAIQtBACEiA0AgIyALaiIEIAcgC2oiJCgCADYCACAEQQRqICRBBGooAgA2AgAgBEEIaiAkQQhqKAIANgIAIARBDGogJEEMaigCADYCACALQRBqIQsgDiAiQQRqIiJHDQALCyAPRQ0AICEgIkECdCILaiAHIAtqKAIANgIACyAeIANqIR4gH0EBaiIfIAJHDQALCws=",EH="b32721f8",BH={name:xH,data:CH,hash:EH};function bH(S){return s(this,void 0,void 0,function*(){let{costFactor:q,blockSize:M,parallelism:Y,hashLength:ie}=S,ae=Wv(),ge=yield Sx({password:S.password,salt:S.salt,iterations:1,hashLength:128*M*Y,hashFunction:ae,outputType:"binary"}),xe=yield v(BH,0),Je=128*M*q,qt=256*M;xe.setMemorySize(ge.length+Je+qt),xe.writeMemory(ge,0),xe.getExports().scrypt(M,q,Y);let jt=xe.getMemory().subarray(0,128*M*Y),rr=yield Sx({password:S.password,salt:jt,iterations:1,hashLength:ie,hashFunction:ae,outputType:"binary"});if(S.outputType==="hex"){let Ot=new Uint8Array(ie*2);return w(Ot,rr,ie)}return rr})}let vH=S=>S&&!(S&S-1),kH=S=>{if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(!Number.isInteger(S.blockSize)||S.blockSize<1)throw new Error("Block size should be a positive number");if(!Number.isInteger(S.costFactor)||S.costFactor<2||!vH(S.costFactor))throw new Error("Cost factor should be a power of 2, greater than 1");if(!Number.isInteger(S.parallelism)||S.parallelism<1)throw new Error("Parallelism should be a positive number");if(!Number.isInteger(S.hashLength)||S.hashLength<1)throw new Error("Hash length should be a positive number.");if(S.outputType===void 0&&(S.outputType="hex"),!["hex","binary"].includes(S.outputType))throw new Error(`Insupported output type ${S.outputType}. Valid values: ['hex', 'binary']`)};function SH(S){return s(this,void 0,void 0,function*(){return kH(S),bH(S)})}var DH="bcrypt",QH="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",_H="8bd8822d",nk={name:DH,data:QH,hash:_H};function FH(S){return s(this,void 0,void 0,function*(){let{costFactor:q,password:M,salt:Y}=S,ie=yield v(nk,0);ie.writeMemory(B(Y),0);let ae=B(M);ie.writeMemory(ae,16);let ge=S.outputType==="encoded"?1:0;ie.getExports().bcrypt(ae.length,q,ge);let xe=ie.getMemory();if(S.outputType==="encoded")return f(xe,60);if(S.outputType==="hex"){let Je=new Uint8Array(48);return w(Je,xe,24)}return xe.slice(0,24)})}let PH=S=>{if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(!Number.isInteger(S.costFactor)||S.costFactor<4||S.costFactor>31)throw new Error("Cost factor should be a number between 4 and 31");if(S.password=B(S.password),S.password.length<1)throw new Error("Password should be at least 1 byte long");if(S.password.length>72)throw new Error("Password should be at most 72 bytes long");if(S.salt=B(S.salt),S.salt.length!==16)throw new Error("Salt should be 16 bytes long");if(S.outputType===void 0&&(S.outputType="encoded"),!["hex","binary","encoded"].includes(S.outputType))throw new Error(`Insupported output type ${S.outputType}. Valid values: ['hex', 'binary', 'encoded']`)};function TH(S){return s(this,void 0,void 0,function*(){return PH(S),FH(S)})}let UH=S=>!(!/^\$2[axyb]\$[0-3][0-9]\$[./A-Za-z0-9]{53}$/.test(S)||S[4]==="0"&&Number(S[5])<4||S[4]==="3"&&Number(S[5])>1),RH=S=>{if(!S||typeof S!="object")throw new Error("Invalid options parameter. It requires an object.");if(S.hash===void 0||typeof S.hash!="string")throw new Error("Hash should be specified");if(S.hash.length!==60)throw new Error("Hash should be 60 bytes long");if(!UH(S.hash))throw new Error("Invalid hash");if(S.password=B(S.password),S.password.length<1)throw new Error("Password should be at least 1 byte long");if(S.password.length>72)throw new Error("Password should be at most 72 bytes long")};function NH(S){return s(this,void 0,void 0,function*(){RH(S);let{hash:q,password:M}=S,Y=yield v(nk,0);Y.writeMemory(B(q),0);let ie=B(M);return Y.writeMemory(ie,60),!!Y.getExports().bcrypt_verify(ie.length)})}var qH="whirlpool",OH="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",MH="8d8f6035",sk={name:qH,data:OH,hash:MH};let LH=new o,K0=null;function HH(S){if(K0===null)return E(LH,sk,64).then(q=>(K0=q,K0.calculate(S)));try{let q=K0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function KH(){return v(sk,64).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:64};return q})}var GH="sm3",zH="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",JH="b6fb4b8e",ok={name:GH,data:zH,hash:JH};let jH=new o,G0=null;function $H(S){if(G0===null)return E(jH,ok,32).then(q=>(G0=q,G0.calculate(S)));try{let q=G0.calculate(S);return Promise.resolve(q)}catch(q){return Promise.reject(q)}}function VH(){return v(ok,32).then(S=>{S.init();let q={init:()=>(S.init(),q),update:M=>(S.update(M),q),digest:M=>S.digest(M),save:()=>S.save(),load:M=>(S.load(M),q),blockSize:64,digestSize:32};return q})}r.adler32=j,r.argon2Verify=vs,r.argon2d=vo,r.argon2i=Ee,r.argon2id=Ji,r.bcrypt=TH,r.bcryptVerify=NH,r.blake2b=qe,r.blake2s=xl,r.blake3=Ri,r.crc32=Ve,r.crc64=Cx,r.createAdler32=z,r.createBLAKE2b=_e,r.createBLAKE2s=hi,r.createBLAKE3=A,r.createCRC32=Ie,r.createCRC64=Ex,r.createHMAC=ik,r.createKeccak=EL,r.createMD4=rL,r.createMD5=cL,r.createRIPEMD160=pH,r.createSHA1=hL,r.createSHA224=DL,r.createSHA256=Wv,r.createSHA3=wL,r.createSHA384=NL,r.createSHA512=ML,r.createSM3=VH,r.createWhirlpool=KH,r.createXXHash128=dH,r.createXXHash3=nH,r.createXXHash32=JL,r.createXXHash64=XL,r.keccak=CL,r.md4=tL,r.md5=aL,r.pbkdf2=Sx,r.ripemd160=gH,r.scrypt=SH,r.sha1=AL,r.sha224=SL,r.sha256=_L,r.sha3=IL,r.sha384=RL,r.sha512=OL,r.sm3=$H,r.whirlpool=HH,r.xxhash128=lH,r.xxhash3=iH,r.xxhash32=zL,r.xxhash64=WL}))});var AP,Dc,ME,$m,hP=J(()=>{"use strict";AP=xr(jm(),1),Dc=32,ME=16,$m=class r{dataKey;constructor(e){this.dataKey=e}static async create(e,t){if(!e)throw new Error("[Crypt] password \u4E0D\u80FD\u4E3A\u7A7A");let i=await(0,AP.scrypt)({password:e,salt:t||"",costFactor:16384,blockSize:8,parallelism:1,hashLength:64,outputType:"binary"}),n=await crypto.subtle.importKey("raw",i.slice(0,32),{name:"AES-CTR"},!1,["encrypt","decrypt"]);return new r(n)}encryptedSize(e){return e+Dc}decryptedSize(e){return Math.max(0,e-Dc)}async encrypt(e){let t=crypto.getRandomValues(new Uint8Array(ME)),i=new Uint8Array(Dc);i.set(t,0),crypto.getRandomValues(i.subarray(ME));let n=new Uint8Array(await crypto.subtle.encrypt({name:"AES-CTR",counter:t,length:128},this.dataKey,e)),s=new Uint8Array(Dc+n.length);return s.set(i,0),s.set(n,Dc),s}async decrypt(e){if(e.length<Dc)throw new Error("[Crypt] \u52A0\u5BC6\u6587\u4EF6\u8FC7\u77ED\uFF08\u7F3A\u5C11\u6587\u4EF6\u5934\uFF09");let t=e.slice(0,ME),i=e.slice(Dc);return new Uint8Array(await crypto.subtle.decrypt({name:"AES-CTR",counter:t,length:128},this.dataKey,i))}}});var Vm,gP=J(()=>{"use strict";Be();hP();Vm=class{addition;cipher=null;remoteDriver=null;remoteRoot="/";suffix;constructor(e){this.addition=e||{},this.suffix=this.addition.encrypted_suffix??".bin"}async init(){let e=this.addition.filename_encryption||"off";if(e!=="off")throw new Error(`[Crypt] filename_encryption='${e}' \u5C1A\u672A\u5B9E\u73B0\uFF0C\u5F53\u524D\u4EC5\u652F\u6301 off \u6A21\u5F0F\uFF08\u5185\u5BB9\u52A0\u5BC6\u3001\u6587\u4EF6\u540D\u4EC5\u52A0\u540E\u7F00\uFF09`);let{resolvePath:t}=await Promise.resolve().then(()=>(Ye(),Jr)),{getDriver:i}=await Promise.resolve().then(()=>(Yt(),Mi)),n=await t(this.addition.remote_path||"/");if(n.isVirtual||!n.storage)throw new Error(`[Crypt] remote_path \u672A\u5339\u914D\u5230\u6709\u6548\u5B58\u50A8: ${this.addition.remote_path}`);this.remoteRoot=n.physical||"/",this.remoteDriver=await i(n.storage.driver,n.storage),this.cipher=await $m.create(this.addition.password,this.addition.salt||"")}ensureReady(){if(!this.cipher||!this.remoteDriver)throw new Error("[Crypt] driver not initialized")}joinRemote(e){let t=String(e||"/");return((this.remoteRoot==="/"?"":this.remoteRoot)+t).replace(/\/{2,}/g,"/")||"/"}decryptName(e){return this.suffix&&e.endsWith(this.suffix)?e.slice(0,-this.suffix.length):e}async list(e,t){this.ensureReady();let i=this.joinRemote(t);return(await this.remoteDriver.list("",i)).map(s=>{if(s.is_dir)return s;let o=this.decryptName(s.name);return{...s,name:o,size:this.cipher.decryptedSize(s.size),type:X(o,!1)}})}async get(e,t){this.ensureReady();let i=this.joinRemote(t),n=await this.remoteDriver.get("",i);if(n.is_dir)return n;let s=this.decryptName(n.name);return{...n,name:s,size:this.cipher.decryptedSize(n.size),type:X(s,!1),raw_url:""}}async mkdir(e,t){this.ensureReady(),await this.remoteDriver.mkdir("",this.joinRemote(t))}async rename(e,t,i){this.ensureReady();let n=this.joinRemote(t);await this.isFile(n)?await this.remoteDriver.rename("",n+this.suffix,i+this.suffix):await this.remoteDriver.rename("",n,i)}async remove(e,t,i){this.ensureReady();let n=this.joinRemote(t);await this.isFile(n)?await this.remoteDriver.remove("",n+this.suffix,i):await this.remoteDriver.remove("",n,i)}async move(e,t,i,n,s){this.ensureReady();let o=this.joinRemote(n),a=this.joinRemote(s);await this.isFile(o)?await this.remoteDriver.move(e,t,i,o+this.suffix,a+this.suffix):await this.remoteDriver.move(e,t,i,o,a)}async copy(e,t,i,n,s){this.ensureReady();let o=this.joinRemote(n),a=this.joinRemote(s);await this.isFile(o)?await this.remoteDriver.copy(e,t,i,o+this.suffix,a+this.suffix):await this.remoteDriver.copy(e,t,i,o,a)}async put(e,t,i){this.ensureReady();let n=this.joinRemote(t)+this.suffix,s=await this.cipher.encrypt(new Uint8Array(i));await this.remoteDriver.put("",n,Buffer.from(s))}async createReadStream(e,t){this.ensureReady();let i=this.joinRemote(e)+this.suffix,n=await this.remoteDriver.get("",i);if(!n.raw_url)throw new Error("[Crypt] \u5E95\u5C42\u9A71\u52A8\u672A\u8FD4\u56DE\u4E0B\u8F7D\u94FE\u63A5\uFF0C\u65E0\u6CD5\u89E3\u5BC6\u4E0B\u8F7D");let s=await fetch(n.raw_url);if(!s.ok)throw new Error(`[Crypt] \u5E95\u5C42\u4E0B\u8F7D\u5931\u8D25: HTTP ${s.status}`);let o=new Uint8Array(await s.arrayBuffer()),a=await this.cipher.decrypt(o),c=t?.start??0,l=t?.end??a.length-1,d=a.slice(c,l+1);return new ReadableStream({start(u){u.enqueue(d),u.close()}})}async isFile(e){if(!this.suffix)return!1;try{let t=await this.remoteDriver.get("",e+this.suffix);return!!t&&!t.is_dir}catch{return!1}}}});var Ym,pP=J(()=>{"use strict";Be();Ym=class{addition;constructor(e){this.addition=e||{}}randSize(){let e=this.addition.min_file_size||0,t=this.addition.max_file_size||0;return t<=e?e:e+Math.floor(Math.random()*(t-e))}now(){return new Date().toISOString()}async list(){let e=[];for(let t=0;t<(this.addition.num_file||0);t++){let i=`file_${t}.bin`;e.push({name:i,size:this.randSize(),is_dir:!1,modified:this.now(),sign:"",type:X(i,!1)})}for(let t=0;t<(this.addition.num_folder||0);t++){let i=`folder_${t}`;e.push({name:i,size:0,is_dir:!0,modified:this.now(),sign:"",type:1})}return e}async get(e,t){let i=String(t||"").split("/").filter(Boolean),n=i[i.length-1]||"file.bin";return{name:n,size:this.randSize(),is_dir:!1,modified:this.now(),sign:"",type:X(n,!1),raw_url:""}}async mkdir(){}async rename(){}async remove(){}async move(){}async copy(){}async put(){}async createReadStream(e,t){let i=t?.start??0,n=t?.end??i+this.randSize()-1,s=Math.max(0,n-i+1),o=64*1024,a=s;return new ReadableStream({pull(c){if(a<=0){c.close();return}let l=Math.min(o,a),d=new Uint8Array(l);crypto.getRandomValues(d),c.enqueue(d),a-=l}})}}});var Wm,mP=J(()=>{"use strict";Be();He();Wm=class{addition;token;onTokenUpdate;constructor(e,t){this.addition=e||{},this.token=this.addition.token||"",this.onTokenUpdate=t}get baseUrl(){return(this.addition.url||"").replace(/\/+$/,"")}async init(){if(!this.baseUrl)throw new Error("[AListV3] url is required");try{let e=await this.request("/me","GET");this.addition.username&&e.username!==this.addition.username&&await this.login()}catch{this.addition.username&&await this.login()}}async login(){if(!this.addition.username)return;let e=await this.request("/auth/login","POST",{username:this.addition.username,password:this.addition.password},!1);if(this.token=e.token,this.onTokenUpdate)try{await this.onTokenUpdate(this.token)}catch{}}async request(e,t,i,n=!0){let s=await fetch(`${this.baseUrl}/api${e}`,{method:t,headers:{"Content-Type":"application/json",...this.token?{Authorization:this.token}:{}},...i!==void 0?{body:JSON.stringify(i)}:{}}),o=await s.json().catch(()=>({code:s.status,message:"invalid response",data:null}));if(o.code!==200){if((o.code===401||o.code===403)&&n)return await this.login(),this.request(e,t,i,!1);throw new Error(`[AListV3] request failed: code=${o.code}, message=${o.message}`)}return o.data}toFileItem(e){return{name:e.name,size:e.size||0,is_dir:!!e.is_dir,modified:e.modified||new Date().toISOString(),sign:e.sign||"",thumb:e.thumb||"",type:e.type??X(e.name,!!e.is_dir),raw_url:""}}async list(e,t){let n=((await this.request("/fs/list","POST",{path:t,password:this.addition.meta_password||"",page:1,per_page:0,refresh:!1})).content||[]).map(s=>this.toFileItem(s));return de(n,"name","asc")}async get(e,t){let i=await this.request("/fs/get","POST",{path:t,password:this.addition.meta_password||""});return{...this.toFileItem(i),raw_url:i.raw_url||""}}async mkdir(e,t){await this.request("/fs/mkdir","POST",{path:t})}async rename(e,t,i){await this.request("/fs/rename","POST",{path:t,name:i})}async remove(e,t,i){let n=t.split("/").slice(0,-1).join("/")||"/";await this.request("/fs/remove","POST",{dir:n,names:i})}async move(e,t,i,n,s){let o=n.split("/").slice(0,-1).join("/")||"/",a=s.split("/").slice(0,-1).join("/")||"/";await this.request("/fs/move","POST",{src_dir:o,dst_dir:a,names:i})}async copy(e,t,i,n,s){let o=n.split("/").slice(0,-1).join("/")||"/",a=s.split("/").slice(0,-1).join("/")||"/";await this.request("/fs/copy","POST",{src_dir:o,dst_dir:a,names:i})}async put(e,t,i){let n=await fetch(`${this.baseUrl}/api/fs/put`,{method:"PUT",headers:{Authorization:this.token,"File-Path":encodeURIComponent(t),"Content-Type":"application/octet-stream"},body:i}),s=await n.json().catch(()=>({code:n.status,message:"invalid response",data:null}));if(s.code!==200)throw new Error(`[AListV3] put failed: code=${s.code}, message=${s.message}`)}}});function yP(r){let e=r.indexOf("http://"),t=r.indexOf("https://"),i=e>=0?e:t;if(i<0)throw new Error(`invalid line: ${r}, because url is required for file`);let n=r.slice(i),s=r.slice(0,i),o={url:n,name:"",level:0,modified:0,size:0,children:[]};if(i>0){if(!s.endsWith(":"))throw new Error(`invalid line: ${r}, file info must end with ':'`);let a=s.slice(0,-1);if(!a)throw new Error(`invalid line: ${r}, file name can't be empty`);let c=a.split(":");if(o.name=c[0],c.length>1){let l=parseInt(c[1],10);if(Number.isNaN(l))throw new Error(`invalid line: ${r}, file size must be an integer`);if(o.size=l,c.length>2){let d=parseInt(c[2],10);if(Number.isNaN(d))throw new Error(`invalid line: ${r}, modified must be a unix timestamp`);o.modified=d}}}else{let c=new URL(n).pathname.split("/").filter(Boolean);o.name=c[c.length-1]||n}return o}function oV(r){let e={url:"",name:"root",level:-1,modified:0,size:0,children:[]},t=[e],i=String(r||"").split(` -`);for(let n of i){let s=0;for(;s<n.length&&n[s]===" ";)s++;if(s%2!==0)throw new Error(`the line '${n}' indent is not a multiple of 2`);let o=s/2,a=n.slice(s).trim();if(a){for(;o<=t[t.length-1].level;)t.pop();if(a.endsWith(":")){let c={url:"",name:a.slice(0,-1),level:o,modified:0,size:0,children:[]};t[t.length-1].children.push(c),t.push(c)}else{let c=yP(a);c.level=o,t[t.length-1].children.push(c)}}}return e}function $l(r){if(r.url)return r.size;let e=0;for(let t of r.children)e+=$l(t);return r.size=e,e}function aV(r){let e=t=>{if(t.level===-1)return t.children.map(e).join(` -`);let i=" ".repeat(t.level);if(!t.url){let a=t.children.map(e).join(` -`);return`${i}${t.name}:${a?` -`+a:""}`}let s=new URL(t.url).pathname.split("/").filter(Boolean).pop()||"";if(t.size===0&&t.modified===0)return`${i}${s===t.name?t.url:`${t.name}:${t.url}`}`;let o=t.name;return(t.size!==0||t.modified!==0)&&(o+=`:${t.size}`),t.modified!==0&&(o+=`:${t.modified}`),`${i}${o}:${t.url}`};return e(r)}function sa(r){return r==="/"||!r?["root"]:["root",...r.replace(/\/+$/,"").split("/").filter(Boolean)]}function Xn(r,e){let t=sa(e),i=r;for(let n=0;n<t.length;n++){if(!i||i.name!==t[n])return null;if(n===t.length-1)return i;let s=null;for(let o of i.children)if(o.name===t[n+1]){s=o;break}i=s}return i}async function cV(r){try{jr(r,"UrlTree head_size");let e=await fetch(r,{method:"HEAD"});if(!e.ok)return 0;let t=parseInt(e.headers.get("Content-Length")||"0",10);return Number.isFinite(t)?t:0}catch{return 0}}var Xm,IP=J(()=>{"use strict";Be();He();xc();Xm=class{addition;root;onUpdate;constructor(e,t){this.addition=e||{},this.root=oV(this.addition.url_structure||""),$l(this.root),this.onUpdate=t}async init(){if(!this.addition.url_structure||!this.addition.url_structure.trim())throw new Error("[UrlTree] url_structure is required")}persist(){if(this.addition.writable&&(this.addition.url_structure=aV(this.root),this.onUpdate))try{this.onUpdate(this.addition.url_structure)}catch{}}nodeToFileItem(e,t){let i=!e.url;return{name:e.name,size:e.size,is_dir:i,modified:e.modified?new Date(e.modified*1e3).toISOString():new Date().toISOString(),sign:"",thumb:"",type:X(e.name,i),raw_url:i?"":e.url}}async list(e,t){let i=Xn(this.root,t);if(!i)throw new Error(`[UrlTree] path not found: ${t}`);if(i.url)throw new Error(`[UrlTree] not a folder: ${t}`);let n=i.children.map(s=>this.nodeToFileItem(s,`${t.replace(/\/+$/,"")}/${s.name}`));return de(n,"name","asc")}async get(e,t){let i=Xn(this.root,t);if(!i)throw new Error(`[UrlTree] path not found: ${t}`);return this.nodeToFileItem(i,t)}async mkdir(e,t){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let i=sa(t),n=i[i.length-1],s=Xn(this.root,"/"+i.slice(1,-1).join("/"));if(!s||s.url)throw new Error("[UrlTree] parent not found");s.children.push({url:"",name:n,level:s.level+1,modified:0,size:0,children:[]}),this.persist()}async rename(e,t,i){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let n=Xn(this.root,t);if(!n)throw new Error("[UrlTree] path not found");n.name=i,this.persist()}async remove(e,t,i){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let n=sa(t),s=n[n.length-1],o=Xn(this.root,"/"+n.slice(1,-1).join("/"));if(!o)throw new Error("[UrlTree] parent not found");o.children=o.children.filter(a=>a.name!==s),$l(this.root),this.persist()}async move(e,t,i,n,s){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let o=Xn(this.root,"/"+sa(n).slice(1,-1).join("/")),a=Xn(this.root,"/"+sa(s).slice(1,-1).join("/"));if(!o||!a||a.url)throw new Error("[UrlTree] invalid path");for(let c of i){let l=o.children.findIndex(u=>u.name===c);if(l<0)continue;let[d]=o.children.splice(l,1);d.level=a.level+1,a.children.push(d)}$l(this.root),this.persist()}async copy(e,t,i,n,s){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let o=Xn(this.root,"/"+sa(n).slice(1,-1).join("/")),a=Xn(this.root,"/"+sa(s).slice(1,-1).join("/"));if(!o||!a||a.url)throw new Error("[UrlTree] invalid path");for(let c of i){let l=o.children.find(d=>d.name===c);l&&a.children.push(this.deepCopy(l,a.level+1))}$l(this.root),this.persist()}async put(e,t,i){if(!this.addition.writable)throw new Error("[UrlTree] not writable");let n=i.toString("utf8").trim(),s=Xn(this.root,"/"+sa(t).slice(1,-1).join("/"));if(!s||s.url)throw new Error("[UrlTree] parent not found");let o=t.split("/").filter(Boolean).pop()||"file",a=yP(`${o}:${n}`);a.level=s.level+1,s.children.push(a),this.addition.head_size&&(a.size=await cV(a.url)),$l(this.root),this.persist()}deepCopy(e,t){return{url:e.url,name:e.name,level:t,modified:e.modified,size:e.size,children:e.children.map(i=>this.deepCopy(i,t+1))}}}});function pf(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:"/"+i}function wP(r){let e=r.lastIndexOf("/");return e>0?r.slice(0,e):"/"}function xP(r){return r.split("/").filter(Boolean).pop()||""}function lV(r){if(r.includes(":")){let t=r.indexOf(":"),i=r.slice(0,t),n=r.slice(t+1);if(!i.includes("/"))return[i,n]}let e=r.split("/").filter(Boolean);return[e[e.length-1]||r,r]}function CP(r){let e=String(r||"/").replace(/^\//,""),t=e.indexOf("/");return t<0?[e,""]:[e.slice(0,t),e.slice(t+1)]}var Zm,EP=J(()=>{"use strict";Be();He();Zm=class{addition;pathMap=new Map;remotes=new Map;supportSuffix=new Set;downloadSuffix=new Set;autoFlatten=!1;oneKey="";constructor(e){this.addition=e||{}}async init(){let e=this.addition.paths||"";if(!e.trim())throw new Error("[Strm] paths is required");for(let o of e.split(` -`)){let a=o.trim();if(!a)continue;let[c,l]=lV(a);this.pathMap.has(c)||this.pathMap.set(c,[]),this.pathMap.get(c).push(l)}this.pathMap.size===1&&(this.autoFlatten=!0,this.oneKey=this.pathMap.keys().next().value??"");let t=(this.addition.filterFileTypes||"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac").split(",").map(o=>o.trim().toLowerCase()).filter(Boolean);this.supportSuffix=new Set(t);let i=(this.addition.downloadFileTypes||"ass,srt,vtt,sub,strm").split(",").map(o=>o.trim().toLowerCase()).filter(Boolean);this.downloadSuffix=new Set(i);let{resolvePath:n}=await Promise.resolve().then(()=>(Ye(),Jr)),{getDriver:s}=await Promise.resolve().then(()=>(Yt(),Mi));for(let o of this.pathMap.values())for(let a of o)try{let c=await n(a);if(!c.isVirtual&&c.storage){let l=await s(c.storage.driver,c.storage);this.remotes.set(a,{driver:l,physical:c.physical||"/"})}}catch(c){console.warn(`[Strm] failed to resolve remote path '${a}':`,c)}}encodePath(e){return e.split("/").map(t=>encodeURIComponent(t)).join("/")}getLink(e){let t=e;this.addition.encodePath&&(t=this.encodePath(e));let i=this.addition.PathPrefix||"/d";return t=pf(i,t),t.startsWith("/")||(t="/"+t),this.addition.withoutUrl?t:`${(this.addition.siteUrl||"").replace(/\/+$/,"")}${t}`}ext(e){let t=e.lastIndexOf(".");return t>=0?e.slice(t+1).toLowerCase():""}async listRemote(e,t){let i=this.remotes.get(e);if(!i)return[];let n=pf(i.physical,t);try{return await i.driver.list("",n)}catch{return[]}}convert(e,t){let i=[];for(let n of t){if(n.is_dir){i.push(n);continue}let s=this.ext(n.name),o=pf(e,n.name);if(this.downloadSuffix.has(s))i.push({...n,size:n.size});else if(this.supportSuffix.has(s)){let a=n.name.replace(/\.[^.]+$/,"")+".strm";i.push({name:a,size:new TextEncoder().encode(this.getLink(o)).length,is_dir:!1,modified:n.modified,sign:o,thumb:n.thumb||"",type:X(a,!1),raw_url:""})}}return i}async list(e,t){let i=t||"/";if(i==="/"&&!this.autoFlatten){let l=[];for(let d of this.pathMap.keys())l.push({name:d,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""});return l}let{root:n,sub:s}=this.autoFlatten?{root:this.oneKey,sub:i.replace(/^\//,"")}:(()=>{let[l,d]=CP(i);return{root:l,sub:d}})(),o=this.pathMap.get(n);if(!o)throw new Error(`[Strm] path not found: ${i}`);let a=[],c=new Set;for(let l of o){if(!this.remotes.get(l))continue;let u=pf(l,s),f=await this.listRemote(l,s);for(let h of this.convert(u,f))c.has(h.name)||(c.add(h.name),a.push(h))}return de(a,"name","asc")}async get(e,t){let i=t||"/";if(i.endsWith(".strm")){let a=wP(i),c=xP(i),d=(await this.list("",a)).find(u=>u.name===c);if(!d)throw new Error(`[Strm] not found: ${i}`);return d}let[n,s]=CP(i),o=this.pathMap.get(n);if(!o)throw new Error(`[Strm] path not found: ${i}`);for(let a of o){let c=this.remotes.get(a);if(!c)continue;let l=pf(c.physical,s);try{let d=await c.driver.get("",l);if(d)return d}catch{}}throw new Error(`[Strm] not found: ${i}`)}async mkdir(){throw new Error("[Strm] mkdir is not supported")}async rename(){throw new Error("[Strm] rename is not supported")}async remove(){throw new Error("[Strm] remove is not supported")}async move(){throw new Error("[Strm] move is not supported")}async copy(){throw new Error("[Strm] copy is not supported")}async put(){throw new Error("[Strm] put is not supported")}async createReadStream(e){let t=e||"/",i=wP(t),n=xP(t),o=(await this.list("",i)).find(l=>l.name===n);if(!o||!o.sign)throw new Error(`[Strm] not found: ${t}`);let a=this.getLink(o.sign),c=new TextEncoder().encode(a);return new ReadableStream({start(l){l.enqueue(c),l.close()}})}}});function oa(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:"/"+i}function LE(r){let e=r.lastIndexOf("/");return e>0?r.slice(0,e):"/"}function Vl(r){return r.split("/").filter(Boolean).pop()||""}var ey,BP=J(()=>{"use strict";Be();ey=class{addition;remoteDriver=null;remoteRoot="/";chunkPrefix;customExt;partSize;constructor(e){this.addition=e||{},this.chunkPrefix=this.addition.chunk_prefix||"[openlist_chunk]",this.customExt=this.addition.custom_ext||"",this.partSize=Number(this.addition.part_size)||0}async init(){if(this.partSize<=0)throw new Error("[Chunk] part_size must be positive");if(!this.chunkPrefix)throw new Error("[Chunk] chunk_prefix must not be empty");let{resolvePath:e}=await Promise.resolve().then(()=>(Ye(),Jr)),{getDriver:t}=await Promise.resolve().then(()=>(Yt(),Mi)),i=await e(this.addition.remote_path||"/");if(i.isVirtual||!i.storage)throw new Error(`[Chunk] remote_path \u672A\u5339\u914D\u5230\u6709\u6548\u5B58\u50A8: ${this.addition.remote_path}`);this.remoteRoot=i.physical||"/",this.remoteDriver=await t(i.storage.driver,i.storage)}ensureReady(){if(!this.remoteDriver)throw new Error("[Chunk] driver not initialized")}joinRemote(e){let t=String(e||"/");return((this.remoteRoot==="/"?"":this.remoteRoot)+t).replace(/\/{2,}/g,"/")||"/"}getPartName(e){return`${e}${this.customExt}`}chunkDirRemote(e){let t=LE(this.joinRemote(e)),i=Vl(e);return oa(t,this.chunkPrefix+i)}async listParts(e){this.ensureReady();let t=this.chunkDirRemote(e),i=await this.remoteDriver.list("",t),n=[];for(let s of i){if(s.is_dir)continue;let o=parseInt(s.name.replace(new RegExp(`${this.customExt}$`),""),10);Number.isNaN(o)||(n[o]={name:s.name,size:s.size,modified:s.modified,raw_url:s.raw_url||"",raw_url_headers:s.raw_url_headers})}return n.filter(s=>!!s)}async totalChunkSize(e){return(await this.listParts(e)).reduce((i,n)=>i+n.size,0)}async list(e,t){this.ensureReady();let i=this.joinRemote(t),n=await this.remoteDriver.list("",i),s=[],o=[];for(let a of n){if(a.is_dir&&a.name.startsWith(this.chunkPrefix)){let c=a.name.slice(this.chunkPrefix.length),l=s.length;s.push(null),o.push((async()=>{let d=oa(t,a.name),u=await this.totalChunkSize(d);s[l]={name:c,size:u,is_dir:!1,modified:a.modified,sign:this.chunkPrefix+c,type:X(c,!1),raw_url:""}})());continue}!this.addition.show_hidden&&a.name.startsWith(".")||s.push(a)}return await Promise.all(o),s.filter(a=>!!a)}async get(e,t){this.ensureReady();let i=this.joinRemote(t);try{let a=await this.remoteDriver.get("",i);if(a&&!a.is_dir||a)return a}catch{}let n=Vl(t),s=await this.totalChunkSize(t),o=await this.listParts(t);return{name:n,size:s,is_dir:!1,modified:o[0]?.modified||new Date().toISOString(),sign:this.chunkPrefix+n,type:X(n,!1),raw_url:""}}async mkdir(e,t){this.ensureReady(),await this.remoteDriver.mkdir("",this.joinRemote(t))}async rename(e,t,i){this.ensureReady();let n=this.joinRemote(t);try{await this.remoteDriver.get("",n),await this.remoteDriver.rename("",n,i)}catch{let s=LE(n),o=oa(s,this.chunkPrefix+Vl(t));await this.remoteDriver.rename("",o,this.chunkPrefix+i)}}async remove(e,t,i){this.ensureReady();let n=this.joinRemote(t);try{await this.remoteDriver.get("",n),await this.remoteDriver.remove("",n,i)}catch{await this.remoteDriver.remove("",this.chunkDirRemote(t),i)}}async move(e,t,i,n,s){this.ensureReady();let o=this.joinRemote(n),a=this.joinRemote(s);try{await this.remoteDriver.get("",o),await this.remoteDriver.move(e,t,i,o,a)}catch{await this.remoteDriver.move(e,t,i,this.chunkDirRemote(n),oa(a,this.chunkPrefix+Vl(n)))}}async copy(e,t,i,n,s){this.ensureReady();let o=this.joinRemote(n),a=this.joinRemote(s);try{await this.remoteDriver.get("",o),await this.remoteDriver.copy(e,t,i,o,a)}catch{await this.remoteDriver.copy(e,t,i,this.chunkDirRemote(n),oa(a,this.chunkPrefix+Vl(n)))}}async put(e,t,i){this.ensureReady();let n=i.length,s=this.joinRemote(t);if(this.addition.chunk_large_file_only&&n<=this.partSize){await this.remoteDriver.put("",s,i);return}if(n<=this.partSize){await this.remoteDriver.put("",s,i);return}let o=this.chunkDirRemote(t);try{await this.remoteDriver.mkdir("",o)}catch{}let a=0,c=0;for(;a<n;){let l=Math.min(a+this.partSize,n),d=i.subarray(a,l),u=oa(o,this.getPartName(c));await this.remoteDriver.put("",u,Buffer.from(d)),a=l,c++}}async createReadStream(e,t){this.ensureReady();let i=await this.listParts(e),n=i.reduce((d,u)=>d+u.size,0),s=this.remoteDriver,o=this.chunkPrefix,a=t?.start??0,c=t?t.end-t.start+1:n;return new ReadableStream({start:async d=>{for(let u of i){let f=u.size;if(a>=f){a-=f;continue}let h=u.raw_url;if(!h){let m=LE(e),w=oa(oa(m,o+Vl(e)),u.name);try{h=(await s.get("",w)).raw_url||""}catch{h=""}}if(!h){d.error(new Error(`[Chunk] failed to resolve download url for ${u.name}`));return}let g=await fetch(h,{headers:u.raw_url_headers||{}});if(!g.ok){d.error(new Error(`[Chunk] download part ${u.name} failed: HTTP ${g.status}`));return}let I=new Uint8Array(await g.arrayBuffer());if(a>0&&(I=I.subarray(a),a=0),c>=0&&I.length>c&&(I=I.subarray(0,c)),I.length>0&&d.enqueue(I),c>=0&&(c-=I.length,c<=0))break}d.close()}})}}});function dV(r){let e=r.replace(/^https?:\/\//,""),t=e.indexOf(".");return(t>=0?e.slice(0,t):e).toLowerCase()}function bP(r){return r.endsWith("/")?r:r+"/"}function mf(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:i}function uV(r){let e=atob(r),t=new Uint8Array(e.length);for(let i=0;i<e.length;i++)t[i]=e.charCodeAt(i);return t}function fV(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e)}async function vP(r,e){let t=await crypto.subtle.importKey("raw",e,{name:"HMAC",hash:"SHA-256"},!1,["sign"]),i=await crypto.subtle.sign("HMAC",t,new TextEncoder().encode(r));return fV(new Uint8Array(i))}function AV(r){let e={blobs:[],prefixes:[]},t=/<Blob>([\s\S]*?)<\/Blob>/g,i=/<BlobPrefix>[\s\S]*?<Name>([^<]+)<\/Name>[\s\S]*?<\/BlobPrefix>/g,n;for(;n=t.exec(r);){let s=n[1],o=(s.match(/<Name>([^<]+)<\/Name>/)||[])[1]||"";if(o.endsWith("/"))continue;let a=parseInt((s.match(/<Content-Length>(\d+)<\/Content-Length>/)||[])[1]||"0",10),c=(s.match(/<Last-Modified>([^<]+)<\/Last-Modified>/)||[])[1]||"";e.blobs.push({name:o,size:a,modified:c?new Date(c).toISOString():new Date().toISOString()})}for(;n=i.exec(r);)e.prefixes.push(n[1]);return e}var ty,kP=J(()=>{"use strict";Be();He();ty=class{addition;accountName;keyBytes;endpoint;container;constructor(e){this.addition=e||{},this.accountName=dV(this.addition.endpoint||""),this.keyBytes=uV(this.addition.access_key||""),this.endpoint=(this.addition.endpoint||"").replace(/\/+$/,""),this.container=(this.addition.container_name||"").replace(/^\/+|\/+$/g,"")}async init(){if(!/^[a-z0-9]+$/.test(this.accountName))throw new Error("[AzureBlob] invalid storage account name");if(!this.container)throw new Error("[AzureBlob] container_name is required");if(!this.addition.access_key)throw new Error("[AzureBlob] access_key is required")}async sharedKeySignature(e,t,i,n,s,o){let a=n["x-ms-date"]||"",c=Object.keys(n).filter(u=>u.startsWith("x-ms-")).sort().map(u=>`${u.toLowerCase()}:${n[u].trim()} -`).join(""),l=`/${this.accountName}${t?"/"+t:""} -${i.split("&").filter(Boolean).sort().map(u=>{let[f,h]=u.split("=");return`${f.toLowerCase()}:${h||""}`}).join(` -`)}`,d=`${e} - - -${s} - -${o} - - - - - - -${c}${l}`;return vP(d,this.keyBytes)}async request(e,t,i,n={}){let s=new Date().toUTCString(),o=n.contentType||"",a=n.body?String(n.body.length):"",c={"x-ms-date":s,"x-ms-version":"2021-08-06",...n.extraHeaders||{}},l=await this.sharedKeySignature(e,t,i,c,a,o),d=`${this.endpoint}/${this.container}${t?"/"+t:""}${i?"?"+i:""}`,u={Authorization:`SharedKey ${this.accountName}:${l}`,...c};return a&&(u["Content-Length"]=a),o&&(u["Content-Type"]=o),fetch(d,{method:e,headers:u,...n.body?{body:n.body}:{}})}async sasUrl(e){let i=new Date(Date.now()+(this.addition.sign_url_expire||4)*3600*1e3).toISOString().replace(/\.\d+Z$/,"Z"),n=`/blob/${this.accountName}/${this.container}/${e}`,s=`r - - -${i} -${n} - - - - - - - - -`,o=encodeURIComponent(await vP(s,this.keyBytes));return`${this.endpoint}/${this.container}/${e}?sp=r&st=&se=${encodeURIComponent(i)}&sv=2021-08-06&sr=b&sig=${o}`}getKey(e){return String(e||"/").replace(/^\/+/,"")}async list(e,t){let i=this.getKey(t);i&&(i=bP(i));let n=`restype=container&comp=list&delimiter=%2F&prefix=${encodeURIComponent(i)}`,s=await this.request("GET","",n);if(!s.ok)throw new Error(`[AzureBlob] list failed: HTTP ${s.status}`);let o=await s.text(),{blobs:a,prefixes:c}=AV(o),l=[];for(let d of c){let u=d.replace(/\/+$/,"").split("/").filter(Boolean).pop()||d;l.push({name:u,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1,raw_url:""})}for(let d of a){let u=d.name.split("/").filter(Boolean).pop()||d.name;l.push({name:u,size:d.size,is_dir:!1,modified:d.modified,sign:d.name,type:X(u,!1),raw_url:""})}return de(l,"name","asc")}async get(e,t){let i=this.getKey(t),n=i.split("/").filter(Boolean).pop()||i,s=await this.sasUrl(i);return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:i,type:X(n,!1),raw_url:s}}async mkdir(e,t){let i=bP(this.getKey(t)),n=await this.request("PUT",i,"",{body:new Uint8Array(0),contentType:"application/octet-stream",extraHeaders:{"x-ms-blob-type":"BlockBlob","x-ms-meta-hdi_isfolder":"true"}});if(!n.ok)throw new Error(`[AzureBlob] mkdir failed: HTTP ${n.status}`)}async rename(e,t,i){let n=this.getKey(t),s=mf(t.split("/").slice(0,-1).join("/"),i).replace(/^\/+/,"");await this.copyBlob(n,s),await this.deleteBlob(n)}async remove(e,t,i){let n=this.getKey(t);await this.deleteBlob(n)}async move(e,t,i,n,s){for(let o of i){let a=mf(this.getKey(n),o),c=mf(this.getKey(s),o);await this.copyBlob(a,c),await this.deleteBlob(a)}}async copy(e,t,i,n,s){for(let o of i){let a=mf(this.getKey(n),o),c=mf(this.getKey(s),o);await this.copyBlob(a,c)}}async put(e,t,i){let n=this.getKey(t),s=await this.request("PUT",n,"",{body:new Uint8Array(i),contentType:"application/octet-stream",extraHeaders:{"x-ms-blob-type":"BlockBlob"}});if(!s.ok)throw new Error(`[AzureBlob] put failed: HTTP ${s.status}`)}async copyBlob(e,t){let i=`${this.endpoint}/${this.container}/${e}`,n=await this.request("PUT",t,"",{body:new Uint8Array(0),contentType:"application/octet-stream",extraHeaders:{"x-ms-blob-type":"BlockBlob","x-ms-copy-source":i}});if(n.status!==201&&n.status!==202&&!n.ok)throw new Error(`[AzureBlob] copy failed: HTTP ${n.status}`)}async deleteBlob(e){let t=await this.request("DELETE",e,"");if(t.status!==202&&t.status!==404&&!t.ok)throw new Error(`[AzureBlob] delete failed: HTTP ${t.status}`)}}});function yf(r,e){let t=String(r||"").replace(/\/+$/,""),i=String(e||"").replace(/^\/+/,"");return t?i?t+"/"+i:t:i}var ry,SP=J(()=>{"use strict";Be();He();Gr();ry=class{addition;authHeader;apiBase;constructor(e){this.addition=e||{};let t=this.addition.operator_name||"",i=this.addition.operator_password||"";this.authHeader=`Basic ${btoa(`${t}:${i}`)}`,this.apiBase=`https://v0.api.upyun.com/${this.addition.bucket||""}`}async init(){if(!this.addition.bucket||!this.addition.operator_name||!this.addition.operator_password)throw new Error("[USS] bucket / operator_name / operator_password are required")}getKey(e,t=!1){let i=String(e||"/").replace(/^\/+/,"");return t&&i&&!i.endsWith("/")&&(i+="/"),i}getDownloadHost(){let e=this.addition.endpoint||"";return/^https?:\/\//.test(e)||(e="https://"+e),e.replace(/\/+$/,"")}async list(e,t){let i=this.getKey(t,!0),n=`${this.apiBase}/${i}`,s=await fetch(n,{method:"GET",headers:{Authorization:this.authHeader,Accept:"application/json"}});if(!s.ok)throw new Error(`[USS] list failed: HTTP ${s.status}`);let o=await s.text(),a=[];try{let c=JSON.parse(o);if(Array.isArray(c)){for(let l of c){let d=l.name||l.file_name||"",u=l.type==="F"||l.is_dir===!0||d.endsWith("/");a.push({name:d.replace(/\/+$/,""),size:l.length||l.size||0,is_dir:u,modified:l.last_modified?new Date(l.last_modified*1e3).toISOString():new Date().toISOString(),sign:"",type:X(d,u),raw_url:""})}return de(a,"name","asc")}}catch{}for(let c of o.split(` -`)){let l=c.trim();if(!l)continue;let d=l.split(" "),u=(d[0]||"").trim(),h=(d[1]||"").trim()==="F"||u.endsWith("/");a.push({name:u.replace(/\/+$/,""),size:parseInt(d[2]||"0",10)||0,is_dir:h,modified:d[3]?new Date(parseInt(d[3],10)*1e3).toISOString():new Date().toISOString(),sign:"",type:X(u,h),raw_url:""})}return de(a,"name","asc")}async get(e,t){let i=this.getKey(t),n=i.split("/").filter(Boolean).pop()||i,s=Math.floor(Date.now()/1e3)+(this.addition.sign_url_expire||4)*3600,a=`${this.addition.anti_theft_chain_token||this.addition.operator_password}&${s}&/${i}`,c=$t(a).slice(12,20)+s,l=encodeURIComponent(n),d=`${this.getDownloadHost()}/${i}?_upd=${l}&_upt=${c}`;return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:i,type:X(n,!1),raw_url:d}}async mkdir(e,t){let i=this.getKey(t,!0),n=await fetch(`${this.apiBase}/${i}`,{method:"POST",headers:{Authorization:this.authHeader,Folder:"true"}});if(!n.ok)throw new Error(`[USS] mkdir failed: HTTP ${n.status}`)}async rename(e,t,i){let n=this.getKey(t),s=yf(t.split("/").slice(0,-1).join("/"),i).replace(/^\/+/,"");await this.moveOrCopy(n,s,"move")}async remove(e,t,i){let n=this.getKey(t),s=await fetch(`${this.apiBase}/${n}`,{method:"DELETE",headers:{Authorization:this.authHeader}});if(!s.ok)throw new Error(`[USS] remove failed: HTTP ${s.status}`)}async move(e,t,i,n,s){for(let o of i){let a=yf(this.getKey(n),o),c=yf(this.getKey(s),o);await this.moveOrCopy(a,c,"move")}}async copy(e,t,i,n,s){for(let o of i){let a=yf(this.getKey(n),o),c=yf(this.getKey(s),o);await this.moveOrCopy(a,c,"copy")}}async put(e,t,i){let n=this.getKey(t),s=await fetch(`${this.apiBase}/${n}`,{method:"PUT",headers:{Authorization:this.authHeader,"Content-Type":"application/octet-stream"},body:i});if(!s.ok)throw new Error(`[USS] put failed: HTTP ${s.status}`)}async moveOrCopy(e,t,i){let n=i==="move"?"X-Upyun-Move-Source":"X-Upyun-Copy-Source",s=await fetch(`${this.apiBase}/${t}`,{method:"PUT",headers:{Authorization:this.authHeader,[n]:`/${e}`}});if(!s.ok)throw new Error(`[USS] ${i} failed: HTTP ${s.status}`)}}});function hV(r){return r?r.message?r.message:r.msg?r.msg:"request failed":"request failed"}var Js,HE,iy,DP=J(()=>{"use strict";Js="https://alidocs.dingtalk.com",HE="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36";iy=class{constructor(e){this.cookie=e}cookie;headers(){return{Cookie:this.cookie,Accept:"application/json, text/plain, */*",Referer:Js+"/",Origin:Js,"User-Agent":HE}}async request(e,t){let i=await fetch(e,{...t,headers:{...this.headers(),...t?.headers||{}}}),n=await i.json().catch(()=>({}));if(!i.ok||n.isSuccess===!1)throw new Error(hV(n));return n}async checkCookie(){await this.request(`${Js}/portal/api/v1/mine/info`)}async list(e){let t=new URLSearchParams({dentryUuid:e,withParentAncestors:"true",orderType:"SORT_KEY",sortType:"desc",listDentrySource:"2",pageSize:"1000"});return(await this.request(`${Js}/box/api/v2/dentry/list?${t.toString()}`)).data?.children||[]}async download(e){let t=new URLSearchParams({dentryUuid:e,version:"1",supportDownloadTypes:"URL_PRE_SIGNATURE,HTTP_TO_CENTER",downloadType:"URL_PRE_SIGNATURE"}),n=(await this.request(`${Js}/box/api/v2/file/download?${t.toString()}`)).data?.ossUrlPreSignatureInfo?.preSignUrls||[];if(n.length===0)throw new Error("empty download url");return n[0]}async post(e,t){await this.request(`${Js}${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})}}});function gV(r){let e={...r||{}};return e.cookie=(e.cookie||"").trim(),e.root_folder_id=(e.root_folder_id||"").trim(),e}function QP(r){let e=r.dentryType==="folder";return{name:r.name,size:e?0:r.fileSize||0,is_dir:e,modified:r.updatedTime?new Date(r.updatedTime).toISOString():new Date().toISOString(),created:r.createdTime?new Date(r.createdTime).toISOString():void 0,sign:r.dentryUuid,type:X(r.name,e)}}var ny,_P=J(()=>{"use strict";Be();He();DP();ny=class{addition;client;idCache=new Map;constructor(e){this.addition=gV(e),this.client=new iy(this.addition.cookie)}async init(){if(!this.addition.cookie)throw new Error("cookie is empty");if(!this.addition.root_folder_id)throw new Error("root folder id is empty");await this.client.checkCookie()}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}joinPath(e,t){return e==="/"?`/${t}`:`${e}/${t}`}async resolveId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id;let i=this.idCache.get(t);if(i)return i;let n=t.split("/").filter(Boolean),s=this.addition.root_folder_id,o="/";for(let a of n){let l=(await this.client.list(s)).find(d=>d.name===a&&d.dentryType==="folder");if(!l)throw new Error(`folder not found: ${a}`);s=l.dentryUuid,o=this.joinPath(o,a),this.idCache.set(o,s)}return s}async resolveItemId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id;let i=this.idCache.get(t);if(i)return i;let n=t.split("/").slice(0,-1).join("/")||"/",s=t.split("/").pop()||"",o=await this.resolveId(n),c=(await this.client.list(o)).find(l=>l.name===s);if(!c)throw new Error(`item not found: ${s}`);return this.idCache.set(t,c.dentryUuid),c.dentryUuid}async list(e,t){let i=await this.resolveId(t),n=await this.client.list(i),s=this.cleanPath(t),o=n.filter(a=>a.dentryUuid&&a.name).map(a=>(this.idCache.set(this.joinPath(s,a.name),a.dentryUuid),QP(a)));return de(o,"name","asc")}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.addition.root_folder_id,type:1};let s=i.split("/").slice(0,-1).join("/")||"/",o=await this.resolveId(s),c=(await this.client.list(o)).find(u=>u.name===n);if(!c)throw new Error("file not found");let l=c.dentryType==="folder",d="";return l||(d=await this.client.download(c.dentryUuid)),this.idCache.set(i,c.dentryUuid),{...QP(c),raw_url:d,raw_url_headers:{"User-Agent":HE,Referer:Js+"/",Origin:Js}}}async mkdir(e,t){let i=this.cleanPath(t),n=i.split("/").slice(0,-1).join("/")||"/",s=i.split("/").pop()||"",o=await this.resolveId(n);await this.client.post("/box/api/v2/dentry/createfolder",{dentryType:"folder",name:s,parentDentryUuid:o,conflictHandleStrategy:"auto_rename"})}async rename(e,t,i){let n=await this.resolveItemId(t);await this.client.post("/box/api/v2/dentry/rename",{dentryUuid:n,name:i})}async move(e,t,i,n,s){let o=await this.resolveItemId(n),a=this.cleanPath(s).split("/").slice(0,-1).join("/")||"/",c=await this.resolveId(a);await this.client.post("/box/api/v2/dentry/move",{targetParentDentryUuid:c,sourceDentryUuid:o,operateFrom:1})}async copy(e,t,i,n,s){let o=await this.resolveItemId(n),a=this.cleanPath(s).split("/").slice(0,-1).join("/")||"/",c=await this.resolveId(a);await this.client.post("/box/api/v2/dentry/copy",{sourceDentryUuid:o,targetParentDentryUuid:c,operateFrom:1,onlyCopyMeta:!1})}async remove(e,t,i){let n=await this.resolveItemId(t);await this.client.post("/box/api/v1/dentry/recycle",{dentryUuid:n})}async put(e,t,i){throw new Error("AliDoc does not support upload")}}});var pV,sy,FP=J(()=>{"use strict";pV='MediaBrowser Client="OpenList", Device="OpenList", DeviceId="openlist-emby", Version="1.0.0"',sy=class{baseURL;token;userID;constructor(e,t="",i=""){this.baseURL=(e||"").trim().replace(/\/+$/,""),this.token=t,this.userID=i}async login(e,t){let i=await fetch(`${this.baseURL}/Users/AuthenticateByName`,{method:"POST",headers:{"Content-Type":"application/json","X-Emby-Authorization":pV},body:JSON.stringify({Username:e,Pw:t})});if(!i.ok)throw new Error(`emby auth failed: status=${i.status}`);let n=await i.json();if(!n.AccessToken||!n.User?.Id)throw new Error("emby auth response missing access token or user id");this.token=n.AccessToken,this.userID=n.User.Id}async getItems(e){let t=new URLSearchParams({ParentId:e,Recursive:"false",Fields:"Path,Size,DateCreated,SeriesName,IndexNumber,ParentIndexNumber",api_key:this.token}),i=await fetch(`${this.baseURL}/Users/${this.userID}/Items?${t.toString()}`);if(!i.ok)throw new Error(`emby list failed: status=${i.status}`);return await i.json()}async getItemDetail(e){let t=new URLSearchParams({Fields:"MediaSources",api_key:this.token}),i=await fetch(`${this.baseURL}/Users/${this.userID}/Items/${encodeURIComponent(e)}?${t.toString()}`);return i.ok?await i.json():null}}});function mV(r){let e={...r||{}};return e.url=(e.url||"").trim().replace(/\/+$/,""),e.api_key=(e.api_key||"").trim(),e.user_id=(e.user_id||"").trim(),e.username=(e.username||"").trim(),e.password=(e.password||"").trim(),e.link_method=(e.link_method||"stream").toLowerCase(),e.root_folder_id=(e.root_folder_id||"").trim()||"1",e}function PP(r){return{name:r.Name,size:r.IsFolder?0:r.Size||0,is_dir:r.IsFolder,modified:r.DateCreated?new Date(r.DateCreated).toISOString():new Date().toISOString(),sign:r.Id,type:X(r.Name,r.IsFolder)}}var oy,TP=J(()=>{"use strict";Be();He();FP();oy=class{addition;client;idCache=new Map;constructor(e){this.addition=mV(e),this.client=new sy(this.addition.url,this.addition.api_key,this.addition.user_id)}async init(){if(!this.addition.url)throw new Error("url is required");if(this.addition.api_key){if(!this.addition.user_id)throw new Error("user_id is required when api_key is set");return}if(!this.addition.username)throw new Error("please provide api_key+user_id or username(+password)");await this.client.login(this.addition.username,this.addition.password)}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}joinPath(e,t){return e==="/"?`/${t}`:`${e}/${t}`}async resolveId(e){let t=this.cleanPath(e);if(t==="/")return this.addition.root_folder_id;let i=this.idCache.get(t);if(i)return i;let n=t.split("/").filter(Boolean),s=this.addition.root_folder_id,o="/";for(let a of n){let l=(await this.client.getItems(s)).Items.find(d=>d.Name===a&&d.IsFolder);if(!l)throw new Error(`folder not found: ${a}`);s=l.Id,o=this.joinPath(o,a),this.idCache.set(o,s)}return s}async list(e,t){let i=await this.resolveId(t),n=await this.client.getItems(i),s=this.cleanPath(t),o=n.Items.map(a=>(this.idCache.set(this.joinPath(s,a.Name),a.Id),PP(a)));return de(o,"name","asc")}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.addition.root_folder_id,type:1};let s=i.split("/").slice(0,-1).join("/")||"/",o=await this.resolveId(s),c=(await this.client.getItems(o)).Items.find(u=>u.Name===n);if(!c)throw new Error("file not found");let l=c.IsFolder,d="";return l||(d=await this.buildLink(c.Id)),this.idCache.set(i,c.Id),{...PP(c),raw_url:d,raw_url_headers:{"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"}}}async buildLink(e){let t=this.addition.link_method==="download",i="",n="";if(!t){let o=await this.client.getItemDetail(e);if(o&&o.MediaSources?.length){let a=o.MediaSources.find(c=>c.Id&&c.SupportsDirectStream)||o.MediaSources.find(c=>c.Id);a&&(i=a.Id,n=a.Container)}}let s=new URL(this.addition.url);return t?s.pathname=`${s.pathname.replace(/\/+$/,"")}/Items/${encodeURIComponent(e)}/Download`:n?s.pathname=`${s.pathname.replace(/\/+$/,"")}/Videos/${encodeURIComponent(e)}/stream.${n}`:s.pathname=`${s.pathname.replace(/\/+$/,"")}/Videos/${encodeURIComponent(e)}/stream`,s.searchParams.set("api_key",this.client.token),i&&s.searchParams.set("MediaSourceId",i),t||s.searchParams.set("Static","true"),s.toString()}async mkdir(e,t){throw new Error("Emby is read-only")}async rename(e,t,i){throw new Error("Emby is read-only")}async move(e,t,i,n,s){throw new Error("Emby is read-only")}async copy(e,t,i,n,s){throw new Error("Emby is read-only")}async remove(e,t,i){throw new Error("Emby is read-only")}async put(e,t,i){throw new Error("Emby is read-only")}}});function cy(r,e){let t=(r||"").trim();if(t||(t=e),!t)throw new Error("empty url");t.includes("://")||(t="https://"+t);let i=new URL(t);if(!i.host)throw new Error("invalid url: "+r);return i.pathname=i.pathname.replace(/\/+$/,""),i.toString()}function aa(r){return r?"/"+r.replace(/^\/+/,"").split("/").filter(Boolean).join("/"):"/"}function UP(r){let e=new Uint8Array(r),t="";for(let i of e)t+=String.fromCharCode(i);return btoa(t).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}async function yV(r){return crypto.subtle.digest("SHA-256",new TextEncoder().encode(r))}async function IV(r,e){let t=await crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"HMAC",hash:"SHA-256"},!1,["sign"]);return crypto.subtle.sign("HMAC",t,new TextEncoder().encode(e))}function wV(r){return Array.from(r.keys()).filter(t=>t!=="token"&&t!=="expires").sort().map(t=>`${t}=${r.get(t)||""}`).join("&")}function Qc(r,e,t){let i=cy(r.endpoint,ay),n=new URL(i),s=aa(e),o=(r.storage_zone_name||"").replace(/^\/+|\/+$/g,"");return s==="/"?n.pathname=`/${o}/`:(n.pathname=`/${o}/${s.replace(/^\/+/,"")}`,t&&!n.pathname.endsWith("/")&&(n.pathname+="/")),n.toString()}function NP(r,e){let t=new URL(r.cdn_base_url),i=aa(e),n=t.pathname.replace(/\/+$/,"");return i==="/"?t.pathname=n?n+"/":"/":t.pathname=n+"/"+i.replace(/^\/+/,""),t.toString()}async function qP(r,e){let t=r.sign_url_expire>0?r.sign_url_expire:4,i=Math.floor(Date.now()/1e3)+t*3600,n=new URL(e),s=wV(n.searchParams),o=decodeURIComponent(n.pathname),a=(r.cdn_token_method||"sha256").toLowerCase(),c;if(a==="hmac_sha256"){let l=await IV(r.cdn_token_key,o+String(i)+s);c="HS256-"+UP(l)}else{let l=await yV(r.cdn_token_key+o+String(i)+s);c=UP(l)}return n.searchParams.set("token",c),n.searchParams.set("expires",String(i)),n.toString()}async function Yl(r){if(r.ok)return;let e=await r.text().catch(()=>"")||"",t=e.trim();try{let i=JSON.parse(e);i?.length&&i[0].Message&&(t=i[0].Message)}catch{}throw new Error(`bunny storage request failed: ${r.status} ${t}`)}var ay,RP,OP=J(()=>{"use strict";ay="storage.bunnycdn.com",RP=".openlist"});function xV(r){let e={...r||{}};return e.storage_zone_name=(e.storage_zone_name||"").trim(),e.access_key=(e.access_key||"").trim(),e.endpoint=(e.endpoint||"").trim()||ay,e.cdn_base_url=(e.cdn_base_url||"").trim(),e.cdn_token_key=(e.cdn_token_key||"").trim(),e.cdn_token_method=(e.cdn_token_method||"sha256").toLowerCase(),e.cdn_token_include_ip=!!e.cdn_token_include_ip,e.sign_url_expire=Number(e.sign_url_expire||4),e.placeholder=(e.placeholder||"").trim()||RP,e.root_folder_path=e.root_folder_path||"/",e}function CV(r){if(!r)return new Date().toISOString();let e=new Date(r);return isNaN(e.getTime())?new Date().toISOString():e.toISOString()}var ly,MP=J(()=>{"use strict";Be();He();OP();ly=class{addition;constructor(e){this.addition=xV(e)}async init(){cy(this.addition.endpoint,ay),this.addition.cdn_base_url&&cy(this.addition.cdn_base_url,"")}toFileItem(e,t){let i=e==="/"?`/${t.ObjectName}`:`${e}/${t.ObjectName}`;return{name:t.ObjectName,size:t.IsDirectory?0:t.Length||0,is_dir:t.IsDirectory,modified:CV(t.LastChanged),sign:t.Guid||i,type:X(t.ObjectName,t.IsDirectory)}}async list(e,t){let i=aa(t),n=Qc(this.addition,i,!0),s=await fetch(n,{headers:{AccessKey:this.addition.access_key}});await Yl(s);let c=(await s.json()).filter(l=>!(!l.ObjectName||!l.IsDirectory&&l.ObjectName===this.addition.placeholder)).map(l=>this.toFileItem(i,l));return de(c,"name","asc")}async get(e,t){let i=aa(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1};let s=i.split("/").slice(0,-1).join("/")||"/",o=Qc(this.addition,s,!0),a=await fetch(o,{headers:{AccessKey:this.addition.access_key}});await Yl(a);let l=(await a.json()).find(f=>f.ObjectName===n);if(!l)throw new Error("object not found");let d="",u={};return l.IsDirectory||(this.addition.cdn_base_url?(d=NP(this.addition,i),this.addition.cdn_token_key&&(d=await qP(this.addition,d))):(d=Qc(this.addition,i,!1),u={AccessKey:this.addition.access_key})),{...this.toFileItem(s,l),raw_url:d,raw_url_headers:u}}async mkdir(e,t){let i=aa(t),n=i==="/"?`/${this.addition.placeholder}`:`${i}/${this.addition.placeholder}`,s=Qc(this.addition,n,!1),o=await fetch(s,{method:"PUT",headers:{AccessKey:this.addition.access_key,"Content-Type":"application/octet-stream","Content-Length":"0"}});await Yl(o)}async rename(e,t,i){throw new Error("Bunny Storage does not support rename")}async move(e,t,i,n,s){throw new Error("Bunny Storage does not support move")}async copy(e,t,i,n,s){throw new Error("Bunny Storage does not support copy")}async remove(e,t,i){let n=aa(t),s=await fetch(Qc(this.addition,n,!0),{method:"DELETE",headers:{AccessKey:this.addition.access_key}});await Yl(s)}async put(e,t,i){let n=aa(t),s=Qc(this.addition,n,!1),o=await fetch(s,{method:"PUT",headers:{AccessKey:this.addition.access_key,"Content-Type":"application/octet-stream","Content-Length":String(i.length)},body:new Uint8Array(i)});await Yl(o)}}});function uy(r){return r.split("/").map(e=>encodeURIComponent(e)).join("/")}function LP(r,e){if(!r)return 0;for(let t of e){let i=r[t];if(i!=null){let n=Number(i);if(!isNaN(n))return n}}return 0}function HP(r){return LP(r,["FileSizeBytes","File-Size","fileSize","size"])}function KP(r){return LP(r,["TimeStamp","timestamp","modified"])}var dy,GP=J(()=>{"use strict";dy=class{constructor(e,t){this.address=e;this.token=t}address;token;headers(){return{Authorization:"Bearer "+this.token,Accept:"application/json"}}async request(e,t,i){let s=null;for(let o=0;o<3;o++)try{let a=await fetch(this.address+t,{method:e,headers:{...this.headers(),...i&&!(i instanceof FormData)?{"Content-Type":"application/json"}:{}},body:i?i instanceof FormData?i:JSON.stringify(i):void 0});if(a.status===429){await new Promise(u=>setTimeout(u,(o+1)*2e3));continue}let c=await a.text(),l={};try{l=c?JSON.parse(c):{}}catch{l={}}let d=l;if(d.error||d.message)throw new Error("API error: "+(d.error||d.message));if(!a.ok)throw new Error(`HTTP ${a.status}`);return l}catch(a){if(s=a,o<2){await new Promise(c=>setTimeout(c,(o+1)*1e3));continue}}throw s||new Error("max retries exceeded")}async list(e,t,i){return this.request("GET",`/api/manage/list?dir=${encodeURIComponent(e)}&start=${t}&count=${i}`)}async remove(e,t){return this.request("POST",`/api/manage/delete${uy(e)}?folder=${t}`)}}});function EV(r){let e={...r||{}};return e.address=(e.address||"").trim().replace(/\/+$/,""),e.token=(e.token||"").trim(),e.small_channel_name=(e.small_channel_name||"").trim(),e.large_channel_name=(e.large_channel_name||"").trim(),e.large_channel_type=(e.large_channel_type||"").trim(),e.upload_thread=Math.min(Number(e.upload_thread||3),32),e.root_folder_path=e.root_folder_path||"/",e}var KE,fy,zP=J(()=>{"use strict";Be();He();GP();KE=1e3;fy=class{addition;client;publicUrlPrefix="";constructor(e){this.addition=EV(e),this.client=new dy(this.addition.address,this.addition.token)}async init(){if(!this.addition.address)throw new Error("address is required");if(!this.addition.token)throw new Error("token is required");await this.client.list("/",0,1)}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}async list(e,t){let i=this.cleanPath(t),n=new Set,s=new Set,o=[],a=0;for(;;){let c=await this.client.list(i,a,KE);if(!c.files?.length&&!c.directories?.length)break;for(let l of c.directories||[]){let d="/"+(l||"").replace(/^\/+|\/+$/g,"");n.has(d)||(n.add(d),o.push({name:d.split("/").filter(Boolean).pop()||"",size:0,is_dir:!0,modified:new Date().toISOString(),sign:d,type:1}))}for(let l of c.files||[])if(!s.has(l.name)){s.add(l.name);let d=l.name.split("/").filter(Boolean).pop()||l.name,u=HP(l.metadata),f=KP(l.metadata);o.push({name:d,size:u,is_dir:!1,modified:f?new Date(f).toISOString():new Date().toISOString(),sign:"/"+(l.name||"").replace(/^\/+/,""),type:X(d,!1)})}if((c.files?.length||0)+(c.directories?.length||0)<KE)break;a+=KE}return de(o,"name","asc")}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1};let s=this.publicUrlPrefix?this.publicUrlPrefix+uy(i):this.addition.address+"/file"+uy(i);return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:i,type:X(n,!1),raw_url:s}}async mkdir(e,t){}async rename(e,t,i){throw new Error("cloudflare_imgbed does not support rename")}async move(e,t,i,n,s){throw new Error("cloudflare_imgbed does not support move")}async copy(e,t,i,n,s){throw new Error("cloudflare_imgbed does not support copy")}async remove(e,t,i){let n=this.cleanPath(t);await this.client.remove(n,!1)}async put(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/")||"/",o=n.split("/").pop()||"upload",a=this.addition.small_channel_name;if(!a)throw new Error("channel name not configured");let c=new URLSearchParams({returnFormat:"default",channelName:a,uploadFolder:s,autoRetry:"true"}),l=new FormData;l.append("file",new Blob([new Uint8Array(i)],{type:"application/octet-stream"}),o);let d=await fetch(`${this.addition.address}/upload?${c.toString()}`,{method:"POST",headers:{Authorization:"Bearer "+this.addition.token},body:l});if(!d.ok)throw new Error(`upload failed ${d.status}: ${await d.text()}`);let u=await d.json();if(!u?.length||!u[0].src)throw new Error("no src returned");if(u[0].publicUrl)try{let f=new URL(u[0].publicUrl);this.publicUrlPrefix=f.protocol+"//"+f.host}catch{}}}});function $P(r){let e=(r||"").trim().replace(/\s/g,"");if(!e)return"";if(e.startsWith("+"))return e.startsWith("+86")&&e.length>3?"+86 "+e.slice(3):e;let t=e.replace(/\D/g,"");return t.startsWith("86")&&t.length>11,t.length===11?"+86 "+t:e}function VP(){let r=crypto.getRandomValues(new Uint8Array(16));return Array.from(r).map(e=>e.toString(16).padStart(2,"0")).join("")}function YP(r){let e=(r||"").toLowerCase().replace(/-/g,"");return e.length!==32||!/^[0-9a-f]+$/.test(e)?"":e}function GE(r){return r>0?new Date(r*1e3).toISOString():new Date().toISOString()}function jP(r){let e=(r||"").trim().toLowerCase();return e?e==="success"||e==="ok"||e.includes("\u6210\u529F"):!0}function bV(r){let e=(r||"").trim().toLowerCase();return e?e==="\u4E0A\u4F20\u5DF2\u5B8C\u6210"||e==="upload completed"||e==="already uploaded"||e==="\u79D2\u4F20\u6210\u529F":!1}var JP,BV,If,WP=J(()=>{"use strict";Gr();JP="https://account.guangyapan.com",BV="https://api.guangyapan.com";If=class{accessToken="";refreshToken="";clientId;deviceId;deviceSign;onTokensChanged;constructor(e){this.clientId=e.client_id||"",this.deviceId=e.device_id||"",this.deviceSign=e.device_sign||"",this.accessToken=e.access_token||"",this.refreshToken=e.refresh_token||""}accountHeaders(){return{Accept:"application/json, text/plain, */*","Content-Type":"application/json","X-Device-Model":"chrome%2F147.0.0.0","X-Device-Name":"PC-Chrome","X-Device-Sign":this.deviceSign,"X-Net-Work-Type":"NONE","X-OS-Version":"MacIntel","X-Platform-Version":"1","X-Protocol-Version":"301","X-Provider-Name":"NONE","X-SDK-Version":"9.0.2","X-Client-Id":this.clientId,"X-Client-Version":"0.0.1","X-Device-Id":this.deviceId}}apiHeaders(){return{Accept:"application/json, text/plain, */*","Content-Type":"application/json",Did:this.deviceId,Dt:"4",Authorization:"Bearer "+this.accessToken}}get hasToken(){return!!this.accessToken}get hasRefreshToken(){return!!this.refreshToken}async validateToken(){let e=await fetch(`${JP}/v1/user/me`,{headers:{...this.accountHeaders(),Authorization:"Bearer "+this.accessToken}});if(!e.ok)throw new Error(`validate token failed: ${e.status}`)}async refresh(){if(!this.refreshToken)throw new Error("refresh_token is empty");let e=await fetch(`${JP}/v1/auth/token`,{method:"POST",headers:this.accountHeaders(),body:JSON.stringify({client_id:this.clientId,grant_type:"refresh_token",refresh_token:this.refreshToken})}),t=await e.json();if(!e.ok||t.error||!t.access_token)throw new Error("refresh token failed: "+(t.error_description||t.error||e.status));this.accessToken=t.access_token,t.refresh_token&&(this.refreshToken=t.refresh_token),await this.onTokensChanged?.(this.accessToken,this.refreshToken)}async postAPI(e,t){let i=async()=>{let o=await fetch(`${BV}${e}`,{method:"POST",headers:this.apiHeaders(),body:JSON.stringify(t??{})});return{res:o,json:await o.json().catch(()=>({}))}},{res:n,json:s}=await i();if((n.status===401||n.status===403)&&this.refreshToken){await this.refresh();let o=await i();n=o.res,s=o.json}if(!n.ok)throw new Error(`request failed: status=${n.status}`);return s}async getFileList(e,t,i,n){return this.postAPI("/userres/v1/file/get_file_list",{parentId:e,page:0,pageSize:t,orderBy:i,sortType:n})}async getDownloadURL(e){let t=await this.postAPI("/nd.bizuserres.s/v1/get_res_download_url",{fileId:e});return(t?.data?.signedURL||t?.data?.downloadUrl||"").trim()}async getUploadToken(e,t,i,n){let s={fileSize:i};n&&(s.md5=n);let o=await this.postAPI("/nd.bizuserres.s/v1/get_res_center_token",{capacity:2,name:t,parentId:e,res:s}),a=o.msg||"",c=o.code===156||bV(a);if(!jP(a)&&!c)throw new Error("get upload token failed: "+a);return{data:o.data,code:o.code,alreadyDone:c}}async waitTaskDone(e){for(let t=0;t<30;t++){let i=await this.postAPI("/nd.bizuserres.s/v1/get_task_status",{taskId:e});if(!jP(i.msg))throw new Error("get task status failed: "+i.msg);if(i.data?.status===2)return;if(i.data?.status===-1||i.data?.status===3)throw new Error(`task ${e} failed with status=${i.data.status}`);await new Promise(n=>setTimeout(n,300))}throw new Error(`task ${e} timeout`)}async waitUploadTaskInfo(e){for(let t=0;t<300;t++){if((await this.postAPI("/nd.bizuserres.s/v1/file/get_info_by_task_id",{taskId:e})).data?.fileId)return;await new Promise(n=>setTimeout(n,1e3))}throw new Error(`upload task ${e} timeout`)}static md5(e){return $t(e)}}});function vV(r){let e={...r||{}};return e.client_id=(e.client_id||"").trim(),e.device_id=YP(e.device_id||""),e.device_id||(e.device_id=VP()),e.device_sign=(e.device_sign||"").trim()||"wdi10."+e.device_id,e.page_size=Number(e.page_size||100),e.order_by=Number(e.order_by===void 0?3:e.order_by),e.sort_type=Number(e.sort_type===void 0?1:e.sort_type),e.root_folder_path=(e.root_folder_path||"").trim(),e.access_token=(e.access_token||"").trim(),e.refresh_token=(e.refresh_token||"").trim(),e.phone_number=$P(e.phone_number||""),e}function XP(r){let e=r.resType===2;return{name:r.fileName,size:e?0:r.fileSize||0,is_dir:e,modified:GE(r.utime),created:GE(r.ctime),sign:r.fileId,type:X(r.fileName,e)}}function kV(r,e){let t=(r||"").trim();if(!t)return t;!t.startsWith("http://")&&!t.startsWith("https://")&&(t="https://"+t);let i;try{i=new URL(t)}catch{return t}let n=i.host;return e&&n.startsWith(e+".")&&(n=n.slice(e.length+1)),i.host=n,i.toString().replace(/\/+$/,"")}var Ay,ZP=J(()=>{"use strict";Be();He();Gr();WP();Ay=class{addition;client;idCache=new Map;constructor(e,t){this.addition=vV(e),this.client=new If(this.addition),this.client.onTokensChanged=t}async init(){if(!this.addition.client_id)throw new Error("client_id is required");if(this.addition.access_token)try{await this.client.validateToken();return}catch{this.addition.access_token=""}if(this.addition.refresh_token){await this.client.refresh(),await this.client.validateToken();return}throw new Error("login failed: provide a valid access_token, or refresh_token, or phone_number + verify_code")}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}joinPath(e,t){return e==="/"?`/${t}`:`${e}/${t}`}async resolveId(e){let t=this.cleanPath(e);if(t==="/")return"";let i=this.idCache.get(t);if(i)return i;let n=t.split("/").filter(Boolean),s="",o="/";for(let a of n){let l=(await this.client.getFileList(s,this.addition.page_size,this.addition.order_by,this.addition.sort_type)).data?.list?.find(d=>d.fileName===a&&d.resType===2);if(!l)throw new Error(`folder not found: ${a}`);s=l.fileId,o=this.joinPath(o,a),this.idCache.set(o,s)}return s}async resolveItemId(e){let t=this.cleanPath(e);if(t==="/")return"";let i=this.idCache.get(t);if(i)return i;let n=t.split("/").slice(0,-1).join("/")||"/",s=t.split("/").pop()||"",o=await this.resolveId(n),c=(await this.client.getFileList(o,this.addition.page_size,this.addition.order_by,this.addition.sort_type)).data?.list?.find(l=>l.fileName===s);if(!c)throw new Error(`item not found: ${s}`);return this.idCache.set(t,c.fileId),c.fileId}async list(e,t){let i=await this.resolveId(t),n=this.cleanPath(t),s=[];for(let o=0;o<1e4;o++){let a=await this.client.getFileList(i,this.addition.page_size,this.addition.order_by,this.addition.sort_type),c=a.data?.list||[];for(let l of c)this.idCache.set(this.joinPath(n,l.fileName),l.fileId),s.push(XP(l));if(c.length<this.addition.page_size||a.data?.total>0&&s.length>=a.data.total)break}return de(s,"name","asc")}async get(e,t){let i=this.cleanPath(t),n=i.split("/").filter(Boolean).pop()||"root";if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1};let s=i.split("/").slice(0,-1).join("/")||"/",o=await this.resolveId(s),c=(await this.client.getFileList(o,this.addition.page_size,this.addition.order_by,this.addition.sort_type)).data?.list?.find(u=>u.fileName===n);if(!c)throw new Error("file not found");let l=c.resType===2,d="";return l||(d=await this.client.getDownloadURL(c.fileId)),this.idCache.set(i,c.fileId),{...XP(c),raw_url:d}}async mkdir(e,t){let i=this.cleanPath(t),n=i.split("/").slice(0,-1).join("/")||"/",s=i.split("/").pop()||"",o=await this.resolveId(n);await this.client.postAPI("/nd.bizuserres.s/v1/file/create_dir",{parentId:o,dirName:s})}async rename(e,t,i){let n=await this.resolveItemId(t);await this.client.postAPI("/nd.bizuserres.s/v1/file/rename",{fileId:n,newName:i})}async move(e,t,i,n,s){let o=await this.resolveItemId(n),a=this.cleanPath(s).split("/").slice(0,-1).join("/")||"/",c=await this.resolveId(a),l=await this.client.postAPI("/nd.bizuserres.s/v1/file/move_file",{fileIds:[o],parentId:c});l?.data?.taskId&&await this.client.waitTaskDone(l.data.taskId)}async copy(e,t,i,n,s){let o=await this.resolveItemId(n),a=this.cleanPath(s).split("/").slice(0,-1).join("/")||"/",c=await this.resolveId(a),l=await this.client.postAPI("/nd.bizuserres.s/v1/file/copy_file",{fileIds:[o],parentId:c});l?.data?.taskId&&await this.client.waitTaskDone(l.data.taskId)}async remove(e,t,i){let n=await this.resolveItemId(t),s=await this.client.postAPI("/nd.bizuserres.s/v1/file/delete_file",{fileIds:[n]});s?.data?.taskId&&await this.client.waitTaskDone(s.data.taskId)}async put(e,t,i){let n=this.cleanPath(t),s=n.split("/").slice(0,-1).join("/")||"/",o=n.split("/").pop()||"upload",a=await this.resolveId(s),c=new Uint8Array(i),l=If.md5(c),{data:d,alreadyDone:u}=await this.client.getUploadToken(a,o,c.length,l),f=d?.taskId||"";if(u){if(!f)throw new Error("instant upload returns empty task id");await this.client.waitUploadTaskInfo(f);return}if(!d.objectPath||!d.bucketName||!d.endPoint||!d.accessKeyID||!d.secretAccessKey)throw new Error("upload token is incomplete");let h=kV(d.endPoint,d.bucketName),g=`https://${d.bucketName}.${new URL(h).host}/${d.objectPath.replace(/^\/+/,"")}`,p=new Date().toUTCString(),I="application/octet-stream",m=d.sessionToken?`x-oss-security-token:${d.sessionToken} -`:"",w=`/${d.bucketName}/${d.objectPath.replace(/^\/+/,"")}`,B=`PUT - -${I} -${p} -${m}${w}`,C=await qu(B,d.secretAccessKey),x=`OSS ${d.accessKeyID}:${C}`,Q={"Content-Type":I,Date:p,Authorization:x};d.sessionToken&&(Q["x-oss-security-token"]=d.sessionToken);let R=await fetch(g,{method:"PUT",headers:Q,body:c});if(!R.ok)throw new Error(`OSS upload failed: ${R.status} ${await R.text()}`);f&&await this.client.waitUploadTaskInfo(f)}}});var e5=oe(zE=>{var SV=typeof zE>"u"?{}:zE;(function(r){"use strict";var e="__namespace",t=function(A){return A==null},i=function(A){return A===e||Number.isInteger(A)&&A>=1&&A<=11},n=function(A){return A&&i(A.nodeType)&&typeof A.nodeName=="string"};function s(A){var k=Array.prototype.slice,O=A.length,V=function(W,ue){return function(){return ue.apply(this,W.concat(k.call(arguments)))}},H=function(){var W=k.call(arguments);return W.length<O?V(W,H):A.apply(this,k.apply(arguments,[0,O]))};return H}var o=function(A,k){for(var O=0;O<k.length;O+=1)A(k[O],O,k)},a=function(A,k,O){var V=k;return o(function(H,W){V=A(V,H,W)},O),V},c=function(A,k){var O=new Array(k.length);return o(function(V,H){O[H]=A(V)},k),O},l=function(A,k){var O=[];return o(function(V,H){A(V,H)&&O.push(V)},k),O},d=function(A,k){for(var O=0;O<A.length;O+=1)if(A[O]===k)return!0;return!1};function u(A){return function(){return A}}function f(A){return A.toString()}var h=function(A,k){return k.join(A)},g=function(A,k,O){return A+O+k},p=Array.prototype.concat,I=function(A,k){var O=new Ce;O.addArray(A);var V=O.toArray();return k?V.reverse():V},m=32767;function w(A){for(var k=[],O=0;O<A.length;O+=m){var V=A.slice(O,O+m);k=p.apply(k,V)}return k}function B(A,k){for(var O=Object(A),V=1;V<arguments.length;V++){var H=arguments[V];if(H!=null)for(var W in H)Object.prototype.hasOwnProperty.call(H,W)&&(O[W]=H[W])}return O}var C={ELEMENT_NODE:1,ATTRIBUTE_NODE:2,TEXT_NODE:3,CDATA_SECTION_NODE:4,PROCESSING_INSTRUCTION_NODE:7,COMMENT_NODE:8,DOCUMENT_NODE:9,DOCUMENT_TYPE_NODE:10,DOCUMENT_FRAGMENT_NODE:11,NAMESPACE_NODE:e};x.prototype=new Object,x.prototype.constructor=x,x.superclass=Object.prototype;function x(){this.init()}x.prototype.init=function(){this.reduceActions=[],this.reduceActions[3]=function(A){return new _(A[0],A[2])},this.reduceActions[5]=function(A){return new v(A[0],A[2])},this.reduceActions[7]=function(A){return new E(A[0],A[2])},this.reduceActions[8]=function(A){return new P(A[0],A[2])},this.reduceActions[10]=function(A){return new N(A[0],A[2])},this.reduceActions[11]=function(A){return new j(A[0],A[2])},this.reduceActions[12]=function(A){return new z(A[0],A[2])},this.reduceActions[13]=function(A){return new L(A[0],A[2])},this.reduceActions[15]=function(A){return new G(A[0],A[2])},this.reduceActions[16]=function(A){return new K(A[0],A[2])},this.reduceActions[18]=function(A){return new $(A[0],A[2])},this.reduceActions[19]=function(A){return new te(A[0],A[2])},this.reduceActions[20]=function(A){return new re(A[0],A[2])},this.reduceActions[22]=function(A){return new D(A[1])},this.reduceActions[24]=function(A){return new ee(A[0],A[2])},this.reduceActions[25]=function(A){return new Z(void 0,void 0,A[0])},this.reduceActions[27]=function(A){return A[0].locationPath=A[2],A[0]},this.reduceActions[28]=function(A){return A[0].locationPath=A[2],A[0].locationPath.steps.unshift(new se(se.DESCENDANTORSELF,ce.nodeTest,[])),A[0]},this.reduceActions[29]=function(A){return new Z(A[0],[],void 0)},this.reduceActions[30]=function(A){return be.instance_of(A[0],Z)?(A[0].filterPredicates==null&&(A[0].filterPredicates=[]),A[0].filterPredicates.push(A[1]),A[0]):new Z(A[0],[A[1]],void 0)},this.reduceActions[32]=function(A){return A[1]},this.reduceActions[33]=function(A){return new we(A[0])},this.reduceActions[34]=function(A){return new Ee(A[0])},this.reduceActions[36]=function(A){return new Mr(A[0],[])},this.reduceActions[37]=function(A){return new Mr(A[0],A[2])},this.reduceActions[38]=function(A){return[A[0]]},this.reduceActions[39]=function(A){return A[2].unshift(A[0]),A[2]},this.reduceActions[43]=function(A){return new _e(!0,[])},this.reduceActions[44]=function(A){return A[1].absolute=!0,A[1]},this.reduceActions[46]=function(A){return new _e(!1,[A[0]])},this.reduceActions[47]=function(A){return A[0].steps.push(A[2]),A[0]},this.reduceActions[49]=function(A){return new se(A[0],A[1],[])},this.reduceActions[50]=function(A){return new se(se.CHILD,A[0],[])},this.reduceActions[51]=function(A){return new se(A[0],A[1],A[2])},this.reduceActions[52]=function(A){return new se(se.CHILD,A[0],A[1])},this.reduceActions[54]=function(A){return[A[0]]},this.reduceActions[55]=function(A){return A[1].unshift(A[0]),A[1]},this.reduceActions[56]=function(A){return A[0]=="ancestor"?se.ANCESTOR:A[0]=="ancestor-or-self"?se.ANCESTORORSELF:A[0]=="attribute"?se.ATTRIBUTE:A[0]=="child"?se.CHILD:A[0]=="descendant"?se.DESCENDANT:A[0]=="descendant-or-self"?se.DESCENDANTORSELF:A[0]=="following"?se.FOLLOWING:A[0]=="following-sibling"?se.FOLLOWINGSIBLING:A[0]=="namespace"?se.NAMESPACE:A[0]=="parent"?se.PARENT:A[0]=="preceding"?se.PRECEDING:A[0]=="preceding-sibling"?se.PRECEDINGSIBLING:A[0]=="self"?se.SELF:-1},this.reduceActions[57]=function(A){return se.ATTRIBUTE},this.reduceActions[59]=function(A){return A[0]=="comment"?ce.commentTest:A[0]=="text"?ce.textTest:A[0]=="processing-instruction"?ce.anyPiTest:A[0]=="node"?ce.nodeTest:new ce(-1,void 0)},this.reduceActions[60]=function(A){return new ce.PITest(A[2])},this.reduceActions[61]=function(A){return A[1]},this.reduceActions[63]=function(A){return A[1].absolute=!0,A[1].steps.unshift(new se(se.DESCENDANTORSELF,ce.nodeTest,[])),A[1]},this.reduceActions[64]=function(A){return A[0].steps.push(new se(se.DESCENDANTORSELF,ce.nodeTest,[])),A[0].steps.push(A[2]),A[0]},this.reduceActions[65]=function(A){return new se(se.SELF,ce.nodeTest,[])},this.reduceActions[66]=function(A){return new se(se.PARENT,ce.nodeTest,[])},this.reduceActions[67]=function(A){return new dr(A[1])},this.reduceActions[68]=function(A){return ce.nameTestAny},this.reduceActions[69]=function(A){return new ce.NameTestPrefixAny(A[0].split(":")[0])},this.reduceActions[70]=function(A){return new ce.NameTestQName(A[0])}},x.actionTable=[" s s sssssssss s ss s ss"," s ","r rrrrrrrrr rrrrrrr rr r "," rrrrr "," s s sssssssss s ss s ss","rs rrrrrrrr s sssssrrrrrr rrs rs "," s s sssssssss s ss s ss"," s "," s ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr "," s "," s "," s s sssss s s ","r rrrrrrrrr rrrrrrr rr r ","a ","r s rr r ","r sr rr r ","r s rr s rr r ","r rssrr rss rr r ","r rrrrr rrrss rr r ","r rrrrrsss rrrrr rr r ","r rrrrrrrr rrrrr rr r ","r rrrrrrrr rrrrrs rr r ","r rrrrrrrr rrrrrr rr r ","r rrrrrrrr rrrrrr rr r ","r srrrrrrrr rrrrrrs rr sr ","r srrrrrrrr rrrrrrs rr r ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrr rrrrrr rr r ","r rrrrrrrr rrrrrr rr r ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr r "," sssss ","r rrrrrrrrr rrrrrrr rr sr ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr "," s ","r srrrrrrrr rrrrrrs rr r ","r rrrrrrrr rrrrr rr r "," s "," s "," rrrrr "," s s sssssssss s sss s ss","r srrrrrrrr rrrrrrs rr r "," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss s ss s ss"," s s sssssssss ss s ss"," s s sssssssss s ss s ss"," s s sssss s s "," s s sssss s s ","r rrrrrrrrr rrrrrrr rr rr "," s s sssss s s "," s s sssss s s ","r rrrrrrrrr rrrrrrr rr sr ","r rrrrrrrrr rrrrrrr rr sr ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr rr "," s ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr "," rr "," s "," rs ","r sr rr r ","r s rr s rr r ","r rssrr rss rr r ","r rssrr rss rr r ","r rrrrr rrrss rr r ","r rrrrr rrrss rr r ","r rrrrr rrrss rr r ","r rrrrr rrrss rr r ","r rrrrrsss rrrrr rr r ","r rrrrrsss rrrrr rr r ","r rrrrrrrr rrrrr rr r ","r rrrrrrrr rrrrr rr r ","r rrrrrrrr rrrrr rr r ","r rrrrrrrr rrrrrr rr r "," r "," s ","r srrrrrrrr rrrrrrs rr r ","r srrrrrrrr rrrrrrs rr r ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr r ","r rrrrrrrrr rrrrrrr rr rr ","r rrrrrrrrr rrrrrrr rr rr "," s s sssssssss s ss s ss","r rrrrrrrrr rrrrrrr rr rr "," r "],x.actionTableNumber=[` 1 0 /.-,+*)(' & %$ # "!`," J ","a aaaaaaaaa aaaaaaa aa a "," YYYYY ",` 1 0 /.-,+*)(' & %$ # "!`,`K1 KKKKKKKK . +*)('KKKKKK KK# K" `,` 1 0 /.-,+*)(' & %$ # "!`," N "," O ","e eeeeeeeee eeeeeee ee ee ","f fffffffff fffffff ff ff ","d ddddddddd ddddddd dd dd ","B BBBBBBBBB BBBBBBB BB BB ","A AAAAAAAAA AAAAAAA AA AA "," P "," Q ",` 1 . +*)(' # " `,"b bbbbbbbbb bbbbbbb bb b "," ","! S !! ! ",'" T" "" " ',"$ V $$ U $$ $ ","& &ZY&& &XW && & ",") ))))) )))\\[ )) ) ",". ....._^] ..... .. . ","1 11111111 11111 11 1 ","5 55555555 55555` 55 5 ","7 77777777 777777 77 7 ","9 99999999 999999 99 9 ",": c:::::::: ::::::b :: a: ","I fIIIIIIII IIIIIIe II I ","= ========= ======= == == ","? ????????? ??????? ?? ?? ","C CCCCCCCCC CCCCCCC CC CC ","J JJJJJJJJ JJJJJJ JJ J ","M MMMMMMMM MMMMMM MM M ","N NNNNNNNNN NNNNNNN NN N ","P PPPPPPPPP PPPPPPP PP P "," +*)(' ","R RRRRRRRRR RRRRRRR RR aR ","U UUUUUUUUU UUUUUUU UU U ","Z ZZZZZZZZZ ZZZZZZZ ZZ ZZ ","c ccccccccc ccccccc cc cc "," j ","L fLLLLLLLL LLLLLLe LL L ","6 66666666 66666 66 6 "," k "," l "," XXXXX ",` 1 0 /.-,+*)(' & %$m # "!`,"_ f________ ______e __ _ ",` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 0 /.-,+*)(' %$ # "!`,` 1 0 /.-,+*)(' & %$ # "!`,` 1 . +*)(' # " `,` 1 . +*)(' # " `,"> >>>>>>>>> >>>>>>> >> >> ",` 1 . +*)(' # " `,` 1 . +*)(' # " `,"Q QQQQQQQQQ QQQQQQQ QQ aQ ","V VVVVVVVVV VVVVVVV VV aV ","T TTTTTTTTT TTTTTTT TT T ","@ @@@@@@@@@ @@@@@@@ @@ @@ "," \x87 ","[ [[[[[[[[[ [[[[[[[ [[ [[ ","D DDDDDDDDD DDDDDDD DD DD "," HH "," \x88 "," F\x89 ","# T# ## # ","% V %% U %% % ","' 'ZY'' 'XW '' ' ","( (ZY(( (XW (( ( ","+ +++++ +++\\[ ++ + ","* ***** ***\\[ ** * ","- ----- ---\\[ -- - ",", ,,,,, ,,,\\[ ,, , ","0 00000_^] 00000 00 0 ","/ /////_^] ///// // / ","2 22222222 22222 22 2 ","3 33333333 33333 33 3 ","4 44444444 44444 44 4 ","8 88888888 888888 88 8 "," ^ "," \x8A ","; f;;;;;;;; ;;;;;;e ;; ; ","< f<<<<<<<< <<<<<<e << < ","O OOOOOOOOO OOOOOOO OO O ","` ````````` ``````` `` ` ","S SSSSSSSSS SSSSSSS SS S ","W WWWWWWWWW WWWWWWW WW W ","\\ \\\\\\\\\\\\\\\\\\ \\\\\\\\\\\\\\ \\\\ \\\\ ","E EEEEEEEEE EEEEEEE EE EE ",` 1 0 /.-,+*)(' & %$ # "!`,"] ]]]]]]]]] ]]]]]]] ]] ]] "," G "],x.gotoTable=["3456789:;<=>?@ AB CDEFGH IJ "," "," "," ","L456789:;<=>?@ AB CDEFGH IJ "," M EFGH IJ "," N;<=>?@ AB CDEFGH IJ "," "," "," "," "," "," "," "," "," "," S EFGH IJ "," "," "," "," "," "," "," "," "," "," "," "," "," e "," "," "," "," "," "," "," "," "," h J "," i j "," "," "," "," "," "," "," "," "," ","o456789:;<=>?@ ABpqCDEFGH IJ "," "," r6789:;<=>?@ AB CDEFGH IJ "," s789:;<=>?@ AB CDEFGH IJ "," t89:;<=>?@ AB CDEFGH IJ "," u89:;<=>?@ AB CDEFGH IJ "," v9:;<=>?@ AB CDEFGH IJ "," w9:;<=>?@ AB CDEFGH IJ "," x9:;<=>?@ AB CDEFGH IJ "," y9:;<=>?@ AB CDEFGH IJ "," z:;<=>?@ AB CDEFGH IJ "," {:;<=>?@ AB CDEFGH IJ "," |;<=>?@ AB CDEFGH IJ "," };<=>?@ AB CDEFGH IJ "," ~;<=>?@ AB CDEFGH IJ "," \x7F=>?@ AB CDEFGH IJ ","\x80456789:;<=>?@ AB CDEFGH IJ\x81"," \x82 EFGH IJ "," \x83 EFGH IJ "," "," \x84 GH IJ "," \x85 GH IJ "," i \x86 "," i \x87 "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," ","o456789:;<=>?@ AB\x8CqCDEFGH IJ "," "," "],x.productions=[[1,1,2],[2,1,3],[3,1,4],[3,3,3,-9,4],[4,1,5],[4,3,4,-8,5],[5,1,6],[5,3,5,-22,6],[5,3,5,-5,6],[6,1,7],[6,3,6,-23,7],[6,3,6,-24,7],[6,3,6,-6,7],[6,3,6,-7,7],[7,1,8],[7,3,7,-25,8],[7,3,7,-26,8],[8,1,9],[8,3,8,-12,9],[8,3,8,-11,9],[8,3,8,-10,9],[9,1,10],[9,2,-26,9],[10,1,11],[10,3,10,-27,11],[11,1,12],[11,1,13],[11,3,13,-28,14],[11,3,13,-4,14],[13,1,15],[13,2,13,16],[15,1,17],[15,3,-29,2,-30],[15,1,-15],[15,1,-16],[15,1,18],[18,3,-13,-29,-30],[18,4,-13,-29,19,-30],[19,1,20],[19,3,20,-31,19],[20,1,2],[12,1,14],[12,1,21],[21,1,-28],[21,2,-28,14],[21,1,22],[14,1,23],[14,3,14,-28,23],[14,1,24],[23,2,25,26],[23,1,26],[23,3,25,26,27],[23,2,26,27],[23,1,28],[27,1,16],[27,2,16,27],[25,2,-14,-3],[25,1,-32],[26,1,29],[26,3,-20,-29,-30],[26,4,-21,-29,-15,-30],[16,3,-33,30,-34],[30,1,2],[22,2,-4,14],[24,3,14,-4,23],[28,1,-35],[28,1,-2],[17,2,-36,-18],[29,1,-17],[29,1,-19],[29,1,-18]],x.DOUBLEDOT=2,x.DOUBLECOLON=3,x.DOUBLESLASH=4,x.NOTEQUAL=5,x.LESSTHANOREQUAL=6,x.GREATERTHANOREQUAL=7,x.AND=8,x.OR=9,x.MOD=10,x.DIV=11,x.MULTIPLYOPERATOR=12,x.FUNCTIONNAME=13,x.AXISNAME=14,x.LITERAL=15,x.NUMBER=16,x.ASTERISKNAMETEST=17,x.QNAME=18,x.NCNAMECOLONASTERISK=19,x.NODETYPE=20,x.PROCESSINGINSTRUCTIONWITHLITERAL=21,x.EQUALS=22,x.LESSTHAN=23,x.GREATERTHAN=24,x.PLUS=25,x.MINUS=26,x.BAR=27,x.SLASH=28,x.LEFTPARENTHESIS=29,x.RIGHTPARENTHESIS=30,x.COMMA=31,x.AT=32,x.LEFTBRACKET=33,x.RIGHTBRACKET=34,x.DOT=35,x.DOLLAR=36,x.prototype.tokenize=function(A){for(var k=[],O=[],V=A+"\0",H=0,W=V.charAt(H++);;){for(;W==" "||W==" "||W=="\r"||W==` -`;)W=V.charAt(H++);if(W=="\0"||H>=V.length)break;if(W=="("){k.push(x.LEFTPARENTHESIS),O.push(W),W=V.charAt(H++);continue}if(W==")"){k.push(x.RIGHTPARENTHESIS),O.push(W),W=V.charAt(H++);continue}if(W=="["){k.push(x.LEFTBRACKET),O.push(W),W=V.charAt(H++);continue}if(W=="]"){k.push(x.RIGHTBRACKET),O.push(W),W=V.charAt(H++);continue}if(W=="@"){k.push(x.AT),O.push(W),W=V.charAt(H++);continue}if(W==","){k.push(x.COMMA),O.push(W),W=V.charAt(H++);continue}if(W=="|"){k.push(x.BAR),O.push(W),W=V.charAt(H++);continue}if(W=="+"){k.push(x.PLUS),O.push(W),W=V.charAt(H++);continue}if(W=="-"){k.push(x.MINUS),O.push(W),W=V.charAt(H++);continue}if(W=="="){k.push(x.EQUALS),O.push(W),W=V.charAt(H++);continue}if(W=="$"){k.push(x.DOLLAR),O.push(W),W=V.charAt(H++);continue}if(W=="."){if(W=V.charAt(H++),W=="."){k.push(x.DOUBLEDOT),O.push(".."),W=V.charAt(H++);continue}if(W>="0"&&W<="9"){var ue="."+W;for(W=V.charAt(H++);W>="0"&&W<="9";)ue+=W,W=V.charAt(H++);k.push(x.NUMBER),O.push(ue);continue}k.push(x.DOT),O.push(".");continue}if(W=="'"||W=='"'){for(var Re=W,Ve="";H<V.length&&(W=V.charAt(H))!==Re;)Ve+=W,H+=1;if(W!==Re)throw hi.fromMessage("Unterminated string literal: "+Re+Ve);H+=1,k.push(x.LITERAL),O.push(Ve),W=V.charAt(H++);continue}if(W>="0"&&W<="9"){var ue=W;for(W=V.charAt(H++);W>="0"&&W<="9";)ue+=W,W=V.charAt(H++);if(W=="."&&V.charAt(H)>="0"&&V.charAt(H)<="9")for(ue+=W,ue+=V.charAt(H++),W=V.charAt(H++);W>="0"&&W<="9";)ue+=W,W=V.charAt(H++);k.push(x.NUMBER),O.push(ue);continue}if(W=="*"){if(k.length>0){var Ie=k[k.length-1];if(Ie!=x.AT&&Ie!=x.DOUBLECOLON&&Ie!=x.LEFTPARENTHESIS&&Ie!=x.LEFTBRACKET&&Ie!=x.AND&&Ie!=x.OR&&Ie!=x.MOD&&Ie!=x.DIV&&Ie!=x.MULTIPLYOPERATOR&&Ie!=x.SLASH&&Ie!=x.DOUBLESLASH&&Ie!=x.BAR&&Ie!=x.PLUS&&Ie!=x.MINUS&&Ie!=x.EQUALS&&Ie!=x.NOTEQUAL&&Ie!=x.LESSTHAN&&Ie!=x.LESSTHANOREQUAL&&Ie!=x.GREATERTHAN&&Ie!=x.GREATERTHANOREQUAL){k.push(x.MULTIPLYOPERATOR),O.push(W),W=V.charAt(H++);continue}}k.push(x.ASTERISKNAMETEST),O.push(W),W=V.charAt(H++);continue}if(W==":"&&V.charAt(H)==":"){k.push(x.DOUBLECOLON),O.push("::"),H++,W=V.charAt(H++);continue}if(W=="/"){if(W=V.charAt(H++),W=="/"){k.push(x.DOUBLESLASH),O.push("//"),W=V.charAt(H++);continue}k.push(x.SLASH),O.push("/");continue}if(W=="!"&&V.charAt(H)=="="){k.push(x.NOTEQUAL),O.push("!="),H++,W=V.charAt(H++);continue}if(W=="<"){if(V.charAt(H)=="="){k.push(x.LESSTHANOREQUAL),O.push("<="),H++,W=V.charAt(H++);continue}k.push(x.LESSTHAN),O.push("<"),W=V.charAt(H++);continue}if(W==">"){if(V.charAt(H)=="="){k.push(x.GREATERTHANOREQUAL),O.push(">="),H++,W=V.charAt(H++);continue}k.push(x.GREATERTHAN),O.push(">"),W=V.charAt(H++);continue}if(W=="_"||be.isLetter(W.charCodeAt(0))){var Fe=W;for(W=V.charAt(H++);be.isNCNameChar(W.charCodeAt(0));)Fe+=W,W=V.charAt(H++);if(k.length>0){var Ie=k[k.length-1];if(Ie!=x.AT&&Ie!=x.DOUBLECOLON&&Ie!=x.LEFTPARENTHESIS&&Ie!=x.LEFTBRACKET&&Ie!=x.AND&&Ie!=x.OR&&Ie!=x.MOD&&Ie!=x.DIV&&Ie!=x.MULTIPLYOPERATOR&&Ie!=x.SLASH&&Ie!=x.DOUBLESLASH&&Ie!=x.BAR&&Ie!=x.PLUS&&Ie!=x.MINUS&&Ie!=x.EQUALS&&Ie!=x.NOTEQUAL&&Ie!=x.LESSTHAN&&Ie!=x.LESSTHANOREQUAL&&Ie!=x.GREATERTHAN&&Ie!=x.GREATERTHANOREQUAL){if(Fe=="and"){k.push(x.AND),O.push(Fe);continue}if(Fe=="or"){k.push(x.OR),O.push(Fe);continue}if(Fe=="mod"){k.push(x.MOD),O.push(Fe);continue}if(Fe=="div"){k.push(x.DIV),O.push(Fe);continue}}}if(W==":"){if(V.charAt(H)=="*"){k.push(x.NCNAMECOLONASTERISK),O.push(Fe+":*"),H++,W=V.charAt(H++);continue}if(V.charAt(H)=="_"||be.isLetter(V.charCodeAt(H))){for(Fe+=":",W=V.charAt(H++);be.isNCNameChar(W.charCodeAt(0));)Fe+=W,W=V.charAt(H++);if(W=="("){k.push(x.FUNCTIONNAME),O.push(Fe);continue}k.push(x.QNAME),O.push(Fe);continue}if(V.charAt(H)==":"){k.push(x.AXISNAME),O.push(Fe);continue}}if(W=="("){if(Fe=="comment"||Fe=="text"||Fe=="node"){k.push(x.NODETYPE),O.push(Fe);continue}if(Fe=="processing-instruction"){V.charAt(H)==")"?k.push(x.NODETYPE):k.push(x.PROCESSINGINSTRUCTIONWITHLITERAL),O.push(Fe);continue}k.push(x.FUNCTIONNAME),O.push(Fe);continue}k.push(x.QNAME),O.push(Fe);continue}throw new Error("Unexpected character "+W)}return k.push(1),O.push("[EOF]"),[k,O]},x.SHIFT="s",x.REDUCE="r",x.ACCEPT="a",x.prototype.parse=function(Ve){if(!Ve)throw new Error("XPath expression unspecified.");if(typeof Ve!="string")throw new Error("XPath expression must be a string.");var k,O,V=this.tokenize(Ve);if(V!=null){k=V[0],O=V[1];var H=0,W=[],ue=[],Re=[],Ve,Ie,Fe;for(W.push(0),ue.push(1),Re.push("_S"),Ie=k[H],Fe=O[H++];;)switch(Ve=W[W.length-1],x.actionTable[Ve].charAt(Ie-1)){case x.SHIFT:ue.push(-Ie),Re.push(Fe),W.push(x.actionTableNumber[Ve].charCodeAt(Ie-1)-32),Ie=k[H],Fe=O[H++];break;case x.REDUCE:for(var In=x.productions[x.actionTableNumber[Ve].charCodeAt(Ie-1)-32][1],ks=[],Ss=0;Ss<In;Ss++)ue.pop(),ks.unshift(Re.pop()),W.pop();var Ds=W[W.length-1];ue.push(x.productions[x.actionTableNumber[Ve].charCodeAt(Ie-1)-32][0]),this.reduceActions[x.actionTableNumber[Ve].charCodeAt(Ie-1)-32]==null?Re.push(ks[0]):Re.push(this.reduceActions[x.actionTableNumber[Ve].charCodeAt(Ie-1)-32](ks)),W.push(x.gotoTable[Ds].charCodeAt(x.productions[x.actionTableNumber[Ve].charCodeAt(Ie-1)-32][0]-2)-33);break;case x.ACCEPT:return new Q(Re.pop());default:throw new Error("XPath parse error")}}},Q.prototype=new Object,Q.prototype.constructor=Q,Q.superclass=Object.prototype;function Q(A){this.expression=A}Q.prototype.toString=function(){return this.expression.toString()};function R(A,k,O){k in A||(A[k]=O)}Q.prototype.evaluate=function(A){var k=A.expressionContextNode;if(!(t(k)||n(k)))throw new Error("Context node does not appear to be a valid DOM node.");return A.contextNode=A.expressionContextNode,A.contextSize=1,A.contextPosition=1,A.isHtml&&(R(A,"caseInsensitive",!0),R(A,"allowAnyNamespaceForNoPrefix",!0)),R(A,"caseInsensitive",!1),this.expression.evaluate(A)},Q.XML_NAMESPACE_URI="http://www.w3.org/XML/1998/namespace",Q.XMLNS_NAMESPACE_URI="http://www.w3.org/2000/xmlns/",U.prototype=new Object,U.prototype.constructor=U,U.superclass=Object.prototype;function U(){}U.prototype.init=function(){},U.prototype.toString=function(){return"<Expression>"},U.prototype.evaluate=function(A){throw new Error("Could not evaluate expression.")},T.prototype=new U,T.prototype.constructor=T,T.superclass=U.prototype;function T(A){arguments.length>0&&this.init(A)}T.prototype.init=function(A){this.rhs=A},D.prototype=new T,D.prototype.constructor=D,D.superclass=T.prototype;function D(A){arguments.length>0&&this.init(A)}D.prototype.init=function(A){D.superclass.init.call(this,A)},D.prototype.evaluate=function(A){return this.rhs.evaluate(A).number().negate()},D.prototype.toString=function(){return"-"+this.rhs.toString()},b.prototype=new U,b.prototype.constructor=b,b.superclass=U.prototype;function b(A,k){arguments.length>0&&this.init(A,k)}b.prototype.init=function(A,k){this.lhs=A,this.rhs=k},_.prototype=new b,_.prototype.constructor=_,_.superclass=b.prototype;function _(A,k){arguments.length>0&&this.init(A,k)}_.prototype.init=function(A,k){_.superclass.init.call(this,A,k)},_.prototype.toString=function(){return"("+this.lhs.toString()+" or "+this.rhs.toString()+")"},_.prototype.evaluate=function(A){var k=this.lhs.evaluate(A).bool();return k.booleanValue()?k:this.rhs.evaluate(A).bool()},v.prototype=new b,v.prototype.constructor=v,v.superclass=b.prototype;function v(A,k){arguments.length>0&&this.init(A,k)}v.prototype.init=function(A,k){v.superclass.init.call(this,A,k)},v.prototype.toString=function(){return"("+this.lhs.toString()+" and "+this.rhs.toString()+")"},v.prototype.evaluate=function(A){var k=this.lhs.evaluate(A).bool();return k.booleanValue()?this.rhs.evaluate(A).bool():k},E.prototype=new b,E.prototype.constructor=E,E.superclass=b.prototype;function E(A,k){arguments.length>0&&this.init(A,k)}E.prototype.init=function(A,k){E.superclass.init.call(this,A,k)},E.prototype.toString=function(){return"("+this.lhs.toString()+" = "+this.rhs.toString()+")"},E.prototype.evaluate=function(A){return this.lhs.evaluate(A).equals(this.rhs.evaluate(A))},P.prototype=new b,P.prototype.constructor=P,P.superclass=b.prototype;function P(A,k){arguments.length>0&&this.init(A,k)}P.prototype.init=function(A,k){P.superclass.init.call(this,A,k)},P.prototype.toString=function(){return"("+this.lhs.toString()+" != "+this.rhs.toString()+")"},P.prototype.evaluate=function(A){return this.lhs.evaluate(A).notequal(this.rhs.evaluate(A))},N.prototype=new b,N.prototype.constructor=N,N.superclass=b.prototype;function N(A,k){arguments.length>0&&this.init(A,k)}N.prototype.init=function(A,k){N.superclass.init.call(this,A,k)},N.prototype.evaluate=function(A){return this.lhs.evaluate(A).lessthan(this.rhs.evaluate(A))},N.prototype.toString=function(){return"("+this.lhs.toString()+" < "+this.rhs.toString()+")"},j.prototype=new b,j.prototype.constructor=j,j.superclass=b.prototype;function j(A,k){arguments.length>0&&this.init(A,k)}j.prototype.init=function(A,k){j.superclass.init.call(this,A,k)},j.prototype.evaluate=function(A){return this.lhs.evaluate(A).greaterthan(this.rhs.evaluate(A))},j.prototype.toString=function(){return"("+this.lhs.toString()+" > "+this.rhs.toString()+")"},z.prototype=new b,z.prototype.constructor=z,z.superclass=b.prototype;function z(A,k){arguments.length>0&&this.init(A,k)}z.prototype.init=function(A,k){z.superclass.init.call(this,A,k)},z.prototype.evaluate=function(A){return this.lhs.evaluate(A).lessthanorequal(this.rhs.evaluate(A))},z.prototype.toString=function(){return"("+this.lhs.toString()+" <= "+this.rhs.toString()+")"},L.prototype=new b,L.prototype.constructor=L,L.superclass=b.prototype;function L(A,k){arguments.length>0&&this.init(A,k)}L.prototype.init=function(A,k){L.superclass.init.call(this,A,k)},L.prototype.evaluate=function(A){return this.lhs.evaluate(A).greaterthanorequal(this.rhs.evaluate(A))},L.prototype.toString=function(){return"("+this.lhs.toString()+" >= "+this.rhs.toString()+")"},G.prototype=new b,G.prototype.constructor=G,G.superclass=b.prototype;function G(A,k){arguments.length>0&&this.init(A,k)}G.prototype.init=function(A,k){G.superclass.init.call(this,A,k)},G.prototype.evaluate=function(A){return this.lhs.evaluate(A).number().plus(this.rhs.evaluate(A).number())},G.prototype.toString=function(){return"("+this.lhs.toString()+" + "+this.rhs.toString()+")"},K.prototype=new b,K.prototype.constructor=K,K.superclass=b.prototype;function K(A,k){arguments.length>0&&this.init(A,k)}K.prototype.init=function(A,k){K.superclass.init.call(this,A,k)},K.prototype.evaluate=function(A){return this.lhs.evaluate(A).number().minus(this.rhs.evaluate(A).number())},K.prototype.toString=function(){return"("+this.lhs.toString()+" - "+this.rhs.toString()+")"},$.prototype=new b,$.prototype.constructor=$,$.superclass=b.prototype;function $(A,k){arguments.length>0&&this.init(A,k)}$.prototype.init=function(A,k){$.superclass.init.call(this,A,k)},$.prototype.evaluate=function(A){return this.lhs.evaluate(A).number().multiply(this.rhs.evaluate(A).number())},$.prototype.toString=function(){return"("+this.lhs.toString()+" * "+this.rhs.toString()+")"},te.prototype=new b,te.prototype.constructor=te,te.superclass=b.prototype;function te(A,k){arguments.length>0&&this.init(A,k)}te.prototype.init=function(A,k){te.superclass.init.call(this,A,k)},te.prototype.evaluate=function(A){return this.lhs.evaluate(A).number().div(this.rhs.evaluate(A).number())},te.prototype.toString=function(){return"("+this.lhs.toString()+" div "+this.rhs.toString()+")"},re.prototype=new b,re.prototype.constructor=re,re.superclass=b.prototype;function re(A,k){arguments.length>0&&this.init(A,k)}re.prototype.init=function(A,k){re.superclass.init.call(this,A,k)},re.prototype.evaluate=function(A){return this.lhs.evaluate(A).number().mod(this.rhs.evaluate(A).number())},re.prototype.toString=function(){return"("+this.lhs.toString()+" mod "+this.rhs.toString()+")"},ee.prototype=new b,ee.prototype.constructor=ee,ee.superclass=b.prototype;function ee(A,k){arguments.length>0&&this.init(A,k)}ee.prototype.init=function(A,k){ee.superclass.init.call(this,A,k)},ee.prototype.evaluate=function(A){return this.lhs.evaluate(A).nodeset().union(this.rhs.evaluate(A).nodeset())},ee.prototype.toString=function(){return c(f,[this.lhs,this.rhs]).join(" | ")},Z.prototype=new U,Z.prototype.constructor=Z,Z.superclass=U.prototype;function Z(A,k,O){arguments.length>0&&this.init(A,k,O)}Z.prototype.init=function(A,k,O){Z.superclass.init.call(this),this.filter=A,this.filterPredicates=k,this.locationPath=O};function me(A){for(;A&&A.parentNode;)A=A.parentNode;return A}var le=function(A,k,O,V){if(A.length===0)return O;var H=k.extend({});return a(function(W,ue){return H.contextSize=W.length,l(function(Re,Ve){return H.contextNode=Re,H.contextPosition=Ve+1,Z.predicateMatches(ue,H)},W)},I(O,V),A)};Z.getRoot=function(A,k){var O=k[0];if(O&&O.nodeType===C.DOCUMENT_NODE)return O;if(A.virtualRoot)return A.virtualRoot;if(!O)throw new Error("Context node not found when determining document root.");var V=O.ownerDocument;if(V)return V;for(var H=O;H.parentNode!=null;)H=H.parentNode;return H};var Ae=function(A){var k=String(A.name);return k==="xmlns"?"":k.substring(0,6)==="xmlns:"?k.substring(6,k.length):null};Z.applyStep=function(A,k,O){if(!O)throw new Error("Context node not found when evaluating XPath step: "+A);var V=[];switch(k.contextNode=O,A.axis){case se.ANCESTOR:if(k.contextNode===k.virtualRoot)break;var H;for(k.contextNode.nodeType==C.ATTRIBUTE_NODE?H=Z.getOwnerElement(k.contextNode):H=k.contextNode.parentNode;H!=null&&(A.nodeTest.matches(H,k)&&V.push(H),H!==k.virtualRoot);)H=H.parentNode;break;case se.ANCESTORORSELF:for(var H=k.contextNode;H!=null&&(A.nodeTest.matches(H,k)&&V.push(H),H!==k.virtualRoot);H=H.nodeType==C.ATTRIBUTE_NODE?Z.getOwnerElement(H):H.parentNode);break;case se.ATTRIBUTE:var W=k.contextNode.attributes;if(W!=null)for(var ue=0;ue<W.length;ue++){var H=W.item(ue);A.nodeTest.matches(H,k)&&V.push(H)}break;case se.CHILD:for(var H=k.contextNode.firstChild;H!=null;H=H.nextSibling)A.nodeTest.matches(H,k)&&V.push(H);break;case se.DESCENDANT:for(var Fe=[k.contextNode.firstChild];Fe.length>0;)for(var H=Fe.pop();H!=null;)A.nodeTest.matches(H,k)&&V.push(H),H.firstChild!=null?(Fe.push(H.nextSibling),H=H.firstChild):H=H.nextSibling;break;case se.DESCENDANTORSELF:A.nodeTest.matches(k.contextNode,k)&&V.push(k.contextNode);for(var Fe=[k.contextNode.firstChild];Fe.length>0;)for(var H=Fe.pop();H!=null;)A.nodeTest.matches(H,k)&&V.push(H),H.firstChild!=null?(Fe.push(H.nextSibling),H=H.firstChild):H=H.nextSibling;break;case se.FOLLOWING:if(k.contextNode===k.virtualRoot)break;var Fe=[];k.contextNode.firstChild!=null?Fe.unshift(k.contextNode.firstChild):Fe.unshift(k.contextNode.nextSibling);for(var H=k.contextNode.parentNode;H!=null&&H.nodeType!=C.DOCUMENT_NODE&&H!==k.virtualRoot;H=H.parentNode)Fe.unshift(H.nextSibling);do for(var H=Fe.pop();H!=null;)A.nodeTest.matches(H,k)&&V.push(H),H.firstChild!=null?(Fe.push(H.nextSibling),H=H.firstChild):H=H.nextSibling;while(Fe.length>0);break;case se.FOLLOWINGSIBLING:if(k.contextNode===k.virtualRoot)break;for(var H=k.contextNode.nextSibling;H!=null;H=H.nextSibling)A.nodeTest.matches(H,k)&&V.push(H);break;case se.NAMESPACE:var Re={};if(k.contextNode.nodeType==C.ELEMENT_NODE){Re.xml=new ji("xml",null,Q.XML_NAMESPACE_URI,k.contextNode);for(var H=k.contextNode;H!=null&&H.nodeType==C.ELEMENT_NODE;H=H.parentNode)for(var ue=0;ue<H.attributes.length;ue++){var Ve=H.attributes.item(ue),Ie=Ae(Ve);Ie!=null&&Re[Ie]==null&&(Re[Ie]=new ji(Ie,Ve,Ve.value,k.contextNode))}for(var Ie in Re){var O=Re[Ie];A.nodeTest.matches(O,k)&&V.push(O)}}break;case se.PARENT:H=null,k.contextNode!==k.virtualRoot&&(k.contextNode.nodeType==C.ATTRIBUTE_NODE?H=Z.getOwnerElement(k.contextNode):H=k.contextNode.parentNode),H!=null&&A.nodeTest.matches(H,k)&&V.push(H);break;case se.PRECEDING:var Fe;k.virtualRoot!=null?Fe=[k.virtualRoot]:Fe=[me(k.contextNode)];e:for(;Fe.length>0;)for(var H=Fe.pop();H!=null;){if(H==k.contextNode)break e;A.nodeTest.matches(H,k)&&V.unshift(H),H.firstChild!=null?(Fe.push(H.nextSibling),H=H.firstChild):H=H.nextSibling}break;case se.PRECEDINGSIBLING:if(k.contextNode===k.virtualRoot)break;for(var H=k.contextNode.previousSibling;H!=null;H=H.previousSibling)A.nodeTest.matches(H,k)&&V.push(H);break;case se.SELF:A.nodeTest.matches(k.contextNode,k)&&V.push(k.contextNode);break;default:}return V};function ve(A,k,O){return le(A.predicates,k,Z.applyStep(A,k,O),d(rt,A.axis))}function qe(A,k,O){return w(c(ve.bind(null,O,A),k))}Z.applySteps=function(A,k,O){return a(qe.bind(null,k),O,A)},Z.prototype.applyFilter=function(A,k){if(!this.filter)return{nodes:[A.contextNode]};var O=this.filter.evaluate(A);if(!be.instance_of(O,Ce)){if(this.filterPredicates!=null&&this.filterPredicates.length>0||this.locationPath!=null)throw new Error("Path expression filter must evaluate to a nodeset if predicates or location path are used");return{nonNodes:O}}return{nodes:le(this.filterPredicates||[],k,O.toUnsortedArray(),!1)}},Z.applyLocationPath=function(A,k,O){if(!A)return O;var V=A.absolute?[Z.getRoot(k,O)]:O;return Z.applySteps(A.steps,k,V)},Z.prototype.evaluate=function(A){var k=B(new si,A),O=this.applyFilter(A,k);if("nonNodes"in O)return O.nonNodes;var V=new Ce;return V.addArray(Z.applyLocationPath(this.locationPath,k,O.nodes)),V},Z.predicateMatches=function(A,k){var O=A.evaluate(k);return be.instance_of(O,Ee)?k.contextPosition===O.numberValue():O.booleanValue()},Z.predicateString=function(A){return g("[","]",A.toString())},Z.predicatesString=function(A){return h("",c(Z.predicateString,A))},Z.prototype.toString=function(){if(this.filter!=null){var A=f(this.filter);return be.instance_of(this.filter,we)?g("'","'",A):this.filterPredicates!=null&&this.filterPredicates.length?g("(",")",A)+Z.predicatesString(this.filterPredicates):this.locationPath!=null?A+(this.locationPath.absolute?"":"/")+f(this.locationPath):A}return f(this.locationPath)},Z.getOwnerElement=function(A){if(A.ownerElement)return A.ownerElement;try{if(A.selectSingleNode)return A.selectSingleNode("..")}catch{}for(var k=A.nodeType==C.DOCUMENT_NODE?A:A.ownerDocument,O=k.getElementsByTagName("*"),V=0;V<O.length;V++)for(var H=O.item(V),W=H.attributes,ue=0;ue<W.length;ue++){var Re=W.item(ue);if(Re===A)return H}return null},_e.prototype=new Object,_e.prototype.constructor=_e,_e.superclass=Object.prototype;function _e(A,k){arguments.length>0&&this.init(A,k)}_e.prototype.init=function(A,k){this.absolute=A,this.steps=k},_e.prototype.toString=function(){return(this.absolute?"/":"")+c(f,this.steps).join("/")},se.prototype=new Object,se.prototype.constructor=se,se.superclass=Object.prototype;function se(A,k,O){arguments.length>0&&this.init(A,k,O)}se.prototype.init=function(A,k,O){this.axis=A,this.nodeTest=k,this.predicates=O},se.prototype.toString=function(){return se.STEPNAMES[this.axis]+"::"+this.nodeTest.toString()+Z.predicatesString(this.predicates)},se.ANCESTOR=0,se.ANCESTORORSELF=1,se.ATTRIBUTE=2,se.CHILD=3,se.DESCENDANT=4,se.DESCENDANTORSELF=5,se.FOLLOWING=6,se.FOLLOWINGSIBLING=7,se.NAMESPACE=8,se.PARENT=9,se.PRECEDING=10,se.PRECEDINGSIBLING=11,se.SELF=12,se.STEPNAMES=a(function(A,k){return A[k[0]]=k[1],A},{},[[se.ANCESTOR,"ancestor"],[se.ANCESTORORSELF,"ancestor-or-self"],[se.ATTRIBUTE,"attribute"],[se.CHILD,"child"],[se.DESCENDANT,"descendant"],[se.DESCENDANTORSELF,"descendant-or-self"],[se.FOLLOWING,"following"],[se.FOLLOWINGSIBLING,"following-sibling"],[se.NAMESPACE,"namespace"],[se.PARENT,"parent"],[se.PRECEDING,"preceding"],[se.PRECEDINGSIBLING,"preceding-sibling"],[se.SELF,"self"]]);var rt=[se.ANCESTOR,se.ANCESTORORSELF,se.PARENT,se.PRECEDING,se.PRECEDINGSIBLING];ce.prototype=new Object,ce.prototype.constructor=ce,ce.superclass=Object.prototype;function ce(A,k){arguments.length>0&&this.init(A,k)}ce.prototype.init=function(A,k){this.type=A,this.value=k},ce.prototype.toString=function(){return"<unknown nodetest type>"},ce.prototype.matches=function(A,k){console.warn("unknown node test type")},ce.NAMETESTANY=0,ce.NAMETESTPREFIXANY=1,ce.NAMETESTQNAME=2,ce.COMMENT=3,ce.TEXT=4,ce.PI=5,ce.NODE=6,ce.isNodeType=function(A){return function(k){return d(A,k.nodeType)}},ce.makeNodeTestType=function(A,k,O){var V=O||function(){};return V.prototype=new ce(A),V.prototype.constructor=V,B(V.prototype,k),V},ce.makeNodeTypeTest=function(A,k,O){return new(ce.makeNodeTestType(A,{matches:ce.isNodeType(k),toString:u(O)}))},ce.hasPrefix=function(A){return A.prefix||(A.nodeName||A.tagName).indexOf(":")!==-1},ce.isElementOrAttribute=ce.isNodeType([1,2]),ce.nameSpaceMatches=function(A,k,O){var V=O.namespaceURI||"";if(!A)return!V||k.allowAnyNamespaceForNoPrefix&&!ce.hasPrefix(O);var H=k.namespaceResolver.getNamespace(A,k.expressionContextNode);if(H==null)throw new Error("Cannot resolve QName "+A);return H===V},ce.localNameMatches=function(A,k,O){var V=O.localName||O.nodeName;return k.caseInsensitive?A.toLowerCase()===V.toLowerCase():A===V},ce.NameTestPrefixAny=ce.makeNodeTestType(ce.NAMETESTPREFIXANY,{matches:function(A,k){return ce.isElementOrAttribute(A)&&ce.nameSpaceMatches(this.prefix,k,A)},toString:function(){return this.prefix+":*"}},function(k){this.prefix=k}),ce.NameTestQName=ce.makeNodeTestType(ce.NAMETESTQNAME,{matches:function(A,k){return ce.isNodeType([C.ELEMENT_NODE,C.ATTRIBUTE_NODE,C.NAMESPACE_NODE])(A)&&ce.nameSpaceMatches(this.prefix,k,A)&&ce.localNameMatches(this.localName,k,A)},toString:function(){return this.name}},function(k){var O=k.split(":");this.name=k,this.prefix=O.length>1?O[0]:null,this.localName=O[O.length>1?1:0]}),ce.PITest=ce.makeNodeTestType(ce.PI,{matches:function(A,k){return ce.isNodeType([C.PROCESSING_INSTRUCTION_NODE])(A)&&(A.target||A.nodeName)===this.name},toString:function(){return g('processing-instruction("','")',this.name)}},function(A){this.name=A}),ce.nameTestAny=ce.makeNodeTypeTest(ce.NAMETESTANY,[C.ELEMENT_NODE,C.ATTRIBUTE_NODE,C.NAMESPACE_NODE],"*"),ce.textTest=ce.makeNodeTypeTest(ce.TEXT,[C.TEXT_NODE,C.CDATA_SECTION_NODE],"text()"),ce.commentTest=ce.makeNodeTypeTest(ce.COMMENT,[C.COMMENT_NODE],"comment()"),ce.nodeTest=ce.makeNodeTypeTest(ce.NODE,[C.ELEMENT_NODE,C.ATTRIBUTE_NODE,C.TEXT_NODE,C.CDATA_SECTION_NODE,C.PROCESSING_INSTRUCTION_NODE,C.COMMENT_NODE,C.DOCUMENT_NODE],"node()"),ce.anyPiTest=ce.makeNodeTypeTest(ce.PI,[C.PROCESSING_INSTRUCTION_NODE],"processing-instruction()"),dr.prototype=new U,dr.prototype.constructor=dr,dr.superclass=U.prototype;function dr(A){arguments.length>0&&this.init(A)}dr.prototype.init=function(A){this.variable=A},dr.prototype.toString=function(){return"$"+this.variable},dr.prototype.evaluate=function(A){var k=be.resolveQName(this.variable,A.namespaceResolver,A.contextNode,!1);if(k[0]==null)throw new Error("Cannot resolve QName "+fn);var O=A.variableResolver.getVariable(k[1],k[0]);if(!O)throw hi.fromMessage("Undeclared variable: "+this.toString());return O},Mr.prototype=new U,Mr.prototype.constructor=Mr,Mr.superclass=U.prototype;function Mr(A,k){arguments.length>0&&this.init(A,k)}Mr.prototype.init=function(A,k){this.functionName=A,this.arguments=k},Mr.prototype.toString=function(){for(var A=this.functionName+"(",k=0;k<this.arguments.length;k++)k>0&&(A+=", "),A+=this.arguments[k].toString();return A+")"},Mr.prototype.evaluate=function(A){var k=tr.getFunctionFromContext(this.functionName,A);if(!k)throw new Error("Unknown function "+this.functionName);var O=[A].concat(this.arguments);return k.apply(A.functionResolver.thisArg,O)};var at=new Object;at.equals=function(A,k){return A.equals(k)},at.notequal=function(A,k){return A.notequal(k)},at.lessthan=function(A,k){return A.lessthan(k)},at.greaterthan=function(A,k){return A.greaterthan(k)},at.lessthanorequal=function(A,k){return A.lessthanorequal(k)},at.greaterthanorequal=function(A,k){return A.greaterthanorequal(k)},we.prototype=new U,we.prototype.constructor=we,we.superclass=U.prototype;function we(A){arguments.length>0&&this.init(A)}we.prototype.init=function(A){this.str=String(A)},we.prototype.toString=function(){return this.str},we.prototype.evaluate=function(A){return this},we.prototype.string=function(){return this},we.prototype.number=function(){return new Ee(this.str)},we.prototype.bool=function(){return new Se(this.str)},we.prototype.nodeset=function(){throw new Error("Cannot convert string to nodeset")},we.prototype.stringValue=function(){return this.str},we.prototype.numberValue=function(){return this.number().numberValue()},we.prototype.booleanValue=function(){return this.bool().booleanValue()},we.prototype.equals=function(A){return be.instance_of(A,Se)?this.bool().equals(A):be.instance_of(A,Ee)?this.number().equals(A):be.instance_of(A,Ce)?A.compareWithString(this,at.equals):new Se(this.str==A.str)},we.prototype.notequal=function(A){return be.instance_of(A,Se)?this.bool().notequal(A):be.instance_of(A,Ee)?this.number().notequal(A):be.instance_of(A,Ce)?A.compareWithString(this,at.notequal):new Se(this.str!=A.str)},we.prototype.lessthan=function(A){return this.number().lessthan(A)},we.prototype.greaterthan=function(A){return this.number().greaterthan(A)},we.prototype.lessthanorequal=function(A){return this.number().lessthanorequal(A)},we.prototype.greaterthanorequal=function(A){return this.number().greaterthanorequal(A)},Ee.prototype=new U,Ee.prototype.constructor=Ee,Ee.superclass=U.prototype;function Ee(A){arguments.length>0&&this.init(A)}Ee.prototype.init=function(A){this.num=typeof A=="string"?this.parse(A):Number(A)},Ee.prototype.numberFormat=/^\s*-?[0-9]*\.?[0-9]+\s*$/,Ee.prototype.parse=function(A){return this.numberFormat.test(A)?parseFloat(A):Number.NaN};function Ji(A){for(var k=A.split("e-"),O=k[0].replace(".",""),V=Number(k[1]),H=0;H<V-1;H+=1)O="0"+O;return"0."+O}function vo(A){for(var k=A.split("e"),O=k[0].replace(".",""),V=Number(k[1]),H=V+1-O.length,W=0;W<H;W+=1)O+="0";return O}Ee.prototype.toString=function(){var A=this.num.toString();return A.indexOf("e-")!==-1?Ji(A):A.indexOf("e")!==-1?vo(A):A},Ee.prototype.evaluate=function(A){return this},Ee.prototype.string=function(){return new we(this.toString())},Ee.prototype.number=function(){return this},Ee.prototype.bool=function(){return new Se(this.num)},Ee.prototype.nodeset=function(){throw new Error("Cannot convert number to nodeset")},Ee.prototype.stringValue=function(){return this.string().stringValue()},Ee.prototype.numberValue=function(){return this.num},Ee.prototype.booleanValue=function(){return this.bool().booleanValue()},Ee.prototype.negate=function(){return new Ee(-this.num)},Ee.prototype.equals=function(A){return be.instance_of(A,Se)?this.bool().equals(A):be.instance_of(A,we)?this.equals(A.number()):be.instance_of(A,Ce)?A.compareWithNumber(this,at.equals):new Se(this.num==A.num)},Ee.prototype.notequal=function(A){return be.instance_of(A,Se)?this.bool().notequal(A):be.instance_of(A,we)?this.notequal(A.number()):be.instance_of(A,Ce)?A.compareWithNumber(this,at.notequal):new Se(this.num!=A.num)},Ee.prototype.lessthan=function(A){return be.instance_of(A,Ce)?A.compareWithNumber(this,at.greaterthan):be.instance_of(A,Se)||be.instance_of(A,we)?this.lessthan(A.number()):new Se(this.num<A.num)},Ee.prototype.greaterthan=function(A){return be.instance_of(A,Ce)?A.compareWithNumber(this,at.lessthan):be.instance_of(A,Se)||be.instance_of(A,we)?this.greaterthan(A.number()):new Se(this.num>A.num)},Ee.prototype.lessthanorequal=function(A){return be.instance_of(A,Ce)?A.compareWithNumber(this,at.greaterthanorequal):be.instance_of(A,Se)||be.instance_of(A,we)?this.lessthanorequal(A.number()):new Se(this.num<=A.num)},Ee.prototype.greaterthanorequal=function(A){return be.instance_of(A,Ce)?A.compareWithNumber(this,at.lessthanorequal):be.instance_of(A,Se)||be.instance_of(A,we)?this.greaterthanorequal(A.number()):new Se(this.num>=A.num)},Ee.prototype.plus=function(A){return new Ee(this.num+A.num)},Ee.prototype.minus=function(A){return new Ee(this.num-A.num)},Ee.prototype.multiply=function(A){return new Ee(this.num*A.num)},Ee.prototype.div=function(A){return new Ee(this.num/A.num)},Ee.prototype.mod=function(A){return new Ee(this.num%A.num)},Se.prototype=new U,Se.prototype.constructor=Se,Se.superclass=U.prototype;function Se(A){arguments.length>0&&this.init(A)}Se.prototype.init=function(A){this.b=!!A},Se.prototype.toString=function(){return this.b.toString()},Se.prototype.evaluate=function(A){return this},Se.prototype.string=function(){return new we(this.b)},Se.prototype.number=function(){return new Ee(this.b)},Se.prototype.bool=function(){return this},Se.prototype.nodeset=function(){throw new Error("Cannot convert boolean to nodeset")},Se.prototype.stringValue=function(){return this.string().stringValue()},Se.prototype.numberValue=function(){return this.number().numberValue()},Se.prototype.booleanValue=function(){return this.b},Se.prototype.not=function(){return new Se(!this.b)},Se.prototype.equals=function(A){return be.instance_of(A,we)||be.instance_of(A,Ee)?this.equals(A.bool()):be.instance_of(A,Ce)?A.compareWithBoolean(this,at.equals):new Se(this.b==A.b)},Se.prototype.notequal=function(A){return be.instance_of(A,we)||be.instance_of(A,Ee)?this.notequal(A.bool()):be.instance_of(A,Ce)?A.compareWithBoolean(this,at.notequal):new Se(this.b!=A.b)},Se.prototype.lessthan=function(A){return this.number().lessthan(A)},Se.prototype.greaterthan=function(A){return this.number().greaterthan(A)},Se.prototype.lessthanorequal=function(A){return this.number().lessthanorequal(A)},Se.prototype.greaterthanorequal=function(A){return this.number().greaterthanorequal(A)},Se.true_=new Se(!0),Se.false_=new Se(!1),Dt.prototype=new Object,Dt.prototype.constructor=Dt,Dt.superclass=Object.prototype;function Dt(A){this.init(A)}Dt.prototype.init=function(A){this.left=null,this.right=null,this.node=A,this.depth=1},Dt.prototype.balance=function(){var A=this.left==null?0:this.left.depth,k=this.right==null?0:this.right.depth;if(A>k+1){var O=this.left.left==null?0:this.left.left.depth,V=this.left.right==null?0:this.left.right.depth;O<V&&this.left.rotateRR(),this.rotateLL()}else if(A+1<k){var H=this.right.right==null?0:this.right.right.depth,W=this.right.left==null?0:this.right.left.depth;W>H&&this.right.rotateLL(),this.rotateRR()}},Dt.prototype.rotateLL=function(){var A=this.node,k=this.right;this.node=this.left.node,this.right=this.left,this.left=this.left.left,this.right.left=this.right.right,this.right.right=k,this.right.node=A,this.right.updateInNewLocation(),this.updateInNewLocation()},Dt.prototype.rotateRR=function(){var A=this.node,k=this.left;this.node=this.right.node,this.left=this.right,this.right=this.right.right,this.left.right=this.left.left,this.left.left=k,this.left.node=A,this.left.updateInNewLocation(),this.updateInNewLocation()},Dt.prototype.updateInNewLocation=function(){this.getDepthFromChildren()},Dt.prototype.getDepthFromChildren=function(){this.depth=this.node==null?0:1,this.left!=null&&(this.depth=this.left.depth+1),this.right!=null&&this.depth<=this.right.depth&&(this.depth=this.right.depth+1)};function vs(A,k){if(A===k)return 0;if(A.compareDocumentPosition){var O=A.compareDocumentPosition(k);return O&1||O&10?1:O&20?-1:0}for(var V=0,H=0,W=A;W!=null;W=W.parentNode||W.ownerElement)V++;for(var ue=k;ue!=null;ue=ue.parentNode||ue.ownerElement)H++;if(V>H){for(;V>H;)A=A.parentNode||A.ownerElement,V--;if(A===k)return 1}else if(H>V){for(;H>V;)k=k.parentNode||k.ownerElement,H--;if(A===k)return-1}for(var Re=A.parentNode||A.ownerElement,Ve=k.parentNode||k.ownerElement;Re!==Ve;)A=Re,k=Ve,Re=A.parentNode||A.ownerElement,Ve=k.parentNode||k.ownerElement;var Ie=xl(A),Fe=xl(k);if(Ie&&!Fe)return-1;if(!Ie&&Fe)return 1;if(A.isXPathNamespace){if(A.nodeValue===Q.XML_NAMESPACE_URI||!k.isXPathNamespace)return-1;if(k.nodeValue===Q.XML_NAMESPACE_URI)return 1}else if(k.isXPathNamespace)return 1;if(Re)for(var In=Ie?Re.attributes:Re.childNodes,ks=In.length,Ss=A.baseNode||A,Ds=k.baseNode||k,Vi=0;Vi<ks;Vi+=1){var Hn=In[Vi];if(Hn===Ss)return-1;if(Hn===Ds)return 1}throw new Error("Unexpected: could not determine node order")}Dt.prototype.add=function(A){if(A===this.node)return!1;var k=vs(A,this.node),O=!1;return k==-1?this.left==null?(this.left=new Dt(A),O=!0):(O=this.left.add(A),O&&this.balance()):k==1&&(this.right==null?(this.right=new Dt(A),O=!0):(O=this.right.add(A),O&&this.balance())),O&&this.getDepthFromChildren(),O},Ce.prototype=new U,Ce.prototype.constructor=Ce,Ce.superclass=U.prototype;function Ce(){this.init()}Ce.prototype.init=function(){this.tree=null,this.nodes=[],this.size=0},Ce.prototype.toString=function(){var A=this.first();return A==null?"":this.stringForNode(A)},Ce.prototype.evaluate=function(A){return this},Ce.prototype.string=function(){return new we(this.toString())},Ce.prototype.stringValue=function(){return this.toString()},Ce.prototype.number=function(){return new Ee(this.string())},Ce.prototype.numberValue=function(){return Number(this.string())},Ce.prototype.bool=function(){return new Se(this.booleanValue())},Ce.prototype.booleanValue=function(){return!!this.size},Ce.prototype.nodeset=function(){return this},Ce.prototype.stringForNode=function(A){return A.nodeType==C.DOCUMENT_NODE||A.nodeType==C.ELEMENT_NODE||A.nodeType===C.DOCUMENT_FRAGMENT_NODE?this.stringForContainerNode(A):A.nodeType===C.ATTRIBUTE_NODE?A.value||A.nodeValue:A.isNamespaceNode?A.namespace:A.nodeValue},Ce.prototype.stringForContainerNode=function(A){for(var k="",O=A.firstChild;O!=null;O=O.nextSibling){var V=O.nodeType;(V===1||V===3||V===4||V===9||V===11)&&(k+=this.stringForNode(O))}return k},Ce.prototype.buildTree=function(){if(!this.tree&&this.nodes.length){this.tree=new Dt(this.nodes[0]);for(var A=1;A<this.nodes.length;A+=1)this.tree.add(this.nodes[A])}return this.tree},Ce.prototype.first=function(){var A=this.buildTree();if(A==null)return null;for(;A.left!=null;)A=A.left;return A.node},Ce.prototype.add=function(A){for(var k=0;k<this.nodes.length;k+=1)if(A===this.nodes[k])return;this.tree=null,this.nodes.push(A),this.size+=1},Ce.prototype.addArray=function(A){var k=this;o(function(O){k.add(O)},A)},Ce.prototype.toArray=function(){var A=[];return this.toArrayRec(this.buildTree(),A),A},Ce.prototype.toArrayRec=function(A,k){A!=null&&(this.toArrayRec(A.left,k),k.push(A.node),this.toArrayRec(A.right,k))},Ce.prototype.toUnsortedArray=function(){return this.nodes.slice()},Ce.prototype.compareWithString=function(A,k){for(var O=this.toUnsortedArray(),V=0;V<O.length;V++){var H=O[V],W=new we(this.stringForNode(H)),ue=k(W,A);if(ue.booleanValue())return ue}return new Se(!1)},Ce.prototype.compareWithNumber=function(A,k){for(var O=this.toUnsortedArray(),V=0;V<O.length;V++){var H=O[V],W=new Ee(this.stringForNode(H)),ue=k(W,A);if(ue.booleanValue())return ue}return new Se(!1)},Ce.prototype.compareWithBoolean=function(A,k){return k(this.bool(),A)},Ce.prototype.compareWithNodeSet=function(A,k){for(var O=this.toUnsortedArray(),V=function(Re,Ve){return k(Ve,Re)},H=0;H<O.length;H++){var W=new we(this.stringForNode(O[H])),ue=A.compareWithString(W,V);if(ue.booleanValue())return ue}return new Se(!1)},Ce.compareWith=s(function(A,k){return be.instance_of(k,we)?this.compareWithString(k,A):be.instance_of(k,Ee)?this.compareWithNumber(k,A):be.instance_of(k,Se)?this.compareWithBoolean(k,A):this.compareWithNodeSet(k,A)}),Ce.prototype.equals=Ce.compareWith(at.equals),Ce.prototype.notequal=Ce.compareWith(at.notequal),Ce.prototype.lessthan=Ce.compareWith(at.lessthan),Ce.prototype.greaterthan=Ce.compareWith(at.greaterthan),Ce.prototype.lessthanorequal=Ce.compareWith(at.lessthanorequal),Ce.prototype.greaterthanorequal=Ce.compareWith(at.greaterthanorequal),Ce.prototype.union=function(A){var k=new Ce;return k.addArray(this.toUnsortedArray()),k.addArray(A.toUnsortedArray()),k},ji.prototype=new Object,ji.prototype.constructor=ji,ji.superclass=Object.prototype;function ji(A,k,O,V){this.isXPathNamespace=!0,this.baseNode=k,this.ownerDocument=V.ownerDocument,this.nodeName=A,this.prefix=A,this.localName=A,this.namespaceURI=null,this.nodeValue=O,this.ownerElement=V,this.nodeType=C.NAMESPACE_NODE}ji.prototype.toString=function(){return'{ "'+this.prefix+'", "'+this.namespaceURI+'" }'},si.prototype=new Object,si.prototype.constructor=si,si.superclass=Object.prototype;function si(A,k,O){this.variableResolver=A??new wr,this.namespaceResolver=k??new ur,this.functionResolver=O??new tr}si.prototype.extend=function(A){return B(new si,this,A)},wr.prototype=new Object,wr.prototype.constructor=wr,wr.superclass=Object.prototype;function wr(){}wr.prototype.getVariable=function(A,k){return null},tr.prototype=new Object,tr.prototype.constructor=tr,tr.superclass=Object.prototype;function tr(A){this.thisArg=A??Qe,this.functions=new Object,this.addStandardFunctions()}tr.prototype.addStandardFunctions=function(){this.functions["{}last"]=Qe.last,this.functions["{}position"]=Qe.position,this.functions["{}count"]=Qe.count,this.functions["{}id"]=Qe.id,this.functions["{}local-name"]=Qe.localName,this.functions["{}namespace-uri"]=Qe.namespaceURI,this.functions["{}name"]=Qe.name,this.functions["{}string"]=Qe.string,this.functions["{}concat"]=Qe.concat,this.functions["{}starts-with"]=Qe.startsWith,this.functions["{}contains"]=Qe.contains,this.functions["{}substring-before"]=Qe.substringBefore,this.functions["{}substring-after"]=Qe.substringAfter,this.functions["{}substring"]=Qe.substring,this.functions["{}string-length"]=Qe.stringLength,this.functions["{}normalize-space"]=Qe.normalizeSpace,this.functions["{}translate"]=Qe.translate,this.functions["{}boolean"]=Qe.boolean_,this.functions["{}not"]=Qe.not,this.functions["{}true"]=Qe.true_,this.functions["{}false"]=Qe.false_,this.functions["{}lang"]=Qe.lang,this.functions["{}number"]=Qe.number,this.functions["{}sum"]=Qe.sum,this.functions["{}floor"]=Qe.floor,this.functions["{}ceiling"]=Qe.ceiling,this.functions["{}round"]=Qe.round},tr.prototype.addFunction=function(A,k,O){this.functions["{"+A+"}"+k]=O},tr.getFunctionFromContext=function(A,k){var O=be.resolveQName(A,k.namespaceResolver,k.contextNode,!1);if(O[0]===null)throw new Error("Cannot resolve QName "+name);return k.functionResolver.getFunction(O[1],O[0])},tr.prototype.getFunction=function(A,k){return this.functions["{"+k+"}"+A]},ur.prototype=new Object,ur.prototype.constructor=ur,ur.superclass=Object.prototype;function ur(){}ur.prototype.getNamespace=function(A,k){if(A=="xml")return Q.XML_NAMESPACE_URI;if(A=="xmlns")return Q.XMLNS_NAMESPACE_URI;for(k.nodeType==C.DOCUMENT_NODE?k=k.documentElement:k.nodeType==C.ATTRIBUTE_NODE?k=Z.getOwnerElement(k):k.nodeType!=C.ELEMENT_NODE&&(k=k.parentNode);k!=null&&k.nodeType==C.ELEMENT_NODE;){for(var O=k.attributes,V=0;V<O.length;V++){var H=O.item(V),W=H.name||H.nodeName;if(W==="xmlns"&&A===""||W==="xmlns:"+A)return String(H.value||H.nodeValue)}k=k.parentNode}return null};var Qe=new Object;Qe.last=function(A){if(arguments.length!=1)throw new Error("Function last expects ()");return new Ee(A.contextSize)},Qe.position=function(A){if(arguments.length!=1)throw new Error("Function position expects ()");return new Ee(A.contextPosition)},Qe.count=function(){var A=arguments[0],k;if(arguments.length!=2||!be.instance_of(k=arguments[1].evaluate(A),Ce))throw new Error("Function count expects (node-set)");return new Ee(k.size)},Qe.id=function(){var A=arguments[0],k;if(arguments.length!=2)throw new Error("Function id expects (object)");k=arguments[1].evaluate(A),be.instance_of(k,Ce)?k=k.toArray().join(" "):k=k.stringValue();for(var O=k.split(/[\x0d\x0a\x09\x20]+/),V=0,H=new Ce,W=A.contextNode.nodeType==C.DOCUMENT_NODE?A.contextNode:A.contextNode.ownerDocument,ue=0;ue<O.length;ue++){var Re;W.getElementById?Re=W.getElementById(O[ue]):Re=be.getElementById(W,O[ue]),Re!=null&&(H.add(Re),V++)}return H},Qe.localName=function(A,k){var O;if(arguments.length==1)O=A.contextNode;else if(arguments.length==2)O=k.evaluate(A).first();else throw new Error("Function local-name expects (node-set?)");return O==null?new we(""):new we(O.localName||O.baseName||O.target||O.nodeName||"")},Qe.namespaceURI=function(){var A=arguments[0],k;if(arguments.length==1)k=A.contextNode;else if(arguments.length==2)k=arguments[1].evaluate(A).first();else throw new Error("Function namespace-uri expects (node-set?)");return k==null?new we(""):new we(k.namespaceURI||"")},Qe.name=function(){var A=arguments[0],k;if(arguments.length==1)k=A.contextNode;else if(arguments.length==2)k=arguments[1].evaluate(A).first();else throw new Error("Function name expects (node-set?)");return k==null?new we(""):k.nodeType==C.ELEMENT_NODE?new we(k.nodeName):k.nodeType==C.ATTRIBUTE_NODE?new we(k.name||k.nodeName):k.nodeType===C.PROCESSING_INSTRUCTION_NODE?new we(k.target||k.nodeName):k.localName==null?new we(""):new we(k.localName)},Qe.string=function(){var A=arguments[0];if(arguments.length==1)return new we(Ce.prototype.stringForNode(A.contextNode));if(arguments.length==2)return arguments[1].evaluate(A).string();throw new Error("Function string expects (object?)")},Qe.concat=function(A){if(arguments.length<3)throw new Error("Function concat expects (string, string[, string]*)");for(var k="",O=1;O<arguments.length;O++)k+=arguments[O].evaluate(A).stringValue();return new we(k)},Qe.startsWith=function(){var A=arguments[0];if(arguments.length!=3)throw new Error("Function startsWith expects (string, string)");var k=arguments[1].evaluate(A).stringValue(),O=arguments[2].evaluate(A).stringValue();return new Se(k.substring(0,O.length)==O)},Qe.contains=function(){var A=arguments[0];if(arguments.length!=3)throw new Error("Function contains expects (string, string)");var k=arguments[1].evaluate(A).stringValue(),O=arguments[2].evaluate(A).stringValue();return new Se(k.indexOf(O)!==-1)},Qe.substringBefore=function(){var A=arguments[0];if(arguments.length!=3)throw new Error("Function substring-before expects (string, string)");var k=arguments[1].evaluate(A).stringValue(),O=arguments[2].evaluate(A).stringValue();return new we(k.substring(0,k.indexOf(O)))},Qe.substringAfter=function(){var A=arguments[0];if(arguments.length!=3)throw new Error("Function substring-after expects (string, string)");var k=arguments[1].evaluate(A).stringValue(),O=arguments[2].evaluate(A).stringValue();if(O.length==0)return new we(k);var V=k.indexOf(O);return V==-1?new we(""):new we(k.substring(V+O.length))},Qe.substring=function(){var A=arguments[0];if(!(arguments.length==3||arguments.length==4))throw new Error("Function substring expects (string, number, number?)");var k=arguments[1].evaluate(A).stringValue(),O=Math.round(arguments[2].evaluate(A).numberValue())-1,V=arguments.length==4?O+Math.round(arguments[3].evaluate(A).numberValue()):void 0;return new we(k.substring(O,V))},Qe.stringLength=function(){var A=arguments[0],k;if(arguments.length==1)k=Ce.prototype.stringForNode(A.contextNode);else if(arguments.length==2)k=arguments[1].evaluate(A).stringValue();else throw new Error("Function string-length expects (string?)");return new Ee(k.length)},Qe.normalizeSpace=function(){var A=arguments[0],k;if(arguments.length==1)k=Ce.prototype.stringForNode(A.contextNode);else if(arguments.length==2)k=arguments[1].evaluate(A).stringValue();else throw new Error("Function normalize-space expects (string?)");for(var O=0,V=k.length-1;be.isSpace(k.charCodeAt(V));)V--;for(var H="";O<=V&&be.isSpace(k.charCodeAt(O));)O++;for(;O<=V;)if(be.isSpace(k.charCodeAt(O)))for(H+=" ";O<=V&&be.isSpace(k.charCodeAt(O));)O++;else H+=k.charAt(O),O++;return new we(H)},Qe.translate=function(A,k,O,V){if(arguments.length!=4)throw new Error("Function translate expects (string, string, string)");var H=k.evaluate(A).stringValue(),W=O.evaluate(A).stringValue(),ue=V.evaluate(A).stringValue(),Re=a(function(Ie,Fe,In){return Fe in Ie||(Ie[Fe]=In>ue.length?"":ue[In]),Ie},{},W),Ve=h("",c(function(Ie){return Ie in Re?Re[Ie]:Ie},H));return new we(Ve)},Qe.boolean_=function(){var A=arguments[0];if(arguments.length!=2)throw new Error("Function boolean expects (object)");return arguments[1].evaluate(A).bool()},Qe.not=function(A,k){if(arguments.length!=2)throw new Error("Function not expects (object)");return k.evaluate(A).bool().not()},Qe.true_=function(){if(arguments.length!=1)throw new Error("Function true expects ()");return Se.true_},Qe.false_=function(){if(arguments.length!=1)throw new Error("Function false expects ()");return Se.false_},Qe.lang=function(){var A=arguments[0];if(arguments.length!=2)throw new Error("Function lang expects (string)");for(var k,O=A.contextNode;O!=null&&O.nodeType!=C.DOCUMENT_NODE;O=O.parentNode){var V=O.getAttributeNS(Q.XML_NAMESPACE_URI,"lang");if(V!=null){k=String(V);break}}if(k==null)return Se.false_;var H=arguments[1].evaluate(A).stringValue();return new Se(k.substring(0,H.length)==H&&(k.length==H.length||k.charAt(H.length)=="-"))},Qe.number=function(){var A=arguments[0];if(!(arguments.length==1||arguments.length==2))throw new Error("Function number expects (object?)");return arguments.length==1?new Ee(Ce.prototype.stringForNode(A.contextNode)):arguments[1].evaluate(A).number()},Qe.sum=function(){var A=arguments[0],k;if(arguments.length!=2||!be.instance_of(k=arguments[1].evaluate(A),Ce))throw new Error("Function sum expects (node-set)");k=k.toUnsortedArray();for(var O=0,V=0;V<k.length;V++)O+=new Ee(Ce.prototype.stringForNode(k[V])).numberValue();return new Ee(O)},Qe.floor=function(){var A=arguments[0];if(arguments.length!=2)throw new Error("Function floor expects (number)");return new Ee(Math.floor(arguments[1].evaluate(A).numberValue()))},Qe.ceiling=function(){var A=arguments[0];if(arguments.length!=2)throw new Error("Function ceiling expects (number)");return new Ee(Math.ceil(arguments[1].evaluate(A).numberValue()))},Qe.round=function(){var A=arguments[0];if(arguments.length!=2)throw new Error("Function round expects (number)");return new Ee(Math.round(arguments[1].evaluate(A).numberValue()))};var be=new Object,xl=function(A){return A&&(A.nodeType===C.ATTRIBUTE_NODE||A.ownerElement||A.isXPathNamespace)};be.splitQName=function(A){var k=A.indexOf(":");return k==-1?[null,A]:[A.substring(0,k),A.substring(k+1)]},be.resolveQName=function(A,k,O,V){var H=be.splitQName(A);return H[0]!=null?H[0]=k.getNamespace(H[0],O):V?(H[0]=k.getNamespace("",O),H[0]==null&&(H[0]="")):H[0]="",H},be.isSpace=function(A){return A==9||A==13||A==10||A==32},be.isLetter=function(A){return A>=65&&A<=90||A>=97&&A<=122||A>=192&&A<=214||A>=216&&A<=246||A>=248&&A<=255||A>=256&&A<=305||A>=308&&A<=318||A>=321&&A<=328||A>=330&&A<=382||A>=384&&A<=451||A>=461&&A<=496||A>=500&&A<=501||A>=506&&A<=535||A>=592&&A<=680||A>=699&&A<=705||A==902||A>=904&&A<=906||A==908||A>=910&&A<=929||A>=931&&A<=974||A>=976&&A<=982||A==986||A==988||A==990||A==992||A>=994&&A<=1011||A>=1025&&A<=1036||A>=1038&&A<=1103||A>=1105&&A<=1116||A>=1118&&A<=1153||A>=1168&&A<=1220||A>=1223&&A<=1224||A>=1227&&A<=1228||A>=1232&&A<=1259||A>=1262&&A<=1269||A>=1272&&A<=1273||A>=1329&&A<=1366||A==1369||A>=1377&&A<=1414||A>=1488&&A<=1514||A>=1520&&A<=1522||A>=1569&&A<=1594||A>=1601&&A<=1610||A>=1649&&A<=1719||A>=1722&&A<=1726||A>=1728&&A<=1742||A>=1744&&A<=1747||A==1749||A>=1765&&A<=1766||A>=2309&&A<=2361||A==2365||A>=2392&&A<=2401||A>=2437&&A<=2444||A>=2447&&A<=2448||A>=2451&&A<=2472||A>=2474&&A<=2480||A==2482||A>=2486&&A<=2489||A>=2524&&A<=2525||A>=2527&&A<=2529||A>=2544&&A<=2545||A>=2565&&A<=2570||A>=2575&&A<=2576||A>=2579&&A<=2600||A>=2602&&A<=2608||A>=2610&&A<=2611||A>=2613&&A<=2614||A>=2616&&A<=2617||A>=2649&&A<=2652||A==2654||A>=2674&&A<=2676||A>=2693&&A<=2699||A==2701||A>=2703&&A<=2705||A>=2707&&A<=2728||A>=2730&&A<=2736||A>=2738&&A<=2739||A>=2741&&A<=2745||A==2749||A==2784||A>=2821&&A<=2828||A>=2831&&A<=2832||A>=2835&&A<=2856||A>=2858&&A<=2864||A>=2866&&A<=2867||A>=2870&&A<=2873||A==2877||A>=2908&&A<=2909||A>=2911&&A<=2913||A>=2949&&A<=2954||A>=2958&&A<=2960||A>=2962&&A<=2965||A>=2969&&A<=2970||A==2972||A>=2974&&A<=2975||A>=2979&&A<=2980||A>=2984&&A<=2986||A>=2990&&A<=2997||A>=2999&&A<=3001||A>=3077&&A<=3084||A>=3086&&A<=3088||A>=3090&&A<=3112||A>=3114&&A<=3123||A>=3125&&A<=3129||A>=3168&&A<=3169||A>=3205&&A<=3212||A>=3214&&A<=3216||A>=3218&&A<=3240||A>=3242&&A<=3251||A>=3253&&A<=3257||A==3294||A>=3296&&A<=3297||A>=3333&&A<=3340||A>=3342&&A<=3344||A>=3346&&A<=3368||A>=3370&&A<=3385||A>=3424&&A<=3425||A>=3585&&A<=3630||A==3632||A>=3634&&A<=3635||A>=3648&&A<=3653||A>=3713&&A<=3714||A==3716||A>=3719&&A<=3720||A==3722||A==3725||A>=3732&&A<=3735||A>=3737&&A<=3743||A>=3745&&A<=3747||A==3749||A==3751||A>=3754&&A<=3755||A>=3757&&A<=3758||A==3760||A>=3762&&A<=3763||A==3773||A>=3776&&A<=3780||A>=3904&&A<=3911||A>=3913&&A<=3945||A>=4256&&A<=4293||A>=4304&&A<=4342||A==4352||A>=4354&&A<=4355||A>=4357&&A<=4359||A==4361||A>=4363&&A<=4364||A>=4366&&A<=4370||A==4412||A==4414||A==4416||A==4428||A==4430||A==4432||A>=4436&&A<=4437||A==4441||A>=4447&&A<=4449||A==4451||A==4453||A==4455||A==4457||A>=4461&&A<=4462||A>=4466&&A<=4467||A==4469||A==4510||A==4520||A==4523||A>=4526&&A<=4527||A>=4535&&A<=4536||A==4538||A>=4540&&A<=4546||A==4587||A==4592||A==4601||A>=7680&&A<=7835||A>=7840&&A<=7929||A>=7936&&A<=7957||A>=7960&&A<=7965||A>=7968&&A<=8005||A>=8008&&A<=8013||A>=8016&&A<=8023||A==8025||A==8027||A==8029||A>=8031&&A<=8061||A>=8064&&A<=8116||A>=8118&&A<=8124||A==8126||A>=8130&&A<=8132||A>=8134&&A<=8140||A>=8144&&A<=8147||A>=8150&&A<=8155||A>=8160&&A<=8172||A>=8178&&A<=8180||A>=8182&&A<=8188||A==8486||A>=8490&&A<=8491||A==8494||A>=8576&&A<=8578||A>=12353&&A<=12436||A>=12449&&A<=12538||A>=12549&&A<=12588||A>=44032&&A<=55203||A>=19968&&A<=40869||A==12295||A>=12321&&A<=12329},be.isNCNameChar=function(A){return A>=48&&A<=57||A>=1632&&A<=1641||A>=1776&&A<=1785||A>=2406&&A<=2415||A>=2534&&A<=2543||A>=2662&&A<=2671||A>=2790&&A<=2799||A>=2918&&A<=2927||A>=3047&&A<=3055||A>=3174&&A<=3183||A>=3302&&A<=3311||A>=3430&&A<=3439||A>=3664&&A<=3673||A>=3792&&A<=3801||A>=3872&&A<=3881||A==46||A==45||A==95||be.isLetter(A)||A>=768&&A<=837||A>=864&&A<=865||A>=1155&&A<=1158||A>=1425&&A<=1441||A>=1443&&A<=1465||A>=1467&&A<=1469||A==1471||A>=1473&&A<=1474||A==1476||A>=1611&&A<=1618||A==1648||A>=1750&&A<=1756||A>=1757&&A<=1759||A>=1760&&A<=1764||A>=1767&&A<=1768||A>=1770&&A<=1773||A>=2305&&A<=2307||A==2364||A>=2366&&A<=2380||A==2381||A>=2385&&A<=2388||A>=2402&&A<=2403||A>=2433&&A<=2435||A==2492||A==2494||A==2495||A>=2496&&A<=2500||A>=2503&&A<=2504||A>=2507&&A<=2509||A==2519||A>=2530&&A<=2531||A==2562||A==2620||A==2622||A==2623||A>=2624&&A<=2626||A>=2631&&A<=2632||A>=2635&&A<=2637||A>=2672&&A<=2673||A>=2689&&A<=2691||A==2748||A>=2750&&A<=2757||A>=2759&&A<=2761||A>=2763&&A<=2765||A>=2817&&A<=2819||A==2876||A>=2878&&A<=2883||A>=2887&&A<=2888||A>=2891&&A<=2893||A>=2902&&A<=2903||A>=2946&&A<=2947||A>=3006&&A<=3010||A>=3014&&A<=3016||A>=3018&&A<=3021||A==3031||A>=3073&&A<=3075||A>=3134&&A<=3140||A>=3142&&A<=3144||A>=3146&&A<=3149||A>=3157&&A<=3158||A>=3202&&A<=3203||A>=3262&&A<=3268||A>=3270&&A<=3272||A>=3274&&A<=3277||A>=3285&&A<=3286||A>=3330&&A<=3331||A>=3390&&A<=3395||A>=3398&&A<=3400||A>=3402&&A<=3405||A==3415||A==3633||A>=3636&&A<=3642||A>=3655&&A<=3662||A==3761||A>=3764&&A<=3769||A>=3771&&A<=3772||A>=3784&&A<=3789||A>=3864&&A<=3865||A==3893||A==3895||A==3897||A==3902||A==3903||A>=3953&&A<=3972||A>=3974&&A<=3979||A>=3984&&A<=3989||A==3991||A>=3993&&A<=4013||A>=4017&&A<=4023||A==4025||A>=8400&&A<=8412||A==8417||A>=12330&&A<=12335||A==12441||A==12442||A==183||A==720||A==721||A==903||A==1600||A==3654||A==3782||A==12293||A>=12337&&A<=12341||A>=12445&&A<=12446||A>=12540&&A<=12542},be.coalesceText=function(A){for(var k=A.firstChild;k!=null;k=k.nextSibling)if(k.nodeType==C.TEXT_NODE||k.nodeType==C.CDATA_SECTION_NODE){var O=k.nodeValue,V=k;for(k=k.nextSibling;k!=null&&(k.nodeType==C.TEXT_NODE||k.nodeType==C.CDATA_SECTION_NODE);){O+=k.nodeValue;var H=k;k=k.nextSibling,H.parentNode.removeChild(H)}if(V.nodeType==C.CDATA_SECTION_NODE){var W=V.parentNode;if(V.nextSibling==null)W.removeChild(V),W.appendChild(W.ownerDocument.createTextNode(O));else{var ue=V.nextSibling;W.removeChild(V),W.insertBefore(W.ownerDocument.createTextNode(O),ue)}}else V.nodeValue=O;if(k==null)break}else k.nodeType==C.ELEMENT_NODE&&be.coalesceText(k)},be.instance_of=function(A,k){for(;A!=null;){if(A.constructor===k)return!0;if(A===Object)return!1;A=A.constructor.superclass}return!1},be.getElementById=function(A,k){if(A.nodeType==C.ELEMENT_NODE&&(A.getAttribute("id")==k||A.getAttributeNS(null,"id")==k))return A;for(var O=A.firstChild;O!=null;O=O.nextSibling){var V=be.getElementById(O,k);if(V!=null)return V}return null};var hi=(function(){function A(O,V){var H=V?": "+V.toString():"";switch(O){case k.INVALID_EXPRESSION_ERR:return"Invalid expression"+H;case k.TYPE_ERR:return"Type error"+H}return null}function k(O,V,H){var W=Error.call(this,A(O,V)||H);return W.code=O,W.exception=V,W}return k.prototype=Object.create(Error.prototype),k.prototype.constructor=k,k.superclass=Error,k.prototype.toString=function(){return this.message},k.fromMessage=function(O,V){return new k(null,V,O)},k.INVALID_EXPRESSION_ERR=51,k.TYPE_ERR=52,k})();Lr.prototype={},Lr.prototype.constructor=Lr,Lr.superclass=Object.prototype;function Lr(A,k,O){this.xpath=O.parse(A),this.context=new si,this.context.namespaceResolver=new Ln(k)}Lr.getOwnerDocument=function(A){return A.nodeType===C.DOCUMENT_NODE?A:A.ownerDocument},Lr.detectHtmlDom=function(A){if(!A)return!1;var k=Lr.getOwnerDocument(A);try{return k.implementation.hasFeature("HTML","2.0")}catch{return!0}},Lr.prototype.evaluate=function(A,k,O){this.context.expressionContextNode=A,this.context.caseInsensitive=Lr.detectHtmlDom(A);var V=this.xpath.evaluate(this.context);return new Le(V,k)},Ln.prototype={},Ln.prototype.constructor=Ln,Ln.superclass=Object.prototype;function Ln(A){this.xpathNSResolver=A}Ln.prototype.getNamespace=function(A,k){return this.xpathNSResolver==null?null:this.xpathNSResolver.lookupNamespaceURI(A)},yn.prototype={},yn.prototype.constructor=yn,yn.superclass=Object.prototype;function yn(A){this.node=A,this.namespaceResolver=new ur}yn.prototype.lookupNamespaceURI=function(A){return this.namespaceResolver.getNamespace(A,this.node)},Le.prototype={},Le.prototype.constructor=Le,Le.superclass=Object.prototype;function Le(A,k){switch(k==Le.ANY_TYPE&&(A.constructor===we?k=Le.STRING_TYPE:A.constructor===Ee?k=Le.NUMBER_TYPE:A.constructor===Se?k=Le.BOOLEAN_TYPE:A.constructor===Ce&&(k=Le.UNORDERED_NODE_ITERATOR_TYPE)),this.resultType=k,k){case Le.NUMBER_TYPE:this.numberValue=A.numberValue();return;case Le.STRING_TYPE:this.stringValue=A.stringValue();return;case Le.BOOLEAN_TYPE:this.booleanValue=A.booleanValue();return;case Le.ANY_UNORDERED_NODE_TYPE:case Le.FIRST_ORDERED_NODE_TYPE:if(A.constructor===Ce){this.singleNodeValue=A.first();return}break;case Le.UNORDERED_NODE_ITERATOR_TYPE:case Le.ORDERED_NODE_ITERATOR_TYPE:if(A.constructor===Ce){this.invalidIteratorState=!1,this.nodes=A.toArray(),this.iteratorIndex=0;return}break;case Le.UNORDERED_NODE_SNAPSHOT_TYPE:case Le.ORDERED_NODE_SNAPSHOT_TYPE:if(A.constructor===Ce){this.nodes=A.toArray(),this.snapshotLength=this.nodes.length;return}break}throw new hi(hi.TYPE_ERR)}Le.prototype.iterateNext=function(){if(this.resultType!=Le.UNORDERED_NODE_ITERATOR_TYPE&&this.resultType!=Le.ORDERED_NODE_ITERATOR_TYPE)throw new hi(hi.TYPE_ERR);return this.nodes[this.iteratorIndex++]},Le.prototype.snapshotItem=function(A){if(this.resultType!=Le.UNORDERED_NODE_SNAPSHOT_TYPE&&this.resultType!=Le.ORDERED_NODE_SNAPSHOT_TYPE)throw new hi(hi.TYPE_ERR);return this.nodes[A]},Le.ANY_TYPE=0,Le.NUMBER_TYPE=1,Le.STRING_TYPE=2,Le.BOOLEAN_TYPE=3,Le.UNORDERED_NODE_ITERATOR_TYPE=4,Le.ORDERED_NODE_ITERATOR_TYPE=5,Le.UNORDERED_NODE_SNAPSHOT_TYPE=6,Le.ORDERED_NODE_SNAPSHOT_TYPE=7,Le.ANY_UNORDERED_NODE_TYPE=8,Le.FIRST_ORDERED_NODE_TYPE=9;function ic(A,k){A.createExpression=function(O,V){try{return new Lr(O,V,k)}catch(H){throw new hi(hi.INVALID_EXPRESSION_ERR,H)}},A.createNSResolver=function(O){return new yn(O)},A.evaluate=function(O,V,H,W,ue){if(W<0||W>9)throw{code:0,toString:function(){return"Request type not supported"}};return A.createExpression(O,H,k).evaluate(V,W,ue)}}try{var $i=!0;try{document.implementation&&document.implementation.hasFeature&&document.implementation.hasFeature("XPath",null)&&($i=!1)}catch{}$i&&ic(document,new x)}catch{}ic(r,new x),(function(){var A=new x,k=new ur,O=new tr,V=new wr;function H(he){return{getNamespace:function(Ze,Hr){var wn=he(Ze,Hr);return wn||k.getNamespace(Ze,Hr)}}}function W(he){return H(he.getNamespace.bind(he))}function ue(he){return H(function(Ze){return he[Ze]})}function Re(he){return he&&typeof he.getNamespace=="function"?W(he):typeof he=="function"?H(he):typeof he=="object"?ue(he):k}function Ve(he){if(he===null||typeof he>"u"||he instanceof we||he instanceof Se||he instanceof Ee||he instanceof Ce)return he;switch(typeof he){case"string":return new we(he);case"boolean":return new Se(he);case"number":return new Ee(he)}var Ze=new Ce;return Ze.addArray([].concat(he)),Ze}function Ie(he){return function(Ze){var Hr=Array.prototype.slice.call(arguments,1).map(function(Bx){return Bx.evaluate(Ze)}),wn=he.apply(this,[].concat(Ze,Hr));return Ve(wn)}}function Fe(he){return{getFunction:function(Ze,Hr){var wn=he(Ze,Hr);return wn?Ie(wn):O.getFunction(Ze,Hr)}}}function In(he){return Fe(he.getFunction.bind(he))}function ks(he){return Fe(function(Ze){return he[Ze]})}function Ss(he){return he&&typeof he.getFunction=="function"?In(he):typeof he=="function"?Fe(he):typeof he=="object"?ks(he):O}function Ds(he){return{getVariable:function(Ze,Hr){var wn=he(Ze,Hr);return Ve(wn)}}}function Vi(he){if(he){if(typeof he.getVariable=="function")return Ds(he.getVariable.bind(he));if(typeof he=="function")return Ds(he);if(typeof he=="object")return Ds(function(Ze){return he[Ze]})}return V}function Hn(he,Ze,Hr){he in Hr&&(Ze[he]=Hr[he])}function B0(he){var Ze=new si;return he?(Ze.namespaceResolver=Re(he.namespaces),Ze.functionResolver=Ss(he.functions),Ze.variableResolver=Vi(he.variables),Ze.expressionContextNode=he.node,Hn("allowAnyNamespaceForNoPrefix",Ze,he),Hn("isHtml",Ze,he)):Ze.namespaceResolver=k,Ze}function ku(he,Ze){var Hr=B0(Ze);return he.evaluate(Hr)}var Cx={evaluate:function(he){return ku(this.expression,he)},evaluateNumber:function(he){return this.evaluate(he).numberValue()},evaluateString:function(he){return this.evaluate(he).stringValue()},evaluateBoolean:function(he){return this.evaluate(he).booleanValue()},evaluateNodeSet:function(he){return this.evaluate(he).nodeset()},select:function(he){return this.evaluateNodeSet(he).toArray()},select1:function(he){return this.select(he)[0]}};function Ex(he){var Ze=A.parse(he);return Object.create(Cx,{expression:{value:Ze}})}r.parse=Ex})(),B(r,{XPath:Q,XPathParser:x,XPathResult:Le,Step:se,PathExpr:Z,NodeTest:ce,LocationPath:_e,OrOperation:_,AndOperation:v,BarOperation:ee,EqualsOperation:E,NotEqualOperation:P,LessThanOperation:N,GreaterThanOperation:j,LessThanOrEqualOperation:z,GreaterThanOrEqualOperation:L,PlusOperation:G,MinusOperation:K,MultiplyOperation:$,DivOperation:te,ModOperation:re,UnaryMinusOperation:D,FunctionCall:Mr,VariableReference:dr,XPathContext:si,XNodeSet:Ce,XBoolean:Se,XString:we,XNumber:Ee,NamespaceResolver:ur,FunctionResolver:tr,VariableResolver:wr,Utilities:be}),r.select=function(A,k,O){return r.selectWithResolver(A,k,null,O)},r.useNamespaces=function(A){var k={mappings:A||{},lookupNamespaceURI:function(O){return this.mappings[O]}};return function(O,V,H){return r.selectWithResolver(O,V,k,H)}},r.selectWithResolver=function(A,k,O,V){var H=new Lr(A,O,new x),W=Le.ANY_TYPE,ue=H.evaluate(k,W,null);return ue.resultType==Le.STRING_TYPE?ue=ue.stringValue:ue.resultType==Le.NUMBER_TYPE?ue=ue.numberValue:ue.resultType==Le.BOOLEAN_TYPE?ue=ue.booleanValue:(ue=ue.nodes,V&&(ue=ue[0])),ue},r.select1=function(A,k){return r.select(A,k,!0)};var ko=function(A){return Array.isArray(A)&&A.every(n)},Ri=function(A){return function(k){return n(k)&&k.nodeType===A}};B(r,{isNodeLike:n,isArrayOfNodes:ko,isElement:Ri(C.ELEMENT_NODE),isAttribute:Ri(C.ATTRIBUTE_NODE),isTextNode:Ri(C.TEXT_NODE),isCDATASection:Ri(C.CDATA_SECTION_NODE),isProcessingInstruction:Ri(C.PROCESSING_INSTRUCTION_NODE),isComment:Ri(C.COMMENT_NODE),isDocumentNode:Ri(C.DOCUMENT_NODE),isDocumentTypeNode:Ri(C.DOCUMENT_TYPE_NODE),isDocumentFragment:Ri(C.DOCUMENT_FRAGMENT_NODE)})})(SV)});var Fc=oe(Pr=>{"use strict";function DV(r,e,t){if(t===void 0&&(t=Array.prototype),r&&typeof t.find=="function")return t.find.call(r,e);for(var i=0;i<r.length;i++)if(_c(r,i)){var n=r[i];if(e.call(void 0,n,i,r))return n}}function Wl(r,e){return e===void 0&&(e=Object),e&&typeof e.getOwnPropertyDescriptors=="function"&&(r=e.create(null,e.getOwnPropertyDescriptors(r))),e&&typeof e.freeze=="function"?e.freeze(r):r}function _c(r,e){return Object.prototype.hasOwnProperty.call(r,e)}function QV(r,e){if(r===null||typeof r!="object")throw new TypeError("target is not an object");for(var t in e)_c(e,t)&&(r[t]=e[t]);return r}var t5=Wl({allowfullscreen:!0,async:!0,autofocus:!0,autoplay:!0,checked:!0,controls:!0,default:!0,defer:!0,disabled:!0,formnovalidate:!0,hidden:!0,ismap:!0,itemscope:!0,loop:!0,multiple:!0,muted:!0,nomodule:!0,novalidate:!0,open:!0,playsinline:!0,readonly:!0,required:!0,reversed:!0,selected:!0});function _V(r){return _c(t5,r.toLowerCase())}var r5=Wl({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function FV(r){return _c(r5,r.toLowerCase())}var wf=Wl({script:!1,style:!1,textarea:!0,title:!0});function PV(r){var e=r.toLowerCase();return _c(wf,e)&&!wf[e]}function TV(r){var e=r.toLowerCase();return _c(wf,e)&&wf[e]}function i5(r){return r===xf.HTML}function UV(r){return i5(r)||r===xf.XML_XHTML_APPLICATION}var xf=Wl({HTML:"text/html",XML_APPLICATION:"application/xml",XML_TEXT:"text/xml",XML_XHTML_APPLICATION:"application/xhtml+xml",XML_SVG_IMAGE:"image/svg+xml"}),RV=Object.keys(xf).map(function(r){return xf[r]});function NV(r){return RV.indexOf(r)>-1}var qV=Wl({HTML:"http://www.w3.org/1999/xhtml",SVG:"http://www.w3.org/2000/svg",XML:"http://www.w3.org/XML/1998/namespace",XMLNS:"http://www.w3.org/2000/xmlns/"});Pr.assign=QV;Pr.find=DV;Pr.freeze=Wl;Pr.HTML_BOOLEAN_ATTRIBUTES=t5;Pr.HTML_RAW_TEXT_ELEMENTS=wf;Pr.HTML_VOID_ELEMENTS=r5;Pr.hasDefaultHTMLNamespace=UV;Pr.hasOwn=_c;Pr.isHTMLBooleanAttribute=_V;Pr.isHTMLRawTextElement=PV;Pr.isHTMLEscapableRawTextElement=TV;Pr.isHTMLMimeType=i5;Pr.isHTMLVoidElement=FV;Pr.isValidMimeType=NV;Pr.MIME_TYPE=xf;Pr.NAMESPACE=qV});var vf=oe(bf=>{"use strict";var OV=Fc();function s5(r,e){r.prototype=Object.create(Error.prototype,{constructor:{value:r},name:{value:r.name,enumerable:!0,writable:e}})}var Ef=OV.freeze({Error:"Error",IndexSizeError:"IndexSizeError",DomstringSizeError:"DomstringSizeError",HierarchyRequestError:"HierarchyRequestError",WrongDocumentError:"WrongDocumentError",InvalidCharacterError:"InvalidCharacterError",NoDataAllowedError:"NoDataAllowedError",NoModificationAllowedError:"NoModificationAllowedError",NotFoundError:"NotFoundError",NotSupportedError:"NotSupportedError",InUseAttributeError:"InUseAttributeError",InvalidStateError:"InvalidStateError",SyntaxError:"SyntaxError",InvalidModificationError:"InvalidModificationError",NamespaceError:"NamespaceError",InvalidAccessError:"InvalidAccessError",ValidationError:"ValidationError",TypeMismatchError:"TypeMismatchError",SecurityError:"SecurityError",NetworkError:"NetworkError",AbortError:"AbortError",URLMismatchError:"URLMismatchError",QuotaExceededError:"QuotaExceededError",TimeoutError:"TimeoutError",InvalidNodeTypeError:"InvalidNodeTypeError",DataCloneError:"DataCloneError",EncodingError:"EncodingError",NotReadableError:"NotReadableError",UnknownError:"UnknownError",ConstraintError:"ConstraintError",DataError:"DataError",TransactionInactiveError:"TransactionInactiveError",ReadOnlyError:"ReadOnlyError",VersionError:"VersionError",OperationError:"OperationError",NotAllowedError:"NotAllowedError",OptOutError:"OptOutError"}),o5=Object.keys(Ef);function a5(r){return typeof r=="number"&&r>=1&&r<=25}function MV(r){return typeof r=="string"&&r.substring(r.length-Ef.Error.length)===Ef.Error}function Bf(r,e){a5(r)?(this.name=o5[r],this.message=e||""):(this.message=r,this.name=MV(e)?e:Ef.Error),Error.captureStackTrace&&Error.captureStackTrace(this,Bf)}s5(Bf,!0);Object.defineProperties(Bf.prototype,{code:{enumerable:!0,get:function(){var r=o5.indexOf(this.name);return a5(r)?r:0}}});var c5={INDEX_SIZE_ERR:1,DOMSTRING_SIZE_ERR:2,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,INVALID_CHARACTER_ERR:5,NO_DATA_ALLOWED_ERR:6,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INUSE_ATTRIBUTE_ERR:10,INVALID_STATE_ERR:11,SYNTAX_ERR:12,INVALID_MODIFICATION_ERR:13,NAMESPACE_ERR:14,INVALID_ACCESS_ERR:15,VALIDATION_ERR:16,TYPE_MISMATCH_ERR:17,SECURITY_ERR:18,NETWORK_ERR:19,ABORT_ERR:20,URL_MISMATCH_ERR:21,QUOTA_EXCEEDED_ERR:22,TIMEOUT_ERR:23,INVALID_NODE_TYPE_ERR:24,DATA_CLONE_ERR:25},JE=Object.entries(c5);for(Cf=0;Cf<JE.length;Cf++)n5=JE[Cf][0],Bf[n5]=JE[Cf][1];var n5,Cf;function jE(r,e,t){this.message=r,this.locator=e,this.cause=t,Error.captureStackTrace&&Error.captureStackTrace(this,jE)}s5(jE);bf.DOMException=Bf;bf.DOMExceptionName=Ef;bf.ExceptionCode=c5;bf.ParseError=jE});var eB=oe(Ke=>{"use strict";function h5(r){try{typeof r!="function"&&(r=RegExp);var e=new r("\u{1D306}","u").exec("\u{1D306}");return!!e&&e[0].length===2}catch{}return!1}var ed=h5();function ca(r){if(r.source[0]!=="[")throw new Error(r+" can not be used with chars");return r.source.slice(1,r.source.lastIndexOf("]"))}function Xl(r,e){if(r.source[0]!=="[")throw new Error("/"+r.source+"/ can not be used with chars_without");if(!e||typeof e!="string")throw new Error(JSON.stringify(e)+" is not a valid search");if(r.source.indexOf(e)===-1)throw new Error('"'+e+'" is not is /'+r.source+"/");if(e==="-"&&r.source.indexOf(e)!==1)throw new Error('"'+e+'" is not at the first postion of /'+r.source+"/");return new RegExp(r.source.replace(e,""),ed?"u":"")}function $e(r){var e=this;return new RegExp(Array.prototype.slice.call(arguments).map(function(t){var i=typeof t=="string";if(i&&e===void 0&&t==="|")throw new Error("use regg instead of reg to wrap expressions with `|`!");return i?t:t.source}).join(""),ed?"u":"")}function Oe(r){if(arguments.length===0)throw new Error("no parameters provided");return $e.apply(Oe,["(?:"].concat(Array.prototype.slice.call(arguments),[")"]))}var LV="\uFFFD",la=/[-\x09\x0A\x0D\x20-\x2C\x2E-\uD7FF\uE000-\uFFFD]/;ed&&(la=$e("[",ca(la),"\\u{10000}-\\u{10FFFF}","]"));var HV=new RegExp("[^"+ca(la)+"]",ed?"u":""),hy=/[\x20\x09\x0D\x0A]/,KV=ca(hy),gt=$e(hy,"+"),Qt=$e(hy,"*"),kf=/[:_a-zA-Z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/;ed&&(kf=$e("[",ca(kf),"\\u{10000}-\\u{10FFFF}","]"));var GV=ca(kf),YE=$e("[",GV,ca(/[-.0-9\xB7]/),ca(/[\u0300-\u036F\u203F-\u2040]/),"]"),Li=$e(kf,YE,"*"),zV=$e("^",Li,"$"),l5=$e(YE,"+"),JV=$e("&",Li,";"),jV=Oe(/&#[0-9]+;|&#x[0-9a-fA-F]+;/),Sf=Oe(JV,"|",jV),Df=$e("%",Li,";"),WE=Oe($e('"',Oe(/[^%&"]/,"|",Df,"|",Sf),"*",'"'),"|",$e("'",Oe(/[^%&']/,"|",Df,"|",Sf),"*","'")),$V=Oe('"',Oe(/[^<&"]/,"|",Sf),"*",'"',"|","'",Oe(/[^<&']/,"|",Sf),"*","'"),VV=Xl(kf,":"),YV=Xl(YE,":"),$E=$e(VV,YV,"*"),WV=$e("^",$E,"$"),Qf=$e($E,Oe(":",$E),"?"),XV=$e("^",Qf,"$"),ZV=$e("(",Qf,")"),Zl=Oe(/"[^"]*"|'[^']*'/),eY=$e(/^<\?/,"(",Li,")",Oe(gt,"(?!",hy,")(",la,"*?)"),"?",/\?>/),d5=/[\x20\x0D\x0Aa-zA-Z0-9-'()+,./:=?;!*#@$_%]/,_f=Oe('"',d5,'*"',"|","'",Xl(d5,"'"),"*'"),g5="<!--",p5="-->",tY=$e(g5,Oe(Xl(la,"-"),"|",$e("-",Xl(la,"-"))),"*",p5),u5="#PCDATA",rY=Oe($e(/\(/,Qt,u5,Oe(Qt,/\|/,Qt,Qf),"*",Qt,/\)\*/),"|",$e(/\(/,Qt,u5,Qt,/\)/)),iY=/[?*+]?/,nY=$e(/\([^>]+\)/,iY),sY=Oe("EMPTY","|","ANY","|",rY,"|",nY),oY="<!ELEMENT",aY=$e(oY,gt,Oe(Qf,"|",Df),gt,Oe(sY,"|",Df),Qt,">"),cY=$e("NOTATION",gt,/\(/,Qt,Li,Oe(Qt,/\|/,Qt,Li),"*",Qt,/\)/),lY=$e(/\(/,Qt,l5,Oe(Qt,/\|/,Qt,l5),"*",Qt,/\)/),dY=Oe(cY,"|",lY),uY=Oe(/CDATA|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS/,"|",dY),fY=Oe(/#REQUIRED|#IMPLIED/,"|",Oe(Oe("#FIXED",gt),"?",$V)),AY=Oe(gt,Li,gt,uY,gt,fY),hY="<!ATTLIST",gY=$e(hY,gt,Li,AY,"*",Qt,">"),VE="about:legacy-compat",pY=Oe('"'+VE+'"',"|","'"+VE+"'"),XE="SYSTEM",gy="PUBLIC",py=Oe(Oe(XE,gt,Zl),"|",Oe(gy,gt,_f,gt,Zl)),mY=$e("^",Oe(Oe(XE,gt,"(?<SystemLiteralOnly>",Zl,")"),"|",Oe(gy,gt,"(?<PubidLiteral>",_f,")",gt,"(?<SystemLiteral>",Zl,")"))),yY=$e("^",_f,"$"),IY=$e("^",Zl,"$"),wY=Oe(gt,"NDATA",gt,Li),xY=Oe(WE,"|",Oe(py,wY,"?")),m5="<!ENTITY",CY=$e(m5,gt,Li,gt,xY,Qt,">"),EY=Oe(WE,"|",py),BY=$e(m5,gt,"%",gt,Li,gt,EY,Qt,">"),bY=Oe(CY,"|",BY),vY=$e(gy,gt,_f),kY=$e("<!NOTATION",gt,Li,gt,Oe(py,"|",vY),Qt,">"),ZE=$e(Qt,"=",Qt),f5=/1[.]\d+/,SY=$e(gt,"version",ZE,Oe("'",f5,"'","|",'"',f5,'"')),A5=/[A-Za-z][-A-Za-z0-9._]*/,DY=Oe(gt,"encoding",ZE,Oe('"',A5,'"',"|","'",A5,"'")),QY=Oe(gt,"standalone",ZE,Oe("'",Oe("yes","|","no"),"'","|",'"',Oe("yes","|","no"),'"')),_Y=$e(/^<\?xml/,SY,DY,"?",QY,"?",Qt,/\?>/),FY="<!DOCTYPE",PY="<![CDATA[",TY="]]>",UY=/<!\[CDATA\[/,RY=/\]\]>/,NY=$e(la,"*?",RY),qY=$e(UY,NY);Ke.chars=ca;Ke.chars_without=Xl;Ke.detectUnicodeSupport=h5;Ke.reg=$e;Ke.regg=Oe;Ke.ABOUT_LEGACY_COMPAT=VE;Ke.ABOUT_LEGACY_COMPAT_SystemLiteral=pY;Ke.AttlistDecl=gY;Ke.CDATA_START=PY;Ke.CDATA_END=TY;Ke.CDSect=qY;Ke.Char=la;Ke.Comment=tY;Ke.COMMENT_START=g5;Ke.COMMENT_END=p5;Ke.DOCTYPE_DECL_START=FY;Ke.elementdecl=aY;Ke.EntityDecl=bY;Ke.EntityValue=WE;Ke.ExternalID=py;Ke.ExternalID_match=mY;Ke.Name=Li;Ke.Name_exact=zV;Ke.NCName_exact=WV;Ke.NotationDecl=kY;Ke.Reference=Sf;Ke.PEReference=Df;Ke.PI=eY;Ke.PUBLIC=gy;Ke.PubidLiteral=_f;Ke.PubidLiteral_match=yY;Ke.QName=Qf;Ke.QName_exact=XV;Ke.QName_group=ZV;Ke.S=gt;Ke.SChar_s=KV;Ke.S_OPT=Qt;Ke.SYSTEM=XE;Ke.SystemLiteral=Zl;Ke.SystemLiteral_match=IY;Ke.InvalidChar=HV;Ke.UNICODE_REPLACEMENT_CHARACTER=LV;Ke.UNICODE_SUPPORT=ed;Ke.XMLDecl=_Y});var dB=oe(Ht=>{"use strict";var tn=Fc(),es=tn.find,OY=tn.hasDefaultHTMLNamespace,rd=tn.hasOwn,MY=tn.isHTMLMimeType,LY=tn.isHTMLRawTextElement,HY=tn.isHTMLVoidElement,Ff=tn.MIME_TYPE,ts=tn.NAMESPACE,li=Symbol(),B5=vf(),ke=B5.DOMException,Vr=B5.DOMExceptionName,Lt=eB();function mi(r){if(r!==li)throw new TypeError("Illegal constructor")}function KY(r){return r!==""}function GY(r){return r?r.split(/[\t\n\f\r ]+/).filter(KY):[]}function zY(r,e){return rd(r,e)||(r[e]=!0),r}function y5(r){if(!r)return[];var e=GY(r);return Object.keys(e.reduce(zY,{}))}function JY(r){return function(e){return r&&r.indexOf(e)!==-1}}function b5(r){if(!Lt.QName_exact.test(r))throw new ke(ke.INVALID_CHARACTER_ERR,'invalid character in qualified name "'+r+'"')}function rB(r,e){b5(e),r=r||null;var t=null,i=e;if(e.indexOf(":")>=0){var n=e.split(":");t=n[0],i=n[1]}if(t!==null&&r===null)throw new ke(ke.NAMESPACE_ERR,"prefix is non-null and namespace is null");if(t==="xml"&&r!==tn.NAMESPACE.XML)throw new ke(ke.NAMESPACE_ERR,'prefix is "xml" and namespace is not the XML namespace');if((t==="xmlns"||e==="xmlns")&&r!==tn.NAMESPACE.XMLNS)throw new ke(ke.NAMESPACE_ERR,'either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace');if(r===tn.NAMESPACE.XMLNS&&t!=="xmlns"&&e!=="xmlns")throw new ke(ke.NAMESPACE_ERR,'namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns"');return[r,t,i]}function sd(r,e){for(var t in r)rd(r,t)&&(e[t]=r[t])}function yi(r,e){var t=r.prototype;if(!(t instanceof e)){let i=function(){};i.prototype=e.prototype,i=new i,sd(t,i),r.prototype=t=i}t.constructor!=r&&(typeof r!="function"&&console.error("unknown Class:"+r),t.constructor=r)}var Ii={},en=Ii.ELEMENT_NODE=1,id=Ii.ATTRIBUTE_NODE=2,Pf=Ii.TEXT_NODE=3,v5=Ii.CDATA_SECTION_NODE=4,k5=Ii.ENTITY_REFERENCE_NODE=5,jY=Ii.ENTITY_NODE=6,iB=Ii.PROCESSING_INSTRUCTION_NODE=7,nB=Ii.COMMENT_NODE=8,td=Ii.DOCUMENT_NODE=9,S5=Ii.DOCUMENT_TYPE_NODE=10,da=Ii.DOCUMENT_FRAGMENT_NODE=11,$Y=Ii.NOTATION_NODE=12,Mt=tn.freeze({DOCUMENT_POSITION_DISCONNECTED:1,DOCUMENT_POSITION_PRECEDING:2,DOCUMENT_POSITION_FOLLOWING:4,DOCUMENT_POSITION_CONTAINS:8,DOCUMENT_POSITION_CONTAINED_BY:16,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC:32});function D5(r,e){if(e.length<r.length)return D5(e,r);var t=null;for(var i in r){if(r[i]!==e[i])return t;t=r[i]}return t}function I5(r){return r.guid||(r.guid=Math.random()),r.guid}function hr(){}hr.prototype={length:0,item:function(r){return r>=0&&r<this.length?this[r]:null},toString:function(r){var e;typeof r=="function"?e={requireWellFormed:!1,splitCDATASections:!0,nodeFilter:r}:r?e={requireWellFormed:!!r.requireWellFormed,splitCDATASections:r.splitCDATASections!==!1,nodeFilter:r.nodeFilter||null}:e={requireWellFormed:!1,splitCDATASections:!0,nodeFilter:null};for(var t=[],i=0;i<this.length;i++)lB(this[i],t,null,e);return t.join("")},filter:function(r){return Array.prototype.filter.call(this,r)},indexOf:function(r){return Array.prototype.indexOf.call(this,r)}};hr.prototype[Symbol.iterator]=function(){var r=this,e=0;return{next:function(){return e<r.length?{value:r[e++],done:!1}:{done:!0}},return:function(){return{done:!0}}}};function Zn(r,e){this._node=r,this._refresh=e,wy(this)}function wy(r){var e=r._node._inc||r._node.ownerDocument._inc;if(r._inc!==e){var t=r._refresh(r._node);if(M5(r,"length",t.length),!r.$$length||t.length<r.$$length)for(var i=t.length;i in r;i++)rd(r,i)&&delete r[i];sd(t,r),r._inc=e}}Zn.prototype.item=function(r){return wy(this),this[r]||null};yi(Zn,hr);function nd(){this._nsIndex=Object.create(null),this._noNsIndex=Object.create(null)}function Q5(r,e){for(var t=0;t<r.length;){if(r[t]===e)return t;t++}}function sB(r,e,t){if(!e)return r._noNsIndex;var i=r._nsIndex[e];return!i&&t&&(i=r._nsIndex[e]=Object.create(null)),i}function VY(r,e,t){var i=sB(r,e,!1),n=i&&i[t];return n||null}function YY(r,e){sB(r,e.namespaceURI,!0)[e.localName]=e}function WY(r,e){var t=sB(r,e.namespaceURI,!1);t&&delete t[e.localName]}function XY(r,e,t,i){if(i?e[Q5(e,i)]=t:(e[e.length]=t,e.length++),YY(e,t),r){t.ownerElement=r;var n=r.ownerDocument;n&&(i&&P5(n,r,i),ZY(n,r,t))}}function w5(r,e,t){var i=Q5(e,t);if(i>=0){for(var n=e.length-1;i<=n;)e[i]=e[++i];if(e.length=n,WY(e,t),r){var s=r.ownerDocument;s&&P5(s,r,t),t.ownerElement=null}}}nd.prototype={length:0,item:hr.prototype.item,getNamedItem:function(r){this._ownerElement&&this._ownerElement._isInHTMLDocumentAndNamespace()&&(r=r.toLowerCase());for(var e=0;e<this.length;){var t=this[e];if(t.nodeName===r)return t;e++}return null},setNamedItem:function(r){var e=r.ownerElement;if(e&&e!==this._ownerElement)throw new ke(ke.INUSE_ATTRIBUTE_ERR);var t=VY(this,r.namespaceURI,r.localName);return t===r?r:(XY(this._ownerElement,this,r,t),t)},setNamedItemNS:function(r){return this.setNamedItem(r)},removeNamedItem:function(r){var e=this.getNamedItem(r);if(!e)throw new ke(ke.NOT_FOUND_ERR,r);return w5(this._ownerElement,this,e),e},removeNamedItemNS:function(r,e){var t=this.getNamedItemNS(r,e);if(!t)throw new ke(ke.NOT_FOUND_ERR,r?r+" : "+e:e);return w5(this._ownerElement,this,t),t},getNamedItemNS:function(r,e){r||(r=null);for(var t=0;t<this.length;){var i=this[t];if(i.localName===e&&i.namespaceURI===r)return i;t++}return null}};nd.prototype[Symbol.iterator]=function(){var r=this,e=0;return{next:function(){return e<r.length?{value:r[e++],done:!1}:{done:!0}},return:function(){return{done:!0}}}};function _5(){}_5.prototype={hasFeature:function(r,e){return!0},createDocument:function(r,e,t){var i=Ff.XML_APPLICATION;r===ts.HTML?i=Ff.XML_XHTML_APPLICATION:r===ts.SVG&&(i=Ff.XML_SVG_IMAGE);var n=new $s(li,{contentType:i});if(n.implementation=this,n.childNodes=new hr,n.doctype=t||null,t&&n.appendChild(t),e){var s=n.createElementNS(r,e);n.appendChild(s)}return n},createDocumentType:function(r,e,t,i){b5(r);var n=new Ey(li);return n.name=r,n.nodeName=r,n.publicId=e||"",n.systemId=t||"",n.internalSubset=i||"",n.childNodes=new hr,n},createHTMLDocument:function(r){var e=new $s(li,{contentType:Ff.HTML});if(e.implementation=this,e.childNodes=new hr,r!==!1){e.doctype=this.createDocumentType("html"),e.doctype.ownerDocument=e,e.appendChild(e.doctype);var t=e.createElement("html");e.appendChild(t);var i=e.createElement("head");if(t.appendChild(i),typeof r=="string"){var n=e.createElement("title");n.appendChild(e.createTextNode(r)),i.appendChild(n)}t.appendChild(e.createElement("body"))}return e}};function nt(r){mi(r)}nt.prototype={firstChild:null,lastChild:null,previousSibling:null,nextSibling:null,parentNode:null,get parentElement(){return this.parentNode&&this.parentNode.nodeType===this.ELEMENT_NODE?this.parentNode:null},childNodes:null,ownerDocument:null,nodeValue:null,namespaceURI:null,prefix:null,localName:null,baseURI:"about:blank",get isConnected(){var r=this.getRootNode();return r&&r.nodeType===r.DOCUMENT_NODE},contains:function(r){if(!r)return!1;var e=r;do{if(this===e)return!0;e=e.parentNode}while(e);return!1},getRootNode:function(r){var e=this;do{if(!e.parentNode)return e;e=e.parentNode}while(e)},isEqualNode:function(r){if(!r)return!1;for(var e=[{node:this,other:r}];e.length>0;){var t=e.pop(),i=t.node,n=t.other;if(i.nodeType!==n.nodeType)return!1;switch(i.nodeType){case i.DOCUMENT_TYPE_NODE:if(i.name!==n.name||i.publicId!==n.publicId||i.systemId!==n.systemId)return!1;break;case i.ELEMENT_NODE:if(i.namespaceURI!==n.namespaceURI||i.prefix!==n.prefix||i.localName!==n.localName||i.attributes.length!==n.attributes.length)return!1;for(var s=0;s<i.attributes.length;s++){var o=i.attributes.item(s),a=n.getAttributeNodeNS(o.namespaceURI,o.localName);if(!a)return!1;e.push({node:o,other:a})}break;case i.ATTRIBUTE_NODE:if(i.namespaceURI!==n.namespaceURI||i.localName!==n.localName||i.value!==n.value)return!1;break;case i.PROCESSING_INSTRUCTION_NODE:if(i.target!==n.target||i.data!==n.data)return!1;break;case i.TEXT_NODE:case i.CDATA_SECTION_NODE:case i.COMMENT_NODE:if(i.data!==n.data)return!1;break}if(i.childNodes.length!==n.childNodes.length)return!1;for(var s=i.childNodes.length-1;s>=0;s--)e.push({node:i.childNodes[s],other:n.childNodes[s]})}return!0},isSameNode:function(r){return this===r},insertBefore:function(r,e){return Iy(this,r,e)},replaceChild:function(r,e){Iy(this,r,e,R5),e&&this.removeChild(e)},removeChild:function(r){return T5(this,r)},appendChild:function(r){return this.insertBefore(r,null)},hasChildNodes:function(){return this.firstChild!=null},cloneNode:function(r){return O5(this.ownerDocument||this,this,r)},normalize:function(){Tr(this,null,{enter:function(r){for(var e=r.firstChild;e;){var t=e.nextSibling;if(t!==null&&t.nodeType===Pf&&e.nodeType===Pf){for(var i=[],n=t;n!==null&&n.nodeType===Pf;)i.push(n.data),n=n.nextSibling;for(var s=e.nextSibling;s!==n;){var o=s.nextSibling;s.parentNode=null,s.previousSibling=null,s.nextSibling=null,s=o}e.nextSibling=n,n!==null?n.previousSibling=e:r.lastChild=e,e.appendData(i.join("")),oB(r.ownerDocument,r),e=n}else e=t}return!0}})},isSupported:function(r,e){return this.ownerDocument.implementation.hasFeature(r,e)},lookupPrefix:function(r){for(var e=this;e;){var t=e._nsMap;if(t){for(var i in t)if(rd(t,i)&&t[i]===r)return i}e=e.nodeType==id?e.ownerDocument:e.parentNode}return null},lookupNamespaceURI:function(r){for(var e=this;e;){var t=e._nsMap;if(t&&rd(t,r))return t[r];e=e.nodeType==id?e.ownerDocument:e.parentNode}return null},isDefaultNamespace:function(r){var e=this.lookupPrefix(r);return e==null},compareDocumentPosition:function(r){if(this===r)return 0;var e=r,t=this,i=null,n=null;if(e instanceof Pc&&(i=e,e=i.ownerElement),t instanceof Pc&&(n=t,t=n.ownerElement,i&&e&&t===e))for(var s=0,o;o=t.attributes[s];s++){if(o===i)return Mt.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC+Mt.DOCUMENT_POSITION_PRECEDING;if(o===n)return Mt.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC+Mt.DOCUMENT_POSITION_FOLLOWING}if(!e||!t||t.ownerDocument!==e.ownerDocument)return Mt.DOCUMENT_POSITION_DISCONNECTED+Mt.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC+(I5(t.ownerDocument)>I5(e.ownerDocument)?Mt.DOCUMENT_POSITION_FOLLOWING:Mt.DOCUMENT_POSITION_PRECEDING);if(n&&e===t)return Mt.DOCUMENT_POSITION_CONTAINS+Mt.DOCUMENT_POSITION_PRECEDING;if(i&&e===t)return Mt.DOCUMENT_POSITION_CONTAINED_BY+Mt.DOCUMENT_POSITION_FOLLOWING;for(var a=[],c=e.parentNode;c;){if(!n&&c===t)return Mt.DOCUMENT_POSITION_CONTAINED_BY+Mt.DOCUMENT_POSITION_FOLLOWING;a.push(c),c=c.parentNode}a.reverse();for(var l=[],d=t.parentNode;d;){if(!i&&d===e)return Mt.DOCUMENT_POSITION_CONTAINS+Mt.DOCUMENT_POSITION_PRECEDING;l.push(d),d=d.parentNode}l.reverse();var u=D5(a,l);for(var f in u.childNodes){var h=u.childNodes[f];if(h===t)return Mt.DOCUMENT_POSITION_FOLLOWING;if(h===e)return Mt.DOCUMENT_POSITION_PRECEDING;if(l.indexOf(h)>=0)return Mt.DOCUMENT_POSITION_FOLLOWING;if(a.indexOf(h)>=0)return Mt.DOCUMENT_POSITION_PRECEDING}return 0}};function F5(r){return r=="<"&&"<"||r==">"&&">"||r=="&"&&"&"||r=='"'&&"""||"&#"+r.charCodeAt()+";"}sd(Ii,nt);sd(Ii,nt.prototype);sd(Mt,nt);sd(Mt,nt.prototype);function yy(r,e){Tr(r,null,{enter:function(t){return e(t)?Tr.STOP:!0}})}function Tr(r,e,t){for(var i=[{node:r,context:e,phase:Tr.ENTER}];i.length>0;){var n=i.pop();if(n.phase===Tr.ENTER){var s=t.enter(n.node,n.context);if(s===Tr.STOP)return Tr.STOP;if(i.push({node:n.node,context:s,phase:Tr.EXIT}),s==null)continue;for(var o=n.node.lastChild;o;)i.push({node:o,context:s,phase:Tr.ENTER}),o=o.previousSibling}else t.exit&&t.exit(n.node,n.context)}}Tr.STOP=Symbol("walkDOM.STOP");Tr.ENTER=0;Tr.EXIT=1;function $s(r,e){mi(r);var t=e||{};this.ownerDocument=this,this.contentType=t.contentType||Ff.XML_APPLICATION,this.type=MY(this.contentType)?"html":"xml"}function ZY(r,e,t){r&&r._inc++;var i=t.namespaceURI;i===ts.XMLNS&&(e._nsMap[t.prefix?t.localName:""]=t.value)}function P5(r,e,t,i){r&&r._inc++;var n=t.namespaceURI;n===ts.XMLNS&&delete e._nsMap[t.prefix?t.localName:""]}function oB(r,e,t){if(r&&r._inc){r._inc++;var i=e.childNodes;if(t&&!t.nextSibling)i[i.length++]=t;else{for(var n=e.firstChild,s=0;n;)i[s++]=n,n=n.nextSibling;i.length=s,delete i[i.length]}}}function T5(r,e){if(r!==e.parentNode)throw new ke(ke.NOT_FOUND_ERR,"child's parent is not parent");var t=e.previousSibling,i=e.nextSibling;return t?t.nextSibling=i:r.firstChild=i,i?i.previousSibling=t:r.lastChild=t,oB(r.ownerDocument,r),e.parentNode=null,e.previousSibling=null,e.nextSibling=null,e}function eW(r){return r&&(r.nodeType===nt.DOCUMENT_NODE||r.nodeType===nt.DOCUMENT_FRAGMENT_NODE||r.nodeType===nt.ELEMENT_NODE)}function tW(r){return r&&(r.nodeType===nt.CDATA_SECTION_NODE||r.nodeType===nt.COMMENT_NODE||r.nodeType===nt.DOCUMENT_FRAGMENT_NODE||r.nodeType===nt.DOCUMENT_TYPE_NODE||r.nodeType===nt.ELEMENT_NODE||r.nodeType===nt.PROCESSING_INSTRUCTION_NODE||r.nodeType===nt.TEXT_NODE)}function ua(r){return r&&r.nodeType===nt.DOCUMENT_TYPE_NODE}function js(r){return r&&r.nodeType===nt.ELEMENT_NODE}function U5(r){return r&&r.nodeType===nt.TEXT_NODE}function x5(r,e){var t=r.childNodes||[];if(es(t,js)||ua(e))return!1;var i=es(t,ua);return!(e&&i&&t.indexOf(i)>t.indexOf(e))}function C5(r,e){var t=r.childNodes||[];function i(s){return js(s)&&s!==e}if(es(t,i))return!1;var n=es(t,ua);return!(e&&n&&t.indexOf(n)>t.indexOf(e))}function rW(r,e,t){if(!eW(r))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Unexpected parent node type "+r.nodeType);if(t&&t.parentNode!==r)throw new ke(ke.NOT_FOUND_ERR,"child not in parent");if(!tW(e)||ua(e)&&r.nodeType!==nt.DOCUMENT_NODE)throw new ke(ke.HIERARCHY_REQUEST_ERR,"Unexpected node type "+e.nodeType+" for parent node type "+r.nodeType)}function iW(r,e,t){var i=r.childNodes||[],n=e.childNodes||[];if(e.nodeType===nt.DOCUMENT_FRAGMENT_NODE){var s=n.filter(js);if(s.length>1||es(n,U5))throw new ke(ke.HIERARCHY_REQUEST_ERR,"More than one element or text in fragment");if(s.length===1&&!x5(r,t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Element in fragment can not be inserted before doctype")}if(js(e)&&!x5(r,t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Only one element can be added and only after doctype");if(ua(e)){if(es(i,ua))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Only one doctype is allowed");var o=es(i,js);if(t&&i.indexOf(o)<i.indexOf(t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Doctype can only be inserted before an element");if(!t&&o)throw new ke(ke.HIERARCHY_REQUEST_ERR,"Doctype can not be appended since element is present")}}function R5(r,e,t){var i=r.childNodes||[],n=e.childNodes||[];if(e.nodeType===nt.DOCUMENT_FRAGMENT_NODE){var s=n.filter(js);if(s.length>1||es(n,U5))throw new ke(ke.HIERARCHY_REQUEST_ERR,"More than one element or text in fragment");if(s.length===1&&!C5(r,t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Element in fragment can not be inserted before doctype")}if(js(e)&&!C5(r,t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Only one element can be added and only after doctype");if(ua(e)){if(es(i,function(c){return ua(c)&&c!==t}))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Only one doctype is allowed");var o=es(i,js);if(t&&i.indexOf(o)<i.indexOf(t))throw new ke(ke.HIERARCHY_REQUEST_ERR,"Doctype can only be inserted before an element")}}function Iy(r,e,t,i){rW(r,e,t),r.nodeType===nt.DOCUMENT_NODE&&(i||iW)(r,e,t);var n=e.parentNode;if(n&&n.removeChild(e),e.nodeType===da){var s=e.firstChild;if(s==null)return e;var o=e.lastChild}else s=o=e;var a=t?t.previousSibling:r.lastChild;s.previousSibling=a,o.nextSibling=t,a?a.nextSibling=s:r.firstChild=s,t==null?r.lastChild=o:t.previousSibling=o;do s.parentNode=r;while(s!==o&&(s=s.nextSibling));return oB(r.ownerDocument||r,r,e),e.nodeType==da&&(e.firstChild=e.lastChild=null),e}$s.prototype={implementation:null,nodeName:"#document",nodeType:td,doctype:null,documentElement:null,_inc:1,insertBefore:function(r,e){if(r.nodeType===da){for(var t=r.firstChild;t;){var i=t.nextSibling;this.insertBefore(t,e),t=i}return r}return Iy(this,r,e),r.ownerDocument=this,this.documentElement===null&&r.nodeType===en&&(this.documentElement=r),r},removeChild:function(r){var e=T5(this,r);return e===this.documentElement&&(this.documentElement=null),e},replaceChild:function(r,e){Iy(this,r,e,R5),r.ownerDocument=this,e&&this.removeChild(e),js(r)&&(this.documentElement=r)},importNode:function(r,e){return nW(this,r,e)},getElementById:function(r){var e=null;return yy(this.documentElement,function(t){if(t.nodeType==en&&t.getAttribute("id")==r)return e=t,!0}),e},createElement:function(r){var e=new Vs(li);e.ownerDocument=this,this.type==="html"&&(r=r.toLowerCase()),OY(this.contentType)&&(e.namespaceURI=ts.HTML),e.nodeName=r,e.tagName=r,e.localName=r,e.childNodes=new hr;var t=e.attributes=new nd;return t._ownerElement=e,e},createDocumentFragment:function(){var r=new od(li);return r.ownerDocument=this,r.childNodes=new hr,r},createTextNode:function(r){var e=new Tf(li);return e.ownerDocument=this,e.childNodes=new hr,e.appendData(r),e},createComment:function(r){var e=new xy(li);return e.ownerDocument=this,e.childNodes=new hr,e.appendData(r),e},createCDATASection:function(r){if(r.indexOf("]]>")!==-1)throw new ke(ke.INVALID_CHARACTER_ERR,'data contains "]]>"');var e=new Cy(li);return e.ownerDocument=this,e.childNodes=new hr,e.appendData(r),e},createProcessingInstruction:function(r,e){var t=new by(li);return t.ownerDocument=this,t.childNodes=new hr,t.nodeName=t.target=r,t.nodeValue=t.data=e,t},createAttribute:function(r){if(!Lt.QName_exact.test(r))throw new ke(ke.INVALID_CHARACTER_ERR,'invalid character in name "'+r+'"');return this.type==="html"&&(r=r.toLowerCase()),this._createAttribute(r)},_createAttribute:function(r){var e=new Pc(li);return e.ownerDocument=this,e.childNodes=new hr,e.name=r,e.nodeName=r,e.localName=r,e.specified=!0,e},createEntityReference:function(r){if(!Lt.Name_exact.test(r))throw new ke(ke.INVALID_CHARACTER_ERR,'not a valid xml name "'+r+'"');if(this.type==="html")throw new ke("document is an html document",Vr.NotSupportedError);var e=new By(li);return e.ownerDocument=this,e.childNodes=new hr,e.nodeName=r,e},createElementNS:function(r,e){var t=rB(r,e),i=new Vs(li),n=i.attributes=new nd;return i.childNodes=new hr,i.ownerDocument=this,i.nodeName=e,i.tagName=e,i.namespaceURI=t[0],i.prefix=t[1],i.localName=t[2],n._ownerElement=i,i},createAttributeNS:function(r,e){var t=rB(r,e),i=new Pc(li);return i.ownerDocument=this,i.childNodes=new hr,i.nodeName=e,i.name=e,i.specified=!0,i.namespaceURI=t[0],i.prefix=t[1],i.localName=t[2],i}};yi($s,nt);function Vs(r){mi(r),this._nsMap=Object.create(null)}Vs.prototype={nodeType:en,attributes:null,getQualifiedName:function(){return this.prefix?this.prefix+":"+this.localName:this.localName},_isInHTMLDocumentAndNamespace:function(){return this.ownerDocument.type==="html"&&this.namespaceURI===ts.HTML},hasAttributes:function(){return!!(this.attributes&&this.attributes.length)},hasAttribute:function(r){return!!this.getAttributeNode(r)},getAttribute:function(r){var e=this.getAttributeNode(r);return e?e.value:null},getAttributeNode:function(r){return this._isInHTMLDocumentAndNamespace()&&(r=r.toLowerCase()),this.attributes.getNamedItem(r)},setAttribute:function(r,e){this._isInHTMLDocumentAndNamespace()&&(r=r.toLowerCase());var t=this.getAttributeNode(r);t?t.value=t.nodeValue=""+e:(t=this.ownerDocument._createAttribute(r),t.value=t.nodeValue=""+e,this.setAttributeNode(t))},removeAttribute:function(r){var e=this.getAttributeNode(r);e&&this.removeAttributeNode(e)},setAttributeNode:function(r){return this.attributes.setNamedItem(r)},setAttributeNodeNS:function(r){return this.attributes.setNamedItemNS(r)},removeAttributeNode:function(r){return this.attributes.removeNamedItem(r.nodeName)},removeAttributeNS:function(r,e){var t=this.getAttributeNodeNS(r,e);t&&this.removeAttributeNode(t)},hasAttributeNS:function(r,e){return this.getAttributeNodeNS(r,e)!=null},getAttributeNS:function(r,e){var t=this.getAttributeNodeNS(r,e);return t?t.value:null},setAttributeNS:function(r,e,t){var i=rB(r,e),n=i[2],s=this.getAttributeNodeNS(r,n);s?s.value=s.nodeValue=""+t:(s=this.ownerDocument.createAttributeNS(r,e),s.value=s.nodeValue=""+t,this.setAttributeNode(s))},getAttributeNodeNS:function(r,e){return this.attributes.getNamedItemNS(r,e)},getElementsByClassName:function(r){var e=y5(r);return new Zn(this,function(t){var i=[];return e.length>0&&yy(t,function(n){if(n!==t&&n.nodeType===en){var s=n.getAttribute("class");if(s){var o=r===s;if(!o){var a=y5(s);o=e.every(JY(a))}o&&i.push(n)}}}),i})},getElementsByTagName:function(r){var e=(this.nodeType===td?this:this.ownerDocument).type==="html",t=r.toLowerCase();return new Zn(this,function(i){var n=[];return yy(i,function(s){if(!(s===i||s.nodeType!==en))if(r==="*")n.push(s);else{var o=s.getQualifiedName(),a=e&&s.namespaceURI===ts.HTML?t:r;o===a&&n.push(s)}}),n})},getElementsByTagNameNS:function(r,e){return new Zn(this,function(t){var i=[];return yy(t,function(n){n!==t&&n.nodeType===en&&(r==="*"||n.namespaceURI===r)&&(e==="*"||n.localName==e)&&i.push(n)}),i})}};$s.prototype.getElementsByClassName=Vs.prototype.getElementsByClassName;$s.prototype.getElementsByTagName=Vs.prototype.getElementsByTagName;$s.prototype.getElementsByTagNameNS=Vs.prototype.getElementsByTagNameNS;yi(Vs,nt);function Pc(r){mi(r),this.namespaceURI=null,this.prefix=null,this.ownerElement=null}Pc.prototype.nodeType=id;yi(Pc,nt);function fa(r){mi(r)}fa.prototype={data:"",substringData:function(r,e){return this.data.substring(r,r+e)},appendData:function(r){r=this.data+r,this.nodeValue=this.data=r,this.length=r.length},insertData:function(r,e){this.replaceData(r,0,e)},deleteData:function(r,e){this.replaceData(r,e,"")},replaceData:function(r,e,t){var i=this.data.substring(0,r),n=this.data.substring(r+e);t=i+t+n,this.nodeValue=this.data=t,this.length=t.length}};yi(fa,nt);function Tf(r){mi(r)}Tf.prototype={nodeName:"#text",nodeType:Pf,splitText:function(r){var e=this.data,t=e.substring(r);e=e.substring(0,r),this.data=this.nodeValue=e,this.length=e.length;var i=this.ownerDocument.createTextNode(t);return this.parentNode&&this.parentNode.insertBefore(i,this.nextSibling),i}};yi(Tf,fa);function xy(r){mi(r)}xy.prototype={nodeName:"#comment",nodeType:nB};yi(xy,fa);function Cy(r){mi(r)}Cy.prototype={nodeName:"#cdata-section",nodeType:v5};yi(Cy,Tf);function Ey(r){mi(r)}Ey.prototype.nodeType=S5;yi(Ey,nt);function aB(r){mi(r)}aB.prototype.nodeType=$Y;yi(aB,nt);function cB(r){mi(r)}cB.prototype.nodeType=jY;yi(cB,nt);function By(r){mi(r)}By.prototype.nodeType=k5;yi(By,nt);function od(r){mi(r)}od.prototype.nodeName="#document-fragment";od.prototype.nodeType=da;yi(od,nt);function by(r){mi(r)}by.prototype.nodeType=iB;yi(by,fa);function N5(){}N5.prototype.serializeToString=function(r,e){return q5.call(r,e)};nt.prototype.toString=q5;function q5(r){var e;typeof r=="function"?e={requireWellFormed:!1,splitCDATASections:!0,nodeFilter:r}:r!=null?e={requireWellFormed:!!r.requireWellFormed,splitCDATASections:r.splitCDATASections!==!1,nodeFilter:r.nodeFilter||null}:e={requireWellFormed:!1,splitCDATASections:!0,nodeFilter:null};var t=[],i=this.nodeType===td&&this.documentElement||this,n=i.prefix,s=i.namespaceURI;if(s&&n==null){var n=i.lookupPrefix(s);if(n==null)var o=[{namespace:s,prefix:null}]}return lB(this,t,o,e),t.join("")}function E5(r,e,t){var i=r.prefix||"",n=r.namespaceURI;if(!n||i==="xml"&&n===ts.XML||n===ts.XMLNS)return!1;for(var s=t.length;s--;){var o=t[s];if(o.prefix===i)return o.namespace!==n}return!0}function my(r,e,t,i){if(i&&!Lt.QName_exact.test(e))throw new ke('The attribute name "'+e+'" is not a valid XML QName',Vr.InvalidStateError);r.push(" ",e,'="',t.replace(/[<>&"\t\n\r]/g,F5),'"')}function lB(r,e,t,i){t||(t=[]);var n=i.nodeFilter,s=i.requireWellFormed,o=i.splitCDATASections,a=r.nodeType===td?r:r.ownerDocument,c=a.type==="html";Tr(r,{ns:t},{enter:function(l,d){var u=d.ns;if(n)if(l=n(l),l){if(typeof l=="string")return e.push(l),null}else return null;switch(l.nodeType){case en:var f=l.attributes,h=f.length,g=l.tagName,p=g;if(!c&&!l.prefix&&l.namespaceURI){for(var I,m=0;m<f.length;m++)if(f.item(m).name==="xmlns"){I=f.item(m).value;break}if(!I)for(var w=u.length-1;w>=0;w--){var B=u[w];if(B.prefix===""&&B.namespace===l.namespaceURI){I=B.namespace;break}}if(I!==l.namespaceURI)for(var w=u.length-1;w>=0;w--){var B=u[w];if(B.namespace===l.namespaceURI){B.prefix&&(p=B.prefix+":"+g);break}}}if(s&&!Lt.QName_exact.test(p))throw new ke('The element name "'+p+'" is not a valid XML QName',Vr.InvalidStateError);e.push("<",p);for(var C=u.slice(),x=0;x<h;x++){var Q=f.item(x);Q.prefix=="xmlns"?C.push({prefix:Q.localName,namespace:Q.value}):Q.nodeName=="xmlns"&&C.push({prefix:"",namespace:Q.value})}for(var x=0;x<h;x++){var Q=f.item(x);if(E5(Q,c,C)){var R=Q.prefix||"",U=Q.namespaceURI;my(e,R?"xmlns:"+R:"xmlns",U,s),C.push({prefix:R,namespace:U})}var T=n?n(Q):Q;T&&(typeof T=="string"?e.push(T):my(e,T.name,T.value,s))}if(g===p&&E5(l,c,C)){var D=l.prefix||"",U=l.namespaceURI;my(e,D?"xmlns:"+D:"xmlns",U,s),C.push({prefix:D,namespace:U})}var b=!l.firstChild;if(b&&(c||l.namespaceURI===ts.HTML)&&(b=HY(g)),b)return e.push("/>"),null;if(e.push(">"),c&&LY(g)){for(var _=l.firstChild;_;)_.data?e.push(_.data):lB(_,e,C.slice(),i),_=_.nextSibling;return e.push("</",p,">"),null}return{ns:C,tag:p};case td:case da:if(s&&l.nodeType===td&&l.documentElement==null)throw new ke("The Document has no documentElement",Vr.InvalidStateError);return{ns:u};case id:return my(e,l.name,l.value,s),null;case Pf:if(s&&Lt.InvalidChar.test(l.data))throw new ke("The Text node data contains characters outside the XML Char production",Vr.InvalidStateError);return e.push(l.data.replace(/[<&>]/g,F5)),null;case v5:if(s&&l.data.indexOf("]]>")!==-1)throw new ke('The CDATASection data contains "]]>"',Vr.InvalidStateError);return o?e.push(Lt.CDATA_START,l.data.replace(/]]>/g,"]]]]><![CDATA[>"),Lt.CDATA_END):e.push(Lt.CDATA_START,l.data,Lt.CDATA_END),null;case nB:if(s){if(Lt.InvalidChar.test(l.data))throw new ke("The comment node data contains characters outside the XML Char production",Vr.InvalidStateError);if(l.data.indexOf("--")!==-1||l.data[l.data.length-1]==="-")throw new ke('The comment node data contains "--" or ends with "-"',Vr.InvalidStateError)}return e.push(Lt.COMMENT_START,l.data,Lt.COMMENT_END),null;case S5:var v=l.publicId,E=l.systemId;if(s){if(!Lt.Name_exact.test(l.name))throw new ke('The doctype name "'+l.name+'" is not a valid XML Name',Vr.InvalidStateError);if(v&&!Lt.PubidLiteral_match.test(v))throw new ke("DocumentType publicId is not a valid PubidLiteral",Vr.InvalidStateError);if(E&&E!=="."&&!Lt.SystemLiteral_match.test(E))throw new ke("DocumentType systemId is not a valid SystemLiteral",Vr.InvalidStateError);if(l.internalSubset&&l.internalSubset.indexOf("]>")!==-1)throw new ke('DocumentType internalSubset contains "]>"',Vr.InvalidStateError)}return e.push(Lt.DOCTYPE_DECL_START," ",l.name),v?(e.push(" ",Lt.PUBLIC," ",v),E&&E!=="."&&e.push(" ",E)):E&&E!=="."&&e.push(" ",Lt.SYSTEM," ",E),l.internalSubset&&e.push(" [",l.internalSubset,"]"),e.push(">"),null;case iB:if(s){if(!Lt.NCName_exact.test(l.target)||l.target.toLowerCase()==="xml")throw new ke('The processing instruction target "'+l.target+'" is not a valid XML NCName or is reserved',Vr.InvalidStateError);if(Lt.InvalidChar.test(l.data))throw new ke("The ProcessingInstruction data contains characters outside the XML Char production",Vr.InvalidStateError);if(l.data.indexOf("?>")!==-1)throw new ke('The ProcessingInstruction data contains "?>"',Vr.InvalidStateError)}return e.push("<?",l.target," ",l.data,"?>"),null;case k5:if(s&&!Lt.Name_exact.test(l.nodeName))throw new ke('The entity reference name "'+l.nodeName+'" is not a valid XML Name',Vr.InvalidStateError);return e.push("&",l.nodeName,";"),null;default:return e.push("??",l.nodeName),null}},exit:function(l,d){d&&d.tag&&e.push("</",d.tag,">")}})}function nW(r,e,t){var i;return Tr(e,null,{enter:function(n,s){var o=n.cloneNode(!1);o.ownerDocument=r,o.parentNode=null,s===null?i=o:s.appendChild(o);var a=n.nodeType===id||t;return a?o:null}}),i}function O5(r,e,t){var i;return Tr(e,null,{enter:function(n,s){var o=new n.constructor(li);for(var a in n)if(rd(n,a)){var c=n[a];typeof c!="object"&&c!=o[a]&&(o[a]=c)}n.childNodes&&(o.childNodes=new hr),o.ownerDocument=r;var l=t;switch(o.nodeType){case en:var d=n.attributes,u=o.attributes=new nd,f=d.length;u._ownerElement=o;for(var h=0;h<f;h++)o.setAttributeNode(O5(r,d.item(h),!0));break;case id:l=!0}return s!==null?s.appendChild(o):i=o,l?o:null}}),i}function M5(r,e,t){r[e]=t}function tB(r){for(var e=[],t=r.firstChild;t;)t.nodeType===en&&e.push(t),t=t.nextSibling;return e}try{Object.defineProperty&&(Object.defineProperty(Zn.prototype,"length",{get:function(){return wy(this),this.$$length}}),Object.defineProperty(nt.prototype,"textContent",{get:function(){if(this.nodeType===en||this.nodeType===da){var r=[];return Tr(this,null,{enter:function(e){if(e.nodeType===en||e.nodeType===da)return!0;if(e.nodeType===iB||e.nodeType===nB)return null;r.push(e.nodeValue)}}),r.join("")}return this.nodeValue},set:function(r){switch(this.nodeType){case en:case da:for(;this.firstChild;)this.removeChild(this.firstChild);(r||String(r))&&this.appendChild(this.ownerDocument.createTextNode(r));break;default:this.data=r,this.value=r,this.nodeValue=r}}}),Object.defineProperty(fa.prototype,"data",{get:function(){return this._data!=null?this._data:""},set:function(r){this._data=r,this.length=typeof r=="string"?r.length:0}}),Object.defineProperty(fa.prototype,"nodeValue",{get:function(){return this.data},set:function(r){this.data=r},enumerable:!0,configurable:!0}),Object.defineProperty(Vs.prototype,"children",{get:function(){return new Zn(this,tB)}}),Object.defineProperty($s.prototype,"children",{get:function(){return new Zn(this,tB)}}),Object.defineProperty(od.prototype,"children",{get:function(){return new Zn(this,tB)}}),M5=function(r,e,t){r["$$"+e]=t})}catch{}Ht._updateLiveList=wy;Ht.Attr=Pc;Ht.CDATASection=Cy;Ht.CharacterData=fa;Ht.Comment=xy;Ht.Document=$s;Ht.DocumentFragment=od;Ht.DocumentType=Ey;Ht.DOMImplementation=_5;Ht.Element=Vs;Ht.Entity=cB;Ht.EntityReference=By;Ht.LiveNodeList=Zn;Ht.NamedNodeMap=nd;Ht.Node=nt;Ht.NodeList=hr;Ht.Notation=aB;Ht.Text=Tf;Ht.ProcessingInstruction=by;Ht.walkDOM=Tr;Ht.XMLSerializer=N5});var H5=oe(Uf=>{"use strict";var L5=Fc().freeze;Uf.XML_ENTITIES=L5({amp:"&",apos:"'",gt:">",lt:"<",quot:'"'});Uf.HTML_ENTITIES=L5({Aacute:"\xC1",aacute:"\xE1",Abreve:"\u0102",abreve:"\u0103",ac:"\u223E",acd:"\u223F",acE:"\u223E\u0333",Acirc:"\xC2",acirc:"\xE2",acute:"\xB4",Acy:"\u0410",acy:"\u0430",AElig:"\xC6",aelig:"\xE6",af:"\u2061",Afr:"\u{1D504}",afr:"\u{1D51E}",Agrave:"\xC0",agrave:"\xE0",alefsym:"\u2135",aleph:"\u2135",Alpha:"\u0391",alpha:"\u03B1",Amacr:"\u0100",amacr:"\u0101",amalg:"\u2A3F",AMP:"&",amp:"&",And:"\u2A53",and:"\u2227",andand:"\u2A55",andd:"\u2A5C",andslope:"\u2A58",andv:"\u2A5A",ang:"\u2220",ange:"\u29A4",angle:"\u2220",angmsd:"\u2221",angmsdaa:"\u29A8",angmsdab:"\u29A9",angmsdac:"\u29AA",angmsdad:"\u29AB",angmsdae:"\u29AC",angmsdaf:"\u29AD",angmsdag:"\u29AE",angmsdah:"\u29AF",angrt:"\u221F",angrtvb:"\u22BE",angrtvbd:"\u299D",angsph:"\u2222",angst:"\xC5",angzarr:"\u237C",Aogon:"\u0104",aogon:"\u0105",Aopf:"\u{1D538}",aopf:"\u{1D552}",ap:"\u2248",apacir:"\u2A6F",apE:"\u2A70",ape:"\u224A",apid:"\u224B",apos:"'",ApplyFunction:"\u2061",approx:"\u2248",approxeq:"\u224A",Aring:"\xC5",aring:"\xE5",Ascr:"\u{1D49C}",ascr:"\u{1D4B6}",Assign:"\u2254",ast:"*",asymp:"\u2248",asympeq:"\u224D",Atilde:"\xC3",atilde:"\xE3",Auml:"\xC4",auml:"\xE4",awconint:"\u2233",awint:"\u2A11",backcong:"\u224C",backepsilon:"\u03F6",backprime:"\u2035",backsim:"\u223D",backsimeq:"\u22CD",Backslash:"\u2216",Barv:"\u2AE7",barvee:"\u22BD",Barwed:"\u2306",barwed:"\u2305",barwedge:"\u2305",bbrk:"\u23B5",bbrktbrk:"\u23B6",bcong:"\u224C",Bcy:"\u0411",bcy:"\u0431",bdquo:"\u201E",becaus:"\u2235",Because:"\u2235",because:"\u2235",bemptyv:"\u29B0",bepsi:"\u03F6",bernou:"\u212C",Bernoullis:"\u212C",Beta:"\u0392",beta:"\u03B2",beth:"\u2136",between:"\u226C",Bfr:"\u{1D505}",bfr:"\u{1D51F}",bigcap:"\u22C2",bigcirc:"\u25EF",bigcup:"\u22C3",bigodot:"\u2A00",bigoplus:"\u2A01",bigotimes:"\u2A02",bigsqcup:"\u2A06",bigstar:"\u2605",bigtriangledown:"\u25BD",bigtriangleup:"\u25B3",biguplus:"\u2A04",bigvee:"\u22C1",bigwedge:"\u22C0",bkarow:"\u290D",blacklozenge:"\u29EB",blacksquare:"\u25AA",blacktriangle:"\u25B4",blacktriangledown:"\u25BE",blacktriangleleft:"\u25C2",blacktriangleright:"\u25B8",blank:"\u2423",blk12:"\u2592",blk14:"\u2591",blk34:"\u2593",block:"\u2588",bne:"=\u20E5",bnequiv:"\u2261\u20E5",bNot:"\u2AED",bnot:"\u2310",Bopf:"\u{1D539}",bopf:"\u{1D553}",bot:"\u22A5",bottom:"\u22A5",bowtie:"\u22C8",boxbox:"\u29C9",boxDL:"\u2557",boxDl:"\u2556",boxdL:"\u2555",boxdl:"\u2510",boxDR:"\u2554",boxDr:"\u2553",boxdR:"\u2552",boxdr:"\u250C",boxH:"\u2550",boxh:"\u2500",boxHD:"\u2566",boxHd:"\u2564",boxhD:"\u2565",boxhd:"\u252C",boxHU:"\u2569",boxHu:"\u2567",boxhU:"\u2568",boxhu:"\u2534",boxminus:"\u229F",boxplus:"\u229E",boxtimes:"\u22A0",boxUL:"\u255D",boxUl:"\u255C",boxuL:"\u255B",boxul:"\u2518",boxUR:"\u255A",boxUr:"\u2559",boxuR:"\u2558",boxur:"\u2514",boxV:"\u2551",boxv:"\u2502",boxVH:"\u256C",boxVh:"\u256B",boxvH:"\u256A",boxvh:"\u253C",boxVL:"\u2563",boxVl:"\u2562",boxvL:"\u2561",boxvl:"\u2524",boxVR:"\u2560",boxVr:"\u255F",boxvR:"\u255E",boxvr:"\u251C",bprime:"\u2035",Breve:"\u02D8",breve:"\u02D8",brvbar:"\xA6",Bscr:"\u212C",bscr:"\u{1D4B7}",bsemi:"\u204F",bsim:"\u223D",bsime:"\u22CD",bsol:"\\",bsolb:"\u29C5",bsolhsub:"\u27C8",bull:"\u2022",bullet:"\u2022",bump:"\u224E",bumpE:"\u2AAE",bumpe:"\u224F",Bumpeq:"\u224E",bumpeq:"\u224F",Cacute:"\u0106",cacute:"\u0107",Cap:"\u22D2",cap:"\u2229",capand:"\u2A44",capbrcup:"\u2A49",capcap:"\u2A4B",capcup:"\u2A47",capdot:"\u2A40",CapitalDifferentialD:"\u2145",caps:"\u2229\uFE00",caret:"\u2041",caron:"\u02C7",Cayleys:"\u212D",ccaps:"\u2A4D",Ccaron:"\u010C",ccaron:"\u010D",Ccedil:"\xC7",ccedil:"\xE7",Ccirc:"\u0108",ccirc:"\u0109",Cconint:"\u2230",ccups:"\u2A4C",ccupssm:"\u2A50",Cdot:"\u010A",cdot:"\u010B",cedil:"\xB8",Cedilla:"\xB8",cemptyv:"\u29B2",cent:"\xA2",CenterDot:"\xB7",centerdot:"\xB7",Cfr:"\u212D",cfr:"\u{1D520}",CHcy:"\u0427",chcy:"\u0447",check:"\u2713",checkmark:"\u2713",Chi:"\u03A7",chi:"\u03C7",cir:"\u25CB",circ:"\u02C6",circeq:"\u2257",circlearrowleft:"\u21BA",circlearrowright:"\u21BB",circledast:"\u229B",circledcirc:"\u229A",circleddash:"\u229D",CircleDot:"\u2299",circledR:"\xAE",circledS:"\u24C8",CircleMinus:"\u2296",CirclePlus:"\u2295",CircleTimes:"\u2297",cirE:"\u29C3",cire:"\u2257",cirfnint:"\u2A10",cirmid:"\u2AEF",cirscir:"\u29C2",ClockwiseContourIntegral:"\u2232",CloseCurlyDoubleQuote:"\u201D",CloseCurlyQuote:"\u2019",clubs:"\u2663",clubsuit:"\u2663",Colon:"\u2237",colon:":",Colone:"\u2A74",colone:"\u2254",coloneq:"\u2254",comma:",",commat:"@",comp:"\u2201",compfn:"\u2218",complement:"\u2201",complexes:"\u2102",cong:"\u2245",congdot:"\u2A6D",Congruent:"\u2261",Conint:"\u222F",conint:"\u222E",ContourIntegral:"\u222E",Copf:"\u2102",copf:"\u{1D554}",coprod:"\u2210",Coproduct:"\u2210",COPY:"\xA9",copy:"\xA9",copysr:"\u2117",CounterClockwiseContourIntegral:"\u2233",crarr:"\u21B5",Cross:"\u2A2F",cross:"\u2717",Cscr:"\u{1D49E}",cscr:"\u{1D4B8}",csub:"\u2ACF",csube:"\u2AD1",csup:"\u2AD0",csupe:"\u2AD2",ctdot:"\u22EF",cudarrl:"\u2938",cudarrr:"\u2935",cuepr:"\u22DE",cuesc:"\u22DF",cularr:"\u21B6",cularrp:"\u293D",Cup:"\u22D3",cup:"\u222A",cupbrcap:"\u2A48",CupCap:"\u224D",cupcap:"\u2A46",cupcup:"\u2A4A",cupdot:"\u228D",cupor:"\u2A45",cups:"\u222A\uFE00",curarr:"\u21B7",curarrm:"\u293C",curlyeqprec:"\u22DE",curlyeqsucc:"\u22DF",curlyvee:"\u22CE",curlywedge:"\u22CF",curren:"\xA4",curvearrowleft:"\u21B6",curvearrowright:"\u21B7",cuvee:"\u22CE",cuwed:"\u22CF",cwconint:"\u2232",cwint:"\u2231",cylcty:"\u232D",Dagger:"\u2021",dagger:"\u2020",daleth:"\u2138",Darr:"\u21A1",dArr:"\u21D3",darr:"\u2193",dash:"\u2010",Dashv:"\u2AE4",dashv:"\u22A3",dbkarow:"\u290F",dblac:"\u02DD",Dcaron:"\u010E",dcaron:"\u010F",Dcy:"\u0414",dcy:"\u0434",DD:"\u2145",dd:"\u2146",ddagger:"\u2021",ddarr:"\u21CA",DDotrahd:"\u2911",ddotseq:"\u2A77",deg:"\xB0",Del:"\u2207",Delta:"\u0394",delta:"\u03B4",demptyv:"\u29B1",dfisht:"\u297F",Dfr:"\u{1D507}",dfr:"\u{1D521}",dHar:"\u2965",dharl:"\u21C3",dharr:"\u21C2",DiacriticalAcute:"\xB4",DiacriticalDot:"\u02D9",DiacriticalDoubleAcute:"\u02DD",DiacriticalGrave:"`",DiacriticalTilde:"\u02DC",diam:"\u22C4",Diamond:"\u22C4",diamond:"\u22C4",diamondsuit:"\u2666",diams:"\u2666",die:"\xA8",DifferentialD:"\u2146",digamma:"\u03DD",disin:"\u22F2",div:"\xF7",divide:"\xF7",divideontimes:"\u22C7",divonx:"\u22C7",DJcy:"\u0402",djcy:"\u0452",dlcorn:"\u231E",dlcrop:"\u230D",dollar:"$",Dopf:"\u{1D53B}",dopf:"\u{1D555}",Dot:"\xA8",dot:"\u02D9",DotDot:"\u20DC",doteq:"\u2250",doteqdot:"\u2251",DotEqual:"\u2250",dotminus:"\u2238",dotplus:"\u2214",dotsquare:"\u22A1",doublebarwedge:"\u2306",DoubleContourIntegral:"\u222F",DoubleDot:"\xA8",DoubleDownArrow:"\u21D3",DoubleLeftArrow:"\u21D0",DoubleLeftRightArrow:"\u21D4",DoubleLeftTee:"\u2AE4",DoubleLongLeftArrow:"\u27F8",DoubleLongLeftRightArrow:"\u27FA",DoubleLongRightArrow:"\u27F9",DoubleRightArrow:"\u21D2",DoubleRightTee:"\u22A8",DoubleUpArrow:"\u21D1",DoubleUpDownArrow:"\u21D5",DoubleVerticalBar:"\u2225",DownArrow:"\u2193",Downarrow:"\u21D3",downarrow:"\u2193",DownArrowBar:"\u2913",DownArrowUpArrow:"\u21F5",DownBreve:"\u0311",downdownarrows:"\u21CA",downharpoonleft:"\u21C3",downharpoonright:"\u21C2",DownLeftRightVector:"\u2950",DownLeftTeeVector:"\u295E",DownLeftVector:"\u21BD",DownLeftVectorBar:"\u2956",DownRightTeeVector:"\u295F",DownRightVector:"\u21C1",DownRightVectorBar:"\u2957",DownTee:"\u22A4",DownTeeArrow:"\u21A7",drbkarow:"\u2910",drcorn:"\u231F",drcrop:"\u230C",Dscr:"\u{1D49F}",dscr:"\u{1D4B9}",DScy:"\u0405",dscy:"\u0455",dsol:"\u29F6",Dstrok:"\u0110",dstrok:"\u0111",dtdot:"\u22F1",dtri:"\u25BF",dtrif:"\u25BE",duarr:"\u21F5",duhar:"\u296F",dwangle:"\u29A6",DZcy:"\u040F",dzcy:"\u045F",dzigrarr:"\u27FF",Eacute:"\xC9",eacute:"\xE9",easter:"\u2A6E",Ecaron:"\u011A",ecaron:"\u011B",ecir:"\u2256",Ecirc:"\xCA",ecirc:"\xEA",ecolon:"\u2255",Ecy:"\u042D",ecy:"\u044D",eDDot:"\u2A77",Edot:"\u0116",eDot:"\u2251",edot:"\u0117",ee:"\u2147",efDot:"\u2252",Efr:"\u{1D508}",efr:"\u{1D522}",eg:"\u2A9A",Egrave:"\xC8",egrave:"\xE8",egs:"\u2A96",egsdot:"\u2A98",el:"\u2A99",Element:"\u2208",elinters:"\u23E7",ell:"\u2113",els:"\u2A95",elsdot:"\u2A97",Emacr:"\u0112",emacr:"\u0113",empty:"\u2205",emptyset:"\u2205",EmptySmallSquare:"\u25FB",emptyv:"\u2205",EmptyVerySmallSquare:"\u25AB",emsp:"\u2003",emsp13:"\u2004",emsp14:"\u2005",ENG:"\u014A",eng:"\u014B",ensp:"\u2002",Eogon:"\u0118",eogon:"\u0119",Eopf:"\u{1D53C}",eopf:"\u{1D556}",epar:"\u22D5",eparsl:"\u29E3",eplus:"\u2A71",epsi:"\u03B5",Epsilon:"\u0395",epsilon:"\u03B5",epsiv:"\u03F5",eqcirc:"\u2256",eqcolon:"\u2255",eqsim:"\u2242",eqslantgtr:"\u2A96",eqslantless:"\u2A95",Equal:"\u2A75",equals:"=",EqualTilde:"\u2242",equest:"\u225F",Equilibrium:"\u21CC",equiv:"\u2261",equivDD:"\u2A78",eqvparsl:"\u29E5",erarr:"\u2971",erDot:"\u2253",Escr:"\u2130",escr:"\u212F",esdot:"\u2250",Esim:"\u2A73",esim:"\u2242",Eta:"\u0397",eta:"\u03B7",ETH:"\xD0",eth:"\xF0",Euml:"\xCB",euml:"\xEB",euro:"\u20AC",excl:"!",exist:"\u2203",Exists:"\u2203",expectation:"\u2130",ExponentialE:"\u2147",exponentiale:"\u2147",fallingdotseq:"\u2252",Fcy:"\u0424",fcy:"\u0444",female:"\u2640",ffilig:"\uFB03",fflig:"\uFB00",ffllig:"\uFB04",Ffr:"\u{1D509}",ffr:"\u{1D523}",filig:"\uFB01",FilledSmallSquare:"\u25FC",FilledVerySmallSquare:"\u25AA",fjlig:"fj",flat:"\u266D",fllig:"\uFB02",fltns:"\u25B1",fnof:"\u0192",Fopf:"\u{1D53D}",fopf:"\u{1D557}",ForAll:"\u2200",forall:"\u2200",fork:"\u22D4",forkv:"\u2AD9",Fouriertrf:"\u2131",fpartint:"\u2A0D",frac12:"\xBD",frac13:"\u2153",frac14:"\xBC",frac15:"\u2155",frac16:"\u2159",frac18:"\u215B",frac23:"\u2154",frac25:"\u2156",frac34:"\xBE",frac35:"\u2157",frac38:"\u215C",frac45:"\u2158",frac56:"\u215A",frac58:"\u215D",frac78:"\u215E",frasl:"\u2044",frown:"\u2322",Fscr:"\u2131",fscr:"\u{1D4BB}",gacute:"\u01F5",Gamma:"\u0393",gamma:"\u03B3",Gammad:"\u03DC",gammad:"\u03DD",gap:"\u2A86",Gbreve:"\u011E",gbreve:"\u011F",Gcedil:"\u0122",Gcirc:"\u011C",gcirc:"\u011D",Gcy:"\u0413",gcy:"\u0433",Gdot:"\u0120",gdot:"\u0121",gE:"\u2267",ge:"\u2265",gEl:"\u2A8C",gel:"\u22DB",geq:"\u2265",geqq:"\u2267",geqslant:"\u2A7E",ges:"\u2A7E",gescc:"\u2AA9",gesdot:"\u2A80",gesdoto:"\u2A82",gesdotol:"\u2A84",gesl:"\u22DB\uFE00",gesles:"\u2A94",Gfr:"\u{1D50A}",gfr:"\u{1D524}",Gg:"\u22D9",gg:"\u226B",ggg:"\u22D9",gimel:"\u2137",GJcy:"\u0403",gjcy:"\u0453",gl:"\u2277",gla:"\u2AA5",glE:"\u2A92",glj:"\u2AA4",gnap:"\u2A8A",gnapprox:"\u2A8A",gnE:"\u2269",gne:"\u2A88",gneq:"\u2A88",gneqq:"\u2269",gnsim:"\u22E7",Gopf:"\u{1D53E}",gopf:"\u{1D558}",grave:"`",GreaterEqual:"\u2265",GreaterEqualLess:"\u22DB",GreaterFullEqual:"\u2267",GreaterGreater:"\u2AA2",GreaterLess:"\u2277",GreaterSlantEqual:"\u2A7E",GreaterTilde:"\u2273",Gscr:"\u{1D4A2}",gscr:"\u210A",gsim:"\u2273",gsime:"\u2A8E",gsiml:"\u2A90",Gt:"\u226B",GT:">",gt:">",gtcc:"\u2AA7",gtcir:"\u2A7A",gtdot:"\u22D7",gtlPar:"\u2995",gtquest:"\u2A7C",gtrapprox:"\u2A86",gtrarr:"\u2978",gtrdot:"\u22D7",gtreqless:"\u22DB",gtreqqless:"\u2A8C",gtrless:"\u2277",gtrsim:"\u2273",gvertneqq:"\u2269\uFE00",gvnE:"\u2269\uFE00",Hacek:"\u02C7",hairsp:"\u200A",half:"\xBD",hamilt:"\u210B",HARDcy:"\u042A",hardcy:"\u044A",hArr:"\u21D4",harr:"\u2194",harrcir:"\u2948",harrw:"\u21AD",Hat:"^",hbar:"\u210F",Hcirc:"\u0124",hcirc:"\u0125",hearts:"\u2665",heartsuit:"\u2665",hellip:"\u2026",hercon:"\u22B9",Hfr:"\u210C",hfr:"\u{1D525}",HilbertSpace:"\u210B",hksearow:"\u2925",hkswarow:"\u2926",hoarr:"\u21FF",homtht:"\u223B",hookleftarrow:"\u21A9",hookrightarrow:"\u21AA",Hopf:"\u210D",hopf:"\u{1D559}",horbar:"\u2015",HorizontalLine:"\u2500",Hscr:"\u210B",hscr:"\u{1D4BD}",hslash:"\u210F",Hstrok:"\u0126",hstrok:"\u0127",HumpDownHump:"\u224E",HumpEqual:"\u224F",hybull:"\u2043",hyphen:"\u2010",Iacute:"\xCD",iacute:"\xED",ic:"\u2063",Icirc:"\xCE",icirc:"\xEE",Icy:"\u0418",icy:"\u0438",Idot:"\u0130",IEcy:"\u0415",iecy:"\u0435",iexcl:"\xA1",iff:"\u21D4",Ifr:"\u2111",ifr:"\u{1D526}",Igrave:"\xCC",igrave:"\xEC",ii:"\u2148",iiiint:"\u2A0C",iiint:"\u222D",iinfin:"\u29DC",iiota:"\u2129",IJlig:"\u0132",ijlig:"\u0133",Im:"\u2111",Imacr:"\u012A",imacr:"\u012B",image:"\u2111",ImaginaryI:"\u2148",imagline:"\u2110",imagpart:"\u2111",imath:"\u0131",imof:"\u22B7",imped:"\u01B5",Implies:"\u21D2",in:"\u2208",incare:"\u2105",infin:"\u221E",infintie:"\u29DD",inodot:"\u0131",Int:"\u222C",int:"\u222B",intcal:"\u22BA",integers:"\u2124",Integral:"\u222B",intercal:"\u22BA",Intersection:"\u22C2",intlarhk:"\u2A17",intprod:"\u2A3C",InvisibleComma:"\u2063",InvisibleTimes:"\u2062",IOcy:"\u0401",iocy:"\u0451",Iogon:"\u012E",iogon:"\u012F",Iopf:"\u{1D540}",iopf:"\u{1D55A}",Iota:"\u0399",iota:"\u03B9",iprod:"\u2A3C",iquest:"\xBF",Iscr:"\u2110",iscr:"\u{1D4BE}",isin:"\u2208",isindot:"\u22F5",isinE:"\u22F9",isins:"\u22F4",isinsv:"\u22F3",isinv:"\u2208",it:"\u2062",Itilde:"\u0128",itilde:"\u0129",Iukcy:"\u0406",iukcy:"\u0456",Iuml:"\xCF",iuml:"\xEF",Jcirc:"\u0134",jcirc:"\u0135",Jcy:"\u0419",jcy:"\u0439",Jfr:"\u{1D50D}",jfr:"\u{1D527}",jmath:"\u0237",Jopf:"\u{1D541}",jopf:"\u{1D55B}",Jscr:"\u{1D4A5}",jscr:"\u{1D4BF}",Jsercy:"\u0408",jsercy:"\u0458",Jukcy:"\u0404",jukcy:"\u0454",Kappa:"\u039A",kappa:"\u03BA",kappav:"\u03F0",Kcedil:"\u0136",kcedil:"\u0137",Kcy:"\u041A",kcy:"\u043A",Kfr:"\u{1D50E}",kfr:"\u{1D528}",kgreen:"\u0138",KHcy:"\u0425",khcy:"\u0445",KJcy:"\u040C",kjcy:"\u045C",Kopf:"\u{1D542}",kopf:"\u{1D55C}",Kscr:"\u{1D4A6}",kscr:"\u{1D4C0}",lAarr:"\u21DA",Lacute:"\u0139",lacute:"\u013A",laemptyv:"\u29B4",lagran:"\u2112",Lambda:"\u039B",lambda:"\u03BB",Lang:"\u27EA",lang:"\u27E8",langd:"\u2991",langle:"\u27E8",lap:"\u2A85",Laplacetrf:"\u2112",laquo:"\xAB",Larr:"\u219E",lArr:"\u21D0",larr:"\u2190",larrb:"\u21E4",larrbfs:"\u291F",larrfs:"\u291D",larrhk:"\u21A9",larrlp:"\u21AB",larrpl:"\u2939",larrsim:"\u2973",larrtl:"\u21A2",lat:"\u2AAB",lAtail:"\u291B",latail:"\u2919",late:"\u2AAD",lates:"\u2AAD\uFE00",lBarr:"\u290E",lbarr:"\u290C",lbbrk:"\u2772",lbrace:"{",lbrack:"[",lbrke:"\u298B",lbrksld:"\u298F",lbrkslu:"\u298D",Lcaron:"\u013D",lcaron:"\u013E",Lcedil:"\u013B",lcedil:"\u013C",lceil:"\u2308",lcub:"{",Lcy:"\u041B",lcy:"\u043B",ldca:"\u2936",ldquo:"\u201C",ldquor:"\u201E",ldrdhar:"\u2967",ldrushar:"\u294B",ldsh:"\u21B2",lE:"\u2266",le:"\u2264",LeftAngleBracket:"\u27E8",LeftArrow:"\u2190",Leftarrow:"\u21D0",leftarrow:"\u2190",LeftArrowBar:"\u21E4",LeftArrowRightArrow:"\u21C6",leftarrowtail:"\u21A2",LeftCeiling:"\u2308",LeftDoubleBracket:"\u27E6",LeftDownTeeVector:"\u2961",LeftDownVector:"\u21C3",LeftDownVectorBar:"\u2959",LeftFloor:"\u230A",leftharpoondown:"\u21BD",leftharpoonup:"\u21BC",leftleftarrows:"\u21C7",LeftRightArrow:"\u2194",Leftrightarrow:"\u21D4",leftrightarrow:"\u2194",leftrightarrows:"\u21C6",leftrightharpoons:"\u21CB",leftrightsquigarrow:"\u21AD",LeftRightVector:"\u294E",LeftTee:"\u22A3",LeftTeeArrow:"\u21A4",LeftTeeVector:"\u295A",leftthreetimes:"\u22CB",LeftTriangle:"\u22B2",LeftTriangleBar:"\u29CF",LeftTriangleEqual:"\u22B4",LeftUpDownVector:"\u2951",LeftUpTeeVector:"\u2960",LeftUpVector:"\u21BF",LeftUpVectorBar:"\u2958",LeftVector:"\u21BC",LeftVectorBar:"\u2952",lEg:"\u2A8B",leg:"\u22DA",leq:"\u2264",leqq:"\u2266",leqslant:"\u2A7D",les:"\u2A7D",lescc:"\u2AA8",lesdot:"\u2A7F",lesdoto:"\u2A81",lesdotor:"\u2A83",lesg:"\u22DA\uFE00",lesges:"\u2A93",lessapprox:"\u2A85",lessdot:"\u22D6",lesseqgtr:"\u22DA",lesseqqgtr:"\u2A8B",LessEqualGreater:"\u22DA",LessFullEqual:"\u2266",LessGreater:"\u2276",lessgtr:"\u2276",LessLess:"\u2AA1",lesssim:"\u2272",LessSlantEqual:"\u2A7D",LessTilde:"\u2272",lfisht:"\u297C",lfloor:"\u230A",Lfr:"\u{1D50F}",lfr:"\u{1D529}",lg:"\u2276",lgE:"\u2A91",lHar:"\u2962",lhard:"\u21BD",lharu:"\u21BC",lharul:"\u296A",lhblk:"\u2584",LJcy:"\u0409",ljcy:"\u0459",Ll:"\u22D8",ll:"\u226A",llarr:"\u21C7",llcorner:"\u231E",Lleftarrow:"\u21DA",llhard:"\u296B",lltri:"\u25FA",Lmidot:"\u013F",lmidot:"\u0140",lmoust:"\u23B0",lmoustache:"\u23B0",lnap:"\u2A89",lnapprox:"\u2A89",lnE:"\u2268",lne:"\u2A87",lneq:"\u2A87",lneqq:"\u2268",lnsim:"\u22E6",loang:"\u27EC",loarr:"\u21FD",lobrk:"\u27E6",LongLeftArrow:"\u27F5",Longleftarrow:"\u27F8",longleftarrow:"\u27F5",LongLeftRightArrow:"\u27F7",Longleftrightarrow:"\u27FA",longleftrightarrow:"\u27F7",longmapsto:"\u27FC",LongRightArrow:"\u27F6",Longrightarrow:"\u27F9",longrightarrow:"\u27F6",looparrowleft:"\u21AB",looparrowright:"\u21AC",lopar:"\u2985",Lopf:"\u{1D543}",lopf:"\u{1D55D}",loplus:"\u2A2D",lotimes:"\u2A34",lowast:"\u2217",lowbar:"_",LowerLeftArrow:"\u2199",LowerRightArrow:"\u2198",loz:"\u25CA",lozenge:"\u25CA",lozf:"\u29EB",lpar:"(",lparlt:"\u2993",lrarr:"\u21C6",lrcorner:"\u231F",lrhar:"\u21CB",lrhard:"\u296D",lrm:"\u200E",lrtri:"\u22BF",lsaquo:"\u2039",Lscr:"\u2112",lscr:"\u{1D4C1}",Lsh:"\u21B0",lsh:"\u21B0",lsim:"\u2272",lsime:"\u2A8D",lsimg:"\u2A8F",lsqb:"[",lsquo:"\u2018",lsquor:"\u201A",Lstrok:"\u0141",lstrok:"\u0142",Lt:"\u226A",LT:"<",lt:"<",ltcc:"\u2AA6",ltcir:"\u2A79",ltdot:"\u22D6",lthree:"\u22CB",ltimes:"\u22C9",ltlarr:"\u2976",ltquest:"\u2A7B",ltri:"\u25C3",ltrie:"\u22B4",ltrif:"\u25C2",ltrPar:"\u2996",lurdshar:"\u294A",luruhar:"\u2966",lvertneqq:"\u2268\uFE00",lvnE:"\u2268\uFE00",macr:"\xAF",male:"\u2642",malt:"\u2720",maltese:"\u2720",Map:"\u2905",map:"\u21A6",mapsto:"\u21A6",mapstodown:"\u21A7",mapstoleft:"\u21A4",mapstoup:"\u21A5",marker:"\u25AE",mcomma:"\u2A29",Mcy:"\u041C",mcy:"\u043C",mdash:"\u2014",mDDot:"\u223A",measuredangle:"\u2221",MediumSpace:"\u205F",Mellintrf:"\u2133",Mfr:"\u{1D510}",mfr:"\u{1D52A}",mho:"\u2127",micro:"\xB5",mid:"\u2223",midast:"*",midcir:"\u2AF0",middot:"\xB7",minus:"\u2212",minusb:"\u229F",minusd:"\u2238",minusdu:"\u2A2A",MinusPlus:"\u2213",mlcp:"\u2ADB",mldr:"\u2026",mnplus:"\u2213",models:"\u22A7",Mopf:"\u{1D544}",mopf:"\u{1D55E}",mp:"\u2213",Mscr:"\u2133",mscr:"\u{1D4C2}",mstpos:"\u223E",Mu:"\u039C",mu:"\u03BC",multimap:"\u22B8",mumap:"\u22B8",nabla:"\u2207",Nacute:"\u0143",nacute:"\u0144",nang:"\u2220\u20D2",nap:"\u2249",napE:"\u2A70\u0338",napid:"\u224B\u0338",napos:"\u0149",napprox:"\u2249",natur:"\u266E",natural:"\u266E",naturals:"\u2115",nbsp:"\xA0",nbump:"\u224E\u0338",nbumpe:"\u224F\u0338",ncap:"\u2A43",Ncaron:"\u0147",ncaron:"\u0148",Ncedil:"\u0145",ncedil:"\u0146",ncong:"\u2247",ncongdot:"\u2A6D\u0338",ncup:"\u2A42",Ncy:"\u041D",ncy:"\u043D",ndash:"\u2013",ne:"\u2260",nearhk:"\u2924",neArr:"\u21D7",nearr:"\u2197",nearrow:"\u2197",nedot:"\u2250\u0338",NegativeMediumSpace:"\u200B",NegativeThickSpace:"\u200B",NegativeThinSpace:"\u200B",NegativeVeryThinSpace:"\u200B",nequiv:"\u2262",nesear:"\u2928",nesim:"\u2242\u0338",NestedGreaterGreater:"\u226B",NestedLessLess:"\u226A",NewLine:` -`,nexist:"\u2204",nexists:"\u2204",Nfr:"\u{1D511}",nfr:"\u{1D52B}",ngE:"\u2267\u0338",nge:"\u2271",ngeq:"\u2271",ngeqq:"\u2267\u0338",ngeqslant:"\u2A7E\u0338",nges:"\u2A7E\u0338",nGg:"\u22D9\u0338",ngsim:"\u2275",nGt:"\u226B\u20D2",ngt:"\u226F",ngtr:"\u226F",nGtv:"\u226B\u0338",nhArr:"\u21CE",nharr:"\u21AE",nhpar:"\u2AF2",ni:"\u220B",nis:"\u22FC",nisd:"\u22FA",niv:"\u220B",NJcy:"\u040A",njcy:"\u045A",nlArr:"\u21CD",nlarr:"\u219A",nldr:"\u2025",nlE:"\u2266\u0338",nle:"\u2270",nLeftarrow:"\u21CD",nleftarrow:"\u219A",nLeftrightarrow:"\u21CE",nleftrightarrow:"\u21AE",nleq:"\u2270",nleqq:"\u2266\u0338",nleqslant:"\u2A7D\u0338",nles:"\u2A7D\u0338",nless:"\u226E",nLl:"\u22D8\u0338",nlsim:"\u2274",nLt:"\u226A\u20D2",nlt:"\u226E",nltri:"\u22EA",nltrie:"\u22EC",nLtv:"\u226A\u0338",nmid:"\u2224",NoBreak:"\u2060",NonBreakingSpace:"\xA0",Nopf:"\u2115",nopf:"\u{1D55F}",Not:"\u2AEC",not:"\xAC",NotCongruent:"\u2262",NotCupCap:"\u226D",NotDoubleVerticalBar:"\u2226",NotElement:"\u2209",NotEqual:"\u2260",NotEqualTilde:"\u2242\u0338",NotExists:"\u2204",NotGreater:"\u226F",NotGreaterEqual:"\u2271",NotGreaterFullEqual:"\u2267\u0338",NotGreaterGreater:"\u226B\u0338",NotGreaterLess:"\u2279",NotGreaterSlantEqual:"\u2A7E\u0338",NotGreaterTilde:"\u2275",NotHumpDownHump:"\u224E\u0338",NotHumpEqual:"\u224F\u0338",notin:"\u2209",notindot:"\u22F5\u0338",notinE:"\u22F9\u0338",notinva:"\u2209",notinvb:"\u22F7",notinvc:"\u22F6",NotLeftTriangle:"\u22EA",NotLeftTriangleBar:"\u29CF\u0338",NotLeftTriangleEqual:"\u22EC",NotLess:"\u226E",NotLessEqual:"\u2270",NotLessGreater:"\u2278",NotLessLess:"\u226A\u0338",NotLessSlantEqual:"\u2A7D\u0338",NotLessTilde:"\u2274",NotNestedGreaterGreater:"\u2AA2\u0338",NotNestedLessLess:"\u2AA1\u0338",notni:"\u220C",notniva:"\u220C",notnivb:"\u22FE",notnivc:"\u22FD",NotPrecedes:"\u2280",NotPrecedesEqual:"\u2AAF\u0338",NotPrecedesSlantEqual:"\u22E0",NotReverseElement:"\u220C",NotRightTriangle:"\u22EB",NotRightTriangleBar:"\u29D0\u0338",NotRightTriangleEqual:"\u22ED",NotSquareSubset:"\u228F\u0338",NotSquareSubsetEqual:"\u22E2",NotSquareSuperset:"\u2290\u0338",NotSquareSupersetEqual:"\u22E3",NotSubset:"\u2282\u20D2",NotSubsetEqual:"\u2288",NotSucceeds:"\u2281",NotSucceedsEqual:"\u2AB0\u0338",NotSucceedsSlantEqual:"\u22E1",NotSucceedsTilde:"\u227F\u0338",NotSuperset:"\u2283\u20D2",NotSupersetEqual:"\u2289",NotTilde:"\u2241",NotTildeEqual:"\u2244",NotTildeFullEqual:"\u2247",NotTildeTilde:"\u2249",NotVerticalBar:"\u2224",npar:"\u2226",nparallel:"\u2226",nparsl:"\u2AFD\u20E5",npart:"\u2202\u0338",npolint:"\u2A14",npr:"\u2280",nprcue:"\u22E0",npre:"\u2AAF\u0338",nprec:"\u2280",npreceq:"\u2AAF\u0338",nrArr:"\u21CF",nrarr:"\u219B",nrarrc:"\u2933\u0338",nrarrw:"\u219D\u0338",nRightarrow:"\u21CF",nrightarrow:"\u219B",nrtri:"\u22EB",nrtrie:"\u22ED",nsc:"\u2281",nsccue:"\u22E1",nsce:"\u2AB0\u0338",Nscr:"\u{1D4A9}",nscr:"\u{1D4C3}",nshortmid:"\u2224",nshortparallel:"\u2226",nsim:"\u2241",nsime:"\u2244",nsimeq:"\u2244",nsmid:"\u2224",nspar:"\u2226",nsqsube:"\u22E2",nsqsupe:"\u22E3",nsub:"\u2284",nsubE:"\u2AC5\u0338",nsube:"\u2288",nsubset:"\u2282\u20D2",nsubseteq:"\u2288",nsubseteqq:"\u2AC5\u0338",nsucc:"\u2281",nsucceq:"\u2AB0\u0338",nsup:"\u2285",nsupE:"\u2AC6\u0338",nsupe:"\u2289",nsupset:"\u2283\u20D2",nsupseteq:"\u2289",nsupseteqq:"\u2AC6\u0338",ntgl:"\u2279",Ntilde:"\xD1",ntilde:"\xF1",ntlg:"\u2278",ntriangleleft:"\u22EA",ntrianglelefteq:"\u22EC",ntriangleright:"\u22EB",ntrianglerighteq:"\u22ED",Nu:"\u039D",nu:"\u03BD",num:"#",numero:"\u2116",numsp:"\u2007",nvap:"\u224D\u20D2",nVDash:"\u22AF",nVdash:"\u22AE",nvDash:"\u22AD",nvdash:"\u22AC",nvge:"\u2265\u20D2",nvgt:">\u20D2",nvHarr:"\u2904",nvinfin:"\u29DE",nvlArr:"\u2902",nvle:"\u2264\u20D2",nvlt:"<\u20D2",nvltrie:"\u22B4\u20D2",nvrArr:"\u2903",nvrtrie:"\u22B5\u20D2",nvsim:"\u223C\u20D2",nwarhk:"\u2923",nwArr:"\u21D6",nwarr:"\u2196",nwarrow:"\u2196",nwnear:"\u2927",Oacute:"\xD3",oacute:"\xF3",oast:"\u229B",ocir:"\u229A",Ocirc:"\xD4",ocirc:"\xF4",Ocy:"\u041E",ocy:"\u043E",odash:"\u229D",Odblac:"\u0150",odblac:"\u0151",odiv:"\u2A38",odot:"\u2299",odsold:"\u29BC",OElig:"\u0152",oelig:"\u0153",ofcir:"\u29BF",Ofr:"\u{1D512}",ofr:"\u{1D52C}",ogon:"\u02DB",Ograve:"\xD2",ograve:"\xF2",ogt:"\u29C1",ohbar:"\u29B5",ohm:"\u03A9",oint:"\u222E",olarr:"\u21BA",olcir:"\u29BE",olcross:"\u29BB",oline:"\u203E",olt:"\u29C0",Omacr:"\u014C",omacr:"\u014D",Omega:"\u03A9",omega:"\u03C9",Omicron:"\u039F",omicron:"\u03BF",omid:"\u29B6",ominus:"\u2296",Oopf:"\u{1D546}",oopf:"\u{1D560}",opar:"\u29B7",OpenCurlyDoubleQuote:"\u201C",OpenCurlyQuote:"\u2018",operp:"\u29B9",oplus:"\u2295",Or:"\u2A54",or:"\u2228",orarr:"\u21BB",ord:"\u2A5D",order:"\u2134",orderof:"\u2134",ordf:"\xAA",ordm:"\xBA",origof:"\u22B6",oror:"\u2A56",orslope:"\u2A57",orv:"\u2A5B",oS:"\u24C8",Oscr:"\u{1D4AA}",oscr:"\u2134",Oslash:"\xD8",oslash:"\xF8",osol:"\u2298",Otilde:"\xD5",otilde:"\xF5",Otimes:"\u2A37",otimes:"\u2297",otimesas:"\u2A36",Ouml:"\xD6",ouml:"\xF6",ovbar:"\u233D",OverBar:"\u203E",OverBrace:"\u23DE",OverBracket:"\u23B4",OverParenthesis:"\u23DC",par:"\u2225",para:"\xB6",parallel:"\u2225",parsim:"\u2AF3",parsl:"\u2AFD",part:"\u2202",PartialD:"\u2202",Pcy:"\u041F",pcy:"\u043F",percnt:"%",period:".",permil:"\u2030",perp:"\u22A5",pertenk:"\u2031",Pfr:"\u{1D513}",pfr:"\u{1D52D}",Phi:"\u03A6",phi:"\u03C6",phiv:"\u03D5",phmmat:"\u2133",phone:"\u260E",Pi:"\u03A0",pi:"\u03C0",pitchfork:"\u22D4",piv:"\u03D6",planck:"\u210F",planckh:"\u210E",plankv:"\u210F",plus:"+",plusacir:"\u2A23",plusb:"\u229E",pluscir:"\u2A22",plusdo:"\u2214",plusdu:"\u2A25",pluse:"\u2A72",PlusMinus:"\xB1",plusmn:"\xB1",plussim:"\u2A26",plustwo:"\u2A27",pm:"\xB1",Poincareplane:"\u210C",pointint:"\u2A15",Popf:"\u2119",popf:"\u{1D561}",pound:"\xA3",Pr:"\u2ABB",pr:"\u227A",prap:"\u2AB7",prcue:"\u227C",prE:"\u2AB3",pre:"\u2AAF",prec:"\u227A",precapprox:"\u2AB7",preccurlyeq:"\u227C",Precedes:"\u227A",PrecedesEqual:"\u2AAF",PrecedesSlantEqual:"\u227C",PrecedesTilde:"\u227E",preceq:"\u2AAF",precnapprox:"\u2AB9",precneqq:"\u2AB5",precnsim:"\u22E8",precsim:"\u227E",Prime:"\u2033",prime:"\u2032",primes:"\u2119",prnap:"\u2AB9",prnE:"\u2AB5",prnsim:"\u22E8",prod:"\u220F",Product:"\u220F",profalar:"\u232E",profline:"\u2312",profsurf:"\u2313",prop:"\u221D",Proportion:"\u2237",Proportional:"\u221D",propto:"\u221D",prsim:"\u227E",prurel:"\u22B0",Pscr:"\u{1D4AB}",pscr:"\u{1D4C5}",Psi:"\u03A8",psi:"\u03C8",puncsp:"\u2008",Qfr:"\u{1D514}",qfr:"\u{1D52E}",qint:"\u2A0C",Qopf:"\u211A",qopf:"\u{1D562}",qprime:"\u2057",Qscr:"\u{1D4AC}",qscr:"\u{1D4C6}",quaternions:"\u210D",quatint:"\u2A16",quest:"?",questeq:"\u225F",QUOT:'"',quot:'"',rAarr:"\u21DB",race:"\u223D\u0331",Racute:"\u0154",racute:"\u0155",radic:"\u221A",raemptyv:"\u29B3",Rang:"\u27EB",rang:"\u27E9",rangd:"\u2992",range:"\u29A5",rangle:"\u27E9",raquo:"\xBB",Rarr:"\u21A0",rArr:"\u21D2",rarr:"\u2192",rarrap:"\u2975",rarrb:"\u21E5",rarrbfs:"\u2920",rarrc:"\u2933",rarrfs:"\u291E",rarrhk:"\u21AA",rarrlp:"\u21AC",rarrpl:"\u2945",rarrsim:"\u2974",Rarrtl:"\u2916",rarrtl:"\u21A3",rarrw:"\u219D",rAtail:"\u291C",ratail:"\u291A",ratio:"\u2236",rationals:"\u211A",RBarr:"\u2910",rBarr:"\u290F",rbarr:"\u290D",rbbrk:"\u2773",rbrace:"}",rbrack:"]",rbrke:"\u298C",rbrksld:"\u298E",rbrkslu:"\u2990",Rcaron:"\u0158",rcaron:"\u0159",Rcedil:"\u0156",rcedil:"\u0157",rceil:"\u2309",rcub:"}",Rcy:"\u0420",rcy:"\u0440",rdca:"\u2937",rdldhar:"\u2969",rdquo:"\u201D",rdquor:"\u201D",rdsh:"\u21B3",Re:"\u211C",real:"\u211C",realine:"\u211B",realpart:"\u211C",reals:"\u211D",rect:"\u25AD",REG:"\xAE",reg:"\xAE",ReverseElement:"\u220B",ReverseEquilibrium:"\u21CB",ReverseUpEquilibrium:"\u296F",rfisht:"\u297D",rfloor:"\u230B",Rfr:"\u211C",rfr:"\u{1D52F}",rHar:"\u2964",rhard:"\u21C1",rharu:"\u21C0",rharul:"\u296C",Rho:"\u03A1",rho:"\u03C1",rhov:"\u03F1",RightAngleBracket:"\u27E9",RightArrow:"\u2192",Rightarrow:"\u21D2",rightarrow:"\u2192",RightArrowBar:"\u21E5",RightArrowLeftArrow:"\u21C4",rightarrowtail:"\u21A3",RightCeiling:"\u2309",RightDoubleBracket:"\u27E7",RightDownTeeVector:"\u295D",RightDownVector:"\u21C2",RightDownVectorBar:"\u2955",RightFloor:"\u230B",rightharpoondown:"\u21C1",rightharpoonup:"\u21C0",rightleftarrows:"\u21C4",rightleftharpoons:"\u21CC",rightrightarrows:"\u21C9",rightsquigarrow:"\u219D",RightTee:"\u22A2",RightTeeArrow:"\u21A6",RightTeeVector:"\u295B",rightthreetimes:"\u22CC",RightTriangle:"\u22B3",RightTriangleBar:"\u29D0",RightTriangleEqual:"\u22B5",RightUpDownVector:"\u294F",RightUpTeeVector:"\u295C",RightUpVector:"\u21BE",RightUpVectorBar:"\u2954",RightVector:"\u21C0",RightVectorBar:"\u2953",ring:"\u02DA",risingdotseq:"\u2253",rlarr:"\u21C4",rlhar:"\u21CC",rlm:"\u200F",rmoust:"\u23B1",rmoustache:"\u23B1",rnmid:"\u2AEE",roang:"\u27ED",roarr:"\u21FE",robrk:"\u27E7",ropar:"\u2986",Ropf:"\u211D",ropf:"\u{1D563}",roplus:"\u2A2E",rotimes:"\u2A35",RoundImplies:"\u2970",rpar:")",rpargt:"\u2994",rppolint:"\u2A12",rrarr:"\u21C9",Rrightarrow:"\u21DB",rsaquo:"\u203A",Rscr:"\u211B",rscr:"\u{1D4C7}",Rsh:"\u21B1",rsh:"\u21B1",rsqb:"]",rsquo:"\u2019",rsquor:"\u2019",rthree:"\u22CC",rtimes:"\u22CA",rtri:"\u25B9",rtrie:"\u22B5",rtrif:"\u25B8",rtriltri:"\u29CE",RuleDelayed:"\u29F4",ruluhar:"\u2968",rx:"\u211E",Sacute:"\u015A",sacute:"\u015B",sbquo:"\u201A",Sc:"\u2ABC",sc:"\u227B",scap:"\u2AB8",Scaron:"\u0160",scaron:"\u0161",sccue:"\u227D",scE:"\u2AB4",sce:"\u2AB0",Scedil:"\u015E",scedil:"\u015F",Scirc:"\u015C",scirc:"\u015D",scnap:"\u2ABA",scnE:"\u2AB6",scnsim:"\u22E9",scpolint:"\u2A13",scsim:"\u227F",Scy:"\u0421",scy:"\u0441",sdot:"\u22C5",sdotb:"\u22A1",sdote:"\u2A66",searhk:"\u2925",seArr:"\u21D8",searr:"\u2198",searrow:"\u2198",sect:"\xA7",semi:";",seswar:"\u2929",setminus:"\u2216",setmn:"\u2216",sext:"\u2736",Sfr:"\u{1D516}",sfr:"\u{1D530}",sfrown:"\u2322",sharp:"\u266F",SHCHcy:"\u0429",shchcy:"\u0449",SHcy:"\u0428",shcy:"\u0448",ShortDownArrow:"\u2193",ShortLeftArrow:"\u2190",shortmid:"\u2223",shortparallel:"\u2225",ShortRightArrow:"\u2192",ShortUpArrow:"\u2191",shy:"\xAD",Sigma:"\u03A3",sigma:"\u03C3",sigmaf:"\u03C2",sigmav:"\u03C2",sim:"\u223C",simdot:"\u2A6A",sime:"\u2243",simeq:"\u2243",simg:"\u2A9E",simgE:"\u2AA0",siml:"\u2A9D",simlE:"\u2A9F",simne:"\u2246",simplus:"\u2A24",simrarr:"\u2972",slarr:"\u2190",SmallCircle:"\u2218",smallsetminus:"\u2216",smashp:"\u2A33",smeparsl:"\u29E4",smid:"\u2223",smile:"\u2323",smt:"\u2AAA",smte:"\u2AAC",smtes:"\u2AAC\uFE00",SOFTcy:"\u042C",softcy:"\u044C",sol:"/",solb:"\u29C4",solbar:"\u233F",Sopf:"\u{1D54A}",sopf:"\u{1D564}",spades:"\u2660",spadesuit:"\u2660",spar:"\u2225",sqcap:"\u2293",sqcaps:"\u2293\uFE00",sqcup:"\u2294",sqcups:"\u2294\uFE00",Sqrt:"\u221A",sqsub:"\u228F",sqsube:"\u2291",sqsubset:"\u228F",sqsubseteq:"\u2291",sqsup:"\u2290",sqsupe:"\u2292",sqsupset:"\u2290",sqsupseteq:"\u2292",squ:"\u25A1",Square:"\u25A1",square:"\u25A1",SquareIntersection:"\u2293",SquareSubset:"\u228F",SquareSubsetEqual:"\u2291",SquareSuperset:"\u2290",SquareSupersetEqual:"\u2292",SquareUnion:"\u2294",squarf:"\u25AA",squf:"\u25AA",srarr:"\u2192",Sscr:"\u{1D4AE}",sscr:"\u{1D4C8}",ssetmn:"\u2216",ssmile:"\u2323",sstarf:"\u22C6",Star:"\u22C6",star:"\u2606",starf:"\u2605",straightepsilon:"\u03F5",straightphi:"\u03D5",strns:"\xAF",Sub:"\u22D0",sub:"\u2282",subdot:"\u2ABD",subE:"\u2AC5",sube:"\u2286",subedot:"\u2AC3",submult:"\u2AC1",subnE:"\u2ACB",subne:"\u228A",subplus:"\u2ABF",subrarr:"\u2979",Subset:"\u22D0",subset:"\u2282",subseteq:"\u2286",subseteqq:"\u2AC5",SubsetEqual:"\u2286",subsetneq:"\u228A",subsetneqq:"\u2ACB",subsim:"\u2AC7",subsub:"\u2AD5",subsup:"\u2AD3",succ:"\u227B",succapprox:"\u2AB8",succcurlyeq:"\u227D",Succeeds:"\u227B",SucceedsEqual:"\u2AB0",SucceedsSlantEqual:"\u227D",SucceedsTilde:"\u227F",succeq:"\u2AB0",succnapprox:"\u2ABA",succneqq:"\u2AB6",succnsim:"\u22E9",succsim:"\u227F",SuchThat:"\u220B",Sum:"\u2211",sum:"\u2211",sung:"\u266A",Sup:"\u22D1",sup:"\u2283",sup1:"\xB9",sup2:"\xB2",sup3:"\xB3",supdot:"\u2ABE",supdsub:"\u2AD8",supE:"\u2AC6",supe:"\u2287",supedot:"\u2AC4",Superset:"\u2283",SupersetEqual:"\u2287",suphsol:"\u27C9",suphsub:"\u2AD7",suplarr:"\u297B",supmult:"\u2AC2",supnE:"\u2ACC",supne:"\u228B",supplus:"\u2AC0",Supset:"\u22D1",supset:"\u2283",supseteq:"\u2287",supseteqq:"\u2AC6",supsetneq:"\u228B",supsetneqq:"\u2ACC",supsim:"\u2AC8",supsub:"\u2AD4",supsup:"\u2AD6",swarhk:"\u2926",swArr:"\u21D9",swarr:"\u2199",swarrow:"\u2199",swnwar:"\u292A",szlig:"\xDF",Tab:" ",target:"\u2316",Tau:"\u03A4",tau:"\u03C4",tbrk:"\u23B4",Tcaron:"\u0164",tcaron:"\u0165",Tcedil:"\u0162",tcedil:"\u0163",Tcy:"\u0422",tcy:"\u0442",tdot:"\u20DB",telrec:"\u2315",Tfr:"\u{1D517}",tfr:"\u{1D531}",there4:"\u2234",Therefore:"\u2234",therefore:"\u2234",Theta:"\u0398",theta:"\u03B8",thetasym:"\u03D1",thetav:"\u03D1",thickapprox:"\u2248",thicksim:"\u223C",ThickSpace:"\u205F\u200A",thinsp:"\u2009",ThinSpace:"\u2009",thkap:"\u2248",thksim:"\u223C",THORN:"\xDE",thorn:"\xFE",Tilde:"\u223C",tilde:"\u02DC",TildeEqual:"\u2243",TildeFullEqual:"\u2245",TildeTilde:"\u2248",times:"\xD7",timesb:"\u22A0",timesbar:"\u2A31",timesd:"\u2A30",tint:"\u222D",toea:"\u2928",top:"\u22A4",topbot:"\u2336",topcir:"\u2AF1",Topf:"\u{1D54B}",topf:"\u{1D565}",topfork:"\u2ADA",tosa:"\u2929",tprime:"\u2034",TRADE:"\u2122",trade:"\u2122",triangle:"\u25B5",triangledown:"\u25BF",triangleleft:"\u25C3",trianglelefteq:"\u22B4",triangleq:"\u225C",triangleright:"\u25B9",trianglerighteq:"\u22B5",tridot:"\u25EC",trie:"\u225C",triminus:"\u2A3A",TripleDot:"\u20DB",triplus:"\u2A39",trisb:"\u29CD",tritime:"\u2A3B",trpezium:"\u23E2",Tscr:"\u{1D4AF}",tscr:"\u{1D4C9}",TScy:"\u0426",tscy:"\u0446",TSHcy:"\u040B",tshcy:"\u045B",Tstrok:"\u0166",tstrok:"\u0167",twixt:"\u226C",twoheadleftarrow:"\u219E",twoheadrightarrow:"\u21A0",Uacute:"\xDA",uacute:"\xFA",Uarr:"\u219F",uArr:"\u21D1",uarr:"\u2191",Uarrocir:"\u2949",Ubrcy:"\u040E",ubrcy:"\u045E",Ubreve:"\u016C",ubreve:"\u016D",Ucirc:"\xDB",ucirc:"\xFB",Ucy:"\u0423",ucy:"\u0443",udarr:"\u21C5",Udblac:"\u0170",udblac:"\u0171",udhar:"\u296E",ufisht:"\u297E",Ufr:"\u{1D518}",ufr:"\u{1D532}",Ugrave:"\xD9",ugrave:"\xF9",uHar:"\u2963",uharl:"\u21BF",uharr:"\u21BE",uhblk:"\u2580",ulcorn:"\u231C",ulcorner:"\u231C",ulcrop:"\u230F",ultri:"\u25F8",Umacr:"\u016A",umacr:"\u016B",uml:"\xA8",UnderBar:"_",UnderBrace:"\u23DF",UnderBracket:"\u23B5",UnderParenthesis:"\u23DD",Union:"\u22C3",UnionPlus:"\u228E",Uogon:"\u0172",uogon:"\u0173",Uopf:"\u{1D54C}",uopf:"\u{1D566}",UpArrow:"\u2191",Uparrow:"\u21D1",uparrow:"\u2191",UpArrowBar:"\u2912",UpArrowDownArrow:"\u21C5",UpDownArrow:"\u2195",Updownarrow:"\u21D5",updownarrow:"\u2195",UpEquilibrium:"\u296E",upharpoonleft:"\u21BF",upharpoonright:"\u21BE",uplus:"\u228E",UpperLeftArrow:"\u2196",UpperRightArrow:"\u2197",Upsi:"\u03D2",upsi:"\u03C5",upsih:"\u03D2",Upsilon:"\u03A5",upsilon:"\u03C5",UpTee:"\u22A5",UpTeeArrow:"\u21A5",upuparrows:"\u21C8",urcorn:"\u231D",urcorner:"\u231D",urcrop:"\u230E",Uring:"\u016E",uring:"\u016F",urtri:"\u25F9",Uscr:"\u{1D4B0}",uscr:"\u{1D4CA}",utdot:"\u22F0",Utilde:"\u0168",utilde:"\u0169",utri:"\u25B5",utrif:"\u25B4",uuarr:"\u21C8",Uuml:"\xDC",uuml:"\xFC",uwangle:"\u29A7",vangrt:"\u299C",varepsilon:"\u03F5",varkappa:"\u03F0",varnothing:"\u2205",varphi:"\u03D5",varpi:"\u03D6",varpropto:"\u221D",vArr:"\u21D5",varr:"\u2195",varrho:"\u03F1",varsigma:"\u03C2",varsubsetneq:"\u228A\uFE00",varsubsetneqq:"\u2ACB\uFE00",varsupsetneq:"\u228B\uFE00",varsupsetneqq:"\u2ACC\uFE00",vartheta:"\u03D1",vartriangleleft:"\u22B2",vartriangleright:"\u22B3",Vbar:"\u2AEB",vBar:"\u2AE8",vBarv:"\u2AE9",Vcy:"\u0412",vcy:"\u0432",VDash:"\u22AB",Vdash:"\u22A9",vDash:"\u22A8",vdash:"\u22A2",Vdashl:"\u2AE6",Vee:"\u22C1",vee:"\u2228",veebar:"\u22BB",veeeq:"\u225A",vellip:"\u22EE",Verbar:"\u2016",verbar:"|",Vert:"\u2016",vert:"|",VerticalBar:"\u2223",VerticalLine:"|",VerticalSeparator:"\u2758",VerticalTilde:"\u2240",VeryThinSpace:"\u200A",Vfr:"\u{1D519}",vfr:"\u{1D533}",vltri:"\u22B2",vnsub:"\u2282\u20D2",vnsup:"\u2283\u20D2",Vopf:"\u{1D54D}",vopf:"\u{1D567}",vprop:"\u221D",vrtri:"\u22B3",Vscr:"\u{1D4B1}",vscr:"\u{1D4CB}",vsubnE:"\u2ACB\uFE00",vsubne:"\u228A\uFE00",vsupnE:"\u2ACC\uFE00",vsupne:"\u228B\uFE00",Vvdash:"\u22AA",vzigzag:"\u299A",Wcirc:"\u0174",wcirc:"\u0175",wedbar:"\u2A5F",Wedge:"\u22C0",wedge:"\u2227",wedgeq:"\u2259",weierp:"\u2118",Wfr:"\u{1D51A}",wfr:"\u{1D534}",Wopf:"\u{1D54E}",wopf:"\u{1D568}",wp:"\u2118",wr:"\u2240",wreath:"\u2240",Wscr:"\u{1D4B2}",wscr:"\u{1D4CC}",xcap:"\u22C2",xcirc:"\u25EF",xcup:"\u22C3",xdtri:"\u25BD",Xfr:"\u{1D51B}",xfr:"\u{1D535}",xhArr:"\u27FA",xharr:"\u27F7",Xi:"\u039E",xi:"\u03BE",xlArr:"\u27F8",xlarr:"\u27F5",xmap:"\u27FC",xnis:"\u22FB",xodot:"\u2A00",Xopf:"\u{1D54F}",xopf:"\u{1D569}",xoplus:"\u2A01",xotime:"\u2A02",xrArr:"\u27F9",xrarr:"\u27F6",Xscr:"\u{1D4B3}",xscr:"\u{1D4CD}",xsqcup:"\u2A06",xuplus:"\u2A04",xutri:"\u25B3",xvee:"\u22C1",xwedge:"\u22C0",Yacute:"\xDD",yacute:"\xFD",YAcy:"\u042F",yacy:"\u044F",Ycirc:"\u0176",ycirc:"\u0177",Ycy:"\u042B",ycy:"\u044B",yen:"\xA5",Yfr:"\u{1D51C}",yfr:"\u{1D536}",YIcy:"\u0407",yicy:"\u0457",Yopf:"\u{1D550}",yopf:"\u{1D56A}",Yscr:"\u{1D4B4}",yscr:"\u{1D4CE}",YUcy:"\u042E",yucy:"\u044E",Yuml:"\u0178",yuml:"\xFF",Zacute:"\u0179",zacute:"\u017A",Zcaron:"\u017D",zcaron:"\u017E",Zcy:"\u0417",zcy:"\u0437",Zdot:"\u017B",zdot:"\u017C",zeetrf:"\u2128",ZeroWidthSpace:"\u200B",Zeta:"\u0396",zeta:"\u03B6",Zfr:"\u2128",zfr:"\u{1D537}",ZHcy:"\u0416",zhcy:"\u0436",zigrarr:"\u21DD",Zopf:"\u2124",zopf:"\u{1D56B}",Zscr:"\u{1D4B5}",zscr:"\u{1D4CF}",zwj:"\u200D",zwnj:"\u200C"});Uf.entityMap=Uf.HTML_ENTITIES});var W5=oe(ky=>{"use strict";var ud=Fc(),Ge=eB(),J5=vf(),sW=ud.isHTMLEscapableRawTextElement,oW=ud.isHTMLMimeType,aW=ud.isHTMLRawTextElement,qf=ud.hasOwn,K5=ud.NAMESPACE,cW=J5.ParseError,lW=J5.DOMException,ad=0,Aa=1,cd=2,Rf=3,ld=4,dd=5,Nf=6,vy=7;function j5(){}j5.prototype={parse:function(r,e,t){var i=this.domBuilder;i.startDocument(),AW(e,e=Object.create(null)),dW(r,e,t,i,this.errorHandler),i.endDocument()}};var uB=/&#?\w+;?/g;function dW(r,e,t,i,n){var s=oW(i.mimeType);r.indexOf(Ge.UNICODE_REPLACEMENT_CHARACTER)>=0&&n.warning("Unicode replacement character detected, source encoding issues?");function o(K){if(K>65535){K-=65536;var $=55296+(K>>10),te=56320+(K&1023);return String.fromCharCode($,te)}else return String.fromCharCode(K)}function a(K){var $=K[K.length-1]===";"?K:K+";";if(!s&&$!==K)return n.error("EntityRef: expecting ;"),K;var te=Ge.Reference.exec($);if(!te||te[0].length!==$.length)return n.error("entity not matching Reference production: "+K),K;var re=$.slice(1,-1);return qf(t,re)?t[re]:re.charAt(0)==="#"?o(parseInt(re.substring(1).replace("x","0x"))):(n.error("entity not found:"+K),K)}function c(K){if(K>I){var $=r.substring(I,K).replace(uB,a);f&&h(I),i.characters($,0,K-I),I=K}}var l=0,d=0,u=/\r\n?|\n|$/g,f=i.locator;function h(K,$){for(;K>=d&&($=u.exec(r));)l=d,d=$.index+$[0].length,f.lineNumber++;f.columnNumber=K-l+1}for(var g=[{currentNSMap:e}],p=[],I=0;;){try{var m=r.indexOf("<",I);if(m<0){if(!s&&p.length>0)return n.fatalError("unclosed xml tag(s): "+p.join(", "));if(!r.substring(I).match(/^\s*$/)){var w=i.doc,B=w.createTextNode(r.substring(I));if(w.documentElement)return n.error("Extra content at the end of the document");w.appendChild(B),i.currentElement=B}return}if(m>I){var C=r.substring(I,m);!s&&p.length===0&&(C=C.replace(new RegExp(Ge.S_OPT.source,"g"),""),C&&n.error("Unexpected content outside root element: '"+C+"'")),c(m)}switch(r.charAt(m+1)){case"/":var N=r.indexOf(">",m+2),x=r.substring(m+2,N>0?N:void 0);if(!x)return n.fatalError("end tag name missing");var Q=Ge.reg("^",Ge.QName_group,Ge.S_OPT,"$"),R=N>0&&Q.exec(x);if(!R){var U=N>0&&Ge.reg("^",Ge.QName_group).exec(x);if(s&&U)n.warning('end tag name contains invalid trailing characters: "'+x+'"'),R=U;else if(U&&new RegExp(Q.source,Q.flags+"m").test(x))n.error('end tag name is followed by a line break and trailing content: "'+x+'"'),R=U;else return n.fatalError('end tag name contains invalid characters: "'+x+'"')}if(!i.currentElement&&!i.doc.documentElement)return;var T=p[p.length-1]||i.currentElement.tagName||i.doc.documentElement.tagName||"";if(T!==R[1]){var D=R[1].toLowerCase();if(!s||T.toLowerCase()!==D)return n.fatalError('Opening and ending tag mismatch: "'+T+'" != "'+x+'"')}var b=g.pop();p.pop();var _=b.localNSMap;if(i.endElement(b.uri,b.localName,T),_)for(var v in _)qf(_,v)&&i.endPrefixMapping(v);N++;break;case"?":f&&h(m),N=gW(r,m,i,n);break;case"!":f&&h(m),N=V5(r,m,i,n,s);break;default:f&&h(m);var E=new Y5,P=g[g.length-1].currentNSMap,N=uW(r,m,E,P,a,n,s),j=E.length;if(E.closed||(s&&ud.isHTMLVoidElement(E.tagName)?E.closed=!0:p.push(E.tagName)),f&&j){for(var z=G5(f,{}),L=0;L<j;L++){var G=E[L];h(G.offset),G.locator=G5(f,{})}i.locator=z,z5(E,i,P)&&g.push(E),i.locator=f}else z5(E,i,P)&&g.push(E);s&&!E.closed?N=fW(r,N,E.tagName,a,i):N++}}catch(K){if(K instanceof cW)throw K;if(K instanceof lW)return n.fatalError("Error constructing the DOM: "+K.name+": "+K.message,K);n.error("element parse error: "+K),N=-1}N>I?I=N:c(Math.max(m,I)+1)}}function G5(r,e){return e.lineNumber=r.lineNumber,e.columnNumber=r.columnNumber,e}function uW(r,e,t,i,n,s,o){function a(h,g,p){if(qf(t.attributeNames,h))return s.fatalError("Attribute "+h+" redefined");if(!o&&g.indexOf("<")>=0)return s.fatalError("Unescaped '<' not allowed in attributes values");t.addValue(h,g.replace(/[\t\n\r]/g," ").replace(uB,n),p)}for(var c,l,d=++e,u=ad;;){var f=r.charAt(d);if(u===ad&&f==="<")throw new Error("unexpected < in tag name: "+r.slice(e,d));switch(f){case"=":if(u===Aa)c=r.slice(e,d),u=Rf;else if(u===cd)u=Rf;else throw new Error("attribute equal must after attrName");break;case"'":case'"':if(u===Rf||u===Aa)if(u===Aa&&(s.warning('attribute value must after "="'),c=r.slice(e,d)),e=d+1,d=r.indexOf(f,e),d>0)l=r.slice(e,d),a(c,l,e-1),u=dd;else throw new Error("attribute value no end '"+f+"' match");else if(u==ld)l=r.slice(e,d),a(c,l,e),s.warning('attribute "'+c+'" missed start quot('+f+")!!"),e=d+1,u=dd;else throw new Error('attribute value must after "="');break;case"/":switch(u){case ad:t.setTagName(r.slice(e,d));case dd:case Nf:case vy:u=vy,t.closed=!0;case ld:case Aa:break;case cd:t.closed=!0;break;default:throw new Error("attribute invalid close char('/')")}break;case"":return s.error("unexpected end of input"),u==ad&&t.setTagName(r.slice(e,d)),d;case">":switch(u){case ad:t.setTagName(r.slice(e,d));case dd:case Nf:case vy:break;case ld:case Aa:l=r.slice(e,d),l.slice(-1)==="/"&&(t.closed=!0,l=l.slice(0,-1));case cd:u===cd&&(l=c),u==ld?(s.warning('attribute "'+l+'" missed quot(")!'),a(c,l,e)):(o||s.warning('attribute "'+l+'" missed value!! "'+l+'" instead!!'),a(l,l,e));break;case Rf:if(!o)return s.fatalError(`AttValue: ' or " expected`)}return d;case"\x80":f=" ";default:if(f<=" ")switch(u){case ad:t.setTagName(r.slice(e,d)),u=Nf;break;case Aa:c=r.slice(e,d),u=cd;break;case ld:var l=r.slice(e,d);s.warning('attribute "'+l+'" missed quot(")!!'),a(c,l,e);case dd:u=Nf;break}else switch(u){case cd:o||s.warning('attribute "'+c+'" missed value!! "'+c+'" instead2!!'),a(c,c,e),e=d,u=Aa;break;case dd:s.warning('attribute space is required"'+c+'"!!');case Nf:u=Aa,e=d;break;case Rf:u=ld,e=d;break;case vy:throw new Error("elements closed character '/' and '>' must be connected to")}}d++}}function z5(r,e,t){for(var i=r.tagName,n=null,u=r.length;u--;){var s=r[u],o=s.qName,a=s.value,f=o.indexOf(":");if(f>0)var c=s.prefix=o.slice(0,f),l=o.slice(f+1),d=c==="xmlns"&&l;else l=o,c=null,d=o==="xmlns"&&"";s.localName=l,d!==!1&&(n==null&&(n=Object.create(null),t=Object.create(t)),t[d]=n[d]=a,s.uri=K5.XMLNS,e.startPrefixMapping(d,a))}for(var u=r.length;u--;)s=r[u],s.prefix&&(s.prefix==="xml"&&(s.uri=K5.XML),s.prefix!=="xmlns"&&(s.uri=t[s.prefix]));var f=i.indexOf(":");f>0?(c=r.prefix=i.slice(0,f),l=r.localName=i.slice(f+1)):(c=null,l=r.localName=i);var h=r.uri=t[c||""];if(e.startElement(h,l,i,r),r.closed){if(e.endElement(h,l,i),n)for(c in n)qf(n,c)&&e.endPrefixMapping(c)}else return r.currentNSMap=t,r.localNSMap=n,!0}function fW(r,e,t,i,n){var s=sW(t);if(s||aW(t)){var o=new RegExp("</"+t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+">","ig");o.lastIndex=e;var a=o.exec(r),c=a?a.index:-1;if(c<0)return e+1;var l=r.substring(e+1,c);return s&&(l=l.replace(uB,i)),n.characters(l,0,l.length),c}return e+1}function AW(r,e){for(var t in r)qf(r,t)&&(e[t]=r[t])}function $5(r,e){var t=e;function i(d){return d=d||0,r.charAt(t+d)}function n(d){d=d||1,t+=d}function s(){for(var d=0;t<r.length;){var u=i();if(u!==" "&&u!==` -`&&u!==" "&&u!=="\r")return d;d++,n()}return-1}function o(){return r.substring(t)}function a(d){return r.substring(t,t+d.length)===d}function c(d){return r.substring(t,t+d.length).toUpperCase()===d.toUpperCase()}function l(d){var u=Ge.reg("^",d),f=u.exec(o());return f?(n(f[0].length),f[0]):null}return{char:i,getIndex:function(){return t},getMatch:l,getSource:function(){return r},skip:n,skipBlanks:s,substringFromIndex:o,substringStartsWith:a,substringStartsWithCaseInsensitive:c}}function hW(r,e){function t(a,c){var l=Ge.PI.exec(a.substringFromIndex());return l?l[1].toLowerCase()==="xml"?c.fatalError("xml declaration is only allowed at the start of the document, but found at position "+a.getIndex()):(a.skip(l[0].length),l[0]):c.fatalError("processing instruction is not well-formed at position "+a.getIndex())}var i=r.getSource();if(r.char()==="["){r.skip(1);for(var n=r.getIndex();r.getIndex()<i.length;){if(r.skipBlanks(),r.char()==="]"){var s=i.substring(n,r.getIndex());return r.skip(1),s}var o=null;if(r.char()==="<"&&r.char(1)==="!")switch(r.char(2)){case"E":r.char(3)==="L"?o=r.getMatch(Ge.elementdecl):r.char(3)==="N"&&(o=r.getMatch(Ge.EntityDecl));break;case"A":o=r.getMatch(Ge.AttlistDecl);break;case"N":o=r.getMatch(Ge.NotationDecl);break;case"-":o=r.getMatch(Ge.Comment);break}else if(r.char()==="<"&&r.char(1)==="?")o=t(r,e);else if(r.char()==="%")o=r.getMatch(Ge.PEReference);else return e.fatalError("Error detected in Markup declaration");if(!o)return e.fatalError("Error in internal subset at position "+r.getIndex())}return e.fatalError("doctype internal subset is not well-formed, missing ]")}}function V5(r,e,t,i,n){var s=$5(r,e);switch(n?s.char(2).toUpperCase():s.char(2)){case"-":var o=s.getMatch(Ge.Comment);return o?(t.comment(o,Ge.COMMENT_START.length,o.length-Ge.COMMENT_START.length-Ge.COMMENT_END.length),s.getIndex()):i.fatalError("comment is not well-formed at position "+s.getIndex());case"[":var a=s.getMatch(Ge.CDSect);return a?!n&&!t.currentElement?i.fatalError("CDATA outside of element"):(t.startCDATA(),t.characters(a,Ge.CDATA_START.length,a.length-Ge.CDATA_START.length-Ge.CDATA_END.length),t.endCDATA(),s.getIndex()):i.fatalError("Invalid CDATA starting at position "+e);case"D":{if(t.doc&&t.doc.documentElement)return i.fatalError("Doctype not allowed inside or after documentElement at position "+s.getIndex());if(n?!s.substringStartsWithCaseInsensitive(Ge.DOCTYPE_DECL_START):!s.substringStartsWith(Ge.DOCTYPE_DECL_START))return i.fatalError("Expected "+Ge.DOCTYPE_DECL_START+" at position "+s.getIndex());if(s.skip(Ge.DOCTYPE_DECL_START.length),s.skipBlanks()<1)return i.fatalError("Expected whitespace after "+Ge.DOCTYPE_DECL_START+" at position "+s.getIndex());var c={name:void 0,publicId:void 0,systemId:void 0,internalSubset:void 0};if(c.name=s.getMatch(Ge.Name),!c.name)return i.fatalError("doctype name missing or contains unexpected characters at position "+s.getIndex());if(n&&c.name.toLowerCase()!=="html"&&i.warning("Unexpected DOCTYPE in HTML document at position "+s.getIndex()),s.skipBlanks(),s.substringStartsWith(Ge.PUBLIC)||s.substringStartsWith(Ge.SYSTEM)){var l=Ge.ExternalID_match.exec(s.substringFromIndex());if(!l)return i.fatalError("doctype external id is not well-formed at position "+s.getIndex());l.groups.SystemLiteralOnly!==void 0?c.systemId=l.groups.SystemLiteralOnly:(c.systemId=l.groups.SystemLiteral,c.publicId=l.groups.PubidLiteral),s.skip(l[0].length)}else if(n&&s.substringStartsWithCaseInsensitive(Ge.SYSTEM)){if(s.skip(Ge.SYSTEM.length),s.skipBlanks()<1)return i.fatalError("Expected whitespace after "+Ge.SYSTEM+" at position "+s.getIndex());if(c.systemId=s.getMatch(Ge.ABOUT_LEGACY_COMPAT_SystemLiteral),!c.systemId)return i.fatalError("Expected "+Ge.ABOUT_LEGACY_COMPAT+" in single or double quotes after "+Ge.SYSTEM+" at position "+s.getIndex())}return n&&c.systemId&&!Ge.ABOUT_LEGACY_COMPAT_SystemLiteral.test(c.systemId)&&i.warning("Unexpected doctype.systemId in HTML document at position "+s.getIndex()),n||(s.skipBlanks(),c.internalSubset=hW(s,i)),s.skipBlanks(),s.char()!==">"?i.fatalError("doctype not terminated with > at position "+s.getIndex()):(s.skip(1),t.startDTD(c.name,c.publicId,c.systemId,c.internalSubset),t.endDTD(),s.getIndex())}default:return i.fatalError('Not well-formed XML starting with "<!" at position '+e)}}function gW(r,e,t,i){var n=r.substring(e).match(Ge.PI);if(!n)return i.fatalError("Invalid processing instruction starting at position "+e);if(n[1].toLowerCase()==="xml"){if(e>0)return i.fatalError("processing instruction at position "+e+" is an xml declaration which is only at the start of the document");if(!Ge.XMLDecl.test(r.substring(e)))return i.fatalError("xml declaration is not well-formed")}return t.processingInstruction(n[1],n[2]),e+n[0].length}function Y5(){this.attributeNames=Object.create(null)}Y5.prototype={setTagName:function(r){if(!Ge.QName_exact.test(r))throw new Error("invalid tagName:"+r);this.tagName=r},addValue:function(r,e,t){if(!Ge.QName_exact.test(r))throw new Error("invalid attribute:"+r);this.attributeNames[r]=this.length,this[this.length++]={qName:r,value:e,offset:t}},length:0,getLocalName:function(r){return this[r].localName},getLocator:function(r){return this[r].locator},getQName:function(r){return this[r].qName},getURI:function(r){return this[r].uri},getValue:function(r){return this[r].value}};ky.XMLReader=j5;ky.parseUtils=$5;ky.parseDoctypeCommentOrCData=V5});var n6=oe(Ad=>{"use strict";var Tc=Fc(),pW=dB(),mW=vf(),X5=H5(),yW=W5(),IW=pW.DOMImplementation,wW=Tc.hasDefaultHTMLNamespace,xW=Tc.isHTMLMimeType,CW=Tc.isValidMimeType,t6=Tc.MIME_TYPE,fB=Tc.NAMESPACE,Z5=mW.ParseError,EW=yW.XMLReader;function r6(r){return r.replace(/\r[\n\u0085]/g,` -`).replace(/[\r\u0085\u2028\u2029]/g,` -`)}function i6(r){if(r=r||{},r.locator===void 0&&(r.locator=!0),this.assign=r.assign||Tc.assign,this.domHandler=r.domHandler||Dy,this.onError=r.onError||r.errorHandler,r.errorHandler&&typeof r.errorHandler!="function")throw new TypeError("errorHandler object is no longer supported, switch to onError!");r.errorHandler&&r.errorHandler("warning","The `errorHandler` option has been deprecated, use `onError` instead!",this),this.normalizeLineEndings=r.normalizeLineEndings||r6,this.locator=!!r.locator,this.xmlns=this.assign(Object.create(null),r.xmlns)}i6.prototype.parseFromString=function(r,e){if(!CW(e))throw new TypeError('DOMParser.parseFromString: the provided mimeType "'+e+'" is not valid.');var t=this.assign(Object.create(null),this.xmlns),i=X5.XML_ENTITIES,n=t[""]||null;wW(e)?(i=X5.HTML_ENTITIES,n=fB.HTML):e===t6.XML_SVG_IMAGE&&(n=fB.SVG),t[""]=n,t.xml=t.xml||fB.XML;var s=new this.domHandler({mimeType:e,defaultNamespace:n,onError:this.onError}),o=this.locator?{}:void 0;this.locator&&s.setDocumentLocator(o);var a=new EW;a.errorHandler=s,a.domBuilder=s;var c=!Tc.isHTMLMimeType(e);return c&&typeof r!="string"&&a.errorHandler.fatalError("source is not a string"),a.parse(this.normalizeLineEndings(String(r)),t,i),s.doc.documentElement||a.errorHandler.fatalError("missing root element"),s.doc};function Dy(r){var e=r||{};this.mimeType=e.mimeType||t6.XML_APPLICATION,this.defaultNamespace=e.defaultNamespace||null,this.cdata=!1,this.currentElement=void 0,this.doc=void 0,this.locator=void 0,this.onError=e.onError}function fd(r,e){e.lineNumber=r.lineNumber,e.columnNumber=r.columnNumber}Dy.prototype={startDocument:function(){var r=new IW;this.doc=xW(this.mimeType)?r.createHTMLDocument(!1):r.createDocument(this.defaultNamespace,"")},startElement:function(r,e,t,i){var n=this.doc,s=n.createElementNS(r,t||e),o=i.length;Sy(this,s),this.currentElement=s,this.locator&&fd(this.locator,s);for(var a=0;a<o;a++){var r=i.getURI(a),c=i.getValue(a),t=i.getQName(a),l=n.createAttributeNS(r,t);this.locator&&fd(i.getLocator(a),l),l.value=l.nodeValue=c,s.setAttributeNode(l)}},endElement:function(r,e,t){this.currentElement=this.currentElement.parentNode},startPrefixMapping:function(r,e){},endPrefixMapping:function(r){},processingInstruction:function(r,e){var t=this.doc.createProcessingInstruction(r,e);this.locator&&fd(this.locator,t),Sy(this,t)},ignorableWhitespace:function(r,e,t){},characters:function(r,e,t){if(r=e6.apply(this,arguments),r){if(this.cdata)var i=this.doc.createCDATASection(r);else var i=this.doc.createTextNode(r);this.currentElement?this.currentElement.appendChild(i):/^\s*$/.test(r)&&this.doc.appendChild(i),this.locator&&fd(this.locator,i)}},skippedEntity:function(r){},endDocument:function(){this.doc.normalize()},setDocumentLocator:function(r){r&&(r.lineNumber=0),this.locator=r},comment:function(r,e,t){r=e6.apply(this,arguments);var i=this.doc.createComment(r);this.locator&&fd(this.locator,i),Sy(this,i)},startCDATA:function(){this.cdata=!0},endCDATA:function(){this.cdata=!1},startDTD:function(r,e,t,i){var n=this.doc.implementation;if(n&&n.createDocumentType){var s=n.createDocumentType(r,e,t,i);this.locator&&fd(this.locator,s),Sy(this,s),this.doc.doctype=s}},reportError:function(r,e){if(typeof this.onError=="function")try{this.onError(r,e,this)}catch(t){throw new Z5("Reporting "+r+' "'+e+'" caused '+t,this.locator)}else console.error("[xmldom "+r+"] "+e,BW(this.locator))},warning:function(r){this.reportError("warning",r)},error:function(r){this.reportError("error",r)},fatalError:function(r,e){throw this.reportError("fatalError",r),new Z5(r,this.locator,e)}};function BW(r){if(r)return` -@#[line:`+r.lineNumber+",col:"+r.columnNumber+"]"}function e6(r,e,t){return typeof r=="string"?r.substr(e,t):r.length>=e+t||e?new java.lang.String(r,e,t)+"":r}"endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g,function(r){Dy.prototype[r]=function(){return null}});function Sy(r,e){r.currentElement?r.currentElement.appendChild(e):r.doc.appendChild(e)}function bW(r){if(r==="error")throw"onErrorStopParsing"}function vW(){throw"onWarningStopParsing"}Ad.__DOMHandler=Dy;Ad.DOMParser=i6;Ad.normalizeLineEndings=r6;Ad.onErrorStopParsing=bW;Ad.onWarningStopParsing=vW});var s6=oe(st=>{"use strict";var hd=Fc();st.assign=hd.assign;st.hasDefaultHTMLNamespace=hd.hasDefaultHTMLNamespace;st.isHTMLMimeType=hd.isHTMLMimeType;st.isValidMimeType=hd.isValidMimeType;st.MIME_TYPE=hd.MIME_TYPE;st.NAMESPACE=hd.NAMESPACE;var Qy=vf();st.DOMException=Qy.DOMException;st.DOMExceptionName=Qy.DOMExceptionName;st.ExceptionCode=Qy.ExceptionCode;st.ParseError=Qy.ParseError;var gr=dB();st.Attr=gr.Attr;st.CDATASection=gr.CDATASection;st.CharacterData=gr.CharacterData;st.Comment=gr.Comment;st.Document=gr.Document;st.DocumentFragment=gr.DocumentFragment;st.DocumentType=gr.DocumentType;st.DOMImplementation=gr.DOMImplementation;st.Element=gr.Element;st.Entity=gr.Entity;st.EntityReference=gr.EntityReference;st.LiveNodeList=gr.LiveNodeList;st.NamedNodeMap=gr.NamedNodeMap;st.Node=gr.Node;st.NodeList=gr.NodeList;st.Notation=gr.Notation;st.ProcessingInstruction=gr.ProcessingInstruction;st.Text=gr.Text;st.XMLSerializer=gr.XMLSerializer;var _y=n6();st.DOMParser=_y.DOMParser;st.normalizeLineEndings=_y.normalizeLineEndings;st.onErrorStopParsing=_y.onErrorStopParsing;st.onWarningStopParsing=_y.onWarningStopParsing});function o6(r){if(typeof r=="string")return r.trim()||void 0;if(typeof r=="number"||typeof r=="boolean")return String(r);if(Array.isArray(r)&&r.length>0){let e=r[0];return(e?.nodeValue??e?.textContent??"").trim()||void 0}}function SW(r){let e=r;return e=e.replace(/<!DOCTYPE[^>]*>/gi,""),e=e.replace(/<!--[\s\S]*?-->/g,""),e=e.replace(new RegExp(`<(${kW})([^>]*?)/?>`,"gi"),"<$1$2/>"),e}function c6(r,e,t,i,n,s){let o=new a6.DOMParser({onError:()=>{}}).parseFromString(SW(r),"text/xml"),a=Of.select(e,o),c=[];for(let l of a)try{let d=Of.select(t,l);if(d.length===0)continue;let u=d[0],f="",h="";if(u.nodeType===1){let m=u;f=m.textContent?.trim()||"",h=m.getAttribute("href")||""}else if(u.nodeType===2){let m=u;f=m.value.trim(),h=m.value}else f=u.textContent?.trim()||"";let g=f.endsWith("/");if(g&&(f=f.slice(0,-1)),!f||s.includes(f))continue;let p=o6(Of.select(i,l)),I=o6(Of.select(n,l));c.push({name:f,url:h,isDir:g,size:p,modified:I})}catch{}return c}function QW(r){for(let e=r.length-1;e>=0;e--)if(r[e]>="0"&&r[e]<="9")return[r.slice(0,e+1).trim(),r.slice(e+1).trim()];return["",r]}function l6(r){if(!r||r.trim()==="-")return 0;let e=r.trim(),[t,i]=QW(e),n=i.toLowerCase(),s=DW[n]??1,o=parseFloat(t);return isNaN(o)?0:Math.round(o*s)}function d6(r,e){if(!r)return new Date().toISOString();try{let t=r.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);if(t){let[,n,s,o,a,c]=t;return new Date(`${n}-${s}-${o}T${a}:${c}:00`).toISOString()}let i=new Date(r);if(!isNaN(i.getTime()))return i.toISOString()}catch{}return new Date().toISOString()}var Of,a6,kW,DW,u6=J(()=>{"use strict";Of=xr(e5(),1),a6=xr(s6(),1);kW="area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr";DW={"":1,b:1,byte:1,bytes:1,k:1024,kb:1024,kib:1024,m:1<<20,mb:1<<20,mib:1<<20,g:1<<30,gb:1<<30,gib:1<<30,t:256,tb:256,tib:256,p:1<<50,pb:1<<50,pib:1<<50}});function _W(r){let e={...r||{}};return e.url=(e.url||"").trim(),e.url&&!/:\/\//.test(e.url)&&(e.url="https://"+e.url),e.url&&!e.url.endsWith("/")&&(e.url+="/"),e.item_xpath=e.item_xpath||f6,e.name_xpath=e.name_xpath||A6,e.size_xpath=e.size_xpath||h6,e.modified_xpath=e.modified_xpath||g6,e.modified_time_format=e.modified_time_format||"",e.ignore_file_names=e.ignore_file_names||"",e}var f6,A6,h6,g6,Fy,p6=J(()=>{"use strict";Be();He();u6();f6="//pre/a",A6="@href",h6="string(following-sibling::text()[1])",g6="string(following-sibling::text()[2])";Fy=class{addition;ignoreNames=[];constructor(e){this.addition=_W(e),this.addition.ignore_file_names&&(this.ignoreNames=this.addition.ignore_file_names.split(/\r?\n|,/).map(t=>t.trim()).filter(t=>t))}async init(){if(!this.addition.url)throw new Error("url is required")}cleanRel(e){return(e||"").split("/").filter(Boolean).join("/")}buildDirURL(e){let t=this.cleanRel(e);return this.addition.url+t+(t?"/":"")}nodeToFileItem(e,t){let i=new URL(e.url,t).toString(),n=e.isDir;return{name:e.name,size:e.size?l6(e.size):0,is_dir:n,modified:e.modified?d6(e.modified,this.addition.modified_time_format||""):new Date().toISOString(),sign:"",type:X(e.name,n),raw_url:n?"":i}}async list(e,t){let i=this.buildDirURL(t),n=await fetch(i);if(!n.ok)throw new Error(`Failed to fetch ${i}: HTTP ${n.status}`);let s=await n.text(),a=c6(s,this.addition.item_xpath||f6,this.addition.name_xpath||A6,this.addition.size_xpath||h6,this.addition.modified_xpath||g6,this.ignoreNames).filter(c=>c.name!==".."&&c.name!==".").map(c=>this.nodeToFileItem(c,i));return de(a,"name","asc")}async get(e,t){let i=this.cleanRel(t),n=i.split("/").filter(Boolean).pop()||"root";if(!i)return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.addition.url,type:1};let s=this.addition.url+i;return{name:n,size:0,is_dir:!1,modified:new Date().toISOString(),sign:s,type:X(n,!1),raw_url:s}}async mkdir(e,t){throw new Error("AutoIndex is read-only")}async rename(e,t,i){throw new Error("AutoIndex is read-only")}async remove(e,t,i){throw new Error("AutoIndex is read-only")}async move(e,t,i,n,s){throw new Error("AutoIndex is read-only")}async copy(e,t,i,n,s){throw new Error("AutoIndex is read-only")}async put(e,t,i){throw new Error("AutoIndex is read-only")}}});var ha,m6,y6,I6,w6,x6,C6,ixe,E6,nxe,B6,b6,v6,k6,sxe,S6,AB=J(()=>{"use strict";ha="https://drive.proton.me/api",m6="https://account.proton.me/api/auth/info",y6="https://account.proton.me/api/auth/v4",I6="https://account.proton.me/api/auth/v4/2fa",w6="https://account.proton.me/api/auth/v4/refresh",x6=ha+"/core/v4/keys/salts",C6=ha+"/core/v4/users",ixe=ha+"/core/v4/addresses",E6=ha+"/drive/shares",nxe=ha+"/drive/volumes",B6="application/vnd.protonmail.v1+json",b6="js@0.3.0",v6="ProtonDrive/v1.70.0 (Windows NT 10.0.22000; Win64; x64)",k6="web-drive@5.2.0+0f69f7a8",sxe=4*1024*1024,S6=1});var Q6=oe((D6,ga)=>{(function(r,e){typeof define=="function"&&define.amd?define([],e):typeof wt=="function"&&typeof ga=="object"&&ga&&ga.exports?ga.exports=e():(r.dcodeIO=r.dcodeIO||{}).bcrypt=e()})(D6,function(){"use strict";var r={},e=null;function t(D){if(typeof ga<"u"&&ga&&ga.exports)try{return wt("crypto").randomBytes(D)}catch{}try{var b;return(self.crypto||self.msCrypto).getRandomValues(b=new Uint32Array(D)),Array.prototype.slice.call(b)}catch{}if(!e)throw Error("Neither WebCryptoAPI nor a crypto module is available. Use bcrypt.setRandomFallback to set an alternative");return e(D)}var i=!1;try{t(1),i=!0}catch{}e=null,r.setRandomFallback=function(D){e=D},r.genSaltSync=function(D,b){if(D=D||g,typeof D!="number")throw Error("Illegal arguments: "+typeof D+", "+typeof b);D<4?D=4:D>31&&(D=31);var _=[];return _.push("$2a$"),D<10&&_.push("0"),_.push(D.toString()),_.push("$"),_.push(d(t(h),h)),_.join("")},r.genSalt=function(D,b,_){if(typeof b=="function"&&(_=b,b=void 0),typeof D=="function"&&(_=D,D=void 0),typeof D>"u")D=g;else if(typeof D!="number")throw Error("illegal arguments: "+typeof D);function v(E){s(function(){try{E(null,r.genSaltSync(D))}catch(P){E(P)}})}if(_){if(typeof _!="function")throw Error("Illegal callback: "+typeof _);v(_)}else return new Promise(function(E,P){v(function(N,j){if(N){P(N);return}E(j)})})},r.hashSync=function(D,b){if(typeof b>"u"&&(b=g),typeof b=="number"&&(b=r.genSaltSync(b)),typeof D!="string"||typeof b!="string")throw Error("Illegal arguments: "+typeof D+", "+typeof b);return T(D,b)},r.hash=function(D,b,_,v){function E(P){typeof D=="string"&&typeof b=="number"?r.genSalt(b,function(N,j){T(D,j,P,v)}):typeof D=="string"&&typeof b=="string"?T(D,b,P,v):s(P.bind(this,Error("Illegal arguments: "+typeof D+", "+typeof b)))}if(_){if(typeof _!="function")throw Error("Illegal callback: "+typeof _);E(_)}else return new Promise(function(P,N){E(function(j,z){if(j){N(j);return}P(z)})})};function n(D,b){for(var _=0,v=0,E=0,P=D.length;E<P;++E)D.charCodeAt(E)===b.charCodeAt(E)?++_:++v;return _<0?!1:v===0}r.compareSync=function(D,b){if(typeof D!="string"||typeof b!="string")throw Error("Illegal arguments: "+typeof D+", "+typeof b);return b.length!==60?!1:n(r.hashSync(D,b.substr(0,b.length-31)),b)},r.compare=function(D,b,_,v){function E(P){if(typeof D!="string"||typeof b!="string"){s(P.bind(this,Error("Illegal arguments: "+typeof D+", "+typeof b)));return}if(b.length!==60){s(P.bind(this,null,!1));return}r.hash(D,b.substr(0,29),function(N,j){N?P(N):P(null,n(j,b))},v)}if(_){if(typeof _!="function")throw Error("Illegal callback: "+typeof _);E(_)}else return new Promise(function(P,N){E(function(j,z){if(j){N(j);return}P(z)})})},r.getRounds=function(D){if(typeof D!="string")throw Error("Illegal arguments: "+typeof D);return parseInt(D.split("$")[2],10)},r.getSalt=function(D){if(typeof D!="string")throw Error("Illegal arguments: "+typeof D);if(D.length!==60)throw Error("Illegal hash length: "+D.length+" != 60");return D.substring(0,29)};var s=typeof process<"u"&&process&&typeof process.nextTick=="function"?typeof setImmediate=="function"?setImmediate:process.nextTick:setTimeout;function o(D){var b=[],_=0;return f.encodeUTF16toUTF8(function(){return _>=D.length?null:D.charCodeAt(_++)},function(v){b.push(v)}),b}var a="./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split(""),c=[-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,1,54,55,56,57,58,59,60,61,62,63,-1,-1,-1,-1,-1,-1,-1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,-1,-1,-1,-1,-1,-1,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,-1,-1,-1,-1,-1],l=String.fromCharCode;function d(D,b){var _=0,v=[],E,P;if(b<=0||b>D.length)throw Error("Illegal len: "+b);for(;_<b;){if(E=D[_++]&255,v.push(a[E>>2&63]),E=(E&3)<<4,_>=b){v.push(a[E&63]);break}if(P=D[_++]&255,E|=P>>4&15,v.push(a[E&63]),E=(P&15)<<2,_>=b){v.push(a[E&63]);break}P=D[_++]&255,E|=P>>6&3,v.push(a[E&63]),v.push(a[P&63])}return v.join("")}function u(D,b){var _=0,v=D.length,E=0,P=[],N,j,z,L,G,K;if(b<=0)throw Error("Illegal len: "+b);for(;_<v-1&&E<b&&(K=D.charCodeAt(_++),N=K<c.length?c[K]:-1,K=D.charCodeAt(_++),j=K<c.length?c[K]:-1,!(N==-1||j==-1||(G=N<<2>>>0,G|=(j&48)>>4,P.push(l(G)),++E>=b||_>=v)||(K=D.charCodeAt(_++),z=K<c.length?c[K]:-1,z==-1)||(G=(j&15)<<4>>>0,G|=(z&60)>>2,P.push(l(G)),++E>=b||_>=v)));)K=D.charCodeAt(_++),L=K<c.length?c[K]:-1,G=(z&3)<<6>>>0,G|=L,P.push(l(G)),++E;var $=[];for(_=0;_<E;_++)$.push(P[_].charCodeAt(0));return $}var f=(function(){"use strict";var D={};return D.MAX_CODEPOINT=1114111,D.encodeUTF8=function(b,_){var v=null;for(typeof b=="number"&&(v=b,b=function(){return null});v!==null||(v=b())!==null;)v<128?_(v&127):v<2048?(_(v>>6&31|192),_(v&63|128)):v<65536?(_(v>>12&15|224),_(v>>6&63|128),_(v&63|128)):(_(v>>18&7|240),_(v>>12&63|128),_(v>>6&63|128),_(v&63|128)),v=null},D.decodeUTF8=function(b,_){for(var v,E,P,N,j=function(z){z=z.slice(0,z.indexOf(null));var L=Error(z.toString());throw L.name="TruncatedError",L.bytes=z,L};(v=b())!==null;)if((v&128)===0)_(v);else if((v&224)===192)(E=b())===null&&j([v,E]),_((v&31)<<6|E&63);else if((v&240)===224)((E=b())===null||(P=b())===null)&&j([v,E,P]),_((v&15)<<12|(E&63)<<6|P&63);else if((v&248)===240)((E=b())===null||(P=b())===null||(N=b())===null)&&j([v,E,P,N]),_((v&7)<<18|(E&63)<<12|(P&63)<<6|N&63);else throw RangeError("Illegal starting byte: "+v)},D.UTF16toUTF8=function(b,_){for(var v,E=null;(v=E!==null?E:b())!==null;){if(v>=55296&&v<=57343&&(E=b())!==null&&E>=56320&&E<=57343){_((v-55296)*1024+E-56320+65536),E=null;continue}_(v)}E!==null&&_(E)},D.UTF8toUTF16=function(b,_){var v=null;for(typeof b=="number"&&(v=b,b=function(){return null});v!==null||(v=b())!==null;)v<=65535?_(v):(v-=65536,_((v>>10)+55296),_(v%1024+56320)),v=null},D.encodeUTF16toUTF8=function(b,_){D.UTF16toUTF8(b,function(v){D.encodeUTF8(v,_)})},D.decodeUTF8toUTF16=function(b,_){D.decodeUTF8(b,function(v){D.UTF8toUTF16(v,_)})},D.calculateCodePoint=function(b){return b<128?1:b<2048?2:b<65536?3:4},D.calculateUTF8=function(b){for(var _,v=0;(_=b())!==null;)v+=D.calculateCodePoint(_);return v},D.calculateUTF16asUTF8=function(b){var _=0,v=0;return D.UTF16toUTF8(b,function(E){++_,v+=D.calculateCodePoint(E)}),[_,v]},D})();Date.now=Date.now||function(){return+new Date};var h=16,g=10,p=16,I=100,m=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],w=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592,3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370,48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946,1266315497,3048417604,3681880366,3289982499,290971e4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509,1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880,613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303,2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385,1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030,4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055,3913112168,2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499,499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905,3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651,309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610,1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037,2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,3720792119,3617206836,2455994898,1729034894,1080033504,976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200,2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241,3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891,3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,2941484381,1077988104,1320477388,886195818,18198404,3786409e3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588,3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493,1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462],B=[1332899944,1700884034,1701343084,1684370003,1668446532,1869963892];function C(D,b,_,v){var E,P=D[b],N=D[b+1];return P^=_[0],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[1],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[2],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[3],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[4],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[5],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[6],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[7],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[8],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[9],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[10],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[11],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[12],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[13],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[14],E=v[P>>>24],E+=v[256|P>>16&255],E^=v[512|P>>8&255],E+=v[768|P&255],N^=E^_[15],E=v[N>>>24],E+=v[256|N>>16&255],E^=v[512|N>>8&255],E+=v[768|N&255],P^=E^_[16],D[b]=N^_[p+1],D[b+1]=P,D}function x(D,b){for(var _=0,v=0;_<4;++_)v=v<<8|D[b]&255,b=(b+1)%D.length;return{key:v,offp:b}}function Q(D,b,_){for(var v=0,E=[0,0],P=b.length,N=_.length,j,z=0;z<P;z++)j=x(D,v),v=j.offp,b[z]=b[z]^j.key;for(z=0;z<P;z+=2)E=C(E,0,b,_),b[z]=E[0],b[z+1]=E[1];for(z=0;z<N;z+=2)E=C(E,0,b,_),_[z]=E[0],_[z+1]=E[1]}function R(D,b,_,v){for(var E=0,P=[0,0],N=_.length,j=v.length,z,L=0;L<N;L++)z=x(b,E),E=z.offp,_[L]=_[L]^z.key;for(E=0,L=0;L<N;L+=2)z=x(D,E),E=z.offp,P[0]^=z.key,z=x(D,E),E=z.offp,P[1]^=z.key,P=C(P,0,_,v),_[L]=P[0],_[L+1]=P[1];for(L=0;L<j;L+=2)z=x(D,E),E=z.offp,P[0]^=z.key,z=x(D,E),E=z.offp,P[1]^=z.key,P=C(P,0,_,v),v[L]=P[0],v[L+1]=P[1]}function U(D,b,_,v,E){var P=B.slice(),N=P.length,j;if(_<4||_>31)if(j=Error("Illegal number of rounds (4-31): "+_),v){s(v.bind(this,j));return}else throw j;if(b.length!==h)if(j=Error("Illegal salt length: "+b.length+" != "+h),v){s(v.bind(this,j));return}else throw j;_=1<<_>>>0;var z,L,G=0,K;Int32Array?(z=new Int32Array(m),L=new Int32Array(w)):(z=m.slice(),L=w.slice()),R(b,D,z,L);function $(){if(E&&E(G/_),G<_)for(var re=Date.now();G<_&&(G=G+1,Q(D,z,L),Q(b,z,L),!(Date.now()-re>I)););else{for(G=0;G<64;G++)for(K=0;K<N>>1;K++)C(P,K<<1,z,L);var ee=[];for(G=0;G<N;G++)ee.push((P[G]>>24&255)>>>0),ee.push((P[G]>>16&255)>>>0),ee.push((P[G]>>8&255)>>>0),ee.push((P[G]&255)>>>0);if(v){v(null,ee);return}else return ee}v&&s($)}if(typeof v<"u")$();else for(var te;;)if(typeof(te=$())<"u")return te||[]}function T(D,b,_,v){var E;if(typeof D!="string"||typeof b!="string")if(E=Error("Invalid string / salt: Not a string"),_){s(_.bind(this,E));return}else throw E;var P,N;if(b.charAt(0)!=="$"||b.charAt(1)!=="2")if(E=Error("Invalid salt version: "+b.substring(0,2)),_){s(_.bind(this,E));return}else throw E;if(b.charAt(2)==="$")P="\0",N=3;else{if(P=b.charAt(2),P!=="a"&&P!=="b"&&P!=="y"||b.charAt(3)!=="$")if(E=Error("Invalid salt revision: "+b.substring(2,4)),_){s(_.bind(this,E));return}else throw E;N=4}if(b.charAt(N+2)>"$")if(E=Error("Missing salt rounds"),_){s(_.bind(this,E));return}else throw E;var j=parseInt(b.substring(N,N+1),10)*10,z=parseInt(b.substring(N+1,N+2),10),L=j+z,G=b.substring(N+3,N+25);D+=P>="a"?"\0":"";var K=o(D),$=u(G,h);function te(re){var ee=[];return ee.push("$2"),P>="a"&&ee.push(P),ee.push("$"),L<10&&ee.push("0"),ee.push(L.toString()),ee.push("$"),ee.push(d($,$.length)),ee.push(d(re,B.length*4-1)),ee.join("")}if(typeof _>"u")return te(U(K,$,L));U(K,$,L,function(re,ee){re?_(re,null):_(null,te(ee))},v)}return r.encodeBase64=d,r.decodeBase64=u,r})});var hB=oe((cxe,_6)=>{_6.exports=Q6()});function gd(r){let e="0x";for(let t of r)e+=t.toString(16).padStart(2,"0");return BigInt(e)}function Mf(r,e){let t=r.toString(16);t.length%2!==0&&(t="0"+t);let i=new Uint8Array(t.length/2);for(let n=0;n<i.length;n++)i[n]=parseInt(t.slice(n*2,n*2+2),16);if(e!==void 0&&i.length<e){let n=new Uint8Array(e);return n.set(i,e-i.length),n}return i}function FW(...r){let e=r.reduce((n,s)=>n+s.length,0),t=new Uint8Array(e),i=0;for(let n of r)t.set(n,i),i+=n.length;return t}function PW(r,e){let t=Math.max(r.length,e.length),i=new Uint8Array(t);for(let n=0;n<t;n++)i[n]=(r[n]||0)^(e[n]||0);return i}async function Uc(...r){let e=r.length===1?r[0]:FW(...r),t=await crypto.subtle.digest("SHA-256",e);return new Uint8Array(t)}function gB(r){let e=atob(r),t=new Uint8Array(e.length);for(let i=0;i<e.length;i++)t[i]=e.charCodeAt(i);return t}function F6(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e)}function TW(r){return r.length>1&&r[0]===0?r.slice(1):r}function pB(r,e,t){if(t===1n)return 0n;let i=1n,n=r%t,s=e;for(;s>0n;)s&1n&&(i=i*n%t),n=n*n%t,s>>=1n;return i}function UW(r){let e="",t=0,i=0;for(let n of r)for(i=i<<8|n,t+=8;t>=6;)t-=6,e+=mB[i>>t&63];return t>0&&(e+=mB[i<<6-t&63]),e}function RW(r,e,t){if(r<2)return new TextEncoder().encode(e);let n=`$2a$10$${UW(t).padEnd(22,".")}`,o=P6.default.hashSync(e,n).slice(n.length);return NW(o)}function NW(r){let e=[],t=0,i=0;for(let n of r){let s=mB.indexOf(n);s<0||(t=t<<6|s,i+=6,i>=8&&(i-=8,e.push(t>>i&255)))}return new Uint8Array(e)}async function T6(r,e,t){let i=t.Version??4,n=TW(gB(t.Modulus)),s=gd(n),o=gd(gB(t.ServerEphemeral)),a=gB(t.Salt);if(o%s===0n)throw new Error("invalid server ephemeral (B % N == 0)");let c=RW(i,e,a),l=await Uc(a,c),d=gd(l),u=crypto.getRandomValues(new Uint8Array(32)),f=gd(u),h=pB(Py,f,s),g=Mf(h,n.length),p=0n;if(i>=2){let v=Mf(Py,n.length),E=await Uc(n,v);p=gd(E)}let I=Mf(o,n.length),m=await Uc(g,I),w=gd(m),B=pB(Py,d,s),C=(o-p*B%s)%s;C<0n&&(C+=s);let x=f+w*d,Q=pB(C,x,s),R=Mf(Q,n.length),U=await Uc(n),T=await Uc(Mf(Py)),D=await Uc(new TextEncoder().encode(r)),b=PW(U,T),_=await Uc(b,D,a,g,I,R);return{ClientEphemeral:F6(g),ClientProof:F6(_),SRPSession:t.SRPSession}}var P6,Py,mB,U6=J(()=>{"use strict";P6=xr(hB(),1),Py=2n;mB="./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"});import{createRequire as tU}from"module";import*as Hi from"node:crypto";function qW(r,e){return e.forEach(function(t){t&&typeof t!="string"&&!Array.isArray(t)&&Object.keys(t).forEach(function(i){if(i!=="default"&&!(i in r)){var n=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(r,i,n.get?n:{enumerable:!0,get:function(){return t[i]}})}})}),Object.freeze(r)}function St(r){return r&&r.getReader&&Array.isArray(r)}function CA(r){if(!St(r)){let e=r.getWriter(),t=e.releaseLock;return e.releaseLock=()=>{e.closed.catch(function(){}),t.call(e)},e}this.stream=r}function or(r){if(St(r))return"array";if(oo.ReadableStream&&oo.ReadableStream.prototype.isPrototypeOf(r))return"web";if(r&&!(oo.ReadableStream&&r instanceof oo.ReadableStream)&&typeof r._read=="function"&&typeof r._readableState=="object")throw new Error("Native Node streams are no longer supported: please manually convert the stream to a WebStream, using e.g. `stream.Readable.toWeb`");return r&&r.getReader?"web-like":!1}function OI(r){return Uint8Array.prototype.isPrototypeOf(r)}function nU(r){if(r.length===1)return r[0];let e=0;for(let n=0;n<r.length;n++){if(!OI(r[n]))throw new Error("concatUint8Array: Data must be in the form of a Uint8Array");e+=r[n].length}let t=new Uint8Array(e),i=0;return r.forEach(function(n){t.set(n,i),i+=n.length}),t}function ms(r){if(this.stream=r,r[xt]&&(this[xt]=r[xt].slice()),St(r)){let i=r.getReader();this._read=i.read.bind(i),this._releaseLock=()=>{},this._cancel=()=>{};return}if(or(r)){let i=r.getReader();this._read=i.read.bind(i),this._releaseLock=()=>{i.closed.catch(function(){}),i.releaseLock()},this._cancel=i.cancel.bind(i);return}let t=!1;this._read=async()=>t||R6.has(r)?{value:void 0,done:!0}:(t=!0,{value:r,done:!1}),this._releaseLock=()=>{if(t)try{R6.add(r)}catch{}}}function EA(r){return or(r)?r:new ReadableStream({start(e){e.enqueue(r),e.close()}})}function sU(r){let e=or(r);if(e){if(e!=="array")throw new Error("Can't convert Stream to ArrayStream here, call `readToEnd` first");return r}let t=new an;return(async()=>{let i=Gi(t);await i.write(r),await i.close()})(),t}function ui(r){return r.some(e=>or(e)&&!St(e))?OW(r):r.some(e=>St(e))?MW(r):typeof r[0]=="string"?r.join(""):nU(r)}function OW(r){let e=r.map(EA),t=oU(async function(s){await Promise.all(n.map(o=>VB(o,s)))}),i=Promise.resolve(),n=e.map((s,o)=>Ua(s,(a,c)=>(i=i.then(()=>Kd(a,t.writable,{preventClose:o!==e.length-1})),i)));return t.readable}function MW(r){let e=new an,t=Promise.resolve();return r.forEach((i,n)=>(t=t.then(()=>Kd(i,e,{preventClose:n!==r.length-1})),t)),e}async function Kd(r,e,{preventClose:t=!1,preventAbort:i=!1,preventCancel:n=!1}={}){if(or(r)&&!St(r)&&!St(e)){r=EA(r);try{if(r[xt]){let a=Gi(e);for(let c=0;c<r[xt].length;c++)await a.ready,await a.write(r[xt][c]);a.releaseLock()}await r.pipeTo(e,{preventClose:t,preventAbort:i,preventCancel:n})}catch{}return}or(r)||(r=sU(r));let s=Fn(r),o=Gi(e);try{for(;;){await o.ready;let{done:a,value:c}=await s.read();if(a){t||await o.close();break}await o.write(c)}}catch(a){i||await o.abort(a)}finally{s.releaseLock(),o.releaseLock()}}function oU(r){let e=!1,t=!1,i,n,s;return{readable:new ReadableStream({start(o){s=o},pull(){i?i():e=!0},async cancel(o){t=!0,r&&await r(o),n&&n(o)}},{highWaterMark:0}),writable:new WritableStream({write:async function(o){if(t)throw new Error("Stream is cancelled");s.enqueue(o),e?e=!1:(await new Promise((a,c)=>{i=a,n=c}),i=null,n=null)},close:s.close.bind(s),abort:s.error.bind(s)})}}function sr(r,e=()=>{},t=()=>{},i={highWaterMark:0}){if(or(r))return aU(r,e,t,i);let n=e(r),s=t();return n!==void 0&&s!==void 0?ui([n,s]):n!==void 0?n:s}async function $B(r,e=async()=>{},t=async()=>{},i={highWaterMark:1}){if(or(r))return aU(r,e,t,i);let n=await e(r),s=await t();return n!==void 0&&s!==void 0?ui([n,s]):n!==void 0?n:s}function aU(r,e,t,i){if(St(r)){let n=new an;return(async()=>{let s=Gi(n);try{let o=await Xt(r),a=await e(o),c=await t(),l;a!==void 0&&c!==void 0?l=ui([a,c]):l=a!==void 0?a:c,await s.write(l),await s.close()}catch(o){await s.abort(o)}})(),n}if(or(r)){let n,s=!1;return new ReadableStream({start(){n=r.getReader()},async pull(o){if(s){o.close(),r.releaseLock();return}try{for(;;){let{value:a,done:c}=await n.read();s=c;let l=await(c?t:e)(a);if(l!==void 0){o.enqueue(l);return}if(c){o.close(),r.releaseLock();return}}}catch(a){o.error(a)}},async cancel(o){await n.cancel(o)}},i)}throw new Error("Unreachable")}function Ua(r,e){if(or(r)&&!St(r)){let i,n=new TransformStream({start(a){i=a}}),s=Kd(r,n.writable),o=oU(async function(a){i.error(a),await s,await new Promise(c=>setTimeout(c))});return e(n.readable,o.writable),o.readable}r=sU(r);let t=new an;return e(r,t),t}function MI(r,e){let t,i=Ua(r,(n,s)=>{let o=Fn(n);o.remainder=()=>(o.releaseLock(),Kd(n,s),i),t=e(o)});return t}function LW(r){if(St(r))throw new Error("ArrayStream cannot be tee()d, use clone() instead");if(or(r)){let e=EA(r).tee();return e[0][xt]=e[1][xt]=r[xt],e}return[Rr(r),Rr(r)]}function Jc(r){if(St(r))return r.clone();if(or(r)){let e=LW(r);return cU(r,e[0]),e[1]}return Rr(r)}function eA(r){return St(r)?Jc(r):or(r)?new ReadableStream({start(e){let t=Ua(r,async(i,n)=>{let s=Fn(i),o=Gi(n);try{for(;;){await o.ready;let{done:a,value:c}=await s.read();if(a){try{e.close()}catch{}await o.close();return}try{e.enqueue(c)}catch{}await o.write(c)}}catch(a){e.error(a),await o.abort(a)}});cU(r,t)}}):Rr(r)}function cU(r,e){Object.entries(Object.getOwnPropertyDescriptors(r.constructor.prototype)).forEach(([t,i])=>{t!=="constructor"&&(i.value?i.value=i.value.bind(e):i.get=i.get.bind(e),Object.defineProperty(r,t,i))})}function Rr(r,e=0,t=1/0){if(St(r))throw new Error("Not implemented");if(or(r)){if(e>=0&&t>=0){let i,n=0;return new ReadableStream({start(){i=r.getReader()},async pull(s){try{for(;;)if(n<t){let{value:o,done:a}=await i.read();if(a){s.close(),r.releaseLock();return}let c;if(n+o.length>=e&&(c=Rr(o,Math.max(e-n,0),t-n)),n+=o.length,c){s.enqueue(c);return}}else{s.close(),r.releaseLock();return}}catch(o){s.error(o)}},async cancel(s){await i.cancel(s)}},{highWaterMark:0})}if(e<0&&(t<0||t===1/0)){let i=[];return sr(r,n=>{n.length>=-e?i=[n]:i.push(n)},()=>Rr(ui(i),e,t))}if(e===0&&t<0){let i;return sr(r,n=>{let s=i?ui([i,n]):n;if(s.length>=-t)return i=Rr(s,t),Rr(s,e,t);i=s})}return console.warn(`stream.slice(input, ${e}, ${t}) not implemented efficiently.`),Zc(async()=>Rr(await Xt(r),e,t))}return r[xt]&&(r=ui(r[xt].concat([r]))),OI(r)?r.subarray(e,t===1/0?r.length:t):r.slice(e,t)}async function Xt(r,e=ui){return St(r)?r.readToEnd(e):or(r)?Fn(r).readToEnd(e):r}async function VB(r,e){if(or(r)){if(r.cancel){let t=await r.cancel(e);return await new Promise(i=>setTimeout(i)),t}if(r.destroy)return r.destroy(e),await new Promise(t=>setTimeout(t)),e}}function Zc(r){let e=new an;return(async()=>{let t=Gi(e);try{await t.write(await r()),await t.close()}catch(i){await t.abort(i)}})(),e}function Fn(r){return new ms(r)}function Gi(r){return new CA(r)}function io(r){let e=new Uint8Array;return sr(r,t=>{e=F.concatUint8Array([e,t]);let i=[],n=45,s=Math.floor(e.length/n),o=s*n,a=rI(e.subarray(0,o));for(let c=0;c<s;c++)i.push(a.substr(c*60,60)),i.push(` -`);return e=e.subarray(o),i.join("")},()=>e.length?rI(e)+` -`:"")}function lU(r){let e="";return sr(r,t=>{e+=t;let i=0,n=[" "," ","\r",` -`];for(let a=0;a<n.length;a++){let c=n[a];for(let l=e.indexOf(c);l!==-1;l=e.indexOf(c,l+1))i++}let s=e.length;for(;s>0&&(s-i)%4!==0;s--)n.includes(e[s])&&i--;let o=iI(e.substr(0,s));return e=e.substr(s),o},()=>iI(e))}function Ci(r){return lU(r.replace(/-/g,"+").replace(/_/g,"/"))}function Ur(r,e){let t=io(r).replace(/[\r\n]/g,"");return t=t.replace(/[+]/g,"-").replace(/[/]/g,"_").replace(/[=]/g,""),t}function HW(r){let e=/^-----BEGIN PGP (MESSAGE, PART \d+\/\d+|MESSAGE, PART \d+|SIGNED MESSAGE|MESSAGE|PUBLIC KEY BLOCK|PRIVATE KEY BLOCK|SIGNATURE)-----$/m,t=r.match(e);if(!t)throw new Error("Unknown ASCII armor type");if(/MESSAGE, PART \d+\/\d+/.test(t[1]))return y.armor.multipartSection;if(/MESSAGE, PART \d+/.test(t[1]))return y.armor.multipartLast;if(/SIGNED MESSAGE/.test(t[1]))return y.armor.signed;if(/MESSAGE/.test(t[1]))return y.armor.message;if(/PUBLIC KEY BLOCK/.test(t[1]))return y.armor.publicKey;if(/PRIVATE KEY BLOCK/.test(t[1]))return y.armor.privateKey;if(/SIGNATURE/.test(t[1]))return y.armor.signature}function Rc(r,e){let t="";return e.showVersion&&(t+="Version: "+e.versionString+` -`),e.showComment&&(t+="Comment: "+e.commentString+` -`),r&&(t+="Comment: "+r+` -`),t+=` -`,t}function Nc(r){let e=GW(r);return io(e)}function GW(r){let e=13501623;return sr(r,t=>{let i=KW?Math.floor(t.length/4):0,n=new Uint32Array(t.buffer,t.byteOffset,i);for(let s=0;s<i;s++)e^=n[s],e=kr[0][e>>24&255]^kr[1][e>>16&255]^kr[2][e>>8&255]^kr[3][e>>0&255];for(let s=i*4;s<t.length;s++)e=e>>8^kr[0][e&255^t[s]]},()=>new Uint8Array([e,e>>8,e>>16]))}function q6(r){for(let e=0;e<r.length;e++)/^([^\s:]|[^\s:][^:]*[^\s:]): .+$/.test(r[e])||F.printDebugError(new Error("Improperly formatted armor header: "+r[e])),/^(Version|Comment|MessageID|Hash|Charset): .+$/.test(r[e])||F.printDebugError(new Error("Unknown header: "+r[e]))}function zW(r){let e=r,t=r.lastIndexOf("=");return t>=0&&t!==r.length-1&&(e=r.slice(0,t)),e}function LI(r){return new Promise((e,t)=>{try{let i=/^-----[^-]+-----$/m,n=/^[ \f\r\t\u00a0\u2000-\u200a\u202f\u205f\u3000]*$/,s,o=[],a=o,c,l=[],d,u=lU(Ua(r,async(f,h)=>{let g=Fn(f);try{for(;;){let I=await g.readLine();if(I===void 0)throw new Error("Misformed armored text");if(I=F.removeTrailingSpaces(I.replace(/[\r\n]/g,"")),!s)i.test(I)&&(s=HW(I));else if(c)!d&&s===y.armor.signed&&(i.test(I)?(l=l.join(`\r -`),d=!0,q6(a),a=[],c=!1):l.push(I.replace(/^- /,"")));else if(i.test(I)&&t(new Error("Mandatory blank line missing between armor headers and armor data")),!n.test(I))a.push(I);else if(q6(a),c=!0,d||s!==y.armor.signed){e({text:l,data:u,headers:o,type:s});break}}}catch(I){t(I);return}let p=Gi(h);try{for(;;){await p.ready;let{done:I,value:m}=await g.read();if(I)throw new Error("Misformed armored text");let w=m+"";if(w.indexOf("=")===-1&&w.indexOf("-")===-1)await p.write(w);else{let B=await g.readToEnd();B.length||(B=""),B=w+B,B=F.removeTrailingSpaces(B.replace(/\r/g,""));let C=B.split(i);if(C.length===1)throw new Error("Misformed armored text");let x=zW(C[0].slice(0,-1));await p.write(x);break}}await p.ready,await p.close()}catch(I){await p.abort(I)}}))}catch(i){t(i)}}).then(async e=>(St(e.data)&&(e.data=await Xt(e.data)),e))}function BA(r,e,t,i,n,s=!1,o=pe){let a,c;r===y.armor.signed&&(a=e.text,c=e.hash,e=e.data);let l=s&&eA(e),d=[];switch(r){case y.armor.multipartSection:d.push("-----BEGIN PGP MESSAGE, PART "+t+"/"+i+`----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push("-----END PGP MESSAGE, PART "+t+"/"+i+`----- -`);break;case y.armor.multipartLast:d.push("-----BEGIN PGP MESSAGE, PART "+t+`----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push("-----END PGP MESSAGE, PART "+t+`----- -`);break;case y.armor.signed:d.push(`-----BEGIN PGP SIGNED MESSAGE----- -`),d.push(c?`Hash: ${c} - -`:` -`),d.push(a.replace(/^-/mg,"- -")),d.push(` ------BEGIN PGP SIGNATURE----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push(`-----END PGP SIGNATURE----- -`);break;case y.armor.message:d.push(`-----BEGIN PGP MESSAGE----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push(`-----END PGP MESSAGE----- -`);break;case y.armor.publicKey:d.push(`-----BEGIN PGP PUBLIC KEY BLOCK----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push(`-----END PGP PUBLIC KEY BLOCK----- -`);break;case y.armor.privateKey:d.push(`-----BEGIN PGP PRIVATE KEY BLOCK----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push(`-----END PGP PRIVATE KEY BLOCK----- -`);break;case y.armor.signature:d.push(`-----BEGIN PGP SIGNATURE----- -`),d.push(Rc(n,o)),d.push(io(e)),l&&d.push("=",Nc(l)),d.push(`-----END PGP SIGNATURE----- -`);break}return F.concat(d)}function Te(r){let e="0123456789ABCDEF",t="";return r.forEach(i=>{t+=e[i>>4]+e[i&15]}),BigInt("0x0"+t)}function tt(r,e){let t=r%e;return t<Bi?t+e:t}function JW(r,e,t){let i=-r;return e&i|t&~i}function Sr(r,e,t){if(t===Bi)throw Error("Modulo cannot be zero");if(t===kd)return BigInt(0);if(e<Bi)throw Error("Unsopported negative exponent");let i=e,n=r;n%=t;let s=BigInt(1);for(;i>Bi;){let o=i&kd;i>>=kd;let a=s*n%t;s=JW(o,a,s),n=n*n%t}return s}function O6(r){return r>=Bi?r:-r}function jW(r,e){let t=BigInt(0),i=BigInt(1),n=BigInt(1),s=BigInt(0),o=O6(r),a=O6(e),c=r<Bi,l=e<Bi;for(;a!==Bi;){let d=o/a,u=t;t=n-d*t,n=u,u=i,i=s-d*i,s=u,u=a,a=o%a,o=u}return{x:c?-n:n,y:l?-s:s,gcd:o}}function Td(r,e){let{gcd:t,x:i}=jW(r,e);if(t!==kd)throw new Error("Inverse does not exist");return tt(i+e,e)}function $W(r,e){let t=r,i=e;for(;i!==Bi;){let n=i;i=t%i,t=n}return t}function YB(r){let e=Number(r);if(e>Number.MAX_SAFE_INTEGER)throw new Error("Number can only safely store up to 53 bits");return e}function VW(r,e){return(r>>BigInt(e)&kd)===Bi?0:1}function Gd(r){let e=r<Bi?BigInt(-1):Bi,t=1,i=r;for(;(i>>=kd)!==e;)t++;return t}function bi(r){let e=r<Bi?BigInt(-1):Bi,t=BigInt(8),i=1,n=r;for(;(n>>=t)!==e;)i++;return i}function Wt(r,e="be",t){let i=r.toString(16);i.length%2===1&&(i="0"+i);let n=i.length/2,s=new Uint8Array(t||n),o=t?t-n:0,a=0;for(;a<n;)s[a+o]=parseInt(i.slice(2*a,2*a+2),16),a++;return e!=="be"&&s.reverse(),s}function Nr(r){let e=typeof crypto<"u"?crypto:YW?.webcrypto;if(e?.getRandomValues){let t=new Uint8Array(r);return e.getRandomValues(t)}else throw new Error("No secure random number generator available.")}function Ra(r,e){if(e<r)throw new Error("Illegal parameter value: max <= min");let t=e-r,i=bi(t),n=Te(Nr(i+8));return tt(n,t)+r}function M6(r,e,t){let i=BigInt(30),n=ds<<BigInt(r-1),s=[1,6,5,4,3,2,1,4,3,2,1,2,1,4,3,2,1,2,1,4,3,2,1,6,5,4,3,2,1,2],o=Ra(n,n<<ds),a=YB(tt(o,i));do o+=BigInt(s[a]),a=(a+s[a])%s.length,Gd(o)>r&&(o=tt(o,n<<ds),o+=n,a=YB(tt(o,i)));while(!dU(o,e,t));return o}function dU(r,e,t){return!(e&&$W(r-ds,e)!==ds||!XW(r)||!WW(r)||!eX(r,t))}function WW(r,e=BigInt(2)){return Sr(e,r-ds,r)===ds}function XW(r){let e=BigInt(0);return ZW.every(t=>tt(r,t)!==e)}function eX(r,e,t){let i=Gd(r);e||(e=Math.max(1,i/48|0));let n=r-ds,s=0;for(;!VW(n,s);)s++;let o=r>>BigInt(s);for(;e>0;e--){let a=Ra(BigInt(2),n),c=Sr(a,o,r);if(c===ds||c===n)continue;let l;for(l=1;l<s;l++){if(c=tt(c*c,r),c===ds)return!1;if(c===n)break}if(l===s)return!1}return!0}function Ao(r){if(!(!nI||!tX.includes(r)))return async function(e){let t=nI.createHash(r);return sr(e,i=>{t.update(i)},()=>new Uint8Array(t.digest()))}}function ho(r,e){let t=async()=>{let{nobleHashes:i}=await Promise.resolve().then(function(){return nie}),n=i.get(r);if(!n)throw new Error("Unsupported hash");return n};return async function(i){if(St(i)&&(i=await Xt(i)),F.isStream(i)){let s=(await t()).create();return sr(i,o=>{s.update(o)},()=>s.digest())}else return L6&&e?new Uint8Array(await L6.digest(e,i)):(await t())(i)}}function hs(r,e){switch(r){case y.hash.md5:return rX(e);case y.hash.sha1:return iX(e);case y.hash.ripemd:return cX(e);case y.hash.sha256:return sX(e);case y.hash.sha384:return oX(e);case y.hash.sha512:return aX(e);case y.hash.sha224:return nX(e);case y.hash.sha3_256:return lX(e);case y.hash.sha3_512:return dX(e);default:throw new Error("Unsupported hash function")}}function pr(r){switch(r){case y.hash.md5:return 16;case y.hash.sha1:case y.hash.ripemd:return 20;case y.hash.sha256:return 32;case y.hash.sha384:return 48;case y.hash.sha512:return 64;case y.hash.sha224:return 28;case y.hash.sha3_256:return 32;case y.hash.sha3_512:return 64;default:throw new Error("Invalid hash algorithm.")}}function uX(r){let e=new Uint8Array(r),t=0;for(;t<r;){let i=Nr(r-t);for(let n=0;n<i.length;n++)i[n]!==0&&(e[t++]=i[n])}return e}function uU(r,e){let t=r.length;if(t>e-11)throw new Error("Message too long");let i=uX(e-t-3),n=new Uint8Array(e);return n[1]=2,n.set(i,2),n.set(r,e-t),n}function fU(r,e){let t=2,i=1;for(let a=t;a<r.length;a++)i&=r[a]!==0,t+=i;let n=t-2,s=r.subarray(t+1),o=r[0]===0&r[1]===2&n>=8&!i;if(e)return F.selectUint8Array(o,s,e);if(o)return s;throw new Error("Decryption error")}function AU(r,e,t){let i;if(e.length!==pr(r))throw new Error("Invalid hash length");let n=new Uint8Array(sn[r].length);for(i=0;i<sn[r].length;i++)n[i]=sn[r][i];let s=n.length+e.length;if(t<s+11)throw new Error("Intended encoded message length too short");let o=new Uint8Array(t-s-3).fill(255),a=new Uint8Array(t);return a[1]=1,a.set(o,2),a.set(n,t-s),a.set(e,t-e.length),a}async function fX(r,e,t,i,n,s,o,a,c){if(pr(r)>=t.length)throw new Error("Digest size cannot exceed key modulus size");if(e&&!F.isStream(e)){if(F.getWebCrypto())try{return await IX(y.read(y.webHash,r),e,t,i,n,s,o,a)}catch(l){F.printDebugError(l)}else if(F.getNodeCrypto())return wX(r,e,t,i,n,s,o,a)}return yX(r,t,n,c)}async function AX(r,e,t,i,n,s){if(e&&!F.isStream(e)){if(F.getWebCrypto())try{return await CX(y.read(y.webHash,r),e,t,i,n)}catch(o){F.printDebugError(o)}else if(F.getNodeCrypto())return EX(r,e,t,i,n)}return xX(r,t,i,n,s)}async function hX(r,e,t){return F.getNodeCrypto()?BX(r,e,t):bX(r,e,t)}async function gX(r,e,t,i,n,s,o,a){if(F.getNodeCrypto()&&!a)try{return vX(r,e,t,i,n,s,o)}catch(c){F.printDebugError(c)}return kX(r,e,t,i,n,s,o,a)}async function pX(r,e){if(e=BigInt(e),F.getWebCrypto()){let o={name:"RSASSA-PKCS1-v1_5",modulusLength:r,publicExponent:Wt(e),hash:{name:"SHA-1"}},a=await Ud.generateKey(o,!0,["sign","verify"]),c=await Ud.exportKey("jwk",a.privateKey);return H6(c,e)}else if(F.getNodeCrypto()){let o={modulusLength:r,publicExponent:YB(e),publicKeyEncoding:{type:"pkcs1",format:"jwk"},privateKeyEncoding:{type:"pkcs1",format:"jwk"}},a=await new Promise((c,l)=>{jc.generateKeyPair("rsa",o,(d,u,f)=>{d?l(d):c(f)})});return H6(a,e)}let t,i,n;do i=M6(r-(r>>1),e,40),t=M6(r>>1,e,40),n=t*i;while(Gd(n)!==r);let s=(t-_a)*(i-_a);return i<t&&([t,i]=[i,t]),{n:Wt(n),e:Wt(e),d:Wt(Td(e,s)),p:Wt(t),q:Wt(i),u:Wt(Td(t,i))}}async function mX(r,e,t,i,n,s){if(r=Te(r),i=Te(i),n=Te(n),i*n!==r)return!1;let o=BigInt(2);if(s=Te(s),tt(i*s,n)!==BigInt(1))return!1;e=Te(e),t=Te(t);let a=BigInt(Math.floor(Gd(r)/3)),c=Ra(o,o<<a),l=c*t*e;return tt(l,i-_a)===c&&tt(l,n-_a)===c}function yX(r,e,t,i){e=Te(e);let n=Te(AU(r,i,bi(e)));return t=Te(t),Wt(Sr(n,t,e),"be",bi(e))}async function IX(r,e,t,i,n,s,o,a){let c=P1(t,i,n,s,o,a),l={name:"RSASSA-PKCS1-v1_5",hash:{name:r}},d=await Ud.importKey("jwk",c,l,!1,["sign"]);return new Uint8Array(await Ud.sign("RSASSA-PKCS1-v1_5",d,e))}function wX(r,e,t,i,n,s,o,a){let c=jc.createSign(y.read(y.hash,r));c.write(e),c.end();let l=P1(t,i,n,s,o,a);return new Uint8Array(c.sign({key:l,format:"jwk",type:"pkcs1"}))}function xX(r,e,t,i,n){if(t=Te(t),e=Te(e),i=Te(i),e>=t)throw new Error("Signature size cannot exceed modulus size");let s=Wt(Sr(e,i,t),"be",bi(t)),o=AU(r,n,bi(t));return F.equalsUint8Array(s,o)}async function CX(r,e,t,i,n){let s=T1(i,n),o=await Ud.importKey("jwk",s,{name:"RSASSA-PKCS1-v1_5",hash:{name:r}},!1,["verify"]);return Ud.verify("RSASSA-PKCS1-v1_5",o,t,e)}function EX(r,e,t,i,n){let o={key:T1(i,n),format:"jwk",type:"pkcs1"},a=jc.createVerify(y.read(y.hash,r));a.write(e),a.end();try{return a.verify(o,t)}catch{return!1}}function BX(r,e,t){let n={key:T1(e,t),format:"jwk",type:"pkcs1",padding:jc.constants.RSA_PKCS1_PADDING};return new Uint8Array(jc.publicEncrypt(n,r))}function bX(r,e,t){if(e=Te(e),r=Te(uU(r,bi(e))),t=Te(t),r>=e)throw new Error("Message size cannot exceed modulus size");return Wt(Sr(r,t,e),"be",bi(e))}function vX(r,e,t,i,n,s,o){let c={key:P1(e,t,i,n,s,o),format:"jwk",type:"pkcs1",padding:jc.constants.RSA_PKCS1_PADDING};try{return new Uint8Array(jc.privateDecrypt(c,r))}catch{throw new Error("Decryption error")}}function kX(r,e,t,i,n,s,o,a){if(r=Te(r),e=Te(e),t=Te(t),i=Te(i),n=Te(n),s=Te(s),o=Te(o),r>=e)throw new Error("Data too large.");let c=tt(i,s-_a),l=tt(i,n-_a),d=Ra(BigInt(2),e),u=Sr(Td(d,e),t,e);r=tt(r*u,e);let f=Sr(r,l,n),h=Sr(r,c,s),p=tt(o*(h-f),s)*n+f;return p=tt(p*d,e),fU(Wt(p,"be",bi(e)),a)}function P1(r,e,t,i,n,s){let o=Te(i),a=Te(n),c=Te(t),l=tt(c,a-_a),d=tt(c,o-_a);return d=Wt(d),l=Wt(l),{kty:"RSA",n:Ur(r),e:Ur(e),d:Ur(t),p:Ur(n),q:Ur(i),dp:Ur(l),dq:Ur(d),qi:Ur(s),ext:!0}}function T1(r,e){return{kty:"RSA",n:Ur(r),e:Ur(e),ext:!0}}function H6(r,e){return{n:Ci(r.n),e:Wt(e),d:Ci(r.d),p:Ci(r.q),q:Ci(r.p),u:Ci(r.qi)}}async function SX(r,e,t,i){e=Te(e),t=Te(t),i=Te(i);let n=uU(r,bi(e)),s=Te(n),o=Ra(va,e-va);return{c1:Wt(Sr(t,o,e)),c2:Wt(tt(Sr(i,o,e)*s,e))}}async function DX(r,e,t,i,n){r=Te(r),e=Te(e),t=Te(t),i=Te(i);let s=tt(Td(Sr(r,i,t),t)*e,t);return fU(Wt(s,"be",bi(t)),n)}async function QX(r,e,t,i){let n=Te(r),s=Te(e),o=Te(t);if(s<=va||s>=n)return!1;let a=BigInt(Gd(n)),c=BigInt(1023);if(a<c||Sr(s,n-va,n)!==va)return!1;let l=s,d=BigInt(1),u=BigInt(2),f=u<<BigInt(17);for(;d<f;){if(l=tt(l*s,n),l===va)return!1;d++}let h=Te(i),g=Ra(u<<a-va,u<<a),p=(n-va)*g+h;return o===Sr(s,p,n)}function hU(r){let e=0,t,i=r[0];return i<192?([e]=r,t=1):i<255?(e=(r[0]-192<<8)+r[1]+192,t=2):i===255&&(e=F.readNumber(r.subarray(1,5)),t=5),{len:e,offset:t}}function HI(r){return r<192?new Uint8Array([r]):r>191&&r<8384?new Uint8Array([(r-192>>8)+192,r-192&255]):F.concatUint8Array([new Uint8Array([255]),F.writeNumber(r,4)])}function FX(r){if(r<0||r>30)throw new Error("Partial Length power must be between 1 and 30");return new Uint8Array([224+r])}function sI(r){return new Uint8Array([192|r])}function K6(r,e){return F.concatUint8Array([sI(r),HI(e)])}function $y(r){return[y.packet.literalData,y.packet.compressedData,y.packet.symmetricallyEncryptedData,y.packet.symEncryptedIntegrityProtectedData,y.packet.aeadEncryptedData].includes(r)}async function PX(r,e,t){let i,n;try{let s=await r.peekBytes(2);if(!s||s.length<2||(s[0]&128)===0)throw new Error("Error during parsing. This message / key probably does not conform to a valid OpenPGP format.");let o=await r.readByte(),a=-1,c=-1,l;c=0,(o&64)!==0&&(c=1);let d;c?a=o&63:(a=(o&63)>>2,d=o&3);let u=$y(a),f=null;if(e&&u){if(e==="array"){let g=new an;i=Gi(g),f=g}else{let g=new TransformStream;i=Gi(g.writable),f=g.readable}n=t({tag:a,packet:f})}else f=[];let h;do{if(c){let g=await r.readByte();if(h=!1,g<192)l=g;else if(g>=192&&g<224)l=(g-192<<8)+await r.readByte()+192;else if(g>223&&g<255){if(l=1<<(g&31),h=!0,!u)throw new TypeError("This packet type does not support partial lengths.")}else l=await r.readByte()<<24|await r.readByte()<<16|await r.readByte()<<8|await r.readByte()}else switch(d){case 0:l=await r.readByte();break;case 1:l=await r.readByte()<<8|await r.readByte();break;case 2:l=await r.readByte()<<24|await r.readByte()<<16|await r.readByte()<<8|await r.readByte();break;default:l=1/0;break}if(l>0){let g=0;for(;;){i&&await i.ready;let{done:p,value:I}=await r.read();if(p){if(l===1/0)break;throw new Error("Unexpected end of packet")}let m=l===1/0?I:I.subarray(0,l-g);if(i?await i.write(m):f.push(m),g+=I.length,g>=l){r.unshift(I.subarray(l-g+I.length));break}}}}while(h);i?(await i.ready,await i.close()):(f=F.concatUint8Array(f),await t({tag:a,packet:f}))}catch(s){if(i)return await i.abort(s),!0;throw s}finally{i&&await n}}async function U1(r){switch(r){case y.publicKey.ed25519:try{let e=F.getWebCrypto(),t=await e.generateKey("Ed25519",!0,["sign","verify"]).catch(s=>{if(s.name==="OperationError"){let o=new Error("Unexpected key generation issue");throw o.name="NotSupportedError",o}throw s}),i=await e.exportKey("jwk",t.privateKey),n=await e.exportKey("jwk",t.publicKey);return{A:new Uint8Array(Ci(n.x)),seed:Ci(i.d,!0)}}catch(e){if(e.name!=="NotSupportedError")throw e;let{default:t}=await Promise.resolve().then(function(){return il}),i=Nr(KI(r)),{publicKey:n}=t.sign.keyPair.fromSeed(i);return{A:n,seed:i}}case y.publicKey.ed448:{let e=await F.getNobleCurve(y.publicKey.ed448),{secretKey:t,publicKey:i}=e.keygen();return{A:i,seed:t}}default:throw new Error("Unsupported EdDSA algorithm")}}async function R1(r,e,t,i,n,s){if(pr(e)<pr(GI(r)))throw new Error("Hash algorithm too weak for EdDSA.");switch(r){case y.publicKey.ed25519:try{let o=F.getWebCrypto(),a=gU(r,i,n),c=await o.importKey("jwk",a,"Ed25519",!1,["sign"]);return{RS:new Uint8Array(await o.sign("Ed25519",c,s))}}catch(o){if(o.name!=="NotSupportedError")throw o;let{default:a}=await Promise.resolve().then(function(){return il}),c=F.concatUint8Array([n,i]);return{RS:a.sign.detached(s,c)}}case y.publicKey.ed448:return{RS:(await F.getNobleCurve(y.publicKey.ed448)).sign(s,n)};default:throw new Error("Unsupported EdDSA algorithm")}}async function N1(r,e,{RS:t},i,n,s){if(pr(e)<pr(GI(r)))throw new Error("Hash algorithm too weak for EdDSA.");switch(r){case y.publicKey.ed25519:try{let o=F.getWebCrypto(),a=O1(r,n),c=await o.importKey("jwk",a,"Ed25519",!1,["verify"]);return await o.verify("Ed25519",c,t,s)}catch(o){if(o.name!=="NotSupportedError")throw o;let{default:a}=await Promise.resolve().then(function(){return il});return a.sign.detached.verify(s,t,n)}case y.publicKey.ed448:return(await F.getNobleCurve(y.publicKey.ed448)).verify(t,s,n);default:throw new Error("Unsupported EdDSA algorithm")}}async function q1(r,e,t){switch(r){case y.publicKey.ed25519:try{let i=F.getWebCrypto(),n=gU(r,e,t),s=O1(r,e),o=await i.importKey("jwk",n,"Ed25519",!1,["sign"]),a=await i.importKey("jwk",s,"Ed25519",!1,["verify"]),c=Nr(8),l=new Uint8Array(await i.sign("Ed25519",o,c));return await i.verify("Ed25519",a,l,c)}catch(i){if(i.name!=="NotSupportedError")return!1;let{default:n}=await Promise.resolve().then(function(){return il}),{publicKey:s}=n.sign.keyPair.fromSeed(t);return F.equalsUint8Array(e,s)}case y.publicKey.ed448:{let n=(await F.getNobleCurve(y.publicKey.ed448)).getPublicKey(t);return F.equalsUint8Array(e,n)}default:return!1}}function KI(r){switch(r){case y.publicKey.ed25519:return 32;case y.publicKey.ed448:return 57;default:throw new Error("Unsupported EdDSA algorithm")}}function GI(r){switch(r){case y.publicKey.ed25519:return y.hash.sha256;case y.publicKey.ed448:return y.hash.sha512;default:throw new Error("Unknown EdDSA algo")}}function pU(r){return r instanceof Uint8Array||ArrayBuffer.isView(r)&&r.constructor.name==="Uint8Array"}function kt(r,...e){if(!pU(r))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(r.length))throw new Error("Uint8Array expected of length "+e+", got length="+r.length)}function oI(r,e=!0){if(r.destroyed)throw new Error("Hash instance has been destroyed");if(e&&r.finished)throw new Error("Hash#digest() has already been called")}function mU(r,e){kt(r);let t=e.outputLen;if(r.length<t)throw new Error("digestInto() expects output buffer of length at least "+t)}function M1(r){return new Uint8Array(r.buffer,r.byteOffset,r.byteLength)}function Et(r){return new Uint32Array(r.buffer,r.byteOffset,Math.floor(r.byteLength/4))}function ei(...r){for(let e=0;e<r.length;e++)r[e].fill(0)}function zI(r){return new DataView(r.buffer,r.byteOffset,r.byteLength)}function RX(r){if(typeof r!="string")throw new Error("string expected");return new Uint8Array(new TextEncoder().encode(r))}function sA(r){if(typeof r=="string")r=RX(r);else if(pU(r))r=vi(r);else throw new Error("Uint8Array expected, got "+typeof r);return r}function yU(r,e){return r.buffer===e.buffer&&r.byteOffset<e.byteOffset+e.byteLength&&e.byteOffset<r.byteOffset+r.byteLength}function L1(r,e){if(yU(r,e)&&r.byteOffset<e.byteOffset)throw new Error("complex overlap of input and output is not supported")}function NX(...r){let e=0;for(let i=0;i<r.length;i++){let n=r[i];kt(n),e+=n.length}let t=new Uint8Array(e);for(let i=0,n=0;i<r.length;i++){let s=r[i];t.set(s,n),n+=s.length}return t}function IU(r,e){if(r.length!==e.length)return!1;let t=0;for(let i=0;i<r.length;i++)t|=r[i]^e[i];return t===0}function vA(r,e,t=!0){if(e===void 0)return new Uint8Array(r);if(e.length!==r)throw new Error("invalid output length, expected "+r+", got: "+e.length);if(t&&!on(e))throw new Error("invalid output, must be aligned");return e}function WB(r,e,t,i){if(typeof r.setBigUint64=="function")return r.setBigUint64(e,t,i);let n=BigInt(32),s=BigInt(4294967295),o=Number(t>>n&s),a=Number(t&s),c=0,l=4;r.setUint32(e+c,o,i),r.setUint32(e+l,a,i)}function qX(r,e,t){let i=new Uint8Array(16),n=zI(i);return WB(n,0,BigInt(e),t),WB(n,8,BigInt(r),t),i}function on(r){return r.byteOffset%4===0}function vi(r){return Uint8Array.from(r)}function LX(r){r.reverse();let e=r[15]&1,t=0;for(let i=0;i<r.length;i++){let n=r[i];r[i]=n>>>1|t,t=(n&1)<<7}return r[0]^=-e&225,r}function wU(r){let e=(i,n)=>r(n,i.length).update(sA(i)).digest(),t=r(new Uint8Array(16),0);return e.outputLen=t.outputLen,e.blockLen=t.blockLen,e.create=(i,n)=>r(i,n),e}function G1(r){return r<<1^KX&-(r>>7)}function Ed(r,e){let t=0;for(;e>0;e>>=1)t^=r&-(e&1),r=G1(r);return t}function xU(r,e){if(r.length!==256)throw new Error("Wrong sbox length");let t=new Uint32Array(256).map((l,d)=>e(r[d])),i=t.map(IB),n=i.map(IB),s=n.map(IB),o=new Uint32Array(256*256),a=new Uint32Array(256*256),c=new Uint16Array(256*256);for(let l=0;l<256;l++)for(let d=0;d<256;d++){let u=l*256+d;o[u]=t[l]^i[d],a[u]=n[l]^s[d],c[u]=r[l]<<8|r[d]}return{sbox:r,sbox2:c,T0:t,T1:i,T2:n,T3:s,T01:o,T23:a}}function el(r){kt(r);let e=r.length;if(![16,24,32].includes(e))throw new Error("aes: invalid key size, should be 16, 24 or 32, got "+e);let{sbox2:t}=z1,i=[];on(r)||i.push(r=vi(r));let n=Et(r),s=n.length,o=c=>cs(t,c,c,c,c),a=new Uint32Array(e+28);a.set(n);for(let c=s;c<a.length;c++){let l=a[c-1];c%s===0?l=o(zX(l))^JX[c/s-1]:s>6&&c%s===4&&(l=o(l)),a[c]=a[c-s]^l}return ei(...i),a}function J1(r){let e=el(r),t=e.slice(),i=e.length,{sbox2:n}=z1,{T0:s,T1:o,T2:a,T3:c}=CU;for(let l=0;l<i;l+=4)for(let d=0;d<4;d++)t[l+d]=e[i-l-4+d];ei(e);for(let l=4;l<i-4;l++){let d=t[l],u=cs(n,d,d,d,d);t[l]=s[u&255]^o[u>>>8&255]^a[u>>>16&255]^c[u>>>24]}return t}function Sa(r,e,t,i,n,s){return r[t<<8&65280|i>>>8&255]^e[n>>>8&65280|s>>>24&255]}function cs(r,e,t,i,n){return r[e&255|t&65280]|r[i>>>16&255|n>>>16&65280]<<16}function Pn(r,e,t,i,n){let{sbox2:s,T01:o,T23:a}=z1,c=0;e^=r[c++],t^=r[c++],i^=r[c++],n^=r[c++];let l=r.length/4-2;for(let g=0;g<l;g++){let p=r[c++]^Sa(o,a,e,t,i,n),I=r[c++]^Sa(o,a,t,i,n,e),m=r[c++]^Sa(o,a,i,n,e,t),w=r[c++]^Sa(o,a,n,e,t,i);e=p,t=I,i=m,n=w}let d=r[c++]^cs(s,e,t,i,n),u=r[c++]^cs(s,t,i,n,e),f=r[c++]^cs(s,i,n,e,t),h=r[c++]^cs(s,n,e,t,i);return{s0:d,s1:u,s2:f,s3:h}}function JI(r,e,t,i,n){let{sbox2:s,T01:o,T23:a}=CU,c=0;e^=r[c++],t^=r[c++],i^=r[c++],n^=r[c++];let l=r.length/4-2;for(let g=0;g<l;g++){let p=r[c++]^Sa(o,a,e,n,i,t),I=r[c++]^Sa(o,a,t,e,n,i),m=r[c++]^Sa(o,a,i,t,e,n),w=r[c++]^Sa(o,a,n,i,t,e);e=p,t=I,i=m,n=w}let d=r[c++]^cs(s,e,n,i,t),u=r[c++]^cs(s,t,e,n,i),f=r[c++]^cs(s,i,t,e,n),h=r[c++]^cs(s,n,i,t,e);return{s0:d,s1:u,s2:f,s3:h}}function EU(r,e,t,i){kt(e,ki),kt(t);let n=t.length;i=vA(n,i),L1(t,i);let s=e,o=Et(s),{s0:a,s1:c,s2:l,s3:d}=Pn(r,o[0],o[1],o[2],o[3]),u=Et(t),f=Et(i);for(let g=0;g+4<=u.length;g+=4){f[g+0]=u[g+0]^a,f[g+1]=u[g+1]^c,f[g+2]=u[g+2]^l,f[g+3]=u[g+3]^d;let p=1;for(let I=s.length-1;I>=0;I--)p=p+(s[I]&255)|0,s[I]=p&255,p>>>=8;({s0:a,s1:c,s2:l,s3:d}=Pn(r,o[0],o[1],o[2],o[3]))}let h=ki*Math.floor(u.length/K1);if(h<n){let g=new Uint32Array([a,c,l,d]),p=M1(g);for(let I=h,m=0;I<n;I++,m++)i[I]=t[I]^p[m];ei(g)}return i}function Xf(r,e,t,i,n){kt(t,ki),kt(i),n=vA(i.length,n);let s=t,o=Et(s),a=zI(s),c=Et(i),l=Et(n),d=e?0:12,u=i.length,f=a.getUint32(d,e),{s0:h,s1:g,s2:p,s3:I}=Pn(r,o[0],o[1],o[2],o[3]);for(let w=0;w+4<=c.length;w+=4)l[w+0]=c[w+0]^h,l[w+1]=c[w+1]^g,l[w+2]=c[w+2]^p,l[w+3]=c[w+3]^I,f=f+1>>>0,a.setUint32(d,f,e),{s0:h,s1:g,s2:p,s3:I}=Pn(r,o[0],o[1],o[2],o[3]);let m=ki*Math.floor(c.length/K1);if(m<u){let w=new Uint32Array([h,g,p,I]),B=M1(w);for(let C=m,x=0;C<u;C++,x++)n[C]=i[C]^B[x];ei(w)}return n}function $X(r){if(kt(r),r.length%ki!==0)throw new Error("aes-(cbc/ecb).decrypt ciphertext should consist of blocks with size "+ki)}function VX(r,e,t){kt(r);let i=r.length,n=i%ki;if(!e&&n!==0)throw new Error("aec/(cbc-ecb): unpadded plaintext with disabled padding");on(r)||(r=vi(r));let s=Et(r);if(e){let a=ki-n;a||(a=ki),i=i+a}t=vA(i,t),L1(r,t);let o=Et(t);return{b:s,o,out:t}}function YX(r,e){if(!e)return r;let t=r.length;if(!t)throw new Error("aes/pcks5: empty ciphertext not allowed");let i=r[t-1];if(i<=0||i>16)throw new Error("aes/pcks5: wrong padding");let n=r.subarray(0,-i);for(let s=0;s<i;s++)if(r[t-s-1]!==i)throw new Error("aes/pcks5: wrong padding");return n}function WX(r){let e=new Uint8Array(16),t=Et(e);e.set(r);let i=ki-r.length;for(let n=ki-i;n<ki;n++)e[n]=i;return t}function XX(r,e,t,i,n){let s=n?n.length:0,o=r.create(t,i.length+s);n&&o.update(n);let a=qX(8*i.length,8*s,e);o.update(i),o.update(a);let c=o.digest();return ei(a),c}function bU(r){return r instanceof Uint32Array||ArrayBuffer.isView(r)&&r.constructor.name==="Uint32Array"}function vU(r,e){if(kt(e,16),!bU(r))throw new Error("_encryptBlock accepts result of expandKeyLE");let t=Et(e),{s0:i,s1:n,s2:s,s3:o}=Pn(r,t[0],t[1],t[2],t[3]);return t[0]=i,t[1]=n,t[2]=s,t[3]=o,e}function kU(r,e){if(kt(e,16),!bU(r))throw new Error("_decryptBlock accepts result of expandKeyLE");let t=Et(e),{s0:i,s1:n,s2:s,s3:o}=JI(r,t[0],t[1],t[2],t[3]);return t[0]=i,t[1]=n,t[2]=s,t[3]=o,e}async function DU(r){switch(r){case y.symmetric.aes128:case y.symmetric.aes192:case y.symmetric.aes256:throw new Error("Not a legacy cipher");case y.symmetric.cast5:case y.symmetric.blowfish:case y.symmetric.twofish:case y.symmetric.tripledes:{let{legacyCiphers:e}=await Promise.resolve().then(function(){return Eie}),t=y.read(y.symmetric,r),i=e.get(t);if(!i)throw new Error("Unsupported cipher algorithm");return i}default:throw new Error("Unsupported cipher algorithm")}}function ZX(r){switch(r){case y.symmetric.aes128:case y.symmetric.aes192:case y.symmetric.aes256:case y.symmetric.twofish:return 16;case y.symmetric.blowfish:case y.symmetric.cast5:case y.symmetric.tripledes:return 8;default:throw new Error("Unsupported cipher")}}function eZ(r){switch(r){case y.symmetric.aes128:case y.symmetric.blowfish:case y.symmetric.cast5:return 16;case y.symmetric.aes192:case y.symmetric.tripledes:return 24;case y.symmetric.aes256:case y.symmetric.twofish:return 32;default:throw new Error("Unsupported cipher")}}function It(r){return{keySize:eZ(r),blockSize:ZX(r)}}async function t1(r,e,t){let{keySize:i}=It(r);if(!F.isAES(r)||e.length!==i)throw new Error("Unexpected algorithm or key size");try{let n=await Dd.importKey("raw",e,{name:"AES-KW"},!1,["wrapKey"]),s=await Dd.importKey("raw",t,{name:"HMAC",hash:"SHA-256"},!0,["sign"]),o=await Dd.wrapKey("raw",s,n,{name:"AES-KW"});return new Uint8Array(o)}catch(n){if(n.name!=="NotSupportedError"&&!(e.length===24&&n.name==="OperationError"))throw n;F.printDebugError("Browser did not support operation: "+n.message)}return SU(e).encrypt(t)}async function r1(r,e,t){let{keySize:i}=It(r);if(!F.isAES(r)||e.length!==i)throw new Error("Unexpected algorithm or key size");let n;try{n=await Dd.importKey("raw",e,{name:"AES-KW"},!1,["unwrapKey"])}catch(s){if(s.name!=="NotSupportedError"&&!(e.length===24&&s.name==="OperationError"))throw s;return F.printDebugError("Browser did not support operation: "+s.message),SU(e).decrypt(t)}try{let s=await Dd.unwrapKey("raw",t,n,{name:"AES-KW"},{name:"HMAC",hash:"SHA-256"},!0,["sign"]);return new Uint8Array(await Dd.exportKey("raw",s))}catch(s){throw s.name==="OperationError"?new Error("Key Data Integrity failed"):s}}async function Fa(r,e,t,i,n){let s=F.getWebCrypto(),o=y.read(y.webHash,r);if(!o)throw new Error("Hash algo not supported with HKDF");let a=await s.importKey("raw",e,"HKDF",!1,["deriveBits"]),c=await s.deriveBits({name:"HKDF",hash:o,salt:t,info:i},a,n*8);return new Uint8Array(c)}async function j1(r){switch(r){case y.publicKey.x25519:try{let e=F.getWebCrypto(),t=await e.generateKey("X25519",!0,["deriveKey","deriveBits"]).catch(s=>{if(s.name==="OperationError"){let o=new Error("Unexpected key generation issue");throw o.name="NotSupportedError",o}throw s}),i=await e.exportKey("jwk",t.privateKey),n=await e.exportKey("jwk",t.publicKey);if(i.x!==n.x){let s=new Error("Unexpected mismatching public point");throw s.name="NotSupportedError",s}return{A:new Uint8Array(Ci(n.x)),k:Ci(i.d)}}catch(e){if(e.name!=="NotSupportedError")throw e;let{default:t}=await Promise.resolve().then(function(){return il}),{secretKey:i,publicKey:n}=t.box.keyPair();return{A:n,k:i}}case y.publicKey.x448:{let e=await F.getNobleCurve(y.publicKey.x448),{secretKey:t,publicKey:i}=e.keygen();return{A:i,k:t}}default:throw new Error("Unsupported ECDH algorithm")}}async function $1(r,e,t){switch(r){case y.publicKey.x25519:try{let{ephemeralPublicKey:i,sharedSecret:n}=await $I(r,e),s=await VI(r,i,e,t);return F.equalsUint8Array(n,s)}catch{return!1}case y.publicKey.x448:{let n=(await F.getNobleCurve(y.publicKey.x448)).getPublicKey(t);return F.equalsUint8Array(e,n)}default:return!1}}async function QU(r,e,t){let{ephemeralPublicKey:i,sharedSecret:n}=await $I(r,t),s=F.concatUint8Array([i,t,n]);switch(r){case y.publicKey.x25519:{let o=y.symmetric.aes128,{keySize:a}=It(o),c=await Fa(y.hash.sha256,s,new Uint8Array,cI.x25519,a),l=await t1(o,c,e);return{ephemeralPublicKey:i,wrappedKey:l}}case y.publicKey.x448:{let o=y.symmetric.aes256,{keySize:a}=It(y.symmetric.aes256),c=await Fa(y.hash.sha512,s,new Uint8Array,cI.x448,a),l=await t1(o,c,e);return{ephemeralPublicKey:i,wrappedKey:l}}default:throw new Error("Unsupported ECDH algorithm")}}async function _U(r,e,t,i,n){let s=await VI(r,e,i,n),o=F.concatUint8Array([e,i,s]);switch(r){case y.publicKey.x25519:{let a=y.symmetric.aes128,{keySize:c}=It(a),l=await Fa(y.hash.sha256,o,new Uint8Array,cI.x25519,c);return r1(a,l,t)}case y.publicKey.x448:{let a=y.symmetric.aes256,{keySize:c}=It(y.symmetric.aes256),l=await Fa(y.hash.sha512,o,new Uint8Array,cI.x448,c);return r1(a,l,t)}default:throw new Error("Unsupported ECDH algorithm")}}function jI(r){switch(r){case y.publicKey.x25519:return 32;case y.publicKey.x448:return 56;default:throw new Error("Unsupported ECDH algorithm")}}async function $I(r,e){switch(r){case y.publicKey.x25519:try{let t=F.getWebCrypto(),i=await t.generateKey("X25519",!0,["deriveKey","deriveBits"]).catch(l=>{if(l.name==="OperationError"){let d=new Error("Unexpected key generation issue");throw d.name="NotSupportedError",d}throw l}),n=await t.exportKey("jwk",i.publicKey);if((await t.exportKey("jwk",i.privateKey)).x!==n.x){let l=new Error("Unexpected mismatching public point");throw l.name="NotSupportedError",l}let o=V1(r,e),a=await t.importKey("jwk",o,"X25519",!1,[]),c=await t.deriveBits({name:"X25519",public:a},i.privateKey,jI(r)*8);return{sharedSecret:new Uint8Array(c),ephemeralPublicKey:new Uint8Array(Ci(n.x))}}catch(t){if(t.name!=="NotSupportedError")throw t;let{default:i}=await Promise.resolve().then(function(){return il}),{secretKey:n,publicKey:s}=i.box.keyPair(),o=i.scalarMult(n,e);return lI(o),{ephemeralPublicKey:s,sharedSecret:o}}case y.publicKey.x448:{let t=await F.getNobleCurve(y.publicKey.x448),{secretKey:i,publicKey:n}=t.keygen(),s=t.getSharedSecret(i,e);return lI(s),{ephemeralPublicKey:n,sharedSecret:s}}default:throw new Error("Unsupported ECDH algorithm")}}async function VI(r,e,t,i){switch(r){case y.publicKey.x25519:try{let n=F.getWebCrypto(),s=tZ(r,t,i),o=V1(r,e),a=await n.importKey("jwk",s,"X25519",!1,["deriveKey","deriveBits"]),c=await n.importKey("jwk",o,"X25519",!1,[]),l=await n.deriveBits({name:"X25519",public:c},a,jI(r)*8);return new Uint8Array(l)}catch(n){if(n.name!=="NotSupportedError")throw n;let{default:s}=await Promise.resolve().then(function(){return il}),o=s.scalarMult(i,e);return lI(o),o}case y.publicKey.x448:{let s=(await F.getNobleCurve(y.publicKey.x448)).getSharedSecret(i,e);return lI(s),s}default:throw new Error("Unsupported ECDH algorithm")}}function lI(r){let e=0;for(let t=0;t<r.length;t++)e|=r[t];if(e===0)throw new Error("Unexpected low order point")}function V1(r,e){if(r===y.publicKey.x25519)return{kty:"OKP",crv:"X25519",x:Ur(e),ext:!0};throw new Error("Unsupported ECDH algorithm")}function tZ(r,e,t){if(r===y.publicKey.x25519){let i=V1(r,e);return i.d=Ur(t),i}else throw new Error("Unsupported ECDH algorithm")}async function Vy(r){let e=new Si(r),{oid:t,hash:i,cipher:n}=e,s=await e.genKeyPair();return{oid:t,Q:s.publicKey,secret:F.leftPad(s.privateKey,e.payloadSize),hash:i,cipher:n}}function PU(r){return FU[r.getName()].hash}async function TU(r,e,t,i){let n={[y.curve.nistP256]:!0,[y.curve.nistP384]:!0,[y.curve.nistP521]:!0,[y.curve.secp256k1]:!0,[y.curve.curve25519Legacy]:r===y.publicKey.ecdh,[y.curve.brainpoolP256r1]:!0,[y.curve.brainpoolP384r1]:!0,[y.curve.brainpoolP512r1]:!0},s=e.getName();if(!n[s])return!1;if(s===y.curve.curve25519Legacy){let c=i.slice().reverse();return t.length<1||t[0]!==64?!1:$1(y.publicKey.x25519,t.subarray(1),c)}let a=(await F.getNobleCurve(y.publicKey.ecdsa,s)).getPublicKey(i,!1);return!!F.equalsUint8Array(a,t)}function $c(r,e){let{payloadSize:t,wireFormatLeadingByte:i,name:n}=r,s=n===y.curve.curve25519Legacy||n===y.curve.ed25519Legacy?t:t*2;if(e[0]!==i||e.length!==s+1)throw new Error("Invalid point encoding")}async function V6(r){let e=await F.getNobleCurve(y.publicKey.ecdsa,r),{secretKey:t}=e.keygen();return{publicKey:e.getPublicKey(t,!1),privateKey:t}}async function iZ(r,e){let t=await wB.generateKey({name:"ECDSA",namedCurve:us[r]},!0,["sign","verify"]),i=await wB.exportKey("jwk",t.privateKey),n=await wB.exportKey("jwk",t.publicKey);return{publicKey:UU(n,e),privateKey:Ci(i.d)}}function nZ(r){let e=dI.createECDH(ss[r]);return e.generateKeys(),{publicKey:new Uint8Array(e.getPublicKey()),privateKey:new Uint8Array(e.getPrivateKey())}}function UU(r,e){let t=Ci(r.x),i=Ci(r.y),n=new Uint8Array(t.length+i.length+1);return n[0]=e,n.set(t,1),n.set(i,t.length+1),n}function YI(r,e,t){let i=r,n=t.slice(1,i+1),s=t.slice(i+1,i*2+1);return{kty:"EC",crv:e,x:Ur(n),y:Ur(s),ext:!0}}function RU(r,e,t,i){let n=YI(r,e,t);return n.d=Ur(i),n}async function Y1(r,e,t,i,n,s){let o=new Si(r);if($c(o,i),t&&!F.isStream(t)){let l={publicKey:i,privateKey:n};switch(o.type){case"web":try{return await oZ(o,e,t,l)}catch(d){if(o.name!=="nistP521"&&(d.name==="DataError"||d.name==="OperationError"))throw d;F.printDebugError("Browser did not support signing: "+d.message)}break;case"node":return cZ(o,e,t,n)}}let c=(await F.getNobleCurve(y.publicKey.ecdsa,o.name)).sign(s,n,{lowS:!1});return{r:Wt(c.r,"be",o.payloadSize),s:Wt(c.s,"be",o.payloadSize)}}async function W1(r,e,t,i,n,s){let o=new Si(r);$c(o,n);let a=async()=>s[0]===0?Y6(o,t,s.subarray(1),n):!1;if(i&&!F.isStream(i))switch(o.type){case"web":try{return await aZ(o,e,t,i,n)||a()}catch(l){if(o.name!=="nistP521"&&(l.name==="DataError"||l.name==="OperationError"))throw l;F.printDebugError("Browser did not support verifying: "+l.message)}break;case"node":return lZ(o,e,t,i,n)||a()}return await Y6(o,t,s,n)||a()}async function sZ(r,e,t){let i=new Si(r);if(i.keyType!==y.publicKey.ecdsa)return!1;switch(i.type){case"web":case"node":{let n=Nr(8),s=y.hash.sha256,o=await hs(s,n);try{let a=await Y1(r,s,n,e,t,o);return await W1(r,s,a,n,e,o)}catch{return!1}}default:return TU(y.publicKey.ecdsa,r,e,t)}}async function Y6(r,e,t,i){return(await F.getNobleCurve(y.publicKey.ecdsa,r.name)).verify(F.concatUint8Array([e.r,e.s]),t,i,{lowS:!1})}async function oZ(r,e,t,i){let n=r.payloadSize,s=RU(r.payloadSize,us[r.name],i.publicKey,i.privateKey),o=await uI.importKey("jwk",s,{name:"ECDSA",namedCurve:us[r.name],hash:{name:y.read(y.webHash,r.hash)}},!1,["sign"]),a=new Uint8Array(await uI.sign({name:"ECDSA",namedCurve:us[r.name],hash:{name:y.read(y.webHash,e)}},o,t));return{r:a.slice(0,n),s:a.slice(n,n<<1)}}async function aZ(r,e,{r:t,s:i},n,s){let o=YI(r.payloadSize,us[r.name],s),a=await uI.importKey("jwk",o,{name:"ECDSA",namedCurve:us[r.name],hash:{name:y.read(y.webHash,r.hash)}},!1,["verify"]),c=F.concatUint8Array([t,i]).buffer;return uI.verify({name:"ECDSA",namedCurve:us[r.name],hash:{name:y.read(y.webHash,e)}},a,c,n)}function cZ(r,e,t,i){let n=F.nodeRequire("eckey-utils"),s=F.getNodeBuffer(),{privateKey:o}=n.generateDer({curveName:ss[r.name],privateKey:s.from(i)}),a=NU.createSign(y.read(y.hash,e));a.write(t),a.end();let c=new Uint8Array(a.sign({key:o,format:"der",type:"sec1",dsaEncoding:"ieee-p1363"})),l=r.payloadSize;return{r:c.subarray(0,l),s:c.subarray(l,l<<1)}}function lZ(r,e,{r:t,s:i},n,s){let o=F.nodeRequire("eckey-utils"),a=F.getNodeBuffer(),{publicKey:c}=o.generateDer({curveName:ss[r.name],publicKey:a.from(s)}),l=NU.createVerify(y.read(y.hash,e));l.write(n),l.end();let d=F.concatUint8Array([t,i]);try{return l.verify({key:c,format:"der",type:"spki",dsaEncoding:"ieee-p1363"},d)}catch{return!1}}async function qU(r,e,t,i,n,s){let o=new Si(r);if($c(o,i),pr(e)<pr(y.hash.sha256))throw new Error("Hash algorithm too weak for EdDSA.");let{RS:a}=await R1(y.publicKey.ed25519,e,t,i.subarray(1),n,s);return{r:a.subarray(0,32),s:a.subarray(32)}}async function OU(r,e,{r:t,s:i},n,s,o){let a=new Si(r);if($c(a,s),pr(e)<pr(y.hash.sha256))throw new Error("Hash algorithm too weak for EdDSA.");let c=F.concatUint8Array([t,i]);return N1(y.publicKey.ed25519,e,{RS:c},n,s.subarray(1),o)}async function MU(r,e,t){return r.getName()!==y.curve.ed25519Legacy||e.length<1||e[0]!==64?!1:q1(y.publicKey.ed25519,e.subarray(1),t)}function fZ(r){let e=8-r.length%8,t=new Uint8Array(r.length+e).fill(e);return t.set(r),t}function AZ(r){let e=r.length;if(e>0){let t=r[e-1];if(t>=1){let i=r.subarray(e-t),n=new Uint8Array(t).fill(t);if(F.equalsUint8Array(i,n))return r.subarray(0,e-t)}}throw new Error("Invalid padding")}async function hZ(r,e,t){return TU(y.publicKey.ecdh,r,e,t)}function LU(r,e,t,i){return F.concatUint8Array([e.write(),new Uint8Array([r]),t.write(),F.stringToUint8Array("Anonymous Sender "),i])}async function HU(r,e,t,i,n=!1,s=!1){let o;if(n){for(o=0;o<e.length&&e[o]===0;o++);e=e.subarray(o)}if(s){for(o=e.length-1;o>=0&&e[o]===0;o--);e=e.subarray(0,o+1)}return(await hs(r,F.concatUint8Array([new Uint8Array([0,0,0,1]),e,i]))).subarray(0,t)}async function gZ(r,e){switch(r.type){case"curve25519Legacy":{let{sharedSecret:t,ephemeralPublicKey:i}=await $I(y.publicKey.x25519,e.subarray(1));return{publicKey:F.concatUint8Array([new Uint8Array([r.wireFormatLeadingByte]),i]),sharedKey:t}}case"web":if(r.web&&F.getWebCrypto())try{return await yZ(r,e)}catch(t){return F.printDebugError(t),X6(r,e)}break;case"node":return wZ(r,e);default:return X6(r,e)}}async function KU(r,e,t,i,n){let s=fZ(t),o=new Si(r);$c(o,i);let{publicKey:a,sharedKey:c}=await gZ(o,i),l=LU(y.publicKey.ecdh,r,e,n),{keySize:d}=It(e.cipher),u=await HU(e.hash,c,d,l),f=await t1(e.cipher,u,s);return{publicKey:a,wrappedKey:f}}async function pZ(r,e,t,i){if(i.length!==r.payloadSize){let n=new Uint8Array(r.payloadSize);n.set(i,r.payloadSize-i.length),i=n}switch(r.type){case"curve25519Legacy":{let n=i.slice().reverse(),s=await VI(y.publicKey.x25519,e.subarray(1),t.subarray(1),n);return{secretKey:n,sharedKey:s}}case"web":if(r.web&&F.getWebCrypto())try{return await mZ(r,e,t,i)}catch(n){return F.printDebugError(n),W6(r,e,i)}break;case"node":return IZ(r,e,i);default:return W6(r,e,i)}}async function GU(r,e,t,i,n,s,o){let a=new Si(r);$c(a,n),$c(a,t);let{sharedKey:c}=await pZ(a,t,n,s),l=LU(y.publicKey.ecdh,r,e,o),{keySize:d}=It(e.cipher),u;for(let f=0;f<3;f++)try{let h=await HU(e.hash,c,d,l,f===1,f===2);return AZ(await r1(e.cipher,h,i))}catch(h){u=h}throw u}async function W6(r,e,t){let s=(await F.getNobleCurve(y.publicKey.ecdh,r.name)).getSharedSecret(t,e).subarray(1);return{secretKey:t,sharedKey:s}}async function X6(r,e){let t=await F.getNobleCurve(y.publicKey.ecdh,r.name),{publicKey:i,privateKey:n}=await r.genKeyPair(),o=t.getSharedSecret(n,e).subarray(1);return{publicKey:i,sharedKey:o}}async function mZ(r,e,t,i){let n=F.getWebCrypto(),s=RU(r.payloadSize,r.web,t,i),o=n.importKey("jwk",s,{name:"ECDH",namedCurve:r.web},!0,["deriveKey","deriveBits"]),a=YI(r.payloadSize,r.web,e),c=n.importKey("jwk",a,{name:"ECDH",namedCurve:r.web},!0,[]);[o,c]=await Promise.all([o,c]);let l=n.deriveBits({name:"ECDH",namedCurve:r.web,public:c},o,r.sharedSize),d=n.exportKey("jwk",o);[l,d]=await Promise.all([l,d]);let u=new Uint8Array(l);return{secretKey:Ci(d.d),sharedKey:u}}async function yZ(r,e){let t=F.getWebCrypto(),i=YI(r.payloadSize,r.web,e),n=t.generateKey({name:"ECDH",namedCurve:r.web},!0,["deriveKey","deriveBits"]),s=t.importKey("jwk",i,{name:"ECDH",namedCurve:r.web},!1,[]);[n,s]=await Promise.all([n,s]);let o=t.deriveBits({name:"ECDH",namedCurve:r.web,public:s},n.privateKey,r.sharedSize),a=t.exportKey("jwk",n.publicKey);[o,a]=await Promise.all([o,a]);let c=new Uint8Array(o);return{publicKey:new Uint8Array(UU(a,r.wireFormatLeadingByte)),sharedKey:c}}function IZ(r,e,t){let n=F.getNodeCrypto().createECDH(r.node);n.setPrivateKey(t);let s=new Uint8Array(n.computeSecret(e));return{secretKey:new Uint8Array(n.getPrivateKey()),sharedKey:s}}function wZ(r,e){let i=F.getNodeCrypto().createECDH(r.node);i.generateKeys();let n=new Uint8Array(i.computeSecret(e));return{publicKey:new Uint8Array(i.getPublicKey()),sharedKey:n}}async function EZ(r,e,t,i,n,s){let o=BigInt(0);i=Te(i),n=Te(n),t=Te(t),s=Te(s);let a,c,l,d;t=tt(t,i),s=tt(s,n);let u=tt(Te(e.subarray(0,bi(n))),n);for(;;){if(a=Ra(Zf,n),c=tt(Sr(t,a,i),n),c===o)continue;let f=tt(s*c,n);if(d=tt(u+f,n),l=tt(Td(a,n)*d,n),l!==o)break}return{r:Wt(c,"be",bi(i)),s:Wt(l,"be",bi(i))}}async function BZ(r,e,t,i,n,s,o,a){if(e=Te(e),t=Te(t),s=Te(s),o=Te(o),n=Te(n),a=Te(a),e<=Yy||e>=o||t<=Yy||t>=o)return F.printDebug("invalid DSA Signature"),!1;let c=tt(Te(i.subarray(0,bi(o))),o),l=Td(t,o);if(l===Yy)return F.printDebug("invalid DSA Signature"),!1;n=tt(n,s),a=tt(a,s);let d=tt(c*l,o),u=tt(e*l,o),f=Sr(n,d,s),h=Sr(a,u,s);return tt(tt(f*h,s),o)===e}async function bZ(r,e,t,i,n){let s=Te(r),o=Te(e),a=Te(t),c=Te(i);if(a<=Zf||a>=s||tt(s-Zf,o)!==Yy||Sr(a,o,s)!==Zf)return!1;let l=BigInt(Gd(o)),d=BigInt(150);if(l<d||!dU(o,null,32))return!1;let u=Te(n),f=BigInt(2),h=Ra(f<<l-Zf,f<<l),g=o*h+u;return c===Sr(a,g,s)}async function vZ(r,e,t,i,n){switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:{let{n:s,e:o}=t;return{c:await hX(i,s,o)}}case y.publicKey.elgamal:{let{p:s,g:o,y:a}=t;return SX(i,s,o,a)}case y.publicKey.ecdh:{let{oid:s,Q:o,kdfParams:a}=t,{publicKey:c,wrappedKey:l}=await KU(s,a,i,o,n);return{V:c,C:new fI(l)}}case y.publicKey.x25519:case y.publicKey.x448:{if(e&&!F.isAES(e))throw new Error("X25519 and X448 keys can only encrypt AES session keys");let{A:s}=t,{ephemeralPublicKey:o,wrappedKey:a}=await QU(r,i,s),c=hI.fromObject({algorithm:e,wrappedKey:a});return{ephemeralPublicKey:o,C:c}}default:return[]}}async function kZ(r,e,t,i,n,s){switch(r){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:{let{c:o}=i,{n:a,e:c}=e,{d:l,p:d,q:u,u:f}=t;return gX(o,a,c,l,d,u,f,s)}case y.publicKey.elgamal:{let{c1:o,c2:a}=i,c=e.p,l=t.x;return DX(o,a,c,l,s)}case y.publicKey.ecdh:{let{oid:o,Q:a,kdfParams:c}=e,{d:l}=t,{V:d,C:u}=i;return GU(o,c,d,u.data,a,l,n)}case y.publicKey.x25519:case y.publicKey.x448:{let{A:o}=e,{k:a}=t,{ephemeralPublicKey:c,C:l}=i;if(l.algorithm!==null&&!F.isAES(l.algorithm))throw new Error("AES session key expected");return _U(r,c,l.wrappedKey,o,a)}default:throw new Error("Unknown public key encryption algorithm.")}}function SZ(r,e){let t=0;switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));return t+=n.length+2,{read:t,publicParams:{n:i,e:n}}}case y.publicKey.dsa:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));t+=n.length+2;let s=F.readMPI(e.subarray(t));t+=s.length+2;let o=F.readMPI(e.subarray(t));return t+=o.length+2,{read:t,publicParams:{p:i,q:n,g:s,y:o}}}case y.publicKey.elgamal:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));t+=n.length+2;let s=F.readMPI(e.subarray(t));return t+=s.length+2,{read:t,publicParams:{p:i,g:n,y:s}}}case y.publicKey.ecdsa:{let i=new co;t+=i.read(e),xB(i);let n=F.readMPI(e.subarray(t));return t+=n.length+2,{read:t,publicParams:{oid:i,Q:n}}}case y.publicKey.eddsaLegacy:{let i=new co;if(t+=i.read(e),xB(i),i.getName()!==y.curve.ed25519Legacy)throw new Error("Unexpected OID for eddsaLegacy");let n=F.readMPI(e.subarray(t));return t+=n.length+2,n=F.leftPad(n,33),{read:t,publicParams:{oid:i,Q:n}}}case y.publicKey.ecdh:{let i=new co;t+=i.read(e),xB(i);let n=F.readMPI(e.subarray(t));t+=n.length+2;let s=new AI;return t+=s.read(e.subarray(t)),{read:t,publicParams:{oid:i,Q:n,kdfParams:s}}}case y.publicKey.ed25519:case y.publicKey.ed448:case y.publicKey.x25519:case y.publicKey.x448:{let i=F.readExactSubarray(e,t,t+Bd(r));return t+=i.length,{read:t,publicParams:{A:i}}}default:throw new ft("Unknown public key encryption algorithm.")}}function Z6(r,e,t){let i=0;switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:{let n=F.readMPI(e.subarray(i));i+=n.length+2;let s=F.readMPI(e.subarray(i));i+=s.length+2;let o=F.readMPI(e.subarray(i));i+=o.length+2;let a=F.readMPI(e.subarray(i));return i+=a.length+2,{read:i,privateParams:{d:n,p:s,q:o,u:a}}}case y.publicKey.dsa:case y.publicKey.elgamal:{let n=F.readMPI(e.subarray(i));return i+=n.length+2,{read:i,privateParams:{x:n}}}case y.publicKey.ecdsa:case y.publicKey.ecdh:{let n=Bd(r,t.oid),s=F.readMPI(e.subarray(i));return i+=s.length+2,s=F.leftPad(s,n),{read:i,privateParams:{d:s}}}case y.publicKey.eddsaLegacy:{let n=Bd(r,t.oid);if(t.oid.getName()!==y.curve.ed25519Legacy)throw new Error("Unexpected OID for eddsaLegacy");let s=F.readMPI(e.subarray(i));return i+=s.length+2,s=F.leftPad(s,n),{read:i,privateParams:{seed:s}}}case y.publicKey.ed25519:case y.publicKey.ed448:{let n=Bd(r),s=F.readExactSubarray(e,i,i+n);return i+=s.length,{read:i,privateParams:{seed:s}}}case y.publicKey.x25519:case y.publicKey.x448:{let n=Bd(r),s=F.readExactSubarray(e,i,i+n);return i+=s.length,{read:i,privateParams:{k:s}}}default:throw new ft("Unknown public key encryption algorithm.")}}function DZ(r,e){let t=0;switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:return{c:F.readMPI(e.subarray(t))};case y.publicKey.elgamal:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));return{c1:i,c2:n}}case y.publicKey.ecdh:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=new fI;return n.read(e.subarray(t)),{V:i,C:n}}case y.publicKey.x25519:case y.publicKey.x448:{let i=Bd(r),n=F.readExactSubarray(e,t,t+i);t+=n.length;let s=new hI;return s.read(e.subarray(t)),{ephemeralPublicKey:n,C:s}}default:throw new ft("Unknown public key encryption algorithm.")}}function Rd(r,e){let t=new Set([y.publicKey.ed25519,y.publicKey.x25519,y.publicKey.ed448,y.publicKey.x448]),i=Object.keys(e).map(n=>{let s=e[n];return F.isUint8Array(s)?t.has(r)?s:F.uint8ArrayToMPI(s):s.write()});return F.concatUint8Array(i)}function QZ(r,e,t){switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:return pX(e,65537).then(({n:i,e:n,d:s,p:o,q:a,u:c})=>({privateParams:{d:s,p:o,q:a,u:c},publicParams:{n:i,e:n}}));case y.publicKey.ecdsa:return Vy(t).then(({oid:i,Q:n,secret:s})=>({privateParams:{d:s},publicParams:{oid:new co(i),Q:n}}));case y.publicKey.eddsaLegacy:return Vy(t).then(({oid:i,Q:n,secret:s})=>({privateParams:{seed:s},publicParams:{oid:new co(i),Q:n}}));case y.publicKey.ecdh:return Vy(t).then(({oid:i,Q:n,secret:s,hash:o,cipher:a})=>({privateParams:{d:s},publicParams:{oid:new co(i),Q:n,kdfParams:new AI({hash:o,cipher:a})}}));case y.publicKey.ed25519:case y.publicKey.ed448:return U1(r).then(({A:i,seed:n})=>({privateParams:{seed:n},publicParams:{A:i}}));case y.publicKey.x25519:case y.publicKey.x448:return j1(r).then(({A:i,k:n})=>({privateParams:{k:n},publicParams:{A:i}}));case y.publicKey.dsa:case y.publicKey.elgamal:throw new Error("Unsupported algorithm for key generation.");default:throw new Error("Unknown public key algorithm.")}}async function _Z(r,e,t){if(!e||!t)throw new Error("Missing key parameters");switch(r){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:{let{n:i,e:n}=e,{d:s,p:o,q:a,u:c}=t;return mX(i,n,s,o,a,c)}case y.publicKey.dsa:{let{p:i,q:n,g:s,y:o}=e,{x:a}=t;return bZ(i,n,s,o,a)}case y.publicKey.elgamal:{let{p:i,g:n,y:s}=e,{x:o}=t;return QX(i,n,s,o)}case y.publicKey.ecdsa:case y.publicKey.ecdh:{let i=CZ[y.read(y.publicKey,r)],{oid:n,Q:s}=e,{d:o}=t;return i.validateParams(n,s,o)}case y.publicKey.eddsaLegacy:{let{Q:i,oid:n}=e,{seed:s}=t;return MU(n,i,s)}case y.publicKey.ed25519:case y.publicKey.ed448:{let{A:i}=e,{seed:n}=t;return q1(r,i,n)}case y.publicKey.x25519:case y.publicKey.x448:{let{A:i}=e,{k:n}=t;return $1(r,i,n)}default:throw new Error("Unknown public key algorithm.")}}function i1(r){let{keySize:e}=It(r);return Nr(e)}function xB(r){try{r.getName()}catch{throw new ft("Unknown curve OID")}}function Bd(r,e){switch(r){case y.publicKey.ecdsa:case y.publicKey.ecdh:case y.publicKey.eddsaLegacy:return new Si(e).payloadSize;case y.publicKey.ed25519:case y.publicKey.ed448:return KI(r);case y.publicKey.x25519:case y.publicKey.x448:return jI(r);default:throw new Error("Unknown elliptic algo")}}function FZ(r,e){switch(r){case y.publicKey.ecdsa:case y.publicKey.eddsaLegacy:return PU(e);case y.publicKey.ed25519:case y.publicKey.ed448:return GI(r);default:throw new Error("Unknown elliptic signing algo")}}function zU(r){let{blockSize:e}=It(r),t=Nr(e),i=new Uint8Array([t[t.length-2],t[t.length-1]]);return F.concat([t,i])}async function aA(r,e,t,i,n){let s=y.read(y.symmetric,r);if(F.getNodeCrypto()&&WI[s])return UZ(r,e,t,i);if(F.isAES(r))return PZ(r,e,t,i);let o=await DU(r),a=new o(e),c=a.blockSize,l=i.slice(),d=new Uint8Array,u=f=>{f&&(d=F.concatUint8Array([d,f]));let h=new Uint8Array(d.length),g,p=0;for(;f?d.length>=c:d.length;){let I=a.encrypt(l);for(g=0;g<c;g++)l[g]=d[g]^I[g],h[p++]=l[g];d=d.subarray(c)}return h.subarray(0,p)};return sr(t,u,u)}async function XI(r,e,t,i){let n=y.read(y.symmetric,r);if(oA&&WI[n])return RZ(r,e,t,i);if(F.isAES(r))return TZ(r,e,t,i);let s=await DU(r),o=new s(e),a=o.blockSize,c=i,l=new Uint8Array,d=u=>{u&&(l=F.concatUint8Array([l,u]));let f=new Uint8Array(l.length),h,g=0;for(;u?l.length>=a:l.length;){let p=o.encrypt(c);for(c=l.subarray(0,a),h=0;h<a;h++)f[g++]=c[h]^p[h];l=l.subarray(a)}return f.subarray(0,g)};return sr(t,d,d)}async function PZ(r,e,t,i){if(Wy&&await gI.isSupported(r)){let n=new gI(r,e,i);return F.isStream(t)?$B(t,s=>n.encryptChunk(s),()=>n.finish()):n.encrypt(t)}else if(F.isStream(t)){let n=new pI(!0,r,e,i);return $B(t,s=>n.processChunk(s),()=>n.finish())}return BU(e,i).encrypt(t)}function TZ(r,e,t,i){if(F.isStream(t)){let n=new pI(!1,r,e,i);return $B(t,s=>n.processChunk(s),()=>n.finish())}return BU(e,i).decrypt(t)}function Ry(r,e){let t=Math.min(r.length,e.length);for(let i=0;i<t;i++)r[i]=r[i]^e[i]}function UZ(r,e,t,i){let n=y.read(y.symmetric,r),s=new oA.createCipheriv(WI[n],e,i);return sr(t,o=>new Uint8Array(s.update(o)))}function RZ(r,e,t,i){let n=y.read(y.symmetric,r),s=new oA.createDecipheriv(WI[n],e,i);return sr(t,o=>new Uint8Array(s.update(o)))}function tT(r,e){let t=r.length-lo;for(let i=0;i<lo;i++)r[i+t]^=e[i];return r}function qZ(r,e,t){if(r.length&&r.length%lo===0)return tT(r,e);let i=new Uint8Array(r.length+(lo-r.length%lo));return i.set(r),i[r.length]=128,tT(i,t)}async function OZ(r){let e=await MZ(r),t=F.double(await e(Xy)),i=F.double(t);return async function(n){return(await e(qZ(n,t,i))).subarray(-lo)}}async function MZ(r){if(F.getNodeCrypto())return async function(e){let i=new NZ.createCipheriv("aes-"+r.length*8+"-cbc",r,Xy).update(e);return new Uint8Array(i)};if(F.getWebCrypto())try{return r=await eT.importKey("raw",r,{name:"AES-CBC",length:r.length*8},!1,["encrypt"]),async function(e){let t=await eT.encrypt({name:"AES-CBC",iv:Xy,length:lo*8},r,e);return new Uint8Array(t).subarray(0,t.byteLength-lo)}}catch(e){if(e.name!=="NotSupportedError"&&!(r.length===24&&e.name==="OperationError"))throw e;F.printDebugError("Browser did not support operation: "+e.message)}return async function(e){return e1(r,Xy,{disablePadding:!0}).encrypt(e)}}async function GZ(r){let e=await OZ(r);return function(t,i){return e(F.concatUint8Array([t,i]))}}async function zZ(r){if(F.getNodeCrypto())return async function(e,t){let i=new LZ.createCipheriv("aes-"+r.length*8+"-ctr",r,t),n=HZ.concat([i.update(e),i.final()]);return new Uint8Array(n)};if(F.getWebCrypto())try{let e=await rT.importKey("raw",r,{name:"AES-CTR",length:r.length*8},!1,["encrypt"]);return async function(t,i){let n=await rT.encrypt({name:"AES-CTR",counter:i,length:go*8},e,t);return new Uint8Array(n)}}catch(e){if(e.name!=="NotSupportedError"&&!(r.length===24&&e.name==="OperationError"))throw e;F.printDebugError("Browser did not support operation: "+e.message)}return async function(e,t){return jX(r,t).encrypt(e)}}async function kA(r,e){if(r!==y.symmetric.aes128&&r!==y.symmetric.aes192&&r!==y.symmetric.aes256)throw new Error("EAX mode supports only AES cipher");let[t,i]=await Promise.all([GZ(e),zZ(e)]);return{encrypt:async function(n,s,o){let[a,c]=await Promise.all([t(iT,s),t(n1,o)]),l=await i(n,a),u=await t(s1,l);for(let f=0;f<xd;f++)u[f]^=c[f]^a[f];return F.concatUint8Array([l,u])},decrypt:async function(n,s,o){if(n.length<xd)throw new Error("Invalid EAX ciphertext");let a=n.subarray(0,-xd),c=n.subarray(-xd),[l,d,u]=await Promise.all([t(iT,s),t(n1,o),t(s1,a)]),f=u;for(let g=0;g<xd;g++)f[g]^=d[g]^l[g];if(!F.equalsUint8Array(c,f))throw new Error("Authentication tag mismatch");return await i(a,l)}}}function nT(r){let e=0;for(let t=1;(r&t)===0;t<<=1)e++;return e}function wi(r,e){for(let t=0;t<r.length;t++)r[t]^=e[t];return r}function Ny(r,e){return wi(r.slice(),e)}async function SA(r,e){let{keySize:t}=It(r);if(!F.isAES(r)||e.length!==t)throw new Error("Unexpected algorithm or key size");let i=0,n=u=>e1(e,Hf,{disablePadding:!0}).encrypt(u),s=u=>e1(e,Hf,{disablePadding:!0}).decrypt(u),o;a();function a(){let u=n(Hf),f=F.double(u);o=[],o[0]=F.double(f),o.x=u,o.$=f}function c(u,f){let h=F.nbits(Math.max(u.length,f.length)/di|0)-1;for(let g=i+1;g<=h;g++)o[g]=F.double(o[g-1]);i=h}function l(u){if(!u.length)return Hf;let f=u.length/di|0,h=new Uint8Array(di),g=new Uint8Array(di);for(let p=0;p<f;p++)wi(h,o[nT(p+1)]),wi(g,n(Ny(h,u))),u=u.subarray(di);if(u.length){wi(h,o.x);let p=new Uint8Array(di);p.set(u,0),p[u.length]=128,wi(p,h),wi(g,n(p))}return g}function d(u,f,h,g){let p=f.length/di|0;c(f,g);let I=F.concatUint8Array([Hf.subarray(0,JU-h.length),JZ,h]),m=I[di-1]&63;I[di-1]&=192;let w=n(I),B=F.concatUint8Array([w,Ny(w.subarray(0,8),w.subarray(1,9))]),C=F.shiftRight(B.subarray(0+(m>>3),17+(m>>3)),8-(m&7)).subarray(1),x=new Uint8Array(di),Q=new Uint8Array(f.length+ba),R,U=0;for(R=0;R<p;R++)wi(C,o[nT(R+1)]),Q.set(wi(u(Ny(C,f)),C),U),wi(x,u===n?f:Q.subarray(U)),f=f.subarray(di),U+=di;if(f.length){wi(C,o.x);let D=n(C);Q.set(Ny(f,D),U);let b=new Uint8Array(di);b.set(u===n?f:Q.subarray(U,-ba),0),b[f.length]=128,wi(x,b),U+=f.length}let T=wi(n(wi(wi(x,C),o.$)),l(g));return Q.set(T,U),Q}return{encrypt:async function(u,f,h){return d(n,u,f,h)},decrypt:async function(u,f,h){if(u.length<ba)throw new Error("Invalid OCB ciphertext");let g=u.subarray(-ba);u=u.subarray(0,-ba);let p=d(s,u,f,h);if(F.equalsUint8Array(g,p.subarray(-ba)))return p.subarray(0,-ba);throw new Error("Authentication tag mismatch")}}}async function Nd(r,e){if(r!==y.symmetric.aes128&&r!==y.symmetric.aes192&&r!==y.symmetric.aes256)throw new Error("GCM mode supports only AES cipher");if(F.getNodeCrypto())return{encrypt:async function(t,i,n=new Uint8Array){let s=new sT.createCipheriv("aes-"+e.length*8+"-gcm",e,i);s.setAAD(n);let o=oT.concat([s.update(t),s.final(),s.getAuthTag()]);return new Uint8Array(o)},decrypt:async function(t,i,n=new Uint8Array){let s=new sT.createDecipheriv("aes-"+e.length*8+"-gcm",e,i);s.setAAD(n),s.setAuthTag(t.slice(t.length-Cd,t.length));let o=oT.concat([s.update(t.slice(0,t.length-Cd)),s.final()]);return new Uint8Array(o)}};if(F.getWebCrypto())try{let t=await EB.importKey("raw",e,{name:BB},!1,["encrypt","decrypt"]),i=navigator.userAgent.match(/Version\/13\.\d(\.\d)* Safari/)||navigator.userAgent.match(/Version\/(13|14)\.\d(\.\d)* Mobile\/\S* Safari/);return{encrypt:async function(n,s,o=new Uint8Array){if(i&&!n.length)return Uy(e,s,o).encrypt(n);let a=await EB.encrypt({name:BB,iv:s,additionalData:o,tagLength:Cd*8},t,n);return new Uint8Array(a)},decrypt:async function(n,s,o=new Uint8Array){if(i&&n.length===Cd)return Uy(e,s,o).decrypt(n);try{let a=await EB.decrypt({name:BB,iv:s,additionalData:o,tagLength:Cd*8},t,n);return new Uint8Array(a)}catch(a){if(a.name==="OperationError")throw new Error("Authentication tag mismatch")}}}}catch(t){if(t.name!=="NotSupportedError"&&!(e.length===24&&t.name==="OperationError"))throw t;F.printDebugError("Browser did not support operation: "+t.message)}return{encrypt:async function(t,i,n){return Uy(e,i,n).encrypt(t)},decrypt:async function(t,i,n){return Uy(e,i,n).decrypt(t)}}}function uo(r,e=!1){switch(r){case y.aead.eax:return kA;case y.aead.ocb:return SA;case y.aead.gcm:return Nd;case y.aead.experimentalGCM:if(!e)throw new Error("Unexpected non-standard `experimentalGCM` AEAD algorithm provided in `config.preferredAEADAlgorithm`: use `gcm` instead");return Nd;default:throw new Error("Unsupported AEAD mode")}}function VZ(r,e){let t=0;switch(r){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:case y.publicKey.rsaSign:{let i=F.readMPI(e.subarray(t));return t+=i.length+2,{read:t,signatureParams:{s:i}}}case y.publicKey.dsa:case y.publicKey.ecdsa:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));return t+=n.length+2,{read:t,signatureParams:{r:i,s:n}}}case y.publicKey.eddsaLegacy:{let i=F.readMPI(e.subarray(t));t+=i.length+2;let n=F.readMPI(e.subarray(t));return t+=n.length+2,{read:t,signatureParams:{r:i,s:n}}}case y.publicKey.ed25519:case y.publicKey.ed448:{let i=2*KI(r),n=F.readExactSubarray(e,t,t+i);return t+=n.length,{read:t,signatureParams:{RS:n}}}default:throw new ft("Unknown signature algorithm.")}}async function YZ(r,e,t,i,n,s){switch(r){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:case y.publicKey.rsaSign:{let{n:o,e:a}=i,c=F.leftPad(t.s,o.length);return AX(e,n,c,o,a,s)}case y.publicKey.dsa:{let{g:o,p:a,q:c,y:l}=i,{r:d,s:u}=t;return BZ(e,d,u,s,o,a,c,l)}case y.publicKey.ecdsa:{let{oid:o,Q:a}=i,c=new Si(o).payloadSize,l=F.leftPad(t.r,c),d=F.leftPad(t.s,c);return W1(o,e,{r:l,s:d},n,a,s)}case y.publicKey.eddsaLegacy:{let{oid:o,Q:a}=i,c=new Si(o).payloadSize,l=F.leftPad(t.r,c),d=F.leftPad(t.s,c);return OU(o,e,{r:l,s:d},n,a,s)}case y.publicKey.ed25519:case y.publicKey.ed448:{let{A:o}=i;return N1(r,e,t,n,o,s)}default:throw new Error("Unknown signature algorithm.")}}async function WZ(r,e,t,i,n,s){if(!t||!i)throw new Error("Missing key parameters");switch(r){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:case y.publicKey.rsaSign:{let{n:o,e:a}=t,{d:c,p:l,q:d,u}=i;return{s:await fX(e,n,o,a,c,l,d,u,s)}}case y.publicKey.dsa:{let{g:o,p:a,q:c}=t,{x:l}=i;return EZ(e,s,o,a,c,l)}case y.publicKey.elgamal:throw new Error("Signing with Elgamal is not defined in the OpenPGP standard.");case y.publicKey.ecdsa:{let{oid:o,Q:a}=t,{d:c}=i;return Y1(o,e,n,a,c,s)}case y.publicKey.eddsaLegacy:{let{oid:o,Q:a}=t,{seed:c}=i;return qU(o,e,n,a,c,s)}case y.publicKey.ed25519:case y.publicKey.ed448:{let{A:o}=t,{seed:a}=i;return R1(r,e,n,o,a,s)}default:throw new Error("Unknown signature algorithm.")}}function mI(r,e=pe){switch(r){case y.s2k.argon2:return new o1(e);case y.s2k.iterated:case y.s2k.gnu:case y.s2k.salted:case y.s2k.simple:return new a1(r,e);default:throw new ft("Unsupported S2K type")}}function jU(r){let{s2kType:e}=r;if(!ree.has(e))throw new Error("The provided `config.s2kType` value is not allowed");return mI(e,r)}function yt(r,e,t){let i=[];return i.push(HI(t.length+1)),i.push(new Uint8Array([(e?128:0)|r])),i.push(t),F.concat(i)}function SB(r){switch(r){case y.hash.sha256:return 16;case y.hash.sha384:return 24;case y.hash.sha512:return 32;case y.hash.sha224:return 16;case y.hash.sha3_256:return 16;case y.hash.sha3_512:return 32;default:throw new Error("Unsupported hash function")}}function Cee(r,e){if(!e[r]){let t;try{t=y.read(y.packet,r)}catch{throw new Sd(`Unknown packet type with tag: ${r}`)}throw new Error(`Packet not allowed in this context: ${t}`)}return new e[r]}function Bee(r){let t=Fn(r);return new ReadableStream({async pull(i){try{let{value:n,done:s}=await t.read();if(s){i.close();return}for(let o=0;o<=n.length;o+=65536)(!o||o<n.length)&&i.enqueue(n.subarray(o,o+65536))}catch(n){i.error(n)}}},{highWaterMark:0})}function II(r,e){return t=>{let i;if(St(t)?i=new ReadableStream({async start(c){try{c.enqueue(await Xt(t)),c.close()}catch(l){c.error(l)}}}):or(t)?i=t:i=EA(t),i=Bee(i),r)try{let c=r();return i.pipeThrough(c)}catch(c){if(c.name!=="TypeError")throw c}let n=Fn(i),s=new e,o=!1,a=!1;return new ReadableStream({start(c){s.ondata=(l,d)=>{c.enqueue(l),o=!0,d&&(c.close(),a=!0)}},async pull(){for(o=!1;!o&&!a;){let{done:c,value:l}=await n.read();if(c){s.push(new Uint8Array,!0);return}else l.length&&s.push(l)}}},{highWaterMark:0})}}function bee(){return async function(r){let{default:e}=await Promise.resolve().then(function(){return ene});return e(EA(r))}}async function xI(r,e,t,i){let n=r instanceof Od&&r.version===2,s=!n&&r.constructor.tag===y.packet.aeadEncryptedData;if(!n&&!s)throw new Error("Unexpected packet type");let o=uo(r.aeadAlgorithm,s),a=e==="decrypt"?o.tagLength:0,c=e==="encrypt"?o.tagLength:0,l=2**(r.chunkSizeByte+6)+a,d=s?8:0,u=new ArrayBuffer(13+d),f=new Uint8Array(u,0,5+d),h=new Uint8Array(u),g=new DataView(u),p=new Uint8Array(u,5,8);f.set([192|r.constructor.tag,r.version,r.cipherAlgorithm,r.aeadAlgorithm,r.chunkSizeByte],0);let I=0,m=Promise.resolve(),w=0,B=0,C,x;if(n){let{keySize:R}=It(r.cipherAlgorithm),{ivLength:U}=o,T=new Uint8Array(u,0,5),D=await Fa(y.hash.sha256,t,r.salt,T,R+U);t=D.subarray(0,R),C=D.subarray(R),C.fill(0,C.length-8),x=new DataView(C.buffer,C.byteOffset,C.byteLength)}else C=r.iv;let Q=await o(r.cipherAlgorithm,t);return Ua(i,async(R,U)=>{if(F.isStream(R)!=="array"){let b=new TransformStream({},{highWaterMark:F.getHardwareConcurrency()*2**(r.chunkSizeByte+6),size:_=>_.length});Kd(b.readable,U),U=b.writable}let T=Fn(R),D=Gi(U);try{for(;;){let b=await T.readBytes(l+a)||new Uint8Array,_=b.subarray(b.length-a);b=b.subarray(0,b.length-a);let v,E,P;if(n)P=C;else{P=C.slice();for(let N=0;N<8;N++)P[C.length-8+N]^=p[N]}if(!I||b.length?(T.unshift(_),v=Q[e](b,P,f),v.catch(()=>{}),B+=b.length-a+c):(g.setInt32(5+d+4,w),v=Q[e](_,P,h),v.catch(()=>{}),B+=c,E=!0),w+=b.length-a,m=m.then(()=>v).then(async N=>{await D.ready,await D.write(N),B-=N.length}).catch(N=>D.abort(N)),(E||B>D.desiredSize)&&await m,!E)n?x.setInt32(C.length-4,++I):g.setInt32(9,++I);else{await D.close();break}}}catch(b){await D.ready.catch(()=>{}),await D.abort(b)}})}function AT(r,e,t,i){switch(e){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.elgamal:case y.publicKey.ecdh:return F.concatUint8Array([new Uint8Array(r===6?[]:[t]),i,F.writeChecksum(i.subarray(i.length%8))]);case y.publicKey.x25519:case y.publicKey.x448:return i;default:throw new Error("Unsupported public key algorithm")}}function Qee(r,e,t,i){switch(e){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.elgamal:case y.publicKey.ecdh:{let n=t.subarray(0,t.length-2),s=t.subarray(t.length-2),o=F.writeChecksum(n.subarray(n.length%8)),a=o[0]===s[0]&o[1]===s[1],c=r===6?{sessionKeyAlgorithm:null,sessionKey:n}:{sessionKeyAlgorithm:n[0],sessionKey:n.subarray(1)};if(i){let l=a&c.sessionKeyAlgorithm===i.sessionKeyAlgorithm&c.sessionKey.length===i.sessionKey.length;return{sessionKey:F.selectUint8Array(l,c.sessionKey,i.sessionKey),sessionKeyAlgorithm:r===6?null:F.selectUint8(l,c.sessionKeyAlgorithm,i.sessionKeyAlgorithm)}}else{if(a&&(r===6||y.read(y.symmetric,c.sessionKeyAlgorithm)))return c;throw new Error("Decryption error")}}case y.publicKey.x25519:case y.publicKey.x448:return{sessionKeyAlgorithm:null,sessionKey:t};default:throw new Error("Unsupported public key algorithm")}}async function DB(r,e,t,i,n,s,o,a){if(e.type==="argon2"&&!n)throw new Error("Using Argon2 S2K without AEAD is not allowed");if(e.type==="simple"&&r===6)throw new Error("Using Simple S2K with version 6 keys is not allowed");let{keySize:c}=It(i),l=await e.produceKey(t,c,a);if(!n||r===5||o)return l;let d=F.concatUint8Array([s,new Uint8Array([r,i,n])]);return Fa(y.hash.sha256,l,new Uint8Array,d,c)}async function eR(r,e){let t=new BI(r.date,e);return t.packets=null,t.algorithm=y.write(y.publicKey,r.algorithm),await t.generate(r.rsaBits,r.curve),await t.computeFingerprintAndKeyID(),t}async function Fee(r,e){let t=new uA(r.date,e);return t.packets=null,t.algorithm=y.write(y.publicKey,r.algorithm),await t.generate(r.rsaBits,r.curve,r.config),await t.computeFingerprintAndKeyID(),t}async function Sn(r,e,t,i,n=new Date,s){let o,a;for(let c=r.length-1;c>=0;c--)try{(!o||r[c].created>=o.created)&&(await r[c].verify(e,t,i,n,void 0,s),o=r[c])}catch(l){a=l}if(!o)throw F.wrapError(`Could not find valid ${y.read(y.signature,t)} signature in key ${e.getKeyID().toHex()}`.replace("certGeneric ","self-").replace(/([a-z])([A-Z])/g,(c,l,d)=>l+" "+d.toLowerCase()),a);return o}function g1(r,e,t=new Date){let i=F.normalizeDate(t);if(i!==null){let n=vI(r,e);return!(r.created<=i&&i<n)}return!1}async function tR(r,e,t,i){let n={};n.key=e,n.bind=r;let s={signatureType:y.signature.subkeyBinding};return t.sign?(s.keyFlags=[y.keyFlags.signData],s.embeddedSignature=await As(n,[],r,{signatureType:y.signature.keyBinding},t.date,void 0,void 0,void 0,i)):s.keyFlags=[y.keyFlags.encryptCommunication|y.keyFlags.encryptStorage],t.keyExpirationTime>0&&(s.keyExpirationTime=t.keyExpirationTime,s.keyNeverExpires=!1),await As(n,[],e,s,t.date,void 0,void 0,void 0,i)}async function Pee(r,e,t=new Date,i=[],n){let s=y.hash.sha256,o=n.preferredHashAlgorithm,a=await Promise.all(r.map(async(f,h)=>(await f.getPrimarySelfSignature(t,i[h],n)).preferredHashAlgorithms||[])),c=new Map;for(let f of a)for(let h of f)try{let g=y.write(y.hash,h);c.set(g,c.has(g)?c.get(g)+1:1)}catch{}let l=f=>r.length===0||c.get(f)===r.length||f===s,d=()=>{if(c.size===0)return s;let h=Array.from(c.keys()).filter(g=>l(g)).sort((g,p)=>pr(g)-pr(p))[0];return pr(h)>=pr(s)?h:s};if(new Set([y.publicKey.ecdsa,y.publicKey.eddsaLegacy,y.publicKey.ed25519,y.publicKey.ed448]).has(e.algorithm)){let f=FZ(e.algorithm,e.publicParams.oid),h=l(o),g=pr(o)>=pr(f);if(h&&g)return o;{let p=d();return pr(p)>=pr(f)?p:f}}return l(o)?o:d()}async function Tee(r=[],e=new Date,t=[],i=pe){let n=y.compression.uncompressed,s=i.preferredCompressionAlgorithm;return(await Promise.all(r.map(async function(a,c){let d=(await a.getPrimarySelfSignature(e,t[c],i)).preferredCompressionAlgorithms;return!!d&&d.indexOf(s)>=0}))).every(Boolean)?s:n}async function Uee(r=[],e=new Date,t=[],i=pe){let n=await Promise.all(r.map((c,l)=>c.getPrimarySelfSignature(e,t[l],i)));if(r.length?n.every(c=>c.features&&c.features[0]&y.features.seipdv2):i.aeadProtect){let c={symmetricAlgo:y.symmetric.aes128,aeadAlgo:y.aead.ocb},l=[{symmetricAlgo:i.preferredSymmetricAlgorithm,aeadAlgo:i.preferredAEADAlgorithm},{symmetricAlgo:i.preferredSymmetricAlgorithm,aeadAlgo:y.aead.ocb},{symmetricAlgo:y.symmetric.aes128,aeadAlgo:i.preferredAEADAlgorithm}];for(let d of l)if(n.every(u=>u.preferredCipherSuites&&u.preferredCipherSuites.some(f=>f[0]===d.symmetricAlgo&&f[1]===d.aeadAlgo)))return d;return c}let o=y.symmetric.aes128,a=i.preferredSymmetricAlgorithm;return{symmetricAlgo:n.every(c=>c.preferredSymmetricAlgorithms&&c.preferredSymmetricAlgorithms.includes(a))?a:o,aeadAlgo:void 0}}async function As(r,e,t,i,n,s,o=[],a=!1,c){if(t.isDummy())throw new Error("Cannot sign with a gnu-dummy key.");if(!t.isDecrypted())throw new Error("Signing key is not decrypted.");let l=new qr;return Object.assign(l,i),l.publicKeyAlgorithm=t.algorithm,l.hashAlgorithm=await Pee(e,t,n,s,c),l.rawNotations=[...o],await l.sign(t,r,n,a,c),l}async function Gc(r,e,t,i=new Date,n){r=r[t],r&&(e[t].length?await Promise.all(r.map(async function(s){!s.isExpired(i)&&(!n||await n(s))&&!e[t].some(function(o){return F.equalsUint8Array(o.writeParams(),s.writeParams())})&&e[t].push(s)})):e[t]=r)}async function Md(r,e,t,i,n,s,o=new Date,a){s=s||r;let c=[];return await Promise.all(i.map(async function(l){try{if(!n||l.issuerKeyID.equals(n.issuerKeyID)){let d=![y.reasonForRevocation.keyRetired,y.reasonForRevocation.keySuperseded,y.reasonForRevocation.userIDInvalid].includes(l.reasonForRevocationFlag);await l.verify(s,e,t,d?null:o,!1,a),c.push(l.issuerKeyID)}}catch{}})),n?(n.revoked=c.some(l=>l.equals(n.issuerKeyID))?!0:n.revoked||!1,n.revoked):c.length>0}function vI(r,e){let t;return e.keyNeverExpires===!1&&(t=r.created.getTime()+e.keyExpirationTime*1e3),t?new Date(t):1/0}function p1(r,e={}){switch(r.type=r.type||e.type,r.curve=r.curve||e.curve,r.rsaBits=r.rsaBits||e.rsaBits,r.keyExpirationTime=r.keyExpirationTime!==void 0?r.keyExpirationTime:e.keyExpirationTime,r.passphrase=F.isString(r.passphrase)?r.passphrase:e.passphrase,r.date=r.date||e.date,r.sign=r.sign||!1,r.type){case"ecc":try{r.curve=y.write(y.curve,r.curve)}catch{throw new Error("Unknown curve")}(r.curve===y.curve.ed25519Legacy||r.curve===y.curve.curve25519Legacy||r.curve==="ed25519"||r.curve==="curve25519")&&(r.curve=r.sign?y.curve.ed25519Legacy:y.curve.curve25519Legacy),r.sign?r.algorithm=r.curve===y.curve.ed25519Legacy?y.publicKey.eddsaLegacy:y.publicKey.ecdsa:r.algorithm=y.publicKey.ecdh;break;case"curve25519":r.algorithm=r.sign?y.publicKey.ed25519:y.publicKey.x25519;break;case"curve448":r.algorithm=r.sign?y.publicKey.ed448:y.publicKey.x448;break;case"rsa":r.algorithm=y.publicKey.rsaEncryptSign;break;default:throw new Error(`Unsupported key type ${r.type}`)}return r}function hT(r,e,t){switch(r.algorithm){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:case y.publicKey.dsa:case y.publicKey.ecdsa:case y.publicKey.eddsaLegacy:case y.publicKey.ed25519:case y.publicKey.ed448:if(!e.keyFlags&&!t.allowMissingKeyFlags)throw new Error("None of the key flags is set: consider passing `config.allowMissingKeyFlags`");return!e.keyFlags||(e.keyFlags[0]&y.keyFlags.signData)!==0;default:return!1}}function gT(r,e,t){switch(r.algorithm){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:case y.publicKey.elgamal:case y.publicKey.ecdh:case y.publicKey.x25519:case y.publicKey.x448:if(!e.keyFlags&&!t.allowMissingKeyFlags)throw new Error("None of the key flags is set: consider passing `config.allowMissingKeyFlags`");return!e.keyFlags||(e.keyFlags[0]&y.keyFlags.encryptCommunication)!==0||(e.keyFlags[0]&y.keyFlags.encryptStorage)!==0;default:return!1}}function pT(r,e,t){if(!e.keyFlags&&!t.allowMissingKeyFlags)throw new Error("None of the key flags is set: consider passing `config.allowMissingKeyFlags`");switch(r.algorithm){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaEncrypt:case y.publicKey.elgamal:case y.publicKey.ecdh:case y.publicKey.x25519:case y.publicKey.x448:return(!e.keyFlags||(e.keyFlags[0]&y.keyFlags.signData)!==0)&&t.allowInsecureDecryptionWithSigningKeys?!0:!e.keyFlags||(e.keyFlags[0]&y.keyFlags.encryptCommunication)!==0||(e.keyFlags[0]&y.keyFlags.encryptStorage)!==0;default:return!1}}function ka(r,e){let t=y.write(y.publicKey,r.algorithm),i=r.getAlgorithmInfo();if(e.rejectPublicKeyAlgorithms.has(t))throw new Error(`${i.algorithm} keys are considered too weak.`);switch(t){case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:case y.publicKey.rsaEncrypt:if(i.bits<e.minRSABits)throw new Error(`RSA keys shorter than ${e.minRSABits} bits are considered too weak.`);break;case y.publicKey.ecdsa:case y.publicKey.eddsaLegacy:case y.publicKey.ecdh:if(e.rejectCurves.has(i.curve))throw new Error(`Support for ${i.algorithm} keys using curve ${i.curve} is disabled.`);break}}function Nee(r){switch(y.write(y.publicKey,r)){case y.publicKey.rsaEncrypt:case y.publicKey.rsaEncryptSign:case y.publicKey.rsaSign:case y.publicKey.dsa:return"rsa";case y.publicKey.ecdsa:case y.publicKey.eddsaLegacy:return"ecc";case y.publicKey.ed25519:return"curve25519";case y.publicKey.ed448:return"curve448";default:throw new Error("Unsupported algorithm")}}async function Oee(r,e){r.sign=!0,r=p1(r),r.subkeys=r.subkeys.map((o,a)=>p1(r.subkeys[a],r));let t=[Fee(r,e)];t=t.concat(r.subkeys.map(o=>eR(o,e)));let i=await Promise.all(t),n=await Mee(i[0],i.slice(1),r,e),s=await n.getRevocationCertificate(r.date,e);return n.revocationSignatures=[],{key:n,revocationCertificate:s}}async function Mee(r,e,t,i){t.passphrase&&await r.encrypt(t.passphrase,i),await Promise.all(e.map(async function(c,l){let d=t.subkeys[l].passphrase;d&&await c.encrypt(d,i)}));let n=new Ct;n.push(r);function s(c,l){return[l,...c.filter(d=>d!==l)]}function o(){let c={};c.keyFlags=[y.keyFlags.certifyKeys|y.keyFlags.signData];let l=s([y.symmetric.aes256,y.symmetric.aes128],i.preferredSymmetricAlgorithm);if(c.preferredSymmetricAlgorithms=l,i.aeadProtect){let d=s([y.aead.gcm,y.aead.eax,y.aead.ocb],i.preferredAEADAlgorithm);c.preferredCipherSuites=d.flatMap(u=>l.map(f=>[f,u]))}return c.preferredHashAlgorithms=s([y.hash.sha512,y.hash.sha256,y.hash.sha3_512,y.hash.sha3_256],i.preferredHashAlgorithm),c.preferredCompressionAlgorithms=s([y.compression.uncompressed,y.compression.zlib,y.compression.zip],i.preferredCompressionAlgorithm),c.features=[0],c.features[0]|=y.features.modificationDetection,i.aeadProtect&&(c.features[0]|=y.features.seipdv2),t.keyExpirationTime>0&&(c.keyExpirationTime=t.keyExpirationTime,c.keyNeverExpires=!1),c}if(r.version===6){let c={key:r},l=o();l.signatureType=y.signature.key;let d=await As(c,[],r,l,t.date,void 0,t.signatureNotations,void 0,i);n.push(d)}await Promise.all(t.userIDs.map(async function(c,l){let d=EI.fromObject(c),u={userID:d,key:r},f=r.version!==6?o():{};f.signatureType=y.signature.certPositive,l===0&&(f.isPrimaryUserID=!0);let h=await As(u,[],r,f,t.date,void 0,t.signatureNotations,void 0,i);return{userIDPacket:d,signaturePacket:h}})).then(c=>{c.forEach(({userIDPacket:l,signaturePacket:d})=>{n.push(l),n.push(d)})}),await Promise.all(e.map(async function(c,l){let d=t.subkeys[l],u=await tR(c,r,d,i);return{secretSubkeyPacket:c,subkeySignaturePacket:u}})).then(c=>{c.forEach(({secretSubkeyPacket:l,subkeySignaturePacket:d})=>{n.push(l),n.push(d)})});let a={key:r};return n.push(await As(a,[],r,{signatureType:y.signature.keyRevocation,reasonForRevocationFlag:y.reasonForRevocation.noReason,reasonForRevocationString:""},t.date,void 0,void 0,void 0,i)),t.passphrase&&r.clearPrivateParams(),e.map(function(c,l){t.subkeys[l].passphrase&&c.clearPrivateParams()}),new DI(n)}async function tw({armoredKey:r,binaryKey:e,config:t,...i}){if(t={...pe,...t},!r&&!e)throw new Error("readPrivateKey: must pass options object containing `armoredKey` or `binaryKey`");if(r&&!F.isString(r))throw new Error("readPrivateKey: options.armoredKey must be a string");if(e&&!F.isUint8Array(e))throw new Error("readPrivateKey: options.binaryKey must be a Uint8Array");let n=Object.keys(i);if(n.length>0)throw new Error(`Unknown option: ${n.join(", ")}`);let s;if(r){let{type:c,data:l}=await LI(r);if(c!==y.armor.privateKey)throw new Error("Armored text not of type private key");s=l}else s=e;let o=await Ct.fromBinary(s,qee,t),a=o.indexOfTag(y.packet.publicKey,y.packet.secretKey);for(let c=0;c<a.length;c++){if(o[a[c]].constructor.tag===y.packet.publicKey)continue;let l=o.slice(a[c],a[c+1]);return new DI(l)}throw new Error("No secret key packet found")}async function IT(r,e,t=[],i=null,n=[],s=new Date,o=[],a=[],c=[],l=!1,d=pe){let u=new Ct,f=r.text===null?y.signature.binary:y.signature.text;if(await Promise.all(e.map(async(h,g)=>{let p=o[g];if(!h.isPrivate())throw new Error("Need private key for signing");let I=await h.getSigningKey(n[g],s,p,d);return As(r,t.length?t:[h],I.keyPacket,{signatureType:f},s,a,c,l,d)})).then(h=>{u.push(...h)}),i){let h=i.packets.filterByTag(y.packet.signature);u.push(...h)}return u}function Gee(r,e,t,i=new Date,n=!1,s=pe){let o,a;for(let u of t){let f=u.getKeys(r.issuerKeyID);if(f.length>0){o=u,a=f[0];break}}let l=r instanceof cn?r.correspondingSig:r,d={keyID:r.issuerKeyID,verified:(async()=>{if(!a)throw new Error(`Could not find signing key with key ID ${r.issuerKeyID.toHex()}`);await r.verify(a.keyPacket,r.signatureType,e[0],i,n,s);let u=await l;if(a.getCreationTime()>u.created)throw new Error("Key is newer than the signature");try{await o.getSigningKey(a.getKeyID(),u.created,void 0,s)}catch(f){if(s.allowInsecureVerificationWithReformattedKeys&&f.message.match(/Signature creation time is in the future/))await o.getSigningKey(a.getKeyID(),i,void 0,s);else throw f}return!0})(),signature:(async()=>{let u=await l,f=new Ct;return u&&f.push(u),new bI(f)})()};return d.signature.catch(()=>{}),d.verified.catch(()=>{}),d}function QB(r,e,t,i=new Date,n=!1,s=pe){return r.filter(o=>["text","binary"].includes(y.read(y.signature,o.signatureType))).map(o=>Gee(o,e,t,i,n,s))}async function DA({armoredMessage:r,binaryMessage:e,config:t,...i}){t={...pe,...t};let n=r||e;if(!n)throw new Error("readMessage: must pass options object containing `armoredMessage` or `binaryMessage`");if(r&&!F.isString(r)&&!F.isStream(r))throw new Error("readMessage: options.armoredMessage must be a string or stream");if(e&&!F.isUint8Array(e)&&!F.isStream(e))throw new Error("readMessage: options.binaryMessage must be a Uint8Array or stream");let s=Object.keys(i);if(s.length>0)throw new Error(`Unknown option: ${s.join(", ")}`);let o=F.isStream(n);if(r){let{type:l,data:d}=await LI(n);if(l!==y.armor.message)throw new Error("Armored text not of type message");n=d}let a=await Ct.fromBinary(n,Lee,t,new qd),c=new AA(a);return c.fromStream=o,c}async function rR({text:r,binary:e,filename:t,date:i=new Date,format:n=r!==void 0?"utf8":"binary",...s}){let o=r!==void 0?r:e;if(o===void 0)throw new Error("createMessage: must pass options object containing `text` or `binary`");if(r&&!F.isString(r)&&!F.isStream(r))throw new Error("createMessage: options.text must be a string or stream");if(e&&!F.isUint8Array(e)&&!F.isStream(e))throw new Error("createMessage: options.binary must be a Uint8Array or stream");let a=Object.keys(s);if(a.length>0)throw new Error(`Unknown option: ${a.join(", ")}`);let c=F.isStream(o),l=new Ta(i);r!==void 0?l.setText(o,y.write(y.literal,n)):l.setBytes(o,y.write(y.literal,n)),t!==void 0&&l.setFilename(t);let d=new Ct;d.push(l);let u=new AA(d);return u.fromStream=c,u}async function tb({userIDs:r=[],passphrase:e,type:t,curve:i,rsaBits:n=4096,keyExpirationTime:s=0,date:o=new Date,subkeys:a=[{}],format:c="armored",signatureNotations:l=[],config:d,...u}){d={...pe,...d},QA(d),!t&&!i?(t=d.v6Keys?"curve25519":"ecc",i="curve25519Legacy"):(t=t||"ecc",i=i||"curve25519Legacy"),r=Zr(r),l=Zr(l);let f=Object.keys(u);if(f.length>0)throw new Error(`Unknown option: ${f.join(", ")}`);if(r.length===0&&!d.v6Keys)throw new Error("UserIDs are required for V4 keys");if(t==="rsa"&&n<d.minRSABits)throw new Error(`rsaBits should be at least ${d.minRSABits}, got: ${n}`);let h={userIDs:r,passphrase:e,type:t,rsaBits:n,curve:i,keyExpirationTime:s,date:o,subkeys:a,signatureNotations:l};try{let{key:g,revocationCertificate:p}=await Oee(h,d);return g.getKeys().forEach(({keyPacket:I})=>ka(I,d)),{privateKey:wT(g,c,d),publicKey:wT(g.toPublic(),c,d),revocationCertificate:p}}catch(g){throw F.wrapError("Error generating keypair",g)}}async function rw({privateKey:r,passphrase:e,config:t,...i}){t={...pe,...t},QA(t);let n=Object.keys(i);if(n.length>0)throw new Error(`Unknown option: ${n.join(", ")}`);if(!r.isPrivate())throw new Error("Cannot decrypt a public key");let s=r.clone(!0),o=F.isArray(e)?e:[e];try{return await Promise.all(s.getKeys().map(a=>F.anyPromise(o.map(c=>a.keyPacket.decrypt(c,t))))),await s.validate(t),s}catch(a){throw s.clearPrivateParams(),F.wrapError("Error decrypting private key",a)}}async function iR({message:r,encryptionKeys:e,signingKeys:t,passwords:i,sessionKey:n,format:s="armored",signature:o=null,wildcard:a=!1,signingKeyIDs:c=[],encryptionKeyIDs:l=[],date:d=new Date,signingUserIDs:u=[],encryptionUserIDs:f=[],signatureNotations:h=[],config:g,...p}){if(g={...pe,...g},QA(g),rb(r),zee(s),e=Zr(e),t=Zr(t),i=Zr(i),c=Zr(c),l=Zr(l),u=Zr(u),f=Zr(f),h=Zr(h),p.detached)throw new Error("The `detached` option has been removed from openpgp.encrypt, separately call openpgp.sign instead. Don't forget to remove the `privateKeys` option as well.");if(p.publicKeys)throw new Error("The `publicKeys` option has been removed from openpgp.encrypt, pass `encryptionKeys` instead");if(p.privateKeys)throw new Error("The `privateKeys` option has been removed from openpgp.encrypt, pass `signingKeys` instead");if(p.armor!==void 0)throw new Error("The `armor` option has been removed from openpgp.encrypt, pass `format` instead.");let I=Object.keys(p);if(I.length>0)throw new Error(`Unknown option: ${I.join(", ")}`);t||(t=[]);try{if((t.length||o)&&(r=await r.sign(t,e,o,c,d,u,l,h,g)),r=r.compress(await Tee(e,d,f,g),g),r=await r.encrypt(e,i,n,a,l,d,f,g),s==="object")return r;let w=s==="armored"?r.armor(g):r.write();return await sR(w)}catch(m){throw F.wrapError("Error encrypting message",m)}}async function iw({message:r,decryptionKeys:e,passwords:t,sessionKeys:i,verificationKeys:n,expectSigned:s=!1,format:o="utf8",signature:a=null,date:c=new Date,config:l,...d}){if(l={...pe,...l},QA(l),rb(r),n=Zr(n),e=Zr(e),t=Zr(t),i=Zr(i),d.privateKeys)throw new Error("The `privateKeys` option has been removed from openpgp.decrypt, pass `decryptionKeys` instead");if(d.publicKeys)throw new Error("The `publicKeys` option has been removed from openpgp.decrypt, pass `verificationKeys` instead");let u=Object.keys(d);if(u.length>0)throw new Error(`Unknown option: ${u.join(", ")}`);try{let f=await r.decrypt(e,t,i,c,l);n||(n=[]);let h={};if(h.signatures=a?await f.verifyDetached(a,n,c,l):await f.verify(n,c,l),h.data=o==="binary"?f.getLiteralData():f.getText(),h.filename=f.getFilename(),jee(h,r,...new Set([f,f.unwrapCompressed()])),s){if(n.length===0)throw new Error("Verification keys are required to verify message signatures");if(h.signatures.length===0)throw new Error("Message is not signed");h.data=ui([h.data,Zc(async()=>(await F.anyPromise(h.signatures.map(g=>g.verified)),o==="binary"?new Uint8Array:""))])}return h.data=await sR(h.data),h}catch(f){throw F.wrapError("Error decrypting message",f)}}async function nR({message:r,decryptionKeys:e,passwords:t,date:i=new Date,config:n,...s}){if(n={...pe,...n},QA(n),rb(r),e=Zr(e),t=Zr(t),s.privateKeys)throw new Error("The `privateKeys` option has been removed from openpgp.decryptSessionKeys, pass `decryptionKeys` instead");let o=Object.keys(s);if(o.length>0)throw new Error(`Unknown option: ${o.join(", ")}`);try{return await r.decryptSessionKeys(e,t,void 0,i,n)}catch(a){throw F.wrapError("Error decrypting session keys",a)}}function rb(r){if(!(r instanceof AA))throw new Error("Parameter [message] needs to be of type Message")}function zee(r){if(r!=="armored"&&r!=="binary"&&r!=="object")throw new Error(`Unsupported format ${r}`)}function QA(r){let e=Object.keys(r);if(e.length!==Jee){for(let t of e)if(pe[t]===void 0)throw new Error(`Unknown config property: ${t}`)}}function Zr(r){return r&&!F.isArray(r)&&(r=[r]),r}async function sR(r){return F.isStream(r)==="array"?Xt(r):r}function jee(r,e,...t){r.data=Ua(e.packets.stream,async(i,n)=>{await Kd(r.data,n,{preventClose:!0});let s=Gi(n);try{await Xt(i,o=>o),await Promise.all(t.map(o=>Xt(o.packets.stream,a=>a))),await s.close()}catch(o){await s.abort(o)}})}function wT(r,e,t){switch(e){case"object":return r;case"armored":return r.armor(t);case"binary":return r.write();default:throw new Error(`Unsupported format ${e}`)}}function _A(r){return r instanceof Uint8Array||ArrayBuffer.isView(r)&&r.constructor.name==="Uint8Array"}function hA(r){if(!Number.isSafeInteger(r)||r<0)throw new Error("positive integer expected, got "+r)}function ps(r,...e){if(!_A(r))throw new Error("Uint8Array expected");if(e.length>0&&!e.includes(r.length))throw new Error("Uint8Array expected of length "+e+", got length="+r.length)}function oR(r){if(typeof r!="function"||typeof r.create!="function")throw new Error("Hash should be wrapped by utils.createHasher");hA(r.outputLen),hA(r.blockLen)}function Ld(r,e=!0){if(r.destroyed)throw new Error("Hash instance has been destroyed");if(e&&r.finished)throw new Error("Hash#digest() has already been called")}function aR(r,e){ps(r);let t=e.outputLen;if(r.length<t)throw new Error("digestInto() expects output buffer of length at least "+t)}function $ee(r){return new Uint32Array(r.buffer,r.byteOffset,Math.floor(r.byteLength/4))}function ln(...r){for(let e=0;e<r.length;e++)r[e].fill(0)}function _B(r){return new DataView(r.buffer,r.byteOffset,r.byteLength)}function rs(r,e){return r<<32-e|r>>>e}function Da(r,e){return r<<e|r>>>32-e>>>0}function Yee(r){return r<<24&4278190080|r<<8&16711680|r>>>8&65280|r>>>24&255}function Wee(r){for(let e=0;e<r.length;e++)r[e]=Yee(r[e]);return r}function zc(r){if(ps(r),cR)return r.toHex();let e="";for(let t=0;t<r.length;t++)e+=Xee[r[t]];return e}function CT(r){if(r>=Zs._0&&r<=Zs._9)return r-Zs._0;if(r>=Zs.A&&r<=Zs.F)return r-(Zs.A-10);if(r>=Zs.a&&r<=Zs.f)return r-(Zs.a-10)}function QI(r){if(typeof r!="string")throw new Error("hex string expected, got "+typeof r);if(cR)return Uint8Array.fromHex(r);let e=r.length,t=e/2;if(e%2)throw new Error("hex string expected, got unpadded hex of length "+e);let i=new Uint8Array(t);for(let n=0,s=0;n<t;n++,s+=2){let o=CT(r.charCodeAt(s)),a=CT(r.charCodeAt(s+1));if(o===void 0||a===void 0){let c=r[s]+r[s+1];throw new Error('hex string expected, got non-hex character "'+c+'" at index '+s)}i[n]=o*16+a}return i}function Zee(r){if(typeof r!="string")throw new Error("string expected");return new Uint8Array(new TextEncoder().encode(r))}function FA(r){return typeof r=="string"&&(r=Zee(r)),ps(r),r}function Dn(...r){let e=0;for(let i=0;i<r.length;i++){let n=r[i];ps(n),e+=n.length}let t=new Uint8Array(e);for(let i=0,n=0;i<r.length;i++){let s=r[i];t.set(s,n),n+=s.length}return t}function po(r){let e=i=>r().update(FA(i)).digest(),t=r();return e.outputLen=t.outputLen,e.blockLen=t.blockLen,e.create=()=>r(),e}function ete(r){let e=(i,n)=>r(n).update(FA(i)).digest(),t=r({});return e.outputLen=t.outputLen,e.blockLen=t.blockLen,e.create=i=>r(i),e}function nw(r=32){if(pd&&typeof pd.getRandomValues=="function")return pd.getRandomValues(new Uint8Array(r));if(pd&&typeof pd.randomBytes=="function")return Uint8Array.from(pd.randomBytes(r));throw new Error("crypto.getRandomValues must be defined")}function Yc(r,e=""){if(typeof r!="boolean"){let t=e&&`"${e}"`;throw new Error(t+"expected boolean, got type="+typeof r)}return r}function _n(r,e,t=""){let i=_A(r),n=r?.length,s=e!==void 0;if(!i||s&&n!==e){let o=t&&`"${t}" `,a=s?` of length ${e}`:"",c=i?`length=${n}`:`type=${typeof r}`;throw new Error(o+"expected Uint8Array"+a+", got "+c)}return r}function Oy(r){let e=r.toString(16);return e.length&1?"0"+e:e}function lR(r){if(typeof r!="string")throw new Error("hex string expected, got "+typeof r);return r===""?ib:BigInt("0x"+r)}function sw(r){return lR(zc(r))}function Wc(r){return ps(r),lR(zc(Uint8Array.from(r).reverse()))}function nb(r,e){return QI(r.toString(16).padStart(e*2,"0"))}function sb(r,e){return nb(r,e).reverse()}function vt(r,e,t){let i;if(typeof e=="string")try{i=QI(e)}catch(s){throw new Error(r+" must be hex string or Uint8Array, cause: "+s)}else if(_A(e))i=Uint8Array.from(e);else throw new Error(r+" must be hex string or Uint8Array");let n=i.length;if(typeof t=="number"&&n!==t)throw new Error(r+" of length "+t+" expected, got "+n);return i}function ET(r){return Uint8Array.from(r)}function rte(r){return Uint8Array.from(r,(e,t)=>{let i=e.charCodeAt(0);if(e.length!==1||i>127)throw new Error(`string contains non-ASCII character "${r[t]}" with code ${i} at position ${t}`);return i})}function ite(r,e,t){return FB(r)&&FB(e)&&FB(t)&&e<=r&&r<t}function pA(r,e,t,i){if(!ite(e,t,i))throw new Error("expected valid "+r+": "+t+" <= n < "+i+", got "+e)}function dR(r){let e;for(e=0;r>ib;r>>=y1,e+=1);return e}function nte(r,e,t){if(typeof r!="number"||r<2)throw new Error("hashLen must be a number");if(typeof e!="number"||e<2)throw new Error("qByteLen must be a number");if(typeof t!="function")throw new Error("hmacFn must be a function");let i=h=>new Uint8Array(h),n=h=>Uint8Array.of(h),s=i(r),o=i(r),a=0,c=()=>{s.fill(1),o.fill(0),a=0},l=(...h)=>t(o,s,...h),d=(h=i(0))=>{o=l(n(0),h),s=l(),h.length!==0&&(o=l(n(1),h),s=l())},u=()=>{if(a++>=1e3)throw new Error("drbg: tried 1000 values");let h=0,g=[];for(;h<e;){s=l();let p=s.slice();g.push(p),h+=s.length}return Dn(...g)};return(h,g)=>{c(),d(h);let p;for(;!(p=g(u()));)d();return c(),p}}function zd(r,e,t={}){if(!r||typeof r!="object")throw new Error("expected valid options object");function i(n,s,o){let a=r[n];if(o&&a===void 0)return;let c=typeof a;if(c!==s||a===null)throw new Error(`param "${n}" is invalid: expected ${s}, got ${c}`)}Object.entries(e).forEach(([n,s])=>i(n,s,!1)),Object.entries(t).forEach(([n,s])=>i(n,s,!0))}function _I(r){let e=new WeakMap;return(t,...i)=>{let n=e.get(t);if(n!==void 0)return n;let s=r(t,...i);return e.set(t,s),s}}function mr(r,e){let t=r%e;return t>=Ei?t:e+t}function bt(r,e,t){let i=r;for(;e-- >Ei;)i*=i,i%=t;return i}function BT(r,e){if(r===Ei)throw new Error("invert: expected non-zero number");if(e<=Ei)throw new Error("invert: expected positive modulus, got "+e);let t=mr(r,e),i=e,n=Ei,s=fi;for(;t!==Ei;){let a=i/t,c=i%t,l=n-s*a;i=t,t=c,n=s,s=l}if(i!==fi)throw new Error("invert: does not exist");return mr(n,e)}function ob(r,e,t){if(!r.eql(r.sqr(e),t))throw new Error("Cannot find square root")}function pR(r,e){let t=(r.ORDER+fi)/fR,i=r.pow(e,t);return ob(r,i,e),i}function ate(r,e){let t=(r.ORDER-AR)/hR,i=r.mul(e,Lc),n=r.pow(i,t),s=r.mul(e,n),o=r.mul(r.mul(s,Lc),n),a=r.mul(s,r.sub(o,r.ONE));return ob(r,a,e),a}function cte(r){let e=Qi(r),t=mR(r),i=t(e,e.neg(e.ONE)),n=t(e,i),s=t(e,e.neg(i)),o=(r+ste)/gR;return(a,c)=>{let l=a.pow(c,o),d=a.mul(l,i),u=a.mul(l,n),f=a.mul(l,s),h=a.eql(a.sqr(d),c),g=a.eql(a.sqr(u),c);l=a.cmov(l,d,h),d=a.cmov(f,u,g);let p=a.eql(a.sqr(d),c),I=a.cmov(l,d,p);return ob(a,I,c),I}}function mR(r){if(r<uR)throw new Error("sqrt is not defined for small field");let e=r-fi,t=0;for(;e%Lc===Ei;)e/=Lc,t++;let i=Lc,n=Qi(r);for(;bT(n,i)===1;)if(i++>1e3)throw new Error("Cannot find square root: probably non-prime P");if(t===1)return pR;let s=n.pow(i,e),o=(e+fi)/Lc;return function(c,l){if(c.is0(l))return l;if(bT(c,l)!==1)throw new Error("Cannot find square root");let d=t,u=c.mul(c.ONE,s),f=c.pow(l,e),h=c.pow(l,o);for(;!c.eql(f,c.ONE);){if(c.is0(f))return c.ZERO;let g=1,p=c.sqr(f);for(;!c.eql(p,c.ONE);)if(g++,p=c.sqr(p),g===d)throw new Error("Cannot find square root");let I=fi<<BigInt(d-g-1),m=c.pow(u,I);d=g,u=c.sqr(m),f=c.mul(f,u),h=c.mul(h,m)}return h}}function lte(r){return r%fR===uR?pR:r%hR===AR?ate:r%gR===ote?cte(r):mR(r)}function ute(r){let e={ORDER:"bigint",MASK:"bigint",BYTES:"number",BITS:"number"},t=dte.reduce((i,n)=>(i[n]="function",i),e);return zd(r,t),r}function fte(r,e,t){if(t<Ei)throw new Error("invalid exponent, negatives unsupported");if(t===Ei)return r.ONE;if(t===fi)return e;let i=r.ONE,n=e;for(;t>Ei;)t&fi&&(i=r.mul(i,n)),n=r.sqr(n),t>>=fi;return i}function yR(r,e,t=!1){let i=new Array(e.length).fill(t?r.ZERO:void 0),n=e.reduce((o,a,c)=>r.is0(a)?o:(i[c]=o,r.mul(o,a)),r.ONE),s=r.inv(n);return e.reduceRight((o,a,c)=>r.is0(a)?o:(i[c]=r.mul(o,i[c]),r.mul(o,a)),s),i}function bT(r,e){let t=(r.ORDER-fi)/Lc,i=r.pow(e,t),n=r.eql(i,r.ONE),s=r.eql(i,r.ZERO),o=r.eql(i,r.neg(r.ONE));if(!n&&!s&&!o)throw new Error("invalid Legendre symbol result");return n?1:s?0:-1}function IR(r,e){e!==void 0&&hA(e);let t=e!==void 0?e:r.toString(2).length,i=Math.ceil(t/8);return{nBitLength:t,nByteLength:i}}function Qi(r,e,t=!1,i={}){if(r<=Ei)throw new Error("invalid field: expected ORDER > 0, got "+r);let n,s,o=!1,a;if(typeof e=="object"&&e!=null){if(i.sqrt||t)throw new Error("cannot specify opts in two arguments");let f=e;f.BITS&&(n=f.BITS),f.sqrt&&(s=f.sqrt),typeof f.isLE=="boolean"&&(t=f.isLE),typeof f.modFromBytes=="boolean"&&(o=f.modFromBytes),a=f.allowedLengths}else typeof e=="number"&&(n=e),i.sqrt&&(s=i.sqrt);let{nBitLength:c,nByteLength:l}=IR(r,n);if(l>2048)throw new Error("invalid field: expected ORDER of <= 2048 bytes");let d,u=Object.freeze({ORDER:r,isLE:t,BITS:c,BYTES:l,MASK:PA(c),ZERO:Ei,ONE:fi,allowedLengths:a,create:f=>mr(f,r),isValid:f=>{if(typeof f!="bigint")throw new Error("invalid field element: expected bigint, got "+typeof f);return Ei<=f&&f<r},is0:f=>f===Ei,isValidNot0:f=>!u.is0(f)&&u.isValid(f),isOdd:f=>(f&fi)===fi,neg:f=>mr(-f,r),eql:(f,h)=>f===h,sqr:f=>mr(f*f,r),add:(f,h)=>mr(f+h,r),sub:(f,h)=>mr(f-h,r),mul:(f,h)=>mr(f*h,r),pow:(f,h)=>fte(u,f,h),div:(f,h)=>mr(f*BT(h,r),r),sqrN:f=>f*f,addN:(f,h)=>f+h,subN:(f,h)=>f-h,mulN:(f,h)=>f*h,inv:f=>BT(f,r),sqrt:s||(f=>(d||(d=lte(r)),d(u,f))),toBytes:f=>t?sb(f,l):nb(f,l),fromBytes:(f,h=!0)=>{if(a){if(!a.includes(f.length)||f.length>l)throw new Error("Field.fromBytes: expected "+a+" bytes, got "+f.length);let p=new Uint8Array(l);p.set(f,t?0:p.length-f.length),f=p}if(f.length!==l)throw new Error("Field.fromBytes: expected "+l+" bytes, got "+f.length);let g=t?Wc(f):sw(f);if(o&&(g=mr(g,r)),!h&&!u.isValid(g))throw new Error("invalid field element: outside of range 0..ORDER");return g},invertBatch:f=>yR(u,f),cmov:(f,h,g)=>g?h:f});return Object.freeze(u)}function wR(r){if(typeof r!="bigint")throw new Error("field order must be bigint");let e=r.toString(2).length;return Math.ceil(e/8)}function xR(r){let e=wR(r);return e+Math.ceil(e/2)}function Ate(r,e,t=!1){let i=r.length,n=wR(e),s=xR(e);if(i<16||i<s||i>1024)throw new Error("expected "+s+"-1024 bytes of input, got "+i);let o=t?Wc(r):sw(r),a=mr(o,e-fi)+fi;return t?sb(a,n):nb(a,n)}function hte(r,e,t,i){if(typeof r.setBigUint64=="function")return r.setBigUint64(e,t,i);let n=BigInt(32),s=BigInt(4294967295),o=Number(t>>n&s),a=Number(t&s),c=i?4:0,l=i?0:4;r.setUint32(e+c,o,i),r.setUint32(e+l,a,i)}function CR(r,e,t){return r&e^~r&t}function ER(r,e,t){return r&e^r&t^e&t}function gte(r,e=!1){return e?{h:Number(r&My),l:Number(r>>vT&My)}:{h:Number(r>>vT&My)|0,l:Number(r&My)|0}}function BR(r,e=!1){let t=r.length,i=new Uint32Array(t),n=new Uint32Array(t);for(let s=0;s<t;s++){let{h:o,l:a}=gte(r[s],e);[i[s],n[s]]=[o,a]}return[i,n]}function eo(r,e,t,i){let n=(e>>>0)+(i>>>0);return{h:r+t+(n/2**32|0)|0,l:n|0}}function UI(r,e){let t=e.negate();return r?t:e}function Kc(r,e){let t=yR(r.Fp,e.map(i=>i.Z));return e.map((i,n)=>r.fromAffine(i.toAffine(t[n])))}function DR(r,e){if(!Number.isSafeInteger(r)||r<=0||r>e)throw new Error("invalid window size, expected [1.."+e+"], got W="+r)}function PB(r,e){DR(r,e);let t=Math.ceil(e/r)+1,i=2**(r-1),n=2**r,s=PA(r),o=BigInt(r);return{windows:t,windowSize:i,mask:s,maxNumber:n,shiftBy:o}}function DT(r,e,t){let{windowSize:i,mask:n,maxNumber:s,shiftBy:o}=t,a=Number(r&n),c=r>>o;a>i&&(a-=s,c+=Hc);let l=e*i,d=l+Math.abs(a)-1,u=a===0,f=a<0,h=e%2!==0;return{nextN:c,offset:d,isZero:u,isNeg:f,isNegF:h,offsetF:l}}function Qte(r,e){if(!Array.isArray(r))throw new Error("array expected");r.forEach((t,i)=>{if(!(t instanceof e))throw new Error("invalid point at index "+i)})}function _te(r,e){if(!Array.isArray(r))throw new Error("array of scalars expected");r.forEach((t,i)=>{if(!e.isValid(t))throw new Error("invalid scalar at index "+i)})}function UB(r){return QR.get(r)||1}function QT(r){if(r!==Hd)throw new Error("invalid wNAF")}function Fte(r,e,t,i){let n=e,s=r.ZERO,o=r.ZERO;for(;t>Hd||i>Hd;)t&Hc&&(s=s.add(n)),i&Hc&&(o=o.add(n)),n=n.double(),t>>=Hc,i>>=Hc;return{p1:s,p2:o}}function _R(r,e,t,i){Qte(t,r),_te(i,e);let n=t.length,s=i.length;if(n!==s)throw new Error("arrays of points and scalars must have equal length");let o=r.ZERO,a=dR(BigInt(n)),c=1;a>12?c=a-3:a>4?c=a-2:a>0&&(c=2);let l=PA(c),d=new Array(Number(l)+1).fill(o),u=Math.floor((e.BITS-1)/c)*c,f=o;for(let h=u;h>=0;h-=c){d.fill(o);for(let p=0;p<s;p++){let I=i[p],m=Number(I>>BigInt(h)&l);d[m]=d[m].add(t[p])}let g=o;for(let p=d.length-1,I=o;p>0;p--)I=I.add(d[p]),g=g.add(I);if(f=f.add(g),h!==0)for(let p=0;p<c;p++)f=f.double()}return f}function _T(r,e,t){if(e){if(e.ORDER!==r)throw new Error("Field.ORDER must match order: Fp == p, Fn == n");return ute(e),e}else return Qi(r,{isLE:t})}function FR(r,e,t={},i){if(i===void 0&&(i=r==="edwards"),!e||typeof e!="object")throw new Error(`expected valid ${r} CURVE object`);for(let c of["p","n","h"]){let l=e[c];if(!(typeof l=="bigint"&&l>Hd))throw new Error(`CURVE.${c} must be positive bigint`)}let n=_T(e.p,t.Fp,i),s=_T(e.n,t.Fn,i),a=["Gx","Gy","a",r==="weierstrass"?"b":"d"];for(let c of a)if(!n.isValid(e[c]))throw new Error(`CURVE.${c} must be valid field element of CURVE.Fp`);return e=Object.freeze(Object.assign({},e)),{CURVE:e,Fp:n,Fn:s}}function Pte(r,e,t){let[[i,n],[s,o]]=e,a=FT(o*r,t),c=FT(-n*r,t),l=r-a*i-c*s,d=-a*n-c*o,u=l<ao,f=d<ao;u&&(l=-l),f&&(d=-d);let h=PA(Math.ceil(dR(t)/2))+Fd;if(l<ao||l>=h||d<ao||d>=h)throw new Error("splitScalar (endomorphism): failed, k="+r);return{k1neg:u,k1:l,k2neg:f,k2:d}}function x1(r){if(!["compact","recovered","der"].includes(r))throw new Error('Signature format must be "compact", "recovered", or "der"');return r}function RB(r,e){let t={};for(let i of Object.keys(e))t[i]=r[i]===void 0?e[i]:r[i];return Yc(t.lowS,"lowS"),Yc(t.prehash,"prehash"),t.format!==void 0&&x1(t.format),t}function bd(r,e){let{BYTES:t}=r,i;if(typeof e=="bigint")i=e;else{let n=vt("private key",e);try{i=r.fromBytes(n)}catch{throw new Error(`invalid private key: expected ui8a of size ${t}, got ${typeof e}`)}}if(!r.isValidNot0(i))throw new Error("invalid private key: out of range [1..N-1]");return i}function Ute(r,e={}){let t=FR("weierstrass",r,e),{Fp:i,Fn:n}=t,s=t.CURVE,{h:o,n:a}=s;zd(e,{},{allowInfinityPoint:"boolean",clearCofactor:"function",isTorsionFree:"function",fromBytes:"function",toBytes:"function",endo:"object",wrapPrivateKey:"boolean"});let{endo:c}=e;if(c&&(!i.is0(s.a)||typeof c.beta!="bigint"||!Array.isArray(c.basises)))throw new Error('invalid endo: expected "beta": bigint and "basises": array');let l=UR(i,n);function d(){if(!i.isOdd)throw new Error("compression is not supported: Field does not have .isOdd()")}function u(_,v,E){let{x:P,y:N}=v.toAffine(),j=i.toBytes(P);if(Yc(E,"isCompressed"),E){d();let z=!i.isOdd(N);return Dn(TR(z),j)}else return Dn(Uint8Array.of(4),j,i.toBytes(N))}function f(_){_n(_,void 0,"Point");let{publicKey:v,publicKeyUncompressed:E}=l,P=_.length,N=_[0],j=_.subarray(1);if(P===v&&(N===2||N===3)){let z=i.fromBytes(j);if(!i.isValid(z))throw new Error("bad point: is not on curve, wrong x");let L=p(z),G;try{G=i.sqrt(L)}catch(te){let re=te instanceof Error?": "+te.message:"";throw new Error("bad point: is not on curve, sqrt error"+re)}d();let K=i.isOdd(G);return(N&1)===1!==K&&(G=i.neg(G)),{x:z,y:G}}else if(P===E&&N===4){let z=i.BYTES,L=i.fromBytes(j.subarray(0,z)),G=i.fromBytes(j.subarray(z,z*2));if(!I(L,G))throw new Error("bad point: is not on curve");return{x:L,y:G}}else throw new Error(`bad point: got length ${P}, expected compressed=${v} or uncompressed=${E}`)}let h=e.toBytes||u,g=e.fromBytes||f;function p(_){let v=i.sqr(_),E=i.mul(v,_);return i.add(i.add(E,i.mul(_,s.a)),s.b)}function I(_,v){let E=i.sqr(v),P=p(_);return i.eql(E,P)}if(!I(s.Gx,s.Gy))throw new Error("bad curve params: generator point");let m=i.mul(i.pow(s.a,Ky),Tte),w=i.mul(i.sqr(s.b),BigInt(27));if(i.is0(i.add(m,w)))throw new Error("bad curve params: a or b");function B(_,v,E=!1){if(!i.isValid(v)||E&&i.is0(v))throw new Error(`bad point coordinate ${_}`);return v}function C(_){if(!(_ instanceof T))throw new Error("ProjectivePoint expected")}function x(_){if(!c||!c.basises)throw new Error("no endo");return Pte(_,c.basises,n.ORDER)}let Q=_I((_,v)=>{let{X:E,Y:P,Z:N}=_;if(i.eql(N,i.ONE))return{x:E,y:P};let j=_.is0();v==null&&(v=j?i.ONE:i.inv(N));let z=i.mul(E,v),L=i.mul(P,v),G=i.mul(N,v);if(j)return{x:i.ZERO,y:i.ZERO};if(!i.eql(G,i.ONE))throw new Error("invZ was invalid");return{x:z,y:L}}),R=_I(_=>{if(_.is0()){if(e.allowInfinityPoint&&!i.is0(_.Y))return;throw new Error("bad point: ZERO")}let{x:v,y:E}=_.toAffine();if(!i.isValid(v)||!i.isValid(E))throw new Error("bad point: x or y not field elements");if(!I(v,E))throw new Error("bad point: equation left != right");if(!_.isTorsionFree())throw new Error("bad point: not in prime-order subgroup");return!0});function U(_,v,E,P,N){return E=new T(i.mul(E.X,_),E.Y,E.Z),v=UI(P,v),E=UI(N,E),v.add(E)}class T{constructor(v,E,P){this.X=B("x",v),this.Y=B("y",E,!0),this.Z=B("z",P),Object.freeze(this)}static CURVE(){return s}static fromAffine(v){let{x:E,y:P}=v||{};if(!v||!i.isValid(E)||!i.isValid(P))throw new Error("invalid affine point");if(v instanceof T)throw new Error("projective point not allowed");return i.is0(E)&&i.is0(P)?T.ZERO:new T(E,P,i.ONE)}static fromBytes(v){let E=T.fromAffine(g(_n(v,void 0,"point")));return E.assertValidity(),E}static fromHex(v){return T.fromBytes(vt("pointHex",v))}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(v=8,E=!0){return b.createCache(this,v),E||this.multiply(Ky),this}assertValidity(){R(this)}hasEvenY(){let{y:v}=this.toAffine();if(!i.isOdd)throw new Error("Field doesn't support isOdd");return!i.isOdd(v)}equals(v){C(v);let{X:E,Y:P,Z:N}=this,{X:j,Y:z,Z:L}=v,G=i.eql(i.mul(E,L),i.mul(j,N)),K=i.eql(i.mul(P,L),i.mul(z,N));return G&&K}negate(){return new T(this.X,i.neg(this.Y),this.Z)}double(){let{a:v,b:E}=s,P=i.mul(E,Ky),{X:N,Y:j,Z:z}=this,L=i.ZERO,G=i.ZERO,K=i.ZERO,$=i.mul(N,N),te=i.mul(j,j),re=i.mul(z,z),ee=i.mul(N,j);return ee=i.add(ee,ee),K=i.mul(N,z),K=i.add(K,K),L=i.mul(v,K),G=i.mul(P,re),G=i.add(L,G),L=i.sub(te,G),G=i.add(te,G),G=i.mul(L,G),L=i.mul(ee,L),K=i.mul(P,K),re=i.mul(v,re),ee=i.sub($,re),ee=i.mul(v,ee),ee=i.add(ee,K),K=i.add($,$),$=i.add(K,$),$=i.add($,re),$=i.mul($,ee),G=i.add(G,$),re=i.mul(j,z),re=i.add(re,re),$=i.mul(re,ee),L=i.sub(L,$),K=i.mul(re,te),K=i.add(K,K),K=i.add(K,K),new T(L,G,K)}add(v){C(v);let{X:E,Y:P,Z:N}=this,{X:j,Y:z,Z:L}=v,G=i.ZERO,K=i.ZERO,$=i.ZERO,te=s.a,re=i.mul(s.b,Ky),ee=i.mul(E,j),Z=i.mul(P,z),me=i.mul(N,L),le=i.add(E,P),Ae=i.add(j,z);le=i.mul(le,Ae),Ae=i.add(ee,Z),le=i.sub(le,Ae),Ae=i.add(E,N);let ve=i.add(j,L);return Ae=i.mul(Ae,ve),ve=i.add(ee,me),Ae=i.sub(Ae,ve),ve=i.add(P,N),G=i.add(z,L),ve=i.mul(ve,G),G=i.add(Z,me),ve=i.sub(ve,G),$=i.mul(te,Ae),G=i.mul(re,me),$=i.add(G,$),G=i.sub(Z,$),$=i.add(Z,$),K=i.mul(G,$),Z=i.add(ee,ee),Z=i.add(Z,ee),me=i.mul(te,me),Ae=i.mul(re,Ae),Z=i.add(Z,me),me=i.sub(ee,me),me=i.mul(te,me),Ae=i.add(Ae,me),ee=i.mul(Z,Ae),K=i.add(K,ee),ee=i.mul(ve,Ae),G=i.mul(le,G),G=i.sub(G,ee),ee=i.mul(le,Z),$=i.mul(ve,$),$=i.add($,ee),new T(G,K,$)}subtract(v){return this.add(v.negate())}is0(){return this.equals(T.ZERO)}multiply(v){let{endo:E}=e;if(!n.isValidNot0(v))throw new Error("invalid scalar: out of range");let P,N,j=z=>b.cached(this,z,L=>Kc(T,L));if(E){let{k1neg:z,k1:L,k2neg:G,k2:K}=x(v),{p:$,f:te}=j(L),{p:re,f:ee}=j(K);N=te.add(ee),P=U(E.beta,$,re,z,G)}else{let{p:z,f:L}=j(v);P=z,N=L}return Kc(T,[P,N])[0]}multiplyUnsafe(v){let{endo:E}=e,P=this;if(!n.isValid(v))throw new Error("invalid scalar: out of range");if(v===ao||P.is0())return T.ZERO;if(v===Fd)return P;if(b.hasCache(this))return this.multiply(v);if(E){let{k1neg:N,k1:j,k2neg:z,k2:L}=x(v),{p1:G,p2:K}=Fte(T,P,j,L);return U(E.beta,G,K,N,z)}else return b.unsafe(P,v)}multiplyAndAddUnsafe(v,E,P){let N=this.multiplyUnsafe(E).add(v.multiplyUnsafe(P));return N.is0()?void 0:N}toAffine(v){return Q(this,v)}isTorsionFree(){let{isTorsionFree:v}=e;return o===Fd?!0:v?v(T,this):b.unsafe(this,a).is0()}clearCofactor(){let{clearCofactor:v}=e;return o===Fd?this:v?v(T,this):this.multiplyUnsafe(o)}isSmallOrder(){return this.multiplyUnsafe(o).is0()}toBytes(v=!0){return Yc(v,"isCompressed"),this.assertValidity(),h(T,this,v)}toHex(v=!0){return zc(this.toBytes(v))}toString(){return`<Point ${this.is0()?"ZERO":this.toHex()}>`}get px(){return this.X}get py(){return this.X}get pz(){return this.Z}toRawBytes(v=!0){return this.toBytes(v)}_setWindowSize(v){this.precompute(v)}static normalizeZ(v){return Kc(T,v)}static msm(v,E){return _R(T,n,v,E)}static fromPrivateKey(v){return T.BASE.multiply(bd(n,v))}}T.BASE=new T(s.Gx,s.Gy,i.ONE),T.ZERO=new T(i.ZERO,i.ONE,i.ZERO),T.Fp=i,T.Fn=n;let D=n.BITS,b=new RI(T,e.endo?Math.ceil(D/2):D);return T.BASE.precompute(8),T}function TR(r){return Uint8Array.of(r?2:3)}function UR(r,e){return{secretKey:e.BYTES,publicKey:1+r.BYTES,publicKeyUncompressed:1+2*r.BYTES,publicKeyHasPrefix:!0,signature:2*e.BYTES}}function Rte(r,e={}){let{Fn:t}=r,i=e.randomBytes||nw,n=Object.assign(UR(r.Fp,t),{seed:xR(t.ORDER)});function s(h){try{return!!bd(t,h)}catch{return!1}}function o(h,g){let{publicKey:p,publicKeyUncompressed:I}=n;try{let m=h.length;return g===!0&&m!==p||g===!1&&m!==I?!1:!!r.fromBytes(h)}catch{return!1}}function a(h=i(n.seed)){return Ate(_n(h,n.seed,"seed"),t.ORDER)}function c(h,g=!0){return r.BASE.multiply(bd(t,h)).toBytes(g)}function l(h){let g=a(h);return{secretKey:g,publicKey:c(g)}}function d(h){if(typeof h=="bigint")return!1;if(h instanceof r)return!0;let{secretKey:g,publicKey:p,publicKeyUncompressed:I}=n;if(t.allowedLengths||g===p)return;let m=vt("key",h).length;return m===p||m===I}function u(h,g,p=!0){if(d(h)===!0)throw new Error("first arg must be private key");if(d(g)===!1)throw new Error("second arg must be public key");let I=bd(t,h);return r.fromHex(g).multiply(I).toBytes(p)}return Object.freeze({getPublicKey:c,getSharedSecret:u,keygen:l,Point:r,utils:{isValidSecretKey:s,isValidPublicKey:o,randomSecretKey:a,isValidPrivateKey:s,randomPrivateKey:a,normPrivateKeyToScalar:h=>bd(t,h),precompute(h=8,g=r.BASE){return g.precompute(h,!1)}},lengths:n})}function Nte(r,e,t={}){oR(e),zd(t,{},{hmac:"function",lowS:"boolean",randomBytes:"function",bits2int:"function",bits2int_modN:"function"});let i=t.randomBytes||nw,n=t.hmac||((E,...P)=>SR(e,E,Dn(...P))),{Fp:s,Fn:o}=r,{ORDER:a,BITS:c}=o,{keygen:l,getPublicKey:d,getSharedSecret:u,utils:f,lengths:h}=Rte(r,t),g={prehash:!1,lowS:typeof t.lowS=="boolean"?t.lowS:!1,format:void 0,extraEntropy:!1},p="compact";function I(E){let P=a>>Fd;return E>P}function m(E,P){if(!o.isValidNot0(P))throw new Error(`invalid signature ${E}: out of range 1..Point.Fn.ORDER`);return P}function w(E,P){x1(P);let N=h.signature,j=P==="compact"?N:P==="recovered"?N+1:void 0;return _n(E,j,`${P} signature`)}class B{constructor(P,N,j){this.r=m("r",P),this.s=m("s",N),j!=null&&(this.recovery=j),Object.freeze(this)}static fromBytes(P,N=p){w(P,N);let j;if(N==="der"){let{r:K,s:$}=so.toSig(_n(P));return new B(K,$)}N==="recovered"&&(j=P[0],N="compact",P=P.subarray(1));let z=o.BYTES,L=P.subarray(0,z),G=P.subarray(z,z*2);return new B(o.fromBytes(L),o.fromBytes(G),j)}static fromHex(P,N){return this.fromBytes(QI(P),N)}addRecoveryBit(P){return new B(this.r,this.s,P)}recoverPublicKey(P){let N=s.ORDER,{r:j,s:z,recovery:L}=this;if(L==null||![0,1,2,3].includes(L))throw new Error("recovery id invalid");if(a*PR<N&&L>1)throw new Error("recovery id is ambiguous for h>1 curve");let K=L===2||L===3?j+a:j;if(!s.isValid(K))throw new Error("recovery id 2 or 3 invalid");let $=s.toBytes(K),te=r.fromBytes(Dn(TR((L&1)===0),$)),re=o.inv(K),ee=x(vt("msgHash",P)),Z=o.create(-ee*re),me=o.create(z*re),le=r.BASE.multiplyUnsafe(Z).add(te.multiplyUnsafe(me));if(le.is0())throw new Error("point at infinify");return le.assertValidity(),le}hasHighS(){return I(this.s)}toBytes(P=p){if(x1(P),P==="der")return QI(so.hexFromSig(this));let N=o.toBytes(this.r),j=o.toBytes(this.s);if(P==="recovered"){if(this.recovery==null)throw new Error("recovery bit must be present");return Dn(Uint8Array.of(this.recovery),N,j)}return Dn(N,j)}toHex(P){return zc(this.toBytes(P))}assertValidity(){}static fromCompact(P){return B.fromBytes(vt("sig",P),"compact")}static fromDER(P){return B.fromBytes(vt("sig",P),"der")}normalizeS(){return this.hasHighS()?new B(this.r,o.neg(this.s),this.recovery):this}toDERRawBytes(){return this.toBytes("der")}toDERHex(){return zc(this.toBytes("der"))}toCompactRawBytes(){return this.toBytes("compact")}toCompactHex(){return zc(this.toBytes("compact"))}}let C=t.bits2int||function(P){if(P.length>8192)throw new Error("input is too large");let N=sw(P),j=P.length*8-c;return j>0?N>>BigInt(j):N},x=t.bits2int_modN||function(P){return o.create(C(P))},Q=PA(c);function R(E){return pA("num < 2^"+c,E,ao,Q),o.toBytes(E)}function U(E,P){return _n(E,void 0,"message"),P?_n(e(E),void 0,"prehashed message"):E}function T(E,P,N){if(["recovered","canonical"].some(Z=>Z in N))throw new Error("sign() legacy options not supported");let{lowS:j,prehash:z,extraEntropy:L}=RB(N,g);E=U(E,z);let G=x(E),K=bd(o,P),$=[R(K),R(G)];if(L!=null&&L!==!1){let Z=L===!0?i(h.secretKey):L;$.push(vt("extraEntropy",Z))}let te=Dn(...$),re=G;function ee(Z){let me=C(Z);if(!o.isValidNot0(me))return;let le=o.inv(me),Ae=r.BASE.multiply(me).toAffine(),ve=o.create(Ae.x);if(ve===ao)return;let qe=o.create(le*o.create(re+ve*K));if(qe===ao)return;let _e=(Ae.x===ve?0:2)|Number(Ae.y&Fd),se=qe;return j&&I(qe)&&(se=o.neg(qe),_e^=1),new B(ve,se,_e)}return{seed:te,k2sig:ee}}function D(E,P,N={}){E=vt("message",E);let{seed:j,k2sig:z}=T(E,P,N);return nte(e.outputLen,o.BYTES,n)(j,z)}function b(E){let P,N=typeof E=="string"||_A(E),j=!N&&E!==null&&typeof E=="object"&&typeof E.r=="bigint"&&typeof E.s=="bigint";if(!N&&!j)throw new Error("invalid signature, expected Uint8Array, hex string or Signature instance");if(j)P=new B(E.r,E.s);else if(N){try{P=B.fromBytes(vt("sig",E),"der")}catch(z){if(!(z instanceof so.Err))throw z}if(!P)try{P=B.fromBytes(vt("sig",E),"compact")}catch{return!1}}return P||!1}function _(E,P,N,j={}){let{lowS:z,prehash:L,format:G}=RB(j,g);if(N=vt("publicKey",N),P=U(vt("message",P),L),"strict"in j)throw new Error("options.strict was renamed to lowS");let K=G===void 0?b(E):B.fromBytes(vt("sig",E),G);if(K===!1)return!1;try{let $=r.fromBytes(N);if(z&&K.hasHighS())return!1;let{r:te,s:re}=K,ee=x(P),Z=o.inv(re),me=o.create(ee*Z),le=o.create(te*Z),Ae=r.BASE.multiplyUnsafe(me).add($.multiplyUnsafe(le));return Ae.is0()?!1:o.create(Ae.x)===te}catch{return!1}}function v(E,P,N={}){let{prehash:j}=RB(N,g);return P=U(P,j),B.fromBytes(E,"recovered").recoverPublicKey(P).toBytes()}return Object.freeze({keygen:l,getPublicKey:d,getSharedSecret:u,utils:f,lengths:h,Point:r,sign:D,verify:_,recoverPublicKey:v,Signature:B,hash:e})}function qte(r){let e={a:r.a,b:r.b,p:r.Fp.ORDER,n:r.n,h:r.h,Gx:r.Gx,Gy:r.Gy},t=r.Fp,i=r.allowedPrivateKeyLengths?Array.from(new Set(r.allowedPrivateKeyLengths.map(o=>Math.ceil(o/2)))):void 0,n=Qi(e.n,{BITS:r.nBitLength,allowedLengths:i,modFromBytes:r.wrapPrivateKey}),s={Fp:t,Fn:n,allowInfinityPoint:r.allowInfinityPoint,endo:r.endo,isTorsionFree:r.isTorsionFree,clearCofactor:r.clearCofactor,fromBytes:r.fromBytes,toBytes:r.toBytes};return{CURVE:e,curveOpts:s}}function Ote(r){let{CURVE:e,curveOpts:t}=qte(r),i={hmac:r.hmac,randomBytes:r.randomBytes,lowS:r.lowS,bits2int:r.bits2int,bits2int_modN:r.bits2int_modN};return{CURVE:e,curveOpts:t,hash:r.hash,ecdsaOpts:i}}function Mte(r,e){let t=e.Point;return Object.assign({},e,{ProjectivePoint:t,CURVE:Object.assign({},r,IR(t.Fn.ORDER,t.Fn.BITS))})}function Lte(r){let{CURVE:e,curveOpts:t,hash:i,ecdsaOpts:n}=Ote(r),s=Ute(e,t),o=Nte(s,i,n);return Mte(r,o)}function tl(r,e){let t=i=>Lte({...r,hash:i});return{...t(e),create:t}}function nre(r,e=24){let t=new Uint32Array(10);for(let i=24-e;i<24;i++){for(let o=0;o<10;o++)t[o]=r[o]^r[o+10]^r[o+20]^r[o+30]^r[o+40];for(let o=0;o<10;o+=2){let a=(o+8)%10,c=(o+2)%10,l=t[c],d=t[c+1],u=PT(l,d,1)^t[a],f=TT(l,d,1)^t[a+1];for(let h=0;h<50;h+=10)r[o+h]^=u,r[o+h+1]^=f}let n=r[2],s=r[3];for(let o=0;o<24;o++){let a=MR[o],c=PT(n,s,a),l=TT(n,s,a),d=OR[o];n=r[d],s=r[d+1],r[d]=c,r[d+1]=l}for(let o=0;o<50;o+=10){for(let a=0;a<10;a++)t[a]=r[o+a];for(let a=0;a<10;a++)r[o+a]^=~t[(a+2)%10]&t[(a+4)%10]}r[0]^=rre[i],r[1]^=ire[i]}ln(t)}function dre(r,e,t,i){let n=r.sqr(t),s=r.sqr(i),o=r.add(r.mul(e.a,n),s),a=r.add(r.ONE,r.mul(e.d,r.mul(n,s)));return r.eql(o,a)}function GR(r,e={}){let t=FR("edwards",r,e,e.FpFnLE),{Fp:i,Fn:n}=t,s=t.CURVE,{h:o}=s;zd(e,{},{uvRatio:"function"});let a=NB<<BigInt(n.BYTES*8)-br,c=I=>i.create(I),l=e.uvRatio||((I,m)=>{try{return{isValid:!0,value:i.sqrt(i.div(I,m))}}catch{return{isValid:!1,value:xa}}});if(!dre(i,s,s.Gx,s.Gy))throw new Error("bad curve params: generator point");function d(I,m,w=!1){let B=w?br:xa;return pA("coordinate "+I,m,B,a),m}function u(I){if(!(I instanceof g))throw new Error("ExtendedPoint expected")}let f=_I((I,m)=>{let{X:w,Y:B,Z:C}=I,x=I.is0();m==null&&(m=x?lre:i.inv(C));let Q=c(w*m),R=c(B*m),U=i.mul(C,m);if(x)return{x:xa,y:br};if(U!==br)throw new Error("invZ was invalid");return{x:Q,y:R}}),h=_I(I=>{let{a:m,d:w}=s;if(I.is0())throw new Error("bad point: ZERO");let{X:B,Y:C,Z:x,T:Q}=I,R=c(B*B),U=c(C*C),T=c(x*x),D=c(T*T),b=c(R*m),_=c(T*c(b+U)),v=c(D+c(w*c(R*U)));if(_!==v)throw new Error("bad point: equation left != right (1)");let E=c(B*C),P=c(x*Q);if(E!==P)throw new Error("bad point: equation left != right (2)");return!0});class g{constructor(m,w,B,C){this.X=d("x",m),this.Y=d("y",w),this.Z=d("z",B,!0),this.T=d("t",C),Object.freeze(this)}static CURVE(){return s}static fromAffine(m){if(m instanceof g)throw new Error("extended point not allowed");let{x:w,y:B}=m||{};return d("x",w),d("y",B),new g(w,B,br,c(w*B))}static fromBytes(m,w=!1){let B=i.BYTES,{a:C,d:x}=s;m=ET(_n(m,B,"point")),Yc(w,"zip215");let Q=ET(m),R=m[B-1];Q[B-1]=R&-129;let U=Wc(Q),T=w?a:i.ORDER;pA("point.y",U,xa,T);let D=c(U*U),b=c(D-br),_=c(x*D-C),{isValid:v,value:E}=l(b,_);if(!v)throw new Error("bad point: invalid y coordinate");let P=(E&br)===br,N=(R&128)!==0;if(!w&&E===xa&&N)throw new Error("bad point: x=0 and x_0=1");return N!==P&&(E=c(-E)),g.fromAffine({x:E,y:U})}static fromHex(m,w=!1){return g.fromBytes(vt("point",m),w)}get x(){return this.toAffine().x}get y(){return this.toAffine().y}precompute(m=8,w=!0){return p.createCache(this,m),w||this.multiply(NB),this}assertValidity(){h(this)}equals(m){u(m);let{X:w,Y:B,Z:C}=this,{X:x,Y:Q,Z:R}=m,U=c(w*R),T=c(x*C),D=c(B*R),b=c(Q*C);return U===T&&D===b}is0(){return this.equals(g.ZERO)}negate(){return new g(c(-this.X),this.Y,this.Z,c(-this.T))}double(){let{a:m}=s,{X:w,Y:B,Z:C}=this,x=c(w*w),Q=c(B*B),R=c(NB*c(C*C)),U=c(m*x),T=w+B,D=c(c(T*T)-x-Q),b=U+Q,_=b-R,v=U-Q,E=c(D*_),P=c(b*v),N=c(D*v),j=c(_*b);return new g(E,P,j,N)}add(m){u(m);let{a:w,d:B}=s,{X:C,Y:x,Z:Q,T:R}=this,{X:U,Y:T,Z:D,T:b}=m,_=c(C*U),v=c(x*T),E=c(R*B*b),P=c(Q*D),N=c((C+x)*(U+T)-_-v),j=P-E,z=P+E,L=c(v-w*_),G=c(N*j),K=c(z*L),$=c(N*L),te=c(j*z);return new g(G,K,te,$)}subtract(m){return this.add(m.negate())}multiply(m){if(!n.isValidNot0(m))throw new Error("invalid scalar: expected 1 <= sc < curve.n");let{p:w,f:B}=p.cached(this,m,C=>Kc(g,C));return Kc(g,[w,B])[0]}multiplyUnsafe(m,w=g.ZERO){if(!n.isValid(m))throw new Error("invalid scalar: expected 0 <= sc < curve.n");return m===xa?g.ZERO:this.is0()||m===br?this:p.unsafe(this,m,B=>Kc(g,B),w)}isSmallOrder(){return this.multiplyUnsafe(o).is0()}isTorsionFree(){return p.unsafe(this,s.n).is0()}toAffine(m){return f(this,m)}clearCofactor(){return o===br?this:this.multiplyUnsafe(o)}toBytes(){let{x:m,y:w}=this.toAffine(),B=i.toBytes(w);return B[B.length-1]|=m&br?128:0,B}toHex(){return zc(this.toBytes())}toString(){return`<Point ${this.is0()?"ZERO":this.toHex()}>`}get ex(){return this.X}get ey(){return this.Y}get ez(){return this.Z}get et(){return this.T}static normalizeZ(m){return Kc(g,m)}static msm(m,w){return _R(g,n,m,w)}_setWindowSize(m){this.precompute(m)}toRawBytes(){return this.toBytes()}}g.BASE=new g(s.Gx,s.Gy,br,c(s.Gx*s.Gy)),g.ZERO=new g(xa,br,br,xa),g.Fp=i,g.Fn=n;let p=new RI(g,n.BITS);return g.BASE.precompute(8),g}function ure(r,e,t={}){if(typeof e!="function")throw new Error('"hash" function param is required');zd(t,{},{adjustScalarBytes:"function",randomBytes:"function",domain:"function",prehash:"function",mapToCurve:"function"});let{prehash:i}=t,{BASE:n,Fp:s,Fn:o}=r,a=t.randomBytes||nw,c=t.adjustScalarBytes||(T=>T),l=t.domain||((T,D,b)=>{if(Yc(b,"phflag"),D.length||b)throw new Error("Contexts/pre-hash are not supported");return T});function d(T){return o.create(Wc(T))}function u(T){let D=B.secretKey;T=vt("private key",T,D);let b=vt("hashed private key",e(T),2*D),_=c(b.slice(0,D)),v=b.slice(D,2*D),E=d(_);return{head:_,prefix:v,scalar:E}}function f(T){let{head:D,prefix:b,scalar:_}=u(T),v=n.multiply(_),E=v.toBytes();return{head:D,prefix:b,scalar:_,point:v,pointBytes:E}}function h(T){return f(T).pointBytes}function g(T=Uint8Array.of(),...D){let b=Dn(...D);return d(e(l(b,vt("context",T),!!i)))}function p(T,D,b={}){T=vt("message",T),i&&(T=i(T));let{prefix:_,scalar:v,pointBytes:E}=f(D),P=g(b.context,_,T),N=n.multiply(P).toBytes(),j=g(b.context,N,E,T),z=o.create(P+j*v);if(!o.isValid(z))throw new Error("sign failed: invalid s");let L=Dn(N,o.toBytes(z));return _n(L,B.signature,"result")}let I={zip215:!0};function m(T,D,b,_=I){let{context:v,zip215:E}=_,P=B.signature;T=vt("signature",T,P),D=vt("message",D),b=vt("publicKey",b,B.publicKey),E!==void 0&&Yc(E,"zip215"),i&&(D=i(D));let N=P/2,j=T.subarray(0,N),z=Wc(T.subarray(N,P)),L,G,K;try{L=r.fromBytes(b,E),G=r.fromBytes(j,E),K=n.multiplyUnsafe(z)}catch{return!1}if(!E&&L.isSmallOrder())return!1;let $=g(v,G.toBytes(),L.toBytes(),D);return G.add(L.multiplyUnsafe($)).subtract(K).clearCofactor().is0()}let w=s.BYTES,B={secretKey:w,publicKey:w,signature:2*w,seed:w};function C(T=a(B.seed)){return _n(T,B.seed,"seed")}function x(T){let D=U.randomSecretKey(T);return{secretKey:D,publicKey:h(D)}}function Q(T){return _A(T)&&T.length===o.BYTES}function R(T,D){try{return!!r.fromBytes(T,D)}catch{return!1}}let U={getExtendedPublicKey:f,randomSecretKey:C,isValidSecretKey:Q,isValidPublicKey:R,toMontgomery(T){let{y:D}=r.fromBytes(T),b=B.publicKey,_=b===32;if(!_&&b!==57)throw new Error("only defined for 25519 and 448");let v=_?s.div(br+D,br-D):s.div(D-br,D+br);return s.toBytes(v)},toMontgomerySecret(T){let D=B.secretKey;_n(T,D);let b=e(T.subarray(0,D));return c(b).subarray(0,D)},randomPrivateKey:C,precompute(T=8,D=r.BASE){return D.precompute(T,!1)}};return Object.freeze({keygen:x,getPublicKey:h,sign:p,verify:m,utils:U,Point:r,lengths:B})}function fre(r){let e={a:r.a,d:r.d,p:r.Fp.ORDER,n:r.n,h:r.h,Gx:r.Gx,Gy:r.Gy},t=r.Fp,i=Qi(e.n,r.nBitLength,!0),n={Fp:t,Fn:i,uvRatio:r.uvRatio},s={randomBytes:r.randomBytes,adjustScalarBytes:r.adjustScalarBytes,domain:r.domain,prehash:r.prehash,mapToCurve:r.mapToCurve};return{CURVE:e,curveOpts:n,hash:r.hash,eddsaOpts:s}}function Are(r,e){let t=e.Point;return Object.assign({},e,{ExtendedPoint:t,CURVE:r,nBitLength:t.Fn.BITS,nByteLength:t.Fn.BYTES})}function hre(r){let{CURVE:e,curveOpts:t,hash:i,eddsaOpts:n}=fre(r),s=GR(e,t),o=ure(s,i,n);return Are(r,o)}function gre(r){return zd(r,{adjustScalarBytes:"function",powPminus2:"function"}),Object.freeze({...r})}function pre(r){let e=gre(r),{P:t,type:i,adjustScalarBytes:n,powPminus2:s,randomBytes:o}=e,a=i==="x25519";if(!a&&i!=="x448")throw new Error("invalid type");let c=o||nw,l=a?255:448,d=a?32:56,u=BigInt(a?9:5),f=BigInt(a?121665:39081),h=a?Gy**BigInt(254):Gy**BigInt(447),g=a?BigInt(8)*Gy**BigInt(251)-Id:BigInt(4)*Gy**BigInt(445)-Id,p=h+g+Id,I=v=>mr(v,t),m=w(u);function w(v){return sb(I(v),d)}function B(v){let E=vt("u coordinate",v,d);return a&&(E[31]&=127),I(Wc(E))}function C(v){return Wc(n(vt("scalar",v,d)))}function x(v,E){let P=U(B(E),C(v));if(P===$f)throw new Error("invalid private or public key received");return w(P)}function Q(v){return x(v,m)}function R(v,E,P){let N=I(v*(E-P));return E=I(E-N),P=I(P+N),{x_2:E,x_3:P}}function U(v,E){pA("u",v,$f,t),pA("scalar",E,h,p);let P=E,N=v,j=Id,z=$f,L=v,G=Id,K=$f;for(let te=BigInt(l-1);te>=$f;te--){let re=P>>te&Id;K^=re,{x_2:j,x_3:L}=R(K,j,L),{x_2:z,x_3:G}=R(K,z,G),K=re;let ee=j+z,Z=I(ee*ee),me=j-z,le=I(me*me),Ae=Z-le,ve=L+G,qe=L-G,_e=I(qe*ee),se=I(ve*me),rt=_e+se,ce=_e-se;L=I(rt*rt),G=I(N*I(ce*ce)),j=I(Z*le),z=I(Ae*(Z+I(f*Ae)))}({x_2:j,x_3:L}=R(K,j,L)),{x_2:z,x_3:G}=R(K,z,G);let $=s(z);return I(j*$)}let T={secretKey:d,publicKey:d,seed:d},D=(v=c(d))=>(ps(v,T.seed),v);function b(v){let E=D(v);return{secretKey:E,publicKey:Q(E)}}return{keygen:b,getSharedSecret:(v,E)=>x(v,E),getPublicKey:v=>Q(v),scalarMult:x,scalarMultBase:Q,utils:{randomSecretKey:D,randomPrivateKey:D},GuBytes:m.slice(),lengths:T}}function zR(r){let e=rl.p,t=r*r*r%e,i=t*t*r%e,n=bt(i,UT,e)*i%e,s=bt(n,UT,e)*i%e,o=bt(s,E1,e)*t%e,a=bt(o,wre,e)*o%e,c=bt(a,xre,e)*a%e,l=bt(c,RT,e)*c%e,d=bt(l,Cre,e)*l%e,u=bt(d,RT,e)*c%e,f=bt(u,E1,e)*t%e,h=bt(f,Ire,e)*r%e;return bt(h,Ere,e)*f%e}function JR(r){return r[0]&=252,r[55]|=128,r[56]=0,r}function Bre(r,e){let t=rl.p,i=mr(r*r*e,t),n=mr(i*r,t),s=mr(n*i*e,t),o=zR(s),a=mr(n*o,t),c=mr(a*a,t);return{isValid:mr(c*e,t)===r,value:a}}function vre(r,e,t){if(e.length>255)throw new Error("context must be smaller than 255, got: "+e.length);return Dn(rte("SigEd448"),new Uint8Array([t?1:0,e.length]),e,r)}function _re(r){let e=cb.p,t=BigInt(3),i=BigInt(6),n=BigInt(11),s=BigInt(22),o=BigInt(23),a=BigInt(44),c=BigInt(88),l=r*r*r%e,d=l*l*r%e,u=bt(d,t,e)*d%e,f=bt(u,t,e)*d%e,h=bt(f,qT,e)*l%e,g=bt(h,n,e)*h%e,p=bt(g,s,e)*g%e,I=bt(p,a,e)*p%e,m=bt(I,c,e)*I%e,w=bt(m,a,e)*p%e,B=bt(w,t,e)*d%e,C=bt(B,o,e)*g%e,x=bt(C,i,e)*l%e,Q=bt(x,qT,e);if(!B1.eql(B1.sqr(Q),r))throw new Error("Cannot find square root");return Q}function OT(r,e,t,i){return r===0?e^t^i:r===1?e&t|~e&i:r===2?(e|~t)^i:r===3?e&i|t&~i:e^(t|~i)}function KT(r,e,t,i){r[e]=t>>24&255,r[e+1]=t>>16&255,r[e+2]=t>>8&255,r[e+3]=t&255,r[e+4]=i>>24&255,r[e+5]=i>>16&255,r[e+6]=i>>8&255,r[e+7]=i&255}function lie(r,e,t,i,n){var s,o=0;for(s=0;s<n;s++)o|=r[e+s]^t[i+s];return(1&o-1>>>8)-1}function sN(r,e,t,i){return lie(r,e,t,i,32)}function Qa(r,e){var t;for(t=0;t<16;t++)r[t]=e[t]|0}function MB(r){var e,t,i=1;for(e=0;e<16;e++)t=r[e]+i+65535,i=Math.floor(t/65536),r[e]=t-i*65536;r[0]+=i-1+37*(i-1)}function vd(r,e,t){for(var i,n=~(t-1),s=0;s<16;s++)i=n&(r[s]^e[s]),r[s]^=i,e[s]^=i}function mA(r,e){var t,i,n,s=Pe(),o=Pe();for(t=0;t<16;t++)o[t]=e[t];for(MB(o),MB(o),MB(o),i=0;i<2;i++){for(s[0]=o[0]-65517,t=1;t<15;t++)s[t]=o[t]-65535-(s[t-1]>>16&1),s[t-1]&=65535;s[15]=o[15]-32767-(s[14]>>16&1),n=s[15]>>16&1,s[14]&=65535,vd(o,s,1-n)}for(t=0;t<16;t++)r[2*t]=o[t]&255,r[2*t+1]=o[t]>>8}function GT(r,e){var t=new Uint8Array(32),i=new Uint8Array(32);return mA(t,r),mA(i,e),sN(t,0,i,0)}function oN(r){var e=new Uint8Array(32);return mA(e,r),e[0]&1}function aN(r,e){var t;for(t=0;t<16;t++)r[t]=e[2*t]+(e[2*t+1]<<8);r[15]&=32767}function as(r,e,t){for(var i=0;i<16;i++)r[i]=e[i]+t[i]}function ls(r,e,t){for(var i=0;i<16;i++)r[i]=e[i]-t[i]}function ot(r,e,t){var i,n,s=0,o=0,a=0,c=0,l=0,d=0,u=0,f=0,h=0,g=0,p=0,I=0,m=0,w=0,B=0,C=0,x=0,Q=0,R=0,U=0,T=0,D=0,b=0,_=0,v=0,E=0,P=0,N=0,j=0,z=0,L=0,G=t[0],K=t[1],$=t[2],te=t[3],re=t[4],ee=t[5],Z=t[6],me=t[7],le=t[8],Ae=t[9],ve=t[10],qe=t[11],_e=t[12],se=t[13],rt=t[14],ce=t[15];i=e[0],s+=i*G,o+=i*K,a+=i*$,c+=i*te,l+=i*re,d+=i*ee,u+=i*Z,f+=i*me,h+=i*le,g+=i*Ae,p+=i*ve,I+=i*qe,m+=i*_e,w+=i*se,B+=i*rt,C+=i*ce,i=e[1],o+=i*G,a+=i*K,c+=i*$,l+=i*te,d+=i*re,u+=i*ee,f+=i*Z,h+=i*me,g+=i*le,p+=i*Ae,I+=i*ve,m+=i*qe,w+=i*_e,B+=i*se,C+=i*rt,x+=i*ce,i=e[2],a+=i*G,c+=i*K,l+=i*$,d+=i*te,u+=i*re,f+=i*ee,h+=i*Z,g+=i*me,p+=i*le,I+=i*Ae,m+=i*ve,w+=i*qe,B+=i*_e,C+=i*se,x+=i*rt,Q+=i*ce,i=e[3],c+=i*G,l+=i*K,d+=i*$,u+=i*te,f+=i*re,h+=i*ee,g+=i*Z,p+=i*me,I+=i*le,m+=i*Ae,w+=i*ve,B+=i*qe,C+=i*_e,x+=i*se,Q+=i*rt,R+=i*ce,i=e[4],l+=i*G,d+=i*K,u+=i*$,f+=i*te,h+=i*re,g+=i*ee,p+=i*Z,I+=i*me,m+=i*le,w+=i*Ae,B+=i*ve,C+=i*qe,x+=i*_e,Q+=i*se,R+=i*rt,U+=i*ce,i=e[5],d+=i*G,u+=i*K,f+=i*$,h+=i*te,g+=i*re,p+=i*ee,I+=i*Z,m+=i*me,w+=i*le,B+=i*Ae,C+=i*ve,x+=i*qe,Q+=i*_e,R+=i*se,U+=i*rt,T+=i*ce,i=e[6],u+=i*G,f+=i*K,h+=i*$,g+=i*te,p+=i*re,I+=i*ee,m+=i*Z,w+=i*me,B+=i*le,C+=i*Ae,x+=i*ve,Q+=i*qe,R+=i*_e,U+=i*se,T+=i*rt,D+=i*ce,i=e[7],f+=i*G,h+=i*K,g+=i*$,p+=i*te,I+=i*re,m+=i*ee,w+=i*Z,B+=i*me,C+=i*le,x+=i*Ae,Q+=i*ve,R+=i*qe,U+=i*_e,T+=i*se,D+=i*rt,b+=i*ce,i=e[8],h+=i*G,g+=i*K,p+=i*$,I+=i*te,m+=i*re,w+=i*ee,B+=i*Z,C+=i*me,x+=i*le,Q+=i*Ae,R+=i*ve,U+=i*qe,T+=i*_e,D+=i*se,b+=i*rt,_+=i*ce,i=e[9],g+=i*G,p+=i*K,I+=i*$,m+=i*te,w+=i*re,B+=i*ee,C+=i*Z,x+=i*me,Q+=i*le,R+=i*Ae,U+=i*ve,T+=i*qe,D+=i*_e,b+=i*se,_+=i*rt,v+=i*ce,i=e[10],p+=i*G,I+=i*K,m+=i*$,w+=i*te,B+=i*re,C+=i*ee,x+=i*Z,Q+=i*me,R+=i*le,U+=i*Ae,T+=i*ve,D+=i*qe,b+=i*_e,_+=i*se,v+=i*rt,E+=i*ce,i=e[11],I+=i*G,m+=i*K,w+=i*$,B+=i*te,C+=i*re,x+=i*ee,Q+=i*Z,R+=i*me,U+=i*le,T+=i*Ae,D+=i*ve,b+=i*qe,_+=i*_e,v+=i*se,E+=i*rt,P+=i*ce,i=e[12],m+=i*G,w+=i*K,B+=i*$,C+=i*te,x+=i*re,Q+=i*ee,R+=i*Z,U+=i*me,T+=i*le,D+=i*Ae,b+=i*ve,_+=i*qe,v+=i*_e,E+=i*se,P+=i*rt,N+=i*ce,i=e[13],w+=i*G,B+=i*K,C+=i*$,x+=i*te,Q+=i*re,R+=i*ee,U+=i*Z,T+=i*me,D+=i*le,b+=i*Ae,_+=i*ve,v+=i*qe,E+=i*_e,P+=i*se,N+=i*rt,j+=i*ce,i=e[14],B+=i*G,C+=i*K,x+=i*$,Q+=i*te,R+=i*re,U+=i*ee,T+=i*Z,D+=i*me,b+=i*le,_+=i*Ae,v+=i*ve,E+=i*qe,P+=i*_e,N+=i*se,j+=i*rt,z+=i*ce,i=e[15],C+=i*G,x+=i*K,Q+=i*$,R+=i*te,U+=i*re,T+=i*ee,D+=i*Z,b+=i*me,_+=i*le,v+=i*Ae,E+=i*ve,P+=i*qe,N+=i*_e,j+=i*se,z+=i*rt,L+=i*ce,s+=38*x,o+=38*Q,a+=38*R,c+=38*U,l+=38*T,d+=38*D,u+=38*b,f+=38*_,h+=38*v,g+=38*E,p+=38*P,I+=38*N,m+=38*j,w+=38*z,B+=38*L,n=1,i=s+n+65535,n=Math.floor(i/65536),s=i-n*65536,i=o+n+65535,n=Math.floor(i/65536),o=i-n*65536,i=a+n+65535,n=Math.floor(i/65536),a=i-n*65536,i=c+n+65535,n=Math.floor(i/65536),c=i-n*65536,i=l+n+65535,n=Math.floor(i/65536),l=i-n*65536,i=d+n+65535,n=Math.floor(i/65536),d=i-n*65536,i=u+n+65535,n=Math.floor(i/65536),u=i-n*65536,i=f+n+65535,n=Math.floor(i/65536),f=i-n*65536,i=h+n+65535,n=Math.floor(i/65536),h=i-n*65536,i=g+n+65535,n=Math.floor(i/65536),g=i-n*65536,i=p+n+65535,n=Math.floor(i/65536),p=i-n*65536,i=I+n+65535,n=Math.floor(i/65536),I=i-n*65536,i=m+n+65535,n=Math.floor(i/65536),m=i-n*65536,i=w+n+65535,n=Math.floor(i/65536),w=i-n*65536,i=B+n+65535,n=Math.floor(i/65536),B=i-n*65536,i=C+n+65535,n=Math.floor(i/65536),C=i-n*65536,s+=n-1+37*(n-1),n=1,i=s+n+65535,n=Math.floor(i/65536),s=i-n*65536,i=o+n+65535,n=Math.floor(i/65536),o=i-n*65536,i=a+n+65535,n=Math.floor(i/65536),a=i-n*65536,i=c+n+65535,n=Math.floor(i/65536),c=i-n*65536,i=l+n+65535,n=Math.floor(i/65536),l=i-n*65536,i=d+n+65535,n=Math.floor(i/65536),d=i-n*65536,i=u+n+65535,n=Math.floor(i/65536),u=i-n*65536,i=f+n+65535,n=Math.floor(i/65536),f=i-n*65536,i=h+n+65535,n=Math.floor(i/65536),h=i-n*65536,i=g+n+65535,n=Math.floor(i/65536),g=i-n*65536,i=p+n+65535,n=Math.floor(i/65536),p=i-n*65536,i=I+n+65535,n=Math.floor(i/65536),I=i-n*65536,i=m+n+65535,n=Math.floor(i/65536),m=i-n*65536,i=w+n+65535,n=Math.floor(i/65536),w=i-n*65536,i=B+n+65535,n=Math.floor(i/65536),B=i-n*65536,i=C+n+65535,n=Math.floor(i/65536),C=i-n*65536,s+=n-1+37*(n-1),r[0]=s,r[1]=o,r[2]=a,r[3]=c,r[4]=l,r[5]=d,r[6]=u,r[7]=f,r[8]=h,r[9]=g,r[10]=p,r[11]=I,r[12]=m,r[13]=w,r[14]=B,r[15]=C}function Qn(r,e){ot(r,e,e)}function cN(r,e){var t=Pe(),i;for(i=0;i<16;i++)t[i]=e[i];for(i=253;i>=0;i--)Qn(t,t),i!==2&&i!==4&&ot(t,t,e);for(i=0;i<16;i++)r[i]=t[i]}function die(r,e){var t=Pe(),i;for(i=0;i<16;i++)t[i]=e[i];for(i=250;i>=0;i--)Qn(t,t),i!==1&&ot(t,t,e);for(i=0;i<16;i++)r[i]=t[i]}function lN(r,e,t){var i=new Uint8Array(32),n=new Float64Array(80),s,o,a=Pe(),c=Pe(),l=Pe(),d=Pe(),u=Pe(),f=Pe();for(o=0;o<31;o++)i[o]=e[o];for(i[31]=e[31]&127|64,i[0]&=248,aN(n,t),o=0;o<16;o++)c[o]=n[o],d[o]=a[o]=l[o]=0;for(a[0]=d[0]=1,o=254;o>=0;--o)s=i[o>>>3]>>>(o&7)&1,vd(a,c,s),vd(l,d,s),as(u,a,l),ls(a,a,l),as(l,c,d),ls(c,c,d),Qn(d,u),Qn(f,a),ot(a,l,a),ot(l,c,u),as(u,a,l),ls(a,a,l),Qn(c,a),ls(l,d,f),ot(a,l,sie),as(a,a,d),ot(l,l,a),ot(a,d,f),ot(d,c,n),Qn(c,u),vd(a,c,s),vd(l,d,s);for(o=0;o<16;o++)n[o+16]=a[o],n[o+32]=l[o],n[o+48]=c[o],n[o+64]=d[o];var h=n.subarray(32),g=n.subarray(16);return cN(h,h),ot(g,g,h),mA(r,g),0}function dN(r,e){return lN(r,e,nN)}function uie(r,e){return lb(e,32),dN(r,e)}function JT(r,e,t,i){for(var n=new Int32Array(16),s=new Int32Array(16),o,a,c,l,d,u,f,h,g,p,I,m,w,B,C,x,Q,R,U,T,D,b,_,v,E,P,N=r[0],j=r[1],z=r[2],L=r[3],G=r[4],K=r[5],$=r[6],te=r[7],re=e[0],ee=e[1],Z=e[2],me=e[3],le=e[4],Ae=e[5],ve=e[6],qe=e[7],_e=0;i>=128;){for(U=0;U<16;U++)T=8*U+_e,n[U]=t[T+0]<<24|t[T+1]<<16|t[T+2]<<8|t[T+3],s[U]=t[T+4]<<24|t[T+5]<<16|t[T+6]<<8|t[T+7];for(U=0;U<80;U++)if(o=N,a=j,c=z,l=L,d=G,u=K,f=$,h=te,g=re,p=ee,I=Z,m=me,w=le,B=Ae,C=ve,x=qe,D=te,b=qe,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=(G>>>14|le<<18)^(G>>>18|le<<14)^(le>>>9|G<<23),b=(le>>>14|G<<18)^(le>>>18|G<<14)^(G>>>9|le<<23),_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,D=G&K^~G&$,b=le&Ae^~le&ve,_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,D=zT[U*2],b=zT[U*2+1],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,D=n[U%16],b=s[U%16],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,Q=E&65535|P<<16,R=_&65535|v<<16,D=Q,b=R,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=(N>>>28|re<<4)^(re>>>2|N<<30)^(re>>>7|N<<25),b=(re>>>28|N<<4)^(N>>>2|re<<30)^(N>>>7|re<<25),_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,D=N&j^N&z^j&z,b=re&ee^re&Z^ee&Z,_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,h=E&65535|P<<16,x=_&65535|v<<16,D=l,b=m,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=Q,b=R,_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,l=E&65535|P<<16,m=_&65535|v<<16,j=o,z=a,L=c,G=l,K=d,$=u,te=f,N=h,ee=g,Z=p,me=I,le=m,Ae=w,ve=B,qe=C,re=x,U%16===15)for(T=0;T<16;T++)D=n[T],b=s[T],_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=n[(T+9)%16],b=s[(T+9)%16],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,Q=n[(T+1)%16],R=s[(T+1)%16],D=(Q>>>1|R<<31)^(Q>>>8|R<<24)^Q>>>7,b=(R>>>1|Q<<31)^(R>>>8|Q<<24)^(R>>>7|Q<<25),_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,Q=n[(T+14)%16],R=s[(T+14)%16],D=(Q>>>19|R<<13)^(R>>>29|Q<<3)^Q>>>6,b=(R>>>19|Q<<13)^(Q>>>29|R<<3)^(R>>>6|Q<<26),_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,n[T]=E&65535|P<<16,s[T]=_&65535|v<<16;D=N,b=re,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[0],b=e[0],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[0]=N=E&65535|P<<16,e[0]=re=_&65535|v<<16,D=j,b=ee,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[1],b=e[1],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[1]=j=E&65535|P<<16,e[1]=ee=_&65535|v<<16,D=z,b=Z,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[2],b=e[2],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[2]=z=E&65535|P<<16,e[2]=Z=_&65535|v<<16,D=L,b=me,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[3],b=e[3],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[3]=L=E&65535|P<<16,e[3]=me=_&65535|v<<16,D=G,b=le,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[4],b=e[4],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[4]=G=E&65535|P<<16,e[4]=le=_&65535|v<<16,D=K,b=Ae,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[5],b=e[5],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[5]=K=E&65535|P<<16,e[5]=Ae=_&65535|v<<16,D=$,b=ve,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[6],b=e[6],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[6]=$=E&65535|P<<16,e[6]=ve=_&65535|v<<16,D=te,b=qe,_=b&65535,v=b>>>16,E=D&65535,P=D>>>16,D=r[7],b=e[7],_+=b&65535,v+=b>>>16,E+=D&65535,P+=D>>>16,v+=_>>>16,E+=v>>>16,P+=E>>>16,r[7]=te=E&65535|P<<16,e[7]=qe=_&65535|v<<16,_e+=128,i-=128}return i}function iA(r,e,t){var i=new Int32Array(8),n=new Int32Array(8),s=new Uint8Array(256),o,a=t;for(i[0]=1779033703,i[1]=3144134277,i[2]=1013904242,i[3]=2773480762,i[4]=1359893119,i[5]=2600822924,i[6]=528734635,i[7]=1541459225,n[0]=4089235720,n[1]=2227873595,n[2]=4271175723,n[3]=1595750129,n[4]=2917565137,n[5]=725511199,n[6]=4215389547,n[7]=327033209,JT(i,n,e,t),t%=128,o=0;o<t;o++)s[o]=e[a-t+o];for(s[t]=128,t=256-128*(t<112?1:0),s[t-9]=0,KT(s,t-8,a/536870912|0,a<<3),JT(i,n,s,t),o=0;o<8;o++)KT(r,8*o,i[o],n[o]);return 0}function D1(r,e){var t=Pe(),i=Pe(),n=Pe(),s=Pe(),o=Pe(),a=Pe(),c=Pe(),l=Pe(),d=Pe();ls(t,r[1],r[0]),ls(d,e[1],e[0]),ot(t,t,d),as(i,r[0],r[1]),as(d,e[0],e[1]),ot(i,i,d),ot(n,r[3],e[3]),ot(n,n,aie),ot(s,r[2],e[2]),as(s,s,s),ls(o,i,t),ls(a,s,n),as(c,s,n),as(l,i,t),ot(r[0],o,a),ot(r[1],l,c),ot(r[2],c,a),ot(r[3],o,l)}function jT(r,e,t){var i;for(i=0;i<4;i++)vd(r[i],e[i],t)}function db(r,e){var t=Pe(),i=Pe(),n=Pe();cN(n,e[2]),ot(t,e[0],n),ot(i,e[1],n),mA(r,i),r[31]^=oN(t)<<7}function uN(r,e,t){var i,n;for(Qa(r[0],S1),Qa(r[1],qI),Qa(r[2],qI),Qa(r[3],S1),n=255;n>=0;--n)i=t[n/8|0]>>(n&7)&1,jT(r,e,i),D1(e,r),D1(r,r),jT(r,e,i)}function ub(r,e){var t=[Pe(),Pe(),Pe(),Pe()];Qa(t[0],LT),Qa(t[1],HT),Qa(t[2],qI),ot(t[3],LT,HT),uN(r,t,e)}function fN(r,e,t){var i=new Uint8Array(64),n=[Pe(),Pe(),Pe(),Pe()],s;for(t||lb(e,32),iA(i,e,32),i[0]&=248,i[31]&=127,i[31]|=64,ub(n,i),db(r,n),s=0;s<32;s++)e[s+32]=r[s];return 0}function AN(r,e){var t,i,n,s;for(i=63;i>=32;--i){for(t=0,n=i-32,s=i-12;n<s;++n)e[n]+=t-16*e[i]*LB[n-(i-32)],t=Math.floor((e[n]+128)/256),e[n]-=t*256;e[n]+=t,e[i]=0}for(t=0,n=0;n<32;n++)e[n]+=t-(e[31]>>4)*LB[n],t=e[n]>>8,e[n]&=255;for(n=0;n<32;n++)e[n]-=t*LB[n];for(i=0;i<32;i++)e[i+1]+=e[i]>>8,r[i]=e[i]&255}function Q1(r){var e=new Float64Array(64),t;for(t=0;t<64;t++)e[t]=r[t];for(t=0;t<64;t++)r[t]=0;AN(r,e)}function fie(r,e,t,i){var n=new Uint8Array(64),s=new Uint8Array(64),o=new Uint8Array(64),a,c,l=new Float64Array(64),d=[Pe(),Pe(),Pe(),Pe()];iA(n,i,32),n[0]&=248,n[31]&=127,n[31]|=64;var u=t+64;for(a=0;a<t;a++)r[64+a]=e[a];for(a=0;a<32;a++)r[32+a]=n[32+a];for(iA(o,r.subarray(32),t+32),Q1(o),ub(d,o),db(r,d),a=32;a<64;a++)r[a]=i[a];for(iA(s,r,t+64),Q1(s),a=0;a<64;a++)l[a]=0;for(a=0;a<32;a++)l[a]=o[a];for(a=0;a<32;a++)for(c=0;c<32;c++)l[a+c]+=s[a]*n[c];return AN(r.subarray(32),l),u}function Aie(r,e){var t=Pe(),i=Pe(),n=Pe(),s=Pe(),o=Pe(),a=Pe(),c=Pe();return Qa(r[2],qI),aN(r[1],e),Qn(n,r[1]),ot(s,n,oie),ls(n,n,r[2]),as(s,r[2],s),Qn(o,s),Qn(a,o),ot(c,a,o),ot(t,c,n),ot(t,t,s),die(t,t),ot(t,t,n),ot(t,t,s),ot(t,t,s),ot(r[0],t,s),Qn(i,r[0]),ot(i,i,s),GT(i,n)&&ot(r[0],r[0],cie),Qn(i,r[0]),ot(i,i,s),GT(i,n)?-1:(oN(r[0])===e[31]>>7&&ls(r[0],S1,r[0]),ot(r[3],r[0],r[1]),0)}function hie(r,e,t,i){var n,s=new Uint8Array(32),o=new Uint8Array(64),a=[Pe(),Pe(),Pe(),Pe()],c=[Pe(),Pe(),Pe(),Pe()];if(t<64||Aie(c,i))return-1;for(n=0;n<t;n++)r[n]=e[n];for(n=0;n<32;n++)r[n+32]=i[n];if(iA(o,r,t),Q1(o),uN(a,c,o),ub(c,e.subarray(32)),D1(a,c),db(s,a),t-=64,sN(e,0,s,0)){for(n=0;n<t;n++)r[n]=0;return-1}for(n=0;n<t;n++)r[n]=e[n+64];return t}function Jd(){for(var r=0;r<arguments.length;r++)if(!(arguments[r]instanceof Uint8Array))throw new TypeError("unexpected type, use Uint8Array")}function mie(r){for(var e=0;e<r.length;e++)r[e]=0}function HB(r,e,t,i,n,s){let o=[16843776,0,65536,16843780,16842756,66564,4,65536,1024,16843776,16843780,1024,16778244,16842756,16777216,4,1028,16778240,16778240,66560,66560,16842752,16842752,16778244,65540,16777220,16777220,65540,0,1028,66564,16777216,65536,16843780,4,16842752,16843776,16777216,16777216,1024,16842756,65536,66560,16777220,1024,4,16778244,66564,16843780,65540,16842752,16778244,16777220,1028,66564,16843776,1028,16778240,16778240,0,65540,66560,0,16842756],a=[-2146402272,-2147450880,32768,1081376,1048576,32,-2146435040,-2147450848,-2147483616,-2146402272,-2146402304,-2147483648,-2147450880,1048576,32,-2146435040,1081344,1048608,-2147450848,0,-2147483648,32768,1081376,-2146435072,1048608,-2147483616,0,1081344,32800,-2146402304,-2146435072,32800,0,1081376,-2146435040,1048576,-2147450848,-2146435072,-2146402304,32768,-2146435072,-2147450880,32,-2146402272,1081376,32,32768,-2147483648,32800,-2146402304,1048576,-2147483616,1048608,-2147450848,-2147483616,1048608,1081344,0,-2147450880,32800,-2147483648,-2146435040,-2146402272,1081344],c=[520,134349312,0,134348808,134218240,0,131592,134218240,131080,134217736,134217736,131072,134349320,131080,134348800,520,134217728,8,134349312,512,131584,134348800,134348808,131592,134218248,131584,131072,134218248,8,134349320,512,134217728,134349312,134217728,131080,520,131072,134349312,134218240,0,512,131080,134349320,134218240,134217736,512,0,134348808,134218248,131072,134217728,134349320,8,131592,131584,134217736,134348800,134218248,520,134348800,131592,8,134348808,131584],l=[8396801,8321,8321,128,8396928,8388737,8388609,8193,0,8396800,8396800,8396929,129,0,8388736,8388609,1,8192,8388608,8396801,128,8388608,8193,8320,8388737,1,8320,8388736,8192,8396928,8396929,129,8388736,8388609,8396800,8396929,129,0,0,8396800,8320,8388736,8388737,1,8396801,8321,8321,128,8396929,129,1,8192,8388609,8193,8396928,8388737,8193,8320,8388608,8396801,128,8388608,8192,8396928],d=[256,34078976,34078720,1107296512,524288,256,1073741824,34078720,1074266368,524288,33554688,1074266368,1107296512,1107820544,524544,1073741824,33554432,1074266112,1074266112,0,1073742080,1107820800,1107820800,33554688,1107820544,1073742080,0,1107296256,34078976,33554432,1107296256,524544,524288,1107296512,256,33554432,1073741824,34078720,1107296512,1074266368,33554688,1073741824,1107820544,34078976,1074266368,256,33554432,1107820544,1107820800,524544,1107296256,1107820800,34078720,0,1074266112,1107296256,524544,33554688,1073742080,524288,0,1074266112,34078976,1073742080],u=[536870928,541065216,16384,541081616,541065216,16,541081616,4194304,536887296,4210704,4194304,536870928,4194320,536887296,536870912,16400,0,4194320,536887312,16384,4210688,536887312,16,541065232,541065232,0,4210704,541081600,16400,4210688,541081600,536870912,536887296,16,541065232,4210688,541081616,4194304,16400,536870928,4194304,536887296,536870912,16400,536870928,541081616,4210688,541065216,4210704,541081600,0,541065232,16,16384,541065216,4210704,16384,4194320,536887312,0,541081600,536870912,4194320,536887312],f=[2097152,69206018,67110914,0,2048,67110914,2099202,69208064,69208066,2097152,0,67108866,2,67108864,69206018,2050,67110912,2099202,2097154,67110912,67108866,69206016,69208064,2097154,69206016,2048,2050,69208066,2099200,2,67108864,2099200,67108864,2099200,2097152,67110914,67110914,69206018,69206018,2,2097154,67108864,67110912,2097152,69208064,2050,2099202,69208064,2050,67108866,69208066,69206016,2099200,0,2,69208066,0,2099202,69206016,2048,67108866,67110912,2048,2097154],h=[268439616,4096,262144,268701760,268435456,268439616,64,268435456,262208,268697600,268701760,266240,268701696,266304,4096,64,268697600,268435520,268439552,4160,266240,262208,268697664,268701696,4160,0,0,268697664,268435520,268439552,266304,262144,266304,262144,268701696,4096,64,268697664,4096,266304,268439552,64,268435520,268697600,268697664,268435456,262144,268439616,0,268701760,262208,268435520,268697600,268439552,268439616,0,268701760,266240,266240,4160,4160,262208,268435456,268701696],g=0,p,I,m,w,B,C,x,Q,R,U,T=e.length,D=r.length===32?3:9;D===3?Q=t?[0,32,2]:[30,-2,-2]:Q=t?[0,32,2,62,30,-2,64,96,2]:[94,62,-2,32,64,2,30,-2,-2],t&&(e=yie(e),T=e.length);let b=new Uint8Array(T),_=0;for(;g<T;){for(C=e[g++]<<24|e[g++]<<16|e[g++]<<8|e[g++],x=e[g++]<<24|e[g++]<<16|e[g++]<<8|e[g++],m=(C>>>4^x)&252645135,x^=m,C^=m<<4,m=(C>>>16^x)&65535,x^=m,C^=m<<16,m=(x>>>2^C)&858993459,C^=m,x^=m<<2,m=(x>>>8^C)&16711935,C^=m,x^=m<<8,m=(C>>>1^x)&1431655765,x^=m,C^=m<<1,C=C<<1|C>>>31,x=x<<1|x>>>31,I=0;I<D;I+=3){for(R=Q[I+1],U=Q[I+2],p=Q[I];p!==R;p+=U)w=x^r[p],B=(x>>>4|x<<28)^r[p+1],m=C,C=x,x=m^(a[w>>>24&63]|l[w>>>16&63]|u[w>>>8&63]|h[w&63]|o[B>>>24&63]|c[B>>>16&63]|d[B>>>8&63]|f[B&63]);m=C,C=x,x=m}C=C>>>1|C<<31,x=x>>>1|x<<31,m=(C>>>1^x)&1431655765,x^=m,C^=m<<1,m=(x>>>8^C)&16711935,C^=m,x^=m<<8,m=(x>>>2^C)&858993459,C^=m,x^=m<<2,m=(C>>>16^x)&65535,x^=m,C^=m<<16,m=(C>>>4^x)&252645135,x^=m,C^=m<<4,b[_++]=C>>>24,b[_++]=C>>>16&255,b[_++]=C>>>8&255,b[_++]=C&255,b[_++]=x>>>24,b[_++]=x>>>16&255,b[_++]=x>>>8&255,b[_++]=x&255}return t||(b=Iie(b)),b}function KB(r){let e=[0,4,536870912,536870916,65536,65540,536936448,536936452,512,516,536871424,536871428,66048,66052,536936960,536936964],t=[0,1,1048576,1048577,67108864,67108865,68157440,68157441,256,257,1048832,1048833,67109120,67109121,68157696,68157697],i=[0,8,2048,2056,16777216,16777224,16779264,16779272,0,8,2048,2056,16777216,16777224,16779264,16779272],n=[0,2097152,134217728,136314880,8192,2105344,134225920,136323072,131072,2228224,134348800,136445952,139264,2236416,134356992,136454144],s=[0,262144,16,262160,0,262144,16,262160,4096,266240,4112,266256,4096,266240,4112,266256],o=[0,1024,32,1056,0,1024,32,1056,33554432,33555456,33554464,33555488,33554432,33555456,33554464,33555488],a=[0,268435456,524288,268959744,2,268435458,524290,268959746,0,268435456,524288,268959744,2,268435458,524290,268959746],c=[0,65536,2048,67584,536870912,536936448,536872960,536938496,131072,196608,133120,198656,537001984,537067520,537004032,537069568],l=[0,262144,0,262144,2,262146,2,262146,33554432,33816576,33554432,33816576,33554434,33816578,33554434,33816578],d=[0,268435456,8,268435464,0,268435456,8,268435464,1024,268436480,1032,268436488,1024,268436480,1032,268436488],u=[0,32,0,32,1048576,1048608,1048576,1048608,8192,8224,8192,8224,1056768,1056800,1056768,1056800],f=[0,16777216,512,16777728,2097152,18874368,2097664,18874880,67108864,83886080,67109376,83886592,69206016,85983232,69206528,85983744],h=[0,4096,134217728,134221824,524288,528384,134742016,134746112,16,4112,134217744,134221840,524304,528400,134742032,134746128],g=[0,4,256,260,0,4,256,260,1,5,257,261,1,5,257,261],p=r.length>8?3:1,I=new Array(32*p),m=[0,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0],w,B,C=0,x=0,Q;for(let R=0;R<p;R++){let U=r[C++]<<24|r[C++]<<16|r[C++]<<8|r[C++],T=r[C++]<<24|r[C++]<<16|r[C++]<<8|r[C++];Q=(U>>>4^T)&252645135,T^=Q,U^=Q<<4,Q=(T>>>-16^U)&65535,U^=Q,T^=Q<<-16,Q=(U>>>2^T)&858993459,T^=Q,U^=Q<<2,Q=(T>>>-16^U)&65535,U^=Q,T^=Q<<-16,Q=(U>>>1^T)&1431655765,T^=Q,U^=Q<<1,Q=(T>>>8^U)&16711935,U^=Q,T^=Q<<8,Q=(U>>>1^T)&1431655765,T^=Q,U^=Q<<1,Q=U<<8|T>>>20&240,U=T<<24|T<<8&16711680|T>>>8&65280|T>>>24&240,T=Q;for(let D=0;D<m.length;D++)m[D]?(U=U<<2|U>>>26,T=T<<2|T>>>26):(U=U<<1|U>>>27,T=T<<1|T>>>27),U&=-15,T&=-15,w=e[U>>>28]|t[U>>>24&15]|i[U>>>20&15]|n[U>>>16&15]|s[U>>>12&15]|o[U>>>8&15]|a[U>>>4&15],B=c[T>>>28]|l[T>>>24&15]|d[T>>>20&15]|u[T>>>16&15]|f[T>>>12&15]|h[T>>>8&15]|g[T>>>4&15],Q=(B>>>16^w)&65535,I[x++]=w^Q,I[x++]=B^Q<<16}return I}function yie(r,e){let t=8-r.length%8,i;if(t<8)i=0;else{if(t===8)return r;throw new Error("des: invalid padding")}let n=new Uint8Array(r.length+t);for(let s=0;s<r.length;s++)n[s]=r[s];for(let s=0;s<t;s++)n[r.length+s]=i;return n}function Iie(r,e){let t=null,i;if(i=0,!t){for(t=1;r[r.length-t]===i;)t++;t--}return r.subarray(0,r.length-t)}function yA(r){this.key=[];for(let e=0;e<3;e++)this.key.push(new Uint8Array(r.subarray(e*8,e*8+8)));this.encrypt=function(e){return HB(KB(this.key[2]),HB(KB(this.key[1]),HB(KB(this.key[0]),e,!0,0,null,null),!1,0,null,null),!0)}}function wie(){this.BlockSize=8,this.KeySize=16,this.setKey=function(o){if(this.masking=new Array(16),this.rotate=new Array(16),this.reset(),o.length===this.KeySize)this.keySchedule(o);else throw new Error("CAST-128: keys must be 16 bytes");return!0},this.reset=function(){for(let o=0;o<16;o++)this.masking[o]=0,this.rotate[o]=0},this.getBlockSize=function(){return this.BlockSize},this.encrypt=function(o){let a=new Array(o.length);for(let c=0;c<o.length;c+=8){let l=o[c]<<24|o[c+1]<<16|o[c+2]<<8|o[c+3],d=o[c+4]<<24|o[c+5]<<16|o[c+6]<<8|o[c+7],u;u=d,d=l^t(d,this.masking[0],this.rotate[0]),l=u,u=d,d=l^i(d,this.masking[1],this.rotate[1]),l=u,u=d,d=l^n(d,this.masking[2],this.rotate[2]),l=u,u=d,d=l^t(d,this.masking[3],this.rotate[3]),l=u,u=d,d=l^i(d,this.masking[4],this.rotate[4]),l=u,u=d,d=l^n(d,this.masking[5],this.rotate[5]),l=u,u=d,d=l^t(d,this.masking[6],this.rotate[6]),l=u,u=d,d=l^i(d,this.masking[7],this.rotate[7]),l=u,u=d,d=l^n(d,this.masking[8],this.rotate[8]),l=u,u=d,d=l^t(d,this.masking[9],this.rotate[9]),l=u,u=d,d=l^i(d,this.masking[10],this.rotate[10]),l=u,u=d,d=l^n(d,this.masking[11],this.rotate[11]),l=u,u=d,d=l^t(d,this.masking[12],this.rotate[12]),l=u,u=d,d=l^i(d,this.masking[13],this.rotate[13]),l=u,u=d,d=l^n(d,this.masking[14],this.rotate[14]),l=u,u=d,d=l^t(d,this.masking[15],this.rotate[15]),l=u,a[c]=d>>>24&255,a[c+1]=d>>>16&255,a[c+2]=d>>>8&255,a[c+3]=d&255,a[c+4]=l>>>24&255,a[c+5]=l>>>16&255,a[c+6]=l>>>8&255,a[c+7]=l&255}return a},this.decrypt=function(o){let a=new Array(o.length);for(let c=0;c<o.length;c+=8){let l=o[c]<<24|o[c+1]<<16|o[c+2]<<8|o[c+3],d=o[c+4]<<24|o[c+5]<<16|o[c+6]<<8|o[c+7],u;u=d,d=l^t(d,this.masking[15],this.rotate[15]),l=u,u=d,d=l^n(d,this.masking[14],this.rotate[14]),l=u,u=d,d=l^i(d,this.masking[13],this.rotate[13]),l=u,u=d,d=l^t(d,this.masking[12],this.rotate[12]),l=u,u=d,d=l^n(d,this.masking[11],this.rotate[11]),l=u,u=d,d=l^i(d,this.masking[10],this.rotate[10]),l=u,u=d,d=l^t(d,this.masking[9],this.rotate[9]),l=u,u=d,d=l^n(d,this.masking[8],this.rotate[8]),l=u,u=d,d=l^i(d,this.masking[7],this.rotate[7]),l=u,u=d,d=l^t(d,this.masking[6],this.rotate[6]),l=u,u=d,d=l^n(d,this.masking[5],this.rotate[5]),l=u,u=d,d=l^i(d,this.masking[4],this.rotate[4]),l=u,u=d,d=l^t(d,this.masking[3],this.rotate[3]),l=u,u=d,d=l^n(d,this.masking[2],this.rotate[2]),l=u,u=d,d=l^i(d,this.masking[1],this.rotate[1]),l=u,u=d,d=l^t(d,this.masking[0],this.rotate[0]),l=u,a[c]=d>>>24&255,a[c+1]=d>>>16&255,a[c+2]=d>>>8&255,a[c+3]=d&255,a[c+4]=l>>>24&255,a[c+5]=l>>16&255,a[c+6]=l>>8&255,a[c+7]=l&255}return a};let r=new Array(4);r[0]=new Array(4),r[0][0]=[4,0,13,15,12,14,8],r[0][1]=[5,2,16,18,17,19,10],r[0][2]=[6,3,23,22,21,20,9],r[0][3]=[7,1,26,25,27,24,11],r[1]=new Array(4),r[1][0]=[0,6,21,23,20,22,16],r[1][1]=[1,4,0,2,1,3,18],r[1][2]=[2,5,7,6,5,4,17],r[1][3]=[3,7,10,9,11,8,19],r[2]=new Array(4),r[2][0]=[4,0,13,15,12,14,8],r[2][1]=[5,2,16,18,17,19,10],r[2][2]=[6,3,23,22,21,20,9],r[2][3]=[7,1,26,25,27,24,11],r[3]=new Array(4),r[3][0]=[0,6,21,23,20,22,16],r[3][1]=[1,4,0,2,1,3,18],r[3][2]=[2,5,7,6,5,4,17],r[3][3]=[3,7,10,9,11,8,19];let e=new Array(4);e[0]=new Array(4),e[0][0]=[24,25,23,22,18],e[0][1]=[26,27,21,20,22],e[0][2]=[28,29,19,18,25],e[0][3]=[30,31,17,16,28],e[1]=new Array(4),e[1][0]=[3,2,12,13,8],e[1][1]=[1,0,14,15,13],e[1][2]=[7,6,8,9,3],e[1][3]=[5,4,10,11,7],e[2]=new Array(4),e[2][0]=[19,18,28,29,25],e[2][1]=[17,16,30,31,28],e[2][2]=[23,22,24,25,18],e[2][3]=[21,20,26,27,22],e[3]=new Array(4),e[3][0]=[8,9,7,6,3],e[3][1]=[10,11,5,4,7],e[3][2]=[12,13,3,2,8],e[3][3]=[14,15,1,0,13],this.keySchedule=function(o){let a=new Array(8),c=new Array(32),l;for(let h=0;h<4;h++)l=h*4,a[h]=o[l]<<24|o[l+1]<<16|o[l+2]<<8|o[l+3];let d=[6,7,4,5],u=0,f;for(let h=0;h<2;h++)for(let g=0;g<4;g++){for(l=0;l<4;l++){let p=r[g][l];f=a[p[1]],f^=s[4][a[p[2]>>>2]>>>24-8*(p[2]&3)&255],f^=s[5][a[p[3]>>>2]>>>24-8*(p[3]&3)&255],f^=s[6][a[p[4]>>>2]>>>24-8*(p[4]&3)&255],f^=s[7][a[p[5]>>>2]>>>24-8*(p[5]&3)&255],f^=s[d[l]][a[p[6]>>>2]>>>24-8*(p[6]&3)&255],a[p[0]]=f}for(l=0;l<4;l++){let p=e[g][l];f=s[4][a[p[0]>>>2]>>>24-8*(p[0]&3)&255],f^=s[5][a[p[1]>>>2]>>>24-8*(p[1]&3)&255],f^=s[6][a[p[2]>>>2]>>>24-8*(p[2]&3)&255],f^=s[7][a[p[3]>>>2]>>>24-8*(p[3]&3)&255],f^=s[4+l][a[p[4]>>>2]>>>24-8*(p[4]&3)&255],c[u]=f,u++}}for(let h=0;h<16;h++)this.masking[h]=c[h],this.rotate[h]=c[16+h]&31};function t(o,a,c){let l=a+o,d=l<<c|l>>>32-c;return(s[0][d>>>24]^s[1][d>>>16&255])-s[2][d>>>8&255]+s[3][d&255]}function i(o,a,c){let l=a^o,d=l<<c|l>>>32-c;return s[0][d>>>24]-s[1][d>>>16&255]+s[2][d>>>8&255]^s[3][d&255]}function n(o,a,c){let l=a-o,d=l<<c|l>>>32-c;return(s[0][d>>>24]+s[1][d>>>16&255]^s[2][d>>>8&255])-s[3][d&255]}let s=new Array(8);s[0]=[821772500,2678128395,1810681135,1059425402,505495343,2617265619,1610868032,3483355465,3218386727,2294005173,3791863952,2563806837,1852023008,365126098,3269944861,584384398,677919599,3229601881,4280515016,2002735330,1136869587,3744433750,2289869850,2731719981,2714362070,879511577,1639411079,575934255,717107937,2857637483,576097850,2731753936,1725645e3,2810460463,5111599,767152862,2543075244,1251459544,1383482551,3052681127,3089939183,3612463449,1878520045,1510570527,2189125840,2431448366,582008916,3163445557,1265446783,1354458274,3529918736,3202711853,3073581712,3912963487,3029263377,1275016285,4249207360,2905708351,3304509486,1442611557,3585198765,2712415662,2731849581,3248163920,2283946226,208555832,2766454743,1331405426,1447828783,3315356441,3108627284,2957404670,2981538698,3339933917,1669711173,286233437,1465092821,1782121619,3862771680,710211251,980974943,1651941557,430374111,2051154026,704238805,4128970897,3144820574,2857402727,948965521,3333752299,2227686284,718756367,2269778983,2731643755,718440111,2857816721,3616097120,1113355533,2478022182,410092745,1811985197,1944238868,2696854588,1415722873,1682284203,1060277122,1998114690,1503841958,82706478,2315155686,1068173648,845149890,2167947013,1768146376,1993038550,3566826697,3390574031,940016341,3355073782,2328040721,904371731,1205506512,4094660742,2816623006,825647681,85914773,2857843460,1249926541,1417871568,3287612,3211054559,3126306446,1975924523,1353700161,2814456437,2438597621,1800716203,722146342,2873936343,1151126914,4160483941,2877670899,458611604,2866078500,3483680063,770352098,2652916994,3367839148,3940505011,3585973912,3809620402,718646636,2504206814,2914927912,3631288169,2857486607,2860018678,575749918,2857478043,718488780,2069512688,3548183469,453416197,1106044049,3032691430,52586708,3378514636,3459808877,3211506028,1785789304,218356169,3571399134,3759170522,1194783844,1523787992,3007827094,1975193539,2555452411,1341901877,3045838698,3776907964,3217423946,2802510864,2889438986,1057244207,1636348243,3761863214,1462225785,2632663439,481089165,718503062,24497053,3332243209,3344655856,3655024856,3960371065,1195698900,2971415156,3710176158,2115785917,4027663609,3525578417,2524296189,2745972565,3564906415,1372086093,1452307862,2780501478,1476592880,3389271281,18495466,2378148571,901398090,891748256,3279637769,3157290713,2560960102,1447622437,4284372637,216884176,2086908623,1879786977,3588903153,2242455666,2938092967,3559082096,2810645491,758861177,1121993112,215018983,642190776,4169236812,1196255959,2081185372,3508738393,941322904,4124243163,2877523539,1848581667,2205260958,3180453958,2589345134,3694731276,550028657,2519456284,3789985535,2973870856,2093648313,443148163,46942275,2734146937,1117713533,1115362972,1523183689,3717140224,1551984063],s[1]=[522195092,4010518363,1776537470,960447360,4267822970,4005896314,1435016340,1929119313,2913464185,1310552629,3579470798,3724818106,2579771631,1594623892,417127293,2715217907,2696228731,1508390405,3994398868,3925858569,3695444102,4019471449,3129199795,3770928635,3520741761,990456497,4187484609,2783367035,21106139,3840405339,631373633,3783325702,532942976,396095098,3548038825,4267192484,2564721535,2011709262,2039648873,620404603,3776170075,2898526339,3612357925,4159332703,1645490516,223693667,1567101217,3362177881,1029951347,3470931136,3570957959,1550265121,119497089,972513919,907948164,3840628539,1613718692,3594177948,465323573,2659255085,654439692,2575596212,2699288441,3127702412,277098644,624404830,4100943870,2717858591,546110314,2403699828,3655377447,1321679412,4236791657,1045293279,4010672264,895050893,2319792268,494945126,1914543101,2777056443,3894764339,2219737618,311263384,4275257268,3458730721,669096869,3584475730,3835122877,3319158237,3949359204,2005142349,2713102337,2228954793,3769984788,569394103,3855636576,1425027204,108000370,2736431443,3671869269,3043122623,1750473702,2211081108,762237499,3972989403,2798899386,3061857628,2943854345,867476300,964413654,1591880597,1594774276,2179821409,552026980,3026064248,3726140315,2283577634,3110545105,2152310760,582474363,1582640421,1383256631,2043843868,3322775884,1217180674,463797851,2763038571,480777679,2718707717,2289164131,3118346187,214354409,200212307,3810608407,3025414197,2674075964,3997296425,1847405948,1342460550,510035443,4080271814,815934613,833030224,1620250387,1945732119,2703661145,3966000196,1388869545,3456054182,2687178561,2092620194,562037615,1356438536,3409922145,3261847397,1688467115,2150901366,631725691,3840332284,549916902,3455104640,394546491,837744717,2114462948,751520235,2221554606,2415360136,3999097078,2063029875,803036379,2702586305,821456707,3019566164,360699898,4018502092,3511869016,3677355358,2402471449,812317050,49299192,2570164949,3259169295,2816732080,3331213574,3101303564,2156015656,3705598920,3546263921,143268808,3200304480,1638124008,3165189453,3341807610,578956953,2193977524,3638120073,2333881532,807278310,658237817,2969561766,1641658566,11683945,3086995007,148645947,1138423386,4158756760,1981396783,2401016740,3699783584,380097457,2680394679,2803068651,3334260286,441530178,4016580796,1375954390,761952171,891809099,2183123478,157052462,3683840763,1592404427,341349109,2438483839,1417898363,644327628,2233032776,2353769706,2201510100,220455161,1815641738,182899273,2995019788,3627381533,3702638151,2890684138,1052606899,588164016,1681439879,4038439418,2405343923,4229449282,167996282,1336969661,1688053129,2739224926,1543734051,1046297529,1138201970,2121126012,115334942,1819067631,1902159161,1941945968,2206692869,1159982321],s[2]=[2381300288,637164959,3952098751,3893414151,1197506559,916448331,2350892612,2932787856,3199334847,4009478890,3905886544,1373570990,2450425862,4037870920,3778841987,2456817877,286293407,124026297,3001279700,1028597854,3115296800,4208886496,2691114635,2188540206,1430237888,1218109995,3572471700,308166588,570424558,2187009021,2455094765,307733056,1310360322,3135275007,1384269543,2388071438,863238079,2359263624,2801553128,3380786597,2831162807,1470087780,1728663345,4072488799,1090516929,532123132,2389430977,1132193179,2578464191,3051079243,1670234342,1434557849,2711078940,1241591150,3314043432,3435360113,3091448339,1812415473,2198440252,267246943,796911696,3619716990,38830015,1526438404,2806502096,374413614,2943401790,1489179520,1603809326,1920779204,168801282,260042626,2358705581,1563175598,2397674057,1356499128,2217211040,514611088,2037363785,2186468373,4022173083,2792511869,2913485016,1173701892,4200428547,3896427269,1334932762,2455136706,602925377,2835607854,1613172210,41346230,2499634548,2457437618,2188827595,41386358,4172255629,1313404830,2405527007,3801973774,2217704835,873260488,2528884354,2478092616,4012915883,2555359016,2006953883,2463913485,575479328,2218240648,2099895446,660001756,2341502190,3038761536,3888151779,3848713377,3286851934,1022894237,1620365795,3449594689,1551255054,15374395,3570825345,4249311020,4151111129,3181912732,310226346,1133119310,530038928,136043402,2476768958,3107506709,2544909567,1036173560,2367337196,1681395281,1758231547,3641649032,306774401,1575354324,3716085866,1990386196,3114533736,2455606671,1262092282,3124342505,2768229131,4210529083,1833535011,423410938,660763973,2187129978,1639812e3,3508421329,3467445492,310289298,272797111,2188552562,2456863912,310240523,677093832,1013118031,901835429,3892695601,1116285435,3036471170,1337354835,243122523,520626091,277223598,4244441197,4194248841,1766575121,594173102,316590669,742362309,3536858622,4176435350,3838792410,2501204839,1229605004,3115755532,1552908988,2312334149,979407927,3959474601,1148277331,176638793,3614686272,2083809052,40992502,1340822838,2731552767,3535757508,3560899520,1354035053,122129617,7215240,2732932949,3118912700,2718203926,2539075635,3609230695,3725561661,1928887091,2882293555,1988674909,2063640240,2491088897,1459647954,4189817080,2302804382,1113892351,2237858528,1927010603,4002880361,1856122846,1594404395,2944033133,3855189863,3474975698,1643104450,4054590833,3431086530,1730235576,2984608721,3084664418,2131803598,4178205752,267404349,1617849798,1616132681,1462223176,736725533,2327058232,551665188,2945899023,1749386277,2575514597,1611482493,674206544,2201269090,3642560800,728599968,1680547377,2620414464,1388111496,453204106,4156223445,1094905244,2754698257,2201108165,3757000246,2704524545,3922940700,3996465027],s[3]=[2645754912,532081118,2814278639,3530793624,1246723035,1689095255,2236679235,4194438865,2116582143,3859789411,157234593,2045505824,4245003587,1687664561,4083425123,605965023,672431967,1336064205,3376611392,214114848,4258466608,3232053071,489488601,605322005,3998028058,264917351,1912574028,756637694,436560991,202637054,135989450,85393697,2152923392,3896401662,2895836408,2145855233,3535335007,115294817,3147733898,1922296357,3464822751,4117858305,1037454084,2725193275,2127856640,1417604070,1148013728,1827919605,642362335,2929772533,909348033,1346338451,3547799649,297154785,1917849091,4161712827,2883604526,3968694238,1469521537,3780077382,3375584256,1763717519,136166297,4290970789,1295325189,2134727907,2798151366,1566297257,3672928234,2677174161,2672173615,965822077,2780786062,289653839,1133871874,3491843819,35685304,1068898316,418943774,672553190,642281022,2346158704,1954014401,3037126780,4079815205,2030668546,3840588673,672283427,1776201016,359975446,3750173538,555499703,2769985273,1324923,69110472,152125443,3176785106,3822147285,1340634837,798073664,1434183902,15393959,216384236,1303690150,3881221631,3711134124,3960975413,106373927,2578434224,1455997841,1801814300,1578393881,1854262133,3188178946,3258078583,2302670060,1539295533,3505142565,3078625975,2372746020,549938159,3278284284,2620926080,181285381,2865321098,3970029511,68876850,488006234,1728155692,2608167508,836007927,2435231793,919367643,3339422534,3655756360,1457871481,40520939,1380155135,797931188,234455205,2255801827,3990488299,397000196,739833055,3077865373,2871719860,4022553888,772369276,390177364,3853951029,557662966,740064294,1640166671,1699928825,3535942136,622006121,3625353122,68743880,1742502,219489963,1664179233,1577743084,1236991741,410585305,2366487942,823226535,1050371084,3426619607,3586839478,212779912,4147118561,1819446015,1911218849,530248558,3486241071,3252585495,2886188651,3410272728,2342195030,20547779,2982490058,3032363469,3631753222,312714466,1870521650,1493008054,3491686656,615382978,4103671749,2534517445,1932181,2196105170,278426614,6369430,3274544417,2913018367,697336853,2143000447,2946413531,701099306,1558357093,2805003052,3500818408,2321334417,3567135975,216290473,3591032198,23009561,1996984579,3735042806,2024298078,3739440863,569400510,2339758983,3016033873,3097871343,3639523026,3844324983,3256173865,795471839,2951117563,4101031090,4091603803,3603732598,971261452,534414648,428311343,3389027175,2844869880,694888862,1227866773,2456207019,3043454569,2614353370,3749578031,3676663836,459166190,4132644070,1794958188,51825668,2252611902,3084671440,2036672799,3436641603,1099053433,2469121526,3059204941,1323291266,2061838604,1018778475,2233344254,2553501054,334295216,3556750194,1065731521,183467730],s[4]=[2127105028,745436345,2601412319,2788391185,3093987327,500390133,1155374404,389092991,150729210,3891597772,3523549952,1935325696,716645080,946045387,2901812282,1774124410,3869435775,4039581901,3293136918,3438657920,948246080,363898952,3867875531,1286266623,1598556673,68334250,630723836,1104211938,1312863373,613332731,2377784574,1101634306,441780740,3129959883,1917973735,2510624549,3238456535,2544211978,3308894634,1299840618,4076074851,1756332096,3977027158,297047435,3790297736,2265573040,3621810518,1311375015,1667687725,47300608,3299642885,2474112369,201668394,1468347890,576830978,3594690761,3742605952,1958042578,1747032512,3558991340,1408974056,3366841779,682131401,1033214337,1545599232,4265137049,206503691,103024618,2855227313,1337551222,2428998917,2963842932,4015366655,3852247746,2796956967,3865723491,3747938335,247794022,3755824572,702416469,2434691994,397379957,851939612,2314769512,218229120,1380406772,62274761,214451378,3170103466,2276210409,3845813286,28563499,446592073,1693330814,3453727194,29968656,3093872512,220656637,2470637031,77972100,1667708854,1358280214,4064765667,2395616961,325977563,4277240721,4220025399,3605526484,3355147721,811859167,3069544926,3962126810,652502677,3075892249,4132761541,3498924215,1217549313,3250244479,3858715919,3053989961,1538642152,2279026266,2875879137,574252750,3324769229,2651358713,1758150215,141295887,2719868960,3515574750,4093007735,4194485238,1082055363,3417560400,395511885,2966884026,179534037,3646028556,3738688086,1092926436,2496269142,257381841,3772900718,1636087230,1477059743,2499234752,3811018894,2675660129,3285975680,90732309,1684827095,1150307763,1723134115,3237045386,1769919919,1240018934,815675215,750138730,2239792499,1234303040,1995484674,138143821,675421338,1145607174,1936608440,3238603024,2345230278,2105974004,323969391,779555213,3004902369,2861610098,1017501463,2098600890,2628620304,2940611490,2682542546,1171473753,3656571411,3687208071,4091869518,393037935,159126506,1662887367,1147106178,391545844,3452332695,1891500680,3016609650,1851642611,546529401,1167818917,3194020571,2848076033,3953471836,575554290,475796850,4134673196,450035699,2351251534,844027695,1080539133,86184846,1554234488,3692025454,1972511363,2018339607,1491841390,1141460869,1061690759,4244549243,2008416118,2351104703,2868147542,1598468138,722020353,1027143159,212344630,1387219594,1725294528,3745187956,2500153616,458938280,4129215917,1828119673,544571780,3503225445,2297937496,1241802790,267843827,2694610800,1397140384,1558801448,3782667683,1806446719,929573330,2234912681,400817706,616011623,4121520928,3603768725,1761550015,1968522284,4053731006,4192232858,4005120285,872482584,3140537016,3894607381,2287405443,1963876937,3663887957,1584857e3,2975024454,1833426440,4025083860],s[5]=[4143615901,749497569,1285769319,3795025788,2514159847,23610292,3974978748,844452780,3214870880,3751928557,2213566365,1676510905,448177848,3730751033,4086298418,2307502392,871450977,3222878141,4110862042,3831651966,2735270553,1310974780,2043402188,1218528103,2736035353,4274605013,2702448458,3936360550,2693061421,162023535,2827510090,687910808,23484817,3784910947,3371371616,779677500,3503626546,3473927188,4157212626,3500679282,4248902014,2466621104,3899384794,1958663117,925738300,1283408968,3669349440,1840910019,137959847,2679828185,1239142320,1315376211,1547541505,1690155329,739140458,3128809933,3933172616,3876308834,905091803,1548541325,4040461708,3095483362,144808038,451078856,676114313,2861728291,2469707347,993665471,373509091,2599041286,4025009006,4170239449,2149739950,3275793571,3749616649,2794760199,1534877388,572371878,2590613551,1753320020,3467782511,1405125690,4270405205,633333386,3026356924,3475123903,632057672,2846462855,1404951397,3882875879,3915906424,195638627,2385783745,3902872553,1233155085,3355999740,2380578713,2702246304,2144565621,3663341248,3894384975,2502479241,4248018925,3094885567,1594115437,572884632,3385116731,767645374,1331858858,1475698373,3793881790,3532746431,1321687957,619889600,1121017241,3440213920,2070816767,2833025776,1933951238,4095615791,890643334,3874130214,859025556,360630002,925594799,1764062180,3920222280,4078305929,979562269,2810700344,4087740022,1949714515,546639971,1165388173,3069891591,1495988560,922170659,1291546247,2107952832,1813327274,3406010024,3306028637,4241950635,153207855,2313154747,1608695416,1150242611,1967526857,721801357,1220138373,3691287617,3356069787,2112743302,3281662835,1111556101,1778980689,250857638,2298507990,673216130,2846488510,3207751581,3562756981,3008625920,3417367384,2198807050,529510932,3547516680,3426503187,2364944742,102533054,2294910856,1617093527,1204784762,3066581635,1019391227,1069574518,1317995090,1691889997,3661132003,510022745,3238594800,1362108837,1817929911,2184153760,805817662,1953603311,3699844737,120799444,2118332377,207536705,2282301548,4120041617,145305846,2508124933,3086745533,3261524335,1877257368,2977164480,3160454186,2503252186,4221677074,759945014,254147243,2767453419,3801518371,629083197,2471014217,907280572,3900796746,940896768,2751021123,2625262786,3161476951,3661752313,3260732218,1425318020,2977912069,1496677566,3988592072,2140652971,3126511541,3069632175,977771578,1392695845,1698528874,1411812681,1369733098,1343739227,3620887944,1142123638,67414216,3102056737,3088749194,1626167401,2546293654,3941374235,697522451,33404913,143560186,2595682037,994885535,1247667115,3859094837,2699155541,3547024625,4114935275,2968073508,3199963069,2732024527,1237921620,951448369,1898488916,1211705605,2790989240,2233243581,3598044975],s[6]=[2246066201,858518887,1714274303,3485882003,713916271,2879113490,3730835617,539548191,36158695,1298409750,419087104,1358007170,749914897,2989680476,1261868530,2995193822,2690628854,3443622377,3780124940,3796824509,2976433025,4259637129,1551479e3,512490819,1296650241,951993153,2436689437,2460458047,144139966,3136204276,310820559,3068840729,643875328,1969602020,1680088954,2185813161,3283332454,672358534,198762408,896343282,276269502,3014846926,84060815,197145886,376173866,3943890818,3813173521,3545068822,1316698879,1598252827,2633424951,1233235075,859989710,2358460855,3503838400,3409603720,1203513385,1193654839,2792018475,2060853022,207403770,1144516871,3068631394,1121114134,177607304,3785736302,326409831,1929119770,2983279095,4183308101,3474579288,3200513878,3228482096,119610148,1170376745,3378393471,3163473169,951863017,3337026068,3135789130,2907618374,1183797387,2015970143,4045674555,2182986399,2952138740,3928772205,384012900,2454997643,10178499,2879818989,2596892536,111523738,2995089006,451689641,3196290696,235406569,1441906262,3890558523,3013735005,4158569349,1644036924,376726067,1006849064,3664579700,2041234796,1021632941,1374734338,2566452058,371631263,4007144233,490221539,206551450,3140638584,1053219195,1853335209,3412429660,3562156231,735133835,1623211703,3104214392,2738312436,4096837757,3366392578,3110964274,3956598718,3196820781,2038037254,3877786376,2339753847,300912036,3766732888,2372630639,1516443558,4200396704,1574567987,4069441456,4122592016,2699739776,146372218,2748961456,2043888151,35287437,2596680554,655490400,1132482787,110692520,1031794116,2188192751,1324057718,1217253157,919197030,686247489,3261139658,1028237775,3135486431,3059715558,2460921700,986174950,2661811465,4062904701,2752986992,3709736643,367056889,1353824391,731860949,1650113154,1778481506,784341916,357075625,3608602432,1074092588,2480052770,3811426202,92751289,877911070,3600361838,1231880047,480201094,3756190983,3094495953,434011822,87971354,363687820,1717726236,1901380172,3926403882,2481662265,400339184,1490350766,2661455099,1389319756,2558787174,784598401,1983468483,30828846,3550527752,2716276238,3841122214,1765724805,1955612312,1277890269,1333098070,1564029816,2704417615,1026694237,3287671188,1260819201,3349086767,1016692350,1582273796,1073413053,1995943182,694588404,1025494639,3323872702,3551898420,4146854327,453260480,1316140391,1435673405,3038941953,3486689407,1622062951,403978347,817677117,950059133,4246079218,3278066075,1486738320,1417279718,481875527,2549965225,3933690356,760697757,1452955855,3897451437,1177426808,1702951038,4085348628,2447005172,1084371187,3516436277,3068336338,1073369276,1027665953,3284188590,1230553676,1368340146,2226246512,267243139,2274220762,4070734279,2497715176,2423353163,2504755875],s[7]=[3793104909,3151888380,2817252029,895778965,2005530807,3871412763,237245952,86829237,296341424,3851759377,3974600970,2475086196,709006108,1994621201,2972577594,937287164,3734691505,168608556,3189338153,2225080640,3139713551,3033610191,3025041904,77524477,185966941,1208824168,2344345178,1721625922,3354191921,1066374631,1927223579,1971335949,2483503697,1551748602,2881383779,2856329572,3003241482,48746954,1398218158,2050065058,313056748,4255789917,393167848,1912293076,940740642,3465845460,3091687853,2522601570,2197016661,1727764327,364383054,492521376,1291706479,3264136376,1474851438,1685747964,2575719748,1619776915,1814040067,970743798,1561002147,2925768690,2123093554,1880132620,3151188041,697884420,2550985770,2607674513,2659114323,110200136,1489731079,997519150,1378877361,3527870668,478029773,2766872923,1022481122,431258168,1112503832,897933369,2635587303,669726182,3383752315,918222264,163866573,3246985393,3776823163,114105080,1903216136,761148244,3571337562,1690750982,3166750252,1037045171,1888456500,2010454850,642736655,616092351,365016990,1185228132,4174898510,1043824992,2023083429,2241598885,3863320456,3279669087,3674716684,108438443,2132974366,830746235,606445527,4173263986,2204105912,1844756978,2532684181,4245352700,2969441100,3796921661,1335562986,4061524517,2720232303,2679424040,634407289,885462008,3294724487,3933892248,2094100220,339117932,4048830727,3202280980,1458155303,2689246273,1022871705,2464987878,3714515309,353796843,2822958815,4256850100,4052777845,551748367,618185374,3778635579,4020649912,1904685140,3069366075,2670879810,3407193292,2954511620,4058283405,2219449317,3135758300,1120655984,3447565834,1474845562,3577699062,550456716,3466908712,2043752612,881257467,869518812,2005220179,938474677,3305539448,3850417126,1315485940,3318264702,226533026,965733244,321539988,1136104718,804158748,573969341,3708209826,937399083,3290727049,2901666755,1461057207,4013193437,4066861423,3242773476,2421326174,1581322155,3028952165,786071460,3900391652,3918438532,1485433313,4023619836,3708277595,3678951060,953673138,1467089153,1930354364,1533292819,2492563023,1346121658,1685000834,1965281866,3765933717,4190206607,2052792609,3515332758,690371149,3125873887,2180283551,2903598061,3933952357,436236910,289419410,14314871,1242357089,2904507907,1616633776,2666382180,585885352,3471299210,2699507360,1432659641,277164553,3354103607,770115018,2303809295,3741942315,3177781868,2853364978,2269453327,3774259834,987383833,1290892879,225909803,1741533526,890078084,1496906255,1111072499,916028167,243534141,1252605537,2204162171,531204876,290011180,3916834213,102027703,237315147,209093447,1486785922,220223953,2758195998,4175039106,82940208,3127791296,2569425252,518464269,1353887104,3941492737,2377294467,3935040926]}function IA(r){this.cast5=new wie,this.cast5.setKey(r),this.encrypt=function(e){return this.cast5.encrypt(e)}}function is(r,e){return(r<<e|r>>>32-e)&rn}function to(r,e){return r[e]|r[e+1]<<8|r[e+2]<<16|r[e+3]<<24}function Ea(r,e,t){r.splice(e,4,t&255,t>>>8&255,t>>>16&255,t>>>24&255)}function ze(r,e){return r>>>e*8&255}function xie(){let r=null,e=null,t=-1,i=[],n=[[],[],[],[]];function s(g){r=g;let p,I,m,w,B,C=[],x=[],Q=[],R,U=[],T,D,b,_=[[8,1,7,13,6,15,3,2,0,11,5,9,14,12,10,4],[2,8,11,13,15,7,6,14,3,1,9,4,0,10,12,5]],v=[[14,12,11,8,1,2,3,5,15,4,10,6,7,0,9,13],[1,14,2,11,4,12,3,7,6,13,10,5,15,9,0,8]],E=[[11,10,5,14,6,13,9,0,12,8,15,3,2,4,7,1],[4,12,7,5,1,6,9,10,0,14,13,8,2,11,3,15]],P=[[13,7,15,4,1,2,6,14,9,11,3,0,8,5,12,10],[11,9,5,1,12,3,13,14,6,4,7,15,2,0,8,10]],N=[0,8,1,9,2,10,3,11,4,12,5,13,6,14,7,15],j=[0,9,2,11,4,13,6,15,8,1,10,3,12,5,14,7],z=[[],[]],L=[[],[],[],[]];function G(ee){return ee^ee>>2^[0,90,180,238][ee&3]}function K(ee){return ee^ee>>1^ee>>2^[0,238,180,90][ee&3]}function $(ee,Z){let me,le,Ae;for(me=0;me<8;me++)le=Z>>>24,Z=Z<<8&rn|ee>>>24,ee=ee<<8&rn,Ae=le<<1,le&128&&(Ae^=333),Z^=le^Ae<<16,Ae^=le>>>1,le&1&&(Ae^=166),Z^=Ae<<24|Ae<<8;return Z}function te(ee,Z){let me=Z>>4,le=Z&15,Ae=_[ee][me^le],ve=v[ee][N[le]^j[me]];return P[ee][N[ve]^j[Ae]]<<4|E[ee][Ae^ve]}function re(ee,Z){let me=ze(ee,0),le=ze(ee,1),Ae=ze(ee,2),ve=ze(ee,3);switch(R){case 4:me=z[1][me]^ze(Z[3],0),le=z[0][le]^ze(Z[3],1),Ae=z[0][Ae]^ze(Z[3],2),ve=z[1][ve]^ze(Z[3],3);case 3:me=z[1][me]^ze(Z[2],0),le=z[1][le]^ze(Z[2],1),Ae=z[0][Ae]^ze(Z[2],2),ve=z[0][ve]^ze(Z[2],3);case 2:me=z[0][z[0][me]^ze(Z[1],0)]^ze(Z[0],0),le=z[0][z[1][le]^ze(Z[1],1)]^ze(Z[0],1),Ae=z[1][z[0][Ae]^ze(Z[1],2)]^ze(Z[0],2),ve=z[1][z[1][ve]^ze(Z[1],3)]^ze(Z[0],3)}return L[0][me]^L[1][le]^L[2][Ae]^L[3][ve]}for(r=r.slice(0,32),p=r.length;p!==16&&p!==24&&p!==32;)r[p++]=0;for(p=0;p<r.length;p+=4)Q[p>>2]=to(r,p);for(p=0;p<256;p++)z[0][p]=te(0,p),z[1][p]=te(1,p);for(p=0;p<256;p++)T=z[1][p],D=G(T),b=K(T),L[0][p]=T+(D<<8)+(b<<16)+(b<<24),L[2][p]=D+(b<<8)+(T<<16)+(b<<24),T=z[0][p],D=G(T),b=K(T),L[1][p]=b+(b<<8)+(D<<16)+(T<<24),L[3][p]=D+(T<<8)+(b<<16)+(D<<24);for(R=Q.length/2,p=0;p<R;p++)I=Q[p+p],C[p]=I,m=Q[p+p+1],x[p]=m,U[R-p-1]=$(I,m);for(p=0;p<40;p+=2)I=16843009*p,m=I+16843009,I=re(I,C),m=is(re(m,x),8),i[p]=I+m&rn,i[p+1]=is(I+2*m,9);for(p=0;p<256;p++)switch(I=m=w=B=p,R){case 4:I=z[1][I]^ze(U[3],0),m=z[0][m]^ze(U[3],1),w=z[0][w]^ze(U[3],2),B=z[1][B]^ze(U[3],3);case 3:I=z[1][I]^ze(U[2],0),m=z[1][m]^ze(U[2],1),w=z[0][w]^ze(U[2],2),B=z[0][B]^ze(U[2],3);case 2:n[0][p]=L[0][z[0][z[0][I]^ze(U[1],0)]^ze(U[0],0)],n[1][p]=L[1][z[0][z[1][m]^ze(U[1],1)]^ze(U[0],1)],n[2][p]=L[2][z[1][z[0][w]^ze(U[1],2)]^ze(U[0],2)],n[3][p]=L[3][z[1][z[1][B]^ze(U[1],3)]^ze(U[0],3)]}}function o(g){return n[0][ze(g,0)]^n[1][ze(g,1)]^n[2][ze(g,2)]^n[3][ze(g,3)]}function a(g){return n[0][ze(g,3)]^n[1][ze(g,0)]^n[2][ze(g,1)]^n[3][ze(g,2)]}function c(g,p){let I=o(p[0]),m=a(p[1]);p[2]=is(p[2]^I+m+i[4*g+8]&rn,31),p[3]=is(p[3],1)^I+2*m+i[4*g+9]&rn,I=o(p[2]),m=a(p[3]),p[0]=is(p[0]^I+m+i[4*g+10]&rn,31),p[1]=is(p[1],1)^I+2*m+i[4*g+11]&rn}function l(g,p){let I=o(p[0]),m=a(p[1]);p[2]=is(p[2],1)^I+m+i[4*g+10]&rn,p[3]=is(p[3]^I+2*m+i[4*g+11]&rn,31),I=o(p[2]),m=a(p[3]),p[0]=is(p[0],1)^I+m+i[4*g+8]&rn,p[1]=is(p[1]^I+2*m+i[4*g+9]&rn,31)}function d(){i=[],n=[[],[],[],[]]}function u(g,p){e=g,t=p;let I=[to(e,t)^i[0],to(e,t+4)^i[1],to(e,t+8)^i[2],to(e,t+12)^i[3]];for(let m=0;m<8;m++)c(m,I);return Ea(e,t,I[2]^i[4]),Ea(e,t+4,I[3]^i[5]),Ea(e,t+8,I[0]^i[6]),Ea(e,t+12,I[1]^i[7]),t+=16,e}function f(g,p){e=g,t=p;let I=[to(e,t)^i[4],to(e,t+4)^i[5],to(e,t+8)^i[6],to(e,t+12)^i[7]];for(let m=7;m>=0;m--)l(m,I);Ea(e,t,I[2]^i[0]),Ea(e,t+4,I[3]^i[1]),Ea(e,t+8,I[0]^i[2]),Ea(e,t+12,I[1]^i[3]),t+=16}function h(){return e}return{name:"twofish",blocksize:128/8,open:s,close:d,encrypt:u,decrypt:f,finalize:h}}function wA(r){this.tf=xie(),this.tf.open(Array.from(r),0),this.encrypt=function(e){return this.tf.encrypt(Array.from(e),0)}}function Tn(){}function xA(r){this.bf=new Tn,this.bf.init(r),this.encrypt=function(e){return this.bf.encryptBlock(e)}}function wd(r,e,t,i){r[e]+=t[i],r[e+1]+=t[i+1]+(r[e]<t[i])}function VT(r,e){r[0]+=e,r[1]+=r[0]<e}function Ba(r,e,t,i,n,s,o,a){wd(r,t,r,i),wd(r,t,e,o);let c=r[s]^r[t],l=r[s+1]^r[t+1];r[s]=l,r[s+1]=c,wd(r,n,r,s),c=r[i]^r[n],l=r[i+1]^r[n+1],r[i]=c>>>24^l<<8,r[i+1]=l>>>24^c<<8,wd(r,t,r,i),wd(r,t,e,a),c=r[s]^r[t],l=r[s+1]^r[t+1],r[s]=c>>>16^l<<16,r[s+1]=l>>>16^c<<16,wd(r,n,r,s),c=r[i]^r[n],l=r[i+1]^r[n+1],r[i]=l>>>31^c<<1,r[i+1]=c>>>31^l<<1}function YT(r,e){let t=new Uint32Array(32),i=new Uint32Array(r.b.buffer,r.b.byteOffset,32);for(let s=0;s<16;s++)t[s]=r.h[s],t[s+16]=pN[s];t[24]^=r.t0[0],t[25]^=r.t0[1];let n=e?4294967295:0;t[28]^=n,t[29]^=n;for(let s=0;s<12;s++){let o=s<<4;Ba(t,i,0,8,16,24,Xr[o+0],Xr[o+1]),Ba(t,i,2,10,18,26,Xr[o+2],Xr[o+3]),Ba(t,i,4,12,20,28,Xr[o+4],Xr[o+5]),Ba(t,i,6,14,22,30,Xr[o+6],Xr[o+7]),Ba(t,i,0,10,20,30,Xr[o+8],Xr[o+9]),Ba(t,i,2,12,22,24,Xr[o+10],Xr[o+11]),Ba(t,i,4,14,16,26,Xr[o+12],Xr[o+13]),Ba(t,i,6,8,18,28,Xr[o+14],Xr[o+15])}for(let s=0;s<16;s++)r.h[s]^=t[s]^t[s+16]}function tI(r,e,t,i){if(r>F1)throw new Error(`outlen must be at most ${F1} (given: ${r})`);return new _1(r,e,t,i)}function xi(r,e,t){return r[t+0]=e,r[t+1]=e>>8,r[t+2]=e>>16,r[t+3]=e>>24,r}function Oc(r,e,t){if(e>Number.MAX_SAFE_INTEGER)throw new Error("LE64: large numbers unsupported");let i=e;for(let n=t;n<t+7;n++)r[n]=i,i=(i-r[n])/256;return r}function GB(r,e,t){let i=new Uint8Array(64),n=new Uint8Array(4+e.length);if(xi(n,r,0),n.set(e,4),r<=64)return tI(r).update(n).digest(t),t;let s=Math.ceil(r/32)-2;for(let a=0;a<s;a++)tI(64).update(a===0?n:i).digest(i),t.set(i.subarray(0,32),a*32);let o=new Uint8Array(tI(r-32*s).update(i).digest());return t.set(o,s*32),t}function WT(r,e,t,i){return r.fn.XOR(e.byteOffset,t.byteOffset,i.byteOffset),e}function Uie(r,e,t,i){return r.fn.G(e.byteOffset,t.byteOffset,i.byteOffset,r.refs.gZ.byteOffset),i}function Rie(r,e,t,i){return r.fn.G2(e.byteOffset,t.byteOffset,i.byteOffset,r.refs.gZ.byteOffset),i}function*Nie(r,e,t,i,n,s,o,a){r.refs.prngTmp.fill(0);let c=r.refs.prngTmp.subarray(0,48);Oc(c,e,0),Oc(c,t,8),Oc(c,i,16),Oc(c,n,24),Oc(c,s,32),Oc(c,fb,40);for(let l=1;l<=o;l++){Oc(r.refs.prngTmp,l,c.length);let d=Rie(r,r.refs.ZERO1024,r.refs.prngTmp,r.refs.prngR);for(let u=l===1?a*8:0;u<d.length;u+=8)yield d.subarray(u,u+8)}return[]}function qie({type:r,version:e,tagLength:t,password:i,salt:n,ad:s,secret:o,parallelism:a,memorySize:c,passes:l}){let d=(u,f,h,g)=>{if(f<h||f>g)throw new Error(`${u} size should be between ${h} and ${g} bytes`)};if(r!==fb||e!==mN)throw new Error("Unsupported type or version");return d("password",i,Die,Sie),d("salt",n,kie,vie),d("tag",t,bie,Bie),d("memory",c,8*a,Qie),s&&d("associated data",s,0,_ie),o&&d("secret",o,0,Fie),{type:r,version:e,tagLength:t,password:i,salt:n,ad:s,secret:o,lanes:a,memorySize:c,passes:l}}function Mie(r,{memory:e,instance:t}){if(!Tie)throw new Error("BigEndian system not supported");let i=qie({type:fb,version:mN,...r}),{G:n,G2:s,xor:o,getLZ:a}=t.exports,c={},l={};l.G=n,l.G2=s,l.XOR=o;let d=4*i.lanes*Math.floor(i.memorySize/(4*i.lanes)),u=d*ro+10*yN;if(e.buffer.byteLength<u){let D=Math.ceil((u-e.buffer.byteLength)/Oie);e.grow(D)}let f=0;c.gZ=new Uint8Array(e.buffer,f,ro),f+=c.gZ.length,c.prngR=new Uint8Array(e.buffer,f,ro),f+=c.prngR.length,c.prngTmp=new Uint8Array(e.buffer,f,ro),f+=c.prngTmp.length,c.ZERO1024=new Uint8Array(e.buffer,f,1024),f+=c.ZERO1024.length;let h=new Uint32Array(e.buffer,f,2);f+=h.length*Uint32Array.BYTES_PER_ELEMENT;let g={fn:l,refs:c},p=new Uint8Array(e.buffer,f,ro);f+=p.length;let I=new Uint8Array(e.buffer,f,i.memorySize*ro),m=new Uint8Array(e.buffer,0,f),w=Lie(i),B=d/i.lanes,C=new Array(i.lanes).fill(null).map(()=>new Array(B)),x=(D,b)=>(C[D][b]=I.subarray(D*B*1024+b*1024,D*B*1024+b*1024+ro),C[D][b]);for(let D=0;D<i.lanes;D++){let b=new Uint8Array(w.length+8);b.set(w),xi(b,0,w.length),xi(b,D,w.length+4),GB(ro,b,x(D,0)),xi(b,1,w.length),GB(ro,b,x(D,1))}let Q=4,R=B/Q;for(let D=0;D<i.passes;D++)for(let b=0;b<Q;b++){let _=D===0&&b<=1;for(let v=0;v<i.lanes;v++){let E=b===0&&D===0?2:0,P=_?Nie(g,D,v,b,d,i.passes,R,E):null;for(E;E<R;E++){let N=b*R+E,j=N>0?C[v][N-1]:C[v][B-1],z=_?P.next().value:j;a(h.byteOffset,z.byteOffset,v,i.lanes,D,b,E,Q,R);let L=h[0],G=h[1];D===0&&x(v,N),Uie(g,j,C[L][G],D>0?p:C[v][N]),D>0&&WT(g,C[v][N],p,C[v][N])}}}let U=C[0][B-1];for(let D=1;D<i.lanes;D++)WT(g,U,U,C[D][B-1]);let T=GB(i.tagLength,U,new Uint8Array(i.tagLength));return m.fill(0),e.grow(0),T}function Lie(r){let e=tI(Pie),t=new Uint8Array(4),i=new Uint8Array(24);xi(i,r.lanes,0),xi(i,r.tagLength,4),xi(i,r.memorySize,8),xi(i,r.passes,12),xi(i,r.version,16),xi(i,r.type,20);let n=[i];r.password?(n.push(xi(new Uint8Array(4),r.password.length,0)),n.push(r.password)):n.push(t),r.salt?(n.push(xi(new Uint8Array(4),r.salt.length,0)),n.push(r.salt)):n.push(t),r.secret?(n.push(xi(new Uint8Array(4),r.secret.length,0)),n.push(r.secret)):n.push(t),r.ad?(n.push(xi(new Uint8Array(4),r.ad.length,0)),n.push(r.ad)):n.push(t),e.update(Hie(n));let s=e.digest();return new Uint8Array(s)}function Hie(r){if(r.length===1)return r[0];let e=0;for(let n=0;n<r.length;n++){if(!(r[n]instanceof Uint8Array))throw new Error("concatArrays: Data must be in the form of a Uint8Array");e+=r[n].length}let t=new Uint8Array(e),i=0;return r.forEach(n=>{t.set(n,i),i+=n.length}),t}async function Kie(r,e,t){let i={env:{memory:r}};if(jy===void 0)try{let s=await e(i);return jy=!0,s}catch{jy=!1}return(jy?e:t)(i)}async function Gie(r,e){let t=new WebAssembly.Memory({initial:1040,maximum:65536}),i=await Kie(t,r,e);return s=>Mie(s,{instance:i.instance,memory:t})}function IN(r,e,t,i){function n(o,a,c){var l=c?WebAssembly.instantiateStreaming:WebAssembly.instantiate,d=c?WebAssembly.compileStreaming:WebAssembly.compile;return a?l(o,a):d(o)}var s=null;return s=Buffer.from(t,"base64"),n(s,i,!1)}function zie(r){return IN(0,null,"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",r)}function Jie(r){return IN(0,null,"AGFzbQEAAAABPwhgBH9/f38AYAABf2AAAGADf39/AGARf39/f39/f39/f39/f39/f38AYAl/f39/f39/f38Bf2ABfwBgAX8BfwITAQNlbnYGbWVtb3J5AgGQCICABAMLCgIDBAAABQEGBwEEBQFwAQICBgkBfwFBkIjAAgsHfQoDeG9yAAEBRwADAkcyAAQFZ2V0TFoABRlfX2luZGlyZWN0X2Z1bmN0aW9uX3RhYmxlAQALX2luaXRpYWxpemUAABBfX2Vycm5vX2xvY2F0aW9uAAkJc3RhY2tTYXZlAAYMc3RhY2tSZXN0b3JlAAcKc3RhY2tBbGxvYwAICQcBAEEBCwEACssaCgMAAQtQAQJ/A0AgACAEQQN0IgNqIAIgA2opAwAgASADaikDAIU3AwAgACADQQhyIgNqIAIgA2opAwAgASADaikDAIU3AwAgBEECaiIEQYABRw0ACwveDwICfgF/IAAgAUEDdGoiEyATKQMAIhEgACAFQQN0aiIBKQMAIhJ8IBFCAYZC/v///x+DIBJC/////w+DfnwiETcDACAAIA1BA3RqIgUgESAFKQMAhUIgiSIRNwMAIAAgCUEDdGoiCSARIAkpAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAEgESABKQMAhUIoiSIRNwMAIBMgESATKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACAFIBEgBSkDAIVCMIkiETcDACAJIBEgCSkDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgASARIAEpAwCFQgGJNwMAIAAgAkEDdGoiDSANKQMAIhEgACAGQQN0aiICKQMAIhJ8IBFCAYZC/v///x+DIBJC/////w+DfnwiETcDACAAIA5BA3RqIgYgESAGKQMAhUIgiSIRNwMAIAAgCkEDdGoiCiARIAopAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAIgESACKQMAhUIoiSIRNwMAIA0gESANKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACAGIBEgBikDAIVCMIkiETcDACAKIBEgCikDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgAiARIAIpAwCFQgGJNwMAIAAgA0EDdGoiDiAOKQMAIhEgACAHQQN0aiIDKQMAIhJ8IBFCAYZC/v///x+DIBJC/////w+DfnwiETcDACAAIA9BA3RqIgcgESAHKQMAhUIgiSIRNwMAIAAgC0EDdGoiCyARIAspAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAMgESADKQMAhUIoiSIRNwMAIA4gESAOKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACAHIBEgBykDAIVCMIkiETcDACALIBEgCykDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgAyARIAMpAwCFQgGJNwMAIAAgBEEDdGoiDyAPKQMAIhEgACAIQQN0aiIEKQMAIhJ8IBFCAYZC/v///x+DIBJC/////w+DfnwiETcDACAAIBBBA3RqIgggESAIKQMAhUIgiSIRNwMAIAAgDEEDdGoiACARIAApAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAQgESAEKQMAhUIoiSIRNwMAIA8gESAPKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACAIIBEgCCkDAIVCMIkiETcDACAAIBEgACkDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgBCARIAQpAwCFQgGJNwMAIBMgEykDACIRIAIpAwAiEnwgEUIBhkL+////H4MgEkL/////D4N+fCIRNwMAIAggESAIKQMAhUIgiSIRNwMAIAsgESALKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACACIBEgAikDAIVCKIkiETcDACATIBEgEykDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgCCARIAgpAwCFQjCJIhE3AwAgCyARIAspAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAIgESACKQMAhUIBiTcDACANIA0pAwAiESADKQMAIhJ8IBFCAYZC/v///x+DIBJC/////w+DfnwiETcDACAFIBEgBSkDAIVCIIkiETcDACAAIBEgACkDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgAyARIAMpAwCFQiiJIhE3AwAgDSARIA0pAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAUgESAFKQMAhUIwiSIRNwMAIAAgESAAKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACADIBEgAykDAIVCAYk3AwAgDiAOKQMAIhEgBCkDACISfCARQgGGQv7///8fgyASQv////8Pg358IhE3AwAgBiARIAYpAwCFQiCJIhE3AwAgCSARIAkpAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAQgESAEKQMAhUIoiSIRNwMAIA4gESAOKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACAGIBEgBikDAIVCMIkiETcDACAJIBEgCSkDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgBCARIAQpAwCFQgGJNwMAIA8gDykDACIRIAEpAwAiEnwgEUIBhkL+////H4MgEkL/////D4N+fCIRNwMAIAcgESAHKQMAhUIgiSIRNwMAIAogESAKKQMAIhJ8IBFC/////w+DIBJCAYZC/v///x+DfnwiETcDACABIBEgASkDAIVCKIkiETcDACAPIBEgDykDACISfCARQv////8PgyASQgGGQv7///8fg358IhE3AwAgByARIAcpAwCFQjCJIhE3AwAgCiARIAopAwAiEnwgEUL/////D4MgEkIBhkL+////H4N+fCIRNwMAIAEgESABKQMAhUIBiTcDAAvdCAEPfwNAIAIgBUEDdCIGaiABIAZqKQMAIAAgBmopAwCFNwMAIAIgBkEIciIGaiABIAZqKQMAIAAgBmopAwCFNwMAIAVBAmoiBUGAAUcNAAsDQCADIARBA3QiAGogACACaikDADcDACADIARBAXIiAEEDdCIBaiABIAJqKQMANwMAIAMgBEECciIBQQN0IgVqIAIgBWopAwA3AwAgAyAEQQNyIgVBA3QiBmogAiAGaikDADcDACADIARBBHIiBkEDdCIHaiACIAdqKQMANwMAIAMgBEEFciIHQQN0IghqIAIgCGopAwA3AwAgAyAEQQZyIghBA3QiCWogAiAJaikDADcDACADIARBB3IiCUEDdCIKaiACIApqKQMANwMAIAMgBEEIciIKQQN0IgtqIAIgC2opAwA3AwAgAyAEQQlyIgtBA3QiDGogAiAMaikDADcDACADIARBCnIiDEEDdCINaiACIA1qKQMANwMAIAMgBEELciINQQN0Ig5qIAIgDmopAwA3AwAgAyAEQQxyIg5BA3QiD2ogAiAPaikDADcDACADIARBDXIiD0EDdCIQaiACIBBqKQMANwMAIAMgBEEOciIQQQN0IhFqIAIgEWopAwA3AwAgAyAEQQ9yIhFBA3QiEmogAiASaikDADcDACADIARB//8DcSAAQf//A3EgAUH//wNxIAVB//8DcSAGQf//A3EgB0H//wNxIAhB//8DcSAJQf//A3EgCkH//wNxIAtB//8DcSAMQf//A3EgDUH//wNxIA5B//8DcSAPQf//A3EgEEH//wNxIBFB//8DcRACIARB8ABJIQAgBEEQaiEEIAANAAtBACEBIANBAEEBQRBBEUEgQSFBMEExQcAAQcEAQdAAQdEAQeAAQeEAQfAAQfEAEAIgA0ECQQNBEkETQSJBI0EyQTNBwgBBwwBB0gBB0wBB4gBB4wBB8gBB8wAQAiADQQRBBUEUQRVBJEElQTRBNUHEAEHFAEHUAEHVAEHkAEHlAEH0AEH1ABACIANBBkEHQRZBF0EmQSdBNkE3QcYAQccAQdYAQdcAQeYAQecAQfYAQfcAEAIgA0EIQQlBGEEZQShBKUE4QTlByABByQBB2ABB2QBB6ABB6QBB+ABB+QAQAiADQQpBC0EaQRtBKkErQTpBO0HKAEHLAEHaAEHbAEHqAEHrAEH6AEH7ABACIANBDEENQRxBHUEsQS1BPEE9QcwAQc0AQdwAQd0AQewAQe0AQfwAQf0AEAIgA0EOQQ9BHkEfQS5BL0E+QT9BzgBBzwBB3gBB3wBB7gBB7wBB/gBB/wAQAgNAIAIgAUEDdCIAaiIEIAAgA2opAwAgBCkDAIU3AwAgAiAAQQhyIgRqIgUgAyAEaikDACAFKQMAhTcDACACIABBEHIiBGoiBSADIARqKQMAIAUpAwCFNwMAIAIgAEEYciIAaiIEIAAgA2opAwAgBCkDAIU3AwAgAUEEaiIBQYABRw0ACwsWACAAIAEgAiADEAMgACACIAIgAxADC3sCAX8BfiACIQkgATUCACEKIAQgBXIEQCABKAIEIANwIQkLIAAgCTYCACAAIAdBAWsgBSAEGyAIbCAGQQFrQQBBfyAGGyACIAlGG2oiASAFQQFqIAhsQQAgBBtqIAGtIAogCn5CIIh+QiCIp0F/c2ogByAIbHA2AgQgAAsEACMACwYAIAAkAAsQACMAIABrQXBxIgAkACAACwUAQYAICw==",r)}function Vie(r){return r&&r.__esModule&&Object.prototype.hasOwnProperty.call(r,"default")?r.default:r}function Yie(){if(XT)return zB;XT=1;function r(i){this.name="Bzip2Error",this.message=i,this.stack=new Error().stack}r.prototype=new Error;var e={Error:function(i){throw new r(i)}},t={};return t.Bzip2Error=r,t.crcTable=[0,79764919,159529838,222504665,319059676,398814059,445009330,507990021,638119352,583659535,797628118,726387553,890018660,835552979,1015980042,944750013,1276238704,1221641927,1167319070,1095957929,1595256236,1540665371,1452775106,1381403509,1780037320,1859660671,1671105958,1733955601,2031960084,2111593891,1889500026,1952343757,2552477408,2632100695,2443283854,2506133561,2334638140,2414271883,2191915858,2254759653,3190512472,3135915759,3081330742,3009969537,2905550212,2850959411,2762807018,2691435357,3560074640,3505614887,3719321342,3648080713,3342211916,3287746299,3467911202,3396681109,4063920168,4143685023,4223187782,4286162673,3779000052,3858754371,3904687514,3967668269,881225847,809987520,1023691545,969234094,662832811,591600412,771767749,717299826,311336399,374308984,453813921,533576470,25881363,88864420,134795389,214552010,2023205639,2086057648,1897238633,1976864222,1804852699,1867694188,1645340341,1724971778,1587496639,1516133128,1461550545,1406951526,1302016099,1230646740,1142491917,1087903418,2896545431,2825181984,2770861561,2716262478,3215044683,3143675388,3055782693,3001194130,2326604591,2389456536,2200899649,2280525302,2578013683,2640855108,2418763421,2498394922,3769900519,3832873040,3912640137,3992402750,4088425275,4151408268,4197601365,4277358050,3334271071,3263032808,3476998961,3422541446,3585640067,3514407732,3694837229,3640369242,1762451694,1842216281,1619975040,1682949687,2047383090,2127137669,1938468188,2001449195,1325665622,1271206113,1183200824,1111960463,1543535498,1489069629,1434599652,1363369299,622672798,568075817,748617968,677256519,907627842,853037301,1067152940,995781531,51762726,131386257,177728840,240578815,269590778,349224269,429104020,491947555,4046411278,4126034873,4172115296,4234965207,3794477266,3874110821,3953728444,4016571915,3609705398,3555108353,3735388376,3664026991,3290680682,3236090077,3449943556,3378572211,3174993278,3120533705,3032266256,2961025959,2923101090,2868635157,2813903052,2742672763,2604032198,2683796849,2461293480,2524268063,2284983834,2364738477,2175806836,2238787779,1569362073,1498123566,1409854455,1355396672,1317987909,1246755826,1192025387,1137557660,2072149281,2135122070,1912620623,1992383480,1753615357,1816598090,1627664531,1707420964,295390185,358241886,404320391,483945776,43990325,106832002,186451547,266083308,932423249,861060070,1041341759,986742920,613929101,542559546,756411363,701822548,3316196985,3244833742,3425377559,3370778784,3601682597,3530312978,3744426955,3689838204,3819031489,3881883254,3928223919,4007849240,4037393693,4100235434,4180117107,4259748804,2310601993,2373574846,2151335527,2231098320,2596047829,2659030626,2470359227,2550115596,2947551409,2876312838,2788305887,2733848168,3165939309,3094707162,3040238851,2985771188],t.array=function(i){var n=0,s=0,o=[0,1,3,7,15,31,63,127,255];return function(a){for(var c=0;a>0;){var l=8-n;a>=l?(c<<=l,c|=o[l]&i[s++],n=0,a-=l):(c<<=a,c|=(i[s]&o[a]<<8-a-n)>>8-a-n,n+=a,a=0)}return c}},t.simple=function(i,n){var s=t.array(i),o=t.header(s),a=!1,c=1e5*o,l=new Int32Array(c);do a=t.decompress(s,n,l,c);while(!a)},t.header=function(i){this.byteCount=new Int32Array(256),this.symToByte=new Uint8Array(256),this.mtfSymbol=new Int32Array(256),this.selectors=new Uint8Array(32768),i(24)!=4348520&&e.Error("No magic number found");var n=i(8)-48;return(n<1||n>9)&&e.Error("Not a BZIP archive"),n},t.decompress=function(i,n,s,o,a){for(var c=20,l=258,d=0,u=1,f=50,h=-1,g="",p=0;p<6;p++)g+=i(8).toString(16);if(g=="177245385090"){var I=i(32)|0;return I!==a&&e.Error("Error in bzip2: crc32 do not match"),i(null),null}g!="314159265359"&&e.Error("Invalid bzip data");var m=i(32)|0;i(1)&&e.Error("unsupported obsolete version");var w=i(24);w>o&&e.Error("Initial position larger than buffer size");var B=i(16),C=0;for(p=0;p<16;p++)if(B&1<<15-p){var x=i(16);for(te=0;te<16;te++)x&1<<15-te&&(this.symToByte[C++]=16*p+te)}var Q=i(3);(Q<2||Q>6)&&e.Error("Invalid bzip data");var R=i(15);R==0&&e.Error("Invalid bzip data");for(var p=0;p<Q;p++)this.mtfSymbol[p]=p;for(var p=0;p<R;p++){for(var te=0;i(1);te++)te>=Q&&e.Error("Invalid bzip data");for(var U=this.mtfSymbol[te],x=te-1;x>=0;x--)this.mtfSymbol[x+1]=this.mtfSymbol[x];this.mtfSymbol[0]=U,this.selectors[p]=U}for(var G=C+2,T=[],D=new Uint8Array(l),b=new Uint16Array(c+1),_,te=0;te<Q;te++){B=i(5);for(var p=0;p<G;p++){for(;(B<1||B>c)&&e.Error("Invalid bzip data"),!!i(1);)i(1)?B--:B++;D[p]=B}var v,E;v=E=D[0];for(var p=1;p<G;p++)D[p]>E?E=D[p]:D[p]<v&&(v=D[p]);_=T[te]={},_.permute=new Int32Array(l),_.limit=new Int32Array(c+1),_.base=new Int32Array(c+1),_.minLen=v,_.maxLen=E;for(var P=_.base,N=_.limit,j=0,p=v;p<=E;p++)for(var B=0;B<G;B++)D[B]==p&&(_.permute[j++]=B);for(p=v;p<=E;p++)b[p]=N[p]=0;for(p=0;p<G;p++)b[D[p]]++;for(j=B=0,p=v;p<E;p++)j+=b[p],N[p]=j-1,j<<=1,P[p+1]=j-(B+=b[p]);N[E]=j+b[E]-1,P[v]=0}for(var p=0;p<256;p++)this.mtfSymbol[p]=p,this.byteCount[p]=0;var z,L,G,K;for(z=L=G=K=0;;){for(G--||(G=f-1,K>=R&&e.Error("Invalid bzip data"),_=T[this.selectors[K++]],P=_.base,N=_.limit),p=_.minLen,te=i(p);p>_.maxLen&&e.Error("Invalid bzip data"),!(te<=N[p]);)p++,te=te<<1|i(1);te-=P[p],(te<0||te>=l)&&e.Error("Invalid bzip data");var $=_.permute[te];if($==d||$==u){z||(z=1,B=0),$==d?B+=z:B+=2*z,z<<=1;continue}if(z)for(z=0,L+B>o&&e.Error("Invalid bzip data"),U=this.symToByte[this.mtfSymbol[0]],this.byteCount[U]+=B;B--;)s[L++]=U;if($>C)break;L>=o&&e.Error("Invalid bzip data"),p=$-1,U=this.mtfSymbol[p];for(var x=p-1;x>=0;x--)this.mtfSymbol[x+1]=this.mtfSymbol[x];this.mtfSymbol[0]=U,U=this.symToByte[U],this.byteCount[U]++,s[L++]=U}(w<0||w>=L)&&e.Error("Invalid bzip data");for(var te=0,p=0;p<256;p++)x=te+this.byteCount[p],this.byteCount[p]=te,te=x;for(var p=0;p<L;p++)U=s[p]&255,s[this.byteCount[U]]|=p<<8,this.byteCount[U]++;var re=0,ee=0,Z=0;L&&(re=s[w],ee=re&255,re>>=8,Z=-1),L=L;for(var me,le,Ae;L;){for(L--,le=ee,re=s[re],ee=re&255,re>>=8,Z++==3?(me=ee,Ae=le,ee=-1):(me=1,Ae=ee);me--;)h=(h<<8^this.crcTable[(h>>24^Ae)&255])&4294967295,n(Ae);ee!=le&&(Z=0)}return h=(h^-1)>>>0,(h|0)!=(m|0)&&e.Error("Error in bzip2: crc32 do not match"),a=(h^(a<<1|a>>>31))&4294967295,a},zB=t,zB}function Wie(){if(ZT)return JB;ZT=1;var r=[0,1,3,7,15,31,63,127,255];return JB=function(t){var i=0,n=0,s=t(),o=function(a){if(a===null&&i!=0){i=0,n++;return}for(var c=0;a>0;){n>=s.length&&(n=0,s=t());var l=8-i;i===0&&a>0&&o.bytesRead++,a>=l?(c<<=l,c|=r[l]&s[n++],i=0,a-=l):(c<<=a,c|=(s[n]&r[a]<<8-a-i)>>8-a-i,i+=a,a=0)}return c};return o.bytesRead=0,o},JB}function Xie(){if(eU)return jB;eU=1;let r=Yie(),e=Wie();jB=t;function t(i){let n=[],s=0,o=0,a=!1,c=!1,l=null,d=null;function u(p){if(o){let I=1e5*o,m=new Int32Array(I),w=[],B=function(C){w.push(C)};return d=r.decompress(l,B,m,I,d),d===null?(o=0,!1):(p(new Uint8Array(w)),!0)}else return o=r.header(l),d=0,!1}let f=0;function h(p){if(!a)try{return u(function(I){p.enqueue(I),I!==null&&(f+=I.length)})}catch(I){return p.error(I),a=!0,!0}}let g;return new ReadableStream({start(){g=i.getReader()},async pull(p){try{for(;;){for(;!(c||l&&s-l.bytesRead+1>=25e3+1e5*(o||4));){let{value:I,done:m}=await g.read();m?c=!0:(n.push(I),s+=I.length,l===null&&(l=e(function(){return n.shift()})))}for(;c?l&&s>l.bytesRead:l&&s-l.bytesRead+1>=25e3+1e5*(o||4);)if(h(p))return;if(c&&!a&&(!l||s<=l.bytesRead)){d===null?p.close():p.error(new Error("input stream ended prematurely"));return}}}catch(I){p.error(I)}},async cancel(p){await g.abort(p)}},{highWaterMark:0})}return jB}var oo,Pd,rU,iU,Wf,an,R6,xt,Lf,y,pe,N6,F,yB,rI,iI,kr,KW,Bi,kd,YW,ds,ZW,L6,nI,tX,rX,iX,nX,sX,oX,aX,cX,lX,dX,sn,Ud,jc,_a,va,_X,co,ft,Sd,tA,nA,O1,gU,TX,UX,bA,no,H1,os,OX,MX,nn,HX,aI,XB,G6,ki,K1,Ty,KX,ZB,GX,zX,IB,z6,z1,CU,JX,jX,e1,BU,Uy,J6,j6,SU,$6,Dd,cI,rZ,wB,dI,us,Ys,ss,FU,Si,uI,NU,dZ,uZ,xZ,CZ,Yy,Zf,fI,AI,hI,Wy,oA,qc,WI,gI,pI,CB,eT,NZ,lo,Xy,rT,LZ,HZ,go,KZ,xd,iT,n1,s1,di,JU,ba,Hf,JZ,EB,sT,oT,jZ,$Z,Cd,BB,XZ,ZZ,eee,aT,Qd,qy,Kf,tee,o1,a1,ree,iee,_d,nee,see,Gt,Ki,X1,ZI,ew,c1,$U,_d,VU,l1,YU,oee,cT,d1,Ws,ut,fs,Pa,ut,ut,ut,ut,cA,ut,aee,cee,lee,dee,bB,kn,vB,Z1,rA,uee,vr,fee,Xs,Gf,kB,u1,lT,zf,eb,dT,Aee,yI,hee,WU,XU,ZU,gee,pee,Zy,eI,mee,yee,Iee,wee,Ta,fo,Jf,uT,xee,qr,cn,Ct,ns,Kt,qd,Eee,Vc,wI,vee,kee,See,Od,Dee,fT,f1,lA,dA,gs,_ee,A1,CI,h1,uA,EI,BI,bI,m1,fA,Ree,mT,yT,kI,SI,DI,qee,Lee,Hee,Kee,AA,Jee,pd,Vee,xT,cR,Xee,Zs,gA,tte,ib,y1,FB,PA,Ei,fi,Lc,uR,fR,AR,ste,hR,ote,gR,dte,Xc,pa,ma,Yr,Wr,My,vT,kT,ST,md,yd,Ly,Hy,pte,mte,yte,Ite,wte,xte,Cte,Ete,Bte,bte,vte,ya,FI,I1,bR,kte,Ste,Ia,wa,PI,w1,ab,Dte,vR,kR,TI,SR,Hd,Hc,TB,QR,RI,FT,C1,so,ao,Fd,PR,Ky,Tte,RR,NR,qR,Hte,Kte,Gte,zte,Jte,jte,$te,Vte,Yte,Wte,jf,Xte,Zte,ere,tre,OR,MR,LR,HR,rre,ire,PT,TT,NI,KR,sre,ore,are,cre,xa,br,NB,lre,$f,Id,Gy,rl,mre,yre,Ire,E1,UT,wre,xre,RT,Cre,Ere,bre,NT,kre,Sre,Dre,cb,Qre,qT,B1,Fre,jR,Pre,$R,Tre,Ure,Rre,VR,YR,WR,Nre,qre,Ore,XR,Mre,Lre,Hre,Kre,Gre,Vf,Ca,b1,zre,Jre,ZR,jre,eN,tN,rN,iN,$re,Vre,Yre,Wre,zy,v1,Xre,Zre,eie,tie,MT,Jy,qB,k1,rie,iie,nie,OB,Di,Pe,lb,nN,S1,qI,sie,oie,aie,LT,HT,cie,zT,LB,$T,gie,hN,gN,Mc,ow,aw,pie,il,rn,Cie,Eie,pN,Xr,_1,F1,Yf,fb,mN,Bie,bie,vie,kie,Sie,Die,Qie,_ie,Fie,ro,Pie,Tie,yN,Oie,jy,jie,$ie,zB,XT,JB,ZT,jB,eU,wN,Zie,ene,Ab=J(()=>{oo=typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};Pd=Symbol("doneWritingPromise"),rU=Symbol("doneWritingResolve"),iU=Symbol("doneWritingReject"),Wf=Symbol("readingIndex"),an=class r extends Array{constructor(){super(),Object.setPrototypeOf(this,r.prototype),this[Pd]=new Promise((e,t)=>{this[rU]=e,this[iU]=t}),this[Pd].catch(()=>{})}};an.prototype.getReader=function(){return this[Wf]===void 0&&(this[Wf]=0),{read:async()=>(await this[Pd],this[Wf]===this.length?{value:void 0,done:!0}:{value:this[this[Wf]++],done:!1})}};an.prototype.readToEnd=async function(r){await this[Pd];let e=r(this.slice(this[Wf]));return this.length=0,e};an.prototype.clone=function(){let r=new an;return r[Pd]=this[Pd].then(()=>{r.push(...this)}),r};CA.prototype.write=async function(r){this.stream.push(r)};CA.prototype.close=async function(){this.stream[rU]()};CA.prototype.abort=async function(r){return this.stream[iU](r),r};CA.prototype.releaseLock=function(){};typeof oo.process=="object"&&oo.process.versions;R6=new WeakSet,xt=Symbol("externalBuffer");ms.prototype.read=async function(){return this[xt]&&this[xt].length?{done:!1,value:this[xt].shift()}:this._read()};ms.prototype.releaseLock=function(){this[xt]&&(this.stream[xt]=this[xt]),this._releaseLock()};ms.prototype.cancel=function(r){return this._cancel(r)};ms.prototype.readLine=async function(){let r=[],e;for(;!e;){let{done:t,value:i}=await this.read();if(i+="",t)return r.length?ui(r):void 0;let n=i.indexOf(` -`)+1;n&&(e=ui(r.concat(i.substr(0,n))),r=[]),n!==i.length&&r.push(i.substr(n))}return this.unshift(...r),e};ms.prototype.readByte=async function(){let{done:r,value:e}=await this.read();if(r)return;let t=e[0];return this.unshift(Rr(e,1)),t};ms.prototype.readBytes=async function(r){let e=[],t=0;for(;;){let{done:i,value:n}=await this.read();if(i)return e.length?ui(e):void 0;if(e.push(n),t+=n.length,t>=r){let s=ui(e);return this.unshift(Rr(s,r)),Rr(s,0,r)}}};ms.prototype.peekBytes=async function(r){let e=await this.readBytes(r);return this.unshift(e),e};ms.prototype.unshift=function(...r){if(this[xt]||(this[xt]=[]),r.length===1&&OI(r[0])&&this[xt].length&&r[0].length&&this[xt][0].byteOffset>=r[0].length){this[xt][0]=new Uint8Array(this[xt][0].buffer,this[xt][0].byteOffset-r[0].length,this[xt][0].byteLength+r[0].length);return}this[xt].unshift(...r.filter(e=>e&&e.length))};ms.prototype.readToEnd=async function(r=ui){let e=[];for(;;){let{done:t,value:i}=await this.read();if(t)break;e.push(i)}return r(e)};Lf=Symbol("byValue"),y={curve:{nistP256:"nistP256",p256:"nistP256",nistP384:"nistP384",p384:"nistP384",nistP521:"nistP521",p521:"nistP521",secp256k1:"secp256k1",ed25519Legacy:"ed25519Legacy",ed25519:"ed25519Legacy",curve25519Legacy:"curve25519Legacy",curve25519:"curve25519Legacy",brainpoolP256r1:"brainpoolP256r1",brainpoolP384r1:"brainpoolP384r1",brainpoolP512r1:"brainpoolP512r1"},s2k:{simple:0,salted:1,iterated:3,argon2:4,gnu:101},publicKey:{rsaEncryptSign:1,rsaEncrypt:2,rsaSign:3,elgamal:16,dsa:17,ecdh:18,ecdsa:19,eddsaLegacy:22,aedh:23,aedsa:24,x25519:25,x448:26,ed25519:27,ed448:28},symmetric:{idea:1,tripledes:2,cast5:3,blowfish:4,aes128:7,aes192:8,aes256:9,twofish:10},compression:{uncompressed:0,zip:1,zlib:2,bzip2:3},hash:{md5:1,sha1:2,ripemd:3,sha256:8,sha384:9,sha512:10,sha224:11,sha3_256:12,sha3_512:14},webHash:{"SHA-1":2,"SHA-256":8,"SHA-384":9,"SHA-512":10},aead:{eax:1,ocb:2,gcm:3,experimentalGCM:100},packet:{publicKeyEncryptedSessionKey:1,signature:2,symEncryptedSessionKey:3,onePassSignature:4,secretKey:5,publicKey:6,secretSubkey:7,compressedData:8,symmetricallyEncryptedData:9,marker:10,literalData:11,trust:12,userID:13,publicSubkey:14,userAttribute:17,symEncryptedIntegrityProtectedData:18,modificationDetectionCode:19,aeadEncryptedData:20,padding:21},literal:{binary:98,text:116,utf8:117,mime:109},signature:{binary:0,text:1,standalone:2,certGeneric:16,certPersona:17,certCasual:18,certPositive:19,certRevocation:48,subkeyBinding:24,keyBinding:25,key:31,keyRevocation:32,subkeyRevocation:40,timestamp:64,thirdParty:80},signatureSubpacket:{signatureCreationTime:2,signatureExpirationTime:3,exportableCertification:4,trustSignature:5,regularExpression:6,revocable:7,keyExpirationTime:9,placeholderBackwardsCompatibility:10,preferredSymmetricAlgorithms:11,revocationKey:12,issuerKeyID:16,notationData:20,preferredHashAlgorithms:21,preferredCompressionAlgorithms:22,keyServerPreferences:23,preferredKeyServer:24,primaryUserID:25,policyURI:26,keyFlags:27,signersUserID:28,reasonForRevocation:29,features:30,signatureTarget:31,embeddedSignature:32,issuerFingerprint:33,preferredAEADAlgorithms:34,preferredCipherSuites:39},keyFlags:{certifyKeys:1,signData:2,encryptCommunication:4,encryptStorage:8,splitPrivateKey:16,authentication:32,sharedPrivateKey:128},armor:{multipartSection:0,multipartLast:1,signed:2,message:3,publicKey:4,privateKey:5,signature:6},reasonForRevocation:{noReason:0,keySuperseded:1,keyCompromised:2,keyRetired:3,userIDInvalid:32},features:{modificationDetection:1,aead:2,v5Keys:4,seipdv2:8},write:function(r,e){if(typeof e=="number"&&(e=this.read(r,e)),r[e]!==void 0)return r[e];throw new Error("Invalid enum value.")},read:function(r,e){if(r[Lf]||(r[Lf]=[],Object.entries(r).forEach(([t,i])=>{r[Lf][i]=t})),r[Lf][e]!==void 0)return r[Lf][e];throw new Error("Invalid enum value.")}},pe={preferredHashAlgorithm:y.hash.sha512,preferredSymmetricAlgorithm:y.symmetric.aes256,preferredCompressionAlgorithm:y.compression.uncompressed,aeadProtect:!1,parseAEADEncryptedV4KeysAsLegacy:!1,preferredAEADAlgorithm:y.aead.gcm,aeadChunkSizeByte:12,v6Keys:!1,enableParsingV5Entities:!1,s2kType:y.s2k.iterated,s2kIterationCountByte:224,s2kArgon2Params:{passes:3,parallelism:4,memoryExponent:16},maxArgon2MemoryExponent:30,allowUnauthenticatedMessages:!1,allowUnauthenticatedStream:!1,minRSABits:2047,passwordCollisionCheck:!1,allowInsecureDecryptionWithSigningKeys:!1,allowInsecureVerificationWithReformattedKeys:!1,allowMissingKeyFlags:!1,constantTimePKCS1Decryption:!1,constantTimePKCS1DecryptionSupportedSymmetricAlgorithms:new Set([y.symmetric.aes128,y.symmetric.aes192,y.symmetric.aes256]),ignoreUnsupportedPackets:!0,ignoreMalformedPackets:!1,enforceGrammar:!0,additionalAllowedPackets:[],showVersion:!1,showComment:!1,versionString:"OpenPGP.js 6.3.1",commentString:"https://openpgpjs.org",maxUserIDLength:1024*5,maxDecompressedMessageSize:1/0,knownNotations:[],nonDeterministicSignaturesViaNotation:!0,useEllipticFallback:!0,rejectHashAlgorithms:new Set([y.hash.md5,y.hash.ripemd]),rejectMessageHashAlgorithms:new Set([y.hash.md5,y.hash.ripemd,y.hash.sha1]),rejectPublicKeyAlgorithms:new Set([y.publicKey.elgamal,y.publicKey.dsa]),rejectCurves:new Set([y.curve.secp256k1])},N6=(()=>{try{return process.env.NODE_ENV==="development"}catch{}return!1})(),F={isString:function(r){return typeof r=="string"||r instanceof String},nodeRequire:tU(import.meta.url),isArray:function(r){return r instanceof Array},isUint8Array:OI,isStream:or,getNobleCurve:async(r,e)=>{if(!pe.useEllipticFallback)throw new Error("This curve is only supported in the full build of OpenPGP.js");let{nobleCurves:t}=await Promise.resolve().then(function(){return Gre});switch(r){case y.publicKey.ecdh:case y.publicKey.ecdsa:{let i=t.get(e);if(!i)throw new Error("Unsupported curve");return i}case y.publicKey.x448:return t.get("x448");case y.publicKey.ed448:return t.get("ed448");default:throw new Error("Unsupported curve")}},readNumber:function(r){let e=0;for(let t=0;t<r.length;t++)e+=256**t*r[r.length-1-t];return e},writeNumber:function(r,e){let t=new Uint8Array(e);for(let i=0;i<e;i++)t[i]=r>>8*(e-i-1)&255;return t},readDate:function(r){let e=F.readNumber(r);return new Date(e*1e3)},writeDate:function(r){let e=Math.floor(r.getTime()/1e3);return F.writeNumber(e,4)},normalizeDate:function(r=Date.now()){return r===null||r===1/0?r:new Date(Math.floor(+r/1e3)*1e3)},readMPI:function(r){let t=(r[0]<<8|r[1])+7>>>3;return F.readExactSubarray(r,2,2+t)},readExactSubarray:function(r,e,t){if(r.length<t)throw new Error("Input array too short");return r.subarray(e,t)},leftPad(r,e){if(r.length>e)throw new Error("Input array too long");let t=new Uint8Array(e),i=e-r.length;return t.set(r,i),t},uint8ArrayToMPI:function(r){let e=F.uint8ArrayBitLength(r);if(e===0)throw new Error("Zero MPI");let t=r.subarray(r.length-Math.ceil(e/8)),i=new Uint8Array([(e&65280)>>8,e&255]);return F.concatUint8Array([i,t])},uint8ArrayBitLength:function(r){let e;for(e=0;e<r.length&&r[e]===0;e++);if(e===r.length)return 0;let t=r.subarray(e);return(t.length-1)*8+F.nbits(t[0])},hexToUint8Array:function(r){let e=new Uint8Array(r.length>>1);for(let t=0;t<r.length>>1;t++)e[t]=parseInt(r.substr(t<<1,2),16);return e},uint8ArrayToHex:function(r){let e="0123456789abcdef",t="";return r.forEach(i=>{t+=e[i>>4]+e[i&15]}),t},stringToUint8Array:function(r){return sr(r,e=>{if(!F.isString(e))throw new Error("stringToUint8Array: Data must be in the form of a string");let t=new Uint8Array(e.length);for(let i=0;i<e.length;i++)t[i]=e.charCodeAt(i);return t})},uint8ArrayToString:function(r){r=new Uint8Array(r);let e=[],t=16384,i=r.length;for(let n=0;n<i;n+=t)e.push(String.fromCharCode.apply(String,r.subarray(n,n+t<i?n+t:i)));return e.join("")},encodeUTF8:function(r){let e=new TextEncoder("utf-8");function t(i,n=!1){return e.encode(i,{stream:!n})}return sr(r,t,()=>t("",!0))},decodeUTF8:function(r){let e=new TextDecoder("utf-8");function t(i,n=!1){return e.decode(i,{stream:!n})}return sr(r,t,()=>t(new Uint8Array,!0))},concat:ui,concatUint8Array:nU,equalsUint8Array:function(r,e){if(!F.isUint8Array(r)||!F.isUint8Array(e))throw new Error("Data must be in the form of a Uint8Array");if(r.length!==e.length)return!1;for(let t=0;t<r.length;t++)if(r[t]!==e[t])return!1;return!0},findLastIndex:function(r,e){for(let t=r.length;t>=0;t--)if(e(r[t],t,r))return t;return-1},writeChecksum:function(r){let e=0;for(let t=0;t<r.length;t++)e=e+r[t]&65535;return F.writeNumber(e,2)},printDebug:function(r){N6&&console.log("[OpenPGP.js debug]",r)},printDebugError:function(r){N6&&console.error("[OpenPGP.js debug]",r)},nbits:function(r){let e=1,t=r>>>16;return t!==0&&(r=t,e+=16),t=r>>8,t!==0&&(r=t,e+=8),t=r>>4,t!==0&&(r=t,e+=4),t=r>>2,t!==0&&(r=t,e+=2),t=r>>1,t!==0&&(r=t,e+=1),e},double:function(r){let e=new Uint8Array(r.length),t=r.length-1;for(let i=0;i<t;i++)e[i]=r[i]<<1^r[i+1]>>7;return e[t]=r[t]<<1^(r[0]>>7)*135,e},shiftRight:function(r,e){if(e)for(let t=r.length-1;t>=0;t--)r[t]>>=e,t>0&&(r[t]|=r[t-1]<<8-e);return r},getWebCrypto:function(){let e=typeof oo<"u"&&oo.crypto&&oo.crypto.subtle||this.getNodeCrypto()?.webcrypto.subtle;if(!e)throw new Error("The WebCrypto API is not available");return e},getNodeCrypto:function(){return this.nodeRequire("crypto")},getNodeZlib:function(){return this.nodeRequire("zlib")},getNodeBuffer:function(){return(this.nodeRequire("buffer")||{}).Buffer},getHardwareConcurrency:function(){return typeof navigator<"u"?navigator.hardwareConcurrency||1:this.nodeRequire("os").cpus().length},isEmailAddress:function(r){return F.isString(r)?/^[^\p{C}\p{Z}@<>\\]+@[^\p{C}\p{Z}@<>\\]+[^\p{C}\p{Z}\p{P}]$/u.test(r):!1},canonicalizeEOL:function(r){let i=!1;return sr(r,n=>{i&&(n=F.concatUint8Array([new Uint8Array([13]),n])),n[n.length-1]===13?(i=!0,n=n.subarray(0,-1)):i=!1;let s,o=[];for(let l=0;s=n.indexOf(10,l)+1,s;l=s)n[s-2]!==13&&o.push(s);if(!o.length)return n;let a=new Uint8Array(n.length+o.length),c=0;for(let l=0;l<o.length;l++){let d=n.subarray(o[l-1]||0,o[l]);a.set(d,c),c+=d.length,a[c-1]=13,a[c]=10,c++}return a.set(n.subarray(o[o.length-1]||0),c),a},()=>i?new Uint8Array([13]):void 0)},nativeEOL:function(r){let i=!1;return sr(r,n=>{i&&n[0]!==10?n=F.concatUint8Array([new Uint8Array([13]),n]):n=new Uint8Array(n),n[n.length-1]===13?(i=!0,n=n.subarray(0,-1)):i=!1;let s,o=0;for(let a=0;a!==n.length;a=s){s=n.indexOf(13,a)+1,s||(s=n.length);let c=s-(n[s]===10?1:0);a&&n.copyWithin(o,a,c),o+=c-a}return n.subarray(0,o)},()=>i?new Uint8Array([13]):void 0)},removeTrailingSpaces:function(r){return r.split(` -`).map(e=>{let t=e.length-1;for(;t>=0&&(e[t]===" "||e[t]===" "||e[t]==="\r");t--);return e.substr(0,t+1)}).join(` -`)},wrapError:function(r,e){if(!e)return r instanceof Error?r:new Error(r);if(r instanceof Error){try{r.message+=": "+e.message,r.cause=e}catch{}return r}return new Error(r+": "+e.message,{cause:e})},constructAllowedPackets:function(r){let e={};return r.forEach(t=>{if(!t.tag)throw new Error("Invalid input: expected a packet class");e[t.tag]=t}),e},anyPromise:function(r){return new Promise((e,t)=>{let i;Promise.all(r.map(async n=>{try{e(await n)}catch(s){i=s}})).then(()=>{t(i)})})},selectUint8Array:function(r,e,t){let i=Math.max(e.length,t.length),n=new Uint8Array(i),s=0;for(let o=0;o<n.length;o++)n[o]=e[o]&256-r|t[o]&255+r,s+=r&o<e.length|1-r&o<t.length;return n.subarray(0,s)},selectUint8:function(r,e,t){return e&256-r|t&255+r},isAES:function(r){return r===y.symmetric.aes128||r===y.symmetric.aes192||r===y.symmetric.aes256}},yB=F.getNodeBuffer();yB?(rI=r=>yB.from(r).toString("base64"),iI=r=>{let e=yB.from(r,"base64");return new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}):(rI=r=>btoa(F.uint8ArrayToString(r)),iI=r=>F.stringToUint8Array(atob(r)));kr=[new Array(255),new Array(255),new Array(255),new Array(255)];for(let r=0;r<=255;r++){let e=r<<16;for(let t=0;t<8;t++)e=e<<1^((e&8388608)!==0?8801531:0);kr[0][r]=(e&16711680)>>16|e&65280|(e&255)<<16}for(let r=0;r<=255;r++)kr[1][r]=kr[0][r]>>8^kr[0][kr[0][r]&255];for(let r=0;r<=255;r++)kr[2][r]=kr[1][r]>>8^kr[0][kr[1][r]&255];for(let r=0;r<=255;r++)kr[3][r]=kr[2][r]>>8^kr[0][kr[2][r]&255];KW=(function(){let r=new ArrayBuffer(2);return new DataView(r).setInt16(0,255,!0),new Int16Array(r)[0]===255})();Bi=BigInt(0),kd=BigInt(1);YW=F.getNodeCrypto();ds=BigInt(1);ZW=[7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997,1009,1013,1019,1021,1031,1033,1039,1049,1051,1061,1063,1069,1087,1091,1093,1097,1103,1109,1117,1123,1129,1151,1153,1163,1171,1181,1187,1193,1201,1213,1217,1223,1229,1231,1237,1249,1259,1277,1279,1283,1289,1291,1297,1301,1303,1307,1319,1321,1327,1361,1367,1373,1381,1399,1409,1423,1427,1429,1433,1439,1447,1451,1453,1459,1471,1481,1483,1487,1489,1493,1499,1511,1523,1531,1543,1549,1553,1559,1567,1571,1579,1583,1597,1601,1607,1609,1613,1619,1621,1627,1637,1657,1663,1667,1669,1693,1697,1699,1709,1721,1723,1733,1741,1747,1753,1759,1777,1783,1787,1789,1801,1811,1823,1831,1847,1861,1867,1871,1873,1877,1879,1889,1901,1907,1913,1931,1933,1949,1951,1973,1979,1987,1993,1997,1999,2003,2011,2017,2027,2029,2039,2053,2063,2069,2081,2083,2087,2089,2099,2111,2113,2129,2131,2137,2141,2143,2153,2161,2179,2203,2207,2213,2221,2237,2239,2243,2251,2267,2269,2273,2281,2287,2293,2297,2309,2311,2333,2339,2341,2347,2351,2357,2371,2377,2381,2383,2389,2393,2399,2411,2417,2423,2437,2441,2447,2459,2467,2473,2477,2503,2521,2531,2539,2543,2549,2551,2557,2579,2591,2593,2609,2617,2621,2633,2647,2657,2659,2663,2671,2677,2683,2687,2689,2693,2699,2707,2711,2713,2719,2729,2731,2741,2749,2753,2767,2777,2789,2791,2797,2801,2803,2819,2833,2837,2843,2851,2857,2861,2879,2887,2897,2903,2909,2917,2927,2939,2953,2957,2963,2969,2971,2999,3001,3011,3019,3023,3037,3041,3049,3061,3067,3079,3083,3089,3109,3119,3121,3137,3163,3167,3169,3181,3187,3191,3203,3209,3217,3221,3229,3251,3253,3257,3259,3271,3299,3301,3307,3313,3319,3323,3329,3331,3343,3347,3359,3361,3371,3373,3389,3391,3407,3413,3433,3449,3457,3461,3463,3467,3469,3491,3499,3511,3517,3527,3529,3533,3539,3541,3547,3557,3559,3571,3581,3583,3593,3607,3613,3617,3623,3631,3637,3643,3659,3671,3673,3677,3691,3697,3701,3709,3719,3727,3733,3739,3761,3767,3769,3779,3793,3797,3803,3821,3823,3833,3847,3851,3853,3863,3877,3881,3889,3907,3911,3917,3919,3923,3929,3931,3943,3947,3967,3989,4001,4003,4007,4013,4019,4021,4027,4049,4051,4057,4073,4079,4091,4093,4099,4111,4127,4129,4133,4139,4153,4157,4159,4177,4201,4211,4217,4219,4229,4231,4241,4243,4253,4259,4261,4271,4273,4283,4289,4297,4327,4337,4339,4349,4357,4363,4373,4391,4397,4409,4421,4423,4441,4447,4451,4457,4463,4481,4483,4493,4507,4513,4517,4519,4523,4547,4549,4561,4567,4583,4591,4597,4603,4621,4637,4639,4643,4649,4651,4657,4663,4673,4679,4691,4703,4721,4723,4729,4733,4751,4759,4783,4787,4789,4793,4799,4801,4813,4817,4831,4861,4871,4877,4889,4903,4909,4919,4931,4933,4937,4943,4951,4957,4967,4969,4973,4987,4993,4999].map(r=>BigInt(r));L6=F.getWebCrypto(),nI=F.getNodeCrypto(),tX=nI&&nI.getHashes();rX=Ao("md5")||ho("md5"),iX=Ao("sha1")||ho("sha1","SHA-1"),nX=Ao("sha224")||ho("sha224"),sX=Ao("sha256")||ho("sha256","SHA-256"),oX=Ao("sha384")||ho("sha384","SHA-384"),aX=Ao("sha512")||ho("sha512","SHA-512"),cX=Ao("ripemd160")||ho("ripemd160"),lX=Ao("sha3-256")||ho("sha3_256"),dX=Ao("sha3-512")||ho("sha3_512");sn=[];sn[1]=[48,32,48,12,6,8,42,134,72,134,247,13,2,5,5,0,4,16];sn[2]=[48,33,48,9,6,5,43,14,3,2,26,5,0,4,20];sn[3]=[48,33,48,9,6,5,43,36,3,2,1,5,0,4,20];sn[8]=[48,49,48,13,6,9,96,134,72,1,101,3,4,2,1,5,0,4,32];sn[9]=[48,65,48,13,6,9,96,134,72,1,101,3,4,2,2,5,0,4,48];sn[10]=[48,81,48,13,6,9,96,134,72,1,101,3,4,2,3,5,0,4,64];sn[11]=[48,45,48,13,6,9,96,134,72,1,101,3,4,2,4,5,0,4,28];sn[12]=[48,49,48,13,6,9,96,134,72,1,101,3,4,2,8,5,0,4,32];sn[14]=[48,81,48,13,6,9,96,134,72,1,101,3,4,2,10,5,0,4,64];Ud=F.getWebCrypto(),jc=F.getNodeCrypto(),_a=BigInt(1);va=BigInt(1);_X={"2a8648ce3d030107":y.curve.nistP256,"2b81040022":y.curve.nistP384,"2b81040023":y.curve.nistP521,"2b8104000a":y.curve.secp256k1,"2b06010401da470f01":y.curve.ed25519Legacy,"2b060104019755010501":y.curve.curve25519Legacy,"2b2403030208010107":y.curve.brainpoolP256r1,"2b240303020801010b":y.curve.brainpoolP384r1,"2b240303020801010d":y.curve.brainpoolP512r1},co=class r{constructor(e){if(e instanceof r)this.oid=e.oid;else if(F.isArray(e)||F.isUint8Array(e)){if(e=new Uint8Array(e),e[0]===6){if(e[1]!==e.length-2)throw new Error("Length mismatch in DER encoded oid");e=e.subarray(2)}this.oid=e}else this.oid=""}read(e){if(e.length>=1){let t=e[0];if(e.length>=1+t)return this.oid=e.subarray(1,1+t),1+this.oid.length}throw new Error("Invalid oid")}write(){return F.concatUint8Array([new Uint8Array([this.oid.length]),this.oid])}toHex(){return F.uint8ArrayToHex(this.oid)}getName(){let e=_X[this.toHex()];if(!e)throw new Error("Unknown curve object identifier.");return e}};ft=class r extends Error{constructor(...e){super(...e),Error.captureStackTrace&&Error.captureStackTrace(this,r),this.name="UnsupportedError"}},Sd=class extends ft{constructor(...e){super(...e),Error.captureStackTrace&&Error.captureStackTrace(this,ft),this.name="UnknownPacketError"}},tA=class extends ft{constructor(...e){super(...e),Error.captureStackTrace&&Error.captureStackTrace(this,ft),this.name="MalformedPacketError"}},nA=class{constructor(e,t){this.tag=e,this.rawContent=t}write(){return this.rawContent}};O1=(r,e)=>{if(r===y.publicKey.ed25519)return{kty:"OKP",crv:"Ed25519",x:Ur(e),ext:!0};throw new Error("Unsupported EdDSA algorithm")},gU=(r,e,t)=>{if(r===y.publicKey.ed25519){let i=O1(r,e);return i.d=Ur(t),i}else throw new Error("Unsupported EdDSA algorithm")},TX=Object.freeze({__proto__:null,generate:U1,getPayloadSize:KI,getPreferredHashAlgo:GI,sign:R1,validateParams:q1,verify:N1});UX=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;bA=(r,e)=>{function t(i,...n){if(kt(i),!UX)throw new Error("Non little-endian hardware is not yet supported");if(r.nonceLength!==void 0){let d=n[0];if(!d)throw new Error("nonce / iv required");r.varSizeNonce?kt(d):kt(d,r.nonceLength)}let s=r.tagLength;s&&n[1]!==void 0&&kt(n[1]);let o=e(i,...n),a=(d,u)=>{if(u!==void 0){if(d!==2)throw new Error("cipher output not supported");kt(u)}},c=!1;return{encrypt(d,u){if(c)throw new Error("cannot encrypt() twice with same key + nonce");return c=!0,kt(d),a(o.encrypt.length,u),o.encrypt(d,u)},decrypt(d,u){if(kt(d),s&&d.length<s)throw new Error("invalid ciphertext length: smaller than tagLength="+s);return a(o.decrypt.length,u),o.decrypt(d,u)}}}return Object.assign(t,r),t};no=16,H1=new Uint8Array(16),os=Et(H1),OX=225,MX=(r,e,t,i)=>{let n=i&1;return{s3:t<<31|i>>>1,s2:e<<31|t>>>1,s1:r<<31|e>>>1,s0:r>>>1^OX<<24&-(n&1)}},nn=r=>(r>>>0&255)<<24|(r>>>8&255)<<16|(r>>>16&255)<<8|r>>>24&255|0;HX=r=>r>64*1024?8:r>1024?4:2,aI=class{constructor(e,t){this.blockLen=no,this.outputLen=no,this.s0=0,this.s1=0,this.s2=0,this.s3=0,this.finished=!1,e=sA(e),kt(e,16);let i=zI(e),n=i.getUint32(0,!1),s=i.getUint32(4,!1),o=i.getUint32(8,!1),a=i.getUint32(12,!1),c=[];for(let g=0;g<128;g++)c.push({s0:nn(n),s1:nn(s),s2:nn(o),s3:nn(a)}),{s0:n,s1:s,s2:o,s3:a}=MX(n,s,o,a);let l=HX(t||1024);if(![1,2,4,8].includes(l))throw new Error("ghash: invalid window size, expected 2, 4 or 8");this.W=l;let u=128/l,f=this.windowSize=2**l,h=[];for(let g=0;g<u;g++)for(let p=0;p<f;p++){let I=0,m=0,w=0,B=0;for(let C=0;C<l;C++){if(!(p>>>l-C-1&1))continue;let{s0:Q,s1:R,s2:U,s3:T}=c[l*g+C];I^=Q,m^=R,w^=U,B^=T}h.push({s0:I,s1:m,s2:w,s3:B})}this.t=h}_updateBlock(e,t,i,n){e^=this.s0,t^=this.s1,i^=this.s2,n^=this.s3;let{W:s,t:o,windowSize:a}=this,c=0,l=0,d=0,u=0,f=(1<<s)-1,h=0;for(let g of[e,t,i,n])for(let p=0;p<4;p++){let I=g>>>8*p&255;for(let m=8/s-1;m>=0;m--){let w=I>>>s*m&f,{s0:B,s1:C,s2:x,s3:Q}=o[h*a+w];c^=B,l^=C,d^=x,u^=Q,h+=1}}this.s0=c,this.s1=l,this.s2=d,this.s3=u}update(e){oI(this),e=sA(e),kt(e);let t=Et(e),i=Math.floor(e.length/no),n=e.length%no;for(let s=0;s<i;s++)this._updateBlock(t[s*4+0],t[s*4+1],t[s*4+2],t[s*4+3]);return n&&(H1.set(e.subarray(i*no)),this._updateBlock(os[0],os[1],os[2],os[3]),ei(os)),this}destroy(){let{t:e}=this;for(let t of e)t.s0=0,t.s1=0,t.s2=0,t.s3=0}digestInto(e){oI(this),mU(e,this),this.finished=!0;let{s0:t,s1:i,s2:n,s3:s}=this,o=Et(e);return o[0]=t,o[1]=i,o[2]=n,o[3]=s,e}digest(){let e=new Uint8Array(no);return this.digestInto(e),this.destroy(),e}},XB=class extends aI{constructor(e,t){e=sA(e),kt(e);let i=LX(vi(e));super(i,t),ei(i)}update(e){e=sA(e),oI(this);let t=Et(e),i=e.length%no,n=Math.floor(e.length/no);for(let s=0;s<n;s++)this._updateBlock(nn(t[s*4+3]),nn(t[s*4+2]),nn(t[s*4+1]),nn(t[s*4+0]));return i&&(H1.set(e.subarray(n*no)),this._updateBlock(nn(os[3]),nn(os[2]),nn(os[1]),nn(os[0])),ei(os)),this}digestInto(e){oI(this),mU(e,this),this.finished=!0;let{s0:t,s1:i,s2:n,s3:s}=this,o=Et(e);return o[0]=t,o[1]=i,o[2]=n,o[3]=s,e.reverse()}};G6=wU((r,e)=>new aI(r,e));wU((r,e)=>new XB(r,e));ki=16,K1=4,Ty=new Uint8Array(ki),KX=283;ZB=(()=>{let r=new Uint8Array(256);for(let t=0,i=1;t<256;t++,i^=G1(i))r[t]=i;let e=new Uint8Array(256);e[0]=99;for(let t=0;t<255;t++){let i=r[255-t];i|=i<<8,e[r[t]]=(i^i>>4^i>>5^i>>6^i>>7^99)&255}return ei(r),e})(),GX=ZB.map((r,e)=>ZB.indexOf(e)),zX=r=>r<<24|r>>>8,IB=r=>r<<8|r>>>24,z6=r=>r<<24&4278190080|r<<8&16711680|r>>>8&65280|r>>>24&255;z1=xU(ZB,r=>Ed(r,3)<<24|r<<16|r<<8|Ed(r,2)),CU=xU(GX,r=>Ed(r,11)<<24|Ed(r,13)<<16|Ed(r,9)<<8|Ed(r,14)),JX=(()=>{let r=new Uint8Array(16);for(let e=0,t=1;e<16;e++,t=G1(t))r[e]=t;return r})();jX=bA({blockSize:16,nonceLength:16},function(e,t){function i(n,s){if(kt(n),s!==void 0&&(kt(s),!on(s)))throw new Error("unaligned destination");let o=el(e),a=vi(t),c=[o,a];on(n)||c.push(n=vi(n));let l=EU(o,a,n,s);return ei(...c),l}return{encrypt:(n,s)=>i(n,s),decrypt:(n,s)=>i(n,s)}});e1=bA({blockSize:16,nonceLength:16},function(e,t,i={}){let n=!i.disablePadding;return{encrypt(s,o){let a=el(e),{b:c,o:l,out:d}=VX(s,n,o),u=t,f=[a];on(u)||f.push(u=vi(u));let h=Et(u),g=h[0],p=h[1],I=h[2],m=h[3],w=0;for(;w+4<=c.length;)g^=c[w+0],p^=c[w+1],I^=c[w+2],m^=c[w+3],{s0:g,s1:p,s2:I,s3:m}=Pn(a,g,p,I,m),l[w++]=g,l[w++]=p,l[w++]=I,l[w++]=m;if(n){let B=WX(s.subarray(w*4));g^=B[0],p^=B[1],I^=B[2],m^=B[3],{s0:g,s1:p,s2:I,s3:m}=Pn(a,g,p,I,m),l[w++]=g,l[w++]=p,l[w++]=I,l[w++]=m}return ei(...f),d},decrypt(s,o){$X(s);let a=J1(e),c=t,l=[a];on(c)||l.push(c=vi(c));let d=Et(c);o=vA(s.length,o),on(s)||l.push(s=vi(s)),L1(s,o);let u=Et(s),f=Et(o),h=d[0],g=d[1],p=d[2],I=d[3];for(let m=0;m+4<=u.length;){let w=h,B=g,C=p,x=I;h=u[m+0],g=u[m+1],p=u[m+2],I=u[m+3];let{s0:Q,s1:R,s2:U,s3:T}=JI(a,h,g,p,I);f[m++]=Q^w,f[m++]=R^B,f[m++]=U^C,f[m++]=T^x}return ei(...l),YX(o,n)}}}),BU=bA({blockSize:16,nonceLength:16},function(e,t){function i(n,s,o){kt(n);let a=n.length;if(o=vA(a,o),yU(n,o))throw new Error("overlapping src and dst not supported.");let c=el(e),l=t,d=[c];on(l)||d.push(l=vi(l)),on(n)||d.push(n=vi(n));let u=Et(n),f=Et(o),h=s?f:u,g=Et(l),p=g[0],I=g[1],m=g[2],w=g[3];for(let C=0;C+4<=u.length;){let{s0:x,s1:Q,s2:R,s3:U}=Pn(c,p,I,m,w);f[C+0]=u[C+0]^x,f[C+1]=u[C+1]^Q,f[C+2]=u[C+2]^R,f[C+3]=u[C+3]^U,p=h[C++],I=h[C++],m=h[C++],w=h[C++]}let B=ki*Math.floor(u.length/K1);if(B<a){({s0:p,s1:I,s2:m,s3:w}=Pn(c,p,I,m,w));let C=M1(new Uint32Array([p,I,m,w]));for(let x=B,Q=0;x<a;x++,Q++)o[x]=n[x]^C[Q];ei(C)}return ei(...d),o}return{encrypt:(n,s)=>i(n,!0,s),decrypt:(n,s)=>i(n,!1,s)}});Uy=bA({blockSize:16,nonceLength:12,tagLength:16,varSizeNonce:!0},function(e,t,i){if(t.length<8)throw new Error("aes/gcm: invalid nonce length");let n=16;function s(a,c,l){let d=XX(G6,!1,a,l,i);for(let u=0;u<c.length;u++)d[u]^=c[u];return d}function o(){let a=el(e),c=Ty.slice(),l=Ty.slice();if(Xf(a,!1,l,l,c),t.length===12)l.set(t);else{let u=Ty.slice(),f=zI(u);WB(f,8,BigInt(t.length*8),!1);let h=G6.create(c).update(t).update(u);h.digestInto(l),h.destroy()}let d=Xf(a,!1,l,Ty);return{xk:a,authKey:c,counter:l,tagMask:d}}return{encrypt(a){let{xk:c,authKey:l,counter:d,tagMask:u}=o(),f=new Uint8Array(a.length+n),h=[c,l,d,u];on(a)||h.push(a=vi(a)),Xf(c,!1,d,a,f.subarray(0,a.length));let g=s(l,u,f.subarray(0,f.length-n));return h.push(g),f.set(g,a.length),ei(...h),f},decrypt(a){let{xk:c,authKey:l,counter:d,tagMask:u}=o(),f=[c,l,u,d];on(a)||f.push(a=vi(a));let h=a.subarray(0,-n),g=a.subarray(-n),p=s(l,u,h);if(f.push(p),!IU(p,g))throw new Error("aes/gcm: invalid ghash tag");let I=Xf(c,!1,d,h);return ei(...f),I}}});J6={encrypt(r,e){if(e.length>=2**32)throw new Error("plaintext should be less than 4gb");let t=el(r);if(e.length===16)vU(t,e);else{let i=Et(e),n=i[0],s=i[1];for(let o=0,a=1;o<6;o++)for(let c=2;c<i.length;c+=2,a++){let{s0:l,s1:d,s2:u,s3:f}=Pn(t,n,s,i[c],i[c+1]);n=l,s=d^z6(a),i[c]=u,i[c+1]=f}i[0]=n,i[1]=s}t.fill(0)},decrypt(r,e){if(e.length-8>=2**32)throw new Error("ciphertext should be less than 4gb");let t=J1(r),i=e.length/8-1;if(i===1)kU(t,e);else{let n=Et(e),s=n[0],o=n[1];for(let a=0,c=i*6;a<6;a++)for(let l=i*2;l>=1;l-=2,c--){o^=z6(c);let{s0:d,s1:u,s2:f,s3:h}=JI(t,s,o,n[l],n[l+1]);s=d,o=u,n[l]=f,n[l+1]=h}n[0]=s,n[1]=o}t.fill(0)}},j6=new Uint8Array(8).fill(166),SU=bA({blockSize:8},r=>({encrypt(e){if(!e.length||e.length%8!==0)throw new Error("invalid plaintext length");if(e.length===8)throw new Error("8-byte keys not allowed in AESKW, use AESKWP instead");let t=NX(j6,e);return J6.encrypt(r,t),t},decrypt(e){if(e.length%8!==0||e.length<24)throw new Error("invalid ciphertext length");let t=vi(e);if(J6.decrypt(r,t),!IU(t.subarray(0,8),j6))throw new Error("integrity check failed");return t.subarray(0,8).fill(0),t.subarray(8)}})),$6={expandKeyLE:el,expandKeyDecLE:J1,encrypt:Pn,decrypt:JI,encryptBlock:vU,decryptBlock:kU,ctrCounter:EU,ctr32:Xf};Dd=F.getWebCrypto();cI={x25519:F.encodeUTF8("OpenPGP X25519"),x448:F.encodeUTF8("OpenPGP X448")};rZ=Object.freeze({__proto__:null,decrypt:_U,encrypt:QU,generate:j1,generateEphemeralEncryptionMaterial:$I,getPayloadSize:jI,recomputeSharedSecret:VI,validateParams:$1}),wB=F.getWebCrypto(),dI=F.getNodeCrypto(),us={[y.curve.nistP256]:"P-256",[y.curve.nistP384]:"P-384",[y.curve.nistP521]:"P-521"},Ys=dI?dI.getCurves():[],ss=dI?{[y.curve.secp256k1]:Ys.includes("secp256k1")?"secp256k1":void 0,[y.curve.nistP256]:Ys.includes("prime256v1")?"prime256v1":void 0,[y.curve.nistP384]:Ys.includes("secp384r1")?"secp384r1":void 0,[y.curve.nistP521]:Ys.includes("secp521r1")?"secp521r1":void 0,[y.curve.ed25519Legacy]:Ys.includes("ED25519")?"ED25519":void 0,[y.curve.curve25519Legacy]:Ys.includes("X25519")?"X25519":void 0,[y.curve.brainpoolP256r1]:Ys.includes("brainpoolP256r1")?"brainpoolP256r1":void 0,[y.curve.brainpoolP384r1]:Ys.includes("brainpoolP384r1")?"brainpoolP384r1":void 0,[y.curve.brainpoolP512r1]:Ys.includes("brainpoolP512r1")?"brainpoolP512r1":void 0}:{},FU={[y.curve.nistP256]:{oid:[6,8,42,134,72,206,61,3,1,7],keyType:y.publicKey.ecdsa,hash:y.hash.sha256,cipher:y.symmetric.aes128,node:ss[y.curve.nistP256],web:us[y.curve.nistP256],payloadSize:32,sharedSize:256,wireFormatLeadingByte:4},[y.curve.nistP384]:{oid:[6,5,43,129,4,0,34],keyType:y.publicKey.ecdsa,hash:y.hash.sha384,cipher:y.symmetric.aes192,node:ss[y.curve.nistP384],web:us[y.curve.nistP384],payloadSize:48,sharedSize:384,wireFormatLeadingByte:4},[y.curve.nistP521]:{oid:[6,5,43,129,4,0,35],keyType:y.publicKey.ecdsa,hash:y.hash.sha512,cipher:y.symmetric.aes256,node:ss[y.curve.nistP521],web:us[y.curve.nistP521],payloadSize:66,sharedSize:528,wireFormatLeadingByte:4},[y.curve.secp256k1]:{oid:[6,5,43,129,4,0,10],keyType:y.publicKey.ecdsa,hash:y.hash.sha256,cipher:y.symmetric.aes128,node:ss[y.curve.secp256k1],payloadSize:32,wireFormatLeadingByte:4},[y.curve.ed25519Legacy]:{oid:[6,9,43,6,1,4,1,218,71,15,1],keyType:y.publicKey.eddsaLegacy,hash:y.hash.sha512,node:!1,payloadSize:32,wireFormatLeadingByte:64},[y.curve.curve25519Legacy]:{oid:[6,10,43,6,1,4,1,151,85,1,5,1],keyType:y.publicKey.ecdh,hash:y.hash.sha256,cipher:y.symmetric.aes128,node:!1,payloadSize:32,wireFormatLeadingByte:64},[y.curve.brainpoolP256r1]:{oid:[6,9,43,36,3,3,2,8,1,1,7],keyType:y.publicKey.ecdsa,hash:y.hash.sha256,cipher:y.symmetric.aes128,node:ss[y.curve.brainpoolP256r1],payloadSize:32,wireFormatLeadingByte:4},[y.curve.brainpoolP384r1]:{oid:[6,9,43,36,3,3,2,8,1,1,11],keyType:y.publicKey.ecdsa,hash:y.hash.sha384,cipher:y.symmetric.aes192,node:ss[y.curve.brainpoolP384r1],payloadSize:48,wireFormatLeadingByte:4},[y.curve.brainpoolP512r1]:{oid:[6,9,43,36,3,3,2,8,1,1,13],keyType:y.publicKey.ecdsa,hash:y.hash.sha512,cipher:y.symmetric.aes256,node:ss[y.curve.brainpoolP512r1],payloadSize:64,wireFormatLeadingByte:4}},Si=class{constructor(e){try{this.name=e instanceof co?e.getName():y.write(y.curve,e)}catch{throw new ft("Unknown curve")}let t=FU[this.name];this.keyType=t.keyType,this.oid=t.oid,this.hash=t.hash,this.cipher=t.cipher,this.node=t.node,this.web=t.web,this.payloadSize=t.payloadSize,this.sharedSize=t.sharedSize,this.wireFormatLeadingByte=t.wireFormatLeadingByte,this.web&&F.getWebCrypto()?this.type="web":this.node&&F.getNodeCrypto()?this.type="node":this.name===y.curve.curve25519Legacy?this.type="curve25519Legacy":this.name===y.curve.ed25519Legacy&&(this.type="ed25519Legacy")}async genKeyPair(){switch(this.type){case"web":try{return await iZ(this.name,this.wireFormatLeadingByte)}catch(e){return F.printDebugError("Browser did not support generating ec key "+e.message),V6(this.name)}case"node":return nZ(this.name);case"curve25519Legacy":{let{k:e,A:t}=await j1(y.publicKey.x25519),i=e.slice().reverse();return i[0]=i[0]&127|64,i[31]&=248,{publicKey:F.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]),t]),privateKey:i}}case"ed25519Legacy":{let{seed:e,A:t}=await U1(y.publicKey.ed25519);return{publicKey:F.concatUint8Array([new Uint8Array([this.wireFormatLeadingByte]),t]),privateKey:e}}default:return V6(this.name)}}};uI=F.getWebCrypto(),NU=F.getNodeCrypto();dZ=Object.freeze({__proto__:null,sign:Y1,validateParams:sZ,verify:W1});uZ=Object.freeze({__proto__:null,sign:qU,validateParams:MU,verify:OU});xZ=Object.freeze({__proto__:null,decrypt:GU,encrypt:KU,validateParams:hZ}),CZ=Object.freeze({__proto__:null,CurveWithOID:Si,ecdh:xZ,ecdhX:rZ,ecdsa:dZ,eddsa:TX,eddsaLegacy:uZ,generate:Vy,getPreferredHashAlgo:PU}),Yy=BigInt(0),Zf=BigInt(1);fI=class{constructor(e){e&&(this.data=e)}read(e){if(e.length>=1){let t=e[0];if(e.length>=1+t)return this.data=e.subarray(1,1+t),1+this.data.length}throw new Error("Invalid symmetric key")}write(){return F.concatUint8Array([new Uint8Array([this.data.length]),this.data])}},AI=class{constructor(e){if(e){let{hash:t,cipher:i}=e;this.hash=t,this.cipher=i}else this.hash=null,this.cipher=null}read(e){if(e.length<4||e[0]!==3||e[1]!==1)throw new ft("Cannot read KDFParams");return this.hash=e[2],this.cipher=e[3],4}write(){return new Uint8Array([3,1,this.hash,this.cipher])}},hI=class r{static fromObject({wrappedKey:e,algorithm:t}){let i=new r;return i.wrappedKey=e,i.algorithm=t,i}read(e){let t=0,i=e[t++];this.algorithm=i%2?e[t++]:null,i-=i%2,this.wrappedKey=F.readExactSubarray(e,t,t+i),t+=i}write(){return F.concatUint8Array([this.algorithm?new Uint8Array([this.wrappedKey.length+1,this.algorithm]):new Uint8Array([this.wrappedKey.length]),this.wrappedKey])}};Wy=F.getWebCrypto(),oA=F.getNodeCrypto(),qc=oA?oA.getCiphers():[],WI={idea:qc.includes("idea-cfb")?"idea-cfb":void 0,tripledes:qc.includes("des-ede3-cfb")?"des-ede3-cfb":void 0,cast5:qc.includes("cast5-cfb")?"cast5-cfb":void 0,blowfish:qc.includes("bf-cfb")?"bf-cfb":void 0,aes128:qc.includes("aes-128-cfb")?"aes-128-cfb":void 0,aes192:qc.includes("aes-192-cfb")?"aes-192-cfb":void 0,aes256:qc.includes("aes-256-cfb")?"aes-256-cfb":void 0};gI=class{constructor(e,t,i){let{blockSize:n}=It(e);this.key=t,this.iv=i,this.prevBlock=i.slice(),this.nextBlock=new Uint8Array(n),this.i=0,this.blockSize=n,this.zeroBlock=new Uint8Array(this.blockSize)}static isSupported(e){let{keySize:t}=It(e);return Wy.importKey("raw",new Uint8Array(t),"aes-cbc",!1,["encrypt"]).then(()=>!0,()=>!1)}async _runCBC(e,t){let i="AES-CBC";this.keyRef=this.keyRef||await Wy.importKey("raw",this.key,i,!1,["encrypt"]);let n=await Wy.encrypt({name:i,iv:t||this.zeroBlock},this.keyRef,e);return new Uint8Array(n).subarray(0,e.length)}async encryptChunk(e){let t=this.nextBlock.length-this.i,i=e.subarray(0,t);if(this.nextBlock.set(i,this.i),this.i+e.length>=2*this.blockSize){let s=(e.length-t)%this.blockSize,o=F.concatUint8Array([this.nextBlock,e.subarray(t,e.length-s)]),a=F.concatUint8Array([this.prevBlock,o.subarray(0,o.length-this.blockSize)]),c=await this._runCBC(a);return Ry(c,o),this.prevBlock=c.slice(-this.blockSize),s>0&&this.nextBlock.set(e.subarray(-s)),this.i=s,c}this.i+=i.length;let n;if(this.i===this.nextBlock.length){let s=this.nextBlock;n=await this._runCBC(this.prevBlock),Ry(n,s),this.prevBlock=n.slice(),this.i=0;let o=e.subarray(i.length);this.nextBlock.set(o,this.i),this.i+=o.length}else n=new Uint8Array;return n}async finish(){let e;if(this.i===0)e=new Uint8Array;else{this.nextBlock=this.nextBlock.subarray(0,this.i);let t=this.nextBlock,i=await this._runCBC(this.prevBlock);Ry(i,t),e=i.subarray(0,t.length)}return this.clearSensitiveData(),e}clearSensitiveData(){this.nextBlock.fill(0),this.prevBlock.fill(0),this.keyRef=null,this.key=null}async encrypt(e){let i=(await this._runCBC(F.concatUint8Array([new Uint8Array(this.blockSize),e]),this.iv)).subarray(0,e.length);return Ry(i,e),this.clearSensitiveData(),i}},pI=class{constructor(e,t,i,n){this.forEncryption=e;let{blockSize:s}=It(t);this.key=$6.expandKeyLE(i),n.byteOffset%4!==0&&(n=n.slice()),this.prevBlock=CB(n),this.nextBlock=new Uint8Array(s),this.i=0,this.blockSize=s}_runCFB(e){let t=CB(e),i=new Uint8Array(e.length),n=CB(i);for(let s=0;s+4<=n.length;s+=4){let{s0:o,s1:a,s2:c,s3:l}=$6.encrypt(this.key,this.prevBlock[0],this.prevBlock[1],this.prevBlock[2],this.prevBlock[3]);n[s+0]=t[s+0]^o,n[s+1]=t[s+1]^a,n[s+2]=t[s+2]^c,n[s+3]=t[s+3]^l,this.prevBlock=(this.forEncryption?n:t).slice(s,s+4)}return i}async processChunk(e){let t=this.nextBlock.length-this.i,i=e.subarray(0,t);if(this.nextBlock.set(i,this.i),this.i+e.length>=2*this.blockSize){let s=(e.length-t)%this.blockSize,o=F.concatUint8Array([this.nextBlock,e.subarray(t,e.length-s)]),a=this._runCFB(o);return s>0&&this.nextBlock.set(e.subarray(-s)),this.i=s,a}this.i+=i.length;let n;if(this.i===this.nextBlock.length){n=this._runCFB(this.nextBlock),this.i=0;let s=e.subarray(i.length);this.nextBlock.set(s,this.i),this.i+=s.length}else n=new Uint8Array;return n}async finish(){let e;return this.i===0?e=new Uint8Array:e=this._runCFB(this.nextBlock).subarray(0,this.i),this.clearSensitiveData(),e}clearSensitiveData(){this.nextBlock.fill(0),this.prevBlock.fill(0),this.key.fill(0)}};CB=r=>new Uint32Array(r.buffer,r.byteOffset,Math.floor(r.byteLength/4));eT=F.getWebCrypto(),NZ=F.getNodeCrypto(),lo=16;Xy=new Uint8Array(lo);rT=F.getWebCrypto(),LZ=F.getNodeCrypto(),HZ=F.getNodeBuffer(),go=16,KZ=go,xd=go,iT=new Uint8Array(go),n1=new Uint8Array(go);n1[go-1]=1;s1=new Uint8Array(go);s1[go-1]=2;kA.getNonce=function(r,e){let t=r.slice();for(let i=0;i<e.length;i++)t[8+i]^=e[i];return t};kA.blockLength=go;kA.ivLength=KZ;kA.tagLength=xd;di=16,JU=15,ba=16;Hf=new Uint8Array(di),JZ=new Uint8Array([1]);SA.getNonce=function(r,e){let t=r.slice();for(let i=0;i<e.length;i++)t[7+i]^=e[i];return t};SA.blockLength=di;SA.ivLength=JU;SA.tagLength=ba;EB=F.getWebCrypto(),sT=F.getNodeCrypto(),oT=F.getNodeBuffer(),jZ=16,$Z=12,Cd=16,BB="AES-GCM";Nd.getNonce=function(r,e){let t=r.slice();for(let i=0;i<e.length;i++)t[4+i]^=e[i];return t};Nd.blockLength=jZ;Nd.ivLength=$Z;Nd.tagLength=Cd;XZ=2,ZZ=19,eee=16,aT=30,Qd=class r extends Error{constructor(...e){super(...e),Error.captureStackTrace&&Error.captureStackTrace(this,r),this.name="Argon2OutOfMemoryError"}},tee=2<<19,o1=class{constructor(e=pe){let{passes:t,parallelism:i,memoryExponent:n}=e.s2kArgon2Params;this.type="argon2",this.salt=null,this.t=t,this.p=i,this.encodedM=n}generateSalt(){this.salt=Nr(eee)}read(e){let t=0;return this.salt=e.subarray(t,t+16),t+=16,this.t=e[t++],this.p=e[t++],this.encodedM=e[t++],t}write(){let e=[new Uint8Array([y.write(y.s2k,this.type)]),this.salt,new Uint8Array([this.t,this.p,this.encodedM])];return F.concatUint8Array(e)}async produceKey(e,t,i){if(i.maxArgon2MemoryExponent>aT)throw new Qd(`'config.maxArgon2MemoryExponent' exceeds the max allowed value of ${aT}`);if(this.encodedM>i.maxArgon2MemoryExponent)throw new Qd("Argon2 required memory exceeds `config.maxArgon2MemoryExponent`");let n=1<<this.encodedM;try{qy=qy||(await Promise.resolve().then(function(){return $ie})).default,Kf=Kf||qy();let s=await Kf,o=F.encodeUTF8(e),a=s({version:ZZ,type:XZ,password:o,salt:this.salt,tagLength:t,memorySize:n,parallelism:this.p,passes:this.t});return n>tee&&(Kf=qy(),Kf.catch(()=>{})),a}catch(s){throw s.message&&(s.message.includes("Unable to grow instance memory")||s.message.includes("failed to grow memory")||s.message.includes("WebAssembly.Memory.grow")||s.message.includes("Out of memory"))?new Qd("Could not allocate required memory for Argon2"):s}}},a1=class{constructor(e,t=pe){this.algorithm=y.hash.sha256,this.type=y.read(y.s2k,e),this.c=t.s2kIterationCountByte,this.salt=null}generateSalt(){switch(this.type){case"salted":case"iterated":this.salt=Nr(8)}}getCount(){return 16+(this.c&15)<<(this.c>>4)+6}read(e){let t=0;switch(this.algorithm=e[t++],this.type){case"simple":break;case"salted":this.salt=e.subarray(t,t+8),t+=8;break;case"iterated":this.salt=e.subarray(t,t+8),t+=8,this.c=e[t++];break;case"gnu":if(F.uint8ArrayToString(e.subarray(t,t+3))==="GNU")if(t+=3,1e3+e[t++]===1001)this.type="gnu-dummy";else throw new ft("Unknown s2k gnu protection mode.");else throw new ft("Unknown s2k type.");break;default:throw new ft("Unknown s2k type.")}return t}write(){if(this.type==="gnu-dummy")return new Uint8Array([101,0,...F.stringToUint8Array("GNU"),1]);let e=[new Uint8Array([y.write(y.s2k,this.type),this.algorithm])];switch(this.type){case"simple":break;case"salted":e.push(this.salt);break;case"iterated":e.push(this.salt),e.push(new Uint8Array([this.c]));break;case"gnu":throw new Error("GNU s2k type not supported.");default:throw new Error("Unknown s2k type.")}return F.concatUint8Array(e)}async produceKey(e,t,i){e=F.encodeUTF8(e);let n=[],s=0,o=0;for(;s<t;){let a;switch(this.type){case"simple":a=F.concatUint8Array([new Uint8Array(o),e]);break;case"salted":a=F.concatUint8Array([new Uint8Array(o),this.salt,e]);break;case"iterated":{let l=F.concatUint8Array([this.salt,e]),d=l.length,u=Math.max(this.getCount(),d);a=new Uint8Array(o+u),a.set(l,o);for(let f=o+d;f<u;f+=d,d*=2)a.copyWithin(f,o,f);break}case"gnu":throw new Error("GNU s2k type not supported.");default:throw new Error("Unknown s2k type.")}let c=await hs(this.algorithm,a);n.push(c),s+=c.length,o++}return F.concatUint8Array(n).subarray(0,t)}},ree=new Set([y.s2k.argon2,y.s2k.iterated]);iee=tU("/");try{_d=iee("worker_threads"),nee=_d.Worker,see=_d.isMarkedAsUntransferable}catch{}Gt=Uint8Array,Ki=Uint16Array,X1=Int32Array,ZI=new Gt([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),ew=new Gt([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),c1=new Gt([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),$U=function(r,e){for(var t=new Ki(31),i=0;i<31;++i)t[i]=e+=1<<r[i-1];for(var n=new X1(t[30]),i=1;i<30;++i)for(var s=t[i];s<t[i+1];++s)n[s]=s-t[i]<<5|i;return{b:t,r:n}},_d=$U(ZI,2),VU=_d.b,l1=_d.r;VU[28]=258,l1[258]=28;YU=$U(ew,0),oee=YU.b,cT=YU.r,d1=new Ki(32768);for(ut=0;ut<32768;++ut)Ws=(ut&43690)>>1|(ut&21845)<<1,Ws=(Ws&52428)>>2|(Ws&13107)<<2,Ws=(Ws&61680)>>4|(Ws&3855)<<4,d1[ut]=((Ws&65280)>>8|(Ws&255)<<8)>>1;fs=(function(r,e,t){for(var i=r.length,n=0,s=new Ki(e);n<i;++n)r[n]&&++s[r[n]-1];var o=new Ki(e);for(n=1;n<e;++n)o[n]=o[n-1]+s[n-1]<<1;var a;if(t){a=new Ki(1<<e);var c=15-e;for(n=0;n<i;++n)if(r[n])for(var l=n<<4|r[n],d=e-r[n],u=o[r[n]-1]++<<d,f=u|(1<<d)-1;u<=f;++u)a[d1[u]>>c]=l}else for(a=new Ki(i),n=0;n<i;++n)r[n]&&(a[n]=d1[o[r[n]-1]++]>>15-r[n]);return a}),Pa=new Gt(288);for(ut=0;ut<144;++ut)Pa[ut]=8;for(ut=144;ut<256;++ut)Pa[ut]=9;for(ut=256;ut<280;++ut)Pa[ut]=7;for(ut=280;ut<288;++ut)Pa[ut]=8;cA=new Gt(32);for(ut=0;ut<32;++ut)cA[ut]=5;aee=fs(Pa,9,0),cee=fs(Pa,9,1),lee=fs(cA,5,0),dee=fs(cA,5,1),bB=function(r){for(var e=r[0],t=1;t<r.length;++t)r[t]>e&&(e=r[t]);return e},kn=function(r,e,t){var i=e/8|0;return(r[i]|r[i+1]<<8)>>(e&7)&t},vB=function(r,e){var t=e/8|0;return(r[t]|r[t+1]<<8|r[t+2]<<16)>>(e&7)},Z1=function(r){return(r+7)/8|0},rA=function(r,e,t){return(e==null||e<0)&&(e=0),(t==null||t>r.length)&&(t=r.length),new Gt(r.subarray(e,t))},uee=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],vr=function(r,e,t){var i=new Error(e||uee[r]);if(i.code=r,Error.captureStackTrace&&Error.captureStackTrace(i,vr),!t)throw i;return i},fee=function(r,e,t,i){var n=r.length,s=0;if(!n||e.f&&!e.l)return t||new Gt(0);var o=!t,a=o||e.i!=2,c=e.i;o&&(t=new Gt(n*3));var l=function(qe){var _e=t.length;if(qe>_e){var se=new Gt(Math.max(_e*2,qe));se.set(t),t=se}},d=e.f||0,u=e.p||0,f=e.b||0,h=e.l,g=e.d,p=e.m,I=e.n,m=n*8;do{if(!h){d=kn(r,u,1);var w=kn(r,u+1,3);if(u+=3,w)if(w==1)h=cee,g=dee,p=9,I=5;else if(w==2){var Q=kn(r,u,31)+257,R=kn(r,u+10,15)+4,U=Q+kn(r,u+5,31)+1;u+=14;for(var T=new Gt(U),D=new Gt(19),b=0;b<R;++b)D[c1[b]]=kn(r,u+b*3,7);u+=R*3;for(var _=bB(D),v=(1<<_)-1,E=fs(D,_,1),b=0;b<U;){var P=E[kn(r,u,v)];u+=P&15;var B=P>>4;if(B<16)T[b++]=B;else{var N=0,j=0;for(B==16?(j=3+kn(r,u,3),u+=2,N=T[b-1]):B==17?(j=3+kn(r,u,7),u+=3):B==18&&(j=11+kn(r,u,127),u+=7);j--;)T[b++]=N}}var z=T.subarray(0,Q),L=T.subarray(Q);p=bB(z),I=bB(L),h=fs(z,p,1),g=fs(L,I,1)}else vr(1);else{var B=Z1(u)+4,C=r[B-4]|r[B-3]<<8,x=B+C;if(x>n){c&&vr(0);break}a&&l(f+C),t.set(r.subarray(B,x),f),e.b=f+=C,e.p=u=x*8,e.f=d;continue}if(u>m){c&&vr(0);break}}a&&l(f+131072);for(var G=(1<<p)-1,K=(1<<I)-1,$=u;;$=u){var N=h[vB(r,u)&G],te=N>>4;if(u+=N&15,u>m){c&&vr(0);break}if(N||vr(2),te<256)t[f++]=te;else if(te==256){$=u,h=null;break}else{var re=te-254;if(te>264){var b=te-257,ee=ZI[b];re=kn(r,u,(1<<ee)-1)+VU[b],u+=ee}var Z=g[vB(r,u)&K],me=Z>>4;Z||vr(3),u+=Z&15;var L=oee[me];if(me>3){var ee=ew[me];L+=vB(r,u)&(1<<ee)-1,u+=ee}if(u>m){c&&vr(0);break}a&&l(f+131072);var le=f+re;if(f<L){var Ae=s-L,ve=Math.min(L,le);for(Ae+f<0&&vr(3);f<ve;++f)t[f]=i[Ae+f]}for(;f<le;++f)t[f]=t[f-L]}}e.l=h,e.p=$,e.b=f,e.f=d,h&&(d=1,e.m=p,e.d=g,e.n=I)}while(!d);return f!=t.length&&o?rA(t,0,f):t.subarray(0,f)},Xs=function(r,e,t){t<<=e&7;var i=e/8|0;r[i]|=t,r[i+1]|=t>>8},Gf=function(r,e,t){t<<=e&7;var i=e/8|0;r[i]|=t,r[i+1]|=t>>8,r[i+2]|=t>>16},kB=function(r,e){for(var t=[],i=0;i<r.length;++i)r[i]&&t.push({s:i,f:r[i]});var n=t.length,s=t.slice();if(!n)return{t:yI,l:0};if(n==1){var o=new Gt(t[0].s+1);return o[t[0].s]=1,{t:o,l:1}}t.sort(function(x,Q){return x.f-Q.f}),t.push({s:-1,f:25001});var a=t[0],c=t[1],l=0,d=1,u=2;for(t[0]={s:-1,f:a.f+c.f,l:a,r:c};d!=n-1;)a=t[t[l].f<t[u].f?l++:u++],c=t[l!=d&&t[l].f<t[u].f?l++:u++],t[d++]={s:-1,f:a.f+c.f,l:a,r:c};for(var f=s[0].s,i=1;i<n;++i)s[i].s>f&&(f=s[i].s);var h=new Ki(f+1),g=u1(t[d-1],h,0);if(g>e){var i=0,p=0,I=g-e,m=1<<I;for(s.sort(function(Q,R){return h[R.s]-h[Q.s]||Q.f-R.f});i<n;++i){var w=s[i].s;if(h[w]>e)p+=m-(1<<g-h[w]),h[w]=e;else break}for(p>>=I;p>0;){var B=s[i].s;h[B]<e?p-=1<<e-h[B]++-1:++i}for(;i>=0&&p;--i){var C=s[i].s;h[C]==e&&(--h[C],++p)}g=e}return{t:new Gt(h),l:g}},u1=function(r,e,t){return r.s==-1?Math.max(u1(r.l,e,t+1),u1(r.r,e,t+1)):e[r.s]=t},lT=function(r){for(var e=r.length;e&&!r[--e];);for(var t=new Ki(++e),i=0,n=r[0],s=1,o=function(c){t[i++]=c},a=1;a<=e;++a)if(r[a]==n&&a!=e)++s;else{if(!n&&s>2){for(;s>138;s-=138)o(32754);s>2&&(o(s>10?s-11<<5|28690:s-3<<5|12305),s=0)}else if(s>3){for(o(n),--s;s>6;s-=6)o(8304);s>2&&(o(s-3<<5|8208),s=0)}for(;s--;)o(n);s=1,n=r[a]}return{c:t.subarray(0,i),n:e}},zf=function(r,e){for(var t=0,i=0;i<e.length;++i)t+=r[i]*e[i];return t},eb=function(r,e,t){var i=t.length,n=Z1(e+2);r[n]=i&255,r[n+1]=i>>8,r[n+2]=r[n]^255,r[n+3]=r[n+1]^255;for(var s=0;s<i;++s)r[n+s+4]=t[s];return(n+4+i)*8},dT=function(r,e,t,i,n,s,o,a,c,l,d){Xs(e,d++,t),++n[256];for(var u=kB(n,15),f=u.t,h=u.l,g=kB(s,15),p=g.t,I=g.l,m=lT(f),w=m.c,B=m.n,C=lT(p),x=C.c,Q=C.n,R=new Ki(19),U=0;U<w.length;++U)++R[w[U]&31];for(var U=0;U<x.length;++U)++R[x[U]&31];for(var T=kB(R,7),D=T.t,b=T.l,_=19;_>4&&!D[c1[_-1]];--_);var v=l+5<<3,E=zf(n,Pa)+zf(s,cA)+o,P=zf(n,f)+zf(s,p)+o+14+3*_+zf(R,D)+2*R[16]+3*R[17]+7*R[18];if(c>=0&&v<=E&&v<=P)return eb(e,d,r.subarray(c,c+l));var N,j,z,L;if(Xs(e,d,1+(P<E)),d+=2,P<E){N=fs(f,h,0),j=f,z=fs(p,I,0),L=p;var G=fs(D,b,0);Xs(e,d,B-257),Xs(e,d+5,Q-1),Xs(e,d+10,_-4),d+=14;for(var U=0;U<_;++U)Xs(e,d+3*U,D[c1[U]]);d+=3*_;for(var K=[w,x],$=0;$<2;++$)for(var te=K[$],U=0;U<te.length;++U){var re=te[U]&31;Xs(e,d,G[re]),d+=D[re],re>15&&(Xs(e,d,te[U]>>5&127),d+=te[U]>>12)}}else N=aee,j=Pa,z=lee,L=cA;for(var U=0;U<a;++U){var ee=i[U];if(ee>255){var re=ee>>18&31;Gf(e,d,N[re+257]),d+=j[re+257],re>7&&(Xs(e,d,ee>>23&31),d+=ZI[re]);var Z=ee&31;Gf(e,d,z[Z]),d+=L[Z],Z>3&&(Gf(e,d,ee>>5&8191),d+=ew[Z])}else Gf(e,d,N[ee]),d+=j[ee]}return Gf(e,d,N[256]),d+j[256]},Aee=new X1([65540,131080,131088,131104,262176,1048704,1048832,2114560,2117632]),yI=new Gt(0),hee=function(r,e,t,i,n,s){var o=s.z||r.length,a=new Gt(i+o+5*(1+Math.ceil(o/7e3))+n),c=a.subarray(i,a.length-n),l=s.l,d=(s.r||0)&7;if(e){d&&(c[0]=s.r>>3);for(var u=Aee[e-1],f=u>>13,h=u&8191,g=(1<<t)-1,p=s.p||new Ki(32768),I=s.h||new Ki(g+1),m=Math.ceil(t/3),w=2*m,B=function(rt){return(r[rt]^r[rt+1]<<m^r[rt+2]<<w)&g},C=new X1(25e3),x=new Ki(288),Q=new Ki(32),R=0,U=0,T=s.i||0,D=0,b=s.w||0,_=0;T+2<o;++T){var v=B(T),E=T&32767,P=I[v];if(p[E]=P,I[v]=E,b<=T){var N=o-T;if((R>7e3||D>24576)&&(N>423||!l)){d=dT(r,c,0,C,x,Q,U,D,_,T-_,d),D=R=U=0,_=T;for(var j=0;j<286;++j)x[j]=0;for(var j=0;j<30;++j)Q[j]=0}var z=2,L=0,G=h,K=E-P&32767;if(N>2&&v==B(T-K))for(var $=Math.min(f,N)-1,te=Math.min(32767,T),re=Math.min(258,N);K<=te&&--G&&E!=P;){if(r[T+z]==r[T+z-K]){for(var ee=0;ee<re&&r[T+ee]==r[T+ee-K];++ee);if(ee>z){if(z=ee,L=K,ee>$)break;for(var Z=Math.min(K,ee-2),me=0,j=0;j<Z;++j){var le=T-K+j&32767,Ae=p[le],ve=le-Ae&32767;ve>me&&(me=ve,P=le)}}}E=P,P=p[E],K+=E-P&32767}if(L){C[D++]=268435456|l1[z]<<18|cT[L];var qe=l1[z]&31,_e=cT[L]&31;U+=ZI[qe]+ew[_e],++x[257+qe],++Q[_e],b=T+z,++R}else C[D++]=r[T],++x[r[T]]}}for(T=Math.max(T,b);T<o;++T)C[D++]=r[T],++x[r[T]];d=dT(r,c,l,C,x,Q,U,D,_,T-_,d),l||(s.r=d&7|c[d/8|0]<<3,d-=7,s.h=I,s.p=p,s.i=T,s.w=b)}else{for(var T=s.w||0;T<o+l;T+=65535){var se=T+65535;se>=o&&(c[d/8|0]=l,se=o),d=eb(c,d+1,r.subarray(T,se))}s.i=o}return rA(a,0,i+Z1(d)+n)},WU=function(){var r=1,e=0;return{p:function(t){for(var i=r,n=e,s=t.length|0,o=0;o!=s;){for(var a=Math.min(o+2655,s);o<a;++o)n+=i+=t[o];i=(i&65535)+15*(i>>16),n=(n&65535)+15*(n>>16)}r=i,e=n},d:function(){return r%=65521,e%=65521,(r&255)<<24|(r&65280)<<8|(e&255)<<8|e>>8}}},XU=function(r,e,t,i,n){if(!n&&(n={l:1},e.dictionary)){var s=e.dictionary.subarray(-32768),o=new Gt(s.length+r.length);o.set(s),o.set(r,s.length),r=o,n.w=s.length}return hee(r,e.level==null?6:e.level,e.mem==null?n.l?Math.ceil(Math.max(8,Math.min(13,Math.log(r.length)))*1.5):20:12+e.mem,t,i,n)},ZU=function(r,e,t){for(;t;++e)r[e]=t,t>>>=8},gee=function(r,e){var t=e.level,i=t==0?0:t<6?1:t==9?3:2;if(r[0]=120,r[1]=i<<6|(e.dictionary&&32),r[1]|=31-(r[0]<<8|r[1])%31,e.dictionary){var n=WU();n.p(e.dictionary),ZU(r,2,n.d())}},pee=function(r,e){return((r[0]&15)!=8||r[0]>>4>7||(r[0]<<8|r[1])%31)&&vr(6,"invalid zlib data"),(r[1]>>5&1)==+!e&&vr(6,"invalid zlib data: "+(r[1]&32?"need":"unexpected")+" dictionary"),(r[1]>>3&4)+2},Zy=(function(){function r(e,t){if(typeof e=="function"&&(t=e,e={}),this.ondata=t,this.o=e||{},this.s={l:0,i:32768,w:32768,z:32768},this.b=new Gt(98304),this.o.dictionary){var i=this.o.dictionary.subarray(-32768);this.b.set(i,32768-i.length),this.s.i=32768-i.length}}return r.prototype.p=function(e,t){this.ondata(XU(e,this.o,0,0,this.s),t)},r.prototype.push=function(e,t){this.ondata||vr(5),this.s.l&&vr(4);var i=e.length+this.s.z;if(i>this.b.length){if(i>2*this.b.length-32768){var n=new Gt(i&-32768);n.set(this.b.subarray(0,this.s.z)),this.b=n}var s=this.b.length-this.s.z;this.b.set(e.subarray(0,s),this.s.z),this.s.z=this.b.length,this.p(this.b,!1),this.b.set(this.b.subarray(-32768)),this.b.set(e.subarray(s),32768),this.s.z=e.length-s+32768,this.s.i=32766,this.s.w=32768}else this.b.set(e,this.s.z),this.s.z+=e.length;this.s.l=t&1,(this.s.z>this.s.w+8191||t)&&(this.p(this.b,t||!1),this.s.w=this.s.i,this.s.i-=2),t&&(this.s=this.o={},this.b=yI)},r.prototype.flush=function(e){if(this.ondata||vr(5),this.s.l&&vr(4),this.p(this.b,!1),this.s.w=this.s.i,this.s.i-=2,e){var t=new Gt(6);t[0]=this.s.r>>3;var i=eb(t,this.s.r,yI);this.s.r=0,this.ondata(t.subarray(0,i>>3),!1)}},r})(),eI=(function(){function r(e,t){typeof e=="function"&&(t=e,e={}),this.ondata=t;var i=e&&e.dictionary&&e.dictionary.subarray(-32768);this.s={i:0,b:i?i.length:0},this.o=new Gt(32768),this.p=new Gt(0),i&&this.o.set(i)}return r.prototype.e=function(e){if(this.ondata||vr(5),this.d&&vr(4),!this.p.length)this.p=e;else if(e.length){var t=new Gt(this.p.length+e.length);t.set(this.p),t.set(e,this.p.length),this.p=t}},r.prototype.c=function(e){this.s.i=+(this.d=e||!1);var t=this.s.b,i=fee(this.p,this.s,this.o);this.ondata(rA(i,t,this.s.b),this.d),this.o=rA(i,this.s.b-32768),this.s.b=this.o.length,this.p=rA(this.p,this.s.p/8|0),this.s.p&=7},r.prototype.push=function(e,t){this.e(e),this.c(t)},r})(),mee=(function(){function r(e,t){this.c=WU(),this.v=1,Zy.call(this,e,t)}return r.prototype.push=function(e,t){this.c.p(e),Zy.prototype.push.call(this,e,t)},r.prototype.p=function(e,t){var i=XU(e,this.o,this.v&&(this.o.dictionary?6:2),t&&4,this.s);this.v&&(gee(i,this.o),this.v=0),t&&ZU(i,i.length-4,this.c.d()),this.ondata(i,t)},r.prototype.flush=function(e){Zy.prototype.flush.call(this,e)},r})(),yee=(function(){function r(e,t){eI.call(this,e,t),this.v=e&&e.dictionary?2:1}return r.prototype.push=function(e,t){if(eI.prototype.e.call(this,e),this.v){if(this.p.length<6&&!t)return;this.p=this.p.subarray(pee(this.p,this.v-1)),this.v=0}t&&(this.p.length<4&&vr(6,"invalid zlib data"),this.p=this.p.subarray(0,-4)),eI.prototype.c.call(this,t)},r})(),Iee=typeof TextDecoder<"u"&&new TextDecoder,wee=0;try{Iee.decode(yI,{stream:!0}),wee=1}catch{}Ta=class{static get tag(){return y.packet.literalData}constructor(e=new Date){this.format=y.literal.utf8,this.date=F.normalizeDate(e),this.text=null,this.data=null,this.filename=""}setText(e,t=y.literal.utf8){this.format=t,this.text=e,this.data=null}getText(e=!1){return(this.text===null||F.isStream(this.text))&&(this.text=F.decodeUTF8(F.nativeEOL(this.getBytes(e)))),this.text}setBytes(e,t){this.format=t,this.data=e,this.text=null}getBytes(e=!1){return this.data===null&&(this.data=F.canonicalizeEOL(F.encodeUTF8(this.text))),e?eA(this.data):this.data}setFilename(e){this.filename=e}getFilename(){return this.filename}async read(e){await MI(e,async t=>{let i=await t.readByte(),n=await t.readByte();this.filename=F.decodeUTF8(await t.readBytes(n)),this.date=F.readDate(await t.readBytes(4));let s=t.remainder();St(s)&&(s=await Xt(s)),this.setBytes(s,i)})}writeHeader(){let e=F.encodeUTF8(this.filename),t=new Uint8Array([e.length]),i=new Uint8Array([this.format]),n=F.writeDate(this.date);return F.concatUint8Array([i,t,e,n])}write(){let e=this.writeHeader(),t=this.getBytes();return F.concat([e,t])}},fo=class r{constructor(){this.bytes=""}read(e){return this.bytes=F.uint8ArrayToString(e.subarray(0,8)),this.bytes.length}write(){return F.stringToUint8Array(this.bytes)}toHex(){return F.uint8ArrayToHex(F.stringToUint8Array(this.bytes))}equals(e,t=!1){return t&&(e.isWildcard()||this.isWildcard())||this.bytes===e.bytes}isNull(){return this.bytes===""}isWildcard(){return/^0+$/.test(this.toHex())}static mapToHex(e){return e.toHex()}static fromID(e){let t=new r;return t.read(F.hexToUint8Array(e)),t}static wildcard(){let e=new r;return e.read(new Uint8Array(8)),e}},Jf=Symbol("verified"),uT="salt@notations.openpgpjs.org",xee=new Set([y.signatureSubpacket.issuerKeyID,y.signatureSubpacket.issuerFingerprint,y.signatureSubpacket.embeddedSignature]),qr=class r{static get tag(){return y.packet.signature}constructor(){this.version=null,this.signatureType=null,this.hashAlgorithm=null,this.publicKeyAlgorithm=null,this.signatureData=null,this.unhashedSubpackets=[],this.unknownSubpackets=[],this.signedHashValue=null,this.salt=null,this.created=null,this.signatureExpirationTime=null,this.signatureNeverExpires=!0,this.exportable=null,this.trustLevel=null,this.trustAmount=null,this.regularExpression=null,this.revocable=null,this.keyExpirationTime=null,this.keyNeverExpires=null,this.preferredSymmetricAlgorithms=null,this.revocationKeyClass=null,this.revocationKeyAlgorithm=null,this.revocationKeyFingerprint=null,this.issuerKeyID=new fo,this.rawNotations=[],this.notations={},this.preferredHashAlgorithms=null,this.preferredCompressionAlgorithms=null,this.keyServerPreferences=null,this.preferredKeyServer=null,this.isPrimaryUserID=null,this.policyURI=null,this.keyFlags=null,this.signersUserID=null,this.reasonForRevocationFlag=null,this.reasonForRevocationString=null,this.features=null,this.signatureTargetPublicKeyAlgorithm=null,this.signatureTargetHashAlgorithm=null,this.signatureTargetHash=null,this.embeddedSignature=null,this.issuerKeyVersion=null,this.issuerFingerprint=null,this.preferredAEADAlgorithms=null,this.preferredCipherSuites=null,this.revoked=null,this[Jf]=null}read(e,t=pe){let i=0;if(this.version=e[i++],this.version===5&&!t.enableParsingV5Entities)throw new ft("Support for v5 entities is disabled; turn on `config.enableParsingV5Entities` if needed");if(this.version!==4&&this.version!==5&&this.version!==6)throw new ft(`Version ${this.version} of the signature packet is unsupported.`);if(this.signatureType=e[i++],this.publicKeyAlgorithm=e[i++],this.hashAlgorithm=e[i++],i+=this.readSubPackets(e.subarray(i,e.length),!0),!this.created)throw new Error("Missing signature creation time subpacket.");if(this.signatureData=e.subarray(0,i),i+=this.readSubPackets(e.subarray(i,e.length),!1),this.signedHashValue=e.subarray(i,i+2),i+=2,this.version===6){let a=e[i++];this.salt=e.subarray(i,i+a),i+=a}let n=e.subarray(i,e.length),{read:s,signatureParams:o}=VZ(this.publicKeyAlgorithm,n);if(s<n.length)throw new Error("Error reading MPIs");this.params=o}writeParams(){return this.params instanceof Promise?Zc(async()=>Rd(this.publicKeyAlgorithm,await this.params)):Rd(this.publicKeyAlgorithm,this.params)}write(){let e=[];return e.push(this.signatureData),e.push(this.writeUnhashedSubPackets()),e.push(this.signedHashValue),this.version===6&&(e.push(new Uint8Array([this.salt.length])),e.push(this.salt)),e.push(this.writeParams()),F.concat(e)}async sign(e,t,i=new Date,n=!1,s){this.version=e.version,this.created=F.normalizeDate(i),this.issuerKeyVersion=e.version,this.issuerFingerprint=e.getFingerprintBytes(),this.issuerKeyID=e.getKeyID();let o=[new Uint8Array([this.version,this.signatureType,this.publicKeyAlgorithm,this.hashAlgorithm])];if(this.version===6){let d=SB(this.hashAlgorithm);if(this.salt===null)this.salt=Nr(d);else if(d!==this.salt.length)throw new Error("Provided salt does not have the required length")}else if(s.nonDeterministicSignaturesViaNotation)if(this.rawNotations.filter(({name:u})=>u===uT).length===0){let u=Nr(SB(this.hashAlgorithm));this.rawNotations.push({name:uT,value:u,humanReadable:!1,critical:!1})}else throw new Error("Unexpected existing salt notation");o.push(this.writeHashedSubPackets()),this.unhashedSubpackets=[],this.signatureData=F.concat(o);let a=this.toHash(this.signatureType,t,n),c=await this.hash(this.signatureType,t,a,n);this.signedHashValue=Rr(Jc(c),0,2);let l=async()=>WZ(this.publicKeyAlgorithm,this.hashAlgorithm,e.publicParams,e.privateParams,a,await Xt(c));F.isStream(c)?this.params=l():(this.params=await l(),this[Jf]=!0)}writeHashedSubPackets(){let e=y.signatureSubpacket,t=[],i;if(this.created===null)throw new Error("Missing signature creation time");t.push(yt(e.signatureCreationTime,!0,F.writeDate(this.created))),this.signatureExpirationTime!==null&&t.push(yt(e.signatureExpirationTime,!0,F.writeNumber(this.signatureExpirationTime,4))),this.exportable!==null&&t.push(yt(e.exportableCertification,!0,new Uint8Array([this.exportable?1:0]))),this.trustLevel!==null&&(i=new Uint8Array([this.trustLevel,this.trustAmount]),t.push(yt(e.trustSignature,!0,i))),this.regularExpression!==null&&t.push(yt(e.regularExpression,!0,this.regularExpression)),this.revocable!==null&&t.push(yt(e.revocable,!0,new Uint8Array([this.revocable?1:0]))),this.keyExpirationTime!==null&&t.push(yt(e.keyExpirationTime,!0,F.writeNumber(this.keyExpirationTime,4))),this.preferredSymmetricAlgorithms!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.preferredSymmetricAlgorithms)),t.push(yt(e.preferredSymmetricAlgorithms,!1,i))),this.revocationKeyClass!==null&&(i=new Uint8Array([this.revocationKeyClass,this.revocationKeyAlgorithm]),i=F.concat([i,this.revocationKeyFingerprint]),t.push(yt(e.revocationKey,!1,i))),!this.issuerKeyID.isNull()&&this.issuerKeyVersion<5&&t.push(yt(e.issuerKeyID,!1,this.issuerKeyID.write())),this.rawNotations.forEach(({name:o,value:a,humanReadable:c,critical:l})=>{i=[new Uint8Array([c?128:0,0,0,0])];let d=F.encodeUTF8(o);i.push(F.writeNumber(d.length,2)),i.push(F.writeNumber(a.length,2)),i.push(d),i.push(a),i=F.concat(i),t.push(yt(e.notationData,l,i))}),this.preferredHashAlgorithms!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.preferredHashAlgorithms)),t.push(yt(e.preferredHashAlgorithms,!1,i))),this.preferredCompressionAlgorithms!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.preferredCompressionAlgorithms)),t.push(yt(e.preferredCompressionAlgorithms,!1,i))),this.keyServerPreferences!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.keyServerPreferences)),t.push(yt(e.keyServerPreferences,!1,i))),this.preferredKeyServer!==null&&t.push(yt(e.preferredKeyServer,!1,F.encodeUTF8(this.preferredKeyServer))),this.isPrimaryUserID!==null&&t.push(yt(e.primaryUserID,!1,new Uint8Array([this.isPrimaryUserID?1:0]))),this.policyURI!==null&&t.push(yt(e.policyURI,!1,F.encodeUTF8(this.policyURI))),this.keyFlags!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.keyFlags)),t.push(yt(e.keyFlags,!0,i))),this.signersUserID!==null&&t.push(yt(e.signersUserID,!1,F.encodeUTF8(this.signersUserID))),this.reasonForRevocationFlag!==null&&(i=F.stringToUint8Array(String.fromCharCode(this.reasonForRevocationFlag)+this.reasonForRevocationString),t.push(yt(e.reasonForRevocation,!0,i))),this.features!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.features)),t.push(yt(e.features,!1,i))),this.signatureTargetPublicKeyAlgorithm!==null&&(i=[new Uint8Array([this.signatureTargetPublicKeyAlgorithm,this.signatureTargetHashAlgorithm])],i.push(F.stringToUint8Array(this.signatureTargetHash)),i=F.concat(i),t.push(yt(e.signatureTarget,!0,i))),this.embeddedSignature!==null&&t.push(yt(e.embeddedSignature,!0,this.embeddedSignature.write())),this.issuerFingerprint!==null&&(i=[new Uint8Array([this.issuerKeyVersion]),this.issuerFingerprint],i=F.concat(i),t.push(yt(e.issuerFingerprint,this.version>=5,i))),this.preferredAEADAlgorithms!==null&&(i=F.stringToUint8Array(F.uint8ArrayToString(this.preferredAEADAlgorithms)),t.push(yt(e.preferredAEADAlgorithms,!1,i))),this.preferredCipherSuites!==null&&(i=new Uint8Array([].concat(...this.preferredCipherSuites)),t.push(yt(e.preferredCipherSuites,!1,i)));let n=F.concat(t),s=F.writeNumber(n.length,this.version===6?4:2);return F.concat([s,n])}writeUnhashedSubPackets(){let e=this.unhashedSubpackets.map(({type:n,critical:s,body:o})=>yt(n,s,o)),t=F.concat(e),i=F.writeNumber(t.length,this.version===6?4:2);return F.concat([i,t])}readSubPacket(e,t=!0){let i=0,n=!!(e[i]&128),s=e[i]&127;if(i++,!(!t&&(this.unhashedSubpackets.push({type:s,critical:n,body:e.subarray(i,e.length)}),!xee.has(s))))switch(s){case y.signatureSubpacket.signatureCreationTime:this.created=F.readDate(e.subarray(i,e.length));break;case y.signatureSubpacket.signatureExpirationTime:{let o=F.readNumber(e.subarray(i,e.length));this.signatureNeverExpires=o===0,this.signatureExpirationTime=o;break}case y.signatureSubpacket.exportableCertification:this.exportable=e[i++]===1;break;case y.signatureSubpacket.trustSignature:this.trustLevel=e[i++],this.trustAmount=e[i++];break;case y.signatureSubpacket.regularExpression:this.regularExpression=e[i];break;case y.signatureSubpacket.revocable:this.revocable=e[i++]===1;break;case y.signatureSubpacket.keyExpirationTime:{let o=F.readNumber(e.subarray(i,e.length));this.keyExpirationTime=o,this.keyNeverExpires=o===0;break}case y.signatureSubpacket.preferredSymmetricAlgorithms:this.preferredSymmetricAlgorithms=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.revocationKey:this.revocationKeyClass=e[i++],this.revocationKeyAlgorithm=e[i++],this.revocationKeyFingerprint=e.subarray(i,i+20);break;case y.signatureSubpacket.issuerKeyID:if(this.version===4)this.issuerKeyID.read(e.subarray(i,e.length));else if(t)throw new Error("Unexpected Issuer Key ID subpacket");break;case y.signatureSubpacket.notationData:{let o=!!(e[i]&128);i+=4;let a=F.readNumber(e.subarray(i,i+2));i+=2;let c=F.readNumber(e.subarray(i,i+2));i+=2;let l=F.decodeUTF8(e.subarray(i,i+a)),d=e.subarray(i+a,i+a+c);this.rawNotations.push({name:l,humanReadable:o,value:d,critical:n}),o&&(this.notations[l]=F.decodeUTF8(d));break}case y.signatureSubpacket.preferredHashAlgorithms:this.preferredHashAlgorithms=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.preferredCompressionAlgorithms:this.preferredCompressionAlgorithms=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.keyServerPreferences:this.keyServerPreferences=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.preferredKeyServer:this.preferredKeyServer=F.decodeUTF8(e.subarray(i,e.length));break;case y.signatureSubpacket.primaryUserID:this.isPrimaryUserID=e[i++]!==0;break;case y.signatureSubpacket.policyURI:this.policyURI=F.decodeUTF8(e.subarray(i,e.length));break;case y.signatureSubpacket.keyFlags:this.keyFlags=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.signersUserID:this.signersUserID=F.decodeUTF8(e.subarray(i,e.length));break;case y.signatureSubpacket.reasonForRevocation:this.reasonForRevocationFlag=e[i++],this.reasonForRevocationString=F.decodeUTF8(e.subarray(i,e.length));break;case y.signatureSubpacket.features:this.features=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.signatureTarget:{this.signatureTargetPublicKeyAlgorithm=e[i++],this.signatureTargetHashAlgorithm=e[i++];let o=pr(this.signatureTargetHashAlgorithm);this.signatureTargetHash=F.uint8ArrayToString(e.subarray(i,i+o));break}case y.signatureSubpacket.embeddedSignature:this.embeddedSignature=new r,this.embeddedSignature.read(e.subarray(i,e.length));break;case y.signatureSubpacket.issuerFingerprint:this.issuerKeyVersion=e[i++],this.issuerFingerprint=e.subarray(i,e.length),this.issuerKeyVersion>=5?this.issuerKeyID.read(this.issuerFingerprint):this.issuerKeyID.read(this.issuerFingerprint.subarray(-8));break;case y.signatureSubpacket.preferredAEADAlgorithms:this.preferredAEADAlgorithms=[...e.subarray(i,e.length)];break;case y.signatureSubpacket.preferredCipherSuites:this.preferredCipherSuites=[];for(let o=i;o<e.length;o+=2)this.preferredCipherSuites.push([e[o],e[o+1]]);break;default:this.unknownSubpackets.push({type:s,critical:n,body:e.subarray(i,e.length)});break}}readSubPackets(e,t=!0,i){let n=this.version===6?4:2,s=F.readNumber(e.subarray(0,n)),o=n;for(;o<2+s;){let a=hU(e.subarray(o,e.length));o+=a.offset,this.readSubPacket(e.subarray(o,o+a.len),t,i),o+=a.len}return o}toSign(e,t){let i=y.signature;switch(e){case i.binary:return t.text!==null?F.encodeUTF8(t.getText(!0)):t.getBytes(!0);case i.text:{let n=t.getBytes(!0);return F.canonicalizeEOL(n)}case i.standalone:return new Uint8Array(0);case i.certGeneric:case i.certPersona:case i.certCasual:case i.certPositive:case i.certRevocation:{let n,s;if(t.userID)s=180,n=t.userID;else if(t.userAttribute)s=209,n=t.userAttribute;else throw new Error("Either a userID or userAttribute packet needs to be supplied for certification.");let o=n.write();return F.concat([this.toSign(i.key,t),new Uint8Array([s]),F.writeNumber(o.length,4),o])}case i.subkeyBinding:case i.subkeyRevocation:case i.keyBinding:return F.concat([this.toSign(i.key,t),this.toSign(i.key,{key:t.bind})]);case i.key:if(t.key===void 0)throw new Error("Key packet is required for this signature.");return t.key.writeForHash(this.version);case i.keyRevocation:return this.toSign(i.key,t);case i.timestamp:return new Uint8Array(0);case i.thirdParty:throw new Error("Not implemented");default:throw new Error("Unknown signature type.")}}calculateTrailer(e,t){let i=0;return sr(Jc(this.signatureData),n=>{i+=n.length},()=>{let n=[];return this.version===5&&(this.signatureType===y.signature.binary||this.signatureType===y.signature.text)&&(t?n.push(new Uint8Array(6)):n.push(e.writeHeader())),n.push(new Uint8Array([this.version,255])),this.version===5&&n.push(new Uint8Array(4)),n.push(F.writeNumber(i,4)),F.concat(n)})}toHash(e,t,i=!1){let n=this.toSign(e,t);return F.concat([this.salt||new Uint8Array,n,this.signatureData,this.calculateTrailer(t,i)])}async hash(e,t,i,n=!1){if(this.version===6&&this.salt.length!==SB(this.hashAlgorithm))throw new Error("Signature salt does not have the expected length");return i||(i=this.toHash(e,t,n)),hs(this.hashAlgorithm,i)}async verify(e,t,i,n=new Date,s=!1,o=pe){if(!this.issuerKeyID.equals(e.getKeyID()))throw new Error("Signature was not issued by the given public key");if(this.publicKeyAlgorithm!==e.algorithm)throw new Error("Public key algorithm used to sign signature does not match issuer key algorithm.");let a=t===y.signature.binary||t===y.signature.text;if(!(this[Jf]&&!a)){let d,u;if(this.hashed?u=await this.hashed:(d=this.toHash(t,i,s),u=await this.hash(t,i,d)),u=await Xt(u),this.signedHashValue[0]!==u[0]||this.signedHashValue[1]!==u[1])throw new Error("Signed digest did not match");if(this.params=await this.params,this[Jf]=await YZ(this.publicKeyAlgorithm,this.hashAlgorithm,this.params,e.publicParams,d,u),!this[Jf])throw new Error("Signature verification failed")}let l=F.normalizeDate(n);if(l&&this.created>l)throw new Error("Signature creation time is in the future");if(l&&l>=this.getExpirationTime())throw new Error("Signature is expired");if(o.rejectHashAlgorithms.has(this.hashAlgorithm))throw new Error("Insecure hash algorithm: "+y.read(y.hash,this.hashAlgorithm).toUpperCase());if(o.rejectMessageHashAlgorithms.has(this.hashAlgorithm)&&[y.signature.binary,y.signature.text].includes(this.signatureType))throw new Error("Insecure message hash algorithm: "+y.read(y.hash,this.hashAlgorithm).toUpperCase());if(this.unknownSubpackets.forEach(({type:d,critical:u})=>{if(u)throw new Error(`Unknown critical signature subpacket type ${d}`)}),this.rawNotations.forEach(({name:d,critical:u})=>{if(u&&o.knownNotations.indexOf(d)<0)throw new Error(`Unknown critical notation: ${d}`)}),this.revocationKeyClass!==null)throw new Error("This key is intended to be revoked with an authorized key, which OpenPGP.js does not support.")}isExpired(e=new Date){let t=F.normalizeDate(e);return t!==null?!(this.created<=t&&t<this.getExpirationTime()):!1}getExpirationTime(){return this.signatureNeverExpires?1/0:new Date(this.created.getTime()+this.signatureExpirationTime*1e3)}};cn=class r{static get tag(){return y.packet.onePassSignature}static fromSignaturePacket(e,t){let i=new r;return i.version=e.version===6?6:3,i.signatureType=e.signatureType,i.hashAlgorithm=e.hashAlgorithm,i.publicKeyAlgorithm=e.publicKeyAlgorithm,i.issuerKeyID=e.issuerKeyID,i.salt=e.salt,i.issuerFingerprint=e.issuerFingerprint,i.flags=t?1:0,i}constructor(){this.version=null,this.signatureType=null,this.hashAlgorithm=null,this.publicKeyAlgorithm=null,this.salt=null,this.issuerKeyID=null,this.issuerFingerprint=null,this.flags=null}read(e){let t=0;if(this.version=e[t++],this.version!==3&&this.version!==6)throw new ft(`Version ${this.version} of the one-pass signature packet is unsupported.`);if(this.signatureType=e[t++],this.hashAlgorithm=e[t++],this.publicKeyAlgorithm=e[t++],this.version===6){let i=e[t++];this.salt=e.subarray(t,t+i),t+=i,this.issuerFingerprint=e.subarray(t,t+32),t+=32,this.issuerKeyID=new fo,this.issuerKeyID.read(this.issuerFingerprint)}else this.issuerKeyID=new fo,this.issuerKeyID.read(e.subarray(t,t+8)),t+=8;return this.flags=e[t++],this}write(){let e=[new Uint8Array([this.version,this.signatureType,this.hashAlgorithm,this.publicKeyAlgorithm])];return this.version===6?e.push(new Uint8Array([this.salt.length]),this.salt,this.issuerFingerprint):e.push(this.issuerKeyID.write()),e.push(new Uint8Array([this.flags])),F.concatUint8Array(e)}calculateTrailer(...e){return Zc(async()=>qr.prototype.calculateTrailer.apply(await this.correspondingSig,e))}async verify(){let e=await this.correspondingSig;if(!e||e.constructor.tag!==y.packet.signature)throw new Error("Corresponding signature packet missing");if(e.signatureType!==this.signatureType||e.hashAlgorithm!==this.hashAlgorithm||e.publicKeyAlgorithm!==this.publicKeyAlgorithm||!e.issuerKeyID.equals(this.issuerKeyID)||this.version===3&&e.version===6||this.version===6&&e.version!==6||this.version===6&&!F.equalsUint8Array(e.issuerFingerprint,this.issuerFingerprint)||this.version===6&&!F.equalsUint8Array(e.salt,this.salt))throw new Error("Corresponding signature packet does not match one-pass signature packet");return e.hashed=this.hashed,e.verify.apply(e,arguments)}};cn.prototype.hash=qr.prototype.hash;cn.prototype.toHash=qr.prototype.toHash;cn.prototype.toSign=qr.prototype.toSign;Ct=class r extends Array{static async fromBinary(e,t,i=pe,n=null,s=!1){let o=new r;return await o.read(e,t,i,n,s),o}async read(e,t,i=pe,n=null,s=!1){let o;i.additionalAllowedPackets.length&&(o=F.constructAllowedPackets(i.additionalAllowedPackets),t={...t,...o}),this.stream=Ua(e,async(c,l)=>{let d=Fn(c),u=Gi(l);try{let f=F.isStream(c);for(;;){await u.ready;let h,g;if(await PX(d,f,async m=>{try{if(m.tag===y.packet.marker||m.tag===y.packet.trust||m.tag===y.packet.padding)return;let w=Cee(m.tag,t);try{n?.recordPacket(m.tag,o)}catch(B){if(i.enforceGrammar)throw B;F.printDebugError(B)}w.packets=new r,w.fromStream=F.isStream(m.packet),g=w.fromStream;try{await w.read(m.packet,i)}catch(B){throw B instanceof ft?B:F.wrapError(new tA(`Parsing ${w.constructor.name} failed`),B)}await u.write(w)}catch(w){let B=w instanceof Sd&&m.tag<=39,C=w instanceof ft&&!(w instanceof Sd)&&!i.ignoreUnsupportedPackets,x=w instanceof tA&&!i.ignoreMalformedPackets,Q=$y(m.tag),R=!(w instanceof Sd||w instanceof ft||w instanceof tA);if(B||C||x||Q||R)s?h=w:await u.abort(w);else{let U=new nA(m.tag,m.packet);await u.write(U)}F.printDebugError(w)}}),g&&(f=null),h)throw await d.readToEnd(),h;let p=await d.peekBytes(2);if(!p||!p.length){try{n?.recordEnd()}catch(m){if(i.enforceGrammar)throw m;F.printDebugError(m)}await u.ready,await u.close();return}}}catch(f){await u.abort(f)}});let a=Fn(this.stream);for(;;){let{done:c,value:l}=await a.read();if(c?this.stream=null:this.push(l),c||$y(l.constructor.tag))break}a.releaseLock()}write(){let e=[];for(let t=0;t<this.length;t++){let i=this[t]instanceof nA?this[t].tag:this[t].constructor.tag,n=this[t].write();if(F.isStream(n)&&$y(this[t].constructor.tag)){let s=[],o=0,a=512;e.push(sI(i)),e.push(sr(n,c=>{if(s.push(c),o+=c.length,o>=a){let l=Math.min(Math.log(o)/Math.LN2|0,30),d=2**l,u=F.concat([FX(l)].concat(s));return s=[u.subarray(1+d)],o=s[0].length,u.subarray(0,1+d)}},()=>F.concat([HI(o)].concat(s))))}else{if(F.isStream(n)){let s=0;e.push(sr(Jc(n),o=>{s+=o.length},()=>K6(i,s)))}else e.push(K6(i,n.length));e.push(n)}}return F.concat(e)}filterByTag(...e){let t=new r,i=n=>s=>n===s;for(let n=0;n<this.length;n++)e.some(i(this[n].constructor.tag))&&t.push(this[n]);return t}findPacket(e){return this.find(t=>t.constructor.tag===e)}indexOfTag(...e){let t=[],i=this,n=s=>o=>s===o;for(let s=0;s<this.length;s++)e.some(n(i[s].constructor.tag))&&t.push(s);return t}},ns=class r extends Error{constructor(...e){super(...e),Error.captureStackTrace&&Error.captureStackTrace(this,r),this.name="GrammarError"}};(function(r){r[r.EmptyMessage=0]="EmptyMessage",r[r.PlaintextOrEncryptedData=1]="PlaintextOrEncryptedData",r[r.EncryptedSessionKeys=2]="EncryptedSessionKeys",r[r.StandaloneAdditionalAllowedData=3]="StandaloneAdditionalAllowedData"})(Kt||(Kt={}));qd=class{constructor(){this.state=Kt.EmptyMessage,this.leadingOnePassSignatureCounter=0}recordPacket(e,t){switch(this.state){case Kt.EmptyMessage:case Kt.StandaloneAdditionalAllowedData:switch(e){case y.packet.literalData:case y.packet.compressedData:case y.packet.aeadEncryptedData:case y.packet.symEncryptedIntegrityProtectedData:case y.packet.symmetricallyEncryptedData:this.state=Kt.PlaintextOrEncryptedData;return;case y.packet.signature:if(this.state===Kt.StandaloneAdditionalAllowedData&&--this.leadingOnePassSignatureCounter<0)throw new ns("Trailing signature packet without OPS");return;case y.packet.onePassSignature:if(this.state===Kt.StandaloneAdditionalAllowedData)throw new ns("OPS following StandaloneAdditionalAllowedData");this.leadingOnePassSignatureCounter++;return;case y.packet.publicKeyEncryptedSessionKey:case y.packet.symEncryptedSessionKey:this.state=Kt.EncryptedSessionKeys;return;default:if(!t?.[e])throw new ns(`Unexpected packet ${e} in state ${this.state}`);this.state=Kt.StandaloneAdditionalAllowedData;return}case Kt.PlaintextOrEncryptedData:switch(e){case y.packet.signature:if(--this.leadingOnePassSignatureCounter<0)throw new ns("Trailing signature packet without OPS");this.state=Kt.PlaintextOrEncryptedData;return;default:if(!t?.[e])throw new ns(`Unexpected packet ${e} in state ${this.state}`);this.state=Kt.PlaintextOrEncryptedData;return}case Kt.EncryptedSessionKeys:switch(e){case y.packet.publicKeyEncryptedSessionKey:case y.packet.symEncryptedSessionKey:this.state=Kt.EncryptedSessionKeys;return;case y.packet.symEncryptedIntegrityProtectedData:case y.packet.aeadEncryptedData:case y.packet.symmetricallyEncryptedData:this.state=Kt.PlaintextOrEncryptedData;return;case y.packet.signature:if(--this.leadingOnePassSignatureCounter<0)throw new ns("Trailing signature packet without OPS");this.state=Kt.PlaintextOrEncryptedData;return;default:if(!t?.[e])throw new ns(`Unexpected packet ${e} in state ${this.state}`);this.state=Kt.EncryptedSessionKeys}}}recordEnd(){switch(this.state){case Kt.EmptyMessage:case Kt.PlaintextOrEncryptedData:case Kt.EncryptedSessionKeys:case Kt.StandaloneAdditionalAllowedData:if(this.leadingOnePassSignatureCounter>0)throw new ns("Missing trailing signature packets")}}},Eee=F.constructAllowedPackets([Ta,cn,qr]),Vc=class{static get tag(){return y.packet.compressedData}constructor(e=pe){this.packets=null,this.algorithm=e.preferredCompressionAlgorithm,this.compressed=null}async read(e,t=pe){await MI(e,async i=>{this.algorithm=await i.readByte(),this.compressed=i.remainder(),await this.decompress(t)})}write(){return this.compressed===null&&this.compress(),F.concat([new Uint8Array([this.algorithm]),this.compressed])}async decompress(e=pe){let t=y.read(y.compression,this.algorithm),i=kee[t];if(!i)throw new Error(`${t} decompression not supported`);let n=await i(this.compressed);if(e.maxDecompressedMessageSize!==1/0){let s=0;n=sr(n,o=>{if(s+=o.length,s>e.maxDecompressedMessageSize)throw new Error("Maximum decompressed message size exceeded");return o})}(!or(this.compressed)||St(this.compressed))&&(n=await Xt(n)),this.packets=await Ct.fromBinary(n,Eee,e,new qd)}compress(){let e=y.read(y.compression,this.algorithm),t=vee[e];if(!t)throw new Error(`${e} compression not supported`);let i=this.packets.write(),n=t(i);(!or(i)||St(i))&&(n=Zc(()=>Xt(n))),this.compressed=n}};wI=r=>({compressor:typeof CompressionStream<"u"&&(()=>new CompressionStream(r)),decompressor:typeof DecompressionStream<"u"&&(()=>new DecompressionStream(r))}),vee={zip:II(wI("deflate-raw").compressor,Zy),zlib:II(wI("deflate").compressor,mee)},kee={uncompressed:r=>r,zip:II(wI("deflate-raw").decompressor,eI),zlib:II(wI("deflate").decompressor,yee),bzip2:bee()},See=F.constructAllowedPackets([Ta,Vc,cn,qr]),Od=class r{static get tag(){return y.packet.symEncryptedIntegrityProtectedData}static fromObject({version:e,aeadAlgorithm:t}){if(e!==1&&e!==2)throw new Error("Unsupported SEIPD version");let i=new r;return i.version=e,e===2&&(i.aeadAlgorithm=t),i}constructor(){this.version=null,this.cipherAlgorithm=null,this.aeadAlgorithm=null,this.chunkSizeByte=null,this.salt=null,this.encrypted=null,this.packets=null}async read(e){await MI(e,async t=>{if(this.version=await t.readByte(),this.version!==1&&this.version!==2)throw new ft(`Version ${this.version} of the SEIP packet is unsupported.`);this.version===2&&(this.cipherAlgorithm=await t.readByte(),this.aeadAlgorithm=await t.readByte(),this.chunkSizeByte=await t.readByte(),this.salt=await t.readBytes(32)),this.encrypted=t.remainder()})}write(){return this.version===2?F.concat([new Uint8Array([this.version,this.cipherAlgorithm,this.aeadAlgorithm,this.chunkSizeByte]),this.salt,this.encrypted]):F.concat([new Uint8Array([this.version]),this.encrypted])}async encrypt(e,t,i=pe){let{blockSize:n,keySize:s}=It(e);if(t.length!==s)throw new Error("Unexpected session key size");let o=this.packets.write();if(St(o)&&(o=await Xt(o)),this.version===2)this.cipherAlgorithm=e,this.salt=Nr(32),this.chunkSizeByte=i.aeadChunkSizeByte,this.encrypted=await xI(this,"encrypt",t,o);else{let a=await zU(e),c=new Uint8Array([211,20]),l=F.concat([a,o,c]),d=await hs(y.hash.sha1,eA(l)),u=F.concat([l,d]);this.encrypted=await aA(e,t,u,new Uint8Array(n))}return!0}async decrypt(e,t,i=pe){if(t.length!==It(e).keySize)throw new Error("Unexpected session key size");let n=Jc(this.encrypted);St(n)&&(n=await Xt(n));let s,o=!1;if(this.version===2){if(this.cipherAlgorithm!==e)throw new Error("Unexpected session key algorithm");s=await xI(this,"decrypt",t,n)}else{let{blockSize:a}=It(e),c=await XI(e,t,n,new Uint8Array(a)),l=Rr(eA(c),-20),d=Rr(c,0,-20),u=Promise.all([Xt(await hs(y.hash.sha1,eA(d))),Xt(l)]).then(([h,g])=>{if(!F.equalsUint8Array(h,g))throw new Error("Modification detected.");return new Uint8Array}),f=Rr(d,a+2);s=Rr(f,0,-2),s=ui([s,Zc(()=>u)]),F.isStream(n)&&i.allowUnauthenticatedStream?o=!0:s=await Xt(s)}return this.packets=await Ct.fromBinary(s,See,i,new qd,o),!0}};Dee=F.constructAllowedPackets([Ta,Vc,cn,qr]),fT=1,f1=class{static get tag(){return y.packet.aeadEncryptedData}constructor(){this.version=fT,this.cipherAlgorithm=null,this.aeadAlgorithm=y.aead.eax,this.chunkSizeByte=null,this.iv=null,this.encrypted=null,this.packets=null}async read(e){await MI(e,async t=>{let i=await t.readByte();if(i!==fT)throw new ft(`Version ${i} of the AEAD-encrypted data packet is not supported.`);this.cipherAlgorithm=await t.readByte(),this.aeadAlgorithm=await t.readByte(),this.chunkSizeByte=await t.readByte();let n=uo(this.aeadAlgorithm,!0);this.iv=await t.readBytes(n.ivLength),this.encrypted=t.remainder()})}write(){return F.concat([new Uint8Array([this.version,this.cipherAlgorithm,this.aeadAlgorithm,this.chunkSizeByte]),this.iv,this.encrypted])}async decrypt(e,t,i=pe){this.packets=await Ct.fromBinary(await xI(this,"decrypt",t,Jc(this.encrypted)),Dee,i,new qd)}async encrypt(e,t,i=pe){this.cipherAlgorithm=e;let{ivLength:n}=uo(this.aeadAlgorithm,!0);this.iv=Nr(n),this.chunkSizeByte=i.aeadChunkSizeByte;let s=this.packets.write();this.encrypted=await xI(this,"encrypt",t,s)}},lA=class r{static get tag(){return y.packet.publicKeyEncryptedSessionKey}constructor(){this.version=null,this.publicKeyID=new fo,this.publicKeyVersion=null,this.publicKeyFingerprint=null,this.publicKeyAlgorithm=null,this.sessionKey=null,this.sessionKeyAlgorithm=null,this.encrypted={}}static fromObject({version:e,encryptionKeyPacket:t,anonymousRecipient:i,sessionKey:n,sessionKeyAlgorithm:s}){let o=new r;if(e!==3&&e!==6)throw new Error("Unsupported PKESK version");return o.version=e,e===6&&(o.publicKeyVersion=i?null:t.version,o.publicKeyFingerprint=i?null:t.getFingerprintBytes()),o.publicKeyID=i?fo.wildcard():t.getKeyID(),o.publicKeyAlgorithm=t.algorithm,o.sessionKey=n,o.sessionKeyAlgorithm=s,o}read(e){let t=0;if(this.version=e[t++],this.version!==3&&this.version!==6)throw new ft(`Version ${this.version} of the PKESK packet is unsupported.`);if(this.version===6){let i=e[t++];if(i){this.publicKeyVersion=e[t++];let n=i-1;this.publicKeyFingerprint=e.subarray(t,t+n),t+=n,this.publicKeyVersion>=5?this.publicKeyID.read(this.publicKeyFingerprint):this.publicKeyID.read(this.publicKeyFingerprint.subarray(-8))}else this.publicKeyID=fo.wildcard()}else t+=this.publicKeyID.read(e.subarray(t,t+8));if(this.publicKeyAlgorithm=e[t++],this.encrypted=DZ(this.publicKeyAlgorithm,e.subarray(t)),this.publicKeyAlgorithm===y.publicKey.x25519||this.publicKeyAlgorithm===y.publicKey.x448){if(this.version===3)this.sessionKeyAlgorithm=y.write(y.symmetric,this.encrypted.C.algorithm);else if(this.encrypted.C.algorithm!==null)throw new Error("Unexpected cleartext symmetric algorithm")}}write(){let e=[new Uint8Array([this.version])];return this.version===6?this.publicKeyFingerprint!==null?(e.push(new Uint8Array([this.publicKeyFingerprint.length+1,this.publicKeyVersion])),e.push(this.publicKeyFingerprint)):e.push(new Uint8Array([0])):e.push(this.publicKeyID.write()),e.push(new Uint8Array([this.publicKeyAlgorithm]),Rd(this.publicKeyAlgorithm,this.encrypted)),F.concatUint8Array(e)}async encrypt(e){let t=y.write(y.publicKey,this.publicKeyAlgorithm),i=this.version===3?this.sessionKeyAlgorithm:null,n=e.version===5?e.getFingerprintBytes().subarray(0,20):e.getFingerprintBytes(),s=AT(this.version,t,i,this.sessionKey);this.encrypted=await vZ(t,i,e.publicParams,s,n)}async decrypt(e,t){if(this.publicKeyAlgorithm!==e.algorithm)throw new Error("Decryption error");let i=t?AT(this.version,this.publicKeyAlgorithm,t.sessionKeyAlgorithm,t.sessionKey):null,n=e.version===5?e.getFingerprintBytes().subarray(0,20):e.getFingerprintBytes(),s=await kZ(this.publicKeyAlgorithm,e.publicParams,e.privateParams,this.encrypted,n,i),{sessionKey:o,sessionKeyAlgorithm:a}=Qee(this.version,this.publicKeyAlgorithm,s,t);if(this.version===3){let c=this.publicKeyAlgorithm!==y.publicKey.x25519&&this.publicKeyAlgorithm!==y.publicKey.x448;if(this.sessionKeyAlgorithm=c?a:this.sessionKeyAlgorithm,o.length!==It(this.sessionKeyAlgorithm).keySize)throw new Error("Unexpected session key size")}this.sessionKey=o}};dA=class r{static get tag(){return y.packet.symEncryptedSessionKey}constructor(e=pe){this.version=e.aeadProtect?6:4,this.sessionKey=null,this.sessionKeyEncryptionAlgorithm=null,this.sessionKeyAlgorithm=null,this.aeadAlgorithm=y.write(y.aead,e.preferredAEADAlgorithm),this.encrypted=null,this.s2k=null,this.iv=null}read(e){let t=0;if(this.version=e[t++],this.version!==4&&this.version!==5&&this.version!==6)throw new ft(`Version ${this.version} of the SKESK packet is unsupported.`);this.version===6&&t++;let i=e[t++];this.version>=5&&(this.aeadAlgorithm=e[t++],this.version===6&&t++);let n=e[t++];if(this.s2k=mI(n),t+=this.s2k.read(e.subarray(t,e.length)),this.version>=5){let s=uo(this.aeadAlgorithm,!0);this.iv=e.subarray(t,t+=s.ivLength)}this.version>=5||t<e.length?(this.encrypted=e.subarray(t,e.length),this.sessionKeyEncryptionAlgorithm=i):this.sessionKeyAlgorithm=i}write(){let e=this.encrypted===null?this.sessionKeyAlgorithm:this.sessionKeyEncryptionAlgorithm,t,i=this.s2k.write();if(this.version===6){let n=i.length,s=3+n+this.iv.length;t=F.concatUint8Array([new Uint8Array([this.version,s,e,this.aeadAlgorithm,n]),i,this.iv,this.encrypted])}else this.version===5?t=F.concatUint8Array([new Uint8Array([this.version,e,this.aeadAlgorithm]),i,this.iv,this.encrypted]):(t=F.concatUint8Array([new Uint8Array([this.version,e]),i]),this.encrypted!==null&&(t=F.concatUint8Array([t,this.encrypted])));return t}async decrypt(e,t=pe){let i=this.sessionKeyEncryptionAlgorithm!==null?this.sessionKeyEncryptionAlgorithm:this.sessionKeyAlgorithm,{blockSize:n,keySize:s}=It(i),o=await this.s2k.produceKey(e,s,t);if(this.version>=5){let a=uo(this.aeadAlgorithm,!0),c=new Uint8Array([192|r.tag,this.version,this.sessionKeyEncryptionAlgorithm,this.aeadAlgorithm]),l=this.version===6?await Fa(y.hash.sha256,o,new Uint8Array,c,s):o,d=await a(i,l);this.sessionKey=await d.decrypt(this.encrypted,this.iv,c)}else if(this.encrypted!==null){let a=await XI(i,o,this.encrypted,new Uint8Array(n));if(this.sessionKeyAlgorithm=y.write(y.symmetric,a[0]),this.sessionKey=a.subarray(1,a.length),this.sessionKey.length!==It(this.sessionKeyAlgorithm).keySize)throw new Error("Unexpected session key size")}else this.sessionKey=o}async encrypt(e,t=pe){let i=this.sessionKeyEncryptionAlgorithm!==null?this.sessionKeyEncryptionAlgorithm:this.sessionKeyAlgorithm;this.sessionKeyEncryptionAlgorithm=i,this.s2k=jU(t),this.s2k.generateSalt();let{blockSize:n,keySize:s}=It(i),o=await this.s2k.produceKey(e,s,t);if(this.sessionKey===null&&(this.sessionKey=i1(this.sessionKeyAlgorithm)),this.version>=5){let a=uo(this.aeadAlgorithm);this.iv=Nr(a.ivLength);let c=new Uint8Array([192|r.tag,this.version,this.sessionKeyEncryptionAlgorithm,this.aeadAlgorithm]),l=this.version===6?await Fa(y.hash.sha256,o,new Uint8Array,c,s):o,d=await a(i,l);this.encrypted=await d.encrypt(this.sessionKey,this.iv,c)}else{let a=F.concatUint8Array([new Uint8Array([this.sessionKeyAlgorithm]),this.sessionKey]);this.encrypted=await aA(i,o,a,new Uint8Array(n))}}},gs=class r{static get tag(){return y.packet.publicKey}constructor(e=new Date,t=pe){this.version=t.v6Keys?6:4,this.created=F.normalizeDate(e),this.algorithm=null,this.publicParams=null,this.expirationTimeV3=0,this.fingerprint=null,this.keyID=null}static fromSecretKeyPacket(e){let t=new r,{version:i,created:n,algorithm:s,publicParams:o,keyID:a,fingerprint:c}=e;return t.version=i,t.created=n,t.algorithm=s,t.publicParams=o,t.keyID=a,t.fingerprint=c,t}async read(e,t=pe){let i=0;if(this.version=e[i++],this.version===5&&!t.enableParsingV5Entities)throw new ft("Support for parsing v5 entities is disabled; turn on `config.enableParsingV5Entities` if needed");if(this.version===4||this.version===5||this.version===6){this.created=F.readDate(e.subarray(i,i+4)),i+=4,this.algorithm=e[i++],this.version>=5&&(i+=4);let{read:n,publicParams:s}=SZ(this.algorithm,e.subarray(i));if(this.version===6&&s.oid&&(s.oid.getName()===y.curve.curve25519Legacy||s.oid.getName()===y.curve.ed25519Legacy))throw new Error("Legacy curve25519 cannot be used with v6 keys");return this.publicParams=s,i+=n,await this.computeFingerprintAndKeyID(),i}throw new ft(`Version ${this.version} of the key packet is unsupported.`)}write(){let e=[];e.push(new Uint8Array([this.version])),e.push(F.writeDate(this.created)),e.push(new Uint8Array([this.algorithm]));let t=Rd(this.algorithm,this.publicParams);return this.version>=5&&e.push(F.writeNumber(t.length,4)),e.push(t),F.concatUint8Array(e)}writeForHash(e){let t=this.writePublicKey(),i=149+e,n=e>=5?4:2;return F.concatUint8Array([new Uint8Array([i]),F.writeNumber(t.length,n),t])}isDecrypted(){return null}getCreationTime(){return this.created}getKeyID(){return this.keyID}async computeFingerprintAndKeyID(){if(await this.computeFingerprint(),this.keyID=new fo,this.version>=5)this.keyID.read(this.fingerprint.subarray(0,8));else if(this.version===4)this.keyID.read(this.fingerprint.subarray(12,20));else throw new Error("Unsupported key version")}async computeFingerprint(){let e=this.writeForHash(this.version);if(this.version>=5)this.fingerprint=await hs(y.hash.sha256,e);else if(this.version===4)this.fingerprint=await hs(y.hash.sha1,e);else throw new Error("Unsupported key version")}getFingerprintBytes(){return this.fingerprint}getFingerprint(){return F.uint8ArrayToHex(this.getFingerprintBytes())}hasSameFingerprintAs(e){return this.version===e.version&&F.equalsUint8Array(this.writePublicKey(),e.writePublicKey())}getAlgorithmInfo(){let e={};e.algorithm=y.read(y.publicKey,this.algorithm);let t=this.publicParams.n||this.publicParams.p;return t?e.bits=F.uint8ArrayBitLength(t):this.publicParams.oid&&(e.curve=this.publicParams.oid.getName()),e}};gs.prototype.readPublicKey=gs.prototype.read;gs.prototype.writePublicKey=gs.prototype.write;_ee=F.constructAllowedPackets([Ta,Vc,cn,qr]),A1=class{static get tag(){return y.packet.symmetricallyEncryptedData}constructor(){this.encrypted=null,this.packets=null}read(e){this.encrypted=e}write(){return this.encrypted}async decrypt(e,t,i=pe){if(!i.allowUnauthenticatedMessages)throw new Error("Message is not authenticated.");let{blockSize:n}=It(e),s=await Xt(Jc(this.encrypted)),o=await XI(e,t,s.subarray(n+2),s.subarray(2,n+2));this.packets=await Ct.fromBinary(o,_ee,i)}async encrypt(e,t,i=pe){let n=this.packets.write(),{blockSize:s}=It(e),o=await zU(e),a=await aA(e,t,o,new Uint8Array(s)),c=await aA(e,t,n,a.subarray(2));this.encrypted=F.concat([a,c])}},CI=class r extends gs{static get tag(){return y.packet.publicSubkey}constructor(e,t){super(e,t)}static fromSecretSubkeyPacket(e){let t=new r,{version:i,created:n,algorithm:s,publicParams:o,keyID:a,fingerprint:c}=e;return t.version=i,t.created=n,t.algorithm=s,t.publicParams=o,t.keyID=a,t.fingerprint=c,t}},h1=class r{static get tag(){return y.packet.userAttribute}constructor(){this.attributes=[]}read(e){let t=0;for(;t<e.length;){let i=hU(e.subarray(t,e.length));t+=i.offset,this.attributes.push(F.uint8ArrayToString(e.subarray(t,t+i.len))),t+=i.len}}write(){let e=[];for(let t=0;t<this.attributes.length;t++)e.push(HI(this.attributes[t].length)),e.push(F.stringToUint8Array(this.attributes[t]));return F.concatUint8Array(e)}equals(e){return!e||!(e instanceof r)?!1:this.attributes.every(function(t,i){return t===e.attributes[i]})}},uA=class extends gs{static get tag(){return y.packet.secretKey}constructor(e=new Date,t=pe){super(e,t),this.keyMaterial=null,this.isEncrypted=null,this.s2kUsage=0,this.s2k=null,this.symmetric=null,this.aead=null,this.isLegacyAEAD=null,this.privateParams=null,this.usedModernAEAD=null}async read(e,t=pe){let i=await this.readPublicKey(e,t),n=i;this.s2kUsage=e[i++],this.version===5&&i++,this.version===6&&this.s2kUsage&&i++;try{if(this.s2kUsage===255||this.s2kUsage===254||this.s2kUsage===253){this.symmetric=e[i++],this.s2kUsage===253&&(this.aead=e[i++]),this.version===6&&i++;let s=e[i++];if(this.s2k=mI(s),i+=this.s2k.read(e.subarray(i,e.length)),this.s2k.type==="gnu-dummy")return}else this.s2kUsage&&(this.symmetric=this.s2kUsage);this.s2kUsage&&(this.isLegacyAEAD=this.s2kUsage===253&&(this.version===5||this.version===4&&t.parseAEADEncryptedV4KeysAsLegacy),this.s2kUsage!==253||this.isLegacyAEAD?(this.iv=e.subarray(i,i+It(this.symmetric).blockSize),this.usedModernAEAD=!1):(this.iv=e.subarray(i,i+uo(this.aead).ivLength),this.usedModernAEAD=!0),i+=this.iv.length)}catch(s){if(!this.s2kUsage)throw s;this.unparseableKeyMaterial=e.subarray(n),this.isEncrypted=!0}if(this.version===5&&(i+=4),this.keyMaterial=e.subarray(i),this.isEncrypted=!!this.s2kUsage,!this.isEncrypted){let s;if(this.version===6)s=this.keyMaterial;else if(s=this.keyMaterial.subarray(0,-2),!F.equalsUint8Array(F.writeChecksum(s),this.keyMaterial.subarray(-2)))throw new Error("Key checksum mismatch");try{let{read:o,privateParams:a}=Z6(this.algorithm,s,this.publicParams);if(o<s.length)throw new Error("Error reading MPIs");this.privateParams=a}catch(o){throw o instanceof ft?o:new Error("Error reading MPIs")}}}write(){let e=this.writePublicKey();if(this.unparseableKeyMaterial)return F.concatUint8Array([e,this.unparseableKeyMaterial]);let t=[e];t.push(new Uint8Array([this.s2kUsage]));let i=[];if(this.s2kUsage===255||this.s2kUsage===254||this.s2kUsage===253){i.push(this.symmetric),this.s2kUsage===253&&i.push(this.aead);let n=this.s2k.write();this.version===6&&i.push(n.length),i.push(...n)}return this.s2kUsage&&this.s2k.type!=="gnu-dummy"&&i.push(...this.iv),(this.version===5||this.version===6&&this.s2kUsage)&&t.push(new Uint8Array([i.length])),t.push(new Uint8Array(i)),this.isDummy()||(this.s2kUsage||(this.keyMaterial=Rd(this.algorithm,this.privateParams)),this.version===5&&t.push(F.writeNumber(this.keyMaterial.length,4)),t.push(this.keyMaterial),!this.s2kUsage&&this.version!==6&&t.push(F.writeChecksum(this.keyMaterial))),F.concatUint8Array(t)}isDecrypted(){return this.isEncrypted===!1}isMissingSecretKeyMaterial(){return this.unparseableKeyMaterial!==void 0||this.isDummy()}isDummy(){return!!(this.s2k&&this.s2k.type==="gnu-dummy")}makeDummy(e=pe){this.isDummy()||(this.isDecrypted()&&this.clearPrivateParams(),delete this.unparseableKeyMaterial,this.isEncrypted=null,this.keyMaterial=null,this.s2k=mI(y.s2k.gnu,e),this.s2k.algorithm=0,this.s2k.c=0,this.s2k.type="gnu-dummy",this.s2kUsage=254,this.symmetric=y.symmetric.aes256,this.isLegacyAEAD=null,this.usedModernAEAD=null)}async encrypt(e,t=pe){if(this.isDummy())return;if(!this.isDecrypted())throw new Error("Key packet is already encrypted");if(!e)throw new Error("A non-empty passphrase is required for key encryption.");this.s2k=jU(t),this.s2k.generateSalt();let i=Rd(this.algorithm,this.privateParams);this.symmetric=y.symmetric.aes256;let{blockSize:n}=It(this.symmetric);if(t.aeadProtect){this.s2kUsage=253,this.aead=t.preferredAEADAlgorithm;let s=uo(this.aead);this.isLegacyAEAD=this.version===5,this.usedModernAEAD=!this.isLegacyAEAD;let o=sI(this.constructor.tag),a=await DB(this.version,this.s2k,e,this.symmetric,this.aead,o,this.isLegacyAEAD,t),c=await s(this.symmetric,a);this.iv=this.isLegacyAEAD?Nr(n):Nr(s.ivLength);let l=this.isLegacyAEAD?new Uint8Array:F.concatUint8Array([o,this.writePublicKey()]);this.keyMaterial=await c.encrypt(i,this.iv.subarray(0,s.ivLength),l)}else{this.s2kUsage=254,this.usedModernAEAD=!1;let s=await DB(this.version,this.s2k,e,this.symmetric,void 0,void 0,void 0,t);this.iv=Nr(n),this.keyMaterial=await aA(this.symmetric,s,F.concatUint8Array([i,await hs(y.hash.sha1,i)]),this.iv)}}async decrypt(e,t=pe){if(this.isDummy())return!1;if(this.unparseableKeyMaterial)throw new Error("Key packet cannot be decrypted: unsupported S2K or cipher algo");if(this.isDecrypted())throw new Error("Key packet is already decrypted.");let i,n=sI(this.constructor.tag);if(this.s2kUsage===254||this.s2kUsage===253)i=await DB(this.version,this.s2k,e,this.symmetric,this.aead,n,this.isLegacyAEAD,t);else throw this.s2kUsage===255?new Error("Encrypted private key is authenticated using an insecure two-byte hash"):new Error("Private key is encrypted using an insecure S2K function: unsalted MD5");let s;if(this.s2kUsage===253){let o=uo(this.aead,!0),a=await o(this.symmetric,i);try{let c=this.isLegacyAEAD?new Uint8Array:F.concatUint8Array([n,this.writePublicKey()]);s=await a.decrypt(this.keyMaterial,this.iv.subarray(0,o.ivLength),c)}catch(c){throw c.message==="Authentication tag mismatch"?new Error("Incorrect key passphrase: "+c.message):c}}else{let o=await XI(this.symmetric,i,this.keyMaterial,this.iv);s=o.subarray(0,-20);let a=await hs(y.hash.sha1,s);if(!F.equalsUint8Array(a,o.subarray(-20)))throw new Error("Incorrect key passphrase")}try{let{privateParams:o}=Z6(this.algorithm,s,this.publicParams);this.privateParams=o}catch{throw new Error("Error reading MPIs")}this.isEncrypted=!1,this.keyMaterial=null,this.s2kUsage=0,this.aead=null,this.symmetric=null,this.isLegacyAEAD=null}async validate(){if(this.isDummy())return;if(!this.isDecrypted())throw new Error("Key is not decrypted");if(this.usedModernAEAD)return;let e;try{e=await _Z(this.algorithm,this.publicParams,this.privateParams)}catch{e=!1}if(!e)throw new Error("Key is invalid")}async generate(e,t){if(this.version===6&&(this.algorithm===y.publicKey.ecdh&&t===y.curve.curve25519Legacy||this.algorithm===y.publicKey.eddsaLegacy))throw new Error(`Cannot generate v6 keys of type 'ecc' with curve ${t}. Generate a key of type 'curve25519' instead`);let{privateParams:i,publicParams:n}=await QZ(this.algorithm,e,t);this.privateParams=i,this.publicParams=n,this.isEncrypted=!1}clearPrivateParams(){this.isMissingSecretKeyMaterial()||(Object.keys(this.privateParams).forEach(e=>{this.privateParams[e].fill(0),delete this.privateParams[e]}),this.privateParams=null,this.isEncrypted=!0)}};EI=class r{static get tag(){return y.packet.userID}constructor(){this.userID="",this.name="",this.email="",this.comment=""}static fromObject(e){if(F.isString(e)||e.name&&!F.isString(e.name)||e.email&&!F.isEmailAddress(e.email)||e.comment&&!F.isString(e.comment))throw new Error("Invalid user ID format");let t=new r;Object.assign(t,e);let i=[];return t.name&&i.push(t.name),t.comment&&i.push(`(${t.comment})`),t.email&&i.push(`<${t.email}>`),t.userID=i.join(" "),t}read(e,t=pe){let i=F.decodeUTF8(e);if(i.length>t.maxUserIDLength)throw new Error("User ID string is too long");let n=a=>/^[^\s@]+@[^\s@]+$/.test(a),s=i.indexOf("<"),o=i.lastIndexOf(">");if(s!==-1&&o!==-1&&o>s){let a=i.substring(s+1,o);if(n(a)){this.email=a;let c=i.substring(0,s).trim(),l=c.indexOf("("),d=c.lastIndexOf(")");l!==-1&&d!==-1&&d>l?(this.comment=c.substring(l+1,d).trim(),this.name=c.substring(0,l).trim()):(this.name=c,this.comment="")}}else n(i.trim())&&(this.email=i.trim(),this.name="",this.comment="");this.userID=i}write(){return F.encodeUTF8(this.userID)}equals(e){return e&&e.userID===this.userID}},BI=class extends uA{static get tag(){return y.packet.secretSubkey}constructor(e=new Date,t=pe){super(e,t)}},bI=class{constructor(e){this.packets=e||new Ct}write(){return this.packets.write()}armor(e=pe){let t=this.packets.some(i=>i.constructor.tag===qr.tag&&i.version!==6);return BA(y.armor.signature,this.write(),void 0,void 0,void 0,t,e)}getSigningKeyIDs(){return this.packets.map(e=>e.issuerKeyID)}};m1=class r{constructor(e,t){this.userID=e.constructor.tag===y.packet.userID?e:null,this.userAttribute=e.constructor.tag===y.packet.userAttribute?e:null,this.selfCertifications=[],this.otherCertifications=[],this.revocationSignatures=[],this.mainKey=t}toPacketList(){let e=new Ct;return e.push(this.userID||this.userAttribute),e.push(...this.revocationSignatures),e.push(...this.selfCertifications),e.push(...this.otherCertifications),e}clone(){let e=new r(this.userID||this.userAttribute,this.mainKey);return e.selfCertifications=[...this.selfCertifications],e.otherCertifications=[...this.otherCertifications],e.revocationSignatures=[...this.revocationSignatures],e}async certify(e,t,i){let n=this.mainKey.keyPacket,s={userID:this.userID,userAttribute:this.userAttribute,key:n},o=new r(s.userID||s.userAttribute,this.mainKey);return o.otherCertifications=await Promise.all(e.map(async function(a){if(!a.isPrivate())throw new Error("Need private key for signing");if(a.hasSameFingerprintAs(n))throw new Error("The user's own key can only be used for self-certifications");let c=await a.getSigningKey(void 0,t,void 0,i);return As(s,[a],c.keyPacket,{signatureType:y.signature.certGeneric,keyFlags:[y.keyFlags.certifyKeys|y.keyFlags.signData]},t,void 0,void 0,void 0,i)})),await o.update(this,t,i),o}async isRevoked(e,t,i=new Date,n=pe){let s=this.mainKey.keyPacket;return Md(s,y.signature.certRevocation,{key:s,userID:this.userID,userAttribute:this.userAttribute},this.revocationSignatures,e,t,i,n)}async verifyCertificate(e,t,i=new Date,n){let s=this,o=this.mainKey.keyPacket,a={userID:this.userID,userAttribute:this.userAttribute,key:o},{issuerKeyID:c}=e,l=t.filter(d=>d.getKeys(c).length>0);return l.length===0?null:(await Promise.all(l.map(async d=>{let u=await d.getSigningKey(c,e.created,void 0,n);if(e.revoked||await s.isRevoked(e,u.keyPacket,i,n))throw new Error("User certificate is revoked");try{await e.verify(u.keyPacket,y.signature.certGeneric,a,i,void 0,n)}catch(f){throw F.wrapError("User certificate is invalid",f)}})),!0)}async verifyAllCertifications(e,t=new Date,i){let n=this,s=this.selfCertifications.concat(this.otherCertifications);return Promise.all(s.map(async o=>({keyID:o.issuerKeyID,valid:await n.verifyCertificate(o,e,t,i).catch(()=>!1)})))}async verify(e=new Date,t){if(!this.selfCertifications.length)throw new Error("No self-certifications found");let i=this,n=this.mainKey.keyPacket,s={userID:this.userID,userAttribute:this.userAttribute,key:n},o;for(let a=this.selfCertifications.length-1;a>=0;a--)try{let c=this.selfCertifications[a];if(c.revoked||await i.isRevoked(c,void 0,e,t))throw new Error("Self-certification is revoked");try{await c.verify(n,y.signature.certGeneric,s,e,void 0,t)}catch(l){throw F.wrapError("Self-certification is invalid",l)}return!0}catch(c){o=c}throw o}async update(e,t,i){let n=this.mainKey.keyPacket,s={userID:this.userID,userAttribute:this.userAttribute,key:n};await Gc(e,this,"selfCertifications",t,async function(o){try{return await o.verify(n,y.signature.certGeneric,s,t,!1,i),!0}catch{return!1}}),await Gc(e,this,"otherCertifications",t),await Gc(e,this,"revocationSignatures",t,function(o){return Md(n,y.signature.certRevocation,s,[o],void 0,void 0,t,i)})}async revoke(e,{flag:t=y.reasonForRevocation.noReason,string:i=""}={},n=new Date,s=pe){let o={userID:this.userID,userAttribute:this.userAttribute,key:e},a=new r(o.userID||o.userAttribute,this.mainKey);return a.revocationSignatures.push(await As(o,[],e,{signatureType:y.signature.certRevocation,reasonForRevocationFlag:y.write(y.reasonForRevocation,t),reasonForRevocationString:i},n,void 0,void 0,!1,s)),await a.update(this),a}},fA=class r{constructor(e,t){this.keyPacket=e,this.bindingSignatures=[],this.revocationSignatures=[],this.mainKey=t}toPacketList(){let e=new Ct;return e.push(this.keyPacket),e.push(...this.revocationSignatures),e.push(...this.bindingSignatures),e}clone(){let e=new r(this.keyPacket,this.mainKey);return e.bindingSignatures=[...this.bindingSignatures],e.revocationSignatures=[...this.revocationSignatures],e}async isRevoked(e,t,i=new Date,n=pe){let s=this.mainKey.keyPacket;return Md(s,y.signature.subkeyRevocation,{key:s,bind:this.keyPacket},this.revocationSignatures,e,t,i,n)}async verify(e=new Date,t=pe){let i=this.mainKey.keyPacket,n={key:i,bind:this.keyPacket},s=await Sn(this.bindingSignatures,i,y.signature.subkeyBinding,n,e,t);if(s.revoked||await this.isRevoked(s,null,e,t))throw new Error("Subkey is revoked");if(g1(this.keyPacket,s,e))throw new Error("Subkey is expired");return s}async getExpirationTime(e=new Date,t=pe){let i=this.mainKey.keyPacket,n={key:i,bind:this.keyPacket},s;try{s=await Sn(this.bindingSignatures,i,y.signature.subkeyBinding,n,e,t)}catch{return null}let o=vI(this.keyPacket,s),a=s.getExpirationTime();return o<a?o:a}async update(e,t=new Date,i=pe){let n=this.mainKey.keyPacket;if(!this.hasSameFingerprintAs(e))throw new Error("Subkey update method: fingerprints of subkeys not equal");this.keyPacket.constructor.tag===y.packet.publicSubkey&&e.keyPacket.constructor.tag===y.packet.secretSubkey&&(this.keyPacket=e.keyPacket);let s=this,o={key:n,bind:s.keyPacket};await Gc(e,this,"bindingSignatures",t,async function(a){for(let c=0;c<s.bindingSignatures.length;c++)if(s.bindingSignatures[c].issuerKeyID.equals(a.issuerKeyID))return a.created>s.bindingSignatures[c].created&&(s.bindingSignatures[c]=a),!1;try{return await a.verify(n,y.signature.subkeyBinding,o,t,void 0,i),!0}catch{return!1}}),await Gc(e,this,"revocationSignatures",t,function(a){return Md(n,y.signature.subkeyRevocation,o,[a],void 0,void 0,t,i)})}async revoke(e,{flag:t=y.reasonForRevocation.noReason,string:i=""}={},n=new Date,s=pe){let o={key:e,bind:this.keyPacket},a=new r(this.keyPacket,this.mainKey);return a.revocationSignatures.push(await As(o,[],e,{signatureType:y.signature.subkeyRevocation,reasonForRevocationFlag:y.write(y.reasonForRevocation,t),reasonForRevocationString:i},n,void 0,void 0,!1,s)),await a.update(this),a}hasSameFingerprintAs(e){return this.keyPacket.hasSameFingerprintAs(e.keyPacket||e)}};["getKeyID","getFingerprint","getAlgorithmInfo","getCreationTime","isDecrypted"].forEach(r=>{fA.prototype[r]=function(){return this.keyPacket[r]()}});Ree=F.constructAllowedPackets([qr]),mT=new Set([y.packet.publicKey,y.packet.privateKey]),yT=new Set([y.packet.publicKey,y.packet.privateKey,y.packet.publicSubkey,y.packet.privateSubkey]),kI=class{packetListToStructure(e,t=new Set){let i,n,s,o;for(let a of e){if(a instanceof nA){yT.has(a.tag)&&!o&&(mT.has(a.tag)?o=mT:o=yT);continue}let c=a.constructor.tag;if(o){if(!o.has(c))continue;o=null}if(t.has(c))throw new Error(`Unexpected packet type: ${c}`);switch(c){case y.packet.publicKey:case y.packet.secretKey:if(this.keyPacket)throw new Error("Key block contains multiple keys");if(this.keyPacket=a,n=this.getKeyID(),!n)throw new Error("Missing Key ID");break;case y.packet.userID:case y.packet.userAttribute:i=new m1(a,this),this.users.push(i);break;case y.packet.publicSubkey:case y.packet.secretSubkey:i=null,s=new fA(a,this),this.subkeys.push(s);break;case y.packet.signature:switch(a.signatureType){case y.signature.certGeneric:case y.signature.certPersona:case y.signature.certCasual:case y.signature.certPositive:if(!i){F.printDebug("Dropping certification signatures without preceding user packet");continue}a.issuerKeyID.equals(n)?i.selfCertifications.push(a):i.otherCertifications.push(a);break;case y.signature.certRevocation:i?i.revocationSignatures.push(a):this.directSignatures.push(a);break;case y.signature.key:this.directSignatures.push(a);break;case y.signature.subkeyBinding:if(!s){F.printDebug("Dropping subkey binding signature without preceding subkey packet");continue}s.bindingSignatures.push(a);break;case y.signature.keyRevocation:this.revocationSignatures.push(a);break;case y.signature.subkeyRevocation:if(!s){F.printDebug("Dropping subkey revocation signature without preceding subkey packet");continue}s.revocationSignatures.push(a);break}break}}}toPacketList(){let e=new Ct;return e.push(this.keyPacket),e.push(...this.revocationSignatures),e.push(...this.directSignatures),this.users.map(t=>e.push(...t.toPacketList())),this.subkeys.map(t=>e.push(...t.toPacketList())),e}clone(e=!1){let t=new this.constructor(this.toPacketList());return e&&t.getKeys().forEach(i=>{if(i.keyPacket=Object.create(Object.getPrototypeOf(i.keyPacket),Object.getOwnPropertyDescriptors(i.keyPacket)),!i.keyPacket.isDecrypted())return;let n={};Object.keys(i.keyPacket.privateParams).forEach(s=>{n[s]=new Uint8Array(i.keyPacket.privateParams[s])}),i.keyPacket.privateParams=n}),t}getSubkeys(e=null){return this.subkeys.filter(i=>!e||i.getKeyID().equals(e,!0))}getKeys(e=null){let t=[];return(!e||this.getKeyID().equals(e,!0))&&t.push(this),t.concat(this.getSubkeys(e))}getKeyIDs(){return this.getKeys().map(e=>e.getKeyID())}getUserIDs(){return this.users.map(e=>e.userID?e.userID.userID:null).filter(e=>e!==null)}write(){return this.toPacketList().write()}async getSigningKey(e=null,t=new Date,i={},n=pe){await this.verifyPrimaryKey(t,i,n);let s=this.keyPacket;try{ka(s,n)}catch(c){throw F.wrapError("Could not verify primary key",c)}let o=this.subkeys.slice().sort((c,l)=>l.keyPacket.created-c.keyPacket.created||l.keyPacket.algorithm-c.keyPacket.algorithm),a;for(let c of o)if(!e||c.getKeyID().equals(e))try{await c.verify(t,n);let l={key:s,bind:c.keyPacket},d=await Sn(c.bindingSignatures,s,y.signature.subkeyBinding,l,t,n);if(!hT(c.keyPacket,d,n))continue;if(!d.embeddedSignature)throw new Error("Missing embedded signature");return await Sn([d.embeddedSignature],c.keyPacket,y.signature.keyBinding,l,t,n),ka(c.keyPacket,n),c}catch(l){a=l}try{let c=await this.getPrimarySelfSignature(t,i,n);if((!e||s.getKeyID().equals(e))&&hT(s,c,n))return ka(s,n),this}catch(c){a=c}throw F.wrapError("Could not find valid signing key packet in key "+this.getKeyID().toHex(),a)}async getEncryptionKey(e,t=new Date,i={},n=pe){await this.verifyPrimaryKey(t,i,n);let s=this.keyPacket;try{ka(s,n)}catch(c){throw F.wrapError("Could not verify primary key",c)}let o=this.subkeys.slice().sort((c,l)=>l.keyPacket.created-c.keyPacket.created||l.keyPacket.algorithm-c.keyPacket.algorithm),a;for(let c of o)if(!e||c.getKeyID().equals(e))try{await c.verify(t,n);let l={key:s,bind:c.keyPacket},d=await Sn(c.bindingSignatures,s,y.signature.subkeyBinding,l,t,n);if(gT(c.keyPacket,d,n))return ka(c.keyPacket,n),c}catch(l){a=l}try{let c=await this.getPrimarySelfSignature(t,i,n);if((!e||s.getKeyID().equals(e))&&gT(s,c,n))return ka(s,n),this}catch(c){a=c}throw F.wrapError("Could not find valid encryption key packet in key "+this.getKeyID().toHex(),a)}async isRevoked(e,t,i=new Date,n=pe){return Md(this.keyPacket,y.signature.keyRevocation,{key:this.keyPacket},this.revocationSignatures,e,t,i,n)}async verifyPrimaryKey(e=new Date,t={},i=pe){let n=this.keyPacket;if(await this.isRevoked(null,null,e,i))throw new Error("Primary key is revoked");let s=await this.getPrimarySelfSignature(e,t,i);if(g1(n,s,e))throw new Error("Primary key is expired");if(n.version!==6){let o=await Sn(this.directSignatures,n,y.signature.key,{key:n},e,i).catch(()=>{});if(o&&g1(n,o,e))throw new Error("Primary key is expired")}}async getExpirationTime(e,t=pe){let i;try{let n=await this.getPrimarySelfSignature(null,e,t),s=vI(this.keyPacket,n),o=n.getExpirationTime(),a=this.keyPacket.version!==6&&await Sn(this.directSignatures,this.keyPacket,y.signature.key,{key:this.keyPacket},null,t).catch(()=>{});if(a){let c=vI(this.keyPacket,a);i=Math.min(s,o,c)}else i=s<o?s:o}catch{i=null}return F.normalizeDate(i)}async getPrimarySelfSignature(e=new Date,t={},i=pe){let n=this.keyPacket;if(n.version===6)return Sn(this.directSignatures,n,y.signature.key,{key:n},e,i);let{selfCertification:s}=await this.getPrimaryUser(e,t,i);return s}async getPrimaryUser(e=new Date,t={},i=pe){let n=this.keyPacket,s=[],o;for(let d=0;d<this.users.length;d++)try{let u=this.users[d];if(!u.userID)continue;if(t.name!==void 0&&u.userID.name!==t.name||t.email!==void 0&&u.userID.email!==t.email||t.comment!==void 0&&u.userID.comment!==t.comment)throw new Error("Could not find user that matches that user ID");let f={userID:u.userID,key:n},h=await Sn(u.selfCertifications,n,y.signature.certGeneric,f,e,i);s.push({index:d,user:u,selfCertification:h})}catch(u){o=u}if(!s.length)throw o||new Error("Could not find primary user");await Promise.all(s.map(async d=>{d.selfCertification.revoked||await d.user.isRevoked(d.selfCertification,null,e,i)}));let a=s.sort(function(d,u){let f=d.selfCertification,h=u.selfCertification;return h.revoked-f.revoked||f.isPrimaryUserID-h.isPrimaryUserID||f.created-h.created}).pop(),{user:c,selfCertification:l}=a;if(l.revoked||await c.isRevoked(l,null,e,i))throw new Error("Primary user is revoked");return a}async update(e,t=new Date,i=pe){if(!this.hasSameFingerprintAs(e))throw new Error("Primary key fingerprints must be equal to update the key");if(!this.isPrivate()&&e.isPrivate()){if(!(this.subkeys.length===e.subkeys.length&&this.subkeys.every(o=>e.subkeys.some(a=>o.hasSameFingerprintAs(a)))))throw new Error("Cannot update public key with private key if subkeys mismatch");return e.update(this,i)}let n=this.clone();return await Gc(e,n,"revocationSignatures",t,s=>Md(n.keyPacket,y.signature.keyRevocation,n,[s],null,e.keyPacket,t,i)),await Gc(e,n,"directSignatures",t),await Promise.all(e.users.map(async s=>{let o=n.users.filter(a=>s.userID&&s.userID.equals(a.userID)||s.userAttribute&&s.userAttribute.equals(a.userAttribute));if(o.length>0)await Promise.all(o.map(a=>a.update(s,t,i)));else{let a=s.clone();a.mainKey=n,n.users.push(a)}})),await Promise.all(e.subkeys.map(async s=>{let o=n.subkeys.filter(a=>a.hasSameFingerprintAs(s));if(o.length>0)await Promise.all(o.map(a=>a.update(s,t,i)));else{let a=s.clone();a.mainKey=n,n.subkeys.push(a)}})),n}async getRevocationCertificate(e=new Date,t=pe){let i={key:this.keyPacket},n=await Sn(this.revocationSignatures,this.keyPacket,y.signature.keyRevocation,i,e,t),s=new Ct;s.push(n);let o=this.keyPacket.version!==6;return BA(y.armor.publicKey,s.write(),null,null,"This is a revocation certificate",o,t)}async applyRevocationCertificate(e,t=new Date,i=pe){let n=await LI(e),o=(await Ct.fromBinary(n.data,Ree,i)).findPacket(y.packet.signature);if(!o||o.signatureType!==y.signature.keyRevocation)throw new Error("Could not find revocation signature packet");if(!o.issuerKeyID.equals(this.getKeyID()))throw new Error("Revocation signature does not match key");try{await o.verify(this.keyPacket,y.signature.keyRevocation,{key:this.keyPacket},t,void 0,i)}catch(c){throw F.wrapError("Could not verify revocation signature",c)}let a=this.clone();return a.revocationSignatures.push(o),a}async signPrimaryUser(e,t,i,n=pe){let{index:s,user:o}=await this.getPrimaryUser(t,i,n),a=await o.certify(e,t,n),c=this.clone();return c.users[s]=a,c}async signAllUsers(e,t=new Date,i=pe){let n=this.clone();return n.users=await Promise.all(this.users.map(function(s){return s.certify(e,t,i)})),n}async verifyPrimaryUser(e,t=new Date,i,n=pe){let s=this.keyPacket,{user:o}=await this.getPrimaryUser(t,i,n);return e?await o.verifyAllCertifications(e,t,n):[{keyID:s.getKeyID(),valid:await o.verify(t,n).catch(()=>!1)}]}async verifyAllUsers(e,t=new Date,i=pe){let n=this.keyPacket,s=[];return await Promise.all(this.users.map(async o=>{let a=e?await o.verifyAllCertifications(e,t,i):[{keyID:n.getKeyID(),valid:await o.verify(t,i).catch(()=>!1)}];s.push(...a.map(c=>({userID:o.userID?o.userID.userID:null,userAttribute:o.userAttribute,keyID:c.keyID,valid:c.valid})))})),s}};["getKeyID","getFingerprint","getAlgorithmInfo","getCreationTime","hasSameFingerprintAs"].forEach(r=>{kI.prototype[r]=fA.prototype[r]});SI=class extends kI{constructor(e){if(super(),this.keyPacket=null,this.revocationSignatures=[],this.directSignatures=[],this.users=[],this.subkeys=[],e&&(this.packetListToStructure(e,new Set([y.packet.secretKey,y.packet.secretSubkey])),!this.keyPacket))throw new Error("Invalid key: missing public-key packet")}isPrivate(){return!1}toPublic(){return this}armor(e=pe){let t=this.keyPacket.version!==6;return BA(y.armor.publicKey,this.toPacketList().write(),void 0,void 0,void 0,t,e)}},DI=class r extends SI{constructor(e){if(super(),this.packetListToStructure(e,new Set([y.packet.publicKey,y.packet.publicSubkey])),!this.keyPacket)throw new Error("Invalid key: missing private-key packet")}isPrivate(){return!0}toPublic(){let e=new Ct,t=this.toPacketList();for(let i of t)switch(i.constructor.tag){case y.packet.secretKey:{let n=gs.fromSecretKeyPacket(i);e.push(n);break}case y.packet.secretSubkey:{let n=CI.fromSecretSubkeyPacket(i);e.push(n);break}default:e.push(i)}return new SI(e)}armor(e=pe){let t=this.keyPacket.version!==6;return BA(y.armor.privateKey,this.toPacketList().write(),void 0,void 0,void 0,t,e)}async getDecryptionKeys(e,t=new Date,i={},n=pe){let s=this.keyPacket,o=[],a=null;for(let l=0;l<this.subkeys.length;l++)if(!e||this.subkeys[l].getKeyID().equals(e,!0)){if(this.subkeys[l].keyPacket.isDummy()){a=a||new Error("Gnu-dummy key packets cannot be used for decryption");continue}try{let d={key:s,bind:this.subkeys[l].keyPacket},u=await Sn(this.subkeys[l].bindingSignatures,s,y.signature.subkeyBinding,d,t,n);pT(this.subkeys[l].keyPacket,u,n)&&o.push(this.subkeys[l])}catch(d){a=d}}let c=await this.getPrimarySelfSignature(t,i,n);if((!e||s.getKeyID().equals(e,!0))&&pT(s,c,n)&&(s.isDummy()?a=a||new Error("Gnu-dummy key packets cannot be used for decryption"):o.push(this)),o.length===0)throw a||new Error("No decryption key packets found");return o}isDecrypted(){return this.getKeys().some(({keyPacket:e})=>e.isDecrypted())}async validate(e=pe){if(!this.isPrivate())throw new Error("Cannot validate a public key");let t;if(!this.keyPacket.isDummy())t=this.keyPacket;else{let i=await this.getSigningKey(null,null,void 0,{...e,rejectPublicKeyAlgorithms:new Set,minRSABits:0});i&&!i.keyPacket.isDummy()&&(t=i.keyPacket)}if(t)return t.validate();{let i=this.getKeys();if(i.map(s=>s.keyPacket.isDummy()).every(Boolean))throw new Error("Cannot validate an all-gnu-dummy key");return Promise.all(i.map(s=>s.keyPacket.validate()))}}clearPrivateParams(){this.getKeys().forEach(({keyPacket:e})=>{e.isDecrypted()&&e.clearPrivateParams()})}async revoke({flag:e=y.reasonForRevocation.noReason,string:t=""}={},i=new Date,n=pe){if(!this.isPrivate())throw new Error("Need private key for revoking");let s={key:this.keyPacket},o=this.clone();return o.revocationSignatures.push(await As(s,[],this.keyPacket,{signatureType:y.signature.keyRevocation,reasonForRevocationFlag:y.write(y.reasonForRevocation,e),reasonForRevocationString:t},i,void 0,void 0,void 0,n)),o}async addSubkey(e={}){let t={...pe,...e.config};if(e.passphrase)throw new Error("Subkey could not be encrypted here, please encrypt whole key");if(e.rsaBits<t.minRSABits)throw new Error(`rsaBits should be at least ${t.minRSABits}, got: ${e.rsaBits}`);let i=this.keyPacket;if(i.isDummy())throw new Error("Cannot add subkey to gnu-dummy primary key");if(!i.isDecrypted())throw new Error("Key is not decrypted");let n=i.getAlgorithmInfo();n.type=Nee(n.algorithm),n.rsaBits=n.bits||4096,n.curve=n.curve||"curve25519Legacy",e=p1(e,n);let s=await eR(e,{...t,v6Keys:this.keyPacket.version===6});ka(s,t);let o=await tR(s,i,e,t),a=this.toPacketList();return a.push(s,o),new r(a)}};qee=F.constructAllowedPackets([gs,CI,uA,BI,EI,h1,qr]);Lee=F.constructAllowedPackets([Ta,Vc,f1,Od,A1,lA,dA,cn,qr]),Hee=F.constructAllowedPackets([dA]),Kee=F.constructAllowedPackets([qr]),AA=class r{constructor(e){this.packets=e||new Ct}getEncryptionKeyIDs(){let e=[];return this.packets.filterByTag(y.packet.publicKeyEncryptedSessionKey).forEach(function(i){e.push(i.publicKeyID)}),e}getSigningKeyIDs(){let e=this.unwrapCompressed(),t=e.packets.filterByTag(y.packet.onePassSignature);return t.length>0?t.map(n=>n.issuerKeyID):e.packets.filterByTag(y.packet.signature).map(n=>n.issuerKeyID)}async decrypt(e,t,i,n=new Date,s=pe){let o=this.packets.filterByTag(y.packet.symmetricallyEncryptedData,y.packet.symEncryptedIntegrityProtectedData,y.packet.aeadEncryptedData);if(o.length===0)throw new Error("No encrypted data found");let a=o[0],c=a.cipherAlgorithm,l=i||await this.decryptSessionKeys(e,t,c,n,s),d=null,u=Promise.all(l.map(async({algorithm:h,data:g})=>{if(!F.isUint8Array(g)||!a.cipherAlgorithm&&!F.isString(h))throw new Error("Invalid session key for decryption.");try{let p=a.cipherAlgorithm||y.write(y.symmetric,h);await a.decrypt(p,g,s)}catch(p){F.printDebugError(p),d=p}}));if(VB(a.encrypted),a.encrypted=null,await u,!a.packets||!a.packets.length)throw d||new Error("Decryption failed.");let f=new r(a.packets);return a.packets=new Ct,f}async decryptSessionKeys(e,t,i,n=new Date,s=pe){let o=[],a;if(t){let c=this.packets.filterByTag(y.packet.symEncryptedSessionKey);if(c.length===0)throw new Error("No symmetrically encrypted session key packet found.");await Promise.all(t.map(async function(l,d){let u;d?u=await Ct.fromBinary(c.write(),Hee,s):u=c,await Promise.all(u.map(async function(f){try{await f.decrypt(l,s),o.push(f)}catch(h){F.printDebugError(h),h instanceof Qd&&(a=h)}}))}))}else if(e){let c=this.packets.filterByTag(y.packet.publicKeyEncryptedSessionKey);if(c.length===0)throw new Error("No public key encrypted session key packet found.");await Promise.all(c.map(async function(l){await Promise.all(e.map(async function(d){let u;try{u=(await d.getDecryptionKeys(l.publicKeyID,null,void 0,s)).map(h=>h.keyPacket)}catch(h){a=h;return}let f=[y.symmetric.aes256,y.symmetric.aes128,y.symmetric.tripledes,y.symmetric.cast5];try{let h=await d.getPrimarySelfSignature(n,void 0,s);h.preferredSymmetricAlgorithms&&(f=f.concat(h.preferredSymmetricAlgorithms))}catch{}await Promise.all(u.map(async function(h){if(!h.isDecrypted())throw new Error("Decryption key is not decrypted.");if(s.constantTimePKCS1Decryption&&(l.publicKeyAlgorithm===y.publicKey.rsaEncrypt||l.publicKeyAlgorithm===y.publicKey.rsaEncryptSign||l.publicKeyAlgorithm===y.publicKey.rsaSign||l.publicKeyAlgorithm===y.publicKey.elgamal)){let p=l.write();await Promise.all((i?[i]:Array.from(s.constantTimePKCS1DecryptionSupportedSymmetricAlgorithms)).map(async I=>{let m=new lA;m.read(p);let w={sessionKeyAlgorithm:I,sessionKey:i1(I)};try{await m.decrypt(h,w),o.push(m)}catch(B){F.printDebugError(B),a=B}}))}else try{await l.decrypt(h);let p=i||l.sessionKeyAlgorithm;if(p&&!f.includes(y.write(y.symmetric,p)))throw new Error("A non-preferred symmetric algorithm was used.");o.push(l)}catch(p){F.printDebugError(p),a=p}}))})),VB(l.encrypted),l.encrypted=null}))}else throw new Error("No key or password specified.");if(o.length>0){if(o.length>1){let c=new Set;o=o.filter(l=>{let d=l.sessionKeyAlgorithm+F.uint8ArrayToString(l.sessionKey);return c.has(d)?!1:(c.add(d),!0)})}return o.map(c=>({data:c.sessionKey,algorithm:c.sessionKeyAlgorithm&&y.read(y.symmetric,c.sessionKeyAlgorithm)}))}throw a||new Error("Session key decryption failed.")}getLiteralData(){let t=this.unwrapCompressed().packets.findPacket(y.packet.literalData);return t&&t.getBytes()||null}getFilename(){let t=this.unwrapCompressed().packets.findPacket(y.packet.literalData);return t&&t.getFilename()||null}getText(){let t=this.unwrapCompressed().packets.findPacket(y.packet.literalData);return t?t.getText():null}static async generateSessionKey(e=[],t=new Date,i=[],n=pe){let{symmetricAlgo:s,aeadAlgo:o}=await Uee(e,t,i,n),a=y.read(y.symmetric,s),c=o?y.read(y.aead,o):void 0;return await Promise.all(e.map(d=>d.getEncryptionKey().catch(()=>null).then(u=>{if(u&&(u.keyPacket.algorithm===y.publicKey.x25519||u.keyPacket.algorithm===y.publicKey.x448)&&!c&&!F.isAES(s))throw new Error("Could not generate a session key compatible with the given `encryptionKeys`: X22519 and X448 keys can only be used to encrypt AES session keys; change `config.preferredSymmetricAlgorithm` accordingly.")}))),{data:i1(s),algorithm:a,aeadAlgorithm:c}}async encrypt(e,t,i,n=!1,s=[],o=new Date,a=[],c=pe){if(i){if(!F.isUint8Array(i.data)||!F.isString(i.algorithm))throw new Error("Invalid session key for encryption.")}else if(e&&e.length)i=await r.generateSessionKey(e,o,a,c);else if(t&&t.length)i=await r.generateSessionKey(void 0,void 0,void 0,c);else throw new Error("No keys, passwords, or session key provided.");let{data:l,algorithm:d,aeadAlgorithm:u}=i,f=await r.encryptSessionKey(l,d,u,e,t,n,s,o,a,c),h=Od.fromObject({version:u?2:1,aeadAlgorithm:u?y.write(y.aead,u):null});h.packets=this.packets;let g=y.write(y.symmetric,d);return await h.encrypt(g,l,c),f.packets.push(h),h.packets=new Ct,f}static async encryptSessionKey(e,t,i,n,s,o=!1,a=[],c=new Date,l=[],d=pe){let u=new Ct,f=y.write(y.symmetric,t),h=i&&y.write(y.aead,i);if(n){let g=await Promise.all(n.map(async function(p,I){let m=await p.getEncryptionKey(a[I],c,l,d),w=lA.fromObject({version:h?6:3,encryptionKeyPacket:m.keyPacket,anonymousRecipient:o,sessionKey:e,sessionKeyAlgorithm:f});return await w.encrypt(m.keyPacket),delete w.sessionKey,w}));u.push(...g)}if(s){let g=async function(w,B){try{return await w.decrypt(B,d),1}catch{return 0}},p=(w,B)=>w+B,I=async function(w,B,C,x){let Q=new dA(d);return Q.sessionKey=w,Q.sessionKeyAlgorithm=B,C&&(Q.aeadAlgorithm=C),await Q.encrypt(x,d),d.passwordCollisionCheck&&(await Promise.all(s.map(U=>g(Q,U)))).reduce(p)!==1?I(w,B,x):(delete Q.sessionKey,Q)},m=await Promise.all(s.map(w=>I(e,f,h,w)));u.push(...m)}return new r(u)}async sign(e=[],t=[],i=null,n=[],s=new Date,o=[],a=[],c=[],l=pe){let d=new Ct,u=this.packets.findPacket(y.packet.literalData);if(!u)throw new Error("No literal data packet to sign.");let f=await IT(u,e,t,i,n,s,o,a,c,!1,l),h=f.map((g,p)=>cn.fromSignaturePacket(g,p===0)).reverse();return d.push(...h),d.push(u),d.push(...f),new r(d)}compress(e,t=pe){if(e===y.compression.uncompressed)return this;let i=new Vc(t);i.algorithm=e,i.packets=this.packets;let n=new Ct;return n.push(i),new r(n)}async signDetached(e=[],t=[],i=null,n=[],s=[],o=new Date,a=[],c=[],l=pe){let d=this.packets.findPacket(y.packet.literalData);if(!d)throw new Error("No literal data packet to sign.");return new bI(await IT(d,e,t,i,n,s,o,a,c,!0,l))}async verify(e,t=new Date,i=pe){let n=this.unwrapCompressed(),s=n.packets.filterByTag(y.packet.literalData);if(s.length!==1)throw new Error("Can only verify message with one literal data packet.");let o=n.packets;St(o.stream)&&(o=o.concat(await Xt(o.stream,l=>l||[])));let a=o.filterByTag(y.packet.onePassSignature).reverse(),c=o.filterByTag(y.packet.signature);return a.length&&!c.length&&F.isStream(o.stream)&&!St(o.stream)?(await Promise.all(a.map(async l=>{l.correspondingSig=new Promise((d,u)=>{l.correspondingSigResolve=d,l.correspondingSigReject=u}),l.signatureData=Zc(async()=>(await l.correspondingSig).signatureData),l.hashed=Xt(await l.hash(l.signatureType,s[0],void 0,!1)),l.hashed.catch(()=>{})})),o.stream=Ua(o.stream,async(l,d)=>{let u=Fn(l),f=Gi(d);try{for(let h=0;h<a.length;h++){let{value:g}=await u.read();a[h].correspondingSigResolve(g)}await u.readToEnd(),await f.ready,await f.close()}catch(h){a.forEach(g=>{g.correspondingSigReject(h)}),await f.abort(h)}}),QB(a,s,e,t,!1,i)):QB(c,s,e,t,!1,i)}async verifyDetached(e,t,i=new Date,n=pe){let o=this.unwrapCompressed().packets.filterByTag(y.packet.literalData);if(o.length!==1)throw new Error("Can only verify message with one literal data packet.");let a=e.packets.filterByTag(y.packet.signature);return QB(a,o,t,i,!0,n)}unwrapCompressed(){let e=this.packets.filterByTag(y.packet.compressedData);return e.length?new r(e[0].packets):this}async appendSignature(e,t=pe){await this.packets.read(F.isUint8Array(e)?e:(await LI(e)).data,Kee,t)}write(){return this.packets.write()}armor(e=pe){let t=this.packets[this.packets.length-1],i=t.constructor.tag===Od.tag?t.version!==2:this.packets.some(n=>n.constructor.tag===qr.tag&&n.version!==6);return BA(y.armor.message,this.write(),null,null,null,i,e)}};Jee=Object.keys(pe).length;pd=Hi&&typeof Hi=="object"&&"webcrypto"in Hi?Hi.webcrypto:Hi&&typeof Hi=="object"&&"randomBytes"in Hi?Hi:void 0;Vee=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;xT=Vee?r=>r:Wee,cR=typeof Uint8Array.from([]).toHex=="function"&&typeof Uint8Array.fromHex=="function",Xee=Array.from({length:256},(r,e)=>e.toString(16).padStart(2,"0"));Zs={_0:48,_9:57,A:65,F:70,a:97,f:102};gA=class{};tte=po;ib=BigInt(0),y1=BigInt(1);FB=r=>typeof r=="bigint"&&ib<=r;PA=r=>(y1<<BigInt(r))-y1;Ei=BigInt(0),fi=BigInt(1),Lc=BigInt(2),uR=BigInt(3),fR=BigInt(4),AR=BigInt(5),ste=BigInt(7),hR=BigInt(8),ote=BigInt(9),gR=BigInt(16);dte=["create","isValid","is0","neg","inv","sqrt","sqr","eql","add","sub","mul","pow","div","addN","subN","mulN","sqrN"];Xc=class extends gA{constructor(e,t,i,n){super(),this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.blockLen=e,this.outputLen=t,this.padOffset=i,this.isLE=n,this.buffer=new Uint8Array(e),this.view=_B(this.buffer)}update(e){Ld(this),e=FA(e),ps(e);let{view:t,buffer:i,blockLen:n}=this,s=e.length;for(let o=0;o<s;){let a=Math.min(n-this.pos,s-o);if(a===n){let c=_B(e);for(;n<=s-o;o+=n)this.process(c,o);continue}i.set(e.subarray(o,o+a),this.pos),this.pos+=a,o+=a,this.pos===n&&(this.process(t,0),this.pos=0)}return this.length+=e.length,this.roundClean(),this}digestInto(e){Ld(this),aR(e,this),this.finished=!0;let{buffer:t,view:i,blockLen:n,isLE:s}=this,{pos:o}=this;t[o++]=128,ln(this.buffer.subarray(o)),this.padOffset>n-o&&(this.process(i,0),o=0);for(let u=o;u<n;u++)t[u]=0;hte(i,n-8,BigInt(this.length*8),s),this.process(i,0);let a=_B(e),c=this.outputLen;if(c%4)throw new Error("_sha2: outputLen should be aligned to 32bit");let l=c/4,d=this.get();if(l>d.length)throw new Error("_sha2: outputLen bigger than state");for(let u=0;u<l;u++)a.setUint32(4*u,d[u],s)}digest(){let{buffer:e,outputLen:t}=this;this.digestInto(e);let i=e.slice(0,t);return this.destroy(),i}_cloneInto(e){e||(e=new this.constructor),e.set(...this.get());let{blockLen:t,buffer:i,length:n,finished:s,destroyed:o,pos:a}=this;return e.destroyed=o,e.finished=s,e.length=n,e.pos=a,n%t&&e.buffer.set(i),e}clone(){return this._cloneInto()}},pa=Uint32Array.from([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),ma=Uint32Array.from([3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428]),Yr=Uint32Array.from([3418070365,3238371032,1654270250,914150663,2438529370,812702999,355462360,4144912697,1731405415,4290775857,2394180231,1750603025,3675008525,1694076839,1203062813,3204075428]),Wr=Uint32Array.from([1779033703,4089235720,3144134277,2227873595,1013904242,4271175723,2773480762,1595750129,1359893119,2917565137,2600822924,725511199,528734635,4215389547,1541459225,327033209]),My=BigInt(2**32-1),vT=BigInt(32);kT=(r,e,t)=>r>>>t,ST=(r,e,t)=>r<<32-t|e>>>t,md=(r,e,t)=>r>>>t|e<<32-t,yd=(r,e,t)=>r<<32-t|e>>>t,Ly=(r,e,t)=>r<<64-t|e>>>t-32,Hy=(r,e,t)=>r>>>t-32|e<<64-t,pte=(r,e,t)=>r<<t|e>>>32-t,mte=(r,e,t)=>e<<t|r>>>32-t,yte=(r,e,t)=>e<<t-32|r>>>64-t,Ite=(r,e,t)=>r<<t-32|e>>>64-t;wte=(r,e,t)=>(r>>>0)+(e>>>0)+(t>>>0),xte=(r,e,t,i)=>e+t+i+(r/2**32|0)|0,Cte=(r,e,t,i)=>(r>>>0)+(e>>>0)+(t>>>0)+(i>>>0),Ete=(r,e,t,i,n)=>e+t+i+n+(r/2**32|0)|0,Bte=(r,e,t,i,n)=>(r>>>0)+(e>>>0)+(t>>>0)+(i>>>0)+(n>>>0),bte=(r,e,t,i,n,s)=>e+t+i+n+s+(r/2**32|0)|0,vte=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),ya=new Uint32Array(64),FI=class extends Xc{constructor(e=32){super(64,e,8,!1),this.A=pa[0]|0,this.B=pa[1]|0,this.C=pa[2]|0,this.D=pa[3]|0,this.E=pa[4]|0,this.F=pa[5]|0,this.G=pa[6]|0,this.H=pa[7]|0}get(){let{A:e,B:t,C:i,D:n,E:s,F:o,G:a,H:c}=this;return[e,t,i,n,s,o,a,c]}set(e,t,i,n,s,o,a,c){this.A=e|0,this.B=t|0,this.C=i|0,this.D=n|0,this.E=s|0,this.F=o|0,this.G=a|0,this.H=c|0}process(e,t){for(let u=0;u<16;u++,t+=4)ya[u]=e.getUint32(t,!1);for(let u=16;u<64;u++){let f=ya[u-15],h=ya[u-2],g=rs(f,7)^rs(f,18)^f>>>3,p=rs(h,17)^rs(h,19)^h>>>10;ya[u]=p+ya[u-7]+g+ya[u-16]|0}let{A:i,B:n,C:s,D:o,E:a,F:c,G:l,H:d}=this;for(let u=0;u<64;u++){let f=rs(a,6)^rs(a,11)^rs(a,25),h=d+f+CR(a,c,l)+vte[u]+ya[u]|0,p=(rs(i,2)^rs(i,13)^rs(i,22))+ER(i,n,s)|0;d=l,l=c,c=a,a=o+h|0,o=s,s=n,n=i,i=h+p|0}i=i+this.A|0,n=n+this.B|0,s=s+this.C|0,o=o+this.D|0,a=a+this.E|0,c=c+this.F|0,l=l+this.G|0,d=d+this.H|0,this.set(i,n,s,o,a,c,l,d)}roundClean(){ln(ya)}destroy(){this.set(0,0,0,0,0,0,0,0),ln(this.buffer)}},I1=class extends FI{constructor(){super(28),this.A=ma[0]|0,this.B=ma[1]|0,this.C=ma[2]|0,this.D=ma[3]|0,this.E=ma[4]|0,this.F=ma[5]|0,this.G=ma[6]|0,this.H=ma[7]|0}},bR=BR(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(r=>BigInt(r))),kte=bR[0],Ste=bR[1],Ia=new Uint32Array(80),wa=new Uint32Array(80),PI=class extends Xc{constructor(e=64){super(128,e,16,!1),this.Ah=Wr[0]|0,this.Al=Wr[1]|0,this.Bh=Wr[2]|0,this.Bl=Wr[3]|0,this.Ch=Wr[4]|0,this.Cl=Wr[5]|0,this.Dh=Wr[6]|0,this.Dl=Wr[7]|0,this.Eh=Wr[8]|0,this.El=Wr[9]|0,this.Fh=Wr[10]|0,this.Fl=Wr[11]|0,this.Gh=Wr[12]|0,this.Gl=Wr[13]|0,this.Hh=Wr[14]|0,this.Hl=Wr[15]|0}get(){let{Ah:e,Al:t,Bh:i,Bl:n,Ch:s,Cl:o,Dh:a,Dl:c,Eh:l,El:d,Fh:u,Fl:f,Gh:h,Gl:g,Hh:p,Hl:I}=this;return[e,t,i,n,s,o,a,c,l,d,u,f,h,g,p,I]}set(e,t,i,n,s,o,a,c,l,d,u,f,h,g,p,I){this.Ah=e|0,this.Al=t|0,this.Bh=i|0,this.Bl=n|0,this.Ch=s|0,this.Cl=o|0,this.Dh=a|0,this.Dl=c|0,this.Eh=l|0,this.El=d|0,this.Fh=u|0,this.Fl=f|0,this.Gh=h|0,this.Gl=g|0,this.Hh=p|0,this.Hl=I|0}process(e,t){for(let B=0;B<16;B++,t+=4)Ia[B]=e.getUint32(t),wa[B]=e.getUint32(t+=4);for(let B=16;B<80;B++){let C=Ia[B-15]|0,x=wa[B-15]|0,Q=md(C,x,1)^md(C,x,8)^kT(C,x,7),R=yd(C,x,1)^yd(C,x,8)^ST(C,x,7),U=Ia[B-2]|0,T=wa[B-2]|0,D=md(U,T,19)^Ly(U,T,61)^kT(U,T,6),b=yd(U,T,19)^Hy(U,T,61)^ST(U,T,6),_=Cte(R,b,wa[B-7],wa[B-16]),v=Ete(_,Q,D,Ia[B-7],Ia[B-16]);Ia[B]=v|0,wa[B]=_|0}let{Ah:i,Al:n,Bh:s,Bl:o,Ch:a,Cl:c,Dh:l,Dl:d,Eh:u,El:f,Fh:h,Fl:g,Gh:p,Gl:I,Hh:m,Hl:w}=this;for(let B=0;B<80;B++){let C=md(u,f,14)^md(u,f,18)^Ly(u,f,41),x=yd(u,f,14)^yd(u,f,18)^Hy(u,f,41),Q=u&h^~u&p,R=f&g^~f&I,U=Bte(w,x,R,Ste[B],wa[B]),T=bte(U,m,C,Q,kte[B],Ia[B]),D=U|0,b=md(i,n,28)^Ly(i,n,34)^Ly(i,n,39),_=yd(i,n,28)^Hy(i,n,34)^Hy(i,n,39),v=i&s^i&a^s&a,E=n&o^n&c^o&c;m=p|0,w=I|0,p=h|0,I=g|0,h=u|0,g=f|0,{h:u,l:f}=eo(l|0,d|0,T|0,D|0),l=a|0,d=c|0,a=s|0,c=o|0,s=i|0,o=n|0;let P=wte(D,_,E);i=xte(P,T,b,v),n=P|0}({h:i,l:n}=eo(this.Ah|0,this.Al|0,i|0,n|0)),{h:s,l:o}=eo(this.Bh|0,this.Bl|0,s|0,o|0),{h:a,l:c}=eo(this.Ch|0,this.Cl|0,a|0,c|0),{h:l,l:d}=eo(this.Dh|0,this.Dl|0,l|0,d|0),{h:u,l:f}=eo(this.Eh|0,this.El|0,u|0,f|0),{h,l:g}=eo(this.Fh|0,this.Fl|0,h|0,g|0),{h:p,l:I}=eo(this.Gh|0,this.Gl|0,p|0,I|0),{h:m,l:w}=eo(this.Hh|0,this.Hl|0,m|0,w|0),this.set(i,n,s,o,a,c,l,d,u,f,h,g,p,I,m,w)}roundClean(){ln(Ia,wa)}destroy(){ln(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}},w1=class extends PI{constructor(){super(48),this.Ah=Yr[0]|0,this.Al=Yr[1]|0,this.Bh=Yr[2]|0,this.Bl=Yr[3]|0,this.Ch=Yr[4]|0,this.Cl=Yr[5]|0,this.Dh=Yr[6]|0,this.Dl=Yr[7]|0,this.Eh=Yr[8]|0,this.El=Yr[9]|0,this.Fh=Yr[10]|0,this.Fl=Yr[11]|0,this.Gh=Yr[12]|0,this.Gl=Yr[13]|0,this.Hh=Yr[14]|0,this.Hl=Yr[15]|0}},ab=po(()=>new FI),Dte=po(()=>new I1),vR=po(()=>new PI),kR=po(()=>new w1),TI=class extends gA{constructor(e,t){super(),this.finished=!1,this.destroyed=!1,oR(e);let i=FA(t);if(this.iHash=e.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let n=this.blockLen,s=new Uint8Array(n);s.set(i.length>n?e.create().update(i).digest():i);for(let o=0;o<s.length;o++)s[o]^=54;this.iHash.update(s),this.oHash=e.create();for(let o=0;o<s.length;o++)s[o]^=106;this.oHash.update(s),ln(s)}update(e){return Ld(this),this.iHash.update(e),this}digestInto(e){Ld(this),ps(e,this.outputLen),this.finished=!0,this.iHash.digestInto(e),this.oHash.update(e),this.oHash.digestInto(e),this.destroy()}digest(){let e=new Uint8Array(this.oHash.outputLen);return this.digestInto(e),e}_cloneInto(e){e||(e=Object.create(Object.getPrototypeOf(this),{}));let{oHash:t,iHash:i,finished:n,destroyed:s,blockLen:o,outputLen:a}=this;return e=e,e.finished=n,e.destroyed=s,e.blockLen=o,e.outputLen=a,e.oHash=t._cloneInto(e.oHash),e.iHash=i._cloneInto(e.iHash),e}clone(){return this._cloneInto()}destroy(){this.destroyed=!0,this.oHash.destroy(),this.iHash.destroy()}},SR=(r,e,t)=>new TI(r,e).update(t).digest();SR.create=(r,e)=>new TI(r,e);Hd=BigInt(0),Hc=BigInt(1);TB=new WeakMap,QR=new WeakMap;RI=class{constructor(e,t){this.BASE=e.BASE,this.ZERO=e.ZERO,this.Fn=e.Fn,this.bits=t}_unsafeLadder(e,t,i=this.ZERO){let n=e;for(;t>Hd;)t&Hc&&(i=i.add(n)),n=n.double(),t>>=Hc;return i}precomputeWindow(e,t){let{windows:i,windowSize:n}=PB(t,this.bits),s=[],o=e,a=o;for(let c=0;c<i;c++){a=o,s.push(a);for(let l=1;l<n;l++)a=a.add(o),s.push(a);o=a.double()}return s}wNAF(e,t,i){if(!this.Fn.isValid(i))throw new Error("invalid scalar");let n=this.ZERO,s=this.BASE,o=PB(e,this.bits);for(let a=0;a<o.windows;a++){let{nextN:c,offset:l,isZero:d,isNeg:u,isNegF:f,offsetF:h}=DT(i,a,o);i=c,d?s=s.add(UI(f,t[h])):n=n.add(UI(u,t[l]))}return QT(i),{p:n,f:s}}wNAFUnsafe(e,t,i,n=this.ZERO){let s=PB(e,this.bits);for(let o=0;o<s.windows&&i!==Hd;o++){let{nextN:a,offset:c,isZero:l,isNeg:d}=DT(i,o,s);if(i=a,!l){let u=t[c];n=n.add(d?u.negate():u)}}return QT(i),n}getPrecomputes(e,t,i){let n=TB.get(t);return n||(n=this.precomputeWindow(t,e),e!==1&&(typeof i=="function"&&(n=i(n)),TB.set(t,n))),n}cached(e,t,i){let n=UB(e);return this.wNAF(n,this.getPrecomputes(n,e,i),t)}unsafe(e,t,i,n){let s=UB(e);return s===1?this._unsafeLadder(e,t,n):this.wNAFUnsafe(s,this.getPrecomputes(s,e,i),t,n)}createCache(e,t){DR(t,this.bits),QR.set(e,t),TB.delete(e)}hasCache(e){return UB(e)!==1}};FT=(r,e)=>(r+(r>=0?e:-e)/PR)/e;C1=class extends Error{constructor(e=""){super(e)}},so={Err:C1,_tlv:{encode:(r,e)=>{let{Err:t}=so;if(r<0||r>256)throw new t("tlv.encode: wrong tag");if(e.length&1)throw new t("tlv.encode: unpadded data");let i=e.length/2,n=Oy(i);if(n.length/2&128)throw new t("tlv.encode: long form length too big");let s=i>127?Oy(n.length/2|128):"";return Oy(r)+s+n+e},decode(r,e){let{Err:t}=so,i=0;if(r<0||r>256)throw new t("tlv.encode: wrong tag");if(e.length<2||e[i++]!==r)throw new t("tlv.decode: wrong tlv");let n=e[i++],s=!!(n&128),o=0;if(!s)o=n;else{let c=n&127;if(!c)throw new t("tlv.decode(long): indefinite length not supported");if(c>4)throw new t("tlv.decode(long): byte length is too big");let l=e.subarray(i,i+c);if(l.length!==c)throw new t("tlv.decode: length bytes not complete");if(l[0]===0)throw new t("tlv.decode(long): zero leftmost byte");for(let d of l)o=o<<8|d;if(i+=c,o<128)throw new t("tlv.decode(long): not minimal encoding")}let a=e.subarray(i,i+o);if(a.length!==o)throw new t("tlv.decode: wrong value length");return{v:a,l:e.subarray(i+o)}}},_int:{encode(r){let{Err:e}=so;if(r<ao)throw new e("integer: negative integers are not allowed");let t=Oy(r);if(Number.parseInt(t[0],16)&8&&(t="00"+t),t.length&1)throw new e("unexpected DER parsing assertion: unpadded hex");return t},decode(r){let{Err:e}=so;if(r[0]&128)throw new e("invalid signature integer: negative");if(r[0]===0&&!(r[1]&128))throw new e("invalid signature integer: unnecessary leading zero");return sw(r)}},toSig(r){let{Err:e,_int:t,_tlv:i}=so,n=vt("signature",r),{v:s,l:o}=i.decode(48,n);if(o.length)throw new e("invalid signature: left bytes after parsing");let{v:a,l:c}=i.decode(2,s),{v:l,l:d}=i.decode(2,c);if(d.length)throw new e("invalid signature: left bytes after parsing");return{r:t.decode(a),s:t.decode(l)}},hexFromSig(r){let{_tlv:e,_int:t}=so,i=e.encode(2,t.encode(r.r)),n=e.encode(2,t.encode(r.s)),s=i+n;return e.encode(48,s)}},ao=BigInt(0),Fd=BigInt(1),PR=BigInt(2),Ky=BigInt(3),Tte=BigInt(4);RR={p:BigInt("0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff"),n:BigInt("0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551"),h:BigInt(1),a:BigInt("0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc"),b:BigInt("0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b"),Gx:BigInt("0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296"),Gy:BigInt("0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5")},NR={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff"),n:BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973"),h:BigInt(1),a:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc"),b:BigInt("0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef"),Gx:BigInt("0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7"),Gy:BigInt("0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f")},qR={p:BigInt("0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),n:BigInt("0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409"),h:BigInt(1),a:BigInt("0x1fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc"),b:BigInt("0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00"),Gx:BigInt("0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66"),Gy:BigInt("0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650")},Hte=Qi(RR.p),Kte=Qi(NR.p),Gte=Qi(qR.p),zte=tl({...RR,Fp:Hte,lowS:!1},ab),Jte=tl({...NR,Fp:Kte,lowS:!1},kR),jte=tl({...qR,Fp:Gte,lowS:!1,allowedPrivateKeyLengths:[130,131,132]},vR);$te=zte;Vte=Jte;Yte=jte,Wte=BigInt(0),jf=BigInt(1),Xte=BigInt(2),Zte=BigInt(7),ere=BigInt(256),tre=BigInt(113),OR=[],MR=[],LR=[];for(let r=0,e=jf,t=1,i=0;r<24;r++){[t,i]=[i,(2*t+3*i)%5],OR.push(2*(5*i+t)),MR.push((r+1)*(r+2)/2%64);let n=Wte;for(let s=0;s<7;s++)e=(e<<jf^(e>>Zte)*tre)%ere,e&Xte&&(n^=jf<<(jf<<BigInt(s))-jf);LR.push(n)}HR=BR(LR,!0),rre=HR[0],ire=HR[1],PT=(r,e,t)=>t>32?yte(r,e,t):pte(r,e,t),TT=(r,e,t)=>t>32?Ite(r,e,t):mte(r,e,t);NI=class r extends gA{constructor(e,t,i,n=!1,s=24){if(super(),this.pos=0,this.posOut=0,this.finished=!1,this.destroyed=!1,this.enableXOF=!1,this.blockLen=e,this.suffix=t,this.outputLen=i,this.enableXOF=n,this.rounds=s,hA(i),!(0<e&&e<200))throw new Error("only keccak-f1600 function is supported");this.state=new Uint8Array(200),this.state32=$ee(this.state)}clone(){return this._cloneInto()}keccak(){xT(this.state32),nre(this.state32,this.rounds),xT(this.state32),this.posOut=0,this.pos=0}update(e){Ld(this),e=FA(e),ps(e);let{blockLen:t,state:i}=this,n=e.length;for(let s=0;s<n;){let o=Math.min(t-this.pos,n-s);for(let a=0;a<o;a++)i[this.pos++]^=e[s++];this.pos===t&&this.keccak()}return this}finish(){if(this.finished)return;this.finished=!0;let{state:e,suffix:t,pos:i,blockLen:n}=this;e[i]^=t,(t&128)!==0&&i===n-1&&this.keccak(),e[n-1]^=128,this.keccak()}writeInto(e){Ld(this,!1),ps(e),this.finish();let t=this.state,{blockLen:i}=this;for(let n=0,s=e.length;n<s;){this.posOut>=i&&this.keccak();let o=Math.min(i-this.posOut,s-n);e.set(t.subarray(this.posOut,this.posOut+o),n),this.posOut+=o,n+=o}return e}xofInto(e){if(!this.enableXOF)throw new Error("XOF is not possible for this instance");return this.writeInto(e)}xof(e){return hA(e),this.xofInto(new Uint8Array(e))}digestInto(e){if(aR(e,this),this.finished)throw new Error("digest() was already called");return this.writeInto(e),this.destroy(),e}digest(){return this.digestInto(new Uint8Array(this.outputLen))}destroy(){this.destroyed=!0,ln(this.state)}_cloneInto(e){let{blockLen:t,suffix:i,outputLen:n,rounds:s,enableXOF:o}=this;return e||(e=new r(t,i,n,o,s)),e.state32.set(this.state32),e.pos=this.pos,e.posOut=this.posOut,e.finished=this.finished,e.rounds=s,e.suffix=i,e.outputLen=n,e.enableXOF=o,e.destroyed=this.destroyed,e}},KR=(r,e,t)=>po(()=>new NI(e,r,t)),sre=KR(6,136,256/8),ore=KR(6,72,512/8),are=(r,e,t)=>ete((i={})=>new NI(e,r,i.dkLen===void 0?t:i.dkLen,!0)),cre=are(31,136,256/8);xa=BigInt(0),br=BigInt(1),NB=BigInt(2),lre=BigInt(8);$f=BigInt(0),Id=BigInt(1),Gy=BigInt(2);rl={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff"),n:BigInt("0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3"),h:BigInt(4),a:BigInt(1),d:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffff6756"),Gx:BigInt("0x4f1970c66bed0ded221d15a622bf36da9e146570470f1767ea6de324a3d3a46412ae1af72ab66511433b80e18b00938e2626a82bc70cc05e"),Gy:BigInt("0x693f46716eb6bc248876203756c9c7624bea73736ca3984087789c1e05a0c2d73ad3ff1ce67c39c4fdbd132c4ed7c8ad9808795bf230fa14")},mre=Object.assign({},rl,{d:BigInt("0xd78b4bdc7f0daf19f24f38c29373a2ccad46157242a50f37809b1da3412a12e79ccc9c81264cfe9ad080997058fb61c4243cc32dbaa156b9"),Gx:BigInt("0x79a70b2b70400553ae7c9df416c792c61128751ac92969240c25a07d728bdc93e21f7787ed6972249de732f38496cd11698713093e9c04fc"),Gy:BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffff80000000000000000000000000000000000000000000000000000001")}),yre=po(()=>cre.create({dkLen:114})),Ire=BigInt(1),E1=BigInt(2),UT=BigInt(3);BigInt(4);wre=BigInt(11),xre=BigInt(22),RT=BigInt(44),Cre=BigInt(88),Ere=BigInt(223);bre=Qi(rl.p,{BITS:456,isLE:!0}),NT=Qi(rl.n,{BITS:456,isLE:!0});kre={...rl,Fp:bre,Fn:NT,nBitLength:NT.BITS,hash:yre,adjustScalarBytes:JR,domain:vre,uvRatio:Bre},Sre=hre(kre);GR(mre);Dre=(()=>{let r=rl.p;return pre({P:r,type:"x448",powPminus2:e=>{let t=zR(e),i=bt(t,E1,r);return mr(i*e,r)},adjustScalarBytes:JR})})();cb={p:BigInt("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"),n:BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"),h:BigInt(1),a:BigInt(0),b:BigInt(7),Gx:BigInt("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"),Gy:BigInt("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8")},Qre={beta:BigInt("0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee"),basises:[[BigInt("0x3086d221a7d46bcde86c90e49284eb15"),-BigInt("0xe4437ed6010e88286f547fa90abfe4c3")],[BigInt("0x114ca50f7a8e2f3f657c1108d9d44cfd8"),BigInt("0x3086d221a7d46bcde86c90e49284eb15")]]},qT=BigInt(2);B1=Qi(cb.p,{sqrt:_re}),Fre=tl({...cb,Fp:B1,lowS:!0,endo:Qre},ab),jR=ab,Pre=Dte,$R=Qi(BigInt("0xa9fb57dba1eea9bc3e660a909d838d726e3bf623d52620282013481d1f6e5377")),Tre=$R.create(BigInt("0x7d5a0975fc2c3057eef67530417affe7fb8055c126dc5c6ce94a4b44f330b5d9")),Ure=BigInt("0x26dc5c6ce94a4b44f330b5d9bbd77cbf958416295cf7e1ce6bccdc18ff8c07b6"),Rre=tl({a:Tre,b:Ure,Fp:$R,n:BigInt("0xa9fb57dba1eea9bc3e660a909d838d718c397aa3b561a6f7901e0e82974856a7"),Gx:BigInt("0x8bd2aeb9cb7e57cb2c4b482ffc81b7afb9de27e1e3bd23c23a4453bd9ace3262"),Gy:BigInt("0x547ef835c3dac4fd97f8461a14611dc9c27745132ded8e545c1d54c72f046997"),h:BigInt(1),lowS:!1},jR),VR=vR,YR=kR,WR=Qi(BigInt("0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b412b1da197fb71123acd3a729901d1a71874700133107ec53")),Nre=WR.create(BigInt("0x7bc382c63d8c150c3c72080ace05afa0c2bea28e4fb22787139165efba91f90f8aa5814a503ad4eb04a8c7dd22ce2826")),qre=BigInt("0x04a8c7dd22ce28268b39b55416f0447c2fb77de107dcd2a62e880ea53eeb62d57cb4390295dbc9943ab78696fa504c11"),Ore=tl({a:Nre,b:qre,Fp:WR,n:BigInt("0x8cb91e82a3386d280f5d6f7e50e641df152f7109ed5456b31f166e6cac0425a7cf3ab6af6b7fc3103b883202e9046565"),Gx:BigInt("0x1d1c64f068cf45ffa2a63a81b7c13f6b8847a3e77ef14fe3db7fcafe0cbd10e8e826e03436d646aaef87b2e247d4af1e"),Gy:BigInt("0x8abe1d7520f9c2a45cb1eb8e95cfd55262b70b29feec5864e19c054ff99129280e4646217791811142820341263c5315"),h:BigInt(1),lowS:!1},YR),XR=Qi(BigInt("0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca703308717d4d9b009bc66842aecda12ae6a380e62881ff2f2d82c68528aa6056583a48f3")),Mre=XR.create(BigInt("0x7830a3318b603b89e2327145ac234cc594cbdd8d3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94ca")),Lre=BigInt("0x3df91610a83441caea9863bc2ded5d5aa8253aa10a2ef1c98b9ac8b57f1117a72bf2c7b9e7c1ac4d77fc94cadc083e67984050b75ebae5dd2809bd638016f723"),Hre=tl({a:Mre,b:Lre,Fp:XR,n:BigInt("0xaadd9db8dbe9c48b3fd4e6ae33c9fc07cb308db3b3c9d20ed6639cca70330870553e5c414ca92619418661197fac10471db1d381085ddaddb58796829ca90069"),Gx:BigInt("0x81aee4bdd82ed9645a21322e9c4c6a9385ed9f70b5d916c1b43b62eef4d0098eff3b1f78e2d0d48d50d1687b93b97d5f7c6d5047406a5e688b352209bcb9f822"),Gy:BigInt("0x7dde385d566332ecc0eabfa9cf7822fdf209f70024a57b1aa000c55b881f8111b2dcde494a5f485e5bca4bd88a2763aed1ca2b2fa8f0540678cd1e0f3ad80892"),h:BigInt(1),lowS:!1},VR),Kre=new Map(Object.entries({nistP256:$te,nistP384:Vte,nistP521:Yte,brainpoolP256r1:Rre,brainpoolP384r1:Ore,brainpoolP512r1:Hre,secp256k1:Fre,x448:Dre,ed448:Sre})),Gre=Object.freeze({__proto__:null,nobleCurves:Kre}),Vf=Uint32Array.from([1732584193,4023233417,2562383102,271733878,3285377520]),Ca=new Uint32Array(80),b1=class extends Xc{constructor(){super(64,20,8,!1),this.A=Vf[0]|0,this.B=Vf[1]|0,this.C=Vf[2]|0,this.D=Vf[3]|0,this.E=Vf[4]|0}get(){let{A:e,B:t,C:i,D:n,E:s}=this;return[e,t,i,n,s]}set(e,t,i,n,s){this.A=e|0,this.B=t|0,this.C=i|0,this.D=n|0,this.E=s|0}process(e,t){for(let c=0;c<16;c++,t+=4)Ca[c]=e.getUint32(t,!1);for(let c=16;c<80;c++)Ca[c]=Da(Ca[c-3]^Ca[c-8]^Ca[c-14]^Ca[c-16],1);let{A:i,B:n,C:s,D:o,E:a}=this;for(let c=0;c<80;c++){let l,d;c<20?(l=CR(n,s,o),d=1518500249):c<40?(l=n^s^o,d=1859775393):c<60?(l=ER(n,s,o),d=2400959708):(l=n^s^o,d=3395469782);let u=Da(i,5)+l+a+d+Ca[c]|0;a=o,o=s,s=Da(n,30),n=i,i=u}i=i+this.A|0,n=n+this.B|0,s=s+this.C|0,o=o+this.D|0,a=a+this.E|0,this.set(i,n,s,o,a)}roundClean(){ln(Ca)}destroy(){this.set(0,0,0,0,0),ln(this.buffer)}},zre=po(()=>new b1),Jre=Uint8Array.from([7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8]),ZR=Uint8Array.from(new Array(16).fill(0).map((r,e)=>e)),jre=ZR.map(r=>(9*r+5)%16),eN=(()=>{let t=[[ZR],[jre]];for(let i=0;i<4;i++)for(let n of t)n.push(n[i].map(s=>Jre[s]));return t})(),tN=eN[0],rN=eN[1],iN=[[11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8],[12,13,11,15,6,9,9,7,12,15,11,13,7,8,7,7],[13,15,14,11,7,7,6,8,13,14,13,12,5,5,6,9],[14,11,12,14,8,6,5,5,15,12,15,14,9,9,8,6],[15,12,13,13,9,5,8,6,14,11,12,11,8,6,5,5]].map(r=>Uint8Array.from(r)),$re=tN.map((r,e)=>r.map(t=>iN[e][t])),Vre=rN.map((r,e)=>r.map(t=>iN[e][t])),Yre=Uint32Array.from([0,1518500249,1859775393,2400959708,2840853838]),Wre=Uint32Array.from([1352829926,1548603684,1836072691,2053994217,0]);zy=new Uint32Array(16),v1=class extends Xc{constructor(){super(64,20,8,!0),this.h0=1732584193,this.h1=-271733879,this.h2=-1732584194,this.h3=271733878,this.h4=-1009589776}get(){let{h0:e,h1:t,h2:i,h3:n,h4:s}=this;return[e,t,i,n,s]}set(e,t,i,n,s){this.h0=e|0,this.h1=t|0,this.h2=i|0,this.h3=n|0,this.h4=s|0}process(e,t){for(let h=0;h<16;h++,t+=4)zy[h]=e.getUint32(t,!0);let i=this.h0|0,n=i,s=this.h1|0,o=s,a=this.h2|0,c=a,l=this.h3|0,d=l,u=this.h4|0,f=u;for(let h=0;h<5;h++){let g=4-h,p=Yre[h],I=Wre[h],m=tN[h],w=rN[h],B=$re[h],C=Vre[h];for(let x=0;x<16;x++){let Q=Da(i+OT(h,s,a,l)+zy[m[x]]+p,B[x])+u|0;i=u,u=l,l=Da(a,10)|0,a=s,s=Q}for(let x=0;x<16;x++){let Q=Da(n+OT(g,o,c,d)+zy[w[x]]+I,C[x])+f|0;n=f,f=d,d=Da(c,10)|0,c=o,o=Q}}this.set(this.h1+a+d|0,this.h2+l+f|0,this.h3+u+n|0,this.h4+i+o|0,this.h0+s+c|0)}roundClean(){ln(zy)}destroy(){this.destroyed=!0,ln(this.buffer),this.set(0,0,0,0,0)}},Xre=po(()=>new v1),Zre=zre,eie=Xre,tie=Array.from({length:64},(r,e)=>Math.floor(2**32*Math.abs(Math.sin(e+1)))),MT=(r,e,t)=>r&e^~r&t,Jy=new Uint32Array([1732584193,4023233417,2562383102,271733878]),qB=new Uint32Array(16),k1=class extends Xc{constructor(){super(64,16,8,!0),this.A=Jy[0]|0,this.B=Jy[1]|0,this.C=Jy[2]|0,this.D=Jy[3]|0}get(){let{A:e,B:t,C:i,D:n}=this;return[e,t,i,n]}set(e,t,i,n){this.A=e|0,this.B=t|0,this.C=i|0,this.D=n|0}process(e,t){for(let a=0;a<16;a++,t+=4)qB[a]=e.getUint32(t,!0);let{A:i,B:n,C:s,D:o}=this;for(let a=0;a<64;a++){let c,l,d;a<16?(c=MT(n,s,o),l=a,d=[7,12,17,22]):a<32?(c=MT(o,n,s),l=(5*a+1)%16,d=[5,9,14,20]):a<48?(c=n^s^o,l=(3*a+5)%16,d=[4,11,16,23]):(c=s^(n|~o),l=7*a%16,d=[6,10,15,21]),c=c+i+tie[a]+qB[l],i=o,o=s,s=n,n=n+Da(c,d[a%4])}i=i+this.A|0,n=n+this.B|0,s=s+this.C|0,o=o+this.D|0,this.set(i,n,s,o)}roundClean(){qB.fill(0)}destroy(){this.set(0,0,0,0),this.buffer.fill(0)}},rie=tte(()=>new k1),iie=new Map(Object.entries({md5:rie,sha1:Zre,sha224:Pre,sha256:jR,sha384:YR,sha512:VR,sha3_256:sre,sha3_512:ore,ripemd160:eie})),nie=Object.freeze({__proto__:null,nobleHashes:iie}),OB=Hi&&typeof Hi=="object"&&"webcrypto"in Hi?Hi.webcrypto:void 0,Di={},Pe=function(r){var e,t=new Float64Array(16);if(r)for(e=0;e<r.length;e++)t[e]=r[e];return t},lb=function(){throw new Error("no PRNG")},nN=new Uint8Array(32);nN[0]=9;S1=Pe(),qI=Pe([1]),sie=Pe([56129,1]),oie=Pe([30883,4953,19914,30187,55467,16705,2637,112,59544,30585,16505,36039,65139,11119,27886,20995]),aie=Pe([61785,9906,39828,60374,45398,33411,5274,224,53552,61171,33010,6542,64743,22239,55772,9222]),LT=Pe([54554,36645,11616,51542,42930,38181,51040,26924,56412,64982,57905,49316,21502,52590,14035,8553]),HT=Pe([26200,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214,26214]),cie=Pe([41136,18958,6951,50414,58488,44335,6150,12099,55207,15867,153,11085,57099,20417,9344,11139]);zT=[1116352408,3609767458,1899447441,602891725,3049323471,3964484399,3921009573,2173295548,961987163,4081628472,1508970993,3053834265,2453635748,2937671579,2870763221,3664609560,3624381080,2734883394,310598401,1164996542,607225278,1323610764,1426881987,3590304994,1925078388,4068182383,2162078206,991336113,2614888103,633803317,3248222580,3479774868,3835390401,2666613458,4022224774,944711139,264347078,2341262773,604807628,2007800933,770255983,1495990901,1249150122,1856431235,1555081692,3175218132,1996064986,2198950837,2554220882,3999719339,2821834349,766784016,2952996808,2566594879,3210313671,3203337956,3336571891,1034457026,3584528711,2466948901,113926993,3758326383,338241895,168717936,666307205,1188179964,773529912,1546045734,1294757372,1522805485,1396182291,2643833823,1695183700,2343527390,1986661051,1014477480,2177026350,1206759142,2456956037,344077627,2730485921,1290863460,2820302411,3158454273,3259730800,3505952657,3345764771,106217008,3516065817,3606008344,3600352804,1432725776,4094571909,1467031594,275423344,851169720,430227734,3100823752,506948616,1363258195,659060556,3750685593,883997877,3785050280,958139571,3318307427,1322822218,3812723403,1537002063,2003034995,1747873779,3602036899,1955562222,1575990012,2024104815,1125592928,2227730452,2716904306,2361852424,442776044,2428436474,593698344,2756734187,3733110249,3204031479,2999351573,3329325298,3815920427,3391569614,3928383900,3515267271,566280711,3940187606,3454069534,4118630271,4000239992,116418474,1914138554,174292421,2731055270,289380356,3203993006,460393269,320620315,685471733,587496836,852142971,1086792851,1017036298,365543100,1126000580,2618297676,1288033470,3409855158,1501505948,4234509866,1607167915,987167468,1816402316,1246189591];LB=new Float64Array([237,211,245,92,26,99,18,88,214,156,247,162,222,249,222,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16]);$T=32,gie=32,hN=32,gN=32,Mc=64,ow=32,aw=64,pie=32;Di.scalarMult=function(r,e){if(Jd(r,e),r.length!==gie)throw new Error("bad n size");if(e.length!==$T)throw new Error("bad p size");var t=new Uint8Array($T);return lN(t,r,e),t};Di.box={};Di.box.keyPair=function(){var r=new Uint8Array(hN),e=new Uint8Array(gN);return uie(r,e),{publicKey:r,secretKey:e}};Di.box.keyPair.fromSecretKey=function(r){if(Jd(r),r.length!==gN)throw new Error("bad secret key size");var e=new Uint8Array(hN);return dN(e,r),{publicKey:e,secretKey:new Uint8Array(r)}};Di.sign=function(r,e){if(Jd(r,e),e.length!==aw)throw new Error("bad secret key size");var t=new Uint8Array(Mc+r.length);return fie(t,r,r.length,e),t};Di.sign.detached=function(r,e){for(var t=Di.sign(r,e),i=new Uint8Array(Mc),n=0;n<i.length;n++)i[n]=t[n];return i};Di.sign.detached.verify=function(r,e,t){if(Jd(r,e,t),e.length!==Mc)throw new Error("bad signature size");if(t.length!==ow)throw new Error("bad public key size");var i=new Uint8Array(Mc+r.length),n=new Uint8Array(Mc+r.length),s;for(s=0;s<Mc;s++)i[s]=e[s];for(s=0;s<r.length;s++)i[s+Mc]=r[s];return hie(n,i,i.length,t)>=0};Di.sign.keyPair=function(){var r=new Uint8Array(ow),e=new Uint8Array(aw);return fN(r,e),{publicKey:r,secretKey:e}};Di.sign.keyPair.fromSecretKey=function(r){if(Jd(r),r.length!==aw)throw new Error("bad secret key size");for(var e=new Uint8Array(ow),t=0;t<e.length;t++)e[t]=r[32+t];return{publicKey:e,secretKey:new Uint8Array(r)}};Di.sign.keyPair.fromSeed=function(r){if(Jd(r),r.length!==pie)throw new Error("bad seed size");for(var e=new Uint8Array(ow),t=new Uint8Array(aw),i=0;i<32;i++)t[i]=r[i];return fN(e,t,!0),{publicKey:e,secretKey:t}};Di.setPRNG=function(r){lb=r};(function(){if(OB&&OB.getRandomValues){var r=65536;Di.setPRNG(function(e,t){var i,n=new Uint8Array(t);for(i=0;i<t;i+=r)OB.getRandomValues(n.subarray(i,i+Math.min(t-i,r)));for(i=0;i<t;i++)e[i]=n[i];mie(n)})}})();il=Object.freeze({__proto__:null,default:Di});yA.keySize=yA.prototype.keySize=24;yA.blockSize=yA.prototype.blockSize=8;IA.blockSize=IA.prototype.blockSize=8;IA.keySize=IA.prototype.keySize=16;rn=4294967295;wA.keySize=wA.prototype.keySize=32;wA.blockSize=wA.prototype.blockSize=16;Tn.prototype.BLOCKSIZE=8;Tn.prototype.SBOXES=[[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005,3532540348,1274779536,3789419226,2764799539,1660621633,3471099624,4011903706,913787905,3497959166,737222580,2514213453,2928710040,3937242737,1804850592,3499020752,2949064160,2386320175,2390070455,2415321851,4061277028,2290661394,2416832540,1336762016,1754252060,3520065937,3014181293,791618072,3188594551,3933548030,2332172193,3852520463,3043980520,413987798,3465142937,3030929376,4245938359,2093235073,3534596313,375366246,2157278981,2479649556,555357303,3870105701,2008414854,3344188149,4221384143,3956125452,2067696032,3594591187,2921233993,2428461,544322398,577241275,1471733935,610547355,4027169054,1432588573,1507829418,2025931657,3646575487,545086370,48609733,2200306550,1653985193,298326376,1316178497,3007786442,2064951626,458293330,2589141269,3591329599,3164325604,727753846,2179363840,146436021,1461446943,4069977195,705550613,3059967265,3887724982,4281599278,3313849956,1404054877,2845806497,146425753,1854211946],[1266315497,3048417604,3681880366,3289982499,290971e4,1235738493,2632868024,2414719590,3970600049,1771706367,1449415276,3266420449,422970021,1963543593,2690192192,3826793022,1062508698,1531092325,1804592342,2583117782,2714934279,4024971509,1294809318,4028980673,1289560198,2221992742,1669523910,35572830,157838143,1052438473,1016535060,1802137761,1753167236,1386275462,3080475397,2857371447,1040679964,2145300060,2390574316,1461121720,2956646967,4031777805,4028374788,33600511,2920084762,1018524850,629373528,3691585981,3515945977,2091462646,2486323059,586499841,988145025,935516892,3367335476,2599673255,2839830854,265290510,3972581182,2759138881,3795373465,1005194799,847297441,406762289,1314163512,1332590856,1866599683,4127851711,750260880,613907577,1450815602,3165620655,3734664991,3650291728,3012275730,3704569646,1427272223,778793252,1343938022,2676280711,2052605720,1946737175,3164576444,3914038668,3967478842,3682934266,1661551462,3294938066,4011595847,840292616,3712170807,616741398,312560963,711312465,1351876610,322626781,1910503582,271666773,2175563734,1594956187,70604529,3617834859,1007753275,1495573769,4069517037,2549218298,2663038764,504708206,2263041392,3941167025,2249088522,1514023603,1998579484,1312622330,694541497,2582060303,2151582166,1382467621,776784248,2618340202,3323268794,2497899128,2784771155,503983604,4076293799,907881277,423175695,432175456,1378068232,4145222326,3954048622,3938656102,3820766613,2793130115,2977904593,26017576,3274890735,3194772133,1700274565,1756076034,4006520079,3677328699,720338349,1533947780,354530856,688349552,3973924725,1637815568,332179504,3949051286,53804574,2852348879,3044236432,1282449977,3583942155,3416972820,4006381244,1617046695,2628476075,3002303598,1686838959,431878346,2686675385,1700445008,1080580658,1009431731,832498133,3223435511,2605976345,2271191193,2516031870,1648197032,4164389018,2548247927,300782431,375919233,238389289,3353747414,2531188641,2019080857,1475708069,455242339,2609103871,448939670,3451063019,1395535956,2413381860,1841049896,1491858159,885456874,4264095073,4001119347,1565136089,3898914787,1108368660,540939232,1173283510,2745871338,3681308437,4207628240,3343053890,4016749493,1699691293,1103962373,3625875870,2256883143,3830138730,1031889488,3479347698,1535977030,4236805024,3251091107,2132092099,1774941330,1199868427,1452454533,157007616,2904115357,342012276,595725824,1480756522,206960106,497939518,591360097,863170706,2375253569,3596610801,1814182875,2094937945,3421402208,1082520231,3463918190,2785509508,435703966,3908032597,1641649973,2842273706,3305899714,1510255612,2148256476,2655287854,3276092548,4258621189,236887753,3681803219,274041037,1734335097,3815195456,3317970021,1899903192,1026095262,4050517792,356393447,2410691914,3873677099,3682840055],[3913112168,2491498743,4132185628,2489919796,1091903735,1979897079,3170134830,3567386728,3557303409,857797738,1136121015,1342202287,507115054,2535736646,337727348,3213592640,1301675037,2528481711,1895095763,1721773893,3216771564,62756741,2142006736,835421444,2531993523,1442658625,3659876326,2882144922,676362277,1392781812,170690266,3921047035,1759253602,3611846912,1745797284,664899054,1329594018,3901205900,3045908486,2062866102,2865634940,3543621612,3464012697,1080764994,553557557,3656615353,3996768171,991055499,499776247,1265440854,648242737,3940784050,980351604,3713745714,1749149687,3396870395,4211799374,3640570775,1161844396,3125318951,1431517754,545492359,4268468663,3499529547,1437099964,2702547544,3433638243,2581715763,2787789398,1060185593,1593081372,2418618748,4260947970,69676912,2159744348,86519011,2512459080,3838209314,1220612927,3339683548,133810670,1090789135,1078426020,1569222167,845107691,3583754449,4072456591,1091646820,628848692,1613405280,3757631651,526609435,236106946,48312990,2942717905,3402727701,1797494240,859738849,992217954,4005476642,2243076622,3870952857,3732016268,765654824,3490871365,2511836413,1685915746,3888969200,1414112111,2273134842,3281911079,4080962846,172450625,2569994100,980381355,4109958455,2819808352,2716589560,2568741196,3681446669,3329971472,1835478071,660984891,3704678404,4045999559,3422617507,3040415634,1762651403,1719377915,3470491036,2693910283,3642056355,3138596744,1364962596,2073328063,1983633131,926494387,3423689081,2150032023,4096667949,1749200295,3328846651,309677260,2016342300,1779581495,3079819751,111262694,1274766160,443224088,298511866,1025883608,3806446537,1145181785,168956806,3641502830,3584813610,1689216846,3666258015,3200248200,1692713982,2646376535,4042768518,1618508792,1610833997,3523052358,4130873264,2001055236,3610705100,2202168115,4028541809,2961195399,1006657119,2006996926,3186142756,1430667929,3210227297,1314452623,4074634658,4101304120,2273951170,1399257539,3367210612,3027628629,1190975929,2062231137,2333990788,2221543033,2438960610,1181637006,548689776,2362791313,3372408396,3104550113,3145860560,296247880,1970579870,3078560182,3769228297,1714227617,3291629107,3898220290,166772364,1251581989,493813264,448347421,195405023,2709975567,677966185,3703036547,1463355134,2715995803,1338867538,1343315457,2802222074,2684532164,233230375,2599980071,2000651841,3277868038,1638401717,4028070440,3237316320,6314154,819756386,300326615,590932579,1405279636,3267499572,3150704214,2428286686,3959192993,3461946742,1862657033,1266418056,963775037,2089974820,2263052895,1917689273,448879540,3550394620,3981727096,150775221,3627908307,1303187396,508620638,2975983352,2726630617,1817252668,1876281319,1457606340,908771278,3720792119,3617206836,2455994898,1729034894,1080033504],[976866871,3556439503,2881648439,1522871579,1555064734,1336096578,3548522304,2579274686,3574697629,3205460757,3593280638,3338716283,3079412587,564236357,2993598910,1781952180,1464380207,3163844217,3332601554,1699332808,1393555694,1183702653,3581086237,1288719814,691649499,2847557200,2895455976,3193889540,2717570544,1781354906,1676643554,2592534050,3230253752,1126444790,2770207658,2633158820,2210423226,2615765581,2414155088,3127139286,673620729,2805611233,1269405062,4015350505,3341807571,4149409754,1057255273,2012875353,2162469141,2276492801,2601117357,993977747,3918593370,2654263191,753973209,36408145,2530585658,25011837,3520020182,2088578344,530523599,2918365339,1524020338,1518925132,3760827505,3759777254,1202760957,3985898139,3906192525,674977740,4174734889,2031300136,2019492241,3983892565,4153806404,3822280332,352677332,2297720250,60907813,90501309,3286998549,1016092578,2535922412,2839152426,457141659,509813237,4120667899,652014361,1966332200,2975202805,55981186,2327461051,676427537,3255491064,2882294119,3433927263,1307055953,942726286,933058658,2468411793,3933900994,4215176142,1361170020,2001714738,2830558078,3274259782,1222529897,1679025792,2729314320,3714953764,1770335741,151462246,3013232138,1682292957,1483529935,471910574,1539241949,458788160,3436315007,1807016891,3718408830,978976581,1043663428,3165965781,1927990952,4200891579,2372276910,3208408903,3533431907,1412390302,2931980059,4132332400,1947078029,3881505623,4168226417,2941484381,1077988104,1320477388,886195818,18198404,3786409e3,2509781533,112762804,3463356488,1866414978,891333506,18488651,661792760,1628790961,3885187036,3141171499,876946877,2693282273,1372485963,791857591,2686433993,3759982718,3167212022,3472953795,2716379847,445679433,3561995674,3504004811,3574258232,54117162,3331405415,2381918588,3769707343,4154350007,1140177722,4074052095,668550556,3214352940,367459370,261225585,2610173221,4209349473,3468074219,3265815641,314222801,3066103646,3808782860,282218597,3406013506,3773591054,379116347,1285071038,846784868,2669647154,3771962079,3550491691,2305946142,453669953,1268987020,3317592352,3279303384,3744833421,2610507566,3859509063,266596637,3847019092,517658769,3462560207,3443424879,370717030,4247526661,2224018117,4143653529,4112773975,2788324899,2477274417,1456262402,2901442914,1517677493,1846949527,2295493580,3734397586,2176403920,1280348187,1908823572,3871786941,846861322,1172426758,3287448474,3383383037,1655181056,3139813346,901632758,1897031941,2986607138,3066810236,3447102507,1393639104,373351379,950779232,625454576,3124240540,4148612726,2007998917,544563296,2244738638,2330496472,2058025392,1291430526,424198748,50039436,29584100,3605783033,2429876329,2791104160,1057563949,3255363231,3075367218,3463963227,1469046755,985887462]];Tn.prototype.PARRAY=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731];Tn.prototype.NN=16;Tn.prototype._clean=function(r){return r<0&&(r=(r&2147483647)+2147483648),r};Tn.prototype._F=function(r){let e,t=r&255;r>>>=8;let i=r&255;r>>>=8;let n=r&255;r>>>=8;let s=r&255;return e=this.sboxes[0][s]+this.sboxes[1][n],e^=this.sboxes[2][i],e+=this.sboxes[3][t],e};Tn.prototype._encryptBlock=function(r){let e=r[0],t=r[1],i;for(i=0;i<this.NN;++i){e^=this.parray[i],t=this._F(e)^t;let n=e;e=t,t=n}e^=this.parray[this.NN+0],t^=this.parray[this.NN+1],r[0]=this._clean(t),r[1]=this._clean(e)};Tn.prototype.encryptBlock=function(r){let e,t=[0,0],i=this.BLOCKSIZE/2;for(e=0;e<this.BLOCKSIZE/2;++e)t[0]=t[0]<<8|r[e+0]&255,t[1]=t[1]<<8|r[e+i]&255;this._encryptBlock(t);let n=[];for(e=0;e<this.BLOCKSIZE/2;++e)n[e+0]=t[0]>>>24-8*e&255,n[e+i]=t[1]>>>24-8*e&255;return n};Tn.prototype._decryptBlock=function(r){let e=r[0],t=r[1],i;for(i=this.NN+1;i>1;--i){e^=this.parray[i],t=this._F(e)^t;let n=e;e=t,t=n}e^=this.parray[1],t^=this.parray[0],r[0]=this._clean(t),r[1]=this._clean(e)};Tn.prototype.init=function(r){let e,t=0;for(this.parray=[],e=0;e<this.NN+2;++e){let n=0;for(let s=0;s<4;++s)n=n<<8|r[t]&255,++t>=r.length&&(t=0);this.parray[e]=this.PARRAY[e]^n}for(this.sboxes=[],e=0;e<4;++e)for(this.sboxes[e]=[],t=0;t<256;++t)this.sboxes[e][t]=this.SBOXES[e][t];let i=[0,0];for(e=0;e<this.NN+2;e+=2)this._encryptBlock(i),this.parray[e+0]=i[0],this.parray[e+1]=i[1];for(e=0;e<4;++e)for(t=0;t<256;t+=2)this._encryptBlock(i),this.sboxes[e][t+0]=i[0],this.sboxes[e][t+1]=i[1]};xA.keySize=xA.prototype.keySize=16;xA.blockSize=xA.prototype.blockSize=8;Cie=new Map(Object.entries({tripledes:yA,cast5:IA,twofish:wA,blowfish:xA})),Eie=Object.freeze({__proto__:null,legacyCiphers:Cie});pN=new Uint32Array([4089235720,1779033703,2227873595,3144134277,4271175723,1013904242,1595750129,2773480762,2917565137,1359893119,725511199,2600822924,4215389547,528734635,327033209,1541459225]),Xr=new Uint8Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3,11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4,7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8,9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13,2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9,12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11,13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10,6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5,10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3].map(r=>r*2));_1=class{constructor(e,t,i,n){let s=new Uint8Array(64);this.S={b:new Uint8Array(Yf),h:new Uint32Array(F1/4),t0:new Uint32Array(2),c:0,outlen:e},s[0]=e,t&&(s[1]=t.length),s[2]=1,s[3]=1,i&&s.set(i,32),n&&s.set(n,48);let o=new Uint32Array(s.buffer,s.byteOffset,s.length/Uint32Array.BYTES_PER_ELEMENT);for(let a=0;a<16;a++)this.S.h[a]=pN[a]^o[a];if(t){let a=new Uint8Array(Yf);a.set(t),this.update(a)}}update(e){if(!(e instanceof Uint8Array))throw new Error("Input must be Uint8Array or Buffer");let t=0;for(;t<e.length;){this.S.c===Yf&&(VT(this.S.t0,this.S.c),YT(this.S,!1),this.S.c=0);let i=Yf-this.S.c;this.S.b.set(e.subarray(t,t+i),this.S.c);let n=Math.min(i,e.length-t);this.S.c+=n,t+=n}return this}digest(e){VT(this.S.t0,this.S.c),this.S.b.fill(0,this.S.c),this.S.c=Yf,YT(this.S,!0);let t=e||new Uint8Array(this.S.outlen);for(let i=0;i<this.S.outlen;i++)t[i]=this.S.h[i>>2]>>8*(i&3);return this.S.h=null,t.buffer}};F1=64,Yf=128,fb=2,mN=19,Bie=4294967295,bie=4,vie=4294967295,kie=8,Sie=4294967295,Die=8,Qie=4294967295,_ie=4294967295,Fie=32,ro=1024,Pie=64,Tie=new Uint8Array(new Uint16Array([43981]).buffer)[0]===205;yN=1024,Oie=64*yN;jie=async()=>Gie(r=>zie(r),r=>Jie(r)),$ie=Object.freeze({__proto__:null,default:jie});wN=Xie(),Zie=Vie(wN),ene=qW({__proto__:null,default:Zie},[wN])});function tne(r){let e="",t=0,i=0;for(let n of r)for(i=i<<8|n,t+=8;t>=6;)t-=6,e+=CN[i>>t&63];return t>0&&(e+=CN[i<<6-t&63]),e}function BN(r){let e=atob(r),t=new Uint8Array(e.length);for(let i=0;i<e.length;i++)t[i]=e.charCodeAt(i);return t}function rne(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e)}async function ine(r){return new Uint8Array(await crypto.subtle.digest("SHA-256",r))}function bN(r,e){let t=BN(e),n=`$2a$10$${tne(t).padEnd(22,".")}`;return EN.default.hashSync(r,n).slice(n.length)}async function TA(r,e){let t=await tw({armoredKey:r});return rw({privateKey:t,passphrase:e})}async function jd(r,e){let t=await DA({armoredMessage:r}),{data:i}=await iw({message:t,decryptionKeys:e,format:"utf8"});return i}async function cw(r,e,t){let i=await rR({text:r});return iR({message:i,encryptionKeys:e,signingKeys:t?[t]:void 0,format:"armored"})}async function hb(r){let e=await ine(new TextEncoder().encode(r));return rne(e)}async function vN(r,e){let t=await DA({armoredMessage:r}),i=await nR({message:t,decryptionKeys:e});if(!i.length)throw new Error("failed to decrypt session key");return i[0]}async function kN(r,e){let t=await DA({binaryMessage:r}),{data:i}=await iw({message:t,sessionKeys:e,format:"binary"});return i}async function gb(r,e){if(!r)return 0;try{let t=BN(r),i=await DA({binaryMessage:t}),{data:n}=await iw({message:i,decryptionKeys:e,format:"binary"});return nne(n)}catch{return 0}}function nne(r){let e=0;for(;e<r.length;){let t=r[e++],i=t>>3,n=t&7;if(n===0){let s=0n,o=0n;for(;e<r.length;){let a=r[e++];if(s|=BigInt(a&127)<<o,(a&128)===0)break;o+=7n}if(i===1)return Number(s)}else if(n===2){let s=0,o=0;for(;e<r.length;){let a=r[e++];if(s|=(a&127)<<o,(a&128)===0)break;o+=7}e+=s}else if(n===1)e+=8;else if(n===5)e+=4;else break}return 0}var EN,CN,pb=J(()=>{"use strict";Ab();EN=xr(hB(),1),CN="./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"});async function SN(r,e,t){let i=r.find(a=>a.Primary===1)||r[0];if(!i)throw new Error("user has no keys");let n=t.find(a=>a.ID===i.ID);if(!n)throw new Error("no salt for user key");let s=bN(e,n.KeySalt),o=[];for(let a of r)try{o.push(await TA(a.PrivateKey,s))}catch{}if(o.length===0)throw new Error("failed to unlock user keys (wrong password?)");return{userKR:o,saltedKeyPass:s}}async function DN(r,e,t){let i=new Map;for(let n of r){let s=[];for(let o of n.Keys)try{if(o.Token){let a=await jd(o.Token,e);s.push(await TA(o.PrivateKey,a))}else s.push(await TA(o.PrivateKey,t))}catch{}s.length>0&&i.set(n.ID,s)}return i}async function QN(r,e){let t=e.get(r.AddressID);if(!t)throw new Error("no address keyring for share");let i=await jd(r.Passphrase,t),n=await TA(r.Key,i);return[...t,n]}var lw,_N=J(()=>{"use strict";AB();U6();pb();lw=class{constructor(e){this.addition=e}addition;accessToken="";refreshToken="";uid="";saltedKeyPass="";setAuth(e,t,i){this.accessToken=e,this.refreshToken=t,this.uid=i}getUID(){return this.uid}getSaltedKeyPass(){return this.saltedKeyPass}setSaltedKeyPass(e){this.saltedKeyPass=e}authHeaders(){let e={Accept:B6,"X-Pm-Appversion":k6,"X-Pm-Drive-Sdk-Version":b6,"User-Agent":v6};return this.accessToken&&(e.Authorization=`Bearer ${this.accessToken}`,e["X-Pm-Uid"]=this.uid),e}async request(e,t={}){let i=t.method||"GET",n=this.authHeaders(),s={method:i,headers:n};t.body!==void 0&&(s.body=JSON.stringify(t.body),n["Content-Type"]="application/json");let o=await fetch(e,s),a=await o.text(),c={};try{c=a?JSON.parse(a):{}}catch{c={}}if(!o.ok)throw new Error(`Proton request failed: ${o.status} ${(c.Error||a).slice(0,200)}`);return c}async requestAPI(e,t={}){let i=e.startsWith("http")?e:`${ha}${e}`;if(t.params){let n=new URLSearchParams(Object.entries(t.params).map(([s,o])=>[s,String(o)])).toString();i+=(i.includes("?")?"&":"?")+n}return this.request(i,{method:t.method,body:t.body})}async login(){let e=await this.request(m6,{method:"POST",body:{Username:this.addition.email}});if(!e.Modulus||!e.ServerEphemeral)throw new Error("invalid auth info response");let t=await T6(this.addition.email,this.addition.password,e),i=await this.request(y6,{method:"POST",body:{Username:this.addition.email,ClientEphemeral:t.ClientEphemeral,ClientProof:t.ClientProof,SRPSession:t.SRPSession}});if(i.Code===1001||i.TwoFactor?.Enabled){if(!this.addition.two_fa_code)throw new Error("2FA is enabled but no two_fa_code provided");i=await this.request(I6,{method:"POST",body:{TwoFactorCode:this.addition.two_fa_code,SRPSession:t.SRPSession}})}if(!i.AccessToken||!i.UID)throw new Error(`auth failed: code=${i.Code}`);this.setAuth(i.AccessToken,i.RefreshToken,i.UID)}async refresh(){if(!this.refreshToken)throw new Error("refresh token is empty");let e=await this.request(w6,{method:"POST",body:{RefreshToken:this.refreshToken,ResponseType:"token",GrantType:"refresh_token",RedirectURI:"https://drive.proton.me"}});if(!e.AccessToken)throw new Error("refresh token failed");this.setAuth(e.AccessToken,e.RefreshToken||this.refreshToken,e.UID||this.uid)}async requestWithRetry(e,t){try{return await this.request(e,t)}catch(i){if(String(i.message).includes("401")&&this.refreshToken)return await this.refresh(),this.request(e,t);throw i}}async getSalts(){return(await this.requestWithRetry(x6,{})).KeySalts||[]}async getUser(){return(await this.requestWithRetry(C6,{})).User}async getAddresses(){return(await this.requestWithRetry(`${ha}/core/v4/addresses`,{})).Addresses||[]}async getShares(){return(await this.requestWithRetry(E6,{})).Shares||[]}}});function PN(r){return r?new Date(r*1e3).toISOString():new Date().toISOString()}function sne(r){let e={...r||{}};return e.email=(e.email||"").trim(),e.password=(e.password||"").trim(),e.two_fa_code=(e.two_fa_code||"").trim(),e.root_folder_id=(e.root_folder_id||"").trim()||"root",e.use_reusable_login=!!e.use_reusable_login,e.chunk_size=e.chunk_size||"4",e.reusable_credential=(e.reusable_credential||"").trim(),e}function one(){let r=crypto.getRandomValues(new Uint8Array(16));return btoa(String.fromCharCode(...r))}var FN,dw,TN=J(()=>{"use strict";Be();He();AB();_N();pb();Ab();FN=150;dw=class{addition;client;mainShare=null;mainShareKR=[];userKR=[];addrKRs=new Map;defaultAddrKR=[];defaultAddrID="";idCache=new Map;linkCache=new Map;nodeKRcache=new Map;childrenCache=new Map;constructor(e){this.addition=sne(e),this.client=new lw(this.addition)}async init(){if(!this.addition.email)throw new Error("email is required");if(!this.addition.password)throw new Error("password is required");await this.client.login();let e=await this.client.getSalts(),t=await this.client.getUser(),i=await this.client.getAddresses(),{userKR:n,saltedKeyPass:s}=await SN(t.Keys,this.addition.password,e);if(this.userKR=n,this.client.setSaltedKeyPass(s),this.addrKRs=await DN(i,this.userKR,s),this.addrKRs.size===0)throw new Error("failed to unlock address keys");this.defaultAddrID=i[0]?.ID||"",this.defaultAddrKR=this.addrKRs.get(this.defaultAddrID)||[];let o=await this.client.getShares();if(this.mainShare=o.find(a=>a.Type===S6)||o[0],!this.mainShare)throw new Error("no shares found");this.mainShareKR=await QN(this.mainShare,this.addrKRs),await this.getLinkKR(this.getRootLinkID())}getShareID(){if(!this.mainShare)throw new Error("not initialized");return this.mainShare.ShareID}getVolumeID(){if(!this.mainShare)throw new Error("not initialized");return this.mainShare.VolumeID}getRootLinkID(){if(!this.mainShare)throw new Error("not initialized");return this.addition.root_folder_id&&this.addition.root_folder_id!=="root"?this.addition.root_folder_id:this.mainShare.RootLinkID}async fetchLink(e){let t=this.linkCache.get(e);if(t)return t;let i=await this.client.requestAPI(`/drive/volumes/${this.getVolumeID()}/links/${e}`);if(!i.Link)throw new Error(`link not found: ${e}`);return this.linkCache.set(e,i.Link),i.Link}async getLinkKR(e){let t=this.nodeKRcache.get(e);if(t)return t;let i=await this.fetchLink(e),n=i.ParentLinkID?await this.getLinkKR(i.ParentLinkID):this.mainShareKR,s=await jd(i.NodePassphrase,n),o=await tw({armoredKey:i.NodeKey}),a=await rw({privateKey:o,passphrase:s}),c=[...n,a];return this.nodeKRcache.set(e,c),c}async decryptName(e){if(!e.Name)return"unnamed";let t=await this.getLinkKR(e.LinkID);try{return await jd(e.Name,t)}catch{return"unnamed"}}cleanPath(e){let t="/"+(e||"").split("/").filter(Boolean).join("/");return t==="/"?"/":t}joinPath(e,t){return e==="/"?`/${t}`:`${e}/${t}`}async listChildren(e){let t=this.childrenCache.get(e);if(t)return t;let i=[];for(let n=0;n<1e3;n++){let o=(await this.client.requestAPI(`/drive/volumes/${this.getVolumeID()}/links/${e}`,{params:{Children:"1",Page:String(n),PageSize:String(FN),Thumbnail:"0"}})).Links||[];if(i.push(...o),o.length<FN)break}return this.childrenCache.set(e,i),i}async resolveId(e){let t=this.cleanPath(e);if(t==="/")return this.getRootLinkID();let i=this.idCache.get(t);if(i)return i;let n=t.split("/").filter(Boolean),s=this.getRootLinkID(),o="/";for(let a of n){let c=await this.listChildren(s),l=null;for(let d of c)if(await this.decryptName(d)===a){l=d;break}if(!l)throw new Error(`folder not found: ${a}`);s=l.LinkID,o=this.joinPath(o,a),this.idCache.set(o,s)}return s}async list(e,t){let i=await this.resolveId(t),n=await this.listChildren(i),s=this.cleanPath(t),o=[];for(let a of n){let c=await this.decryptName(a);this.idCache.set(this.joinPath(s,c),a.LinkID);let l=a.Type===2;o.push({name:c,size:l?0:a.Size||0,is_dir:l,modified:PN(a.ModifyTime||a.CreateTime),sign:a.LinkID,type:X(c,l)})}return de(o,"name","asc")}async get(e,t){let i=this.cleanPath(t);if(i==="/")return{name:"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:this.getRootLinkID(),type:1};let n=await this.resolveId(i),s=await this.fetchLink(n),o=await this.decryptName(s),a=s.Type===2,c=0;if(!a){let l=await this.getLinkKR(n);c=await gb(s.XAttr||"",l),c||(c=s.Size||0)}return{name:o,size:c,is_dir:a,modified:PN(s.ModifyTime||s.CreateTime),sign:n,type:X(o,a),raw_url:""}}async createReadStream(e,t){let i=await this.resolveId(e),n=await this.fetchLink(i);if(n.Type===2)throw new Error("cannot stream a directory");let s=await this.getLinkKR(i),o=n.FileProperties?.ContentKeyPacket;if(!o)throw new Error("file has no content key packet");let a=await vN(o,s),c=await this.client.requestAPI(`/drive/volumes/${this.getVolumeID()}/links/${i}/revisions`),l=c.Revisions?.find(I=>I.State===1)||c.Revisions?.[0];if(!l?.Blocks?.length)throw new Error("file has no blocks");let d=l.Blocks,u=await gb(n.XAttr||"",s)||l.Size||0,f=(parseInt(this.addition.chunk_size||"4")||4)*1024*1024,h=t?.start??0,g=t?.end??(u>0?u-1:Number.MAX_SAFE_INTEGER),p=this;return new ReadableStream({async pull(I){try{let m=h;for(let w of d){if(m>g)break;let B=await p.downloadBlock(w.BareURL,w.Token),C=await kN(B,[a]),x=w.Index*f,Q=x+C.length-1,R=Math.max(h,x),U=Math.min(g,Q);R<=U&&(I.enqueue(C.slice(R-x,U-x+1)),m=U+1)}I.close()}catch(m){I.error(m)}}})}async downloadBlock(e,t){let i=await fetch(e,{headers:{Authorization:`Bearer ${t}`}});if(!i.ok)throw new Error(`block download failed: ${i.status}`);return new Uint8Array(await i.arrayBuffer())}async mkdir(e,t){let i=this.cleanPath(t),n=i.split("/").slice(0,-1).join("/")||"/",s=i.split("/").pop()||"",o=await this.resolveId(n),a=await this.getLinkKR(o),c=await cw(s,a),l=await hb(s),{publicKey:d}=await tb({type:"ecc",curve:"curve25519Legacy",userIDs:[{name:s,email:this.mainShare?.Creator||""}]}),u=one(),f=await cw(u,a);await this.client.requestAPI("/drive/shares/"+this.getShareID()+"/folders",{method:"POST",body:{ParentLinkID:o,Name:c,Hash:l,NodeKey:d,NodePassphrase:f,NodePassphraseSignature:"",SignatureAddress:this.defaultAddrID,NameSignatureEmail:this.mainShare?.Creator||""}})}async rename(e,t,i){let n=await this.resolveId(t),s=await this.fetchLink(n),o=await this.getLinkKR(s.ParentLinkID),a=await cw(i,o),c=await hb(i),l=s.Hash||"";await this.client.requestAPI(`/drive/volumes/${this.getVolumeID()}/links/${n}/rename`,{method:"PUT",body:{Name:a,NameSignatureEmail:this.mainShare?.Creator||"",Hash:c,OriginalHash:l}})}async remove(e,t,i){let n=await this.resolveId(t);await this.client.requestAPI(`/drive/volumes/${this.getVolumeID()}/links/${n}/trash`,{method:"POST"})}async move(e,t,i,n,s){throw new Error("ProtonDrive move is not supported yet")}async copy(e,t,i,n,s){throw new Error("ProtonDrive copy is not supported")}async put(e,t,i){throw new Error("ProtonDrive upload is not supported yet")}}});var UN={};Cn(UN,{LocalDriver:()=>mb});async function Na(){if(typeof process<"u"&&process.release?.name==="node"&&!zt)try{zt=await import("fs/promises"),ar=await import("path")}catch{}}var zt,ar,mb,RN=J(()=>{"use strict";Be();zt=null,ar=null;mb=class{async list(e,t){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");let i=[];try{i=await zt.readdir(t,{withFileTypes:!0})}catch{return[]}return await Promise.all(i.map(async s=>{let o=s.isDirectory(),a=0,c=new Date;try{let l=await zt.stat(ar.join(t,s.name));a=l.size,c=l.mtime}catch{}return{name:s.name,size:o?0:a,is_dir:o,created:c.toISOString(),modified:c.toISOString(),sign:"",type:X(s.name,o)}}))}async get(e,t){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");let i=await zt.stat(t),n=i.isDirectory(),s=t.split(/[\\/]+/).filter(Boolean).pop()||"root";return{name:s,size:n?0:i.size,is_dir:n,created:i.ctime?.toISOString()||i.mtime.toISOString(),modified:i.mtime.toISOString(),sign:"",type:X(s,n)}}async mkdir(e,t){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");await zt.mkdir(t,{recursive:!0})}async rename(e,t,i){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");let n=ar.join(ar.dirname(t),i);await zt.rename(t,n)}async remove(e,t,i){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");for(let n of i){let s=ar.join(t,n);await zt.rm(s,{recursive:!0,force:!0})}}async move(e,t,i,n,s){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");for(let o of i){let a=ar.join(n,o),c=ar.join(s,o);await zt.mkdir(ar.dirname(c),{recursive:!0}),await zt.rename(a,c)}}async copy(e,t,i,n,s){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");for(let o of i){let a=ar.join(n,o),c=ar.join(s,o);await zt.mkdir(ar.dirname(c),{recursive:!0}),await zt.cp(a,c,{recursive:!0})}}async put(e,t,i){if(await Na(),!zt||!ar)throw new Error("LocalDriver is not supported in Edge Runtime");await zt.mkdir(ar.dirname(t),{recursive:!0}),await zt.writeFile(t,i)}}});var NN={};Cn(NN,{Client:()=>fne,FTPDriver:()=>lne,SFTPClient:()=>dne,SFTPDriver:()=>ane,createPool:()=>Ane,default:()=>hne,normalizeSFTPAddition:()=>cne,parseAddress:()=>une});var ane,cne,lne,dne,une,fne,Ane,hne,qN=J(()=>{ane=class{constructor(){throw new Error("[Edge/Serverless] SFTP driver requires full Node.js runtime")}},cne=r=>r,lne=class{constructor(){throw new Error("[Edge/Serverless] FTP driver requires full Node.js runtime")}},dne=class{constructor(){throw new Error("[Edge/Serverless] SFTP client requires full Node.js runtime")}},une=()=>({host:"127.0.0.1",port:22}),fne=class{constructor(){throw new Error("[Edge/Serverless] ssh2 is not available in edge/serverless runtime")}},Ane=()=>{throw new Error("[Edge/Serverless] mysql2 is not available in edge/serverless runtime")},hne={}});var ON={};Cn(ON,{Client:()=>wne,FTPDriver:()=>mne,SFTPClient:()=>yne,SFTPDriver:()=>gne,createPool:()=>xne,default:()=>Cne,normalizeSFTPAddition:()=>pne,parseAddress:()=>Ine});var gne,pne,mne,yne,Ine,wne,xne,Cne,MN=J(()=>{gne=class{constructor(){throw new Error("[Edge/Serverless] SFTP driver requires full Node.js runtime")}},pne=r=>r,mne=class{constructor(){throw new Error("[Edge/Serverless] FTP driver requires full Node.js runtime")}},yne=class{constructor(){throw new Error("[Edge/Serverless] SFTP client requires full Node.js runtime")}},Ine=()=>({host:"127.0.0.1",port:22}),wne=class{constructor(){throw new Error("[Edge/Serverless] ssh2 is not available in edge/serverless runtime")}},xne=()=>{throw new Error("[Edge/Serverless] mysql2 is not available in edge/serverless runtime")},Cne={}});var Mi={};Cn(Mi,{copyItems:()=>NA,flushPendingDriverState:()=>Jt,getDriver:()=>lt,getItem:()=>_i,getOrCreateDriver:()=>KN,listItems:()=>Zt,makeDirectory:()=>UA,moveItems:()=>Vd,putItem:()=>Un,removeItems:()=>qa,renameItem:()=>RA,scheduleStoragePersistence:()=>GN});async function Ene(){if(!yb){let{LocalDriver:r}=await Promise.resolve().then(()=>(RN(),UN));yb=new r}return yb}async function Bne(r){if(typeof process<"u"&&process.release?.name==="node"){let{SFTPDriver:e}=await Promise.resolve().then(()=>(qN(),NN)),t=new e(ye(r));return await t.init?.(),t}throw new Error("SFTP storage driver requires Node.js runtime (raw TCP sockets not available in Cloudflare Workers)")}async function bne(r){if(typeof process<"u"&&process.release?.name==="node"){let{FTPDriver:e}=await Promise.resolve().then(()=>(MN(),ON)),t=new e(ye(r));return await t.init?.(),t}throw new Error("FTP storage driver requires Node.js runtime (raw TCP sockets not available in Cloudflare Workers)")}function Sne(r,e){if($d.size>=kne){let t=$d.keys().next().value;t!==void 0&&$d.delete(t)}$d.set(r,e)}async function KN(r,e,t){let i=r.get(e);if(i)return i;let n=t();r.set(e,n);try{return await n}catch(s){throw r.get(e)===n&&r.delete(e),s}}function ye(r){let e=r?.addition;return e?typeof e=="string"?JSON.parse(e||"{}"):e:{}}async function LN(r,e){let t=(r||"").toLowerCase().replace(/[^a-z0-9]/g,"");if(t==="local"){if(typeof process<"u"&&process.release?.name==="node")return Ene();throw new Error("Local storage driver requires Node.js runtime (not available in Cloudflare Workers)")}if(t==="sftp")return Bne(e);if(t==="ftp")return bne(e);if(!e)throw new Error("failed get driver: storage config not found for driver "+r);let i;if(t==="onedriveapp"){i=new Cg(ye(e));try{await i.init?.()}catch(n){throw console.error("onedrive_app init failed:",n),n}}else if(t==="onedrive"||t==="onedrivesb"||t==="onedrivebusiness"||t==="onedrivesharepoint"||t.startsWith("onedrive")&&t!=="onedriveapp"){i=new xg(ye(e),async n=>{try{let s=await ne(),o=(s.storages||[]).find(c=>c.id===e?.id);if(!o)return;let a=typeof o.addition=="string"?JSON.parse(o.addition||"{}"):o.addition||{};if(a.refresh_token=n,o.addition=JSON.stringify(a),e.addition=o.addition,Ib.has(e))return;await fe(s)}catch(s){console.warn("[Onedrive] failed to persist refresh token:",s)}});try{await i.init?.()}catch(n){throw console.error("onedrive init failed:",n),n}}else if(t==="aliyundrive"||t==="aliyundriveopen"||t==="aliyundriveshare"||t==="aliyun"||t==="aliyundriveshare2open"||t==="aliyundriveoauth2"||t.includes("aliyun"))i=new bg(ye(e)),await i.init?.();else if(t==="googlephoto"||t==="googlephotos"||t==="gphoto"||t==="google_photo")i=new gp(ye(e)),await i.init?.();else if(t==="googledrive"||t==="gdrive"||t==="google"||t.startsWith("google"))i=new kg(ye(e)),await i.init?.();else if(t==="quark"||t==="quarkuc"||t==="uc"||t==="quarkcookie")i=new Dg(ye(e)),await i.init?.();else if(t==="115"||t==="115cloud"||t==="115open"||t==="115netdisk")i=new _g(ye(e)),await i.init?.();else if(t==="cloudreve"||t==="cloudrevev3"||t==="cloudreve_v3"){let n=ye(e);i=new Ug(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.cookie=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[cloudreve] failed to persist cookie:",o)}}),await i.init?.()}else if(t==="cloudrevev4"||t==="cloudreve_v4")i=new Pg(ye(e)),await i.init?.();else if(t==="openlistshare"||t==="openlist_share")i=new Og(ye(e)),await i.init?.();else if(t==="openlist")i=new Ng(ye(e)),await i.init?.();else if(t==="teldrive")i=new Lg(ye(e)),await i.init?.();else if(t==="mediafire")i=new Kg(ye(e)),await i.init?.();else if(t==="githubreleases"||t==="github_releases")i=new zg(ye(e)),await i.init?.();else if(t==="cnbreleases"||t==="cnb_releases")i=new jg(ye(e)),await i.init?.();else if(t==="kodbox"||t==="kodo")i=new Vg(ye(e)),await i.init?.();else if(t==="ipfs"||t==="ipfsapi"||t==="ipfs_api")i=new Wg(ye(e)),await i.init?.();else if(t==="lenovonasshare"||t==="lenovonas_share")i=new Zg(ye(e)),await i.init?.();else if(t==="misskey")i=new tp(ye(e)),await i.init?.();else if(t==="doubao"||t==="doubaonew"||t==="doubao_new"||t==="doubaoshare"||t==="doubao_share")i=new ip(ye(e)),await i.init?.();else if(t==="quarkopen"||t==="quark_open"||t==="quarkoa")i=new sp(ye(e)),await i.init?.();else if(t==="quarktv"||t==="uctv"||t==="quark_uc_tv")i=new ap(ye(e)),await i.init?.();else if(t==="123open"||t==="123_open"||t==="123cloudopen")i=new lp(ye(e)),await i.init?.();else if(t==="teambition"||t==="tb")i=new up(ye(e)),await i.init?.();else if(t==="neteasemusic"||t==="netease"||t==="neteasecloudmusic"||t==="netease_music")i=new xp(ye(e)),await i.init?.();else if(t==="halalcloudopen"||t==="halalcloud_open"||t==="halalcloudopenapi"){let n=ye(e);i=new Bp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.refresh_token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[HalalCloud] failed to persist refresh_token:",o)}}),await i.init?.()}else if(t==="degoo"){let n=ye(e);i=new Ip(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};s.accessToken&&(c.access_token=s.accessToken),s.refreshToken&&(c.refresh_token=s.refreshToken),a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[degoo] failed to persist tokens:",o)}}),await i.init?.()}else if(t==="febbox"||t==="febboxpan"){let n=ye(e);i=new mp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.refresh_token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[febbox] failed to persist refresh_token:",o)}}),await i.init?.()}else if(t==="chaoxing"||t==="chaoxinggroupdrive"||t==="chaoxinggroup"||t.startsWith("chaoxing")){let n=ye(e);i=new Ap(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.cookie=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[chaoxing] failed to persist cookie:",o)}}),await i.init?.()}else if(t==="123pan"||t==="123"||t==="123panshare"||t.startsWith("123")){let n=ye(e);i=new vp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.access_token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[123Pan] failed to persist access_token:",o)}}),await i.init?.()}else if(t==="baiduphoto"||t==="baidu_photo"||t==="baiduphotos")i=new Fp(ye(e)),await i.init?.();else if(t==="baidunetdisk"||t==="baidu"||t==="baiduyun"||t==="baidushare"||t.startsWith("baidu")){let n=ye(e);i=new Sp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.access_token=s.access_token,c.refresh_token=s.refresh_token,a.addition=JSON.stringify(Kl(c)),await fe(o)}catch(o){console.warn("[baidu_netdisk] failed to persist token:",o)}}),await i.init?.()}else if(t==="115open"||t==="115"||t==="115pan"||t==="115cloud"||t.startsWith("115")){let n=ye(e);i=new Up(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.access_token=s.access_token,c.refresh_token=s.refresh_token,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[115open] failed to persist token:",o)}}),await i.init?.()}else if(t==="github"||t==="githubapi"||t==="github_api"){let n=ye(e);i=new Np(n),await i.init?.()}else if(t==="thunderexpert"||t==="thunderbrowserexpert"||t==="thunderxexpert"||t.includes("thunder")&&t.includes("expert")||t.includes("xunlei")&&t.includes("expert")){let n=ye(e);i=new Op(n,async s=>{try{s.device_id&&(n.device_id=s.device_id),s.refresh_token&&(n.refresh_token=s.refresh_token),s.captcha_token&&(n.captcha_token=s.captcha_token),e.addition=JSON.stringify(n);let o=await ne(),a=(o.storages||[]).find(c=>c.id===e?.id);if(a){let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};s.refresh_token&&(c.refresh_token=s.refresh_token),s.captcha_token&&(c.captcha_token=s.captcha_token),s.device_id&&(c.device_id=s.device_id),a.addition=JSON.stringify(c),await fe(o)}}catch(o){console.warn("[thunderexpert] failed to persist token:",o)}}),await i.init?.()}else if(t==="thunder"||t==="xunlei"||t==="thunderbrowser"||t==="thunderx"||t.includes("thunder")||t.includes("xunlei")){let n=ye(e);i=new lf(n,async s=>{try{s.device_id&&(n.device_id=s.device_id),s.refresh_token&&(n.refresh_token=s.refresh_token),s.captcha_token&&(n.captcha_token=s.captcha_token),e.addition=JSON.stringify(n);let o=await ne(),a=(o.storages||[]).find(c=>c.id===e?.id);if(a){let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};s.refresh_token&&(c.refresh_token=s.refresh_token),s.captcha_token&&(c.captcha_token=s.captcha_token),s.device_id&&(c.device_id=s.device_id),a.addition=JSON.stringify(c),await fe(o)}}catch(o){console.warn("[thunder] failed to persist token:",o)}}),await i.init?.()}else if(t==="lanzou"||t==="lanzoupan"||t==="ilanzou"||t==="lanzoui"||t==="lanzous"){let n=ye(e);i=new zp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.cookie=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[Lanzou] failed to persist cookie:",o)}}),await i.init?.()}else if(t==="189tv"||t==="cloud189tv"||t==="189tvcloud"||t==="189_tv"){let n=ye(e);i=new Wp(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.access_token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[189TV] failed to persist access_token:",o)}}),await i.init?.()}else if(t==="189"||t==="189cloud"||t==="cloud189"||t==="ctyun"||t==="189pan"||t==="189cloudpc"||t==="189cloudapp"||t.startsWith("189")||t.includes("cloud189")){let n=ye(e);i=new Vp(n),await i.init?.()}else if(t==="webdav"||t==="webdavdriver"){let n=ye(e);i=new Zp(n),await i.init?.()}else if(t==="s3"||t==="doge"||t==="dogecloud"||t==="minio"||t==="ceph"||t==="aws"||t==="r2"||t==="b2"||t==="cos"||t==="oss"||t==="kodo"){let n=ye(e);i=new um(n,e.driver||"S3"),await i.init?.()}else if(t==="wopan"||t==="unicom"||t==="unicomcloud"||t==="woyun"||t==="chinaunicom"){let n=ye(e);i=new nm(n,async(s,o)=>{try{let a=await ne(),c=(a.storages||[]).find(d=>d.id===e?.id);if(!c)return;let l=typeof c.addition=="string"?JSON.parse(c.addition||"{}"):c.addition||{};l.access_token=s,l.refresh_token=o,c.addition=JSON.stringify(kE(l)),await fe(a)}catch(a){console.warn("[WoPan] failed to persist tokens:",a)}}),await i.init?.()}else if(t==="weiyun"||t==="tencentweiyun"||t==="txweiyun"||t.includes("weiyun")){let n=ye(e);i=new gm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.cookies=s,a.addition=JSON.stringify(RE(c)),await fe(o)}catch(o){console.warn("[WeiYun] failed to persist cookies:",o)}}),await i.init?.()}else if(t==="pikpak"||t==="pikpakshare"||t.includes("pikpak")){let n=ye(e);i=new mm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.refresh_token=s.refreshToken,s.captchaToken&&(c.captcha_token=s.captchaToken),a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[PikPak] failed to persist tokens:",o)}}),await i.init?.()}else if(t==="seafile"||t.includes("seafile")){let n=ye(e);i=new Im(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[Seafile] failed to persist token:",o)}}),await i.init?.()}else if(t==="yandex"||t==="yandexdisk"||t==="yandexdrive"||t.includes("yandex")){let n=ye(e);i=new xm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.refresh_token=s.refreshToken,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[Yandex] failed to persist token:",o)}}),await i.init?.()}else if(t==="terabox"||t==="dubox"||t.includes("terabox")){let n=ye(e);i=new Em(n),await i.init?.()}else if(t==="mediatrack"||t==="fenmiao"||t.includes("mediatrack")){let n=ye(e);i=new bm(n),await i.init?.()}else if(t==="alias"||t.includes("alias")){let n=ye(e);i=new vm(n),await i.init?.()}else if(t==="dropbox"||t.includes("dropbox")){let n=ye(e);i=new Sm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.access_token=s.accessToken,c.refresh_token=s.refreshToken,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[Dropbox] failed to persist token:",o)}}),await i.init?.()}else if(t==="wps"||t.includes("wps")||t.includes("kdocs")){let n=ye(e);i=new Qm(n),await i.init?.()}else if(t==="mopan"||t==="mobilecloud"||t==="cmcc"||t==="chinamobile"||t.includes("mopan")){let n=ye(e);i=new am(n,async(s,o)=>{try{let a=await ne(),c=(a.storages||[]).find(u=>u.id===e?.id);if(!c)return;let l=typeof c.addition=="string"?JSON.parse(c.addition||"{}"):c.addition||{};l.device_info=s;let d=o.replace(/^Bearer\s+/i,"");l.token=d,c.addition=JSON.stringify(l),await fe(a)}catch(a){console.warn("[MoPan] failed to persist tokens:",a)}}),await i.init?.()}else if(t==="139"||t==="139yun"||t==="caiyun"||t==="hecaiyun"||t.includes("139")){let n=ye(e);i=new Fm(n),await i.init?.()}else if(t==="mega"||t==="meganz"||t.includes("mega")){let n=ye(e);i=new Tm(n),await i.init?.()}else if(t==="115share"||t==="115sharelink"||t.includes("115")&&t.includes("share")){let n=ye(e);i=new Rm(n),await i.init?.()}else if(t==="123share"||t==="123panshare"||t==="123link"||t.includes("123")&&t.includes("share")){let n=ye(e);i=new qm(n),await i.init?.()}else if(t==="aliyundriveshare"||t==="aliyunshare"||t==="alishare"||t.includes("ali")&&t.includes("share")){let n=ye(e);i=new Mm(n),await i.init?.()}else if(t==="onedrivesharelink"||t==="onedriveshare"||t==="sharepointshare"||t.includes("onedrive")&&t.includes("share")){let n=ye(e);i=new Hm(n),await i.init?.()}else if(t==="pikpakshare"||t.includes("pikpak")&&t.includes("share")){let n=ye(e);i=new Gm(n),await i.init?.()}else if(t==="smb"||t==="samba"||t==="cifs"||t.includes("smb")){let n=ye(e);i=new zm(n),await i.init?.()}else if(t==="crypt"){let n=ye(e);i=new Vm(n),await i.init?.()}else if(t==="virtual"){let n=ye(e);i=new Ym(n)}else if(t==="alistv3"||t==="alist"||t==="alistv2"){let n=ye(e);i=new Wm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.token=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[alist_v3] failed to persist token:",o)}}),await i.init?.()}else if(t==="urltree"||t==="urlstree"){let n=ye(e);i=new Xm(n,async s=>{try{let o=await ne(),a=(o.storages||[]).find(l=>l.id===e?.id);if(!a)return;let c=typeof a.addition=="string"?JSON.parse(a.addition||"{}"):a.addition||{};c.url_structure=s,a.addition=JSON.stringify(c),await fe(o)}catch(o){console.warn("[url_tree] failed to persist structure:",o)}}),await i.init?.()}else if(t==="strm"){let n=ye(e);i=new Zm(n),await i.init?.()}else if(t==="chunk"){let n=ye(e);i=new ey(n),await i.init?.()}else if(t==="azureblob"||t==="azure"||t==="azblob"){let n=ye(e);i=new ty(n),await i.init?.()}else if(t==="uss"||t==="upyun"){let n=ye(e);i=new ry(n),await i.init?.()}else if(t==="autoindex"||t==="nginxautoindex"){let n=ye(e);i=new Fy(n),await i.init?.()}else if(t==="protondrive"||t==="proton"){let n=ye(e);i=new dw(n),await i.init?.()}else if(t==="alidoc"||t==="dingtalkdoc"){let n=ye(e);i=new ny(n),await i.init?.()}else if(t==="emby"||t==="jellyfin"){let n=ye(e);i=new oy(n),await i.init?.()}else if(t==="bunnystorage"||t==="bunnycdn"||t==="bunny"){let n=ye(e);i=new ly(n),await i.init?.()}else if(t==="cloudflareimgbed"||t==="cfimgbed"||t==="cloudflareimage"){let n=ye(e);i=new fy(n),await i.init?.()}else if(t==="guangyapan"||t==="guangya"){let n=ye(e);i=new Ay(n,async(s,o)=>{try{let a=await ne(),c=(a.storages||[]).find(d=>d.id===e?.id);if(!c)return;let l=typeof c.addition=="string"?JSON.parse(c.addition||"{}"):c.addition||{};l.access_token=s,o&&(l.refresh_token=o),c.addition=JSON.stringify(l),await fe(a)}catch(a){console.warn("[GuangYaPan] failed to persist tokens:",a)}}),await i.init?.()}else throw new Error("failed get driver: unsupported driver '"+r+"'");return i}async function lt(r,e,t={}){let i=!!(t.deferTokenPersistence&&e&&typeof e=="object");i&&Ib.add(e);try{if((r||"").toLowerCase().replace(/[^a-z0-9]/g,"")==="local")return LN(r,e);if(!e)throw new Error("failed get driver: storage config not found for driver "+r);let s=`${e.id}_${e.modified}`,o=$d.get(s);return o||KN(vne,s,async()=>{let a=$d.get(s);if(a)return a;let c=await LN(r,e);return Sne(s,c),c})}finally{i&&Ib.delete(e)}}function Dne(r){let e=(r||"").toLowerCase().replace(/[^a-z0-9]/g,"");return e==="189"||e==="189cloud"||e==="cloud189"||e==="ctyun"||e==="189pan"}async function GN(r,e){if(r)try{r(e);return}catch{}await e}async function Qne(r,e){let t=String(r?.id||"");if(!t)return;let n=(uw.get(t)||Promise.resolve()).catch(()=>{}).then(async()=>{let s=await ne(),o=(s.storages||[]).find(c=>String(c.id)===t);if(!o)return;let a=typeof o.addition=="string"?JSON.parse(o.addition||"{}"):o.addition||{};a.cookies!==void 0||zN(r?.driver)?a.cookies=e:a.cookie=e,o.addition=JSON.stringify(a),String(r?.id)===t&&(r.addition=o.addition),await fe(s)});uw.set(t,n);try{await n}finally{uw.get(t)===n&&uw.delete(t)}}function zN(r){let e=(r||"").toLowerCase().replace(/[^a-z0-9]/g,"");return e==="weiyun"||e==="tencentweiyun"||e==="txweiyun"||e.includes("weiyun")}async function Jt(r,e,t,i){if(!Dne(r)&&!zN(r))return;let s=t.consumePendingCookie?.call(t);if(!s)return;let o=Qne(e,s).catch(a=>{console.warn(`[${r}] failed to persist cookie:`,a)});await GN(i?.waitUntil,o)}async function Zt(r,e){let t=await ct(r,e?.env),i=[],n="Virtual";if(t.storage){n=t.storage.driver;try{let l=await lt(n,t.storage);try{i=await l.list(r,t.physical)}finally{await Jt(n,t.storage,l,e)}if(t.storage.status!=="work"){t.storage.status="work";let d=await ne(e?.env),u=(d.storages||[]).find(f=>f.id===t.storage?.id);u&&(u.status="work",await fe(d,e?.env))}}catch(l){try{let d=await ne(e?.env),u=(d.storages||[]).find(f=>f.id===t.storage?.id);u&&(u.status=l.message||String(l),await fe(d,e?.env))}catch(d){console.warn("Failed to persist storage status:",d)}throw l}}else if(!t.isVirtual)throw new Error("failed get storage: storage not found");let o=((await ne(e?.env)).storages||[]).filter(l=>!l.disabled),a=t.cleanPath;return[...o].sort((l,d)=>{let u=Number(l.order)||0,f=Number(d.order)||0;if(u!==f)return u-f;let h=String(l.mount_path||""),g=String(d.mount_path||"");return h<g?-1:h>g?1:0}).forEach(l=>{let d="/"+(l.mount_path||"").split("/").filter(Boolean).join("/");if(d===a||d==="/")return;let u=a==="/"?"/":a+"/";if(d.startsWith(u)){let f=d.slice(u.length).split("/").filter(Boolean)[0];f&&!i.some(h=>h.name===f)&&i.push({name:f,size:0,is_dir:!0,modified:l.modified||new Date().toISOString(),sign:"",type:1})}}),i.forEach(l=>{l.type||(l.type=X(l.name,l.is_dir))}),{content:i,provider:n,storage:t.storage}}async function HN(r,e){try{let t=await Vn().catch(()=>({}));return wg({storage:r,driver:r?.driver||"",filename:e,proxyTypes:Ic(t.proxy_types),textTypes:Ic(t.text_types)})?"/api/p":"/api/d"}catch{return"/api/p"}}async function _i(r,e){let t=await ct(r,e?.env);if(t.isVirtual)return{item:{name:t.cleanPath.split("/").filter(Boolean).pop()||"root",size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",type:1},provider:"Virtual",rawUrl:""};if(t.storage&&t.relative==="/"){let o=t.cleanPath.split("/").filter(Boolean).pop()||"root",a=ye(t.storage);return{item:{name:o,size:0,is_dir:!0,modified:t.storage.modified||new Date().toISOString(),sign:String(a.root_folder_id||""),type:1,raw_url:""},provider:t.storage.driver,rawUrl:`${await HN(t.storage,r)}${Os(r)}`}}let i=t.storage?t.storage.driver:"Local",n=await lt(i,t.storage),s;try{s=await n.get(r,t.physical)}finally{await Jt(i,t.storage,n,e)}return s.type||(s.type=X(s.name,s.is_dir)),{item:s,provider:i,rawUrl:`${await HN(t.storage,r)}${Os(r)}`}}async function UA(r,e){let t=await ct(r,e?.env);if(t.isVirtual)throw new Error("failed get storage: storage not found");let i=await lt(t.storage.driver,t.storage);try{await i.mkdir(r,t.physical)}finally{await Jt(t.storage.driver,t.storage,i,e)}}async function RA(r,e,t){let i=await ct(r,t?.env);if(i.isVirtual)throw new Error("failed get storage: storage not found");let n=await lt(i.storage.driver,i.storage);try{await n.rename(r,i.physical,e)}finally{await Jt(i.storage.driver,i.storage,n,t)}}async function qa(r,e,t){for(let i of e){let n=`${r}/${i}`,s=await ct(n,t?.env);if(s.isVirtual)throw new Error("failed get storage: storage not found");let o=await lt(s.storage.driver,s.storage);try{await o.remove(n,s.physical,[i])}finally{await Jt(s.storage.driver,s.storage,o,t)}}}async function Vd(r,e,t,i){for(let n of t){let s=`${r}/${n}`,o=`${e}/${n}`,a=await ct(s,i?.env),c=await ct(o,i?.env);if(a.isVirtual||c.isVirtual)throw new Error("failed get storage: storage not found");let l=await lt(a.storage.driver,a.storage);try{await l.move(r,e,[n],a.physical,c.physical)}finally{await Jt(a.storage.driver,a.storage,l,i)}}}async function NA(r,e,t,i){for(let n of t){let s=`${r}/${n}`,o=`${e}/${n}`,a=await ct(s,i?.env),c=await ct(o,i?.env);if(a.isVirtual||c.isVirtual)throw new Error("failed get storage: storage not found");let l=await lt(a.storage.driver,a.storage);try{await l.copy(r,e,[n],a.physical,c.physical)}finally{await Jt(a.storage.driver,a.storage,l,i)}}}async function Un(r,e,t){let i=await ct(r,t?.env);if(i.isVirtual)throw new Error("failed get storage: storage not found");let n=await lt(i.storage.driver,i.storage);try{await n.put(r,i.physical,e)}finally{await Jt(i.storage.driver,i.storage,n,t)}}var yb,$d,vne,uw,Ib,kne,Yt=J(()=>{"use strict";Ye();Nl();ef();Be();vQ();TQ();qQ();GQ();VQ();e_();i_();o_();c_();f_();g_();m_();I_();x_();E_();b_();k_();D_();P_();R_();O_();H_();G_();Y_();Z_();r8();s8();m8();S8();T8();M8();J8();Y8();Z8();d4();p4();v4();F4();R4();iF();gF();EF();FF();UF();NF();OF();LF();KF();GF();JF();VF();WF();ZF();tP();nP();oP();cP();dP();uP();gP();pP();mP();IP();EP();BP();kP();SP();_P();TP();MP();zP();ZP();p6();TN();yb=null;$d=new Map,vne=new Map,uw=new Map,Ib=new WeakSet,kne=100});var jN,_ne,Fne,Pne,Tne,JN,fw,wb,$N=J(()=>{sc();jN={name:"HMAC",hash:"SHA-256"},_ne=async r=>{let e=typeof r=="string"?new TextEncoder().encode(r):r;return await crypto.subtle.importKey("raw",e,jN,!1,["sign","verify"])},Fne=async(r,e,t)=>{try{let i=atob(r),n=new Uint8Array(i.length);for(let s=0,o=i.length;s<o;s++)n[s]=i.charCodeAt(s);return await crypto.subtle.verify(jN,t,n,new TextEncoder().encode(e))}catch{return!1}},Pne=/^[!#-:<>-[\]-~]+$/,Tne=/^[ !#-:<-[\]-~]*$/,JN=r=>{let e=0,t=r.length;for(;e<t;){let i=r.charCodeAt(e);if(i!==32&&i!==9)break;e++}for(;t>e;){let i=r.charCodeAt(t-1);if(i!==32&&i!==9)break;t--}return e===0&&t===r.length?r:r.slice(e,t)},fw=(r,e)=>{if(e&&r.indexOf(e)===-1)return{};let t=r.split(";"),i=Object.create(null);for(let n of t){let s=n.indexOf("=");if(s===-1)continue;let o=JN(n.substring(0,s));if(e&&e!==o||!Pne.test(o)||o in i)continue;let a=JN(n.substring(s+1));if(a.startsWith('"')&&a.endsWith('"')&&(a=a.slice(1,-1)),Tne.test(a)&&(i[o]=vl(a),e))break}return i},wb=async(r,e,t)=>{let i=Object.create(null),n=await _ne(e);for(let[s,o]of Object.entries(fw(r,t))){let a=o.lastIndexOf(".");if(a<1)continue;let c=o.substring(0,a),l=o.substring(a+1);if(l.length!==44||!l.endsWith("="))continue;let d=await Fne(l,c,n);i[s]=d?c:!1}return i}});var Aw,xb,VN=J(()=>{$N();Aw=(r,e,t)=>{let i=r.req.raw.headers.get("Cookie");if(typeof e=="string"){if(!i)return;let s=e;return t==="secure"?s="__Secure-"+e:t==="host"&&(s="__Host-"+e),fw(i,s)[s]}return i?fw(i):{}},xb=async(r,e,t,i)=>{let n=r.req.raw.headers.get("Cookie");if(typeof t=="string"){if(!n)return;let o=t;return i==="secure"?o="__Secure-"+t:i==="host"&&(o="__Host-"+t),(await wb(n,e,o))[o]}return n?await wb(n,e):{}}});var Cb,Eb,Une,Bb,bb=J(()=>{Cb=r=>Bb(r.replace(/_|-/g,e=>({_:"/","-":"+"})[e]??e)),Eb=r=>Une(r).replace(/\/|\+/g,e=>({"/":"_","+":"-"})[e]??e),Une=r=>{let e="",t=new Uint8Array(r);for(let i=0,n=t.length;i<n;i++)e+=String.fromCharCode(t[i]);return btoa(e)},Bb=r=>{let e=atob(r),t=new Uint8Array(new ArrayBuffer(e.length)),i=e.length/2;for(let n=0,s=e.length-1;n<=i;n++,s--)t[n]=e.charCodeAt(n),t[s]=e.charCodeAt(s);return t}});var Oa,vb=J(()=>{Oa=(r=>(r.HS256="HS256",r.HS384="HS384",r.HS512="HS512",r.RS256="RS256",r.RS384="RS384",r.RS512="RS512",r.PS256="PS256",r.PS384="PS384",r.PS512="PS512",r.ES256="ES256",r.ES384="ES384",r.ES512="ES512",r.EdDSA="EdDSA",r))(Oa||{})});var Rne,YN,Nne,WN=J(()=>{Rne={deno:"Deno",bun:"Bun",workerd:"Cloudflare-Workers",node:"Node.js"},YN=()=>{let r=globalThis;if(typeof navigator<"u"&&typeof navigator.userAgent=="string"){for(let[t,i]of Object.entries(Rne))if(Nne(i))return t}return typeof r?.EdgeRuntime=="string"?"edge-light":r?.fastly!==void 0?"fastly":r?.process?.release?.name==="node"?"node":"other"},Nne=r=>navigator.userAgent.startsWith(r)});var XN,kb,Sb,nl,ZN,e9,t9,hw,Db,r9,i9,n9,s9,o9,a9,Wd,Qb=J(()=>{XN=class extends Error{constructor(r){super(`${r} is not an implemented algorithm`),this.name="JwtAlgorithmNotImplemented"}},kb=class extends Error{constructor(){super('JWT verification requires "alg" option to be specified'),this.name="JwtAlgorithmRequired"}},Sb=class extends Error{constructor(r,e){super(`JWT algorithm mismatch: expected "${r}", got "${e}"`),this.name="JwtAlgorithmMismatch"}},nl=class extends Error{constructor(r){super(`invalid JWT token: ${r}`),this.name="JwtTokenInvalid"}},ZN=class extends Error{constructor(r){super(`token (${r}) is being used before it's valid`),this.name="JwtTokenNotBefore"}},e9=class extends Error{constructor(r){super(`token (${r}) expired`),this.name="JwtTokenExpired"}},t9=class extends Error{constructor(r,e){super(`Invalid "iat" claim, must be a valid number lower than "${r}" (iat: "${e}")`),this.name="JwtTokenIssuedAt"}},hw=class extends Error{constructor(r,e){super(`expected issuer "${r}", got ${e?`"${e}"`:"none"} `),this.name="JwtTokenIssuer"}},Db=class extends Error{constructor(r){super(`jwt header is invalid: ${JSON.stringify(r)}`),this.name="JwtHeaderInvalid"}},r9=class extends Error{constructor(r){super(`required "kid" in jwt header: ${JSON.stringify(r)}`),this.name="JwtHeaderRequiresKid"}},i9=class extends Error{constructor(r){super(`symmetric algorithm "${r}" is not allowed for JWK verification`),this.name="JwtSymmetricAlgorithmNotAllowed"}},n9=class extends Error{constructor(r,e){super(`algorithm "${r}" is not in the allowed list: [${e.join(", ")}]`),this.name="JwtAlgorithmNotAllowed"}},s9=class extends Error{constructor(r){super(`token(${r}) signature mismatched`),this.name="JwtTokenSignatureMismatched"}},o9=class extends Error{constructor(r){super(`required "aud" in jwt payload: ${JSON.stringify(r)}`),this.name="JwtPayloadRequiresAud"}},a9=class extends Error{constructor(r,e){super(`expected audience "${Array.isArray(r)?r.join(", "):r}", got "${e}"`),this.name="JwtTokenAudience"}},Wd=(r=>(r.Encrypt="encrypt",r.Decrypt="decrypt",r.Sign="sign",r.Verify="verify",r.DeriveKey="deriveKey",r.DeriveBits="deriveBits",r.WrapKey="wrapKey",r.UnwrapKey="unwrapKey",r))(Wd||{})});var sl,c9,_b=J(()=>{sl=new TextEncoder,c9=new TextDecoder});async function d9(r,e,t){let i=f9(e),n=await qne(r,i);return await crypto.subtle.sign(i,n,t)}async function u9(r,e,t,i){let n=f9(e),s=await One(r,n);return await crypto.subtle.verify(n,s,t,i)}function Fb(r){return Bb(r.replace(/-+(BEGIN|END).*?-+/g,"").replace(/\s/g,""))}async function qne(r,e){if(!crypto.subtle||!crypto.subtle.importKey)throw new Error("`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.");if(A9(r)){if(r.type!=="private"&&r.type!=="secret")throw new Error(`unexpected key type: CryptoKey.type is ${r.type}, expected private or secret`);return r}let t=[Wd.Sign];return typeof r=="object"?await crypto.subtle.importKey("jwk",r,e,!1,t):r.includes("PRIVATE")?await crypto.subtle.importKey("pkcs8",Fb(r),e,!1,t):await crypto.subtle.importKey("raw",sl.encode(r),e,!1,t)}async function One(r,e){if(!crypto.subtle||!crypto.subtle.importKey)throw new Error("`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.");if(A9(r)){if(r.type==="public"||r.type==="secret")return r;r=await l9(r)}if(typeof r=="string"&&r.includes("PRIVATE")){let i=await crypto.subtle.importKey("pkcs8",Fb(r),e,!0,[Wd.Sign]);r=await l9(i)}let t=[Wd.Verify];return typeof r=="object"?await crypto.subtle.importKey("jwk",r,e,!1,t):r.includes("PUBLIC")?await crypto.subtle.importKey("spki",Fb(r),e,!1,t):await crypto.subtle.importKey("raw",sl.encode(r),e,!1,t)}async function l9(r){if(r.type!=="private")throw new Error(`unexpected key type: ${r.type}`);if(!r.extractable)throw new Error("unexpected private key is unextractable");let e=await crypto.subtle.exportKey("jwk",r),{kty:t}=e,{alg:i,e:n,n:s}=e,{crv:o,x:a,y:c}=e;return{kty:t,alg:i,e:n,n:s,crv:o,x:a,y:c,key_ops:[Wd.Verify]}}function f9(r){switch(r){case"HS256":return{name:"HMAC",hash:{name:"SHA-256"}};case"HS384":return{name:"HMAC",hash:{name:"SHA-384"}};case"HS512":return{name:"HMAC",hash:{name:"SHA-512"}};case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:{name:"SHA-256"}};case"RS384":return{name:"RSASSA-PKCS1-v1_5",hash:{name:"SHA-384"}};case"RS512":return{name:"RSASSA-PKCS1-v1_5",hash:{name:"SHA-512"}};case"PS256":return{name:"RSA-PSS",hash:{name:"SHA-256"},saltLength:32};case"PS384":return{name:"RSA-PSS",hash:{name:"SHA-384"},saltLength:48};case"PS512":return{name:"RSA-PSS",hash:{name:"SHA-512"},saltLength:64};case"ES256":return{name:"ECDSA",hash:{name:"SHA-256"},namedCurve:"P-256"};case"ES384":return{name:"ECDSA",hash:{name:"SHA-384"},namedCurve:"P-384"};case"ES512":return{name:"ECDSA",hash:{name:"SHA-512"},namedCurve:"P-521"};case"EdDSA":return{name:"Ed25519",namedCurve:"Ed25519"};default:throw new XN(r)}}function A9(r){return YN()==="node"&&crypto.webcrypto?r instanceof crypto.webcrypto.CryptoKey:r instanceof CryptoKey}var h9=J(()=>{WN();bb();Qb();_b()});function g9(r){if(typeof r=="object"&&r!==null){let e=r;return"alg"in e&&Object.values(Oa).includes(e.alg)&&(!("typ"in e)||e.typ==="JWT")}return!1}var Pb,Mne,Tb,p9,Ub,Lne,m9,Rb,Hne,y9=J(()=>{bb();vb();h9();Qb();_b();Pb=r=>Eb(sl.encode(JSON.stringify(r)).buffer).replace(/=/g,""),Mne=r=>Eb(r).replace(/=/g,""),Tb=r=>JSON.parse(c9.decode(Cb(r)));p9=async(r,e,t="HS256")=>{let i=Pb(r),n;typeof e=="object"&&"alg"in e?(t=e.alg,n=Pb({alg:t,typ:"JWT",kid:e.kid})):n=Pb({alg:t,typ:"JWT"});let s=`${n}.${i}`,o=await d9(e,t,sl.encode(s)),a=Mne(o);return`${s}.${a}`},Ub=async(r,e,t)=>{if(!t)throw new kb;let{alg:i,iss:n,nbf:s=!0,exp:o=!0,iat:a=!0,aud:c}=typeof t=="string"?{alg:t}:t;if(!i)throw new kb;let l=r.split(".");if(l.length!==3)throw new nl(r);let{header:d,payload:u}=Rb(r);if(!g9(d))throw new Db(d);if(d.alg!==i)throw new Sb(i,d.alg);let f=Math.floor(Date.now()/1e3);if(s&&u.nbf!==void 0&&(typeof u.nbf!="number"||!Number.isFinite(u.nbf)||u.nbf>f))throw new ZN(r);if(o&&u.exp!==void 0&&(typeof u.exp!="number"||!Number.isFinite(u.exp)||u.exp<=f))throw new e9(r);if(a&&u.iat!==void 0&&(typeof u.iat!="number"||!Number.isFinite(u.iat)||f<u.iat))throw new t9(f,u.iat);if(n){if(!u.iss)throw new hw(n,null);if(typeof n=="string"&&u.iss!==n)throw new hw(n,u.iss);if(n instanceof RegExp&&!n.test(u.iss))throw new hw(n,u.iss)}if(c){if(!u.aud)throw new o9(u);if(!(Array.isArray(u.aud)?u.aud:[u.aud]).some(m=>c instanceof RegExp?c.test(m):typeof c=="string"?m===c:Array.isArray(c)&&c.includes(m)))throw new a9(c,u.aud)}let h=r.substring(0,r.lastIndexOf("."));if(!await u9(e,i,Cb(l[2]),sl.encode(h)))throw new s9(r);return u},Lne=[Oa.HS256,Oa.HS384,Oa.HS512],m9=async(r,e,t)=>{let i=e.verification||{},n=Hne(r);if(!g9(n))throw new Db(n);if(!n.kid)throw new r9(n);if(Lne.includes(n.alg))throw new i9(n.alg);if(!e.allowedAlgorithms.includes(n.alg))throw new n9(n.alg,e.allowedAlgorithms);let s=e.keys?[...e.keys]:void 0;if(e.jwks_uri){let a=await fetch(e.jwks_uri,t);if(!a.ok)throw new Error(`failed to fetch JWKS from ${e.jwks_uri}`);let c=await a.json();if(!c.keys)throw new Error('invalid JWKS response. "keys" field is missing');if(!Array.isArray(c.keys))throw new Error('invalid JWKS response. "keys" field is not an array');s??=[],s.push(...c.keys)}else if(!s)throw new Error('verifyWithJwks requires options for either "keys" or "jwks_uri" or both');let o=s.find(a=>a.kid===n.kid);if(!o)throw new nl(r);if(o.alg&&o.alg!==n.alg)throw new Sb(o.alg,n.alg);return await Ub(r,o,{alg:n.alg,...i})},Rb=r=>{let e=r.split(".");if(e.length!==3)throw new nl(r);try{let t=Tb(e[0]),i=Tb(e[1]);return{header:t,payload:i}}catch{throw new nl(r)}},Hne=r=>{let e=r.split(".");if(e.length!==3)throw new nl(r);try{return Tb(e[0])}catch{throw new nl(r)}}});var Xd,I9=J(()=>{y9();Xd={sign:p9,verify:Ub,decode:Rb,verifyWithJwks:m9}});function Nb(r){let e=(r.realm??r.ctx.req.url).replace(/"/g,'\\"'),t=r.errDescription.replace(/"/g,'\\"');return new Response("Unauthorized",{status:401,statusText:r.statusText,headers:{"WWW-Authenticate":`Bearer realm="${e}",error="${r.error}",error_description="${t}"`}})}var w9,x9,mo,C9,yo,E9=J(()=>{VN();_x();I9();$0();w9=r=>{let e=r.verification||{};if(!r||!r.secret)throw new Error('JWT auth middleware requires options for "secret"');if(!r.alg)throw new Error('JWT auth middleware requires options for "alg"');if(!crypto.subtle||!crypto.subtle.importKey)throw new Error("`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.");return async function(i,n){let s=r.headerName||"Authorization",o=i.req.raw.headers.get(s),a;if(o){let d=o.split(/\s+/);if(d.length!==2||d[0].toLowerCase()!=="bearer"){let u="invalid credentials structure";throw new Fu(401,{message:u,res:Nb({ctx:i,error:"invalid_request",errDescription:u,realm:r.realm})})}else a=d[1]}else r.cookie&&(typeof r.cookie=="string"?a=Aw(i,r.cookie):r.cookie.secret?r.cookie.prefixOptions?a=await xb(i,r.cookie.secret,r.cookie.key,r.cookie.prefixOptions):a=await xb(i,r.cookie.secret,r.cookie.key):r.cookie.prefixOptions?a=Aw(i,r.cookie.key,r.cookie.prefixOptions):a=Aw(i,r.cookie.key));if(!a){let d="no authorization included in request";throw new Fu(401,{message:d,res:Nb({ctx:i,error:"invalid_request",errDescription:d,realm:r.realm})})}let c,l;try{c=await Xd.verify(a,r.secret,{alg:r.alg,...e})}catch(d){l=d}if(!c)throw new Fu(401,{message:"Unauthorized",res:Nb({ctx:i,error:"invalid_token",statusText:"Unauthorized",errDescription:"token verification failure",realm:r.realm}),cause:l});i.set("jwtPayload",c),await n()}};x9=Xd.verifyWithJwks,mo=Xd.verify,C9=Xd.decode,yo=Xd.sign});var gw={};Cn(gw,{AlgorithmTypes:()=>Oa,decode:()=>C9,jwt:()=>w9,sign:()=>yo,verify:()=>mo,verifyWithJwks:()=>x9});var Io=J(()=>{E9();vb()});function B9(r,e){let t=`<?xml version="1.0" encoding="utf-8" ?> -`;t+=`<d:multistatus xmlns:d="DAV:"> -`,t+=` <d:response> -`,t+=` <d:href>${r}</d:href> -`,t+=` <d:propstat> -`,t+=` <d:prop> -`,t+=` <d:resourcetype><d:collection/></d:resourcetype> -`,t+=` <d:getlastmodified>${new Date().toUTCString()}</d:getlastmodified> -`,t+=` </d:prop> -`,t+=` <d:status>HTTP/1.1 200 OK</d:status> -`,t+=` </d:propstat> -`,t+=` </d:response> -`;for(let i of e){let n=`${r}${r.endsWith("/")?"":"/"}${encodeURIComponent(i.name)}`;t+=` <d:response> -`,t+=` <d:href>${n}</d:href> -`,t+=` <d:propstat> -`,t+=` <d:prop> -`,i.isFolder?t+=` <d:resourcetype><d:collection/></d:resourcetype> -`:(t+=` <d:resourcetype/> -`,t+=` <d:getcontentlength>${i.size}</d:getcontentlength> -`,t+=` <d:getcontenttype>application/octet-stream</d:getcontenttype> -`);let s=i.modified?new Date(i.modified).toUTCString():new Date().toUTCString();t+=` <d:getlastmodified>${s}</d:getlastmodified> -`,t+=` </d:prop> -`,t+=` <d:status>HTTP/1.1 200 OK</d:status> -`,t+=` </d:propstat> -`,t+=` </d:response> -`}return t+="</d:multistatus>",t}var b9=J(()=>{"use strict"});function Ne(r,e="Internal server error"){if(!r)return e;let t=typeof r=="string"?r:r?.message||String(r);if(!t)return e;let i=String(t);return i.length>200||/[A-Za-z]:[\\/][^\\/\s]|[\\/][A-Za-z0-9_.-]+[\\/][A-Za-z0-9_.-]/.test(i)&&/\.(ts|js|mjs|cjs|json|toml|yml|yaml)/i.test(i)||/at .*\(|at [A-Za-z0-9_.-]+:[0-9]+:[0-9]+/.test(i)?e:i}var ol,aBe,Ma=J(()=>{"use strict";ol=class extends Error{constructor(t,i,n){super(i);this.code=t;this.message=i;this.originalError=n;this.name="OpenListError"}code;message;originalError},aBe={PathNotFound:new ol(1004,"Path not found"),NotReady:new ol(1003,"Storage not ready"),InvalidConfig:new ol(1001,"Invalid configuration"),Unauthorized:new ol(401,"Unauthorized access"),Forbidden:new ol(403,"Permission denied")}});var v9=J(()=>{"use strict"});var k9=J(()=>{"use strict"});async function Zd(r){if(await qb(r))return!0;let e=r.req.header("Authorization");if(!e)return!1;let t=e.startsWith("Bearer ")?e.substring(7):e;try{let{verify:i}=await Promise.resolve().then(()=>(Io(),gw)),{getJwtSecret:n}=await Promise.resolve().then(()=>(Fi(),pw)),s=await n(r),o=await i(t,s,"HS256");if(o&&o.role===2){let c=((await ne(r.env)).users||[]).find(l=>l.id===o.id||l.username===o.username);return!!(c&&!c.disabled)}}catch{}return!1}async function qb(r){let e=r.req.header("Authorization");if(!e)return!1;let t=e.startsWith("Bearer ")?e.substring(7):e,n=(await ne(r.env)).settings.find(c=>c.key==="token");if(!n||!n.value)return!1;let s=new TextEncoder().encode(t),o=new TextEncoder().encode(String(n.value));if(s.length!==o.length)return!1;let a=0;for(let c=0;c<s.length;c++)a|=s[c]^o[c];return a===0}var OA=J(()=>{"use strict";Ye();b9();Nl();Ma();v9();xc();Gr();k9()});var MA={};Cn(MA,{cleanupExpiredLogs:()=>Vne,getAuditStats:()=>Yne,loadAuditLogsFromKV:()=>zne,logAudit:()=>Kne,queryAuditLogs:()=>Gne});async function Kne(r,e){let t={id:jne(),timestamp:new Date().toISOString(),...r};dn.push(t),dn.length>1e3&&(dn=dn.slice(-1e3)),e&&Jne(t,e).catch(i=>{console.warn("[Audit] Failed to persist log to KV:",i)}),$ne(e)&&console.log(`[AUDIT] ${t.username} ${t.method} ${t.resource} - ${t.status} (${t.status_code})`)}function Gne(r){let e=[...dn];r?.username&&(e=e.filter(i=>i.username===r.username)),r?.action&&(e=e.filter(i=>i.action.includes(r.action))),r?.method&&(e=e.filter(i=>i.method.toUpperCase()===r.method.toUpperCase())),r?.status&&(e=e.filter(i=>i.status===r.status)),r?.startDate&&(e=e.filter(i=>i.timestamp>=r.startDate)),r?.endDate&&(e=e.filter(i=>i.timestamp<=r.endDate));let t=r?.limit||100;return e.slice(-t).reverse()}async function zne(r){try{let{getKvBinding:e}=await Promise.resolve().then(()=>(Ye(),Jr)),t=await e(r);if(t.mode==="none"||!t.binding)return[];let{binding:i,mode:n}=t,s=null;if(n==="blob")s=await i.get(wo);else try{s=await i.get(wo,"text")}catch{s=await i.get(wo)}if(s&&typeof s.text=="function"&&(s=await s.text()),!s)return[];let o=JSON.parse(String(s));return Array.isArray(o)?o:[]}catch(e){return console.warn("[Audit] Failed to load logs from KV:",e),[]}}async function Jne(r,e){try{let{getKvBinding:t}=await Promise.resolve().then(()=>(Ye(),Jr)),i=await t(e);if(i.mode==="none"||!i.binding)return;let{binding:n,mode:s}=i,o=[];try{let c=null;s==="blob"?c=await n.get(wo):c=await n.get(wo,"text"),c&&typeof c.text=="function"&&(c=await c.text()),c&&(o=JSON.parse(String(c)))}catch{}Array.isArray(o)||(o=[]),o.push(r),o.length>1e4&&(o=o.slice(-1e4));let a=JSON.stringify(o);s==="blob"?typeof n.set=="function"?await n.set(wo,a):typeof n.put=="function"&&await n.put(wo,a):typeof n.put=="function"?await n.put(wo,a):typeof n.set=="function"&&await n.set(wo,a)}catch(t){console.warn("[Audit] Persist to KV failed:",t)}}function jne(){if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();let r=Date.now().toString(36),e=Math.random().toString(36).substring(2,9);return`${r}-${e}`}function $ne(r){let e=r||(typeof process<"u"?process.env:{});return e?.ENVIRONMENT==="development"||e?.NODE_ENV==="development"}function Vne(r=30){let e=new Date;e.setDate(e.getDate()-r);let t=e.toISOString();dn=dn.filter(i=>i.timestamp>=t)}function Yne(){let r=dn.length,e=dn.filter(o=>o.status==="success").length,t=dn.filter(o=>o.status==="failure").length,i=new Set(dn.map(o=>o.username)).size,n=new Map;dn.forEach(o=>{let a=n.get(o.action)||0;n.set(o.action,a+1)});let s=Array.from(n.entries()).map(([o,a])=>({action:o,count:a})).sort((o,a)=>a.count-o.count).slice(0,10);return{total:r,success:e,failure:t,uniqueUsers:i,recentActions:s}}var dn,wo,LA=J(()=>{"use strict";dn=[],wo="openlist_audit_logs"});var pw={};Cn(pw,{adminAuthMiddleware:()=>Pi,auditMiddleware:()=>rse,csrfProtection:()=>tse,generateCSRFToken:()=>jb,getJwtSecret:()=>Dr,getUserFromContext:()=>Me,isTokenRevoked:()=>mw,matchCronSecret:()=>Jb,resetJwtSecretCache:()=>Wne,revokeToken:()=>zb});function Ob(r,e){r&&typeof r=="object"&&Kb.set(r,e),Hb=e}function Mb(r){return r&&typeof r=="object"?Kb.get(r)??null:Hb}function Wne(){Hb=null,Kb=new WeakMap}function Q9(){let r=new Uint8Array(32);return crypto.getRandomValues(r),Array.from(r,e=>e.toString(16).padStart(2,"0")).join("")}async function Xne(r){try{let{readPersistedSecret:e}=await Promise.resolve().then(()=>(Ye(),Jr));return await e(r,D9)}catch(e){return console.warn("[JWT] Failed to read secret from KV:",e),null}}async function Zne(r,e){try{let{writePersistedSecret:t}=await Promise.resolve().then(()=>(Ye(),Jr));return await t(r,D9,e)}catch(t){return console.warn("[JWT] Failed to persist secret to KV:",t),!1}}async function Dr(r){let e=r?.env||(typeof process<"u"?process.env:{})||{},t=a=>typeof a=="string"&&a.trim().length>0?a:null,i=t(e.JWT_SECRET);if(i)return i;let n=Mb(e);if(t(n))return n;try{let a=t(await Rs(e,Xo));if(a)return Ob(e,a),a}catch{}let s=t(await Xne(e));if(s)return Ob(e,s),s;let o=e.NODE_ENV==="production"||e.ENVIRONMENT==="production"||e.CF_PAGES==="1"||e.WORKERS_ENV==="production";if(!Mb(e)){let a=Q9();Ob(e,a);let c=await Zne(e,a);o?console.error("[JWT] \u26A0\uFE0F \u{1F534} \u751F\u4EA7\u73AF\u5883\u5B89\u5168\u8B66\u544A\uFF1AJWT_SECRET \u672A\u914D\u7F6E\uFF01"+(c?` -\u2705 \u5DF2\u81EA\u52A8\u751F\u6210\u5BC6\u94A5\u5E76\u6301\u4E45\u5316\u5230 KV\u3002\u6B64\u5BC6\u94A5\u5C06\u6301\u7EED\u4F7F\u7528\uFF0C\u4F46\u5F3A\u70C8\u5EFA\u8BAE\u624B\u52A8\u914D\u7F6E JWT_SECRET \u73AF\u5883\u53D8\u91CF\u4EE5\u63D0\u9AD8\u5B89\u5168\u6027\u3002`:` -\u274C \u65E0\u6CD5\u6301\u4E45\u5316\u5BC6\u94A5\u5230 KV\uFF01\u5BC6\u94A5\u4EC5\u5B58\u4E8E\u5185\u5B58\uFF0C\u91CD\u542F\u540E\u6240\u6709 token \u5C06\u5931\u6548\u3002\u8BF7\u7ACB\u5373\u914D\u7F6E JWT_SECRET \u73AF\u5883\u53D8\u91CF\uFF01`)+` - -\u{1F512} \u5B89\u5168\u5EFA\u8BAE\uFF1A\u751F\u6210\u5BC6\u94A5\u547D\u4EE4: openssl rand -hex 32`):console.warn("[JWT] \u26A0\uFE0F \u5F00\u53D1\u73AF\u5883\u8B66\u544A\uFF1AJWT_SECRET \u672A\u914D\u7F6E\uFF0C\u4F7F\u7528\u4E34\u65F6\u968F\u673A\u5BC6\u94A5\u3002"+(c?"\u5BC6\u94A5\u5DF2\u6301\u4E45\u5316\u5230 KV\uFF0C\u91CD\u542F\u540E\u4FDD\u6301\u6709\u6548\u3002":"\u5BC6\u94A5\u4EC5\u5B58\u4E8E\u5185\u5B58\uFF0C\u91CD\u542F\u540E\u6240\u6709 token \u5C06\u5931\u6548\u3002")+` -\u751F\u4EA7\u73AF\u5883\u90E8\u7F72\u524D\uFF0C\u5EFA\u8BAE\u914D\u7F6E 32+ \u5B57\u7B26\u7684 JWT_SECRET \u73AF\u5883\u53D8\u91CF\uFF08\u63A8\u8350\u800C\u975E\u5F3A\u5236\uFF09\u3002`)}return Mb(e)}async function ese(r){if(!S9){S9=!0;try{let{getKvBinding:e}=await Promise.resolve().then(()=>(Ye(),Jr)),t=await e(r);if(t.mode==="none"||!t.binding)return;let{binding:i,mode:n}=t,s=null;if(n==="blob")s=await i.get(ys);else try{s=await i.get(ys,"text")}catch{s=await i.get(ys)}if(s&&typeof s.text=="function"&&(s=await s.text()),!s)return;let o=JSON.parse(String(s)),a=Math.floor(Date.now()/1e3);for(let c of o)c&&c.jti&&c.exp>a&&Gb.add(c.jti)}catch{}}}async function zb(r,e,t){if(r){Gb.add(r);try{let{getKvBinding:i}=await Promise.resolve().then(()=>(Ye(),Jr)),n=await i(t);if(n.mode==="none"||!n.binding)return;let{binding:s,mode:o}=n,a=[];if(o==="blob"){let d=await s.get(ys);d&&(a=typeof d=="string"?JSON.parse(d):d)}else try{let d=await s.get(ys,"text");d&&(a=JSON.parse(String(d)))}catch{let d=await s.get(ys);d&&(a=typeof d=="string"?JSON.parse(d):d)}let c=Math.floor(Date.now()/1e3);a=a.filter(d=>d&&d.exp>c),a.push({jti:r,exp:e});let l=JSON.stringify(a);o==="blob"?typeof s.set=="function"?await s.set(ys,l):typeof s.put=="function"&&await s.put(ys,l):typeof s.put=="function"?await s.put(ys,l):typeof s.set=="function"&&await s.set(ys,l)}catch(i){console.warn("[JWT] Failed to persist revoked token to KV:",i)}}}async function mw(r,e){return r?(await ese(e),Gb.has(r)):!1}async function Pi(r,e){if(!await Zd(r))return r.json({code:401,message:"Unauthorized admin privilege required",data:null},401);await e()}function Lb(r,e){if(r.length!==e.length)return!1;let t=0;for(let i=0;i<r.length;i++)t|=r.charCodeAt(i)^e.charCodeAt(i);return t===0}async function Jb(r){let t=(r?.env||{}).JWT_SECRET||(typeof process<"u"?process.env?.JWT_SECRET:"");if(!t||typeof t!="string")return!1;let i=r.req.header("x-cron-secret");if(i&&Lb(i,t))return!0;let n=r.req.query("cron_secret");if(n&&Lb(n,t))return!0;try{let o=(await r.req.json())?.cron_secret;if(typeof o=="string"&&o.length>0&&Lb(o,t))return!0}catch{}return!1}async function jb(r){let{sign:e}=await Promise.resolve().then(()=>(Io(),gw)),t=await Dr(r);return await e({type:"csrf",jti:crypto.randomUUID?crypto.randomUUID():Q9().slice(0,16),exp:Math.floor(Date.now()/1e3)+3600},t)}async function tse(r,e){let t=r.req.method.toUpperCase();if(["GET","HEAD","OPTIONS"].includes(t))return e();let i=r.req.header("x-csrf-token")||r.req.header("X-CSRF-Token");if(!i)return r.json({code:403,message:"CSRF token missing. Please include X-CSRF-Token header.",data:null},403);try{let{verify:n}=await Promise.resolve().then(()=>(Io(),gw)),s=await Dr(r);if((await n(i,s,"HS256")).type!=="csrf")throw new Error("Invalid token type");await e()}catch{return r.json({code:403,message:"Invalid or expired CSRF token",data:null},403)}}async function rse(r,e){let t=Date.now(),i=r.req.method.toUpperCase(),n=r.req.path,s=["POST","PUT","DELETE","PATCH"].includes(i);if(await e(),s)try{let{logAudit:o}=await Promise.resolve().then(()=>(LA(),MA)),a=await Me(r).catch(()=>null),c=Date.now()-t,l=r.res.status||200,d=l>=200&&l<400?"success":"failure",u=r.req.header("cf-connecting-ip")||r.req.header("x-real-ip")||r.req.header("x-forwarded-for")?.split(",")[0]?.trim()||"unknown";await o({user_id:a?.id,username:a?.username||"anonymous",ip:u,action:`${i} ${n}`,resource:n,method:i,status:d,status_code:l,details:JSON.stringify({duration_ms:c}),user_agent:r.req.header("user-agent"),duration_ms:c},r.env)}catch(o){console.warn("[Audit] Failed to log:",o)}}async function Me(r){if(await qb(r))return{role:2,permission:0,disabled:!1,username:"api-token",base_path:"/"};let e=r.req.header("Authorization");if(!e){let i=r.req.query("token")||r.req.query("access_token");i&&(e=`Bearer ${i}`)}if(!e){try{let n=((await ne(r.env)).users||[]).find(s=>s.username==="guest");if(n&&!n.disabled)return{id:n.id,role:n.role??1,permission:n.permission??0,disabled:!!n.disabled,username:n.username,base_path:n.base_path||"/",sso_id:n.sso_id||"",allow_ldap:!!n.allow_ldap,otp_secret:n.otp_secret}}catch{}return null}let t=e.startsWith("Bearer ")?e.substring(7):e;try{let i=await Dr(r),n=await mo(t,i,"HS256");if(await mw(n?.jti,r.env))return null;let o=((await ne(r.env)).users||[]).find(a=>a.id===n.id||a.username===n.username);return!o||o.disabled?null:{id:o.id,role:o.role,permission:o.permission??0,disabled:!!o.disabled,username:o.username,base_path:o.base_path||"/",sso_id:o.sso_id||"",allow_ldap:!!o.allow_ldap,otp_secret:o.otp_secret}}catch{return null}}var Hb,Kb,D9,ys,Gb,S9,Fi=J(()=>{"use strict";Io();OA();Ye();Hb=null,Kb=new WeakMap,D9="openlist_jwt_secret";ys="openlist_revoked_tokens",Gb=new Set,S9=!1});var i7=oe(Qw=>{"use strict";Object.defineProperty(Qw,"__esModule",{value:!0});function Vse(r){return r&&typeof r=="object"&&"default"in r?r.default:r}var r7=Vse(wt("crypto")),Yse=(r,e,t)=>r7.createHmac(r,Buffer.from(e,"hex")).update(Buffer.from(t,"hex")).digest().toString("hex"),Wse=(r,e)=>r7.randomBytes(r).toString(e);Qw.createDigest=Yse;Qw.createRandomBytes=Wse});var s7=oe(x2=>{"use strict";var Xse="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",n7=[255,255,26,27,28,29,30,31,255,255,255,255,255,255,255,255,255,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,255,255,255,255,255,255,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,255,255,255,255,255];function Zse(r){var e=Math.floor(r.length/5);return r.length%5===0?e:e+1}x2.encode=function(r){Buffer.isBuffer(r)||(r=new Buffer(r));for(var e=0,t=0,i=0,n=0,s=new Buffer(Zse(r)*8);e<r.length;){var o=r[e];i>3?(n=o&255>>i,i=(i+5)%8,n=n<<i|(e+1<r.length?r[e+1]:0)>>8-i,e++):(n=o>>8-(i+5)&31,i=(i+5)%8,i===0&&e++),s[t]=Xse.charCodeAt(n),t++}for(e=t;e<s.length;e++)s[e]=61;return s};x2.decode=function(r){var e=0,t=0,i,n=0;Buffer.isBuffer(r)||(r=new Buffer(r));for(var s=new Buffer(Math.ceil(r.length*5/8)),o=0;o<r.length&&r[o]!==61;o++){var a=r[o]-48;if(a<n7.length)t=n7[a],e<=3?(e=(e+5)%8,e===0?(i|=t,s[n]=i,n++,i=0):i|=255&t<<8-e):(e=(e+5)%8,i|=255&t>>>e,s[n]=i,n++,i=255&t<<8-e);else throw new Error("Invalid input - it is not base32 encoded string")}return s.slice(0,n)}});var a7=oe(C2=>{var o7=s7();C2.encode=o7.encode;C2.decode=o7.decode});var l7=oe(_w=>{"use strict";Object.defineProperty(_w,"__esModule",{value:!0});function eoe(r){return r&&typeof r=="object"&&"default"in r?r.default:r}var c7=eoe(a7()),toe=(r,e)=>c7.decode(r).toString(e),roe=(r,e)=>c7.encode(Buffer.from(r,e).toString("ascii")).toString().replace(/=/g,"");_w.keyDecoder=toe;_w.keyEncoder=roe});var _7=oe(De=>{"use strict";Object.defineProperty(De,"__esModule",{value:!0});function Nw(r){return Object.keys(r).map(e=>r[e])}(function(r){r.SHA1="sha1",r.SHA256="sha256",r.SHA512="sha512"})(De.HashAlgorithms||(De.HashAlgorithms={}));var Pw=Nw(De.HashAlgorithms);(function(r){r.ASCII="ascii",r.BASE64="base64",r.HEX="hex",r.LATIN1="latin1",r.UTF8="utf8"})(De.KeyEncodings||(De.KeyEncodings={}));var E2=Nw(De.KeyEncodings);(function(r){r.HOTP="hotp",r.TOTP="totp"})(De.Strategy||(De.Strategy={}));var B2=Nw(De.Strategy),qw=()=>{throw new Error("Please provide an options.createDigest implementation.")};function k2(r){return/^(\d+)$/.test(r)}function S2(r,e,t){return r.length>=e?r:`${Array(e+1).join(t)}${r}`.slice(-1*e)}function D2(r){let e=`otpauth://${r.type}/{labelPrefix}:{accountName}?secret={secret}{query}`,t=[];if(B2.indexOf(r.type)<0)throw new Error(`Expecting options.type to be one of ${B2.join(", ")}. Received ${r.type}.`);if(r.type==="hotp"){if(r.counter==null||typeof r.counter!="number")throw new Error('Expecting options.counter to be a number when options.type is "hotp".');t.push(`&counter=${r.counter}`)}return r.type==="totp"&&r.step&&t.push(`&period=${r.step}`),r.digits&&t.push(`&digits=${r.digits}`),r.algorithm&&t.push(`&algorithm=${r.algorithm.toUpperCase()}`),r.issuer&&t.push(`&issuer=${encodeURIComponent(r.issuer)}`),e.replace("{labelPrefix}",encodeURIComponent(r.issuer||r.accountName)).replace("{accountName}",encodeURIComponent(r.accountName)).replace("{secret}",r.secret).replace("{query}",t.join(""))}var Tw=class r{constructor(e={}){this._defaultOptions=Object.freeze({...e}),this._options=Object.freeze({})}create(e={}){return new r(e)}clone(e={}){let t=this.create({...this._defaultOptions,...e});return t.options=this._options,t}get options(){return Object.freeze({...this._defaultOptions,...this._options})}set options(e){this._options=Object.freeze({...this._options,...e})}allOptions(){return this.options}resetOptions(){this._options=Object.freeze({})}};function Q2(r){if(typeof r.createDigest!="function")throw new Error("Expecting options.createDigest to be a function.");if(typeof r.createHmacKey!="function")throw new Error("Expecting options.createHmacKey to be a function.");if(typeof r.digits!="number")throw new Error("Expecting options.digits to be a number.");if(!r.algorithm||Pw.indexOf(r.algorithm)<0)throw new Error(`Expecting options.algorithm to be one of ${Pw.join(", ")}. Received ${r.algorithm}.`);if(!r.encoding||E2.indexOf(r.encoding)<0)throw new Error(`Expecting options.encoding to be one of ${E2.join(", ")}. Received ${r.encoding}.`)}var u7=(r,e,t)=>Buffer.from(e,t).toString("hex");function f7(){return{algorithm:De.HashAlgorithms.SHA1,createHmacKey:u7,createDigest:qw,digits:6,encoding:De.KeyEncodings.ASCII}}function A7(r){let e={...f7(),...r};return Q2(e),Object.freeze(e)}function h7(r){let e=r.toString(16);return S2(e,16,"0")}function g7(r,e){let t=Buffer.from(r,"hex"),i=t[t.length-1]&15,s=((t[i]&127)<<24|(t[i+1]&255)<<16|(t[i+2]&255)<<8|t[i+3]&255)%Math.pow(10,e);return S2(String(s),e,"0")}function ioe(r,e,t){let i=h7(e),n=t.createHmacKey(t.algorithm,r,t.encoding);return t.createDigest(t.algorithm,n,i)}function Ow(r,e,t){let i=t.digest||ioe(r,e,t);return g7(i,t.digits)}function p7(r,e,t,i){if(!k2(r))return!1;let n=Ow(e,t,i);return r===n}function m7(r,e,t,i,n){return D2({algorithm:n.algorithm,digits:n.digits,type:De.Strategy.HOTP,accountName:r,counter:i,issuer:e,secret:t})}var Uw=class r extends Tw{create(e={}){return new r(e)}allOptions(){return A7(this.options)}generate(e,t){return Ow(e,t,this.allOptions())}check(e,t,i){return p7(e,t,i,this.allOptions())}verify(e){if(typeof e!="object")throw new Error("Expecting argument 0 of verify to be an object");return this.check(e.token,e.secret,e.counter)}keyuri(e,t,i,n){return m7(e,t,i,n,this.allOptions())}};function y7(r){if(typeof r=="number")return[Math.abs(r),Math.abs(r)];if(Array.isArray(r)){let[e,t]=r;if(typeof e=="number"&&typeof t=="number")return[Math.abs(e),Math.abs(t)]}throw new Error("Expecting options.window to be an number or [number, number].")}function _2(r){if(Q2(r),y7(r.window),typeof r.epoch!="number")throw new Error("Expecting options.epoch to be a number.");if(typeof r.step!="number")throw new Error("Expecting options.step to be a number.")}var Fw=(r,e,t)=>{let i=r.length,n=Buffer.from(r,e).toString("hex");if(i<t){let s=new Array(t-i+1).join(n);return Buffer.from(s,"hex").slice(0,t).toString("hex")}return n},F2=(r,e,t)=>{switch(r){case De.HashAlgorithms.SHA1:return Fw(e,t,20);case De.HashAlgorithms.SHA256:return Fw(e,t,32);case De.HashAlgorithms.SHA512:return Fw(e,t,64);default:throw new Error(`Expecting algorithm to be one of ${Pw.join(", ")}. Received ${r}.`)}};function I7(){return{algorithm:De.HashAlgorithms.SHA1,createDigest:qw,createHmacKey:F2,digits:6,encoding:De.KeyEncodings.ASCII,epoch:Date.now(),step:30,window:0}}function w7(r){let e={...I7(),...r};return _2(e),Object.freeze(e)}function x7(r,e){return Math.floor(r/e/1e3)}function Mw(r,e){let t=x7(e.epoch,e.step);return Ow(r,t,e)}function d7(r,e,t,i){let n=[];if(i===0)return n;for(let s=1;s<=i;s++){let o=e*s*t;n.push(r+o)}return n}function C7(r,e,t){let i=y7(t),n=e*1e3;return{current:r,past:d7(r,-1,n,i[0]),future:d7(r,1,n,i[1])}}function P2(r,e,t){if(!k2(r))return!1;let i=Mw(e,t);return r===i}function b2(r,e,t,i){let n=null;return r.some((s,o)=>P2(e,t,{...i,epoch:s})?(n=o+1,!0):!1),n}function T2(r,e,t){if(P2(r,e,t))return 0;let i=C7(t.epoch,t.step,t.window),n=b2(i.past,r,e,t);return n!==null?n*-1:b2(i.future,r,e,t)}function U2(r,e){return Math.floor(r/1e3)%e}function E7(r,e){return e-U2(r,e)}function B7(r,e,t,i){return D2({algorithm:i.algorithm,digits:i.digits,step:i.step,type:De.Strategy.TOTP,accountName:r,issuer:e,secret:t})}var Rw=class r extends Uw{create(e={}){return new r(e)}allOptions(){return w7(this.options)}generate(e){return Mw(e,this.allOptions())}checkDelta(e,t){return T2(e,t,this.allOptions())}check(e,t){return typeof this.checkDelta(e,t)=="number"}verify(e){if(typeof e!="object")throw new Error("Expecting argument 0 of verify to be an object");return this.check(e.token,e.secret)}timeRemaining(){let e=this.allOptions();return E7(e.epoch,e.step)}timeUsed(){let e=this.allOptions();return U2(e.epoch,e.step)}keyuri(e,t,i){return B7(e,t,i,this.allOptions())}};function b7(r){if(_2(r),typeof r.keyDecoder!="function")throw new Error("Expecting options.keyDecoder to be a function.");if(r.keyEncoder&&typeof r.keyEncoder!="function")throw new Error("Expecting options.keyEncoder to be a function.")}function v7(){return{algorithm:De.HashAlgorithms.SHA1,createDigest:qw,createHmacKey:F2,digits:6,encoding:De.KeyEncodings.HEX,epoch:Date.now(),step:30,window:0}}function k7(r){let e={...v7(),...r};return b7(e),Object.freeze(e)}function R2(r,e){return e.keyEncoder(r,e.encoding)}function Lw(r,e){return e.keyDecoder(r,e.encoding)}function S7(r,e){let t=e.createRandomBytes(r,e.encoding);return R2(t,e)}function D7(r,e){return Mw(Lw(r,e),e)}function Q7(r,e,t){return T2(r,Lw(e,t),t)}var v2=class r extends Rw{create(e={}){return new r(e)}allOptions(){return k7(this.options)}generate(e){return D7(e,this.allOptions())}checkDelta(e,t){return Q7(e,t,this.allOptions())}encode(e){return R2(e,this.allOptions())}decode(e){return Lw(e,this.allOptions())}generateSecret(e=10){return S7(e,this.allOptions())}};De.Authenticator=v2;De.HASH_ALGORITHMS=Pw;De.HOTP=Uw;De.KEY_ENCODINGS=E2;De.OTP=Tw;De.STRATEGY=B2;De.TOTP=Rw;De.authenticatorCheckWithWindow=Q7;De.authenticatorDecoder=Lw;De.authenticatorDefaultOptions=v7;De.authenticatorEncoder=R2;De.authenticatorGenerateSecret=S7;De.authenticatorOptionValidator=b7;De.authenticatorOptions=k7;De.authenticatorToken=D7;De.createDigestPlaceholder=qw;De.hotpCheck=p7;De.hotpCounter=h7;De.hotpCreateHmacKey=u7;De.hotpDefaultOptions=f7;De.hotpDigestToToken=g7;De.hotpKeyuri=m7;De.hotpOptions=A7;De.hotpOptionsValidator=Q2;De.hotpToken=Ow;De.isTokenValid=k2;De.keyuri=D2;De.objectValues=Nw;De.padStart=S2;De.totpCheck=P2;De.totpCheckByEpoch=b2;De.totpCheckWithWindow=T2;De.totpCounter=x7;De.totpCreateHmacKey=F2;De.totpDefaultOptions=I7;De.totpEpochAvailable=C7;De.totpKeyuri=B7;De.totpOptions=w7;De.totpOptionsValidator=_2;De.totpPadSecret=Fw;De.totpTimeRemaining=E7;De.totpTimeUsed=U2;De.totpToken=Mw});var P7=oe(YA=>{"use strict";Object.defineProperty(YA,"__esModule",{value:!0});var Hw=i7(),F7=l7(),N2=_7(),noe=new N2.HOTP({createDigest:Hw.createDigest}),soe=new N2.TOTP({createDigest:Hw.createDigest}),ooe=new N2.Authenticator({createDigest:Hw.createDigest,createRandomBytes:Hw.createRandomBytes,keyDecoder:F7.keyDecoder,keyEncoder:F7.keyEncoder});YA.authenticator=ooe;YA.hotp=noe;YA.totp=soe});var U7=oe(q2=>{"use strict";Object.defineProperty(q2,"__esModule",{value:!0});var T7=P7();Object.keys(T7).forEach(function(r){r!=="default"&&Object.defineProperty(q2,r,{enumerable:!0,get:function(){return T7[r]}})})});var O2=oe((U1e,R7)=>{R7.exports=function(){return typeof Promise=="function"&&Promise.prototype&&Promise.prototype.then}});var Ja=oe(ll=>{var M2,aoe=[0,26,44,70,100,134,172,196,242,292,346,404,466,532,581,655,733,815,901,991,1085,1156,1258,1364,1474,1588,1706,1828,1921,2051,2185,2323,2465,2611,2761,2876,3034,3196,3362,3532,3706];ll.getSymbolSize=function(e){if(!e)throw new Error('"version" cannot be null or undefined');if(e<1||e>40)throw new Error('"version" should be in range from 1 to 40');return e*4+17};ll.getSymbolTotalCodewords=function(e){return aoe[e]};ll.getBCHDigit=function(r){let e=0;for(;r!==0;)e++,r>>>=1;return e};ll.setToSJISFunction=function(e){if(typeof e!="function")throw new Error('"toSJISFunc" is not a valid function.');M2=e};ll.isKanjiModeEnabled=function(){return typeof M2<"u"};ll.toSJIS=function(e){return M2(e)}});var Kw=oe(gn=>{gn.L={bit:1};gn.M={bit:0};gn.Q={bit:3};gn.H={bit:2};function coe(r){if(typeof r!="string")throw new Error("Param is not a string");switch(r.toLowerCase()){case"l":case"low":return gn.L;case"m":case"medium":return gn.M;case"q":case"quartile":return gn.Q;case"h":case"high":return gn.H;default:throw new Error("Unknown EC Level: "+r)}}gn.isValid=function(e){return e&&typeof e.bit<"u"&&e.bit>=0&&e.bit<4};gn.from=function(e,t){if(gn.isValid(e))return e;try{return coe(e)}catch{return t}}});var O7=oe((q1e,q7)=>{function N7(){this.buffer=[],this.length=0}N7.prototype={get:function(r){let e=Math.floor(r/8);return(this.buffer[e]>>>7-r%8&1)===1},put:function(r,e){for(let t=0;t<e;t++)this.putBit((r>>>e-t-1&1)===1)},getLengthInBits:function(){return this.length},putBit:function(r){let e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),r&&(this.buffer[e]|=128>>>this.length%8),this.length++}};q7.exports=N7});var L7=oe((O1e,M7)=>{function WA(r){if(!r||r<1)throw new Error("BitMatrix size must be defined and greater than 0");this.size=r,this.data=new Uint8Array(r*r),this.reservedBit=new Uint8Array(r*r)}WA.prototype.set=function(r,e,t,i){let n=r*this.size+e;this.data[n]=t,i&&(this.reservedBit[n]=!0)};WA.prototype.get=function(r,e){return this.data[r*this.size+e]};WA.prototype.xor=function(r,e,t){this.data[r*this.size+e]^=t};WA.prototype.isReserved=function(r,e){return this.reservedBit[r*this.size+e]};M7.exports=WA});var H7=oe(Gw=>{var loe=Ja().getSymbolSize;Gw.getRowColCoords=function(e){if(e===1)return[];let t=Math.floor(e/7)+2,i=loe(e),n=i===145?26:Math.ceil((i-13)/(2*t-2))*2,s=[i-7];for(let o=1;o<t-1;o++)s[o]=s[o-1]-n;return s.push(6),s.reverse()};Gw.getPositions=function(e){let t=[],i=Gw.getRowColCoords(e),n=i.length;for(let s=0;s<n;s++)for(let o=0;o<n;o++)s===0&&o===0||s===0&&o===n-1||s===n-1&&o===0||t.push([i[s],i[o]]);return t}});var z7=oe(G7=>{var doe=Ja().getSymbolSize,K7=7;G7.getPositions=function(e){let t=doe(e);return[[0,0],[t-K7,0],[0,t-K7]]}});var J7=oe(Bt=>{Bt.Patterns={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var dl={N1:3,N2:3,N3:40,N4:10};Bt.isValid=function(e){return e!=null&&e!==""&&!isNaN(e)&&e>=0&&e<=7};Bt.from=function(e){return Bt.isValid(e)?parseInt(e,10):void 0};Bt.getPenaltyN1=function(e){let t=e.size,i=0,n=0,s=0,o=null,a=null;for(let c=0;c<t;c++){n=s=0,o=a=null;for(let l=0;l<t;l++){let d=e.get(c,l);d===o?n++:(n>=5&&(i+=dl.N1+(n-5)),o=d,n=1),d=e.get(l,c),d===a?s++:(s>=5&&(i+=dl.N1+(s-5)),a=d,s=1)}n>=5&&(i+=dl.N1+(n-5)),s>=5&&(i+=dl.N1+(s-5))}return i};Bt.getPenaltyN2=function(e){let t=e.size,i=0;for(let n=0;n<t-1;n++)for(let s=0;s<t-1;s++){let o=e.get(n,s)+e.get(n,s+1)+e.get(n+1,s)+e.get(n+1,s+1);(o===4||o===0)&&i++}return i*dl.N2};Bt.getPenaltyN3=function(e){let t=e.size,i=0,n=0,s=0;for(let o=0;o<t;o++){n=s=0;for(let a=0;a<t;a++)n=n<<1&2047|e.get(o,a),a>=10&&(n===1488||n===93)&&i++,s=s<<1&2047|e.get(a,o),a>=10&&(s===1488||s===93)&&i++}return i*dl.N3};Bt.getPenaltyN4=function(e){let t=0,i=e.data.length;for(let s=0;s<i;s++)t+=e.data[s];return Math.abs(Math.ceil(t*100/i/5)-10)*dl.N4};function uoe(r,e,t){switch(r){case Bt.Patterns.PATTERN000:return(e+t)%2===0;case Bt.Patterns.PATTERN001:return e%2===0;case Bt.Patterns.PATTERN010:return t%3===0;case Bt.Patterns.PATTERN011:return(e+t)%3===0;case Bt.Patterns.PATTERN100:return(Math.floor(e/2)+Math.floor(t/3))%2===0;case Bt.Patterns.PATTERN101:return e*t%2+e*t%3===0;case Bt.Patterns.PATTERN110:return(e*t%2+e*t%3)%2===0;case Bt.Patterns.PATTERN111:return(e*t%3+(e+t)%2)%2===0;default:throw new Error("bad maskPattern:"+r)}}Bt.applyMask=function(e,t){let i=t.size;for(let n=0;n<i;n++)for(let s=0;s<i;s++)t.isReserved(s,n)||t.xor(s,n,uoe(e,s,n))};Bt.getBestMask=function(e,t){let i=Object.keys(Bt.Patterns).length,n=0,s=1/0;for(let o=0;o<i;o++){t(o),Bt.applyMask(o,e);let a=Bt.getPenaltyN1(e)+Bt.getPenaltyN2(e)+Bt.getPenaltyN3(e)+Bt.getPenaltyN4(e);Bt.applyMask(o,e),a<s&&(s=a,n=o)}return n}});var H2=oe(L2=>{var ja=Kw(),zw=[1,1,1,1,1,1,1,1,1,1,2,2,1,2,2,4,1,2,4,4,2,4,4,4,2,4,6,5,2,4,6,6,2,5,8,8,4,5,8,8,4,5,8,11,4,8,10,11,4,9,12,16,4,9,16,16,6,10,12,18,6,10,17,16,6,11,16,19,6,13,18,21,7,14,21,25,8,16,20,25,8,17,23,25,9,17,23,34,9,18,25,30,10,20,27,32,12,21,29,35,12,23,34,37,12,25,34,40,13,26,35,42,14,28,38,45,15,29,40,48,16,31,43,51,17,33,45,54,18,35,48,57,19,37,51,60,19,38,53,63,20,40,56,66,21,43,59,70,22,45,62,74,24,47,65,77,25,49,68,81],Jw=[7,10,13,17,10,16,22,28,15,26,36,44,20,36,52,64,26,48,72,88,36,64,96,112,40,72,108,130,48,88,132,156,60,110,160,192,72,130,192,224,80,150,224,264,96,176,260,308,104,198,288,352,120,216,320,384,132,240,360,432,144,280,408,480,168,308,448,532,180,338,504,588,196,364,546,650,224,416,600,700,224,442,644,750,252,476,690,816,270,504,750,900,300,560,810,960,312,588,870,1050,336,644,952,1110,360,700,1020,1200,390,728,1050,1260,420,784,1140,1350,450,812,1200,1440,480,868,1290,1530,510,924,1350,1620,540,980,1440,1710,570,1036,1530,1800,570,1064,1590,1890,600,1120,1680,1980,630,1204,1770,2100,660,1260,1860,2220,720,1316,1950,2310,750,1372,2040,2430];L2.getBlocksCount=function(e,t){switch(t){case ja.L:return zw[(e-1)*4+0];case ja.M:return zw[(e-1)*4+1];case ja.Q:return zw[(e-1)*4+2];case ja.H:return zw[(e-1)*4+3];default:return}};L2.getTotalCodewordsCount=function(e,t){switch(t){case ja.L:return Jw[(e-1)*4+0];case ja.M:return Jw[(e-1)*4+1];case ja.Q:return Jw[(e-1)*4+2];case ja.H:return Jw[(e-1)*4+3];default:return}}});var j7=oe($w=>{var XA=new Uint8Array(512),jw=new Uint8Array(256);(function(){let e=1;for(let t=0;t<255;t++)XA[t]=e,jw[e]=t,e<<=1,e&256&&(e^=285);for(let t=255;t<512;t++)XA[t]=XA[t-255]})();$w.log=function(e){if(e<1)throw new Error("log("+e+")");return jw[e]};$w.exp=function(e){return XA[e]};$w.mul=function(e,t){return e===0||t===0?0:XA[jw[e]+jw[t]]}});var $7=oe(ZA=>{var K2=j7();ZA.mul=function(e,t){let i=new Uint8Array(e.length+t.length-1);for(let n=0;n<e.length;n++)for(let s=0;s<t.length;s++)i[n+s]^=K2.mul(e[n],t[s]);return i};ZA.mod=function(e,t){let i=new Uint8Array(e);for(;i.length-t.length>=0;){let n=i[0];for(let o=0;o<t.length;o++)i[o]^=K2.mul(t[o],n);let s=0;for(;s<i.length&&i[s]===0;)s++;i=i.slice(s)}return i};ZA.generateECPolynomial=function(e){let t=new Uint8Array([1]);for(let i=0;i<e;i++)t=ZA.mul(t,new Uint8Array([1,K2.exp(i)]));return t}});var W7=oe((J1e,Y7)=>{var V7=$7();function G2(r){this.genPoly=void 0,this.degree=r,this.degree&&this.initialize(this.degree)}G2.prototype.initialize=function(e){this.degree=e,this.genPoly=V7.generateECPolynomial(this.degree)};G2.prototype.encode=function(e){if(!this.genPoly)throw new Error("Encoder not initialized");let t=new Uint8Array(e.length+this.degree);t.set(e);let i=V7.mod(t,this.genPoly),n=this.degree-i.length;if(n>0){let s=new Uint8Array(this.degree);return s.set(i,n),s}return i};Y7.exports=G2});var z2=oe(X7=>{X7.isValid=function(e){return!isNaN(e)&&e>=1&&e<=40}});var J2=oe(Eo=>{var Z7="[0-9]+",foe="[A-Z $%*+\\-./:]+",e0="(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";e0=e0.replace(/u/g,"\\u");var Aoe="(?:(?![A-Z0-9 $%*+\\-./:]|"+e0+`)(?:.|[\r -]))+`;Eo.KANJI=new RegExp(e0,"g");Eo.BYTE_KANJI=new RegExp("[^A-Z0-9 $%*+\\-./:]+","g");Eo.BYTE=new RegExp(Aoe,"g");Eo.NUMERIC=new RegExp(Z7,"g");Eo.ALPHANUMERIC=new RegExp(foe,"g");var hoe=new RegExp("^"+e0+"$"),goe=new RegExp("^"+Z7+"$"),poe=new RegExp("^[A-Z0-9 $%*+\\-./:]+$");Eo.testKanji=function(e){return hoe.test(e)};Eo.testNumeric=function(e){return goe.test(e)};Eo.testAlphanumeric=function(e){return poe.test(e)}});var $a=oe(lr=>{var moe=z2(),j2=J2();lr.NUMERIC={id:"Numeric",bit:1,ccBits:[10,12,14]};lr.ALPHANUMERIC={id:"Alphanumeric",bit:2,ccBits:[9,11,13]};lr.BYTE={id:"Byte",bit:4,ccBits:[8,16,16]};lr.KANJI={id:"Kanji",bit:8,ccBits:[8,10,12]};lr.MIXED={bit:-1};lr.getCharCountIndicator=function(e,t){if(!e.ccBits)throw new Error("Invalid mode: "+e);if(!moe.isValid(t))throw new Error("Invalid version: "+t);return t>=1&&t<10?e.ccBits[0]:t<27?e.ccBits[1]:e.ccBits[2]};lr.getBestModeForData=function(e){return j2.testNumeric(e)?lr.NUMERIC:j2.testAlphanumeric(e)?lr.ALPHANUMERIC:j2.testKanji(e)?lr.KANJI:lr.BYTE};lr.toString=function(e){if(e&&e.id)return e.id;throw new Error("Invalid mode")};lr.isValid=function(e){return e&&e.bit&&e.ccBits};function yoe(r){if(typeof r!="string")throw new Error("Param is not a string");switch(r.toLowerCase()){case"numeric":return lr.NUMERIC;case"alphanumeric":return lr.ALPHANUMERIC;case"kanji":return lr.KANJI;case"byte":return lr.BYTE;default:throw new Error("Unknown mode: "+r)}}lr.from=function(e,t){if(lr.isValid(e))return e;try{return yoe(e)}catch{return t}}});var nq=oe(ul=>{var Vw=Ja(),Ioe=H2(),eq=Kw(),Va=$a(),$2=z2(),rq=7973,tq=Vw.getBCHDigit(rq);function woe(r,e,t){for(let i=1;i<=40;i++)if(e<=ul.getCapacity(i,t,r))return i}function iq(r,e){return Va.getCharCountIndicator(r,e)+4}function xoe(r,e){let t=0;return r.forEach(function(i){let n=iq(i.mode,e);t+=n+i.getBitsLength()}),t}function Coe(r,e){for(let t=1;t<=40;t++)if(xoe(r,t)<=ul.getCapacity(t,e,Va.MIXED))return t}ul.from=function(e,t){return $2.isValid(e)?parseInt(e,10):t};ul.getCapacity=function(e,t,i){if(!$2.isValid(e))throw new Error("Invalid QR Code version");typeof i>"u"&&(i=Va.BYTE);let n=Vw.getSymbolTotalCodewords(e),s=Ioe.getTotalCodewordsCount(e,t),o=(n-s)*8;if(i===Va.MIXED)return o;let a=o-iq(i,e);switch(i){case Va.NUMERIC:return Math.floor(a/10*3);case Va.ALPHANUMERIC:return Math.floor(a/11*2);case Va.KANJI:return Math.floor(a/13);case Va.BYTE:default:return Math.floor(a/8)}};ul.getBestVersionForData=function(e,t){let i,n=eq.from(t,eq.M);if(Array.isArray(e)){if(e.length>1)return Coe(e,n);if(e.length===0)return 1;i=e[0]}else i=e;return woe(i.mode,i.getLength(),n)};ul.getEncodedBits=function(e){if(!$2.isValid(e)||e<7)throw new Error("Invalid QR Code version");let t=e<<12;for(;Vw.getBCHDigit(t)-tq>=0;)t^=rq<<Vw.getBCHDigit(t)-tq;return e<<12|t}});var cq=oe(aq=>{var V2=Ja(),oq=1335,Eoe=21522,sq=V2.getBCHDigit(oq);aq.getEncodedBits=function(e,t){let i=e.bit<<3|t,n=i<<10;for(;V2.getBCHDigit(n)-sq>=0;)n^=oq<<V2.getBCHDigit(n)-sq;return(i<<10|n)^Eoe}});var dq=oe((X1e,lq)=>{var Boe=$a();function lu(r){this.mode=Boe.NUMERIC,this.data=r.toString()}lu.getBitsLength=function(e){return 10*Math.floor(e/3)+(e%3?e%3*3+1:0)};lu.prototype.getLength=function(){return this.data.length};lu.prototype.getBitsLength=function(){return lu.getBitsLength(this.data.length)};lu.prototype.write=function(e){let t,i,n;for(t=0;t+3<=this.data.length;t+=3)i=this.data.substr(t,3),n=parseInt(i,10),e.put(n,10);let s=this.data.length-t;s>0&&(i=this.data.substr(t),n=parseInt(i,10),e.put(n,s*3+1))};lq.exports=lu});var fq=oe((Z1e,uq)=>{var boe=$a(),Y2=["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"," ","$","%","*","+","-",".","/",":"];function du(r){this.mode=boe.ALPHANUMERIC,this.data=r}du.getBitsLength=function(e){return 11*Math.floor(e/2)+6*(e%2)};du.prototype.getLength=function(){return this.data.length};du.prototype.getBitsLength=function(){return du.getBitsLength(this.data.length)};du.prototype.write=function(e){let t;for(t=0;t+2<=this.data.length;t+=2){let i=Y2.indexOf(this.data[t])*45;i+=Y2.indexOf(this.data[t+1]),e.put(i,11)}this.data.length%2&&e.put(Y2.indexOf(this.data[t]),6)};uq.exports=du});var hq=oe((ebe,Aq)=>{var voe=$a();function uu(r){this.mode=voe.BYTE,typeof r=="string"?this.data=new TextEncoder().encode(r):this.data=new Uint8Array(r)}uu.getBitsLength=function(e){return e*8};uu.prototype.getLength=function(){return this.data.length};uu.prototype.getBitsLength=function(){return uu.getBitsLength(this.data.length)};uu.prototype.write=function(r){for(let e=0,t=this.data.length;e<t;e++)r.put(this.data[e],8)};Aq.exports=uu});var pq=oe((tbe,gq)=>{var koe=$a(),Soe=Ja();function fu(r){this.mode=koe.KANJI,this.data=r}fu.getBitsLength=function(e){return e*13};fu.prototype.getLength=function(){return this.data.length};fu.prototype.getBitsLength=function(){return fu.getBitsLength(this.data.length)};fu.prototype.write=function(r){let e;for(e=0;e<this.data.length;e++){let t=Soe.toSJIS(this.data[e]);if(t>=33088&&t<=40956)t-=33088;else if(t>=57408&&t<=60351)t-=49472;else throw new Error("Invalid SJIS character: "+this.data[e]+` -Make sure your charset is UTF-8`);t=(t>>>8&255)*192+(t&255),r.put(t,13)}};gq.exports=fu});var mq=oe((rbe,W2)=>{"use strict";var t0={single_source_shortest_paths:function(r,e,t){var i={},n={};n[e]=0;var s=t0.PriorityQueue.make();s.push(e,0);for(var o,a,c,l,d,u,f,h,g;!s.empty();){o=s.pop(),a=o.value,l=o.cost,d=r[a]||{};for(c in d)d.hasOwnProperty(c)&&(u=d[c],f=l+u,h=n[c],g=typeof n[c]>"u",(g||h>f)&&(n[c]=f,s.push(c,f),i[c]=a))}if(typeof t<"u"&&typeof n[t]>"u"){var p=["Could not find a path from ",e," to ",t,"."].join("");throw new Error(p)}return i},extract_shortest_path_from_predecessor_list:function(r,e){for(var t=[],i=e,n;i;)t.push(i),n=r[i],i=r[i];return t.reverse(),t},find_path:function(r,e,t){var i=t0.single_source_shortest_paths(r,e,t);return t0.extract_shortest_path_from_predecessor_list(i,t)},PriorityQueue:{make:function(r){var e=t0.PriorityQueue,t={},i;r=r||{};for(i in e)e.hasOwnProperty(i)&&(t[i]=e[i]);return t.queue=[],t.sorter=r.sorter||e.default_sorter,t},default_sorter:function(r,e){return r.cost-e.cost},push:function(r,e){var t={value:r,cost:e};this.queue.push(t),this.queue.sort(this.sorter)},pop:function(){return this.queue.shift()},empty:function(){return this.queue.length===0}}};typeof W2<"u"&&(W2.exports=t0)});var bq=oe(Au=>{var At=$a(),wq=dq(),xq=fq(),Cq=hq(),Eq=pq(),r0=J2(),Yw=Ja(),Doe=mq();function yq(r){return unescape(encodeURIComponent(r)).length}function i0(r,e,t){let i=[],n;for(;(n=r.exec(t))!==null;)i.push({data:n[0],index:n.index,mode:e,length:n[0].length});return i}function Bq(r){let e=i0(r0.NUMERIC,At.NUMERIC,r),t=i0(r0.ALPHANUMERIC,At.ALPHANUMERIC,r),i,n;return Yw.isKanjiModeEnabled()?(i=i0(r0.BYTE,At.BYTE,r),n=i0(r0.KANJI,At.KANJI,r)):(i=i0(r0.BYTE_KANJI,At.BYTE,r),n=[]),e.concat(t,i,n).sort(function(o,a){return o.index-a.index}).map(function(o){return{data:o.data,mode:o.mode,length:o.length}})}function X2(r,e){switch(e){case At.NUMERIC:return wq.getBitsLength(r);case At.ALPHANUMERIC:return xq.getBitsLength(r);case At.KANJI:return Eq.getBitsLength(r);case At.BYTE:return Cq.getBitsLength(r)}}function Qoe(r){return r.reduce(function(e,t){let i=e.length-1>=0?e[e.length-1]:null;return i&&i.mode===t.mode?(e[e.length-1].data+=t.data,e):(e.push(t),e)},[])}function _oe(r){let e=[];for(let t=0;t<r.length;t++){let i=r[t];switch(i.mode){case At.NUMERIC:e.push([i,{data:i.data,mode:At.ALPHANUMERIC,length:i.length},{data:i.data,mode:At.BYTE,length:i.length}]);break;case At.ALPHANUMERIC:e.push([i,{data:i.data,mode:At.BYTE,length:i.length}]);break;case At.KANJI:e.push([i,{data:i.data,mode:At.BYTE,length:yq(i.data)}]);break;case At.BYTE:e.push([{data:i.data,mode:At.BYTE,length:yq(i.data)}])}}return e}function Foe(r,e){let t={},i={start:{}},n=["start"];for(let s=0;s<r.length;s++){let o=r[s],a=[];for(let c=0;c<o.length;c++){let l=o[c],d=""+s+c;a.push(d),t[d]={node:l,lastCount:0},i[d]={};for(let u=0;u<n.length;u++){let f=n[u];t[f]&&t[f].node.mode===l.mode?(i[f][d]=X2(t[f].lastCount+l.length,l.mode)-X2(t[f].lastCount,l.mode),t[f].lastCount+=l.length):(t[f]&&(t[f].lastCount=l.length),i[f][d]=X2(l.length,l.mode)+4+At.getCharCountIndicator(l.mode,e))}}n=a}for(let s=0;s<n.length;s++)i[n[s]].end=0;return{map:i,table:t}}function Iq(r,e){let t,i=At.getBestModeForData(r);if(t=At.from(e,i),t!==At.BYTE&&t.bit<i.bit)throw new Error('"'+r+'" cannot be encoded with mode '+At.toString(t)+`. - Suggested mode is: `+At.toString(i));switch(t===At.KANJI&&!Yw.isKanjiModeEnabled()&&(t=At.BYTE),t){case At.NUMERIC:return new wq(r);case At.ALPHANUMERIC:return new xq(r);case At.KANJI:return new Eq(r);case At.BYTE:return new Cq(r)}}Au.fromArray=function(e){return e.reduce(function(t,i){return typeof i=="string"?t.push(Iq(i,null)):i.data&&t.push(Iq(i.data,i.mode)),t},[])};Au.fromString=function(e,t){let i=Bq(e,Yw.isKanjiModeEnabled()),n=_oe(i),s=Foe(n,t),o=Doe.find_path(s.map,"start","end"),a=[];for(let c=1;c<o.length-1;c++)a.push(s.table[o[c]].node);return Au.fromArray(Qoe(a))};Au.rawSplit=function(e){return Au.fromArray(Bq(e,Yw.isKanjiModeEnabled()))}});var nv=oe(vq=>{var Xw=Ja(),Z2=Kw(),Poe=O7(),Toe=L7(),Uoe=H7(),Roe=z7(),rv=J7(),iv=H2(),Noe=W7(),Ww=nq(),qoe=cq(),Ooe=$a(),ev=bq();function Moe(r,e){let t=r.size,i=Roe.getPositions(e);for(let n=0;n<i.length;n++){let s=i[n][0],o=i[n][1];for(let a=-1;a<=7;a++)if(!(s+a<=-1||t<=s+a))for(let c=-1;c<=7;c++)o+c<=-1||t<=o+c||(a>=0&&a<=6&&(c===0||c===6)||c>=0&&c<=6&&(a===0||a===6)||a>=2&&a<=4&&c>=2&&c<=4?r.set(s+a,o+c,!0,!0):r.set(s+a,o+c,!1,!0))}}function Loe(r){let e=r.size;for(let t=8;t<e-8;t++){let i=t%2===0;r.set(t,6,i,!0),r.set(6,t,i,!0)}}function Hoe(r,e){let t=Uoe.getPositions(e);for(let i=0;i<t.length;i++){let n=t[i][0],s=t[i][1];for(let o=-2;o<=2;o++)for(let a=-2;a<=2;a++)o===-2||o===2||a===-2||a===2||o===0&&a===0?r.set(n+o,s+a,!0,!0):r.set(n+o,s+a,!1,!0)}}function Koe(r,e){let t=r.size,i=Ww.getEncodedBits(e),n,s,o;for(let a=0;a<18;a++)n=Math.floor(a/3),s=a%3+t-8-3,o=(i>>a&1)===1,r.set(n,s,o,!0),r.set(s,n,o,!0)}function tv(r,e,t){let i=r.size,n=qoe.getEncodedBits(e,t),s,o;for(s=0;s<15;s++)o=(n>>s&1)===1,s<6?r.set(s,8,o,!0):s<8?r.set(s+1,8,o,!0):r.set(i-15+s,8,o,!0),s<8?r.set(8,i-s-1,o,!0):s<9?r.set(8,15-s-1+1,o,!0):r.set(8,15-s-1,o,!0);r.set(i-8,8,1,!0)}function Goe(r,e){let t=r.size,i=-1,n=t-1,s=7,o=0;for(let a=t-1;a>0;a-=2)for(a===6&&a--;;){for(let c=0;c<2;c++)if(!r.isReserved(n,a-c)){let l=!1;o<e.length&&(l=(e[o]>>>s&1)===1),r.set(n,a-c,l),s--,s===-1&&(o++,s=7)}if(n+=i,n<0||t<=n){n-=i,i=-i;break}}}function zoe(r,e,t){let i=new Poe;t.forEach(function(c){i.put(c.mode.bit,4),i.put(c.getLength(),Ooe.getCharCountIndicator(c.mode,r)),c.write(i)});let n=Xw.getSymbolTotalCodewords(r),s=iv.getTotalCodewordsCount(r,e),o=(n-s)*8;for(i.getLengthInBits()+4<=o&&i.put(0,4);i.getLengthInBits()%8!==0;)i.putBit(0);let a=(o-i.getLengthInBits())/8;for(let c=0;c<a;c++)i.put(c%2?17:236,8);return Joe(i,r,e)}function Joe(r,e,t){let i=Xw.getSymbolTotalCodewords(e),n=iv.getTotalCodewordsCount(e,t),s=i-n,o=iv.getBlocksCount(e,t),a=i%o,c=o-a,l=Math.floor(i/o),d=Math.floor(s/o),u=d+1,f=l-d,h=new Noe(f),g=0,p=new Array(o),I=new Array(o),m=0,w=new Uint8Array(r.buffer);for(let R=0;R<o;R++){let U=R<c?d:u;p[R]=w.slice(g,g+U),I[R]=h.encode(p[R]),g+=U,m=Math.max(m,U)}let B=new Uint8Array(i),C=0,x,Q;for(x=0;x<m;x++)for(Q=0;Q<o;Q++)x<p[Q].length&&(B[C++]=p[Q][x]);for(x=0;x<f;x++)for(Q=0;Q<o;Q++)B[C++]=I[Q][x];return B}function joe(r,e,t,i){let n;if(Array.isArray(r))n=ev.fromArray(r);else if(typeof r=="string"){let l=e;if(!l){let d=ev.rawSplit(r);l=Ww.getBestVersionForData(d,t)}n=ev.fromString(r,l||40)}else throw new Error("Invalid data");let s=Ww.getBestVersionForData(n,t);if(!s)throw new Error("The amount of data is too big to be stored in a QR Code");if(!e)e=s;else if(e<s)throw new Error(` -The chosen QR Code version cannot contain this amount of data. -Minimum version required to store current data is: `+s+`. -`);let o=zoe(e,t,n),a=Xw.getSymbolSize(e),c=new Toe(a);return Moe(c,e),Loe(c),Hoe(c,e),tv(c,t,0),e>=7&&Koe(c,e),Goe(c,o),isNaN(i)&&(i=rv.getBestMask(c,tv.bind(null,c,t))),rv.applyMask(i,c),tv(c,t,i),{modules:c,version:e,errorCorrectionLevel:t,maskPattern:i,segments:n}}vq.create=function(e,t){if(typeof e>"u"||e==="")throw new Error("No input text");let i=Z2.M,n,s;return typeof t<"u"&&(i=Z2.from(t.errorCorrectionLevel,Z2.M),n=Ww.from(t.version),s=rv.from(t.maskPattern),t.toSJISFunc&&Xw.setToSJISFunction(t.toSJISFunc)),joe(e,n,i,s)}});var sv=oe((sbe,Sq)=>{"use strict";var $oe=wt("util"),kq=wt("stream"),Nn=Sq.exports=function(){kq.call(this),this._buffers=[],this._buffered=0,this._reads=[],this._paused=!1,this._encoding="utf8",this.writable=!0};$oe.inherits(Nn,kq);Nn.prototype.read=function(r,e){this._reads.push({length:Math.abs(r),allowLess:r<0,func:e}),process.nextTick(function(){this._process(),this._paused&&this._reads&&this._reads.length>0&&(this._paused=!1,this.emit("drain"))}.bind(this))};Nn.prototype.write=function(r,e){if(!this.writable)return this.emit("error",new Error("Stream not writable")),!1;let t;return Buffer.isBuffer(r)?t=r:t=Buffer.from(r,e||this._encoding),this._buffers.push(t),this._buffered+=t.length,this._process(),this._reads&&this._reads.length===0&&(this._paused=!0),this.writable&&!this._paused};Nn.prototype.end=function(r,e){r&&this.write(r,e),this.writable=!1,this._buffers&&(this._buffers.length===0?this._end():(this._buffers.push(null),this._process()))};Nn.prototype.destroySoon=Nn.prototype.end;Nn.prototype._end=function(){this._reads.length>0&&this.emit("error",new Error("Unexpected end of input")),this.destroy()};Nn.prototype.destroy=function(){this._buffers&&(this.writable=!1,this._reads=null,this._buffers=null,this.emit("close"))};Nn.prototype._processReadAllowingLess=function(r){this._reads.shift();let e=this._buffers[0];e.length>r.length?(this._buffered-=r.length,this._buffers[0]=e.slice(r.length),r.func.call(this,e.slice(0,r.length))):(this._buffered-=e.length,this._buffers.shift(),r.func.call(this,e))};Nn.prototype._processRead=function(r){this._reads.shift();let e=0,t=0,i=Buffer.alloc(r.length);for(;e<r.length;){let n=this._buffers[t++],s=Math.min(n.length,r.length-e);n.copy(i,e,0,s),e+=s,s!==n.length&&(this._buffers[--t]=n.slice(s))}t>0&&this._buffers.splice(0,t),this._buffered-=r.length,r.func.call(this,i)};Nn.prototype._process=function(){try{for(;this._buffered>0&&this._reads&&this._reads.length>0;){let r=this._reads[0];if(r.allowLess)this._processReadAllowingLess(r);else if(this._buffered>=r.length)this._processRead(r);else break}this._buffers&&!this.writable&&this._end()}catch(r){this.emit("error",r)}}});var av=oe(ov=>{"use strict";var Ya=[{x:[0],y:[0]},{x:[4],y:[0]},{x:[0,4],y:[4]},{x:[2,6],y:[0,4]},{x:[0,2,4,6],y:[2,6]},{x:[1,3,5,7],y:[0,2,4,6]},{x:[0,1,2,3,4,5,6,7],y:[1,3,5,7]}];ov.getImagePasses=function(r,e){let t=[],i=r%8,n=e%8,s=(r-i)/8,o=(e-n)/8;for(let a=0;a<Ya.length;a++){let c=Ya[a],l=s*c.x.length,d=o*c.y.length;for(let u=0;u<c.x.length&&c.x[u]<i;u++)l++;for(let u=0;u<c.y.length&&c.y[u]<n;u++)d++;l>0&&d>0&&t.push({width:l,height:d,index:a})}return t};ov.getInterlaceIterator=function(r){return function(e,t,i){let n=e%Ya[i].x.length,s=(e-n)/Ya[i].x.length*8+Ya[i].x[n],o=t%Ya[i].y.length,a=(t-o)/Ya[i].y.length*8+Ya[i].y[o];return s*4+a*r*4}}});var cv=oe((abe,Dq)=>{"use strict";Dq.exports=function(e,t,i){let n=e+t-i,s=Math.abs(n-e),o=Math.abs(n-t),a=Math.abs(n-i);return s<=o&&s<=a?e:o<=a?t:i}});var lv=oe((cbe,_q)=>{"use strict";var Voe=av(),Yoe=cv();function Qq(r,e,t){let i=r*e;return t!==8&&(i=Math.ceil(i/(8/t))),i}var hu=_q.exports=function(r,e){let t=r.width,i=r.height,n=r.interlace,s=r.bpp,o=r.depth;if(this.read=e.read,this.write=e.write,this.complete=e.complete,this._imageIndex=0,this._images=[],n){let a=Voe.getImagePasses(t,i);for(let c=0;c<a.length;c++)this._images.push({byteWidth:Qq(a[c].width,s,o),height:a[c].height,lineIndex:0})}else this._images.push({byteWidth:Qq(t,s,o),height:i,lineIndex:0});o===8?this._xComparison=s:o===16?this._xComparison=s*2:this._xComparison=1};hu.prototype.start=function(){this.read(this._images[this._imageIndex].byteWidth+1,this._reverseFilterLine.bind(this))};hu.prototype._unFilterType1=function(r,e,t){let i=this._xComparison,n=i-1;for(let s=0;s<t;s++){let o=r[1+s],a=s>n?e[s-i]:0;e[s]=o+a}};hu.prototype._unFilterType2=function(r,e,t){let i=this._lastLine;for(let n=0;n<t;n++){let s=r[1+n],o=i?i[n]:0;e[n]=s+o}};hu.prototype._unFilterType3=function(r,e,t){let i=this._xComparison,n=i-1,s=this._lastLine;for(let o=0;o<t;o++){let a=r[1+o],c=s?s[o]:0,l=o>n?e[o-i]:0,d=Math.floor((l+c)/2);e[o]=a+d}};hu.prototype._unFilterType4=function(r,e,t){let i=this._xComparison,n=i-1,s=this._lastLine;for(let o=0;o<t;o++){let a=r[1+o],c=s?s[o]:0,l=o>n?e[o-i]:0,d=o>n&&s?s[o-i]:0,u=Yoe(l,c,d);e[o]=a+u}};hu.prototype._reverseFilterLine=function(r){let e=r[0],t,i=this._images[this._imageIndex],n=i.byteWidth;if(e===0)t=r.slice(1,n+1);else switch(t=Buffer.alloc(n),e){case 1:this._unFilterType1(r,t,n);break;case 2:this._unFilterType2(r,t,n);break;case 3:this._unFilterType3(r,t,n);break;case 4:this._unFilterType4(r,t,n);break;default:throw new Error("Unrecognised filter type - "+e)}this.write(t),i.lineIndex++,i.lineIndex>=i.height?(this._lastLine=null,this._imageIndex++,i=this._images[this._imageIndex]):this._lastLine=t,i?this.read(i.byteWidth+1,this._reverseFilterLine.bind(this)):(this._lastLine=null,this.complete())}});var Tq=oe((lbe,Pq)=>{"use strict";var Woe=wt("util"),Fq=sv(),Xoe=lv(),Zoe=Pq.exports=function(r){Fq.call(this);let e=[],t=this;this._filter=new Xoe(r,{read:this.read.bind(this),write:function(i){e.push(i)},complete:function(){t.emit("complete",Buffer.concat(e))}}),this._filter.start()};Woe.inherits(Zoe,Fq)});var gu=oe((dbe,Uq)=>{"use strict";Uq.exports={PNG_SIGNATURE:[137,80,78,71,13,10,26,10],TYPE_IHDR:1229472850,TYPE_IEND:1229278788,TYPE_IDAT:1229209940,TYPE_PLTE:1347179589,TYPE_tRNS:1951551059,TYPE_gAMA:1732332865,COLORTYPE_GRAYSCALE:0,COLORTYPE_PALETTE:1,COLORTYPE_COLOR:2,COLORTYPE_ALPHA:4,COLORTYPE_PALETTE_COLOR:3,COLORTYPE_COLOR_ALPHA:6,COLORTYPE_TO_BPP_MAP:{0:1,2:3,3:1,4:2,6:4},GAMMA_DIVISION:1e5}});var fv=oe((ube,Rq)=>{"use strict";var dv=[];(function(){for(let r=0;r<256;r++){let e=r;for(let t=0;t<8;t++)e&1?e=3988292384^e>>>1:e=e>>>1;dv[r]=e}})();var uv=Rq.exports=function(){this._crc=-1};uv.prototype.write=function(r){for(let e=0;e<r.length;e++)this._crc=dv[(this._crc^r[e])&255]^this._crc>>>8;return!0};uv.prototype.crc32=function(){return this._crc^-1};uv.crc32=function(r){let e=-1;for(let t=0;t<r.length;t++)e=dv[(e^r[t])&255]^e>>>8;return e^-1}});var Av=oe((fbe,Nq)=>{"use strict";var Ir=gu(),eae=fv(),Qr=Nq.exports=function(r,e){this._options=r,r.checkCRC=r.checkCRC!==!1,this._hasIHDR=!1,this._hasIEND=!1,this._emittedHeadersFinished=!1,this._palette=[],this._colorType=0,this._chunks={},this._chunks[Ir.TYPE_IHDR]=this._handleIHDR.bind(this),this._chunks[Ir.TYPE_IEND]=this._handleIEND.bind(this),this._chunks[Ir.TYPE_IDAT]=this._handleIDAT.bind(this),this._chunks[Ir.TYPE_PLTE]=this._handlePLTE.bind(this),this._chunks[Ir.TYPE_tRNS]=this._handleTRNS.bind(this),this._chunks[Ir.TYPE_gAMA]=this._handleGAMA.bind(this),this.read=e.read,this.error=e.error,this.metadata=e.metadata,this.gamma=e.gamma,this.transColor=e.transColor,this.palette=e.palette,this.parsed=e.parsed,this.inflateData=e.inflateData,this.finished=e.finished,this.simpleTransparency=e.simpleTransparency,this.headersFinished=e.headersFinished||function(){}};Qr.prototype.start=function(){this.read(Ir.PNG_SIGNATURE.length,this._parseSignature.bind(this))};Qr.prototype._parseSignature=function(r){let e=Ir.PNG_SIGNATURE;for(let t=0;t<e.length;t++)if(r[t]!==e[t]){this.error(new Error("Invalid file signature"));return}this.read(8,this._parseChunkBegin.bind(this))};Qr.prototype._parseChunkBegin=function(r){let e=r.readUInt32BE(0),t=r.readUInt32BE(4),i="";for(let s=4;s<8;s++)i+=String.fromCharCode(r[s]);let n=!!(r[4]&32);if(!this._hasIHDR&&t!==Ir.TYPE_IHDR){this.error(new Error("Expected IHDR on beggining"));return}if(this._crc=new eae,this._crc.write(Buffer.from(i)),this._chunks[t])return this._chunks[t](e);if(!n){this.error(new Error("Unsupported critical chunk type "+i));return}this.read(e+4,this._skipChunk.bind(this))};Qr.prototype._skipChunk=function(){this.read(8,this._parseChunkBegin.bind(this))};Qr.prototype._handleChunkEnd=function(){this.read(4,this._parseChunkEnd.bind(this))};Qr.prototype._parseChunkEnd=function(r){let e=r.readInt32BE(0),t=this._crc.crc32();if(this._options.checkCRC&&t!==e){this.error(new Error("Crc error - "+e+" - "+t));return}this._hasIEND||this.read(8,this._parseChunkBegin.bind(this))};Qr.prototype._handleIHDR=function(r){this.read(r,this._parseIHDR.bind(this))};Qr.prototype._parseIHDR=function(r){this._crc.write(r);let e=r.readUInt32BE(0),t=r.readUInt32BE(4),i=r[8],n=r[9],s=r[10],o=r[11],a=r[12];if(i!==8&&i!==4&&i!==2&&i!==1&&i!==16){this.error(new Error("Unsupported bit depth "+i));return}if(!(n in Ir.COLORTYPE_TO_BPP_MAP)){this.error(new Error("Unsupported color type"));return}if(s!==0){this.error(new Error("Unsupported compression method"));return}if(o!==0){this.error(new Error("Unsupported filter method"));return}if(a!==0&&a!==1){this.error(new Error("Unsupported interlace method"));return}this._colorType=n;let c=Ir.COLORTYPE_TO_BPP_MAP[this._colorType];this._hasIHDR=!0,this.metadata({width:e,height:t,depth:i,interlace:!!a,palette:!!(n&Ir.COLORTYPE_PALETTE),color:!!(n&Ir.COLORTYPE_COLOR),alpha:!!(n&Ir.COLORTYPE_ALPHA),bpp:c,colorType:n}),this._handleChunkEnd()};Qr.prototype._handlePLTE=function(r){this.read(r,this._parsePLTE.bind(this))};Qr.prototype._parsePLTE=function(r){this._crc.write(r);let e=Math.floor(r.length/3);for(let t=0;t<e;t++)this._palette.push([r[t*3],r[t*3+1],r[t*3+2],255]);this.palette(this._palette),this._handleChunkEnd()};Qr.prototype._handleTRNS=function(r){this.simpleTransparency(),this.read(r,this._parseTRNS.bind(this))};Qr.prototype._parseTRNS=function(r){if(this._crc.write(r),this._colorType===Ir.COLORTYPE_PALETTE_COLOR){if(this._palette.length===0){this.error(new Error("Transparency chunk must be after palette"));return}if(r.length>this._palette.length){this.error(new Error("More transparent colors than palette size"));return}for(let e=0;e<r.length;e++)this._palette[e][3]=r[e];this.palette(this._palette)}this._colorType===Ir.COLORTYPE_GRAYSCALE&&this.transColor([r.readUInt16BE(0)]),this._colorType===Ir.COLORTYPE_COLOR&&this.transColor([r.readUInt16BE(0),r.readUInt16BE(2),r.readUInt16BE(4)]),this._handleChunkEnd()};Qr.prototype._handleGAMA=function(r){this.read(r,this._parseGAMA.bind(this))};Qr.prototype._parseGAMA=function(r){this._crc.write(r),this.gamma(r.readUInt32BE(0)/Ir.GAMMA_DIVISION),this._handleChunkEnd()};Qr.prototype._handleIDAT=function(r){this._emittedHeadersFinished||(this._emittedHeadersFinished=!0,this.headersFinished()),this.read(-r,this._parseIDAT.bind(this,r))};Qr.prototype._parseIDAT=function(r,e){if(this._crc.write(e),this._colorType===Ir.COLORTYPE_PALETTE_COLOR&&this._palette.length===0)throw new Error("Expected palette not found");this.inflateData(e);let t=r-e.length;t>0?this._handleIDAT(t):this._handleChunkEnd()};Qr.prototype._handleIEND=function(r){this.read(r,this._parseIEND.bind(this))};Qr.prototype._parseIEND=function(r){this._crc.write(r),this._hasIEND=!0,this._handleChunkEnd(),this.finished&&this.finished()}});var hv=oe(Oq=>{"use strict";var qq=av(),tae=[function(){},function(r,e,t,i){if(i===e.length)throw new Error("Ran out of data");let n=e[i];r[t]=n,r[t+1]=n,r[t+2]=n,r[t+3]=255},function(r,e,t,i){if(i+1>=e.length)throw new Error("Ran out of data");let n=e[i];r[t]=n,r[t+1]=n,r[t+2]=n,r[t+3]=e[i+1]},function(r,e,t,i){if(i+2>=e.length)throw new Error("Ran out of data");r[t]=e[i],r[t+1]=e[i+1],r[t+2]=e[i+2],r[t+3]=255},function(r,e,t,i){if(i+3>=e.length)throw new Error("Ran out of data");r[t]=e[i],r[t+1]=e[i+1],r[t+2]=e[i+2],r[t+3]=e[i+3]}],rae=[function(){},function(r,e,t,i){let n=e[0];r[t]=n,r[t+1]=n,r[t+2]=n,r[t+3]=i},function(r,e,t){let i=e[0];r[t]=i,r[t+1]=i,r[t+2]=i,r[t+3]=e[1]},function(r,e,t,i){r[t]=e[0],r[t+1]=e[1],r[t+2]=e[2],r[t+3]=i},function(r,e,t){r[t]=e[0],r[t+1]=e[1],r[t+2]=e[2],r[t+3]=e[3]}];function iae(r,e){let t=[],i=0;function n(){if(i===r.length)throw new Error("Ran out of data");let s=r[i];i++;let o,a,c,l,d,u,f,h;switch(e){default:throw new Error("unrecognised depth");case 16:f=r[i],i++,t.push((s<<8)+f);break;case 4:f=s&15,h=s>>4,t.push(h,f);break;case 2:d=s&3,u=s>>2&3,f=s>>4&3,h=s>>6&3,t.push(h,f,u,d);break;case 1:o=s&1,a=s>>1&1,c=s>>2&1,l=s>>3&1,d=s>>4&1,u=s>>5&1,f=s>>6&1,h=s>>7&1,t.push(h,f,u,d,l,c,a,o);break}}return{get:function(s){for(;t.length<s;)n();let o=t.slice(0,s);return t=t.slice(s),o},resetAfterLine:function(){t.length=0},end:function(){if(i!==r.length)throw new Error("extra data found")}}}function nae(r,e,t,i,n,s){let o=r.width,a=r.height,c=r.index;for(let l=0;l<a;l++)for(let d=0;d<o;d++){let u=t(d,l,c);tae[i](e,n,u,s),s+=i}return s}function sae(r,e,t,i,n,s){let o=r.width,a=r.height,c=r.index;for(let l=0;l<a;l++){for(let d=0;d<o;d++){let u=n.get(i),f=t(d,l,c);rae[i](e,u,f,s)}n.resetAfterLine()}}Oq.dataToBitMap=function(r,e){let t=e.width,i=e.height,n=e.depth,s=e.bpp,o=e.interlace,a;n!==8&&(a=iae(r,n));let c;n<=8?c=Buffer.alloc(t*i*4):c=new Uint16Array(t*i*4);let l=Math.pow(2,n)-1,d=0,u,f;if(o)u=qq.getImagePasses(t,i),f=qq.getInterlaceIterator(t,i);else{let h=0;f=function(){let g=h;return h+=4,g},u=[{width:t,height:i}]}for(let h=0;h<u.length;h++)n===8?d=nae(u[h],c,f,s,r,d):sae(u[h],c,f,s,a,l);if(n===8){if(d!==r.length)throw new Error("extra data found")}else a.end();return c}});var gv=oe((hbe,Mq)=>{"use strict";function oae(r,e,t,i,n){let s=0;for(let o=0;o<i;o++)for(let a=0;a<t;a++){let c=n[r[s]];if(!c)throw new Error("index "+r[s]+" not in palette");for(let l=0;l<4;l++)e[s+l]=c[l];s+=4}}function aae(r,e,t,i,n){let s=0;for(let o=0;o<i;o++)for(let a=0;a<t;a++){let c=!1;if(n.length===1?n[0]===r[s]&&(c=!0):n[0]===r[s]&&n[1]===r[s+1]&&n[2]===r[s+2]&&(c=!0),c)for(let l=0;l<4;l++)e[s+l]=0;s+=4}}function cae(r,e,t,i,n){let s=255,o=Math.pow(2,n)-1,a=0;for(let c=0;c<i;c++)for(let l=0;l<t;l++){for(let d=0;d<4;d++)e[a+d]=Math.floor(r[a+d]*s/o+.5);a+=4}}Mq.exports=function(r,e){let t=e.depth,i=e.width,n=e.height,s=e.colorType,o=e.transColor,a=e.palette,c=r;return s===3?oae(r,c,i,n,a):(o&&aae(r,c,i,n,o),t!==8&&(t===16&&(c=Buffer.alloc(i*n*4)),cae(r,c,i,n,t))),c}});var Kq=oe((gbe,Hq)=>{"use strict";var lae=wt("util"),pv=wt("zlib"),Lq=sv(),dae=Tq(),uae=Av(),fae=hv(),Aae=gv(),xs=Hq.exports=function(r){Lq.call(this),this._parser=new uae(r,{read:this.read.bind(this),error:this._handleError.bind(this),metadata:this._handleMetaData.bind(this),gamma:this.emit.bind(this,"gamma"),palette:this._handlePalette.bind(this),transColor:this._handleTransColor.bind(this),finished:this._finished.bind(this),inflateData:this._inflateData.bind(this),simpleTransparency:this._simpleTransparency.bind(this),headersFinished:this._headersFinished.bind(this)}),this._options=r,this.writable=!0,this._parser.start()};lae.inherits(xs,Lq);xs.prototype._handleError=function(r){this.emit("error",r),this.writable=!1,this.destroy(),this._inflate&&this._inflate.destroy&&this._inflate.destroy(),this._filter&&(this._filter.destroy(),this._filter.on("error",function(){})),this.errord=!0};xs.prototype._inflateData=function(r){if(!this._inflate)if(this._bitmapInfo.interlace)this._inflate=pv.createInflate(),this._inflate.on("error",this.emit.bind(this,"error")),this._filter.on("complete",this._complete.bind(this)),this._inflate.pipe(this._filter);else{let t=((this._bitmapInfo.width*this._bitmapInfo.bpp*this._bitmapInfo.depth+7>>3)+1)*this._bitmapInfo.height,i=Math.max(t,pv.Z_MIN_CHUNK);this._inflate=pv.createInflate({chunkSize:i});let n=t,s=this.emit.bind(this,"error");this._inflate.on("error",function(a){n&&s(a)}),this._filter.on("complete",this._complete.bind(this));let o=this._filter.write.bind(this._filter);this._inflate.on("data",function(a){n&&(a.length>n&&(a=a.slice(0,n)),n-=a.length,o(a))}),this._inflate.on("end",this._filter.end.bind(this._filter))}this._inflate.write(r)};xs.prototype._handleMetaData=function(r){this._metaData=r,this._bitmapInfo=Object.create(r),this._filter=new dae(this._bitmapInfo)};xs.prototype._handleTransColor=function(r){this._bitmapInfo.transColor=r};xs.prototype._handlePalette=function(r){this._bitmapInfo.palette=r};xs.prototype._simpleTransparency=function(){this._metaData.alpha=!0};xs.prototype._headersFinished=function(){this.emit("metadata",this._metaData)};xs.prototype._finished=function(){this.errord||(this._inflate?this._inflate.end():this.emit("error","No Inflate block"))};xs.prototype._complete=function(r){if(this.errord)return;let e;try{let t=fae.dataToBitMap(r,this._bitmapInfo);e=Aae(t,this._bitmapInfo),t=null}catch(t){this._handleError(t);return}this.emit("parsed",e)}});var zq=oe((pbe,Gq)=>{"use strict";var pn=gu();Gq.exports=function(r,e,t,i){let n=[pn.COLORTYPE_COLOR_ALPHA,pn.COLORTYPE_ALPHA].indexOf(i.colorType)!==-1;if(i.colorType===i.inputColorType){let g=(function(){let p=new ArrayBuffer(2);return new DataView(p).setInt16(0,256,!0),new Int16Array(p)[0]!==256})();if(i.bitDepth===8||i.bitDepth===16&&g)return r}let s=i.bitDepth!==16?r:new Uint16Array(r.buffer),o=255,a=pn.COLORTYPE_TO_BPP_MAP[i.inputColorType];a===4&&!i.inputHasAlpha&&(a=3);let c=pn.COLORTYPE_TO_BPP_MAP[i.colorType];i.bitDepth===16&&(o=65535,c*=2);let l=Buffer.alloc(e*t*c),d=0,u=0,f=i.bgColor||{};f.red===void 0&&(f.red=o),f.green===void 0&&(f.green=o),f.blue===void 0&&(f.blue=o);function h(){let g,p,I,m=o;switch(i.inputColorType){case pn.COLORTYPE_COLOR_ALPHA:m=s[d+3],g=s[d],p=s[d+1],I=s[d+2];break;case pn.COLORTYPE_COLOR:g=s[d],p=s[d+1],I=s[d+2];break;case pn.COLORTYPE_ALPHA:m=s[d+1],g=s[d],p=g,I=g;break;case pn.COLORTYPE_GRAYSCALE:g=s[d],p=g,I=g;break;default:throw new Error("input color type:"+i.inputColorType+" is not supported at present")}return i.inputHasAlpha&&(n||(m/=o,g=Math.min(Math.max(Math.round((1-m)*f.red+m*g),0),o),p=Math.min(Math.max(Math.round((1-m)*f.green+m*p),0),o),I=Math.min(Math.max(Math.round((1-m)*f.blue+m*I),0),o))),{red:g,green:p,blue:I,alpha:m}}for(let g=0;g<t;g++)for(let p=0;p<e;p++){let I=h(s,d);switch(i.colorType){case pn.COLORTYPE_COLOR_ALPHA:case pn.COLORTYPE_COLOR:i.bitDepth===8?(l[u]=I.red,l[u+1]=I.green,l[u+2]=I.blue,n&&(l[u+3]=I.alpha)):(l.writeUInt16BE(I.red,u),l.writeUInt16BE(I.green,u+2),l.writeUInt16BE(I.blue,u+4),n&&l.writeUInt16BE(I.alpha,u+6));break;case pn.COLORTYPE_ALPHA:case pn.COLORTYPE_GRAYSCALE:{let m=(I.red+I.green+I.blue)/3;i.bitDepth===8?(l[u]=m,n&&(l[u+1]=I.alpha)):(l.writeUInt16BE(m,u),n&&l.writeUInt16BE(I.alpha,u+2));break}default:throw new Error("unrecognised color Type "+i.colorType)}d+=a,u+=c}return l}});var $q=oe((mbe,jq)=>{"use strict";var Jq=cv();function hae(r,e,t,i,n){for(let s=0;s<t;s++)i[n+s]=r[e+s]}function gae(r,e,t){let i=0,n=e+t;for(let s=e;s<n;s++)i+=Math.abs(r[s]);return i}function pae(r,e,t,i,n,s){for(let o=0;o<t;o++){let a=o>=s?r[e+o-s]:0,c=r[e+o]-a;i[n+o]=c}}function mae(r,e,t,i){let n=0;for(let s=0;s<t;s++){let o=s>=i?r[e+s-i]:0,a=r[e+s]-o;n+=Math.abs(a)}return n}function yae(r,e,t,i,n){for(let s=0;s<t;s++){let o=e>0?r[e+s-t]:0,a=r[e+s]-o;i[n+s]=a}}function Iae(r,e,t){let i=0,n=e+t;for(let s=e;s<n;s++){let o=e>0?r[s-t]:0,a=r[s]-o;i+=Math.abs(a)}return i}function wae(r,e,t,i,n,s){for(let o=0;o<t;o++){let a=o>=s?r[e+o-s]:0,c=e>0?r[e+o-t]:0,l=r[e+o]-(a+c>>1);i[n+o]=l}}function xae(r,e,t,i){let n=0;for(let s=0;s<t;s++){let o=s>=i?r[e+s-i]:0,a=e>0?r[e+s-t]:0,c=r[e+s]-(o+a>>1);n+=Math.abs(c)}return n}function Cae(r,e,t,i,n,s){for(let o=0;o<t;o++){let a=o>=s?r[e+o-s]:0,c=e>0?r[e+o-t]:0,l=e>0&&o>=s?r[e+o-(t+s)]:0,d=r[e+o]-Jq(a,c,l);i[n+o]=d}}function Eae(r,e,t,i){let n=0;for(let s=0;s<t;s++){let o=s>=i?r[e+s-i]:0,a=e>0?r[e+s-t]:0,c=e>0&&s>=i?r[e+s-(t+i)]:0,l=r[e+s]-Jq(o,a,c);n+=Math.abs(l)}return n}var Bae={0:hae,1:pae,2:yae,3:wae,4:Cae},bae={0:gae,1:mae,2:Iae,3:xae,4:Eae};jq.exports=function(r,e,t,i,n){let s;if(!("filterType"in i)||i.filterType===-1)s=[0,1,2,3,4];else if(typeof i.filterType=="number")s=[i.filterType];else throw new Error("unrecognised filter types");i.bitDepth===16&&(n*=2);let o=e*n,a=0,c=0,l=Buffer.alloc((o+1)*t),d=s[0];for(let u=0;u<t;u++){if(s.length>1){let f=1/0;for(let h=0;h<s.length;h++){let g=bae[s[h]](r,c,o,n);g<f&&(d=s[h],f=g)}}l[a]=d,a++,Bae[d](r,c,o,l,a,n),a+=o,c+=o}return l}});var mv=oe((ybe,Vq)=>{"use strict";var ti=gu(),vae=fv(),kae=zq(),Sae=$q(),Dae=wt("zlib"),Wa=Vq.exports=function(r){if(this._options=r,r.deflateChunkSize=r.deflateChunkSize||32*1024,r.deflateLevel=r.deflateLevel!=null?r.deflateLevel:9,r.deflateStrategy=r.deflateStrategy!=null?r.deflateStrategy:3,r.inputHasAlpha=r.inputHasAlpha!=null?r.inputHasAlpha:!0,r.deflateFactory=r.deflateFactory||Dae.createDeflate,r.bitDepth=r.bitDepth||8,r.colorType=typeof r.colorType=="number"?r.colorType:ti.COLORTYPE_COLOR_ALPHA,r.inputColorType=typeof r.inputColorType=="number"?r.inputColorType:ti.COLORTYPE_COLOR_ALPHA,[ti.COLORTYPE_GRAYSCALE,ti.COLORTYPE_COLOR,ti.COLORTYPE_COLOR_ALPHA,ti.COLORTYPE_ALPHA].indexOf(r.colorType)===-1)throw new Error("option color type:"+r.colorType+" is not supported at present");if([ti.COLORTYPE_GRAYSCALE,ti.COLORTYPE_COLOR,ti.COLORTYPE_COLOR_ALPHA,ti.COLORTYPE_ALPHA].indexOf(r.inputColorType)===-1)throw new Error("option input color type:"+r.inputColorType+" is not supported at present");if(r.bitDepth!==8&&r.bitDepth!==16)throw new Error("option bit depth:"+r.bitDepth+" is not supported at present")};Wa.prototype.getDeflateOptions=function(){return{chunkSize:this._options.deflateChunkSize,level:this._options.deflateLevel,strategy:this._options.deflateStrategy}};Wa.prototype.createDeflate=function(){return this._options.deflateFactory(this.getDeflateOptions())};Wa.prototype.filterData=function(r,e,t){let i=kae(r,e,t,this._options),n=ti.COLORTYPE_TO_BPP_MAP[this._options.colorType];return Sae(i,e,t,this._options,n)};Wa.prototype._packChunk=function(r,e){let t=e?e.length:0,i=Buffer.alloc(t+12);return i.writeUInt32BE(t,0),i.writeUInt32BE(r,4),e&&e.copy(i,8),i.writeInt32BE(vae.crc32(i.slice(4,i.length-4)),i.length-4),i};Wa.prototype.packGAMA=function(r){let e=Buffer.alloc(4);return e.writeUInt32BE(Math.floor(r*ti.GAMMA_DIVISION),0),this._packChunk(ti.TYPE_gAMA,e)};Wa.prototype.packIHDR=function(r,e){let t=Buffer.alloc(13);return t.writeUInt32BE(r,0),t.writeUInt32BE(e,4),t[8]=this._options.bitDepth,t[9]=this._options.colorType,t[10]=0,t[11]=0,t[12]=0,this._packChunk(ti.TYPE_IHDR,t)};Wa.prototype.packIDAT=function(r){return this._packChunk(ti.TYPE_IDAT,r)};Wa.prototype.packIEND=function(){return this._packChunk(ti.TYPE_IEND,null)}});var Zq=oe((Ibe,Xq)=>{"use strict";var Qae=wt("util"),Yq=wt("stream"),_ae=gu(),Fae=mv(),Wq=Xq.exports=function(r){Yq.call(this);let e=r||{};this._packer=new Fae(e),this._deflate=this._packer.createDeflate(),this.readable=!0};Qae.inherits(Wq,Yq);Wq.prototype.pack=function(r,e,t,i){this.emit("data",Buffer.from(_ae.PNG_SIGNATURE)),this.emit("data",this._packer.packIHDR(e,t)),i&&this.emit("data",this._packer.packGAMA(i));let n=this._packer.filterData(r,e,t);this._deflate.on("error",this.emit.bind(this,"error")),this._deflate.on("data",function(s){this.emit("data",this._packer.packIDAT(s))}.bind(this)),this._deflate.on("end",function(){this.emit("data",this._packer.packIEND()),this.emit("end")}.bind(this)),this._deflate.end(n)}});var sO=oe((n0,nO)=>{"use strict";var eO=wt("assert").ok,pu=wt("zlib"),Pae=wt("util"),tO=wt("buffer").kMaxLength;function fl(r){if(!(this instanceof fl))return new fl(r);r&&r.chunkSize<pu.Z_MIN_CHUNK&&(r.chunkSize=pu.Z_MIN_CHUNK),pu.Inflate.call(this,r),this._offset=this._offset===void 0?this._outOffset:this._offset,this._buffer=this._buffer||this._outBuffer,r&&r.maxLength!=null&&(this._maxLength=r.maxLength)}function Tae(r){return new fl(r)}function rO(r,e){e&&process.nextTick(e),r._handle&&(r._handle.close(),r._handle=null)}fl.prototype._processChunk=function(r,e,t){if(typeof t=="function")return pu.Inflate._processChunk.call(this,r,e,t);let i=this,n=r&&r.length,s=this._chunkSize-this._offset,o=this._maxLength,a=0,c=[],l=0,d;this.on("error",function(g){d=g});function u(g,p){if(i._hadError)return;let I=s-p;if(eO(I>=0,"have should not go down"),I>0){let m=i._buffer.slice(i._offset,i._offset+I);if(i._offset+=I,m.length>o&&(m=m.slice(0,o)),c.push(m),l+=m.length,o-=m.length,o===0)return!1}return(p===0||i._offset>=i._chunkSize)&&(s=i._chunkSize,i._offset=0,i._buffer=Buffer.allocUnsafe(i._chunkSize)),p===0?(a+=n-g,n=g,!0):!1}eO(this._handle,"zlib binding closed");let f;do f=this._handle.writeSync(e,r,a,n,this._buffer,this._offset,s),f=f||this._writeState;while(!this._hadError&&u(f[0],f[1]));if(this._hadError)throw d;if(l>=tO)throw rO(this),new RangeError("Cannot create final Buffer. It would be larger than 0x"+tO.toString(16)+" bytes");let h=Buffer.concat(c,l);return rO(this),h};Pae.inherits(fl,pu.Inflate);function Uae(r,e){if(typeof e=="string"&&(e=Buffer.from(e)),!(e instanceof Buffer))throw new TypeError("Not a string or buffer");let t=r._finishFlushFlag;return t==null&&(t=pu.Z_FINISH),r._processChunk(e,t)}function iO(r,e){return Uae(new fl(e),r)}nO.exports=n0=iO;n0.Inflate=fl;n0.createInflate=Tae;n0.inflateSync=iO});var yv=oe((wbe,aO)=>{"use strict";var oO=aO.exports=function(r){this._buffer=r,this._reads=[]};oO.prototype.read=function(r,e){this._reads.push({length:Math.abs(r),allowLess:r<0,func:e})};oO.prototype.process=function(){for(;this._reads.length>0&&this._buffer.length;){let r=this._reads[0];if(this._buffer.length&&(this._buffer.length>=r.length||r.allowLess)){this._reads.shift();let e=this._buffer;this._buffer=e.slice(r.length),r.func.call(this,e.slice(0,r.length))}else break}if(this._reads.length>0)return new Error("There are some read requests waitng on finished stream");if(this._buffer.length>0)return new Error("unrecognised content at end of stream")}});var lO=oe(cO=>{"use strict";var Rae=yv(),Nae=lv();cO.process=function(r,e){let t=[],i=new Rae(r);return new Nae(e,{read:i.read.bind(i),write:function(s){t.push(s)},complete:function(){}}).start(),i.process(),Buffer.concat(t)}});var AO=oe((Cbe,fO)=>{"use strict";var dO=!0,uO=wt("zlib"),qae=sO();uO.deflateSync||(dO=!1);var Oae=yv(),Mae=lO(),Lae=Av(),Hae=hv(),Kae=gv();fO.exports=function(r,e){if(!dO)throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0");let t;function i(C){t=C}let n;function s(C){n=C}function o(C){n.transColor=C}function a(C){n.palette=C}function c(){n.alpha=!0}let l;function d(C){l=C}let u=[];function f(C){u.push(C)}let h=new Oae(r);if(new Lae(e,{read:h.read.bind(h),error:i,metadata:s,gamma:d,palette:a,transColor:o,inflateData:f,simpleTransparency:c}).start(),h.process(),t)throw t;let p=Buffer.concat(u);u.length=0;let I;if(n.interlace)I=uO.inflateSync(p);else{let x=((n.width*n.bpp*n.depth+7>>3)+1)*n.height;I=qae(p,{chunkSize:x,maxLength:x})}if(p=null,!I||!I.length)throw new Error("bad png - invalid inflate data response");let m=Mae.process(I,n);p=null;let w=Hae.dataToBitMap(m,n);m=null;let B=Kae(w,n);return n.data=B,n.gamma=l||0,n}});var mO=oe((Ebe,pO)=>{"use strict";var hO=!0,gO=wt("zlib");gO.deflateSync||(hO=!1);var Gae=gu(),zae=mv();pO.exports=function(r,e){if(!hO)throw new Error("To use the sync capability of this library in old node versions, please pin pngjs to v2.3.0");let t=e||{},i=new zae(t),n=[];n.push(Buffer.from(Gae.PNG_SIGNATURE)),n.push(i.packIHDR(r.width,r.height)),r.gamma&&n.push(i.packGAMA(r.gamma));let s=i.filterData(r.data,r.width,r.height),o=gO.deflateSync(s,i.getDeflateOptions());if(s=null,!o||!o.length)throw new Error("bad png - invalid compressed data response");return n.push(i.packIDAT(o)),n.push(i.packIEND()),Buffer.concat(n)}});var yO=oe(Iv=>{"use strict";var Jae=AO(),jae=mO();Iv.read=function(r,e){return Jae(r,e||{})};Iv.write=function(r,e){return jae(r,e)}});var xO=oe(wO=>{"use strict";var $ae=wt("util"),IO=wt("stream"),Vae=Kq(),Yae=Zq(),Wae=yO(),Ai=wO.PNG=function(r){IO.call(this),r=r||{},this.width=r.width|0,this.height=r.height|0,this.data=this.width>0&&this.height>0?Buffer.alloc(4*this.width*this.height):null,r.fill&&this.data&&this.data.fill(0),this.gamma=0,this.readable=this.writable=!0,this._parser=new Vae(r),this._parser.on("error",this.emit.bind(this,"error")),this._parser.on("close",this._handleClose.bind(this)),this._parser.on("metadata",this._metadata.bind(this)),this._parser.on("gamma",this._gamma.bind(this)),this._parser.on("parsed",function(e){this.data=e,this.emit("parsed",e)}.bind(this)),this._packer=new Yae(r),this._packer.on("data",this.emit.bind(this,"data")),this._packer.on("end",this.emit.bind(this,"end")),this._parser.on("close",this._handleClose.bind(this)),this._packer.on("error",this.emit.bind(this,"error"))};$ae.inherits(Ai,IO);Ai.sync=Wae;Ai.prototype.pack=function(){return!this.data||!this.data.length?(this.emit("error","No data provided"),this):(process.nextTick(function(){this._packer.pack(this.data,this.width,this.height,this.gamma)}.bind(this)),this)};Ai.prototype.parse=function(r,e){if(e){let t,i;t=function(n){this.removeListener("error",i),this.data=n,e(null,this)}.bind(this),i=function(n){this.removeListener("parsed",t),e(n,null)}.bind(this),this.once("parsed",t),this.once("error",i)}return this.end(r),this};Ai.prototype.write=function(r){return this._parser.write(r),!0};Ai.prototype.end=function(r){this._parser.end(r)};Ai.prototype._metadata=function(r){this.width=r.width,this.height=r.height,this.emit("metadata",r)};Ai.prototype._gamma=function(r){this.gamma=r};Ai.prototype._handleClose=function(){!this._parser.writable&&!this._packer.readable&&this.emit("close")};Ai.bitblt=function(r,e,t,i,n,s,o,a){if(t|=0,i|=0,n|=0,s|=0,o|=0,a|=0,t>r.width||i>r.height||t+n>r.width||i+s>r.height)throw new Error("bitblt reading outside image");if(o>e.width||a>e.height||o+n>e.width||a+s>e.height)throw new Error("bitblt writing outside image");for(let c=0;c<s;c++)r.data.copy(e.data,(a+c)*e.width+o<<2,(i+c)*r.width+t<<2,(i+c)*r.width+t+n<<2)};Ai.prototype.bitblt=function(r,e,t,i,n,s,o){return Ai.bitblt(this,r,e,t,i,n,s,o),this};Ai.adjustGamma=function(r){if(r.gamma){for(let e=0;e<r.height;e++)for(let t=0;t<r.width;t++){let i=r.width*e+t<<2;for(let n=0;n<3;n++){let s=r.data[i+n]/255;s=Math.pow(s,1/2.2/r.gamma),r.data[i+n]=Math.round(s*255)}}r.gamma=0}};Ai.prototype.adjustGamma=function(){Ai.adjustGamma(this)}});var s0=oe(Al=>{function CO(r){if(typeof r=="number"&&(r=r.toString()),typeof r!="string")throw new Error("Color should be defined as hex string");let e=r.slice().replace("#","").split("");if(e.length<3||e.length===5||e.length>8)throw new Error("Invalid hex color: "+r);(e.length===3||e.length===4)&&(e=Array.prototype.concat.apply([],e.map(function(i){return[i,i]}))),e.length===6&&e.push("F","F");let t=parseInt(e.join(""),16);return{r:t>>24&255,g:t>>16&255,b:t>>8&255,a:t&255,hex:"#"+e.slice(0,6).join("")}}Al.getOptions=function(e){e||(e={}),e.color||(e.color={});let t=typeof e.margin>"u"||e.margin===null||e.margin<0?4:e.margin,i=e.width&&e.width>=21?e.width:void 0,n=e.scale||4;return{width:i,scale:i?4:n,margin:t,color:{dark:CO(e.color.dark||"#000000ff"),light:CO(e.color.light||"#ffffffff")},type:e.type,rendererOpts:e.rendererOpts||{}}};Al.getScale=function(e,t){return t.width&&t.width>=e+t.margin*2?t.width/(e+t.margin*2):t.scale};Al.getImageWidth=function(e,t){let i=Al.getScale(e,t);return Math.floor((e+t.margin*2)*i)};Al.qrToImageData=function(e,t,i){let n=t.modules.size,s=t.modules.data,o=Al.getScale(n,i),a=Math.floor((n+i.margin*2)*o),c=i.margin*o,l=[i.color.light,i.color.dark];for(let d=0;d<a;d++)for(let u=0;u<a;u++){let f=(d*a+u)*4,h=i.color.light;if(d>=c&&u>=c&&d<a-c&&u<a-c){let g=Math.floor((d-c)/o),p=Math.floor((u-c)/o);h=l[s[g*n+p]?1:0]}e[f++]=h.r,e[f++]=h.g,e[f++]=h.b,e[f]=h.a}}});var EO=oe(Cs=>{var Xae=wt("fs"),Zae=xO().PNG,wv=s0();Cs.render=function(e,t){let i=wv.getOptions(t),n=i.rendererOpts,s=wv.getImageWidth(e.modules.size,i);n.width=s,n.height=s;let o=new Zae(n);return wv.qrToImageData(o.data,e,i),o};Cs.renderToDataURL=function(e,t,i){typeof i>"u"&&(i=t,t=void 0),Cs.renderToBuffer(e,t,function(n,s){n&&i(n);let o="data:image/png;base64,";o+=s.toString("base64"),i(null,o)})};Cs.renderToBuffer=function(e,t,i){typeof i>"u"&&(i=t,t=void 0);let n=Cs.render(e,t),s=[];n.on("error",i),n.on("data",function(o){s.push(o)}),n.on("end",function(){i(null,Buffer.concat(s))}),n.pack()};Cs.renderToFile=function(e,t,i,n){typeof n>"u"&&(n=i,i=void 0);let s=!1,o=(...c)=>{s||(s=!0,n.apply(null,c))},a=Xae.createWriteStream(e);a.on("error",o),a.on("close",o),Cs.renderToFileStream(a,t,i)};Cs.renderToFileStream=function(e,t,i){Cs.render(t,i).pack().pipe(e)}});var BO=oe(Zw=>{var ece=s0(),tce={WW:" ",WB:"\u2584",BB:"\u2588",BW:"\u2580"},rce={BB:" ",BW:"\u2584",WW:"\u2588",WB:"\u2580"};function ice(r,e,t){return r&&e?t.BB:r&&!e?t.BW:!r&&e?t.WB:t.WW}Zw.render=function(r,e,t){let i=ece.getOptions(e),n=tce;(i.color.dark.hex==="#ffffff"||i.color.light.hex==="#000000")&&(n=rce);let s=r.modules.size,o=r.modules.data,a="",c=Array(s+i.margin*2+1).join(n.WW);c=Array(i.margin/2+1).join(c+` -`);let l=Array(i.margin+1).join(n.WW);a+=c;for(let d=0;d<s;d+=2){a+=l;for(let u=0;u<s;u++){let f=o[d*s+u],h=o[(d+1)*s+u];a+=ice(f,h,n)}a+=l+` -`}return a+=c.slice(0,-1),typeof t=="function"&&t(null,a),a};Zw.renderToFile=function(e,t,i,n){typeof n>"u"&&(n=i,i=void 0);let s=wt("fs"),o=Zw.render(t,i);s.writeFile(e,o,n)}});var vO=oe(bO=>{bO.render=function(r,e,t){let i=r.modules.size,n=r.modules.data,s="\x1B[40m \x1B[0m",o="\x1B[47m \x1B[0m",a="",c=Array(i+3).join(o),l=Array(2).join(o);a+=c+` -`;for(let d=0;d<i;++d){a+=o;for(let u=0;u<i;u++)a+=n[d*i+u]?s:o;a+=l+` -`}return a+=c+` -`,typeof t=="function"&&t(null,a),a}});var QO=oe(DO=>{var nce="\x1B[47m",sce="\x1B[40m",xv="\x1B[37m",Cv="\x1B[30m",hl="\x1B[0m",oce=nce+Cv,ace=sce+xv,cce=function(r,e,t){return{"00":hl+" "+r,"01":hl+e+"\u2584"+r,"02":hl+t+"\u2584"+r,10:hl+e+"\u2580"+r,11:" ",12:"\u2584",20:hl+t+"\u2580"+r,21:"\u2580",22:"\u2588"}},kO=function(r,e,t,i){let n=e+1;if(t>=n||i>=n||i<-1||t<-1)return"0";if(t>=e||i>=e||i<0||t<0)return"1";let s=i*e+t;return r[s]?"2":"1"},SO=function(r,e,t,i){return kO(r,e,t,i)+kO(r,e,t,i+1)};DO.render=function(r,e,t){let i=r.modules.size,n=r.modules.data,s=!!(e&&e.inverse),o=e&&e.inverse?ace:oce,l=cce(o,s?Cv:xv,s?xv:Cv),d=hl+` -`+o,u=o;for(let f=-1;f<i+1;f+=2){for(let h=-1;h<i;h++)u+=l[SO(n,i,h,f)];u+=l[SO(n,i,i,f)]+d}return u+=hl,typeof t=="function"&&t(null,u),u}});var FO=oe(_O=>{var lce=vO(),dce=QO();_O.render=function(r,e,t){return e&&e.small?dce.render(r,e,t):lce.render(r,e,t)}});var Bv=oe(TO=>{var uce=s0();function PO(r,e){let t=r.a/255,i=e+'="'+r.hex+'"';return t<1?i+" "+e+'-opacity="'+t.toFixed(2).slice(1)+'"':i}function Ev(r,e,t){let i=r+e;return typeof t<"u"&&(i+=" "+t),i}function fce(r,e,t){let i="",n=0,s=!1,o=0;for(let a=0;a<r.length;a++){let c=Math.floor(a%e),l=Math.floor(a/e);!c&&!s&&(s=!0),r[a]?(o++,a>0&&c>0&&r[a-1]||(i+=s?Ev("M",c+t,.5+l+t):Ev("m",n,0),n=0,s=!1),c+1<e&&r[a+1]||(i+=Ev("h",o),o=0)):n++}return i}TO.render=function(e,t,i){let n=uce.getOptions(t),s=e.modules.size,o=e.modules.data,a=s+n.margin*2,c=n.color.light.a?"<path "+PO(n.color.light,"fill")+' d="M0 0h'+a+"v"+a+'H0z"/>':"",l="<path "+PO(n.color.dark,"stroke")+' d="'+fce(o,s,n.margin)+'"/>',d='viewBox="0 0 '+a+" "+a+'"',f='<svg xmlns="http://www.w3.org/2000/svg" '+(n.width?'width="'+n.width+'" height="'+n.width+'" ':"")+d+' shape-rendering="crispEdges">'+c+l+`</svg> -`;return typeof i=="function"&&i(null,f),f}});var UO=oe(ex=>{var Ace=Bv();ex.render=Ace.render;ex.renderToFile=function(e,t,i,n){typeof n>"u"&&(n=i,i=void 0);let s=wt("fs"),a='<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'+ex.render(t,i);s.writeFile(e,a,n)}});var RO=oe(tx=>{var bv=s0();function hce(r,e,t){r.clearRect(0,0,e.width,e.height),e.style||(e.style={}),e.height=t,e.width=t,e.style.height=t+"px",e.style.width=t+"px"}function gce(){try{return document.createElement("canvas")}catch{throw new Error("You need to specify a canvas element")}}tx.render=function(e,t,i){let n=i,s=t;typeof n>"u"&&(!t||!t.getContext)&&(n=t,t=void 0),t||(s=gce()),n=bv.getOptions(n);let o=bv.getImageWidth(e.modules.size,n),a=s.getContext("2d"),c=a.createImageData(o,o);return bv.qrToImageData(c.data,e,n),hce(a,s,o),a.putImageData(c,0,0),s};tx.renderToDataURL=function(e,t,i){let n=i;typeof n>"u"&&(!t||!t.getContext)&&(n=t,t=void 0),n||(n={});let s=tx.render(e,t,n),o=n.type||"image/png",a=n.rendererOpts||{};return s.toDataURL(o,a.quality)}});var qO=oe(o0=>{var pce=O2(),vv=nv(),NO=RO(),mce=Bv();function kv(r,e,t,i,n){let s=[].slice.call(arguments,1),o=s.length,a=typeof s[o-1]=="function";if(!a&&!pce())throw new Error("Callback required as last argument");if(a){if(o<2)throw new Error("Too few arguments provided");o===2?(n=t,t=e,e=i=void 0):o===3&&(e.getContext&&typeof n>"u"?(n=i,i=void 0):(n=i,i=t,t=e,e=void 0))}else{if(o<1)throw new Error("Too few arguments provided");return o===1?(t=e,e=i=void 0):o===2&&!e.getContext&&(i=t,t=e,e=void 0),new Promise(function(c,l){try{let d=vv.create(t,i);c(r(d,e,i))}catch(d){l(d)}})}try{let c=vv.create(t,i);n(null,r(c,e,i))}catch(c){n(c)}}o0.create=vv.create;o0.toCanvas=kv.bind(null,NO.render);o0.toDataURL=kv.bind(null,NO.renderToDataURL);o0.toString=kv.bind(null,function(r,e,t){return mce.render(r,t)})});var HO=oe(Xa=>{var OO=O2(),Sv=nv(),yce=EO(),MO=BO(),Ice=FO(),LO=UO();function a0(r,e,t){if(typeof r>"u")throw new Error("String required as first argument");if(typeof t>"u"&&(t=e,e={}),typeof t!="function")if(OO())e=t||{},t=null;else throw new Error("Callback required as last argument");return{opts:e,cb:t}}function wce(r){return r.slice((r.lastIndexOf(".")-1>>>0)+2).toLowerCase()}function rx(r){switch(r){case"svg":return LO;case"txt":case"utf8":return MO;default:return yce}}function xce(r){switch(r){case"svg":return LO;case"terminal":return Ice;default:return MO}}function c0(r,e,t){if(!t.cb)return new Promise(function(i,n){try{let s=Sv.create(e,t.opts);return r(s,t.opts,function(o,a){return o?n(o):i(a)})}catch(s){n(s)}});try{let i=Sv.create(e,t.opts);return r(i,t.opts,t.cb)}catch(i){t.cb(i)}}Xa.create=Sv.create;Xa.toCanvas=qO().toCanvas;Xa.toString=function(e,t,i){let n=a0(e,t,i),s=n.opts?n.opts.type:void 0,o=xce(s);return c0(o.render,e,n)};Xa.toDataURL=function(e,t,i){let n=a0(e,t,i),s=rx(n.opts.type);return c0(s.renderToDataURL,e,n)};Xa.toBuffer=function(e,t,i){let n=a0(e,t,i),s=rx(n.opts.type);return c0(s.renderToBuffer,e,n)};Xa.toFile=function(e,t,i,n){if(typeof e!="string"||!(typeof t=="string"||typeof t=="object"))throw new Error("Invalid argument");if(arguments.length<3&&!OO())throw new Error("Too few arguments provided");let s=a0(t,i,n),o=s.opts.type||wce(e),c=rx(o).renderToFile.bind(null,e);return c0(c,t,s)};Xa.toFileStream=function(e,t,i){if(arguments.length<2)throw new Error("Too few arguments provided");let n=a0(t,i,e.emit.bind(e,"error")),o=rx("png").renderToFileStream.bind(null,e);c0(o,t,n)}});var GO=oe((Nbe,KO)=>{KO.exports=HO()});function Cce(r,e){try{return l0.authenticator.options={window:1},l0.authenticator.verify({token:r,secret:e})}catch(t){return console.error("[TOTP] Verification error:",t),!1}}function JO(){return l0.authenticator.generateSecret()}async function Dv(r,e){return Cce(e,r)}function jO(r,e,t="OpenList"){return l0.authenticator.keyuri(e,t,r)}async function $O(r){return zO.default.toDataURL(r)}var l0,zO,VO=J(()=>{"use strict";l0=xr(U7(),1),zO=xr(GO(),1)});function Ece(r){let e="";for(let t=0;t<r.length;t++)e+=String.fromCharCode(r[t]);return btoa(e)}function YO(r){let e=String(r||"").replace(/[\s\r\n]/g,"").replace(/-/g,"+").replace(/_/g,"/"),t=e.length%4,i=t?e+"=".repeat(4-t):e;try{let n=atob(i),s=new Uint8Array(n.length);for(let o=0;o<n.length;o++)s[o]=n.charCodeAt(o);return s}catch{return null}}function Qv(r){let e=String(r||"").trim().split(/\s+/);if(e.length<2)return null;let t=e[0];if(!Bce.includes(t))return null;let i=YO(e[1]);return!i||i.length<16?null:{type:t,blobBase64:e[1].replace(/[\s\r\n]/g,""),comment:e.slice(2).join(" ")||""}}async function WO(r){let e=Qv(r);if(!e)return null;let t=YO(e.blobBase64);if(!t)return null;let i=await crypto.subtle.digest("SHA-256",t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength)),n=new Uint8Array(i);return"SHA256:"+Ece(n).replace(/=+$/,"")}function XO(){let r=globalThis;return typeof r.crypto?.randomUUID=="function"?r.crypto.randomUUID():Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,10)+Math.random().toString(36).slice(2,10)}var Bce,ZO=J(()=>{"use strict";Bce=["ssh-rsa","ssh-dss","ssh-ed25519","ecdsa-sha2-nistp256","ecdsa-sha2-nistp384","ecdsa-sha2-nistp521","sk-ssh-ed25519@openssh.com","sk-ecdsa-sha2-nistp256@openssh.com","sk-ssh-ed25519@openssh.com.webauthn","sk-ecdsa-sha2-nistp256@openssh.com.webauthn"]});async function mu(r,e){let i=((await ne(e)).users||[]).find(n=>n.id===r);return i?i.ssh_keys||[]:[]}async function eM(r,e,t,i){let n=Qv(e);if(!n)throw new Error("Invalid OpenSSH public key format");let s=await WO(e);if(!s)throw new Error("Failed to compute SSH key fingerprint");let o=await ne(i),a=(o.users||[]).find(l=>l.id===r);if(!a)throw new Error("User not found");if(Array.isArray(a.ssh_keys)||(a.ssh_keys=[]),a.ssh_keys.some(l=>l.fingerprint===s))throw new Error("SSH key with this fingerprint already exists");let c={id:XO(),name:(t||n.comment||n.type).slice(0,64),public_key:e.trim(),fingerprint:s,created_at:new Date().toISOString()};return a.ssh_keys.push(c),await fe(o,i),c}async function ix(r,e,t){let i=await ne(t),n=(i.users||[]).find(o=>o.id===r);if(!n||!Array.isArray(n.ssh_keys))return!1;let s=n.ssh_keys.length;return n.ssh_keys=n.ssh_keys.filter(o=>o.id!==e),n.ssh_keys.length!==s?(await fe(i,t),!0):!1}var _v=J(()=>{"use strict";Ye();ZO()});async function tM(r){let e=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(r));return Array.from(new Uint8Array(e)).map(t=>t.toString(16).padStart(2,"0")).join("")}async function nx(r){return tM(`${r}-${bce}`)}async function sx(r,e){return tM(`${r}-${e}`)}async function vce(r,e){return sx(await nx(r),e)}function Fv(r=16){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t=new Uint8Array(r);crypto.getRandomValues(t);let i="";for(let n=0;n<r;n++)i+=e[t[n]%e.length];return i}function ox(r){return/^[0-9a-f]{64}$/i.test(String(r||""))}async function Ti(r,e){r.salt=Fv(),r.password=await vce(e,r.salt),r.pwd_update_at=new Date().toISOString()}var bce,d0=J(()=>{"use strict";bce="https://github.com/alist-org/alist"});var rM=J(()=>{Jx()});function kce(r=32){let e=new Uint8Array(r);return crypto.getRandomValues(e),btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}function Pv(r,e={}){let t={...iM,...e},i=kce();return sM(r,t.cookieName,i,{maxAge:t.maxAge,...t.cookieOptions}),i}function nM(r,e={}){let t={...iM,...e};sM(r,t.cookieName,"",{maxAge:0,...t.cookieOptions})}function sM(r,e,t,i={}){let n=[`${e}=${encodeURIComponent(t)}`];i.maxAge!==void 0&&n.push(`Max-Age=${i.maxAge}`),i.httpOnly&&n.push("HttpOnly"),i.secure&&n.push("Secure"),i.sameSite&&n.push(`SameSite=${i.sameSite}`),i.path&&n.push(`Path=${i.path}`),r.header("Set-Cookie",n.join("; "))}var iM,oM=J(()=>{"use strict";rM();iM={cookieName:"csrf_token",headerName:"x-csrf-token",maxAge:3600,cookieOptions:{httpOnly:!0,secure:!0,sameSite:"strict",path:"/"}}});function Sce(){return`${Date.now()}-${Math.random().toString(36).substring(2,15)}`}function Dce(r){let e=r.get?.("user"),t=r.req.header("cf-connecting-ip")||r.req.header("x-forwarded-for")||r.req.header("x-real-ip")||"unknown",i=r.req.header("user-agent")||"unknown";return{user_id:e?.id,username:e?.username,ip_address:t,user_agent:i}}function yu(){if(!Tv){let r=new Rv(1e4);Tv=new Uv(r)}return Tv}var Uv,Rv,Tv,aM=J(()=>{"use strict";Uv=class{storage;constructor(e){this.storage=e}async log(e,t,i,n,s,o={}){let a=Dce(e),c={id:Sce(),timestamp:new Date().toISOString(),level:t,category:i,action:n,message:s,success:o.success??!0,...a,resource_type:o.resource_type,resource_id:o.resource_id,resource_name:o.resource_name,details:o.details,error_message:o.error_message};try{await this.storage.write(c)}catch(l){console.error("[AuditLog] Failed to write log:",l)}}async logLoginSuccess(e,t){await this.log(e,"info","auth","login",`User ${t} logged in successfully`,{success:!0})}async logLoginFailure(e,t,i){await this.log(e,"warn","auth","login_failed",`Failed login attempt for user ${t}`,{success:!1,error_message:i})}async logLogout(e){await this.log(e,"info","auth","logout","User logged out",{success:!0})}async logFileUpload(e,t,i){await this.log(e,"info","file","upload",`Uploaded file: ${t}`,{success:!0,resource_type:"file",resource_name:t,details:{file_size:i}})}async logFileDelete(e,t,i){await this.log(e,i?"critical":"error","file","delete",`Deleted file: ${t}`,{success:i,resource_type:"file",resource_name:t})}async logPermissionChange(e,t,i,n){await this.log(e,"critical","permission","update_permission",`Changed permission for user ${t}`,{success:!0,resource_type:"user",resource_name:t,details:{old_role:i,new_role:n}})}async logUserCreate(e,t,i){await this.log(e,"info","user","create",`Created user: ${t}`,{success:!0,resource_type:"user",resource_name:t,details:{role:i}})}async logUserDelete(e,t){await this.log(e,"critical","user","delete",`Deleted user: ${t}`,{success:!0,resource_type:"user",resource_name:t})}async query(e){return this.storage.query(e)}async count(e){return this.storage.count(e)}},Rv=class{logs=[];maxSize;constructor(e=1e4){this.maxSize=e}async write(e){this.logs.push(e),this.logs.length>this.maxSize&&this.logs.shift()}async query(e){let t=this.logs;e.start_time&&(t=t.filter(s=>s.timestamp>=e.start_time)),e.end_time&&(t=t.filter(s=>s.timestamp<=e.end_time)),e.level&&(t=t.filter(s=>s.level===e.level)),e.category&&(t=t.filter(s=>s.category===e.category)),e.user_id&&(t=t.filter(s=>s.user_id===e.user_id)),e.username&&(t=t.filter(s=>s.username===e.username)),e.action&&(t=t.filter(s=>s.action===e.action)),e.resource_type&&(t=t.filter(s=>s.resource_type===e.resource_type)),e.resource_id&&(t=t.filter(s=>s.resource_id===e.resource_id)),e.success!==void 0&&(t=t.filter(s=>s.success===e.success)),t=t.sort((s,o)=>new Date(o.timestamp).getTime()-new Date(s.timestamp).getTime());let i=e.offset||0,n=e.limit||100;return t.slice(i,i+n)}async count(e){return(await this.query({...e,limit:Number.MAX_SAFE_INTEGER})).length}clear(){this.logs=[]}},Tv=null});var gM={};Cn(gM,{authRouter:()=>mn,authUserFromReq:()=>qn,generateRandomPassword:()=>xu,getOrInitUsers:()=>u0,hashPasswordSHA256:()=>Uce,logoutHandler:()=>Cu,meHandler:()=>cx,meRouter:()=>Iu,meUpdateHandler:()=>ax,verifyUserPassword:()=>wu,verifyUserStaticHash:()=>Ov});function Pce(r){return r.req.header("CF-Connecting-IP")||r.req.header("x-real-ip")||r.req.header("x-forwarded-for")?.split(",")[0]?.trim()||"unknown"}function Nv(r,e){return`${Pce(r)}|${String(e||"").toLowerCase()}`}function qv(r){return`__global__|${String(r||"").toLowerCase()}`}function Tce(r){let e=Fce,t=Math.pow(2,Math.floor(r/5));return Math.min(e*t,lM)}async function cM(r,e,t){let i=Date.now(),n=Es.get(r)||{count:0,lockedUntil:0,attempts:0};try{let{getKvBinding:s}=await Promise.resolve().then(()=>(Ye(),Jr)),o=await s(t);if(o.mode!=="none"&&o.binding){let a=`login_fail:${r}`,c=null;try{c=await o.binding.get(a,"text")}catch{c=await o.binding.get(a)}if(c&&typeof c.text=="function"&&(c=await c.text()),c)try{n=JSON.parse(String(c))}catch{}}}catch{}if(!(n.lockedUntil>i)){if(n.count+=1,n.attempts+=1,n.count>=e){let s=Tce(n.attempts);n.lockedUntil=i+s,n.count=0,console.warn(`[Auth] Login attempts exceeded for ${r}. Locked for ${Math.round(s/6e4)} minutes (attempt #${n.attempts}).`)}Es.set(r,n);try{let{getKvBinding:s}=await Promise.resolve().then(()=>(Ye(),Jr)),o=await s(t);if(o.mode!=="none"&&o.binding){let a=`login_fail:${r}`,c=JSON.stringify(n),l=Math.ceil((lM+36e5)/1e3);typeof o.binding.put=="function"&&await o.binding.put(a,c,{expirationTtl:l})}}catch(s){console.warn("[Auth] Failed to persist login failure to KV:",s)}}}async function dM(r,e,t){if(Es.size>1e4){let c=Date.now();for(let[l,d]of Es)d.lockedUntil<c&&d.count===0&&Es.delete(l)}let i=Date.now(),n=Nv(r,e),s=qv(e),o=Es.get(n),a=Es.get(s);try{let{getKvBinding:c}=await Promise.resolve().then(()=>(Ye(),Jr)),l=await c(t);if(l.mode!=="none"&&l.binding)for(let d of[n,s]){let u=`login_fail:${d}`,f=null;try{f=await l.binding.get(u,"text")}catch{f=await l.binding.get(u)}if(f&&typeof f.text=="function"&&(f=await f.text()),f)try{let h=JSON.parse(String(f));d===n&&(o=h),d===s&&(a=h),Es.set(d,h)}catch{}}}catch{}return!!(o&&o.lockedUntil>i||a&&a.lockedUntil>i)}async function uM(r,e,t){await cM(Nv(r,e),Qce,t),await cM(qv(e),_ce,t)}async function fM(r,e,t){let i=Nv(r,e),n=qv(e);Es.delete(i),Es.delete(n);try{let{getKvBinding:s}=await Promise.resolve().then(()=>(Ye(),Jr)),o=await s(t);if(o.mode!=="none"&&o.binding)for(let a of[i,n]){let c=`login_fail:${a}`;typeof o.binding.delete=="function"&&await o.binding.delete(c)}}catch(s){console.warn("[Auth] Failed to clear login failures from KV:",s)}}async function Uce(r){return nx(r)}async function Ov(r,e){let t=String(r?.password||"").trim().toLowerCase(),i=String(e||"").trim().toLowerCase();if(!t||!ox(i)||!ox(t))return!1;if(r?.salt){let n=(await sx(i,String(r.salt))).toLowerCase();return t===n}return t===i}async function wu(r,e){return String(r?.password||"").trim()?Ov(r,await nx(e)):!1}function xu(){let r=new Uint8Array(16);return crypto.getRandomValues(r),Array.from(r).map(e=>e.toString(16).padStart(2,"0")).join("")}function AM(){if(typeof crypto<"u"&&typeof crypto.randomUUID=="function")return crypto.randomUUID();let r=new Uint8Array(16);return crypto.getRandomValues(r),Array.from(r,e=>e.toString(16).padStart(2,"0")).join("")}async function u0(r){let e=await ne(r);if(!e.users||e.users.length===0){let t=r&&r.ADMIN_PASS||(typeof process<"u"?process.env?.ADMIN_PASS:"")||"",i={id:2,username:"guest",password:"",role:1,permission:0,base_path:"/",disabled:!1,sso_id:"",allow_ldap:!1,pwd_update_at:new Date().toISOString()};if(t){let s={id:1,username:"admin",password:"",role:2,permission:0,base_path:"/",disabled:!1,sso_id:"",allow_ldap:!1,pwd_update_at:new Date().toISOString()};await Ti(s,t),e.users=[s,i]}else e.users=[i];await fe(e,r)||console.warn("[Auth] getOrInitUsers: skipped persisting an uninitialized placeholder DB (expected until POST /api/public/init/setup completes).")}else{let t=e.users.find(s=>s.role===2),i=t?String(t.password||"").trim():"",n=/^[0-9a-f]{64}$/i.test(i);if(t&&!n){let s=r&&r.ADMIN_PASS||(typeof process<"u"?process.env?.ADMIN_PASS:"")||"";s?(await Ti(t,s),await fe(e,r)):console.warn(i?"[SECURITY] Admin password uses a legacy hash format this build cannot verify; it has been left untouched. Log in with your existing password and re-set it, or set ADMIN_PASS to force a reset. It will NOT be reset to a default value.":"[SECURITY] Admin password is empty and no ADMIN_PASS is set \u2014 the system is NOT initialized. Open the site in a browser to run the setup wizard, or set ADMIN_PASS to initialize automatically.")}}return{db:e,users:e.users}}async function qn(r){let e=r.req.header("Authorization");if(!e)return null;let t=e.startsWith("Bearer ")?e.substring(7):e;try{let i=await Dr(r),n=await mo(t,i,"HS256");if(await mw(n?.jti,r.env))return null;let s=await ne(r.env);s.users||(s.users=[]);let o=s.users.find(a=>a.id===n.id||a.username===n.username);return o?{db:s,user:o}:null}catch{return null}}async function hM(r,e){if(!r.otp_secret)return{ok:!0,code:200,httpStatus:200,message:"ok"};let t=String(e.otp_code||e.code||"").trim();return t?await Dv(r.otp_secret,t)?{ok:!0,code:200,httpStatus:200,message:"ok"}:{ok:!1,code:401,httpStatus:401,message:"Invalid OTP code"}:{ok:!1,code:402,httpStatus:200,message:"OTP code required"}}var mn,Iu,Qce,_ce,Fce,lM,Es,ax,cx,Cu,gl=J(()=>{"use strict";Cr();Io();Ye();Fi();VO();_v();d0();oM();aM();mn=new Xe,Iu=new Xe,Qce=5,_ce=20,Fce=900*1e3,lM=1440*60*1e3,Es=new Map;mn.post("/login",async r=>{let e=await r.req.json().catch(()=>({})),t=(e.username||"").trim(),i=e.password||"";if(await dM(r,t,r.env))return r.json({code:429,message:"Too many failed login attempts for this account/IP. Please try again later.",data:null},429);let{users:n,db:s}=await u0(r.env),o=n.find(c=>c.username===t&&!c.disabled);if(o&&await wu(o,i)){let l=String(o.password||"").trim();(/^\$2[aby]\$/.test(l)||!o.salt&&ox(l))&&(console.log(`[Auth] Upgrading password hash for user: ${t}`),await Ti(o,i),await fe(s,r.env));let u=await hM(o,e);if(!u.ok)return r.json({code:u.code,message:u.message,data:null},u.httpStatus);await fM(r,t,r.env);let f={id:o.id,username:o.username,role:o.role,exp:Math.floor(Date.now()/1e3)+3600*24*7,jti:AM()},h=await Dr(r),g=await yo(f,h),p=Pv(r);return await yu().logLoginSuccess(r,t),r.json({code:200,message:"success",data:{token:g,csrf_token:p}})}return await yu().logLoginFailure(r,t,"Invalid credentials"),await uM(r,t,r.env),r.json({code:401,message:"Invalid credentials",data:null},401)});mn.post("/login/hash",async r=>{let e=await r.req.json().catch(()=>({})),t=(e.username||"").trim(),i=String(e.password||"").trim().toLowerCase();if(await dM(r,t,r.env))return r.json({code:429,message:"Too many failed login attempts for this account/IP. Please try again later.",data:null},429);let{users:n,db:s}=await u0(r.env),o=n.find(c=>c.username===t&&!c.disabled);if(o&&i.length===64&&await Ov(o,i)){o.salt||(console.log(`[Auth] Upgrading password hash for user: ${t}`),o.salt=Fv(),o.password=await sx(i,o.salt),o.pwd_update_at=new Date().toISOString(),await fe(s,r.env));let l=await hM(o,e);if(!l.ok)return r.json({code:l.code,message:l.message,data:null},l.httpStatus);await fM(r,t,r.env);let d={id:o.id,username:o.username,role:o.role,exp:Math.floor(Date.now()/1e3)+3600*24*7,jti:AM()},u=await Dr(r),f=await yo(d,u),h=Pv(r);return await yu().logLoginSuccess(r,t),r.json({code:200,message:"success",data:{token:f,csrf_token:h}})}return await yu().logLoginFailure(r,t,"Invalid credentials"),await uM(r,t,r.env),r.json({code:401,message:"Invalid credentials",data:null},401)});ax=async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let{db:t,user:i}=e,n=await r.req.json().catch(()=>({}));if(n.username&&n.username.trim()!==""){let s=n.username.trim();if(t.users.some(a=>a.id!==i.id&&a.username===s))return r.json({code:400,message:"Username already exists",data:null},400);i.username=s}return n.password&&n.password.trim()!==""&&await Ti(i,n.password.trim()),await fe(t,r.env),r.json({code:200,message:"success",data:null})},cx=async r=>{let e=await Me(r);return!e||e.disabled?r.json({code:401,message:"Unauthorized",data:null},401):r.json({code:200,message:"success",data:{id:e.id,username:e.username,role:e.role,permission:e.permission??0,base_path:e.base_path||"/",disabled:!!e.disabled,sso_id:e.sso_id||"",allow_ldap:!!e.allow_ldap,otp:!!e.otp_secret}})};mn.get("/me",cx);mn.post("/me/update",ax);Cu=async r=>{let e=r.req.header("Authorization");if(e){let i=e.startsWith("Bearer ")?e.substring(7):e;try{let n=await Dr(r),s=await mo(i,n,"HS256");s?.jti&&await zb(s.jti,s.exp,r.env)}catch{}}return nM(r),await yu().logLogout(r),r.json({code:200,message:"success",data:null})};mn.get("/logout",Cu);mn.post("/logout",Cu);mn.get("/csrf",async r=>{try{let e=await jb(r);return r.json({code:200,message:"success",data:{csrf_token:e}})}catch{return r.json({code:500,message:"Failed to generate CSRF token",data:null},500)}});mn.post("/2fa/generate",async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let{user:t}=e;if(t.otp_secret)return r.json({code:400,message:"2FA already enabled",data:null},400);let i=JO(),n=jO(i,t.username);return r.json({code:200,message:"success",data:{qr:$O(n),secret:i}})});mn.post("/2fa/verify",async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let{db:t,user:i}=e,n=await r.req.json().catch(()=>({})),s=String(n.code||"").trim(),o=String(n.secret||"").trim();return o?/^[A-Z2-7]+$/i.test(o)?await Dv(o,s)?(i.otp_secret=o.toUpperCase(),await fe(t,r.env),r.json({code:200,message:"success",data:null})):r.json({code:400,message:"Invalid code",data:null},400):r.json({code:400,message:"Invalid secret format",data:null},400):r.json({code:400,message:"Missing secret parameter",data:null},400)});Iu.get("/sshkey/list",async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await mu(e.user.id,r.env);return r.json({code:200,message:"success",data:{content:t,total:t.length}})});Iu.post("/sshkey/add",async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await r.req.json().catch(()=>({}));try{let i=await eM(e.user.id,t.key||t.public_key||"",t.name||t.title||"",r.env);return r.json({code:200,message:"success",data:i})}catch(i){return r.json({code:400,message:i.message||"Failed to add SSH key",data:null},400)}});Iu.post("/sshkey/delete",async r=>{let e=await qn(r);if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let t=r.req.query("id");if(!t)return r.json({code:400,message:"Missing id parameter",data:null},400);if(!await ix(e.user.id,t,r.env))return r.json({code:404,message:"SSH key not found",data:null},404);let n=await mu(e.user.id,r.env);return r.json({code:200,message:"success",data:n})})});Cr();var Vk=r=>{let e={origin:"*",allowMethods:["GET","HEAD","PUT","POST","DELETE","PATCH","QUERY"],allowHeaders:[],exposeHeaders:[],...r},t=(n=>typeof n=="string"?n==="*"?()=>n:s=>n===s?s:null:typeof n=="function"?n:s=>n.includes(s)?s:null)(e.origin),i=(n=>typeof n=="function"?n:Array.isArray(n)?()=>n:()=>[])(e.allowMethods);return async function(s,o){function a(l,d){s.res.headers.set(l,d)}let c=await t(s.req.header("origin")||"",s);if(c&&a("Access-Control-Allow-Origin",c),e.credentials&&a("Access-Control-Allow-Credentials","true"),e.exposeHeaders?.length&&a("Access-Control-Expose-Headers",e.exposeHeaders.join(",")),s.req.method==="OPTIONS"){e.origin!=="*"&&a("Vary","Origin"),e.maxAge!=null&&a("Access-Control-Max-Age",e.maxAge.toString());let l=await i(s.req.header("origin")||"",s);l.length&&a("Access-Control-Allow-Methods",l.join(","));let d=e.allowHeaders;if(!d?.length){let u=s.req.header("Access-Control-Request-Headers");u&&(d=u.split(",").map(f=>f.trim()))}return d?.length&&(a("Access-Control-Allow-Headers",d.join(",")),s.res.headers.append("Vary","Access-Control-Request-Headers")),s.res.headers.delete("Content-Length"),s.res.headers.delete("Content-Type"),new Response(null,{headers:s.res.headers,status:204,statusText:"No Content"})}await o(),e.origin!=="*"&&s.header("Vary","Origin",{append:!0})}};Ye();gc();Cr();Yt();Ye();var qA=r=>{let e=[];for(let t of String(r||"").replace(/\\/g,"/").split("/"))if(!(t===""||t===".")){if(t===".."){e.pop();continue}e.push(t)}return"/"+e.join("/")};async function Yd(r,e,t){let n=qA(r).split("/").filter(Boolean);if(n.length<1)return{ok:!1,error:"Invalid share path"};let s,o;if(n[0]==="@s"){if(n.length<2)return{ok:!1,error:"Invalid share path"};s=n[1],o=n.slice(2)}else s=n[0],o=n.slice(1);let a=await ne(t),c=(a.shares||[]).find(g=>g.id===s);if(!c)return{ok:!1,error:"share not found"};if(c.disabled)return{ok:!1,error:"share has been disabled"};if(c.expires&&new Date(c.expires)<new Date)return{ok:!1,error:"share has expired"};if(c.max_accessed>0&&c.accessed!==void 0&&c.accessed>=c.max_accessed)return{ok:!1,error:"share access count exceeded"};if(c.pwd&&c.pwd!==e)return{ok:!1,error:"wrong password"};if(!c.files||c.files.length===0)return{ok:!1,error:"share is empty"};if(c.accessed=(c.accessed||0)+1,fe(a,t).catch(()=>{}),c.files.length>1&&o.length===0)return{ok:!0,share:c,virtualList:!0};let l=(c.files||[]).map(g=>qA(g)),d=g=>l.some(p=>g===p||g.startsWith(p==="/"?"/":p+"/"));if(c.files.length===1)return o.length>0?{ok:!1,error:"path not found in share"}:{ok:!0,share:c,realPath:qA(c.files[0])};let u=o[0],f=c.files.find(g=>{let p=String(g).split("/").filter(Boolean);return p[p.length-1]===u});if(!f)return{ok:!1,error:"path not found in share"};let h=qA([qA(f),...o.slice(1)].join("/"));return d(h)?{ok:!0,share:c,realPath:h}:{ok:!1,error:"path not found in share"}}Ye();Fi();var La={SEE_HIDES:0,ACCESS_WITHOUT_PASSWORD:1,OFFLINE_DOWNLOAD:2,WRITE_CONTENT:3,RENAME:4,MOVE:5,COPY:6,DELETE:7,WEBDAV_READ:8,WEBDAV_MANAGE:9,FTP_READ:10,FTP_MANAGE:11,READ_ARCHIVES:12,DECOMPRESS:13,SHARE:14,CUSTOMIZE_SHARE_ID:15};function ise(r){return!r||r.role===1}function Is(r){return!!r&&r.role===2}function Ha(r,e){return!r||r.disabled?!1:Is(r)?!0:ise(r)?!1:(r.permission>>e&1)===1}function pt(r){return Ha(r,La.WRITE_CONTENT)}function We(r,e="/"){let t=e||"/";if(t.startsWith("/@s"))return t;let i=(r?.base_path||"/").trim();if(!i||i==="/")return t.startsWith("/")?t:`/${t}`;i.startsWith("/")||(i=`/${i}`),i.endsWith("/")&&i.length>1&&(i=i.replace(/\/+$/,""));let n=t.startsWith("/")?t:`/${t}`;return n==="/"?i:`${i}${n}`}Ye();async function $b(r,e){let i=(await ne(e)).metas||[];if(i.length===0)return null;let n=HA(r),s=i.find(a=>HA(a.path)===n);if(s)return s;let o=n;for(;o!=="/";){let a=o.substring(0,o.lastIndexOf("/"))||"/",c=i.find(l=>HA(l.path)===a);if(c)return c;o=a}return null}function al(r,e,t){let i=HA(r),n=HA(e);if(i.toLowerCase()===n.toLowerCase())return!0;if(!t)return!1;let s=n;for(;s!=="/";)if(s=s.substring(0,s.lastIndexOf("/"))||"/",i.toLowerCase()===s.toLowerCase())return!0;return!1}function HA(r){let e=(r||"/").replace(/\\/g,"/").replace(/\/+/g,"/").trim();return e===""||e==="/"?"/":"/"+e.split("/").filter(Boolean).join("/")}function Vb(r,e,t,i=""){return r&&Is(r)||!e?!0:!(e.password&&e.password.trim()!==""&&al(e.path,t,e.p_sub)&&i!==e.password||e.read_users&&e.read_users.length>0&&al(e.path,t,e.read_users_sub)&&(!r||!e.read_users.includes(r.id||0)))}function Yb(r,e,t){return r===null||r&&Is(r)?!0:e&&e.write_users&&e.write_users.length>0&&al(e.path,t,e.write_users_sub)?e.write_users.includes(r.id||0):!0}function Wb(r,e){return r?r.write&&al(r.path,e,r.w_sub):!1}function Xb(r,e){return!r||!r.readme?"":al(r.path,e,r.r_sub)?r.readme:""}function Zb(r,e){return!r||!r.header?"":al(r.path,e,r.header_sub)?r.header:""}function _9(r,e,t){if(!r||!r.hide||r.hide.trim()===""||!al(r.path,e,r.h_sub))return!1;let i=r.hide.split(` -`).map(n=>n.trim()).filter(Boolean);for(let n of i)try{if(new RegExp(n).test(t))return!0}catch{}return!1}function e2(r){if(!r||r.trim()==="")return null;let e=r.split(` -`).map(t=>t.trim()).filter(Boolean);for(let t of e)try{new RegExp(t)}catch{return t}return null}Ye();Fi();Gr();Zu();function nse(r,e){if(typeof r!="string"||typeof e!="string"||r.length!==e.length)return!1;let t=0;for(let i=0;i<r.length;i++)t|=r.charCodeAt(i)^e.charCodeAt(i);return t===0}async function yw(r){try{let e=await ne(r?.env),t={};for(let s of e.settings||[])t[s.key]=s.value;let i=t.sign_all==="true",n=parseInt(t.link_expiration,10)||0;return!i&&n<=0?{enabled:!1,expiresIn:0}:{enabled:!0,expiresIn:n>0?n*3600:0}}catch{return{enabled:!1,expiresIn:0}}}function eu(r){return"/"+String(r||"").split("/").filter(Boolean).join("/")}function sse(r,e,t){let i=eu(r).toLowerCase(),n=eu(e).toLowerCase();if(i===n)return!0;if(!t)return!1;for(;n!=="/";)if(n=eu(n.split("/").slice(0,-1).join("/")),i===n)return!0;return!1}async function ose(r,e){try{let i=(await ne(r?.env)).metas||[];if(!i.length)return null;let n=eu(e);for(;;){let s=i.find(o=>eu(o.path).toLowerCase()===n.toLowerCase());if(s)return s;if(n==="/")return null;n=eu(n.split("/").slice(0,-1).join("/"))}}catch{return null}}async function ase(r,e){try{let{resolvePath:t}=await Promise.resolve().then(()=>(Ye(),Jr)),i=await t(e);return i?.storage?lQ(i.storage):!1}catch{return!1}}async function Iw(r,e){if(await ase(r,e))return!0;let t=await ose(r,e);return!t||!t.password?!1:sse(t.path,e,!!t.p_sub)}async function F9(r,e){return(await yw(r)).enabled?!0:await Iw(r,e)}async function tu(r){try{let e=await ne(r?.env);for(let t of e.settings||[])if(t.key==="link_expiration"){let i=parseInt(t.value,10)||0;if(i>0)return i*3600;break}}catch{}return 0}async function ru(r,e,t){let i=await Dr(r),n=t===0?0:Math.floor(Date.now()/1e3)+t,s=await Wx(`${e}:${n}`,i);return`${n}.${s}`}async function P9(r,e,t){let i=t.lastIndexOf(".");if(i<=0)return!1;let n=parseInt(t.slice(0,i),10),s=t.slice(i+1);if(!Number.isFinite(n)||n!==0&&n<=Math.floor(Date.now()/1e3))return!1;let o=await Dr(r),a=await Wx(`${e}:${n}`,o);return nse(a,s)}Ma();Nl();Yt();async function iu(r={},e){let t=(r.parent||"/").replace(/\/+/g,"/")||"/",i=String(r.keywords||"").trim().toLowerCase(),n=r.scope??0,s=Math.max(1,r.page||1),o=Math.max(1,Math.min(100,r.per_page||30)),a=Math.min(20,Math.max(0,r.max_depth??10)),c=Math.min(2e3,Math.max(1,r.max_results??500)),l=[];async function d(g,p){if(p>a||l.length>=c)return;let I=[];try{I=(await Zt(g)).content||[]}catch{return}for(let m of I){if(l.length>=c)break;let w=!i||m.name.toLowerCase().includes(i),B=!!m.is_dir,C=B&&!m.sign,x=!0;if(n===1&&!B&&(x=!1),n===2&&B&&(x=!1),!C&&w&&x&&l.push({...m,parent:g.endsWith("/")&&g!=="/"?g.slice(0,-1):g}),B){let Q=g==="/"?`/${m.name}`:`${g}/${m.name}`;await d(Q,p+1)}}}await d(t,0);let u=l.length,f=(s-1)*o;return{content:l.slice(f,f+o),total:u}}Zu();function ww(r){let e=new DataView(r),t=r.byteLength,i=-1,n=Math.max(0,t-22-65557);for(let l=t-22;l>=n;l--)if(e.getUint32(l,!0)===101010256){i=l;break}if(i===-1)throw new Error("Invalid ZIP: End of Central Directory not found");let s=e.getUint16(i+10,!0),o=e.getUint32(i+16,!0),a=[],c=o;for(let l=0;l<s&&e.getUint32(c,!0)===33639248;l++){let d=e.getUint16(c+10,!0),u=e.getUint32(c+20,!0),f=e.getUint32(c+24,!0),h=e.getUint16(c+28,!0),g=e.getUint16(c+30,!0),p=e.getUint16(c+32,!0),I=e.getUint32(c+42,!0),m=e.getUint16(c+12,!0),w=e.getUint16(c+14,!0),B=cse(w,m),C=new Uint8Array(r,c+46,h),x=new TextDecoder("utf-8").decode(C);if(c+=46+h+g+p,x.endsWith("/")||e.getUint32(I,!0)!==67324752)continue;let Q=e.getUint16(I+26,!0),R=e.getUint16(I+28,!0),U=I+30+Q+R;a.push({name:x.replace(/\\/g,"/"),isDir:!1,size:f,compressedSize:u,compressionMethod:d,dataOffset:U,modified:B})}return{entries:a}}async function T9(r,e){let t=new Uint8Array(r,e.dataOffset,e.compressedSize);if(e.compressionMethod===0)return new Uint8Array(t);if(e.compressionMethod===8){if(typeof DecompressionStream>"u")throw new Error("DecompressionStream is not supported in this runtime");let i=new DecompressionStream("deflate-raw"),n=i.writable.getWriter();n.write(t),n.close();let s=await new Response(i.readable).arrayBuffer();return new Uint8Array(s)}throw new Error(`Unsupported ZIP compression method: ${e.compressionMethod}`)}function cse(r,e){let t=1980+(r>>9&127),i=r>>5&15,n=r&31,s=e>>11&31,o=e>>5&63,a=(e&31)*2,c=l=>String(l).padStart(2,"0");return`${t}-${c(i)}-${c(n)}T${c(s)}:${c(o)}:${c(a)}Z`}xc();Cr();Fi();xc();Ma();Ye();Yt();var GA=xr(jm(),1);var xw="openlist-sharing-seed";var lse={md5:32,sha1:40,sha256:64};function yr(r){return typeof r=="string"?r:""}var dse=64,use=4096;function U9(r){let e=yr(r).trim();if(e.length>use)throw new Error("Seed file path exceeds the maximum length");let t=e.split("/");if(t.length>dse)throw new Error("Seed file path exceeds the maximum depth");if(e.includes("\0")||e.includes("\\")||e.startsWith("/")||t.length===0||t.some(i=>!i||i==="."||i===".."))throw new Error("Seed file path must be a safe relative path");return t.join("/")}function KA(r,e){let t=yr(r).toLowerCase();if(t&&!new RegExp(`^[0-9a-f]{${lse[e]}}$`).test(t))throw new Error(`Invalid ${e} hash`);return t}function fse(r){return Array.isArray(r)?r.map(e=>{if(!e||typeof e!="object")throw new Error("Invalid seed source");let t=yr(e.type),i=yr(e.url);if(!t||!i)throw new Error("Seed source type and URL are required");try{if(!new URL(i).protocol)throw new Error("missing scheme")}catch{throw new Error("Seed source URL must be absolute")}return{type:t,url:i,expires_at:yr(e.expires_at),share_id:yr(e.share_id)}}):[]}function zi(r){if(!r||typeof r!="object")throw new Error("Invalid seed payload");let e=r;if(e.format!==void 0&&e.format!==xw)throw new Error("Unsupported seed format");if(e.version!==void 0&&Number(e.version)!==1)throw new Error("Unsupported seed version");let t=Number(e.piece_size||10485760);if(!Number.isSafeInteger(t)||t<16*1024||t>64*1024*1024)throw new Error("Invalid piece_size");if(!Array.isArray(e.files)||e.files.length===0||e.files.length>1e5)throw new Error("Seed file count must be between 1 and 100000");let i=e.files.map(o=>{if(!o||typeof o!="object")throw new Error("Invalid seed file");let a=Number(o.size);if(!Number.isSafeInteger(a)||a<0)throw new Error("Invalid seed file size");let c=o.hashes||{},l=c.pieces||{},d=a>0?Math.ceil(a/t):0,u=f=>{if(!Array.isArray(l[f]))return[];let h=l[f].map(g=>KA(g,f));if(h.length!==0&&h.length!==d)throw new Error(`${f} piece count is ${h.length}, expected ${d}`);return h};return{path:U9(o.path),size:a,modified:yr(o.modified),comment:yr(o.comment),hashes:{md5:KA(c.md5,"md5"),sha1:KA(c.sha1,"sha1"),sha256:KA(c.sha256,"sha256"),pieces:{md5:u("md5"),sha1:u("sha1"),sha256:u("sha256")}},sources:fse(o.sources),cas_slice_md5:KA(o.cas_slice_md5,"md5"),cas_create_time:yr(o.cas_create_time),cas_cloud:yr(o.cas_cloud).trim(),missing_channels:Array.isArray(o.missing_channels)?o.missing_channels.map(yr).filter(Boolean):[]}}),n=new Set;for(let o of i){if(n.has(o.path))throw new Error(`Duplicate seed file path: ${o.path}`);n.add(o.path)}let s=yr(e.name)||i[0].path.split("/")[0];if(!s.trim()||s==="."||s===".."||/[\\/\0]/.test(s))throw new Error("Invalid seed name");return{format:xw,version:1,name:s,comment:yr(e.comment),created_at:yr(e.created_at)||new Date().toISOString(),created_by:yr(e.created_by)||"OpenList",piece_size:t,trackers:Array.isArray(e.trackers)?e.trackers.map(yr).filter(Boolean):[],channels:Array.isArray(e.channels)?e.channels.map(o=>{if(!o||typeof o!="object"||!yr(o.driver).trim())throw new Error("Seed channel driver is required");let a=yr(o.mount_path);if(/[?#\0]/.test(a))throw new Error("Seed channel mount_path contains invalid characters");return{driver:yr(o.driver).trim(),mount_path:a}}):[],files:i}}function Cw(r,e){let t=r&&typeof r=="object"?r:{},i=o=>({whole:!!t[o]?.whole,pieces:!!t[o]?.pieces}),n={md5:i("md5"),sha1:i("sha1"),sha256:i("sha256")};!n.md5.whole&&!n.md5.pieces&&!n.sha1.whole&&!n.sha1.pieces&&!n.sha256.whole&&!n.sha256.pieces&&(n={md5:{whole:!0,pieces:!0},sha1:{whole:!0,pieces:!0},sha256:{whole:!0,pieces:!0}});for(let o of e)o==="torrent"&&(n.sha1={whole:!0,pieces:!0}),o==="cas"&&(n.md5={whole:!0,pieces:!0});return n}function t2(r,e){let t={md5:e.md5.whole?r.hashes.md5:"",sha1:e.sha1.whole?r.hashes.sha1:"",sha256:e.sha256.whole?r.hashes.sha256:"",pieces:{md5:e.md5.pieces?r.hashes.pieces.md5:[],sha1:e.sha1.pieces?r.hashes.pieces.sha1:[],sha256:e.sha256.pieces?r.hashes.pieces.sha256:[]}};return{...r,hashes:t}}function R9(r){return U9(r)}var Rn=new TextEncoder,Ga=new TextDecoder("utf-8",{fatal:!0}),hse=32,gse=1e5;function Ew(r){let e=r.reduce((n,s)=>n+s.byteLength,0),t=new Uint8Array(e),i=0;for(let n of r)t.set(n,i),i+=n.byteLength;return t}function nu(r){if(r instanceof Uint8Array)return Ew([Rn.encode(`${r.byteLength}:`),r]);if(typeof r=="string")return nu(Rn.encode(r));if(typeof r=="number"){if(!Number.isSafeInteger(r))throw new Error("Bencode integer is out of range");return Rn.encode(`i${r}e`)}if(Array.isArray(r))return Ew([Rn.encode("l"),...r.map(nu),Rn.encode("e")]);let e=Object.entries(r).sort(([t],[i])=>{let n=Rn.encode(t),s=Rn.encode(i),o=Math.min(n.length,s.length);for(let a=0;a<o;a++)if(n[a]!==s[a])return n[a]-s[a];return n.length-s.length});return Ew([Rn.encode("d"),...e.flatMap(([t,i])=>[nu(t),nu(i)]),Rn.encode("e")])}function pse(r){let e=0,t=0,i=s=>{if(++t>gse)throw new Error("Bencode item limit exceeded");if(s>hse)throw new Error("Bencode nesting limit exceeded");let o=r[e];if(o===105){let a=r.indexOf(101,++e);if(a<0)throw new Error("Unterminated bencode integer");let c=Ga.decode(r.subarray(e,a));if(!/^(0|-?[1-9]\d*)$/.test(c))throw new Error("Invalid bencode integer");let l=Number(c);if(!Number.isSafeInteger(l))throw new Error("Bencode integer is out of range");return e=a+1,l}if(o===108){e++;let a=[];for(;e<r.length&&r[e]!==101;)a.push(i(s+1));if(r[e++]!==101)throw new Error("Unterminated bencode list");return a}if(o===100){e++;let a={};for(;e<r.length&&r[e]!==101;){let c=i(s+1);if(!(c instanceof Uint8Array))throw new Error("Bencode dictionary key is not a string");let l=Ga.decode(c);a[l]=i(s+1)}if(r[e++]!==101)throw new Error("Unterminated bencode dictionary");return a}if(o>=48&&o<=57){let a=r.indexOf(58,e);if(a<0)throw new Error("Invalid bencode byte string");let c=Ga.decode(r.subarray(e,a));if(!/^(0|[1-9]\d*)$/.test(c))throw new Error("Invalid bencode byte string length");let l=Number(c),d=a+1,u=d+l;if(!Number.isSafeInteger(l)||u>r.length)throw new Error("Truncated bencode byte string");return e=u,r.slice(d,u)}throw new Error("Invalid bencode value")},n=i(0);if(e!==r.length)throw new Error("Trailing bencode data");return n}function su(r,e){if(!r||typeof r!="object"||Array.isArray(r)||r instanceof Uint8Array)throw new Error(`${e} must be a dictionary`);return r}function mse(r){return r instanceof Uint8Array?r:new Uint8Array}function un(r){return r instanceof Uint8Array?Ga.decode(r):typeof r=="string"?r:""}function Ka(r){return typeof r=="number"?r:0}function yse(r){return Array.isArray(r)?r.map(e=>un(e)).filter(Boolean):[]}function r2(r){return r==null?"":typeof r=="string"||typeof r=="number"||r instanceof Uint8Array?r:typeof r=="boolean"?r?1:0:Array.isArray(r)?r.map(r2):typeof r=="object"?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,r2(t)])):String(r)}function i2(r){return r instanceof Uint8Array?Ga.decode(r):Array.isArray(r)?r.map(i2):r&&typeof r=="object"?Object.fromEntries(Object.entries(r).map(([e,t])=>[e,i2(t)])):r}function Ise(r){if(!r||r.length%2!==0||!/^[0-9a-f]+$/i.test(r))return new Uint8Array;let e=new Uint8Array(r.length/2);for(let t=0;t<e.length;t++)e[t]=parseInt(r.slice(t*2,t*2+2),16);return e}function wse(r){return Array.from(r,e=>e.toString(16).padStart(2,"0")).join("")}function xse(r){if(r.files.length>1){for(let e of r.files.slice(0,-1))if(e.size%r.piece_size!==0)throw new Error("Cannot convert this multi-file seed to a standard torrent: a file boundary splits a piece")}return r.files.flatMap(e=>e.hashes.pieces.sha1)}async function Cse(r,e){if(!r.hashes.md5)return;let t=r.cas_slice_md5?r.cas_slice_md5.toUpperCase():"",i=r.hashes.pieces.md5.map(n=>n.toUpperCase());if(!t&&i.length>0&&e===10485760&&(t=i.length>1?(await(0,GA.md5)(i.join(` -`))).toUpperCase():i[0]),!!t)return{cloud:r.cas_cloud||"189",file_md5:r.hashes.md5.toUpperCase(),slice_md5:t,slice_md5s:i,slice_size:e}}function Ese(r){return Rn.encode(JSON.stringify(zi(r),null,2))}function n2(r){let e=JSON.parse(Ga.decode(r));if(e?.format!==xw||Number(e?.version)!==1||!e?.created_at||!e?.created_by)throw new Error("Invalid or incomplete OpenList sharing seed");return zi(e)}async function N9(r,e){if(!r.hashes.md5)throw new Error(`CAS requires a whole-file MD5 for ${r.path}`);let t=r.cas_slice_md5?r.cas_slice_md5.toUpperCase():"";if(!t&&r.hashes.pieces.md5.length>0&&e===10485760){let n=r.hashes.pieces.md5.map(s=>s.toUpperCase());t=n.length>1?(await(0,GA.md5)(n.join(` -`))).toUpperCase():n[0]}if(!t){if(r.size>10485760)throw new Error(`CAS requires a legacy slice MD5 or complete 10 MiB MD5 pieces for ${r.path}`);t=r.hashes.md5.toUpperCase()}let i={name:r.path.split("/").pop()||r.path,size:r.size,md5:r.hashes.md5,sliceMd5:t,create_time:r.cas_create_time||String(Math.floor(Date.now()/1e3)),cloud:r.cas_cloud||"189"};return r.hashes.pieces.md5.length>0&&(i.slice_md5s=r.hashes.pieces.md5.map(n=>n.toUpperCase()),i.slice_size=e||10485760),i}async function Bse(r){let e=zi(r);if(e.files.length===0)throw new Error("CAS requires at least one file");let t;if(e.files.length===1){let i=await N9(e.files[0],e.piece_size);t={name:i.name,size:i.size,md5:i.md5,sliceMd5:i.sliceMd5,create_time:i.create_time,slice_md5s:i.slice_md5s,slice_size:i.slice_size,cloud:i.cloud}}else{let i=[],n=0;for(let s of e.files){let o=await N9(s,e.piece_size);i.push(o),n+=o.size}t={name:e.name,size:n,md5:"",sliceMd5:"",create_time:"",files:i}}return Rn.encode(Buffer.from(JSON.stringify(t)).toString("base64"))}function q9(r){let e=Ga.decode(r).trim(),t=e;try{t=Buffer.from(e,"base64").toString("utf8"),JSON.parse(t)}catch{t=e}let i=JSON.parse(t),n=l=>{if(!l.name||!Number.isSafeInteger(l.size)||Number(l.size)<0||!l.md5)throw new Error("Invalid CAS file entry");let d=String(l.md5).toLowerCase(),u=String(l.sliceMd5||l.md5).toLowerCase();if(!/^[0-9a-f]{32}$/i.test(d)||!/^[0-9a-f]{32}$/i.test(u))throw new Error("Invalid CAS hash");let f=Array.isArray(l.slice_md5s)?l.slice_md5s.map(h=>String(h).toLowerCase()):[];return{path:l.name,size:Number(l.size),modified:"",comment:"",hashes:{md5:d,sha1:"",sha256:"",pieces:{md5:f,sha1:[],sha256:[]}},sources:[],cas_slice_md5:u,cas_create_time:l.create_time||"",cas_cloud:String(l.cloud||""),missing_channels:[]}};if(Array.isArray(i.files)&&i.files.length>0){let l=i.files.map(n);return{format:"cas",cas:{name:String(i.name),size:l.reduce((u,f)=>u+f.size,0),md5:"",sliceMd5:"",create_time:"",cloud:String(i.cloud||""),files:i.files},seed:zi({name:i.name,comment:"Imported from OpenList CAS metadata",created_at:new Date().toISOString(),created_by:"OpenList",piece_size:10485760,files:l})}}if(!i.name||!Number.isSafeInteger(i.size)||Number(i.size)<0||!i.md5)throw new Error("Invalid CAS payload");let s=String(i.md5).toLowerCase(),o=String(i.sliceMd5||i.md5);if(!/^[0-9a-f]{32}$/i.test(s)||!/^[0-9a-f]{32}$/i.test(o))throw new Error("Invalid CAS hash");let a=Number(i.create_time);return{format:"cas",cas:{name:String(i.name),size:Number(i.size),md5:s,sliceMd5:o,create_time:String(i.create_time||""),slice_md5s:i.slice_md5s,slice_size:i.slice_size,cloud:String(i.cloud||"")},seed:zi({name:i.name,comment:"Imported from OpenList CAS metadata",created_at:Number.isFinite(a)&&a>0?new Date(a*1e3).toISOString():new Date().toISOString(),created_by:"OpenList",piece_size:i.slice_size||10485760,files:[n({name:i.name,size:Number(i.size),md5:s,sliceMd5:o,create_time:String(i.create_time||""),slice_md5s:i.slice_md5s,slice_size:i.slice_size})]})}}async function s2(r,e){let t=zi(r),i=e||xse(t);if(i.some(o=>o.length!==40))throw new Error("Torrent conversion requires SHA-1 piece hashes");let n={name:t.name,"piece length":t.piece_size,pieces:Ew(i.map(Ise))};t.files.length===1?(n.length=t.files[0].size,t.files[0].hashes.md5&&(n.md5sum=t.files[0].hashes.md5)):n.files=t.files.map(o=>{let a={length:o.size,path:o.path.split("/")};return o.hashes.md5&&(a.md5sum=o.hashes.md5),a});let s={info:n,comment:t.comment,"created by":t.created_by,"creation date":Math.floor(new Date(t.created_at).getTime()/1e3)||Math.floor(Date.now()/1e3),"x-openlist":r2(t)};if(t.trackers.length&&(s.announce=t.trackers[0],s["announce-list"]=t.trackers.map(o=>[o])),t.files.length===1){let o=await Cse(t.files[0],t.piece_size);o&&(s["x-cas"]=o)}return nu(s)}function bse(r,e,t,i,n){let s=Array.isArray(r.files)?r.files:null,o=s?s.map(c=>{let l=su(c,"torrent file");return{path:(Array.isArray(l.path)?l.path.map(un):[]).join("/"),size:Ka(l.length),md5:un(l.md5sum)}}):[{path:e,size:Ka(r.length),md5:un(r.md5sum)}],a=0;return o.map((c,l)=>{let d=c.size>0?Math.ceil(c.size/i):0,f=(l===o.length-1||c.size%i===0)&&a+d<=t.length?t.slice(a,a+d):[];a+=d;let h=o.length===1&&n?un(n.slice_md5):"",g=o.length===1&&n?un(n.create_time):"",p=o.length===1&&n?un(n.cloud):"";return{path:c.path,size:c.size,modified:"",comment:"",hashes:{md5:c.md5.toLowerCase(),sha1:"",sha256:"",pieces:{md5:[],sha1:f,sha256:[]}},sources:[],cas_slice_md5:h.toLowerCase(),cas_create_time:g,cas_cloud:p,missing_channels:[]}})}async function O9(r){let e=su(pse(r),"torrent root"),t=su(e.info,"torrent info"),i=await(0,GA.sha1)(nu(t)),n=un(t.name),s=Ka(t["piece length"]),o=mse(t.pieces);if(!n||s<=0||o.length%20!==0)throw new Error("Invalid torrent info dictionary");let a=Array.isArray(t.files)?t.files.reduce((g,p)=>g+Ka(su(p,"torrent file").length),0):Ka(t.length),c=a>0?Math.ceil(a/s):0;if(o.length/20!==c)throw new Error("Torrent piece count mismatch");if(e["x-openlist"]){let g=zi(i2(e["x-openlist"]));return vse(g,t,n,s),{format:"torrent",seed:g,info_hash:i}}let l=[];for(let g=0;g<o.length;g+=20)l.push(wse(o.subarray(g,g+20)));let d=[un(e.announce),...Array.isArray(e["announce-list"])?e["announce-list"].flatMap(g=>yse(g)):[]].filter((g,p,I)=>g&&I.indexOf(g)===p),u=Ka(e["creation date"]),f=e["x-cas"]?su(e["x-cas"],"x-cas"):void 0;return{format:"torrent",seed:zi({name:n,comment:un(e.comment),created_at:u>0?new Date(u*1e3).toISOString():new Date().toISOString(),created_by:un(e["created by"])||"OpenList",piece_size:s,trackers:d,channels:[],files:bse(t,n,l,s,f)}),info_hash:i}}function vse(r,e,t,i){if(r.name!==t||r.piece_size!==i)throw new Error("x-openlist metadata conflicts with torrent info");let n=Array.isArray(e.files)?e.files:null;if(!n){if(r.files.length!==1||r.files[0].size!==Ka(e.length))throw new Error("x-openlist file list conflicts with torrent info");return}if(r.files.length!==n.length)throw new Error("x-openlist file count conflicts with torrent info");n.forEach((s,o)=>{let a=su(s,"torrent file"),c=(Array.isArray(a.path)?a.path.map(un):[]).join("/");if(r.files[o].path!==c||r.files[o].size!==Ka(a.length))throw new Error(`x-openlist file ${o} conflicts with torrent info`)})}async function L9(r,e){if(e==="oss")return{format:"oss",seed:n2(r)};if(e==="torrent")return O9(r);if(e==="cas")return q9(r);let t=Ga.decode(r.subarray(0,Math.min(r.length,32))).trimStart()[0];return t==="{"?{format:"oss",seed:n2(r)}:t==="d"?O9(r):q9(r)}async function zA(r,e){return e==="oss"?Ese(r):e==="torrent"?s2(r):Bse(r)}var za=xr(jm(),1);async function H9(){let[r,e,t]=await Promise.all([(0,za.createMD5)(),(0,za.createSHA1)(),(0,za.createSHA256)()]);return{md5:r.init(),sha1:e.init(),sha256:t.init()}}function K9(r,e){r.md5.update(e),r.sha1.update(e),r.sha256.update(e)}function o2(r){return{md5:r.md5.digest("hex").toLowerCase(),sha1:r.sha1.digest("hex").toLowerCase(),sha256:r.sha256.digest("hex").toLowerCase()}}function kse(r){r.md5.init(),r.sha1.init(),r.sha256.init()}var JA=class r{constructor(e,t){this.pieceSize=t;this.hasher=e.init()}pieceSize;hasher;written=0;hashes=[];static async create(e){return new r(await(0,za.createSHA1)(),e)}update(e){let t=0;for(;t<e.length;){let i=Math.min(this.pieceSize-this.written,e.length-t);this.hasher.update(e.subarray(t,t+i)),this.written+=i,t+=i,this.written===this.pieceSize&&this.finishPiece()}}digest(){return this.written>0&&this.finishPiece(),[...this.hashes]}finishPiece(){this.hashes.push(this.hasher.digest("hex").toLowerCase()),this.hasher.init(),this.written=0}};async function a2(r,e,t,i,n){if(!Number.isSafeInteger(t)||t<0||t>i)throw new Error(`File exceeds the hashing limit of ${i} bytes`);let s=await H9(),o=await H9(),a={md5:[],sha1:[],sha256:[]},c=r.getReader(),l=0,d=0;try{for(;;){let{done:u,value:f}=await c.read();if(u)break;if(!f?.length)continue;if(l+f.length>i||l+f.length>t)throw new Error("Downloaded file size exceeds the declared or configured limit");l+=f.length,n?.update(f),K9(s,f);let h=0;for(;h<f.length;){let g=Math.min(e-d,f.length-h);if(K9(o,f.subarray(h,h+g)),d+=g,h+=g,d===e){let p=o2(o);a.md5.push(p.md5),a.sha1.push(p.sha1),a.sha256.push(p.sha256),kse(o),d=0}}}}finally{c.releaseLock()}if(l!==t)throw new Error(`Downloaded file size mismatch: expected ${t}, received ${l}`);if(d>0){let u=o2(o);a.md5.push(u.md5),a.sha1.push(u.sha1),a.sha256.push(u.sha256)}return{size:l,hashes:{...o2(s),pieces:a}}}var ws=new Xe,Bw=10*1024*1024,d2=512*1024*1024,G9=1e3,Sse=8*1024*1024,z9=4*1024*1024;function hn(r,e,t){let i=r.env?.[e]??(typeof process<"u"?process.env?.[e]:void 0),n=Number(i);return Number.isSafeInteger(n)&&n>0?n:t}function jA(r){try{return typeof r.executionCtx?.waitUntil!="function"?void 0:{waitUntil:e=>r.executionCtx.waitUntil(e)}}catch{return}}function Or(r,e,t){return r.json({code:e,message:t,data:null},e)}async function au(r){let e=hn(r,"SEED_MAX_METADATA_SIZE",Bw);if(Number(r.req.header("Content-Length")||0)>e)throw new Error(`Seed request exceeds the ${e} byte limit`);let i=await r.req.text();if(new TextEncoder().encode(i).byteLength>e)throw new Error(`Seed request exceeds the ${e} byte limit`);return i.trim()?JSON.parse(i):{}}function xo(r,e="/"){let t=String(r||e).replace(/\\/g,"/").trim();if(t.includes("\0"))throw new Error("Path contains an illegal null byte");let i=t.split("/").filter(Boolean);if(i.some(n=>n==="."||n===".."))throw new Error("Path traversal is not allowed");return"/"+i.join("/")}function u2(r,e){return xo(`${r.replace(/\/+$/,"")}/${e}`)}function J9(r,e){if(!pt(r))throw new Error("Permission denied");let t=xo(e);if(t==="/@s"||t.startsWith("/@s/"))throw new Error("Writing through a share path is not allowed");let i=We(r,t),n=xo(r?.base_path||"/");if(n!=="/"&&i!==n&&!i.startsWith(`${n}/`))throw new Error("Target path is outside the user base path");return i}function Dse(r,e){if(r instanceof Uint8Array)return r;if(Array.isArray(r)&&r.every(t=>Number.isInteger(t)&&t>=0&&t<=255))return Uint8Array.from(r);if(typeof r!="string")throw new Error("Seed content is required");return e==="text"||r.trimStart().startsWith("{")||r.trimStart().startsWith("d")?new TextEncoder().encode(r):new Uint8Array(Buffer.from(r,"base64"))}function bw(r){let e=String(r||"").toLowerCase().replace(/^\./,"");return e==="oss"||e==="torrent"||e==="cas"?e:void 0}function j9(r,e){return`${r.name.replace(/[\\/:*?"<>|\0]/g,"_")||"seed"}.${e==="torrent"?"torrent":e}`}function c2(r){let e=r.split("/").pop()||r,t=e.lastIndexOf(".");return t>0?e.slice(0,t):e}function Qse(r){if(r.length===0)return"OpenList Seed";if(r.length===1)return c2(r[0].virtualPath);let e=c2(r[0].virtualPath);if(r.every(o=>c2(o.virtualPath)===e)&&e)return e;let n=r[0].virtualPath.split("/").slice(0,-1);for(let o of r.slice(1)){let a=o.virtualPath.split("/").slice(0,-1),c=0;for(;c<n.length&&c<a.length&&n[c]===a[c];)c++;n=n.slice(0,c)}return n[n.length-1]||"OpenList Seed"}function f2(r,e,t,i={}){let n=Buffer.from(t).toString("base64");return{format:e,file_name:j9(r,e),seed_data:n,size:t.byteLength,seed:r,...i}}function $9(r){let e=r.env?.ALLOW_SEED??(typeof process<"u"?process.env?.ALLOW_SEED:"");return String(e||"").split(/[\s,;]+/).map(t=>t.toLowerCase()).filter(Boolean)}function V9(r,e){let t=r.toLowerCase();return e.some(i=>i.startsWith("*.")?t.endsWith(i.slice(1))&&t!==i.slice(2):t===i)}function l2(r,e){if(!e.url)throw new Error("Seed source URL is required");let t=new URL(r.req.url),i=new URL(e.url,t.origin);if(jr(i.toString(),"Seed source"),i.username||i.password)throw new Error("Seed source URL credentials are not allowed");let n=e.type.toLowerCase(),s=i.origin===t.origin&&(/^\/(api\/)?p\//.test(i.pathname)||i.pathname.startsWith("/d/")||i.pathname.startsWith("/@s/")),o=["share","openlist-share"].includes(n)&&i.pathname.startsWith("/@s/"),a=["direct","openlist-direct"].includes(n)&&s;if(!o&&!a&&!V9(i.hostname,$9(r)))throw new Error("Seed source host is not an approved OpenList endpoint or configured domain");if(e.expires_at){let c=Date.parse(e.expires_at);if(!Number.isFinite(c)||c<=Date.now())throw new Error("Seed source has expired")}return i}async function vw(r,e,t={},i=!1){let n=e;for(let s=0;s<=3;s++){if(jr(n.toString(),"Seed fetch"),i){let c=new URL(r.req.url);if(!(n.origin===c.origin&&(/^\/(api\/)?p\//.test(n.pathname)||n.pathname.startsWith("/@s/")))&&!V9(n.hostname,$9(r)))throw new Error("Seed fetch redirect left the approved source allowlist")}let o=await fetch(n,{...t,redirect:"manual"});if(o.status<300||o.status>=400)return o;let a=o.headers.get("Location");if(!a)throw new Error("Seed fetch redirect is missing a Location header");n=new URL(a,n)}throw new Error("Seed fetch exceeded the redirect limit")}async function _se(r,e,t){let i=We(e,xo(t)),{item:n,rawUrl:s}=await _i(i,jA(r)),o=hn(r,"SEED_MAX_METADATA_SIZE",Bw);if(n.is_dir||n.size<0||n.size>o)throw new Error(`Seed file exceeds the ${o} byte limit`);let a=n.raw_url||new URL(s,r.req.url).toString(),c=n.raw_url_headers||{};!n.raw_url&&r.req.header("Authorization")&&(c.Authorization=r.req.header("Authorization"));let l=await vw(r,new URL(a,r.req.url),{headers:c});if(!l.ok)throw new Error(`Seed file download failed with HTTP ${l.status}`);if(Number(l.headers.get("Content-Length")||n.size)>o)throw new Error(`Seed file exceeds the ${o} byte limit`);let u=l.body?.getReader();if(!u)throw new Error("Seed file response has no body");let f=[],h=0;for(;;){let{done:I,value:m}=await u.read();if(I)break;if(m?.length){if(h+=m.length,h>o)throw new Error(`Seed file exceeds the ${o} byte limit`);f.push(m)}}let g=new Uint8Array(h),p=0;for(let I of f)g.set(I,p),p+=I.length;return g}async function $A(r,e,t){if(t.seed&&typeof t.seed=="object")return{format:"oss",seed:zi(t.seed)};let i=hn(r,"SEED_MAX_METADATA_SIZE",Bw),n=t.seed_data||t.content||t.data||t.torrent_data,s=t.seed_path||t.path&&!n?await _se(r,e,String(t.seed_path||t.path)):Dse(t.seed_data??t.content??t.data??t.torrent_data,t.encoding);if(s.byteLength>i)throw new Error(`Seed content exceeds the ${i} byte limit`);return L9(s,bw(t.format||t.from_format||t.file_name?.split(".").pop()))}async function A2(r,e,t){let i=Array.isArray(t)?t:[t];if(!i.length||i.some(a=>typeof a!="string"||!a.trim()))throw new Error("paths must contain at least one path");let n=hn(r,"SEED_MAX_FILES",G9),s=[],o=async(a,c)=>{if(s.length>=n)throw new Error(`Seed file count exceeds the ${n} file limit`);let l=We(e,a),{item:d,rawUrl:u}=await _i(l,jA(r));if(!d.is_dir){let h=d.raw_url||new URL(u,r.req.url).toString(),g={...d.raw_url_headers||{}};!d.raw_url&&r.req.header("Authorization")&&(g.Authorization=r.req.header("Authorization")),s.push({virtualPath:l,relativePath:R9(c),size:d.size,modified:d.modified||"",rawUrl:h,headers:g,hashes:d.hashes??(d.hash?{md5:d.hash}:{})});return}let{content:f}=await Zt(l,jA(r));for(let h of f){let g=u2(a,h.name),p=`${c.replace(/\/$/,"")}/${h.name}`;await o(g,p)}};for(let a of i){let c=xo(a),l=c.split("/").filter(Boolean).pop()||"root";await o(c,l)}if(!s.length)throw new Error("No files were found for seed generation");return s}function ou(r,e){if(e==="cas"){let t=[];for(let i of r.files){i.hashes.md5||t.push(`${i.path}: missing whole-file MD5`);let n=!!i.cas_slice_md5,s=r.piece_size===10485760&&i.hashes.pieces.md5.length>0;i.size>10485760&&!n&&!s&&t.push(`${i.path}: missing legacy slice MD5 or complete 10 MiB MD5 pieces`)}return t}if(e==="torrent"){let t=[];return r.files.forEach((i,n)=>{i.hashes.pieces.sha1.length||t.push(`${i.path}: missing SHA-1 piece hashes`),r.files.length>1&&n<r.files.length-1&&i.size%r.piece_size!==0&&t.push(`${i.path}: piece boundary crosses the next file`)}),t}return[]}function Fse(r){return{oss:ou(r,"oss"),torrent:ou(r,"torrent"),cas:ou(r,"cas")}}function Pse(r){return Object.fromEntries(["oss","torrent","cas"].map(e=>{let t=ou(r,e);return[e,{feasible:t.length===0,missing:t}]}))}async function h2(r,e,t,i){if(!t)return;let n=J9(e,t);await Un(n,Buffer.from(i),jA(r))}async function Tse(){try{let e=(await Vn()).seed_default_matrix;if(e){let t=JSON.parse(String(e));if(t&&typeof t=="object")return t}}catch{}}async function Use(r){try{let e=await Vn(),t=String(e.seed_site_url||"").trim();if(t)return t.replace(/\/+$/,"")}catch{}return new URL(r.req.url).origin}async function Rse(r,e,t){let i=e&&typeof e=="object"?e:null,n=!!i?.md5||!!i?.sha1||!!i?.sha256;if(!n)try{let o=(await Vn()).seed_default_matrix;if(o){let a=JSON.parse(String(o));if(a?.md5||a?.sha1||a?.sha256)return Cw(a,t)}}catch{}return Cw(n?e:void 0,t)}async function Nse(r,e,t,i){let n=i.includes("cas")?10485760:Number(t.piece_size||10485760);if(!Number.isSafeInteger(n)||n<16*1024||n>64*1024*1024)throw new Error("Invalid piece_size");let s=await Rse(r,t.hash_matrix,i),o=t.file_comments&&typeof t.file_comments=="object"?t.file_comments:{},a=await A2(r,e,t.paths??t.path),c=hn(r,"SEED_MAX_HASH_BYTES",d2),l=a.reduce((h,g)=>h+g.size,0);if(!Number.isSafeInteger(l)||l>c)throw new Error(`Seed input exceeds the ${c} byte hashing limit`);let d=await JA.create(n),u=await Use(r),f=[];for(let h of a){let g=await vw(r,new URL(h.rawUrl,r.req.url),{headers:h.headers});if(!g.ok)throw new Error(`Source download failed with HTTP ${g.status}: ${h.virtualPath}`);if(!g.body)throw new Error(`Source download has no body: ${h.virtualPath}`);let p=await a2(g.body,n,h.size,c,d),I=String(o[h.relativePath]??o[h.virtualPath]??"");f.push(t2({path:h.relativePath,size:p.size,modified:h.modified,comment:I,hashes:p.hashes,cas_slice_md5:"",cas_create_time:"",cas_cloud:"",missing_channels:[],sources:t.include_direct_source===!0||t.include_sources===!0?[{type:"openlist-direct",url:`${u}/d${h.virtualPath}`,expires_at:"",share_id:""}]:[]},s))}return{torrentPieces:d.digest(),seed:zi({name:String(t.name||Qse(a)),comment:String(t.comment||"Generated by OpenList"),created_at:new Date().toISOString(),created_by:String(t.created_by||"OpenList"),piece_size:n,trackers:Array.isArray(t.trackers)?t.trackers:[],channels:Array.isArray(t.channels)?t.channels:[],files:f})}}function g2(r,e){for(let t of e.sources)try{return{source:t,url:l2(r,t)}}catch{continue}}async function qse(r,e,t,i,n,s=!1){let o=J9(e,i),a=o.split("/").filter(Boolean),c=a.pop();if(!c)throw new Error("Target file name is required");let l="/"+a.join("/"),d=await ct(o),u=await ct(l);if(d.isVirtual||u.isVirtual||!d.storage)throw new Error("Target storage was not found");let f=await lt(d.storage.driver,d.storage),h=f,g={size:t.size,md5:t.hashes.md5,sha1:t.hashes.sha1,sha256:t.hashes.sha256,hashes:t.hashes};try{if(typeof h.rapidUpload=="function"){let w=await h.rapidUpload(o,d.physical,g);if(w===!0||w?.reuse||w?.success)return{status:"reused",method:"rapidUpload"}}if(typeof h.putRapid=="function"){let w=await h.putRapid(o,d.physical,g);if(w===!0||w?.reuse||w?.success)return{status:"reused",method:"putRapid"}}let p;if(typeof h.createUploadSession=="function"&&(p=await h.createUploadSession(l,u.physical,c,t.size,t.hashes.md5),p?.reuse))return{status:"reused",method:"multipart-rapid"};if(s)throw new Error("capability unavailable: target driver did not reuse the available hashes");if(!n)throw new Error("capability unavailable: no rapid-upload match and no approved source URL");let I=await vw(r,n.url,{},!0);if(!I.ok||!I.body)throw new Error(`Source download failed with HTTP ${I.status}`);if(p?.session&&typeof h.uploadPart=="function"&&typeof h.completeUploadSession=="function"){let w=Math.min(hn(r,"SEED_TRANSFER_CHUNK_SIZE",Sse),16777216),B=I.body.getReader(),C=new Uint8Array(0),x=1,Q=0,R=[];for(;;){let{done:U,value:T}=await B.read();if(T?.length){if(Q+=T.length,Q>t.size)throw new Error("Source exceeds the declared file size");let D=new Uint8Array(C.length+T.length);for(D.set(C),D.set(T,C.length),C=D;C.length>=w;){let b=C.slice(0,w);C=C.slice(w);let _=await h.uploadPart(p.session,x++,Buffer.from(b));_?.partMd5&&R.push(_.partMd5)}}if(U)break}if(Q!==t.size)throw new Error("Source size does not match seed metadata");if(C.length||t.size===0){let U=await h.uploadPart(p.session,x,Buffer.from(C));U?.partMd5&&R.push(U.partMd5)}return await h.completeUploadSession(p.session,R),{status:"saved",method:"multipart-stream"}}if(typeof h.putStream=="function")return await h.putStream(o,d.physical,I.body,t.size,g),{status:"saved",method:"putStream"};let m=hn(r,"SEED_BUFFERED_TRANSFER_MAX",z9);if(t.size<=m){let w=new Uint8Array(await I.arrayBuffer());if(w.byteLength!==t.size)throw new Error("Source size does not match seed metadata");return await f.put(o,d.physical,Buffer.from(w)),{status:"saved",method:"bounded-buffer"}}throw new Error("capability unavailable: target driver has no streaming or multipart upload API")}finally{await Jt(d.storage.driver,d.storage,f,jA(r))}}ws.get("/capabilities",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");let t,i=r.req.query("path");if(i)try{let n=We(e,xo(i)),s=await ct(n);if(!s.isVirtual&&s.storage){let a=await lt(s.storage.driver,s.storage);t={driver:s.storage.driver,rapid_upload:typeof a.rapidUpload=="function"||typeof a.putRapid=="function"||typeof a.createUploadSession=="function",stream_upload:typeof a.putStream=="function"||typeof a.createUploadSession=="function"&&typeof a.uploadPart=="function"&&typeof a.completeUploadSession=="function"}}}catch(n){t={error:Ne(n)}}return r.json({code:200,message:"success",data:{formats:["oss","torrent","cas"],version:1,hash_algorithms:["md5","sha1","sha256"],incremental_hashing:!0,torrent_v1:!0,extensions:["x-openlist","x-cas"],offline_download:"conditional",queue_adapter:!1,limits:{metadata_bytes:hn(r,"SEED_MAX_METADATA_SIZE",Bw),hash_bytes:hn(r,"SEED_MAX_HASH_BYTES",d2),files:hn(r,"SEED_MAX_FILES",G9)},target:t}})});ws.post("/capabilities",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");try{let t=await au(r);if(Array.isArray(t.paths)&&t.paths.length>0){let g=await A2(r,e,t.paths),p=await Vn(),I=String(p.seed_default_trackers||"").split(` -`).map(x=>x.trim()).filter(Boolean),m=["md5","sha1","sha256"],w=g.map(x=>{let Q=m.filter(U=>!!x.hashes[U]),R=Q.length<m.length;return{path:x.virtualPath,name:x.relativePath,size:x.size,available_hashes:Q,requires_download:R,requires_fetch:R,estimated_traffic:R?x.size:0,streamable:!0,share_available:!0,direct_source_available:!0}}),B=new Set;for(let x of g)for(let Q of m)x.hashes[Q]&&B.add(Q);let C=m.filter(x=>B.has(x));return r.json({code:200,message:"success",data:{formats:{oss:!0,torrent:!0,cas:!0},files:w,existing_hashes:C,estimated_traffic:w.reduce((x,Q)=>x+Q.estimated_traffic,0),default_matrix:await Tse(),trackers:I}})}let i=await $A(r,e,t),n=We(e,xo(t.path)),s=await ct(n);if(s.isVirtual||!s.storage)throw new Error("Target storage was not found");let a=await lt(s.storage.driver,s.storage),c=typeof a.rapidUpload=="function"||typeof a.putRapid=="function"||typeof a.createUploadSession=="function",l=Array.isArray(a.rapidHashAlgos)?a.rapidHashAlgos.map(g=>String(g).toLowerCase()).filter(g=>["md5","sha1","sha256"].includes(g)):["md5","sha1"],d=typeof a.rapidHashNeedsPieces=="boolean"?a.rapidHashNeedsPieces:!1,u=typeof a.putStream=="function"||typeof a.createUploadSession=="function"&&typeof a.uploadPart=="function"&&typeof a.completeUploadSession=="function",f=hn(r,"SEED_BUFFERED_TRANSFER_MAX",z9),h=i.seed.files.map(g=>{let p=l.some(B=>!!g.hashes[B])||!1,I=d?p&&(g.hashes.pieces.md5.length>0||g.hashes.pieces.sha1.length>0):p,m=!!g2(r,g),w="download_required";return c&&I?w="rapid_upload":m&&u?w="stream_upload":m&&g.size<=f&&(w="bounded_upload"),{path:g.path,method:w,requires_download:w==="download_required"}});return r.json({code:200,message:"success",data:{driver:s.storage.driver,policy:String(t.policy||"inherit"),driver_supports:{cas_rapid:c,rapid_hash_algos:l,rapid_uses_pieces:d},files:h}})}catch(t){return Or(r,400,Ne(t))}});ws.post("/generate",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");try{let t=await au(r);if((t.save_path||t.output_path)&&!pt(e))return Or(r,403,"Permission denied");let n=(Array.isArray(t.formats)?t.formats:[t.format||"oss"]).map(bw);if(n.some(d=>!d))throw new Error("A valid output format is required");let s=n,{seed:o,torrentPieces:a}=await Nse(r,e,t,s),c=t.output_path||(Array.isArray(t.formats)?t.save_path:void 0),l=[];for(let d of s){let u=ou(o,d);if(u.length){l.push({format:d,convertible:!1,diagnostics:u});continue}let f=d==="torrent"?await s2(o,a):await zA(o,d),h=c?u2(xo(c),j9(o,d)):t.save_path;await h2(r,e,h,f),l.push(f2(o,d,f,{convertible:!0,...h?{path:h}:{}}))}return r.json({code:200,message:"success",data:Array.isArray(t.formats)?{seed:o,outputs:l,artifacts:l}:l[0]})}catch(t){let i=Ne(t);return Or(r,i.includes("limit")||i.includes("exceeds")?413:400,i)}});ws.post("/parse",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");try{let t=await au(r),i=await $A(r,e,t),n=i.seed.files.reduce((a,c)=>a+c.size,0),s=i.seed.files.some(a=>!!g2(r,a)),o=i.seed.files.some(a=>!!a.hashes.md5||!!a.hashes.sha1||!!a.hashes.sha256);return r.json({code:200,message:"success",data:{format:i.format,seed:i.seed,files:i.seed.files,total_size:n,diagnostics:Fse(i.seed),conversions:Pse(i.seed),capabilities:{rapid_upload:o,offline_download:s,transfer:s,convert:!0,edit:!0,recalculate:!0},direct_preview:!1,...i.info_hash?{info_hash:i.info_hash}:{}}})}catch(t){return Or(r,400,Ne(t))}});ws.post("/convert",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");try{let t=await au(r);if(t.save_path&&!pt(e))return Or(r,403,"Permission denied");let i=bw(t.to_format||t.target_format||t.output_format);if(!i)throw new Error("A valid target format is required");let n=await $A(r,e,t),s=ou(n.seed,i);if(s.length)return r.json({code:200,message:"success",data:{convertible:!1,diagnostics:s}});let o=await zA(n.seed,i);return await h2(r,e,t.save_path,o),r.json({code:200,message:"success",data:f2(n.seed,i,o,{convertible:!0})})}catch(t){return Or(r,400,Ne(t))}});function Ose(r,e,t,i){if(!e)return;let n=!1;for(let s of i.values())if(s){n=!0;break}n&&!r.channels.some(s=>s.driver===e)&&r.channels.push({driver:e,mount_path:t});for(let s of r.files){if(!i.has(s.path))continue;i.get(s.path)?s.missing_channels=s.missing_channels.filter(a=>a!==e):s.missing_channels.includes(e)||s.missing_channels.push(e)}}async function Y9(r,e){let t=await Me(r);if(!pt(t))return Or(r,403,"Permission denied");if(!e&&!Ha(t,La.OFFLINE_DOWNLOAD))return Or(r,403,"Offline download permission is required");try{let i=await au(r),n=await $A(r,t,i),s=xo(i.target_path||i.destination||i.path||"/"),o=new Set(Array.isArray(i.files)?i.files.map(p=>String(p)):[]),a=new Set(Array.isArray(i.selected_files)?i.selected_files.map(p=>Number(p)):[]);if([...a].some(p=>!Number.isSafeInteger(p)||p<0||p>=n.seed.files.length))throw new Error("selected_files contains an invalid index");let c="",l="";try{let p=await ct(We(t,s));!p.isVirtual&&p.storage&&(c=p.storage.driver||"",l=p.storage.mount_path||"")}catch{}let d=n.seed.files.filter((p,I)=>o.size?o.has(p.path):a.size?a.has(I):!0),u=[],f=new Map,h=0;for(let p of d){let I=u2(s,p.path),m=g2(r,p);try{if(!e&&!m)throw new Error("capability unavailable: offline download requires an approved source URL");let w=await qse(r,t,p,I,m,e);h++,f.set(p.path,!0),u.push({path:p.path,target:I,...w})}catch(w){f.set(p.path,!1),u.push({path:p.path,target:I,status:"unavailable",error:Ne(w)})}}if(h===0)return r.json({code:501,message:e?"capability unavailable: target driver cannot reuse hashes and no transferable source is available":"capability unavailable: no approved source and streaming upload combination is available",data:{results:u}},501);let g={total:u.length,saved:h,results:u};if(i.update_channel===!0&&c){Ose(n.seed,c,l,f);let p=await zA(n.seed,n.format);g.seed_data=Buffer.from(p).toString("base64"),g.seed=n.seed}return r.json({code:200,message:"success",data:g})}catch(i){return Or(r,400,Ne(i))}}ws.post("/rapid_upload",r=>Y9(r,!0));ws.post("/offline_download",r=>Y9(r,!1));ws.post("/update",async r=>{let e=await Me(r);if(!e||e.disabled)return Or(r,401,"Unauthorized");try{let t=await au(r);if(t.save_path&&!pt(e))return Or(r,403,"Permission denied");let i=await $A(r,e,t),n=t.patch&&typeof t.patch=="object"?t.patch:t,s=t.options&&typeof t.options=="object"?t.options:{},o=n.comment??s.comment,a=!!(n.recalculate??s.recalculate),c=zi({...i.seed,name:n.name??i.seed.name,comment:o??i.seed.comment,trackers:n.trackers??i.seed.trackers,channels:n.channels??i.seed.channels,files:n.files??i.seed.files}),l=t.file_comments&&typeof t.file_comments=="object"?t.file_comments:{},d=t.file_sources&&typeof t.file_sources=="object"?t.file_sources:{};for(let h of c.files)if(l[h.path]!==void 0&&(h.comment=String(l[h.path])),Array.isArray(d[h.path])){for(let g of d[h.path])l2(r,g);h.sources=d[h.path]}if(a){let h=Number(t.piece_size||i.seed.piece_size||10485760);if(!Number.isSafeInteger(h)||h<16*1024||h>64*1024*1024)throw new Error("Invalid piece_size");c.piece_size=h;let g=Cw(t.hash_matrix,[i.format]),p=Array.isArray(t.recalc_files)?t.recalc_files:[];if(!p.length)throw new Error("recalculate requires at least one recalc_files entry");let I=new Map;for(let B of p)typeof B?.source_path=="string"&&B.source_path.trim()&&I.set(String(B.path),B.source_path);let m=await JA.create(h),w=hn(r,"SEED_MAX_HASH_BYTES",d2);for(let B of c.files){let C=I.get(B.path);if(!C)continue;let x=await A2(r,e,[C]);if(x.length!==1)throw new Error(`Recalculate path must resolve to exactly one file: ${C}`);let Q=x[0],R=await vw(r,new URL(Q.rawUrl,r.req.url),{headers:Q.headers});if(!R.ok||!R.body)throw new Error(`Recalculate download failed with HTTP ${R.status}`);let U=await a2(R.body,h,Q.size,w,m);B.hashes=t2({...B,hashes:U.hashes},g).hashes,B.size=U.size,B.cas_slice_md5="",B.cas_create_time=""}}for(let h of c.files)for(let g of h.sources)l2(r,g);let u=bw(t.output_format||t.to_format)||i.format,f=await zA(c,u);return await h2(r,e,t.save_path,f),r.json({code:200,message:"success",data:f2(c,u,f)})}catch(t){return Or(r,400,Ne(t))}});var kw=new Map,Mse=1*1024*1024,Lse=64*1024*1024,Hse=10*1024*1024;function W9(r){return!Number.isFinite(r)||r<=0?Hse:Math.min(Lse,Math.max(Mse,Math.floor(r)))}function Kse(r){let e=Array.from(r).sort((s,o)=>s-o);if(e.length===0)return[];let t=[],i=e[0],n=e[0];for(let s=1;s<e.length;s++)e[s]===n+1?n=e[s]:(t.push([i,n]),i=n=e[s]);return t.push([i,n]),t}function cl(r){let e=Kse(r.received),t=r.received.size*r.chunk_size,i=0;for(let s=0;s<r.total_chunks&&r.received.has(s);s++)i=s+1;let n=r.total_chunks>0?Math.floor(r.received.size/r.total_chunks*100):0;return{upload_id:r.upload_id,state:r.state,attempt:r.attempt,path:r.path,size:r.size,chunk_size:r.chunk_size,total_chunks:r.total_chunks,received:e,received_bytes:t,frontier:i,storage_progress:n,error:r.error}}function Sw(r){return kw.get(r)}function X9(r,e){for(let t of kw.values())if(t.path===r&&t.size===e&&(t.state==="receiving"||t.state==="failed_retriable"))return t}function VA(r){kw.set(r.upload_id,r)}function Z9(r){kw.delete(r)}function e7(){return`mp_${typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():Math.random().toString(36).slice(2)+Date.now().toString(36)}`}async function Gse(r){try{let e=await ct(r);return e?.storage?dQ(e.storage):!1}catch{return!1}}var dt=new Xe;dt.route("/seed",ws);var _t=r=>{try{let e=r.executionCtx;return!e||typeof e.waitUntil!="function"?void 0:{waitUntil:t=>e.waitUntil(t),env:r.env}}catch{return}},cr=r=>r.json({code:403,message:"Permission denied",data:null},403),zse=25*1024*1024,Jse=16*1024*1024;function jse(r,e){let t=r.env||{},i=e?"MAX_UPPART":"MAX_UPLOAD",n=t[i]||(typeof process<"u"?process.env?.[i]:"");if(n){let s=parseInt(String(n),10);if(Number.isFinite(s)&&s>0)return s}return e?Jse:zse}function Dw(r,e=!1){let t=parseInt(r.req.header("Content-Length")||"0",10);if(!t)return null;let i=jse(r,e);return t>i?i:null}var p2=(r,e)=>{let t=e==="wrong password",i=e||"share error";return r.json({code:t?403:400,message:i,data:null},t?403:400)};dt.post("/dirs",async r=>{let e=await r.req.json().catch(()=>({})),t=await Me(r),i=e.path||"/";if(!i.startsWith("/@s")&&(!t||t.disabled))return r.json({code:401,message:"Unauthorized",data:null},401);let s=_t(r),o=i;(!e.force_root||!Is(t))&&(o=We(t,o));try{if(o.startsWith("/@s")){let l=await Yd(o,e.password||"",r.env);if(!l.ok)return p2(r,l.error);if(l.virtualList){let f=[];for(let h of l.share.files||[])try{let{item:g}=await _i(h,s);if(g.is_dir){let p=String(h).split("/").filter(Boolean);f.push({name:p[p.length-1]||h,modified:g.modified||new Date().toISOString()})}}catch{}return r.json({code:200,message:"success",data:f})}let{content:d}=await Zt(l.realPath,s),u=d.filter(f=>f.is_dir).map(f=>({name:f.name,modified:f.modified||new Date().toISOString()}));return r.json({code:200,message:"success",data:u})}let{content:a}=await Zt(o,s),c=a.filter(l=>l.is_dir).map(l=>({name:l.name,modified:l.modified||new Date().toISOString()}));return r.json({code:200,message:"success",data:c})}catch(a){return r.json({code:500,message:Ne(a),data:null})}});dt.post("/list",async r=>{let e=await r.req.json().catch(()=>({})),t=await Me(r);if(!(e.path||"/").startsWith("/@s")&&(!t||t.disabled))return r.json({code:401,message:"Unauthorized",data:null},401);let n=_t(r),s=We(t,e.path||"/"),o=parseInt(e.page,10)||1,a=parseInt(e.per_page,10)||0,c=l=>{let d=l.length;if(a<=0)return{content:l,total:d};let f=(Math.max(1,o)-1)*a,h=f+a;return{content:l.slice(f,h),total:d}};try{if(s.startsWith("/@s")){let b=await Yd(s,e.password||"",r.env);if(!b.ok)return p2(r,b.error);if(b.virtualList){let j=[];for(let G of b.share.files||[]){let K=String(G).split("/").filter(Boolean),$=K[K.length-1]||G;try{let{item:te}=await _i(G,n);j.push({name:$,size:te.size||0,is_dir:!!te.is_dir,modified:te.modified||new Date().toISOString(),sign:"",thumb:te.thumb||"",type:te.type??0})}catch{try{await Zt(G,n),j.push({name:$,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",thumb:"",type:1})}catch{j.push({name:$,size:0,is_dir:!1,modified:new Date().toISOString(),sign:"",thumb:"",type:0})}}}let{content:z,total:L}=c(j);return r.json({code:200,message:"success",data:{content:z,total:L,readme:b.share.readme||"",header:b.share.header||"",write:!1,write_content_bypass:!1,provider:"Share"}})}let{content:_,provider:v}=await Zt(b.realPath,n),E=_.map(j=>({name:j.name,size:j.size,is_dir:j.is_dir,created:j.created||j.modified||new Date().toISOString(),modified:j.modified||new Date().toISOString(),sign:j.sign||"",thumb:j.thumb||"",type:j.type??0})),{content:P,total:N}=c(E);return r.json({code:200,message:"success",data:{content:P,total:N,readme:b.share.readme||"",header:b.share.header||"",write:!1,write_content_bypass:!1,provider:v}})}let l=await $b(s,r.env);if(!Vb(t,l,s,e.password||""))return r.json({code:403,message:"Access denied (wrong password or not in read_users)",data:null},403);if(await Gse(s))return r.json({code:403,message:"Index is disabled for this storage",data:null},403);let{content:d,provider:u,storage:f}=await Zt(s,n),h=pt(t)&&Yb(t,l,s),g=Wb(l,s),p=await yw(r),I=await Iw(r,s),m=p.enabled||I,w=m?p.expiresIn||await tu(r):0,B=await Promise.all(d.filter(b=>!_9(l,s,b.name)).map(async b=>{let _=`${s}/${b.name}`.replace(/\/{2,}/g,"/"),v=!b.is_dir&&m?await ru(r,_,w):"",E=b.hashinfo||b.hash_info_str||"",P=b.hash_info||{};return{name:b.name,size:b.size,is_dir:b.is_dir,created:b.created||b.modified||new Date().toISOString(),modified:b.modified||new Date().toISOString(),sign:v,thumb:b.thumb||"",type:b.type??0,hashinfo:E,hash_info:P}})),C=0;if(f&&(C=parseInt(f.page_size,10)||0,!C&&f.addition))try{let b=typeof f.addition=="string"?JSON.parse(f.addition):f.addition;C=parseInt(b?.page_size,10)||0}catch{}let x=a>0?a:C>0?C:0,Q=b=>{let _=b.length;if(x<=0)return{content:b,total:_};let E=(Math.max(1,o)-1)*x,P=E+x;return{content:b.slice(E,P),total:_}},R=(()=>{if(!f)return[];let b=[],_=f;if(/^(s3|minio|cos|oss|r2|b2|cloudflare_r2)/i.test(_.driver||"")&&b.push("s3_presigned"),Array.isArray(_.direct_upload_tools))for(let v of _.direct_upload_tools)b.includes(v)||b.push(v);return b})(),{content:U,total:T}=Q(B),D=f?uQ(f,s):void 0;return r.json({code:200,message:"success",data:{content:U,total:T,readme:Xb(l,s),header:Zb(l,s),write:h,write_content_bypass:g,provider:u,direct_upload_tools:R,page_size:x>0?x:void 0,cache_expiration:D}})}catch(l){return r.json({code:500,message:Ne(l),data:null})}});dt.post("/get",async r=>{let e=await r.req.json().catch(()=>({})),t=await Me(r);if(!(e.path||"/").startsWith("/@s")&&(!t||t.disabled))return r.json({code:401,message:"Unauthorized",data:null},401);let n=_t(r),s=We(t,e.path||"/");try{if(s.startsWith("/@s")){let m=await Yd(s,e.password||"",r.env);if(!m.ok)return p2(r,m.error);if(m.virtualList){let Q=s.split("/").filter(Boolean)[1]||"share";return r.json({code:200,message:"success",data:{name:Q,size:0,is_dir:!0,modified:new Date().toISOString(),sign:"",thumb:"",type:1,raw_url:"",readme:m.share.readme||"",header:m.share.header||"",provider:"Share",related:[],write:!1,write_content_bypass:!1}})}let w=s.split("/").filter(Boolean)[1]||"",{item:B,provider:C}=await _i(m.realPath,n),x=s.replace(/^\/@s\/[^/]+/,"");return r.json({code:200,message:"success",data:{name:B.name,size:B.size,is_dir:B.is_dir,created:B.created||B.modified||new Date().toISOString(),modified:B.modified,sign:B.sign||"",thumb:B.thumb||"",type:B.type??0,raw_url:`/api/sd/${w}${x}`,readme:m.share.readme||"",header:m.share.header||"",provider:C,related:[],write:!1,write_content_bypass:!1}})}let o=await $b(s,r.env);if(!Vb(t,o,s,e.password||""))return r.json({code:403,message:"Access denied (wrong password or not in read_users)",data:null},403);let{item:a,provider:c,rawUrl:l}=await _i(s,n),d=await yw(r),u=d.enabled||await Iw(r,s),f=!a.is_dir&&u?await ru(r,s,d.expiresIn||await tu(r)):"",h=f&&l&&!/[?&]sign=/.test(l)?`${l}${l.includes("?")?"&":"?"}sign=${f}`:l,g=pt(t)&&Yb(t,o,s),p=Wb(o,s),I=[];if(!a.is_dir)try{let m=s.includes("/")&&s.slice(0,s.lastIndexOf("/"))||"/",w=a.name.includes(".")?a.name.slice(0,a.name.lastIndexOf(".")):a.name,{content:B}=await Zt(m,n);I=B.filter(C=>C.name===a.name||C.is_dir?!1:(C.name.includes(".")?C.name.slice(0,C.name.lastIndexOf(".")):C.name)===w).map(C=>({name:C.name,size:C.size,is_dir:!1,modified:C.modified||new Date().toISOString(),sign:"",thumb:C.thumb||"",type:C.type??0}))}catch{I=[]}return r.json({code:200,message:"success",data:{name:a.name,size:a.size,is_dir:a.is_dir,created:a.created||a.modified||new Date().toISOString(),modified:a.modified,sign:f,thumb:a.thumb||"",type:a.type??0,raw_url:h,readme:Xb(o,s),header:Zb(o,s),provider:c,related:I,write:g,write_content_bypass:p}})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});function cu(r){if(typeof r!="string"||!r.trim())throw new Error("Invalid or empty file name");let e=r.trim();if(e==="."||e===".."||e.includes("/")||e.includes("\\")||e.includes("\0"))throw new Error(`Illegal file name '${e}'`);return e}function Co(r){if(typeof r!="string")throw new Error("Path must be a string");if(r.includes("\0"))throw new Error("Path contains illegal null byte");return r}dt.post("/mkdir",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let i=(await r.req.json().catch(()=>({}))).path||"/";try{Co(i)}catch(o){return r.json({code:400,message:o.message,data:null},400)}let n=We(e,i),s=_t(r);try{return await UA(n,s),r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});dt.post("/rename",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{path:t,name:i}=await r.req.json().catch(()=>({})),n="";try{Co(t||"/"),n=cu(i)}catch(o){return r.json({code:400,message:o.message,data:null},400)}let s=_t(r);try{let o=We(e,t||"/");return await RA(o,n,s),r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});dt.post("/remove",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{dir:t,names:i}=await r.req.json().catch(()=>({}));if(!Array.isArray(i)||i.length===0)return r.json({code:400,message:"Parameter 'names' must be a non-empty array",data:null},400);let n=[];try{Co(t||"/"),n=i.map(o=>cu(o))}catch(o){return r.json({code:400,message:o.message,data:null},400)}let s=_t(r);try{let o=We(e,t||"/");return await qa(o,n,s),r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});dt.post("/move",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t,dst_dir:i,names:n}=await r.req.json().catch(()=>({}));if(!Array.isArray(n)||n.length===0)return r.json({code:400,message:"Parameter 'names' must be a non-empty array",data:null},400);let s=[];try{Co(t||"/"),Co(i||"/"),s=n.map(a=>cu(a))}catch(a){return r.json({code:400,message:a.message,data:null},400)}let o=_t(r);try{let a=We(e,t||"/"),c=We(e,i||"/");return await Vd(a,c,s,o),r.json({code:200,message:"success",data:null})}catch(a){return r.json({code:500,message:Ne(a),data:null})}});dt.post("/copy",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t,dst_dir:i,names:n}=await r.req.json().catch(()=>({}));if(!Array.isArray(n)||n.length===0)return r.json({code:400,message:"Parameter 'names' must be a non-empty array",data:null},400);let s=[];try{Co(t||"/"),Co(i||"/"),s=n.map(a=>cu(a))}catch(a){return r.json({code:400,message:a.message,data:null},400)}let o=_t(r);try{let a=We(e,t||"/"),c=We(e,i||"/");return await NA(a,c,s,o),r.json({code:200,message:"success",data:null})}catch(a){return r.json({code:500,message:Ne(a),data:null})}});dt.put("/put",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=decodeURIComponent(r.req.header("File-Path")||"");if(!t.trim())return r.json({code:400,message:"Missing File-Path header",data:null},400);try{Co(t)}catch(o){return r.json({code:400,message:o.message,data:null},400)}let i=We(e,t),n=_t(r),s=Dw(r);if(s!==null)return r.json({code:413,message:`File too large (max ${s} bytes)`,data:null},413);try{let o=await r.req.arrayBuffer();return await Un(i,Buffer.from(o),n),r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});dt.put("/form",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=decodeURIComponent(r.req.header("File-Path")||"");if(!t.trim())return r.json({code:400,message:"Missing File-Path header",data:null},400);try{Co(t)}catch(o){return r.json({code:400,message:o.message,data:null},400)}let i=We(e,t),n=_t(r),s=Dw(r);if(s!==null)return r.json({code:413,message:`File too large (max ${s} bytes)`,data:null},413);try{let a=(await r.req.formData()).get("file");if(!a||typeof a=="string")return r.json({code:400,message:"missing file in form data",data:null});let c=Buffer.from(await a.arrayBuffer());return await Un(i,c,n),r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null})}});dt.post("/upload/create",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{path:t,file_name:i,size:n,md5:s}=await r.req.json().catch(()=>({})),o=We(e,t||"/"),a=_t(r);if(!i)return r.json({code:400,message:"path and file_name are required",data:null});try{let c=await ct(o);if(c.isVirtual)throw new Error("failed get storage: storage not found");let l=await lt(c.storage.driver,c.storage);if(typeof l.createUploadSession!="function")return r.json({code:200,message:"success",data:null});let d;try{d=await l.createUploadSession(o,c.physical,i,Number(n)||0,s||"")}finally{await Jt(c.storage.driver,c.storage,l,a)}return r.json({code:200,message:"success",data:d})}catch(c){return r.json({code:500,message:Ne(c),data:null})}});dt.put("/upload/part",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=r.req.header("X-Upload-Session")||"",i=parseInt(r.req.header("X-Part-Number")||"0",10),n=decodeURIComponent(r.req.header("Upload-Path")||""),s=We(e,n),o=_t(r);if(!t||!(i>=1)||!s)return r.json({code:400,message:"missing X-Upload-Session / X-Part-Number / Upload-Path",data:null});let a=Dw(r,!0);if(a!==null)return r.json({code:413,message:`Part too large (max ${a} bytes)`,data:null},413);try{let c=await ct(s);if(c.isVirtual)throw new Error("failed get storage: storage not found");let l=await lt(c.storage.driver,c.storage);if(typeof l.uploadPart!="function")throw new Error("storage does not support chunked upload");let d=Buffer.from(await r.req.arrayBuffer()),u;try{u=await l.uploadPart(t,i,d)}finally{await Jt(c.storage.driver,c.storage,l,o)}return r.json({code:200,message:"success",data:u??null})}catch(c){return r.json({code:500,message:Ne(c),data:null})}});dt.post("/upload/complete",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{path:t,session:i,partMd5s:n}=await r.req.json().catch(()=>({})),s=We(e,t||"/"),o=_t(r);if(!i)return r.json({code:400,message:"path and session are required",data:null});try{let a=await ct(s);if(a.isVirtual)throw new Error("failed get storage: storage not found");let c=await lt(a.storage.driver,a.storage);if(typeof c.completeUploadSession!="function")throw new Error("storage does not support chunked upload");try{await c.completeUploadSession(i,n)}finally{await Jt(a.storage.driver,a.storage,c,o)}return r.json({code:200,message:"success",data:null})}catch(a){return r.json({code:500,message:Ne(a),data:null})}});dt.post("/add_offline_download",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let{path:t,urls:i}=await r.req.json().catch(()=>({})),n=We(e,t||"/");return!i||i.length===0?r.json({code:400,message:"No URLs provided"}):r.json({code:501,message:"capability unavailable: this runtime has no durable offline-download adapter; use /fs/seed/offline_download with an approved source and a streaming-capable target driver",data:{path:n,accepted:0}},501)});dt.post("/search",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await r.req.json().catch(()=>({})),i=We(e,t.parent||"/");try{let n=await iu({parent:i,keywords:t.keywords||"",scope:t.scope!==void 0?parseInt(t.scope,10):0,page:t.page?parseInt(t.page,10):1,per_page:t.per_page?parseInt(t.per_page,10):30},r.env);return r.json({code:200,message:"success",data:n})}catch(n){return r.json({code:500,message:Ne(n),data:null},500)}});dt.post("/other",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await r.req.json().catch(()=>({})),i=We(e,t.path||"/"),n=t.method;if(!n)return r.json({code:400,message:"Missing required parameter 'method'",data:null},400);if(!pt(e))return cr(r);try{let s=await ct(i);if(s.isVirtual||!s.storage)throw new Error("failed get storage: storage not found");let o=await lt(s.storage.driver,s.storage);if(typeof o.other=="function"){let a=await o.other(n,s.relative,t);return r.json({code:200,message:"success",data:a})}return r.json({code:500,message:`Driver '${s.storage.driver}' does not support other method '${n}'`,data:null},500)}catch(s){return r.json({code:500,message:Ne(s),data:null},500)}});dt.post("/batch_rename",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t,rename_objects:i}=await r.req.json().catch(()=>({}));if(!Array.isArray(i)||i.length===0)return r.json({code:400,message:"rename_objects is required",data:null},400);let n=We(e,t||"/"),s=_t(r);try{for(let o of i){let a=o?.src_name,c=o?.new_name;if(!a||!c)continue;cu(c);let l=`${n}/${a}`.replace(/\/{2,}/g,"/");await RA(l,c,s)}return r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null},500)}});dt.post("/regex_rename",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t,src_name_regex:i,new_name_regex:n}=await r.req.json().catch(()=>({}));if(!i)return r.json({code:400,message:"src_name_regex is required",data:null},400);let s=We(e,t||"/"),o=_t(r);try{let a=new RegExp(i),{content:c}=await Zt(s,o);for(let l of c)if(!l.is_dir&&a.test(l.name)){let d=l.name.replace(a,n||"");cu(d);let u=`${s}/${l.name}`.replace(/\/{2,}/g,"/");await RA(u,d,o)}return r.json({code:200,message:"success",data:null})}catch(a){return r.json({code:500,message:Ne(a),data:null},500)}});dt.post("/recursive_move",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t,dst_dir:i,conflict_policy:n}=await r.req.json().catch(()=>({})),s=We(e,t||"/"),o=We(e,i||"/"),a=n||"overwrite",c=_t(r);try{let l=new Set;if(a!=="overwrite"){let f=await Zt(o,c);l=new Set(f.content.map(h=>h.name))}let d=[s],u=0;for(;d.length>0;){let f=d.shift(),{content:h}=await Zt(f,c);for(let g of h)if(g.is_dir)d.push(`${f}/${g.name}`.replace(/\/{2,}/g,"/"));else{if(l.has(g.name)){if(a==="cancel")return r.json({code:403,message:`file [${g.name}] exists`,data:null},403);if(a==="skip")continue}a!=="overwrite"&&l.add(g.name),await Vd(f,o,[g.name],c),u++}}return r.json({code:200,message:`Successfully moved ${u} files`,data:null})}catch(l){return r.json({code:500,message:Ne(l),data:null},500)}});dt.post("/remove_empty_directory",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{src_dir:t}=await r.req.json().catch(()=>({})),i=We(e,t||"/"),n=_t(r);try{let s=async o=>{let{content:a}=await Zt(o,n);for(let l of a)l.is_dir&&await s(`${o}/${l.name}`.replace(/\/{2,}/g,"/"));if(o===i)return;if((await Zt(o,n)).content.length===0){let l=o.split("/").slice(0,-1).join("/")||"/",d=o.split("/").pop();await qa(l,[d],n)}};return await s(i),r.json({code:200,message:"success",data:null})}catch(s){return r.json({code:500,message:Ne(s),data:null},500)}});dt.post("/link",async r=>{let e=await Me(r);if(!Is(e))return cr(r);let{path:t}=await r.req.json().catch(()=>({})),i=We(e,t||"/"),n=_t(r);try{let s=await ct(i);if(s.isVirtual||!s.storage)return r.json({code:500,message:"storage not found",data:null},500);let o=await lt(s.storage.driver,s.storage);try{let c=await o.get(i,s.physical??"/");if(c&&c.raw_url)return r.json({code:200,message:"success",data:{url:c.raw_url}})}finally{await Jt(s.storage.driver,s.storage,o,n)}let a=await ru(r,i,await tu(r));return r.json({code:200,message:"success",data:{url:`/api/p${Os(i)}?sign=${encodeURIComponent(a)}`}})}catch(s){return r.json({code:500,message:Ne(s),data:null},500)}});dt.post("/get_direct_upload_info",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let{path:t,file_name:i,file_size:n}=await r.req.json().catch(()=>({})),s=We(e,t||"/");try{let o=await ct(s);if(o.isVirtual||!o.storage)return r.json({code:200,message:"success",data:null});let c=await lt(o.storage.driver,o.storage);if(typeof c.getDirectUploadInfo=="function"){let l=await c.getDirectUploadInfo(o.relative,i,n);return r.json({code:200,message:"success",data:l})}if(typeof c.other=="function")for(let l of["direct_upload","get_direct_upload_info"])try{let d=await c.other(l,o.relative,{file_name:i,file_size:n});if(d)return r.json({code:200,message:"success",data:d})}catch{}return r.json({code:200,message:"success",data:null})}catch(o){return r.json({code:500,message:Ne(o),data:null},500)}});function m2(r){let t=(r.startsWith("/")?r:"/"+r).split("/").filter(Boolean),i=t.pop()||"";return{dir:"/"+t.join("/"),name:i}}dt.post("/multipart/init",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=decodeURIComponent(r.req.header("File-Path")||""),i=parseInt(r.req.header("X-File-Size")||"0",10),n=parseInt(r.req.header("X-Chunk-Size")||"0",10),s=r.req.header("X-File-Md5")||"";if(!t.trim()||i<=0)return r.json({code:400,message:"Missing File-Path / X-File-Size header",data:null},400);let{dir:o,name:a}=m2(t),c=We(e,o),l=_t(r);try{let d=await ct(c);if(d.isVirtual)throw new Error("failed get storage: storage not found");let u=await lt(d.storage.driver,d.storage);if(typeof u.createUploadSession!="function")return r.json({code:200,message:"success",data:null});let f=W9(n),h=Math.max(1,Math.ceil(i/f)),g,p=!1,I=X9(t,i);if(I)g=I,p=!0;else{let m=await u.createUploadSession(c,d.physical,a,i,s);g={upload_id:e7(),state:"receiving",attempt:0,path:t,size:i,chunk_size:f,total_chunks:h,received:new Set,driver_session:m?.session||"",partMd5s:new Array(h).fill(void 0),storage_driver:d.storage.driver,created_at:Date.now()},m?.reuse&&(g.state="completed",g.received=new Set(Array.from({length:h},(w,B)=>B))),VA(g)}return await Jt(d.storage.driver,d.storage,u,l),r.json({code:200,message:"success",data:{...cl(g),resumed:p}})}catch(d){return r.json({code:500,message:Ne(d),data:null},500)}});dt.put("/multipart/chunk",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=r.req.header("X-Upload-Id")||"",i=parseInt(r.req.header("X-Chunk-Index")||"-1",10),n=t?Sw(t):void 0;if(!n)return r.json({code:404,message:"upload session not found",data:null},404);if(i<0||i>=n.total_chunks)return r.json({code:400,message:"invalid X-Chunk-Index",data:null},400);if(n.received.has(i))return r.json({code:200,message:"success",data:cl(n)});let s=Dw(r,!0);if(s!==null)return r.json({code:413,message:`Part too large (max ${s} bytes)`,data:null},413);let o=_t(r);try{let a=await ct(We(e,m2(n.path).dir));if(a.isVirtual)throw new Error("failed get storage: storage not found");let c=await lt(a.storage.driver,a.storage);if(typeof c.uploadPart!="function")throw new Error("storage does not support chunked upload");let l=Buffer.from(await r.req.arrayBuffer()),d;try{d=await c.uploadPart(n.driver_session,i+1,l)}finally{await Jt(a.storage.driver,a.storage,c,o)}return n.received.add(i),d?.partMd5&&(n.partMd5s[i]=d.partMd5),VA(n),r.json({code:200,message:"success",data:cl(n)})}catch(a){return r.json({code:500,message:Ne(a),data:null},500)}});dt.post("/multipart/complete",async r=>{let e=await Me(r);if(!pt(e))return cr(r);let t=r.req.header("X-Upload-Id")||"",i=t?Sw(t):void 0;if(!i)return r.json({code:404,message:"upload session not found",data:null},404);if(i.state==="completed")return r.json({code:200,message:"success",data:cl(i)});let n=_t(r);try{let s=await ct(We(e,m2(i.path).dir));if(s.isVirtual)throw new Error("failed get storage: storage not found");let o=await lt(s.storage.driver,s.storage);if(typeof o.completeUploadSession!="function")throw new Error("storage does not support chunked upload");try{await o.completeUploadSession(i.driver_session,i.partMd5s.filter(a=>a!==void 0))}finally{await Jt(s.storage.driver,s.storage,o,n)}return i.state="completed",VA(i),Z9(i.upload_id),r.json({code:200,message:"success",data:cl(i)})}catch(s){return i.state="failed_permanent",i.error=Ne(s),VA(i),r.json({code:500,message:Ne(s),data:null},500)}});dt.get("/multipart/status",async r=>{let e=r.req.query("upload_id")||"",t=e?Sw(e):void 0;return t?r.json({code:200,message:"success",data:cl(t)}):r.json({code:404,message:"upload session not found",data:null},404)});function y2(r){let e=r.toLowerCase();return e.endsWith(".zip")?"zip":e.endsWith(".tar.gz")||e.endsWith(".tgz")?"tar.gz":e.endsWith(".tar.bz2")||e.endsWith(".tbz2")?"tar.bz2":e.endsWith(".tar.xz")||e.endsWith(".txz")?"tar.xz":e.endsWith(".tar")?"tar":e.endsWith(".7z")?"7z":e.endsWith(".rar")?"rar":null}function I2(r){return r==="zip"}async function w2(r,e,t){let i=We(e,t),n=await ct(i);if(n.isVirtual)throw new Error("failed get storage: storage not found");let s=await lt(n.storage.driver,n.storage),o;try{o=await s.get(t,n.physical)}finally{await Jt(n.storage.driver,n.storage,s,_t(r))}if(!o||!o.raw_url)throw new Error("archive driver did not return download link");let a=Eg(n.storage?.addition,r.env);jr(o.raw_url,"Archive download",a);let c=await fetch(o.raw_url,{headers:o.raw_url_headers||{}});if(!c.ok)throw new Error(`archive download failed: HTTP ${c.status}`);return await c.arrayBuffer()}function $se(r){let e=[],t=new Map;for(let i of r.entries){let n=i.name.split("/").filter(Boolean);if(n.length===0)continue;let s=e,o="";for(let a=0;a<n.length;a++){let c=n[a],l=a===n.length-1;if(o=o?`${o}/${c}`:`/${c}`,l)s.push({name:c,size:i.size,is_dir:!1,modified:i.modified||new Date().toISOString(),type:t7(c,!1),children:[]});else{let d=t.get(o);d||(d={name:c,size:0,is_dir:!0,modified:i.modified||new Date().toISOString(),type:1,children:[]},t.set(o,d),s.push(d)),s=d.children}}}return e}function t7(r,e){let t=r.split(".").pop()?.toLowerCase()||"";if(e)return 1;let i=["mp4","mkv","webm","avi","mov","flv","m3u8"],n=["mp3","flac","wav","aac","ogg","m4a"],s=["jpg","jpeg","png","gif","webp","svg","bmp","ico"],o=["txt","md","json","js","ts","css","html","xml","yml","log"];return i.includes(t)?2:n.includes(t)?3:s.includes(t)?4:o.includes(t)?5:6}dt.post("/archive/meta",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await r.req.json().catch(()=>({})),i=String(t.path||r.req.query("path")||"").trim();if(!i)return r.json({code:400,message:"path is required",data:null},400);let n=y2(i);if(!n)return r.json({code:400,message:"unrecognized archive format",data:null},400);if(!I2(n))return r.json({code:501,message:`archive format '${n}' is recognized but not supported in this runtime (Worker supports ZIP only); use the Go backend for ${n} archives`,data:null},501);try{let s=await w2(r,e,i),o=ww(s),a=$se(o);return r.json({code:200,message:"success",data:{comment:"",encrypted:!1,content:a,raw_url:"",sign:""}})}catch(s){return r.json({code:500,message:Ne(s),data:null},500)}});dt.post("/archive/list",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let t=await r.req.json().catch(()=>({})),i=String(t.path||r.req.query("path")||"").trim(),n=String(t.inner_path||"").trim().replace(/\/+/g,"/");if(!i)return r.json({code:400,message:"path is required",data:null},400);let s=y2(i);if(!s)return r.json({code:400,message:"unrecognized archive format",data:null},400);if(!I2(s))return r.json({code:501,message:`archive format '${s}' is recognized but not supported in this runtime; use the Go backend for ${s} archives`,data:null},501);try{let o=await w2(r,e,i),a=ww(o),c=n?n.replace(/^\/+|\/+$/g,"")+"/":"",l=a.entries.filter(f=>c?f.name.startsWith(c)&&f.name!==c.slice(0,-1):!f.name.includes("/")).map(f=>{let h=c?f.name.slice(c.length):f.name;return{name:h.split("/")[0],size:f.size,is_dir:!1,modified:f.modified||new Date().toISOString(),type:t7(h.split("/")[0],!1)}}),d=new Set,u=l.filter(f=>d.has(f.name)?!1:(d.add(f.name),!0));return r.json({code:200,message:"success",data:{content:u,total:u.length}})}catch(o){return r.json({code:500,message:Ne(o),data:null},500)}});dt.post("/archive/decompress",async r=>{let e=await Me(r);if(!pt(e))return r.json({code:403,message:"Permission denied",data:null},403);let t=await r.req.json().catch(()=>({})),i=String(t.src_dir||"").trim(),n=String(t.dst_dir||"").trim(),s=Array.isArray(t.name)?t.name:t.name?[String(t.name)]:[],o=String(t.inner_path||"").trim().replace(/\/+/g,"/");if(!s.length||!n)return r.json({code:400,message:"src_dir/dst_dir/name are required",data:null},400);try{let a=0;for(let c of s){let l=i?`${i}/${c}`:`/${c}`,d=y2(c);if(!d||!I2(d))return r.json({code:400,message:`unsupported archive format: ${c} (only ZIP is supported)`,data:null},400);let u=await w2(r,e,l),f=ww(u),h=o?o.replace(/^\/+|\/+$/g,"")+"/":"";for(let g of f.entries){if(h&&!g.name.startsWith(h))continue;let p=h?g.name.slice(h.length):g.name;if(!p||p.endsWith("/"))continue;let I=`${n.replace(/\/+$/,"")}/${p}`,m=await T9(u,g);await Un(I,Buffer.from(m),_t(r)),a++}}return r.json({code:200,message:"success",data:{task:null,count:a}})}catch(a){return r.json({code:500,message:Ne(a),data:null},500)}});gl();Cr();Ye();Yt();OA();Ma();ef();Cr();Ye();gl();d0();Io();Fi();_v();var On=new Xe;On.get("/list",async r=>{let t=((await ne(r.env)).users||[]).map(i=>({id:i.id,username:i.username,role:i.role,permission:i.permission??0,base_path:i.base_path||"/",disabled:!!i.disabled,sso_id:i.sso_id||"",allow_ldap:!!i.allow_ldap,pwd_update_at:i.pwd_update_at||"",otp:!!i.otp_secret}));return r.json({code:200,message:"success",data:{content:t,total:t.length}})});On.get("/get",async r=>{let e=r.req.query("id");if(!e)return r.json({code:400,message:"Missing id parameter",data:null},400);let t=parseInt(e,10),n=((await ne(r.env)).users||[]).find(s=>s.id===t);return n?r.json({code:200,message:"success",data:{id:n.id,username:n.username,password:"",role:n.role,permission:n.permission??0,base_path:n.base_path||"/",disabled:!!n.disabled,sso_id:n.sso_id||"",allow_ldap:!!n.allow_ldap,otp:!!n.otp_secret}}):r.json({code:404,message:"User not found",data:null},404)});On.post("/create",async r=>{let e=await r.req.json().catch(()=>({}));if(!e.username)return r.json({code:400,message:"Username is required",data:null},400);let t=await ne(r.env);if(t.users||(t.users=[]),t.users.some(l=>l.username===e.username))return r.json({code:400,message:"Username already exists",data:null},400);let s=t.users.reduce((l,d)=>Math.max(l,d.id||0),0)+1,o=e.password||"",a=null;o||(a=xu(),o=a);let c={id:s,username:e.username,role:e.role!==void 0?parseInt(e.role,10):0,permission:e.permission!==void 0?parseInt(e.permission,10):0,base_path:e.base_path||"/",disabled:!!e.disabled,sso_id:e.sso_id||"",allow_ldap:!!e.allow_ldap,pwd_update_at:new Date().toISOString()};return await Ti(c,o),t.users.push(c),await fe(t,r.env),r.json({code:200,message:a?"success (a random password was generated \u2014 pass one explicitly to choose your own)":"success",data:a?{password:a}:null})});On.post("/update",async r=>{let e=await r.req.json().catch(()=>({}));if(!e.id)return r.json({code:400,message:"User ID is required",data:null},400);let t=parseInt(e.id,10),i=await ne(r.env);i.users||(i.users=[]);let n=i.users.findIndex(o=>o.id===t);if(n===-1)return r.json({code:404,message:"User not found",data:null},404);let s=i.users[n];if(e.username&&e.username!==s.username){if(i.users.some(a=>a.id!==t&&a.username===e.username))return r.json({code:400,message:"Username already in use",data:null},400);s.username=e.username}return e.password&&e.password.trim()!==""&&await Ti(s,e.password.trim()),e.role!==void 0&&(s.role=parseInt(e.role,10)),e.permission!==void 0&&(s.permission=parseInt(e.permission,10)),e.base_path!==void 0&&(s.base_path=e.base_path),e.disabled!==void 0&&(s.disabled=!!e.disabled),e.sso_id!==void 0&&(s.sso_id=e.sso_id),e.allow_ldap!==void 0&&(s.allow_ldap=!!e.allow_ldap),i.users[n]=s,await fe(i,r.env),r.json({code:200,message:"success",data:null})});var pM=async r=>{let e=r.req.query("id");if(!e)return r.json({code:400,message:"Missing id parameter",data:null},400);let t=parseInt(e,10);if(t===1)return r.json({code:400,message:"Cannot delete primary admin user",data:null},400);let i=await ne(r.env);return i.users||(i.users=[]),i.users=i.users.filter(n=>n.id!==t),await fe(i,r.env),r.json({code:200,message:"success",data:null})};On.post("/delete",pM);On.post("/cancel",pM);On.get("/sshkey/list",async r=>{let e=parseInt(r.req.query("uid")||"0",10),t=await mu(e,r.env);return r.json({code:200,message:"success",data:{content:t,total:t.length}})});On.post("/sshkey/delete",async r=>{let e=parseInt(r.req.query("uid")||"0",10),t=r.req.query("id");if(!e||!t)return r.json({code:400,message:"Missing uid or id parameter",data:null},400);if(!await ix(e,t,r.env))return r.json({code:404,message:"SSH key not found",data:null},404);let n=await mu(e,r.env);return r.json({code:200,message:"success",data:n})});On.post("/cancel_2fa",async r=>{let e=parseInt(r.req.query("id")||"0",10);if(!e)return r.json({code:400,message:"Missing id parameter",data:null},400);let t=await ne(r.env),i=(t.users||[]).find(n=>n.id===e);return i?(delete i.otp_secret,await fe(t,r.env),r.json({code:200,message:"success",data:null})):r.json({code:404,message:"User not found",data:null},404)});var mM=async r=>{let e=r.req.header("Authorization");if(!e)return r.json({code:401,message:"Unauthorized",data:null},401);let t=e.startsWith("Bearer ")?e.substring(7):e;try{let i=await Dr(r),n=await mo(t,i,"HS256"),s=await r.req.json().catch(()=>({})),o=s.old_password||"",a=s.new_password||"";if(!a)return r.json({code:400,message:"New password is required",data:null},400);let c=await ne(r.env);c.users||(c.users=[]);let l=c.users.findIndex(u=>u.id===n.id||u.username===n.username);if(l===-1)return r.json({code:404,message:"User not found",data:null},404);let d=c.users[l];return!d.password||!await wu(d,o)?r.json({code:400,message:"Incorrect old password",data:null},400):(await Ti(d,a),c.users[l]=d,await fe(c,r.env),r.json({code:200,message:"success",data:null}))}catch(i){return r.json({code:401,message:`Unauthorized: ${i.message||"Invalid token"}`,data:null},401)}};var Ue=new Xe;Ue.use("*",async(r,e)=>{if(!await Zd(r))return r.json({code:401,message:"Unauthorized",data:null});await e()});function Rce(r=32){let e="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",t=new Uint8Array(r);if(typeof crypto<"u"&&typeof crypto.getRandomValues=="function")crypto.getRandomValues(t);else for(let n=0;n<r;n++)t[n]=(Math.random()*256|0)^Date.now()&255;let i="";for(let n=0;n<r;n++)i+=e.charAt(t[n]%e.length);return i}Ue.get("/storage/list",async r=>{let t=[...(await ne(r.env)).storages||[]].sort((i,n)=>{let s=Number(i.order)||0,o=Number(n.order)||0;return s!==o?s-o:(Number(i.id)||0)-(Number(n.id)||0)});return r.json({code:200,message:"success",data:{content:t,total:t.length}})});Ue.post("/storage/load_all",async r=>{let e=await ne(r.env),t=[],i=0,n=0;for(let s of e.storages||[])if(!s.disabled)try{await lt(s.driver,s),i++,t.push({id:s.id,mount_path:s.mount_path,driver:s.driver,status:"ok"})}catch(o){n++,t.push({id:s.id,mount_path:s.mount_path,driver:s.driver,status:"failed",error:o?.message||String(o)})}return r.json({code:200,message:"success",data:{loaded:i,failed:n,results:t}})});Ue.get("/storage/get",async r=>{let e=parseInt(r.req.query("id")||"0",10),i=(await ne(r.env)).storages.find(n=>n.id===e);return i?r.json({code:200,message:"success",data:i}):r.json({code:404,message:"storage not found",data:null})});var IM=r=>{let e=(r||"").toLowerCase().replace(/[^a-z0-9]/g,"");if(!e)return"";let i=Object.keys(Eu).find(n=>n.toLowerCase()===e||n.toLowerCase().replace(/[^a-z0-9]/g,"")===e);return i||(e.startsWith("115")?"115Open":e.startsWith("123")?"123Pan":e.includes("aliyun")?"AliyundriveOpen":e.startsWith("baidu")?"BaiduNetdisk":e.startsWith("189")||e.includes("cloud189")||e.includes("ctyun")?"Cloud189":e==="onedriveapp"?"OnedriveAPP":e.startsWith("onedrive")?"Onedrive":e.startsWith("google")||e.includes("gdrive")?"GoogleDrive":(e.includes("thunder")||e.includes("xunlei"))&&e.includes("expert")?"ThunderExpert":e.includes("thunder")||e.includes("xunlei")?"Thunder":e==="webdav"||e==="webdavdriver"?"WebDav":e==="wopan"||e.includes("unicom")||e.includes("woyun")?"WoPan":e==="quark"||e==="quarkuc"||e==="uc"?"Quark":["s3","doge","dogecloud","minio","ceph","aws","r2","b2","cos","oss","kodo"].includes(e)?"S3":e.startsWith("github")?"Github":e==="local"?"Local":e.includes("pikpak")?"PikPak":e.includes("seafile")?"Seafile":e.includes("yandex")?"YandexDisk":e.includes("terabox")||e.includes("dubox")?"Terabox":e.includes("mediatrack")||e.includes("fenmiao")?"MediaTrack":e.includes("alias")?"Alias":r||"")},wM=(r,e)=>{let t="";if(typeof e=="object"&&e!==null)try{t=JSON.stringify(e)}catch{t="{}"}else t=String(e||"{}");let i=(r||"").toLowerCase();if(i.includes("thunder")||i.includes("xunlei"))try{let n=JSON.parse(t||"{}");if(!n.device_id||typeof n.device_id!="string"||n.device_id.trim().length!==32){let s=typeof crypto<"u"&&typeof crypto.randomUUID=="function"?crypto.randomUUID().replace(/-/g,""):Math.random().toString(16).substring(2).padEnd(16,"0")+Math.random().toString(16).substring(2).padEnd(16,"0");return n.device_id=s.slice(0,32),JSON.stringify(n)}}catch{}return t};Ue.post("/storage/create",async r=>{let e=await r.req.json().catch(()=>({})),t=await ne(r.env);if(!e.driver||typeof e.driver!="string"||e.driver.trim()===""||e.driver==="undefined"||e.driver==="null")return r.json({code:400,message:"Storage driver is required",data:null},400);let i=e.mount_path;if(!i||String(i).trim()===""){let c=Eu[e.driver]?.default_mount_path;c&&(i=c)}let n="/"+String(i||"").split("/").filter(Boolean).join("/");if(t.storages.some(c=>!c.disabled&&"/"+(c.mount_path||"").split("/").filter(Boolean).join("/")===n))return r.json({code:400,message:`mount path already exists: ${n}`,data:null});let s=IM(e.driver),o=wM(s,e.addition||"{}"),a={...e,driver:s,addition:o,mount_path:n,id:t.storages.length?Math.max(...t.storages.map(c=>c.id))+1:1,status:"work",modified:new Date().toISOString()};t.storages.push(a);try{await fe(t,r.env)}catch(c){return t.storages.pop(),r.json({code:500,message:`Failed to save storage config: ${c.message||String(c)}`,data:null})}if(!a.disabled){try{await lt(a.driver,a),a.status="work"}catch(c){a.status=c.message||String(c),String(c.message||c).includes("unsupported driver")&&(a.disabled=!0);try{await fe(t,r.env)}catch(l){console.error("[admin/storage/create] Failed to update storage status:",l)}return r.json({code:500,message:c.message||String(c),data:a})}try{await fe(t,r.env)}catch(c){console.error("[admin/storage/create] Failed to update storage status after connection:",c)}}return r.json({code:200,message:"success",data:a})});Ue.post("/storage/update",async r=>{let e=await r.req.json().catch(()=>({})),t=await ne(r.env),i=e.mount_path;if(!i||String(i).trim()===""){let o=Eu[e.driver]?.default_mount_path;o&&(i=o)}let n=String(i||"").trim()!==""?"/"+String(i||"").split("/").filter(Boolean).join("/"):void 0;if(n&&t.storages.some(o=>o.id!==e.id&&!o.disabled&&"/"+(o.mount_path||"").split("/").filter(Boolean).join("/")===n))return r.json({code:400,message:`mount path already exists: ${n}`,data:null});let s=t.storages.findIndex(o=>o.id===e.id);if(s!==-1){let o=e.driver||t.storages[s].driver,a=IM(o),c=wM(a,e.addition!==void 0?e.addition:t.storages[s].addition||"{}"),l={...t.storages[s],...e,driver:a,addition:c,mount_path:n||t.storages[s].mount_path,modified:new Date().toISOString()};if(!l.disabled)try{await lt(l.driver,l,{deferTokenPersistence:!0}),l.status="work"}catch(d){return l.status=d.message||String(d),String(d.message||d).includes("unsupported driver")&&(l.disabled=!0),t.storages[s]=l,await fe(t,r.env),r.json({code:500,message:d.message||String(d),data:{id:l.id}})}t.storages[s]=l,await fe(t,r.env)}return r.json({code:200,message:"success",data:null})});Ue.post("/storage/delete",async r=>{let e=parseInt(r.req.query("id")||"0",10),t=await ne(r.env);return t.storages=t.storages.filter(i=>i.id!==e),await fe(t,r.env),r.json({code:200,message:"success",data:null})});Ue.post("/storage/enable",async r=>{let e=parseInt(r.req.query("id")||"0",10),t=await ne(r.env),i=t.storages.find(n=>n.id===e);return i&&(i.disabled=!1,i.modified=new Date().toISOString(),await fe(t,r.env),(async()=>{try{await lt(i.driver,i);let n=await ne(r.env),s=n.storages.find(o=>o.id===e);s&&!s.disabled&&(s.status="work",s.modified=new Date().toISOString(),await fe(n,r.env))}catch(n){let s=await ne(r.env),o=s.storages.find(a=>a.id===e);o&&!o.disabled&&(o.status=n.message||String(n),await fe(s,r.env))}})()),r.json({code:200,message:"success",data:null})});Ue.post("/storage/disable",async r=>{let e=parseInt(r.req.query("id")||"0",10),t=await ne(r.env),i=t.storages.find(n=>n.id===e);return i&&(i.disabled=!0,await fe(t,r.env)),r.json({code:200,message:"success",data:null})});Ue.get("/driver/names",r=>r.json({code:200,message:"success",data:["AliyundriveOpen","GoogleDrive","Onedrive","OnedriveAPP","Quark","123Pan","BaiduNetdisk","115Open","GitHub API","Thunder","ThunderExpert","189Cloud","WoPan","Lanzou","WebDav","S3","Doge","PikPak","Seafile","YandexDisk","Terabox","MediaTrack","Alias","Dropbox","WPS","139Yun","Mega_nz","115Share","123PanShare","AliyundriveShare","OnedriveSharelink","PikPakShare","SMB","Crypt","Virtual","AListV3","UrlTree","Strm","AzureBlob","USS","Alidoc","Emby","BunnyStorage","CloudflareImgbed","GuangYaPan","AutoIndex","ProtonDrive"]}));var Ft=[{name:"mount_path",type:"string",default:"",required:!0},{name:"order",type:"number",default:"0",required:!1},{name:"remark",type:"string",default:"",required:!1},{name:"cache_expiration",type:"number",default:"30",required:!1,help:"\u76EE\u5F55\u5185\u5BB9\u7F13\u5B58\u65F6\u957F\uFF08\u5206\u949F\uFF09"}],Pt={name:"custom_cache_policies",type:"text",default:"",required:!1,help:'\u6309\u8DEF\u5F84\u8986\u76D6\u7F13\u5B58\u65F6\u957F\uFF0CJSON \u683C\u5F0F\uFF0C\u5982 [{"path":"/photos/*","cache_expiration":1440}]'},Tt={name:"enable_sign",type:"bool",default:"false",required:!0,help:"Enable signature validation for this storage"},Ut={name:"disable_index",type:"bool",default:"false",required:!1,help:"Disable directory listing for this storage"};function Nce(){return{name:"proxy_range",type:"bool",default:"true",required:!1,help:"Need to enable proxy"}}var yM={name:"web_proxy",type:"bool",default:"false",required:!1},Mv={name:"webdav_policy",type:"select",options:"302_redirect,use_proxy_url,native_proxy",default:"302_redirect",required:!1};function Rt(r=""){let e=yc(r),t=AQ.has(e)?[Nce()]:[];if(gQ(e))return[...t,{...Mv,options:"use_proxy_url,native_proxy",default:"native_proxy"}];let i=HC(e);return[...t,i?{...yM,default:"true"}:{...yM},i?{...Mv,default:"native_proxy"}:{...Mv}]}var er={name:"seed_policy",type:"select",options:"inherit,on,off",default:"inherit",required:!0,help:"Override automatic transfer-seed generation for this storage"},pl={name:"down_proxy_url",type:"string",default:"",required:!1,help:"\u81EA\u5B9A\u4E49\u4E0B\u8F7D\u4EE3\u7406\u5730\u5740\uFF08\u652F\u6301 $path \u5360\u4F4D\u7B26\uFF1B\u7559\u7A7A\u5219\u7528\u672C\u670D\u52A1\u4EE3\u7406\uFF09"},Nt={name:"disable_proxy_sign",type:"bool",default:"false",required:!1,help:"Disable sign for Download proxy URL"},mt=[...Ft,Pt,Tt,Ut,...Rt(),pl,Nt,er],Eu={AliyundriveOpen:{name:"AliyundriveOpen",default_mount_path:"/aliyundrive",common:mt,additional:[{name:"refresh_token",type:"text",default:"",required:!0,help:"true"},{name:"drive_type",type:"select",options:"resource,backup,default",default:"resource",required:!0},{name:"drive_id",type:"string",default:"",required:!1},{name:"root_folder_id",type:"string",default:"root",required:!0},{name:"order_by",type:"select",options:"updated_at,name,size,created_at",default:"updated_at",required:!1},{name:"order_direction",type:"select",options:"DESC,ASC",default:"DESC",required:!1},{name:"api_url_address",type:"string",default:"https://api.oplist.org/alicloud/renewapi",required:!1,help:"true"},{name:"alipan_type",type:"select",options:"alipanQR,alipanTV",default:"alipanQR",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"remove_way",type:"select",options:"trash,delete",default:"trash",required:!1}],config:{name:"AliyundriveOpen",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"root"}},Onedrive:{name:"Onedrive",default_mount_path:"/onedrive",common:[...Ft,Pt,Tt,Ut,...Rt("Onedrive"),pl,Nt],additional:[{name:"root_folder_path",type:"string",default:"/",required:!0},{name:"region",type:"select",options:"global,cn,us,de",default:"global",required:!0},{name:"is_sharepoint",type:"bool",default:"false",required:!1},{name:"use_online_api",type:"bool",default:"true",required:!1},{name:"api_url_address",type:"string",default:"https://api.oplist.org/onedrive/renewapi",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"redirect_uri",type:"string",default:"https://api.oplist.org/onedrive/callback",required:!0},{name:"refresh_token",type:"string",default:"",required:!0},{name:"site_id",type:"string",default:"",required:!1},{name:"chunk_size",type:"number",default:"5",required:!1},{name:"custom_host",type:"string",default:"",required:!1,help:"true"},{name:"disable_disk_usage",type:"bool",default:"false",required:!1,help:"true"},{name:"enable_direct_upload",type:"bool",default:"false",required:!1,help:"true"},{name:"order_by",type:"select",options:"filename,modified_time,size",default:"filename",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Onedrive",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},OnedriveAPP:{name:"OnedriveAPP",default_mount_path:"/onedrive_app",common:[...Ft,Pt,Tt,Ut,...Rt("OnedriveAPP"),pl,Nt],additional:[{name:"root_folder_path",type:"string",default:"/",required:!0},{name:"region",type:"select",options:"global,cn,us,de",default:"global",required:!0},{name:"client_id",type:"string",default:"",required:!0},{name:"client_secret",type:"string",default:"",required:!0},{name:"tenant_id",type:"string",default:"",required:!0},{name:"email",type:"string",default:"",required:!0},{name:"chunk_size",type:"number",default:"5",required:!1},{name:"custom_host",type:"string",default:"",required:!1,help:"true"},{name:"disable_disk_usage",type:"bool",default:"false",required:!1,help:"true"},{name:"enable_direct_upload",type:"bool",default:"false",required:!1,help:"true"},{name:"order_by",type:"select",options:"filename,modified_time,size",default:"filename",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"OnedriveAPP",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},GoogleDrive:{name:"GoogleDrive",default_mount_path:"/google-drive",common:[...Ft,Pt,Tt,Ut,...Rt("GoogleDrive"),Nt,er],additional:[{name:"refresh_token",type:"text",default:"",required:!0,help:"true"},{name:"root_folder_id",type:"string",default:"root",required:!1},{name:"order_by",type:"select",options:"folder,name,modifiedTime desc",default:"folder,name,modifiedTime desc",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1},{name:"api_url_address",type:"string",default:"https://api.alist.nn.ci/googledrive/token",required:!1,help:"true"},{name:"use_online_api",type:"bool",default:"true",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"chunk_size",type:"number",default:"5",required:!1}],config:{name:"GoogleDrive",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"root"}},Quark:{name:"Quark",default_mount_path:"/quark",common:mt,additional:[{name:"variant",type:"select",options:"Quark,UC",default:"Quark",required:!0},{name:"cookie",type:"text",default:"",required:!0,help:"true"},{name:"root_folder_id",type:"string",default:"0",required:!0},{name:"order_by",type:"select",options:"none,file_type,file_name,updated_at",default:"none",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1},{name:"use_transcoding_address",type:"bool",default:"false",required:!1},{name:"only_list_video_file",type:"bool",default:"false",required:!1}],config:{name:"Quark",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"0"}},"123Pan":{name:"123Pan",default_mount_path:"/123",common:[...Ft,Pt,Tt,Ut,...Rt("123Pan"),Nt,er],additional:[{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"access_token",type:"string",default:"",required:!1,help:"\u767B\u5F55\u4EE4\u724C\uFF08\u53EF\u9009\uFF0C\u81EA\u52A8\u6301\u4E45\u5316\uFF0C\u65E0\u9700\u624B\u52A8\u586B\u5199\uFF09\u3002\u4EC5\u9700\u586B\u5199\u4E0A\u65B9 123 \u7F51\u76D8\u624B\u673A\u53F7\u548C\u5BC6\u7801\uFF0C\u767B\u5F55\u540E\u81EA\u52A8\u83B7\u53D6\u5E76\u4FDD\u5B58\uFF0C\u8DF3\u8FC7\u91CD\u590D\u767B\u5F55\u53EF\u907F\u514D\u5883\u5916 IP \u89E6\u53D1\u98CE\u63A7\u3002"},{name:"cookie",type:"text",default:"",required:!1,help:"\u6D4F\u89C8\u5668 Cookie\uFF08\u53EF\u9009\uFF09\u3002\u5728 123 \u7F51\u76D8\u7F51\u9875\u767B\u5F55\u540E\uFF0C\u4ECE\u5F00\u53D1\u8005\u5DE5\u5177\u590D\u5236\u8BF7\u6C42\u5934\u4E2D\u7684 Cookie \u6574\u4E32\u7C98\u8D34\u4E8E\u6B64\uFF08\u542B sso-token\uFF09\uFF0C\u6216\u4ECE Authorization: Bearer <token> \u4E2D\u590D\u5236 token/Bearer \u503C\u3002\u89E3\u6790\u51FA\u7684 JWT \u4F1A\u4F5C\u4E3A Bearer \u4EE4\u724C\u4F7F\u7528\uFF0C\u6548\u679C\u7B49\u540C\u8BBF\u95EE\u4EE4\u724C\uFF0C\u9002\u5408\u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u88AB\u98CE\u63A7\u62E6\u622A\u7684\u73AF\u5883\u3002"},{name:"root_id",type:"string",default:"0",required:!1},{name:"upload_thread",type:"number",default:"3",required:!1,help:"the threads of upload"},{name:"platform",type:"string",default:"web",required:!1,help:"the platform header value, sent with API requests"},{name:"order_by",type:"select",options:"file_id,file_name,size,created_at,updated_at",default:"file_id",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"desc",required:!1}],config:{name:"123Pan",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"0"}},BaiduNetdisk:{name:"BaiduNetdisk",default_mount_path:"/baidu",common:[...Ft,Pt,Tt,Ut,...Rt("BaiduNetdisk"),Nt,er],additional:[{name:"refresh_token",type:"text",default:"",required:!0,help:"true"},{name:"access_token",type:"string",default:"",required:!0,help:"\u8BBF\u95EE\u4EE4\u724C\uFF08\u5FC5\u586B\uFF09\u3002\u901A\u8FC7 https://api.oplist.org/ \u83B7\u53D6\u3002\u82E5\u4EE4\u724C\u5931\u6548\uFF0C\u6302\u8F7D\u65F6\u4F1A\u81EA\u52A8\u6839\u636E refresh_token \u901A\u8FC7\u5728\u7EBF API \u6362\u65B0\u5E76\u6301\u4E45\u5316\u3002"},{name:"use_online_api",type:"bool",default:"true",required:!1,help:"\u4F7F\u7528\u5728\u7EBF API \u5237\u65B0 token\uFF08\u65E0\u9700 ClientID/ClientSecret\uFF09"},{name:"api_url_address",type:"string",default:"https://api.oplist.org/baiduyun/renewapi",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"download_api",type:"select",options:"official,crack,crack_video",default:"official",required:!1},{name:"custom_crack_ua",type:"string",default:"netdisk",required:!0},{name:"order_by",type:"select",options:"name,time,size",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1},{name:"only_list_video_file",type:"bool",default:"false",required:!1},{name:"upload_thread",type:"string",default:"3",required:!1,help:"1<=thread<=32"},{name:"upload_timeout",type:"number",default:"60",required:!1,help:"per-slice upload timeout in seconds"},{name:"custom_upload_part_size",type:"number",default:"0",required:!1,help:"0 for auto"},{name:"use_dynamic_upload_api",type:"bool",default:"true",required:!1,help:"dynamically get upload api domain, when enabled, the 'Upload API' setting will be used as a fallback if failed to get"},{name:"upload_api",type:"string",default:"https://d.pcs.baidu.com",required:!1},{name:"low_bandwith_upload_mode",type:"bool",default:"false",required:!1}],config:{name:"BaiduNetdisk",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},"115Open":{name:"115Open",default_mount_path:"/115",common:[...Ft,Pt,Tt,Ut,...Rt("115Open"),Nt,er],additional:[{name:"access_token",type:"string",default:"",required:!0,help:"\u8BBF\u95EE\u4EE4\u724C\uFF08\u5FC5\u586B\uFF09\u3002\u901A\u8FC7 115 \u5F00\u653E\u5E73\u53F0\u83B7\u53D6\uFF1B\u5931\u6548\u65F6\u81EA\u52A8\u7528 refresh_token \u5237\u65B0\u5E76\u6301\u4E45\u5316\u3002"},{name:"refresh_token",type:"string",default:"",required:!0,help:"\u5237\u65B0\u4EE4\u724C\uFF08\u5FC5\u586B\uFF09\u3002\u901A\u8FC7 115 \u5F00\u653E\u5E73\u53F0\u83B7\u53D6\uFF1Baccess_token \u5931\u6548\u65F6\u81EA\u52A8\u5237\u65B0\u3002"},{name:"root_id",type:"string",default:"0",required:!1,help:"\u6839\u6587\u4EF6\u5939 ID\uFF0C\u9ED8\u8BA4 0\uFF08\u6839\u76EE\u5F55\uFF09"},{name:"order_by",type:"select",options:"file_name,file_size,user_utime,file_type",default:"file_name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1},{name:"page_size",type:"number",default:"200",required:!1,help:"list api per page size (1~1150)"},{name:"limit_rate",type:"float",default:"1",required:!1,help:"limit all api request rate ([limit]r/1s)\uFF0C0 \u8868\u793A\u4E0D\u9650\u901F"}],config:{name:"115Open",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"0"}},"GitHub API":{name:"GitHub API",default_mount_path:"/github",common:mt,additional:[{name:"root_folder_path",type:"string",default:"/",required:!0},{name:"token",type:"string",default:"",required:!0},{name:"owner",type:"string",default:"",required:!0},{name:"repo",type:"string",default:"",required:!0},{name:"ref",type:"string",default:"",required:!1,help:"A branch, a tag or a commit SHA, default branch by default."},{name:"gh_proxy",type:"string",default:"",required:!1,help:"GitHub proxy, e.g. https://ghproxy.net/raw.githubusercontent.com"},{name:"committer_name",type:"string",default:"",required:!1},{name:"committer_email",type:"string",default:"",required:!1},{name:"author_name",type:"string",default:"",required:!1},{name:"author_email",type:"string",default:"",required:!1},{name:"mkdir_commit_message",type:"text",default:"{{.UserName}} mkdir {{.ObjPath}}",required:!1},{name:"delete_commit_message",type:"text",default:"{{.UserName}} remove {{.ObjPath}}",required:!1},{name:"put_commit_message",type:"text",default:"{{.UserName}} upload {{.ObjPath}}",required:!1},{name:"rename_commit_message",type:"text",default:"{{.UserName}} rename {{.ObjPath}} to {{.TargetName}}",required:!1},{name:"copy_commit_message",type:"text",default:"{{.UserName}} copy {{.ObjPath}} to {{.TargetPath}}",required:!1},{name:"move_commit_message",type:"text",default:"{{.UserName}} move {{.ObjPath}} to {{.TargetPath}}",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"GitHub API",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},Thunder:{name:"Thunder",default_mount_path:"/thunder",common:mt,additional:[{name:"root_folder_id",type:"string",default:"",required:!1},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"captcha_token",type:"string",default:"",required:!1},{name:"credit_key",type:"string",default:"",required:!1,help:"credit key, used for login"},{name:"device_id",type:"string",default:"",required:!1,help:"32 hex characters"},{name:"space",type:"string",default:"",required:!1,help:"device id for remote device"},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Thunder",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:""}},ThunderExpert:{name:"ThunderExpert",default_mount_path:"/thunderexpert",common:mt,additional:[{name:"root_folder_id",type:"string",default:"",required:!1},{name:"login_type",type:"select",options:"user,refresh_token",default:"user",required:!0},{name:"sign_type",type:"select",options:"algorithms,captcha_sign",default:"algorithms",required:!0},{name:"username",type:"string",default:"",required:!1,help:"login type is user, this is required"},{name:"password",type:"string",default:"",required:!1,help:"login type is user, this is required"},{name:"refresh_token",type:"string",default:"",required:!1,help:"login type is refresh_token, this is required"},{name:"algorithms",type:"string",default:"9uJNVj/wLmdwKrJaVj/omlQ,Oz64Lp0GigmChHMf/6TNfxx7O9PyopcczMsnf,Eb+L7Ce+Ej48u,jKY0,ASr0zCl6v8W4aidjPK5KHd1Lq3t+vBFf41dqv5+fnOd,wQlozdg6r1qxh0eRmt3QgNXOvSZO6q/GXK,gmirk+ciAvIgA/cxUUCema47jr/YToixTT+Q6O,5IiCoM9B1/788ntB,P07JH0h6qoM6TSUAK2aL9T5s2QBVeY9JWvalf,+oK0AN",required:!1},{name:"captcha_sign",type:"string",default:"",required:!1},{name:"timestamp",type:"string",default:"",required:!1},{name:"captcha_token",type:"string",default:"",required:!1},{name:"credit_key",type:"string",default:"",required:!1,help:"credit key, used for login"},{name:"device_id",type:"string",default:"",required:!1},{name:"client_id",type:"string",default:"",required:!0,help:"\u8FC5\u96F7\u5F00\u653E\u5E73\u53F0 OAuth client_id\uFF08\u5FC5\u586B\uFF09\u3002"},{name:"client_secret",type:"string",default:"",required:!0,help:"\u8FC5\u96F7\u5F00\u653E\u5E73\u53F0 OAuth client_secret\uFF08\u5FC5\u586B\uFF09\u3002"},{name:"client_version",type:"string",default:"8.31.0.9726",required:!0},{name:"package_name",type:"string",default:"com.xunlei.downloadprovider",required:!0},{name:"user_agent",type:"string",default:"ANDROID-com.xunlei.downloadprovider/8.31.0.9726 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/512000 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",required:!0},{name:"download_user_agent",type:"string",default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",required:!0},{name:"use_video_url",type:"bool",default:"false",required:!1},{name:"space",type:"string",default:"",required:!1,help:"device id for remote device"},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"ThunderExpert",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:""}},"189Cloud":{name:"189Cloud",default_mount_path:"/189",common:[...Ft,Pt,Tt,Ut,...Rt("189Cloud"),Nt,er],additional:[{name:"username",type:"string",default:"",required:!0,help:"the phone number used to log in"},{name:"password",type:"string",default:"",required:!0,help:"password for login"},{name:"cookie",type:"text",default:"",required:!1,help:"Fill in the cookie if need captcha (\u82E5\u9047\u6ED1\u5757\u9A8C\u8BC1\u7801\u6216\u8BBE\u5907\u9501\uFF0C\u53EF\u5728\u6D4F\u89C8\u5668\u767B\u5F55\u540E\u590D\u5236 Cookie \u586B\u5165)"},{name:"root_folder_id",type:"string",default:"-11",required:!1,help:"\u6839\u6587\u4EF6\u5939ID\uFF0C\u9ED8\u8BA4\u4E3A -11\uFF08\u4E2A\u4EBA\u4E91\u6839\u76EE\u5F55\uFF09"},{name:"order_by",type:"select",options:"lastOpTime,filename,fileSize",default:"lastOpTime",required:!1},{name:"order_direction",type:"select",options:"desc,asc",default:"desc",required:!1}],config:{name:"189Cloud",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"-11"}},Lanzou:{name:"Lanzou",default_mount_path:"/lanzou",common:mt,additional:[{name:"type",type:"select",options:"cookie,account,url",default:"cookie",required:!0},{name:"account",type:"string",default:"",required:!1,help:"\u8D26\u53F7\uFF08\u624B\u673A\u53F7/UID\uFF09\uFF0C\u4EC5 account \u6A21\u5F0F\u9700\u586B\u5199"},{name:"password",type:"string",default:"",required:!1,help:"\u5BC6\u7801\uFF0C\u4EC5 account \u6A21\u5F0F\u9700\u586B\u5199"},{name:"cookie",type:"text",default:"",required:!1,help:"\u767B\u5F55 Cookie\uFF08\u542B ylogin, phpdisk_info \u7B49\uFF09\uFF0Ccookie \u6A21\u5F0F\u9700\u586B\u5199\uFF1B\u6709\u6548\u671F\u7EA6 15 \u5929"},{name:"root_folder_id",type:"string",default:"-1",required:!1,help:"\u6839\u6587\u4EF6\u5939 ID / \u5206\u4EAB ID\uFF08\u4E2A\u4EBA\u76D8\u9ED8\u8BA4 -1\uFF0C\u5206\u4EAB\u94FE\u63A5\u586B\u5206\u4EAB ID \u5982 b00xxxx\uFF09"},{name:"share_password",type:"string",default:"",required:!1,help:"\u63D0\u53D6\u7801 / \u8BBF\u95EE\u5BC6\u7801\uFF08\u65E0\u5BC6\u7801\u7559\u7A7A\uFF09"},{name:"baseUrl",type:"string",default:"https://pc.woozooo.com",required:!1,help:"\u57FA\u672C API \u57DF\u540D"},{name:"shareUrl",type:"string",default:"https://pan.lanzoui.com",required:!1,help:"\u5206\u4EAB\u9875\u9762\u89E3\u6790\u57DF\u540D"},{name:"user_agent",type:"string",default:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/142.0.0.0 OpenList/42",required:!0,help:"\u53D1\u9001\u7ED9\u84DD\u594F\u4E91 API \u4E0E\u76F4\u94FE\u89E3\u6790\u65F6\u643A\u5E26\u7684\u5BA2\u6237\u7AEF User-Agent"},{name:"repair_file_info",type:"bool",default:"false",required:!1,help:"\u901A\u8FC7 HEAD \u8BF7\u6C42\u4FEE\u6B63\u6587\u4EF6\u7CBE\u786E\u5927\u5C0F\u4E0E\u4FEE\u6539\u65F6\u95F4\uFF08WebDAV \u63A8\u8350\u5F00\u542F\uFF09"},{name:"order_by",type:"select",options:"name,size,time",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Lanzou",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"-1"}},WebDav:{name:"WebDav",default_mount_path:"/webdav",common:[...Ft,Pt,Tt,Ut,...Rt("WebDav"),pl,Nt,er],additional:[{name:"vendor",type:"select",options:"other,sharepoint",default:"other",required:!0},{name:"address",type:"string",default:"",required:!0},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"tls_insecure_skip_verify",type:"bool",default:"false",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"WebDav",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},WoPan:{name:"WoPan",default_mount_path:"/wopan",common:mt,additional:[{name:"root_folder_id",type:"string",default:"0",required:!1},{name:"refresh_token",type:"text",default:"",required:!0},{name:"family_id",type:"string",default:"",required:!1,help:"true"},{name:"sort_rule",type:"select",options:"name_asc,name_desc,time_asc,time_desc,size_asc,size_desc",default:"name_asc",required:!1},{name:"access_token",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"WoPan",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"0",no_overwrite_upload:!0}},S3:{name:"S3",default_mount_path:"/s3",common:mt,additional:[{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"bucket",type:"string",default:"",required:!0},{name:"endpoint",type:"string",default:"",required:!0},{name:"region",type:"string",default:"",required:!1},{name:"access_key_id",type:"string",default:"",required:!0},{name:"secret_access_key",type:"string",default:"",required:!0},{name:"session_token",type:"string",default:"",required:!1},{name:"custom_host",type:"string",default:"",required:!1},{name:"enable_custom_host_presign",type:"bool",default:"false",required:!1},{name:"sign_url_expire",type:"number",default:"4",required:!1},{name:"placeholder",type:"string",default:"",required:!1},{name:"force_path_style",type:"bool",default:"false",required:!1},{name:"list_object_version",type:"select",options:"v1,v2",default:"v1",required:!1},{name:"remove_bucket",type:"bool",default:"false",required:!1,help:"true"},{name:"add_filename_to_disposition",type:"bool",default:"false",required:!1,help:"true"},{name:"enable_direct_upload",type:"bool",default:"false",required:!1},{name:"direct_upload_host",type:"string",default:"",required:!1},{name:"user_agent",type:"string",default:"",required:!1,help:"true"},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"S3",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0}},Doge:{name:"Doge",default_mount_path:"/doge",common:mt,additional:[{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"bucket",type:"string",default:"",required:!0},{name:"endpoint",type:"string",default:"",required:!0},{name:"region",type:"string",default:"",required:!1},{name:"access_key_id",type:"string",default:"",required:!0},{name:"secret_access_key",type:"string",default:"",required:!0},{name:"session_token",type:"string",default:"",required:!1},{name:"custom_host",type:"string",default:"",required:!1},{name:"enable_custom_host_presign",type:"bool",default:"false",required:!1},{name:"sign_url_expire",type:"number",default:"4",required:!1},{name:"placeholder",type:"string",default:"",required:!1},{name:"force_path_style",type:"bool",default:"false",required:!1},{name:"list_object_version",type:"select",options:"v1,v2",default:"v1",required:!1},{name:"remove_bucket",type:"bool",default:"false",required:!1,help:"true"},{name:"add_filename_to_disposition",type:"bool",default:"false",required:!1,help:"true"},{name:"enable_direct_upload",type:"bool",default:"false",required:!1},{name:"direct_upload_host",type:"string",default:"",required:!1},{name:"user_agent",type:"string",default:"",required:!1,help:"true"},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Doge",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0}},WeiYun:{name:"WeiYun",default_mount_path:"/weiyun",common:[...Ft,Pt,Tt,Ut,...Rt("WeiYun"),Nt,er],additional:[{name:"root_folder_id",type:"string",default:"",required:!1},{name:"cookies",type:"text",default:"",required:!0},{name:"order_by",type:"select",options:"name,size,updated_at",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1},{name:"upload_thread",type:"string",default:"4",required:!1,help:"4<=thread<=32"}],config:{name:"WeiYun",local_sort:!1,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"",check_status:!0}},SFTP:{name:"SFTP",default_mount_path:"/sftp",common:[...Ft,Pt,Tt,Ut,...Rt("SFTP"),Nt,er],additional:[{name:"address",type:"string",default:"",required:!0,help:"SSH host:port (e.g. 127.0.0.1:22)"},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!1},{name:"private_key",type:"text",default:"",required:!1},{name:"passphrase",type:"string",default:"",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"ignore_symlink_error",type:"bool",default:"false",required:!1,help:"Ignore symlink error"}],config:{name:"SFTP",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0,no_link_url:!0}},FTP:{name:"FTP",default_mount_path:"/ftp",common:[...Ft,Pt,Tt,Ut,...Rt("FTP"),Nt,er],additional:[{name:"address",type:"string",default:"",required:!0,help:"FTP host:port (e.g. 127.0.0.1:21)"},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"encoding",type:"string",default:"utf-8",required:!0,help:"Character encoding, e.g. utf-8, gbk, gb2312"},{name:"cwd_list",type:"bool",default:"false",required:!1,help:"Enter directory before listing"},{name:"root_folder_path",type:"string",default:"/",required:!1}],config:{name:"FTP",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0,no_link_url:!0}},PikPak:{name:"PikPak",default_mount_path:"/pikpak",common:mt,additional:[{name:"root_folder_id",type:"string",default:"",required:!1},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"platform",type:"select",options:"web,android,pc",default:"web",required:!0},{name:"refresh_token",type:"text",default:"",required:!1},{name:"captcha_token",type:"text",default:"",required:!1},{name:"device_id",type:"string",default:"",required:!1},{name:"disable_media_link",type:"bool",default:"true",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"PikPak",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:""}},Seafile:{name:"Seafile",default_mount_path:"/seafile",common:mt,additional:[{name:"address",type:"string",default:"",required:!0},{name:"username",type:"string",default:"",required:!1},{name:"password",type:"string",default:"",required:!1},{name:"token",type:"string",default:"",required:!1},{name:"repo_id",type:"string",default:"",required:!1},{name:"repo_pwd",type:"string",default:"",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Seafile",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},YandexDisk:{name:"YandexDisk",default_mount_path:"/yandex",common:mt,additional:[{name:"refresh_token",type:"text",default:"",required:!0},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"use_online_api",type:"bool",default:"true",required:!1},{name:"api_url_address",type:"string",default:"https://api.oplist.org/yandexui/renewapi",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,path,created,modified,size",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"YandexDisk",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},Terabox:{name:"Terabox",default_mount_path:"/terabox",common:[...Ft,Pt,Tt,Ut,...Rt("Terabox"),Nt,er],additional:[{name:"cookie",type:"text",default:"",required:!0},{name:"download_api",type:"select",options:"official,crack",default:"official",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"order_by",type:"select",options:"name,time,size",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Terabox",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},MediaTrack:{name:"MediaTrack",default_mount_path:"/mediatrack",common:mt,additional:[{name:"access_token",type:"text",default:"",required:!0},{name:"project_id",type:"string",default:"",required:!1},{name:"root_folder_id",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"updated_at,title,size",default:"title",required:!1},{name:"order_desc",type:"bool",default:"false",required:!1}],config:{name:"MediaTrack",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:""}},Alias:{name:"Alias",default_mount_path:"/alias",common:[...Ft,Pt,Tt,Ut,...Rt("Alias"),pl,Nt,er],additional:[{name:"paths",type:"text",default:"",required:!0,help:"Newline-separated list of paths, e.g. /local or sub:/target"},{name:"read_conflict_policy",type:"select",options:"first,random,all",default:"first",required:!1},{name:"write_conflict_policy",type:"select",options:"disabled,first,deterministic,deterministic_or_all,all,all_strict",default:"disabled",required:!1},{name:"put_conflict_policy",type:"select",options:"disabled,first,deterministic,deterministic_or_all,all,all_strict,random,quota,quota_strict",default:"disabled",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Alias",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!0,no_upload:!1,need_ms:!1,default_root:"/"}},Dropbox:{name:"Dropbox",default_mount_path:"/dropbox",common:mt,additional:[{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"use_online_api",type:"bool",default:"false",required:!1},{name:"api_url_address",type:"string",default:"https://api.oplist.org/dropboxs/renewapi",required:!1},{name:"client_id",type:"string",default:"",required:!1},{name:"client_secret",type:"string",default:"",required:!1},{name:"refresh_token",type:"text",default:"",required:!0},{name:"root_namespace_id",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Dropbox",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},WPS:{name:"WPS",default_mount_path:"/wps",common:mt,additional:[{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"cookie",type:"text",default:"",required:!0},{name:"mode",type:"select",options:"Personal,Business",default:"Personal",required:!1},{name:"custom_ua",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"WPS",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},"139Yun":{name:"139Yun",default_mount_path:"/139",common:[...Ft,Pt,Tt,Ut,...Rt("139Yun"),pl,Nt,er],additional:[{name:"authorization",type:"text",default:"",required:!0},{name:"root_folder_id",type:"string",default:"",required:!1},{name:"type",type:"select",options:"personal_new,family,group,personal,share",default:"personal_new",required:!1},{name:"link_id",type:"text",default:"",required:!1},{name:"cloud_id",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"139Yun",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:""}},Mega_nz:{name:"Mega_nz",default_mount_path:"/mega",common:[...Ft,Pt,Tt,Ut,...Rt("Mega_nz"),Nt,er],additional:[{name:"email",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!0},{name:"two_fa_code",type:"string",default:"",required:!1},{name:"two_fa_secret",type:"string",default:"",required:!1},{name:"move_to_trash",type:"bool",default:"true",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"Mega_nz",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},"115Share":{name:"115Share",default_mount_path:"/115_share",common:mt,additional:[{name:"share_code",type:"text",default:"",required:!0},{name:"receive_code",type:"text",default:"",required:!0},{name:"cookie",type:"text",default:"",required:!1},{name:"root_folder_id",type:"string",default:"0",required:!1},{name:"page_size",type:"number",default:"1000",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"115Share",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"0"}},"123PanShare":{name:"123PanShare",default_mount_path:"/123_share",common:[...Ft,Pt,Tt,Ut,...Rt("123PanShare"),Nt,er],additional:[{name:"sharekey",type:"string",default:"",required:!0},{name:"sharepassword",type:"string",default:"",required:!1},{name:"accesstoken",type:"text",default:"",required:!1},{name:"root_folder_id",type:"string",default:"0",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"123PanShare",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"0"}},AliyundriveShare:{name:"AliyundriveShare",default_mount_path:"/aliyun_share",common:mt,additional:[{name:"share_id",type:"string",default:"",required:!0},{name:"share_pwd",type:"string",default:"",required:!1},{name:"root_folder_id",type:"string",default:"root",required:!1},{name:"order_by",type:"select",options:"name,size,updated_at,created_at",default:"name",required:!1},{name:"order_direction",type:"select",options:"ASC,DESC",default:"ASC",required:!1}],config:{name:"AliyundriveShare",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"root"}},OnedriveSharelink:{name:"OnedriveSharelink",default_mount_path:"/onedrive_share",common:mt,additional:[{name:"url",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"OnedriveSharelink",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"/"}},PikPakShare:{name:"PikPakShare",default_mount_path:"/pikpak_share",common:mt,additional:[{name:"share_id",type:"string",default:"",required:!0},{name:"share_pwd",type:"string",default:"",required:!1},{name:"platform",type:"select",options:"android,web,pc",default:"web",required:!1},{name:"root_folder_id",type:"string",default:"",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"PikPakShare",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:""}},SMB:{name:"SMB",default_mount_path:"/smb",common:[...Ft,Pt,Tt,Ut,...Rt("SMB"),Nt,er],additional:[{name:"address",type:"string",default:"",required:!0},{name:"share_name",type:"string",default:"",required:!0},{name:"username",type:"string",default:"",required:!0},{name:"password",type:"string",default:"",required:!1},{name:"port",type:"number",default:"445",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!1},{name:"order_by",type:"select",options:"name,size,modified",default:"name",required:!1},{name:"order_direction",type:"select",options:"asc,desc",default:"asc",required:!1}],config:{name:"SMB",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!0,no_upload:!1,need_ms:!1,default_root:"/"}},Crypt:{name:"Crypt",default_mount_path:"/crypt",common:[...Ft,Pt,Tt,Ut,...Rt("Crypt"),Nt,er],additional:[{name:"remote_path",type:"string",default:"",required:!0,help:"\u52A0\u5BC6\u6570\u636E\u5B9E\u9645\u5B58\u50A8\u7684\u4F4D\u7F6E\uFF08\u53E6\u4E00\u4E2A\u5DF2\u6302\u8F7D\u5B58\u50A8\u7684\u8DEF\u5F84\uFF0C\u5982 /aliyundrive/enc\uFF09"},{name:"password",type:"string",default:"",required:!0,help:"\u4E3B\u5BC6\u7801\uFF08\u7528\u4E8E\u6D3E\u751F\u52A0\u5BC6\u5BC6\u94A5\uFF09"},{name:"salt",type:"string",default:"",required:!1,help:"\u76D0\uFF08\u7B2C\u4E8C\u5BC6\u7801\uFF09\u3002\u53EF\u9009\u4F46\u63A8\u8350\uFF0C\u63D0\u9AD8\u5BC6\u94A5\u5F3A\u5EA6"},{name:"filename_encryption",type:"select",options:"off,standard,obfuscate",default:"off",required:!0,help:"\u6587\u4EF6\u540D\u52A0\u5BC6\u65B9\u5F0F\u3002\u5F53\u524D\u4EC5\u652F\u6301 off\uFF08\u6587\u4EF6\u540D\u4E0D\u52A0\u5BC6\uFF0C\u4EC5\u5185\u5BB9\u52A0\u5BC6\uFF09"},{name:"directory_name_encryption",type:"select",options:"false,true",default:"false",required:!0},{name:"encrypted_suffix",type:"string",default:".bin",required:!0,help:"\u52A0\u5BC6\u6587\u4EF6\u7684\u540E\u7F00\uFF08\u9AD8\u7EA7\u9009\u9879\uFF09"},{name:"filename_encoding",type:"select",options:"base64,base32,base32768",default:"base64",required:!0},{name:"thumbnail",type:"bool",default:"false",required:!1},{name:"show_hidden",type:"bool",default:"true",required:!1}],config:{name:"Crypt",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!0,no_upload:!1,need_ms:!1,default_root:"/",no_link_url:!0,check_status:!0}},Virtual:{name:"Virtual",default_mount_path:"/virtual",common:[...Ft,Pt,Tt,Ut,...Rt("Virtual"),Nt,er],additional:[{name:"num_file",type:"number",default:"30",required:!0,help:"\u6BCF\u4E2A\u76EE\u5F55\u4E0B\u751F\u6210\u7684\u5360\u4F4D\u6587\u4EF6\u6570\u91CF"},{name:"num_folder",type:"number",default:"30",required:!0,help:"\u6BCF\u4E2A\u76EE\u5F55\u4E0B\u751F\u6210\u7684\u5360\u4F4D\u76EE\u5F55\u6570\u91CF"},{name:"max_file_size",type:"number",default:"1073741824",required:!0,help:"\u5360\u4F4D\u6587\u4EF6\u6700\u5927\u5B57\u8282\u6570"},{name:"min_file_size",type:"number",default:"1048576",required:!0,help:"\u5360\u4F4D\u6587\u4EF6\u6700\u5C0F\u5B57\u8282\u6570"}],config:{name:"Virtual",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!1,need_ms:!0,default_root:"/",no_link_url:!0}},AListV3:{name:"AListV3",default_mount_path:"/alist",common:[...Ft,Pt,Tt,Ut,...Rt("AListV3"),pl,Nt,er],additional:[{name:"url",type:"string",default:"",required:!0,help:"\u53E6\u4E00\u4E2A OpenList / AList \u5B9E\u4F8B\u7684\u5730\u5740\uFF0C\u5982 https://example.com"},{name:"meta_password",type:"string",default:"",required:!1},{name:"username",type:"string",default:"",required:!1},{name:"password",type:"string",default:"",required:!1},{name:"token",type:"string",default:"",required:!1,help:"\u8BBF\u95EE\u4EE4\u724C\uFF08\u53EF\u9009\uFF0C\u81EA\u52A8\u6301\u4E45\u5316\uFF0C\u65E0\u9700\u624B\u52A8\u586B\u5199\uFF09"},{name:"pass_ip_to_upsteam",type:"bool",default:"true",required:!1},{name:"pass_ua_to_upsteam",type:"bool",default:"true",required:!1},{name:"forward_archive_requests",type:"bool",default:"true",required:!1}],config:{name:"AListV3",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},UrlTree:{name:"UrlTree",default_mount_path:"/url_tree",common:mt,additional:[{name:"url_structure",type:"text",default:"",required:!0,help:"URL \u6811\u7ED3\u6784\u6587\u672C\uFF0C\u6BCF\u884C\u7F29\u8FDB 2 \u7A7A\u683C\u8868\u793A\u4E00\u7EA7\u3002\u76EE\u5F55\u884C\u4EE5 : \u7ED3\u5C3E\uFF0C\u6587\u4EF6\u884C\u683C\u5F0F [FileName:][FileSize:][Modified:]Url"},{name:"head_size",type:"bool",default:"false",required:!1,help:"\u7528 HEAD \u8BF7\u6C42\u83B7\u53D6\u6587\u4EF6\u5927\u5C0F\uFF08\u53EF\u80FD\u5931\u8D25\uFF09"},{name:"writable",type:"bool",default:"false",required:!1,help:"\u5141\u8BB8\u589E\u5220\u6539\u6587\u4EF6\u6811\uFF08\u4F1A\u6301\u4E45\u5316\u56DE url_structure\uFF09"}],config:{name:"UrlTree",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!0,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0}},Strm:{name:"Strm",default_mount_path:"/strm",common:[...Ft,Pt,Tt,Ut,...Rt("Strm"),Nt,er],additional:[{name:"paths",type:"text",default:"",required:!0,help:"\u8DEF\u5F84\u6620\u5C04\uFF0C\u6BCF\u884C\u4E00\u4E2A\uFF1A\u865A\u62DF\u540D:\u5E95\u5C42\u8DEF\u5F84\uFF08\u5982 movies:/aliyundrive/movies\uFF09\uFF0C\u6216\u76F4\u63A5\u5E95\u5C42\u8DEF\u5F84"},{name:"siteUrl",type:"text",default:"",required:!1,help:"strm \u6587\u4EF6\u5185\u5BB9\u7684 URL \u524D\u7F00\uFF08\u7559\u7A7A\u5219\u7528\u76F8\u5BF9\u8DEF\u5F84\uFF09"},{name:"PathPrefix",type:"text",default:"/d",required:!1,help:"\u4E0B\u8F7D\u8DEF\u5F84\u524D\u7F00"},{name:"downloadFileTypes",type:"text",default:"ass,srt,vtt,sub,strm",required:!1,help:"\u9700\u539F\u6837\u4E0B\u8F7D\u7684\u6587\u4EF6\u6269\u5C55\u540D\uFF08\u901A\u5E38\u4E3A\u5B57\u5E55\uFF09"},{name:"filterFileTypes",type:"text",default:"mp4,mkv,flv,avi,wmv,ts,rmvb,webm,mp3,flac,aac,wav,ogg,m4a,wma,alac",required:!1,help:"\u652F\u6301\u751F\u6210 strm \u7684\u6587\u4EF6\u6269\u5C55\u540D\uFF08\u89C6\u9891/\u97F3\u9891\uFF09"},{name:"encodePath",type:"bool",default:"true",required:!1},{name:"withoutUrl",type:"bool",default:"false",required:!1},{name:"withSign",type:"bool",default:"false",required:!1}],config:{name:"Strm",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!0,no_upload:!0,need_ms:!1,default_root:"/",no_link_url:!0}},AzureBlob:{name:"AzureBlob",default_mount_path:"/azure",common:mt,additional:[{name:"endpoint",type:"string",default:"",required:!0,help:"Azure \u5B58\u50A8\u7AEF\u70B9\uFF0C\u5982 https://accountname.blob.core.windows.net/"},{name:"access_key",type:"string",default:"",required:!0,help:"Azure \u5B58\u50A8\u8BBF\u95EE\u5BC6\u94A5\uFF08Base64\uFF09"},{name:"container_name",type:"string",default:"",required:!0,help:"\u5BB9\u5668\u540D\u79F0"},{name:"sign_url_expire",type:"number",default:"4",required:!1,help:"SAS URL \u6709\u6548\u671F\uFF08\u5C0F\u65F6\uFF09"}],config:{name:"AzureBlob",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/",check_status:!0}},USS:{name:"USS",default_mount_path:"/uss",common:mt,additional:[{name:"bucket",type:"string",default:"",required:!0},{name:"endpoint",type:"string",default:"",required:!0,help:"\u4E0B\u8F7D\u57DF\u540D\uFF08\u53EF\u542B\u534F\u8BAE\u5934\uFF09"},{name:"operator_name",type:"string",default:"",required:!0},{name:"operator_password",type:"string",default:"",required:!0},{name:"anti_theft_chain_token",type:"string",default:"",required:!1,help:"\u9632\u76D7\u94FE Token\uFF08\u7559\u7A7A\u5219\u7528\u64CD\u4F5C\u5458\u5BC6\u7801\uFF09"},{name:"sign_url_expire",type:"number",default:"4",required:!1,help:"\u94FE\u63A5\u6709\u6548\u671F\uFF08\u5C0F\u65F6\uFF09"}],config:{name:"USS",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},Alidoc:{name:"Alidoc",default_mount_path:"/alidoc",common:mt,additional:[{name:"cookie",type:"text",default:"",required:!0,help:"\u9489\u9489\u6587\u6863\u7F51\u9875 Cookie"},{name:"root_folder_id",type:"string",default:"",required:!0,help:"\u6839\u76EE\u5F55 dentryUuid"}],config:{name:"Alidoc",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:""}},Emby:{name:"Emby",default_mount_path:"/emby",common:mt,additional:[{name:"url",type:"string",default:"",required:!0,help:"Emby \u670D\u52A1\u5668\u5730\u5740\uFF0C\u5982 http://localhost:8096"},{name:"api_key",type:"string",default:"",required:!1},{name:"user_id",type:"string",default:"",required:!1},{name:"username",type:"string",default:"",required:!1},{name:"password",type:"string",default:"",required:!1},{name:"link_method",type:"select",options:"stream,download",default:"stream",required:!1},{name:"root_folder_id",type:"string",default:"1",required:!1,help:"\u6839\u76EE\u5F55 ID\uFF0C\u9ED8\u8BA4 1"}],config:{name:"Emby",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"1"}},BunnyStorage:{name:"BunnyStorage",default_mount_path:"/bunny",common:mt,additional:[{name:"storage_zone_name",type:"string",default:"",required:!0},{name:"access_key",type:"string",default:"",required:!0},{name:"endpoint",type:"string",default:"storage.bunnycdn.com",required:!1},{name:"cdn_base_url",type:"string",default:"",required:!1},{name:"cdn_token_key",type:"string",default:"",required:!1},{name:"cdn_token_method",type:"select",options:"sha256,hmac_sha256",default:"sha256",required:!1},{name:"cdn_token_include_ip",type:"bool",default:"false",required:!1},{name:"sign_url_expire",type:"number",default:"4",required:!1,help:"\u94FE\u63A5\u6709\u6548\u671F\uFF08\u5C0F\u65F6\uFF09"},{name:"placeholder",type:"string",default:".openlist",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!0}],config:{name:"BunnyStorage",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},CloudflareImgbed:{name:"CloudflareImgbed",default_mount_path:"/cfimgbed",common:mt,additional:[{name:"address",type:"string",default:"",required:!0,help:"\u56FE\u5E8A\u670D\u52A1\u5730\u5740"},{name:"token",type:"string",default:"",required:!0},{name:"small_channel_name",type:"string",default:"",required:!1},{name:"large_channel_name",type:"string",default:"",required:!1},{name:"large_channel_type",type:"string",default:"",required:!1},{name:"upload_thread",type:"number",default:"3",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!0}],config:{name:"CloudflareImgbed",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},GuangYaPan:{name:"GuangYaPan",default_mount_path:"/guangyapan",common:mt,additional:[{name:"client_id",type:"string",default:"",required:!0},{name:"device_id",type:"string",default:"",required:!1},{name:"device_sign",type:"string",default:"",required:!1},{name:"access_token",type:"text",default:"",required:!1},{name:"refresh_token",type:"text",default:"",required:!1},{name:"phone_number",type:"string",default:"",required:!1,help:"\u624B\u673A\u53F7\uFF08\u767B\u5F55\u7528\uFF0C+86 \u683C\u5F0F\uFF09"},{name:"verify_code",type:"string",default:"",required:!1},{name:"page_size",type:"number",default:"100",required:!1},{name:"order_by",type:"number",default:"3",required:!1},{name:"sort_type",type:"number",default:"1",required:!1},{name:"root_folder_path",type:"string",default:"/",required:!0}],config:{name:"GuangYaPan",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!1,need_ms:!1,default_root:"/"}},AutoIndex:{name:"AutoIndex",default_mount_path:"/autoindex",common:mt,additional:[{name:"url",type:"string",default:"",required:!0,help:"AutoIndex \u76EE\u5F55\u9875 URL"},{name:"item_xpath",type:"string",default:"//pre/a",required:!0},{name:"name_xpath",type:"string",default:"@href",required:!0},{name:"modified_xpath",type:"string",default:"string(following-sibling::text()[2])",required:!1},{name:"size_xpath",type:"string",default:"string(following-sibling::text()[1])",required:!1},{name:"ignore_file_names",type:"text",default:`. -.. -Parent Directory -Up`,required:!1},{name:"modified_time_format",type:"string",default:"02-Jan-2006 15:04",required:!1}],config:{name:"AutoIndex",local_sort:!0,only_local:!1,only_proxy:!1,no_cache:!1,no_upload:!0,need_ms:!1,default_root:""}},ProtonDrive:{name:"ProtonDrive",default_mount_path:"/proton",common:[...Ft,Pt,Tt,Ut,...Rt("ProtonDrive"),Nt,er],additional:[{name:"email",type:"string",default:"",required:!0,help:"Proton \u8D26\u53F7\u90AE\u7BB1"},{name:"password",type:"string",default:"",required:!0,help:"Proton \u8D26\u53F7\u5BC6\u7801"},{name:"two_fa_code",type:"string",default:"",required:!1,help:"\u4E24\u6B65\u9A8C\u8BC1\u7801\uFF08\u5982\u5DF2\u542F\u7528 2FA\uFF09"},{name:"root_folder_id",type:"string",default:"root",required:!1,help:"\u6839\u6587\u4EF6\u5939 LinkID\uFF0C\u9ED8\u8BA4 root \u8868\u793A\u4E3B\u5171\u4EAB\u6839\u76EE\u5F55"},{name:"chunk_size",type:"number",default:"4",required:!1,help:"\u5206\u5757\u5927\u5C0F\uFF08MB\uFF09"},{name:"use_reusable_login",type:"bool",default:"true",required:!1}],config:{name:"ProtonDrive",local_sort:!0,only_local:!1,only_proxy:!0,no_cache:!1,no_upload:!0,need_ms:!1,default_root:"root"}}};Ue.get("/driver/list",r=>r.json({code:200,message:"success",data:Eu}));Ue.get("/driver/info",r=>{let e=r.req.query("driver")||"",t=Eu[e]||Eu.AliyundriveOpen;return r.json({code:200,message:"success",data:t})});Ue.get("/setting/list",async r=>{let e=await ne(r.env),t=r.req.query("group"),i=r.req.query("groups"),n=e.settings||[];if(t!==void 0){let s=parseInt(t,10);n=n.filter(o=>o.group===s)}else if(i!==void 0){let s=i.split(",").map(o=>parseInt(o,10));n=n.filter(o=>s.includes(o.group))}return r.json({code:200,message:"success",data:n})});Ue.post("/setting/save",async r=>{let e=await r.req.json().catch(()=>[]);if(!Array.isArray(e))return r.json({code:400,message:"body must be an array",data:null});let t=await ne(r.env);t.settings||(t.settings=[]);for(let n of e){if(!n||typeof n!="object"||Array.isArray(n))continue;let s=n.key;if(typeof s!="string"||s.length===0||s.length>128)continue;let o=n.value;o!==null&&typeof o=="object"&&(o=JSON.stringify(o));let a=t.settings.findIndex(c=>c.key===s);a!==-1?(t.settings[a].value=o,typeof n.group=="number"&&(t.settings[a].group=n.group)):t.settings.push({key:s,value:o,type:typeof n.type=="string"?n.type:"string",help:typeof n.help=="string"?n.help:"",group:typeof n.group=="number"?n.group:0,flag:typeof n.flag=="number"?n.flag:0})}let i=new Set;return t.settings=t.settings.filter(n=>!n.key||i.has(n.key)?!1:(i.add(n.key),!0)),await fe(t,r.env),r.json({code:200,message:"success",data:null})});Ue.post("/setting/default",async r=>{let e=r.req.query("group");if(e===void 0)return r.json({code:400,message:"group is required",data:null});let t=parseInt(e,10),i=await ne(r.env);i.settings=(i.settings||[]).filter(o=>o.group!==t);let n=pc.settings.filter(o=>o.group===t),s=new Set(n.map(o=>o.key));return i.settings=i.settings.filter(o=>!s.has(o.key)),i.settings.push(...JSON.parse(JSON.stringify(n))),await fe(i,r.env),r.json({code:200,message:"success",data:n})});Ue.post("/setting/delete",async r=>{let e=r.req.query("key");if(!e)return r.json({code:400,message:"key is required",data:null});let t=await ne(r.env);return t.settings=(t.settings||[]).filter(i=>i.key!==e),await fe(t,r.env),r.json({code:200,message:"success",data:null})});var Bo=async(r,e,t=14)=>{let i=await ne(r);i.settings||(i.settings=[]);for(let[n,s]of Object.entries(e)){if(s===void 0)continue;let o=i.settings.findIndex(a=>a.key===n);o!==-1?i.settings[o].value=s:i.settings.push({key:n,value:s,type:"string",help:n,group:t,flag:0})}await fe(i,r)};Ue.post("/setting/set_115",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{"115_temp_dir":e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_115_open",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{"115_open_temp_dir":e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_123_pan",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{"123_pan_temp_dir":e.temp_dir||"","123_temp_dir":e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_123_open",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{"123_open_temp_dir":e.temp_dir||"","123_open_callback_url":e.callback_url||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_pikpak",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{pikpak_temp_dir:e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_thunder",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{thunder_temp_dir:e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_thunder_browser",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{thunder_browser_temp_dir:e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/set_thunderx",async r=>{let e=await r.req.json().catch(()=>({}));return await Bo(r.env,{thunderx_temp_dir:e.temp_dir||""}),r.json({code:200,message:"success",data:"success"})});Ue.post("/setting/reset_token",async r=>{let e=Rce(32);await Bo(r.env,{token:e});let{resetJwtSecretCache:t}=await Promise.resolve().then(()=>(Fi(),pw));return t(),r.json({code:200,message:"success",data:e})});Ue.get("/meta/list",async r=>{let t=(await ne(r.env)).metas||[];return r.json({code:200,message:"success",data:{content:t,total:t.length}})});Ue.get("/meta/get",async r=>{let e=parseInt(r.req.query("id")||"0",10),i=((await ne(r.env)).metas||[]).find(n=>n.id===e);return i?r.json({code:200,message:"success",data:i}):r.json({code:404,message:"meta not found",data:null})});Ue.post("/meta/create",async r=>{let e=await r.req.json().catch(()=>({})),t=await ne(r.env);t.metas||(t.metas=[]);let i="/"+String(e.path||"").split("/").filter(Boolean).join("/");if(!i||i==="/")return r.json({code:400,message:"path is required",data:null});if(t.metas.some(o=>o.path===i))return r.json({code:400,message:"meta already exists",data:null});let n=String(e.hide||"");if(n){let o=e2(n);if(o)return r.json({code:400,message:`invalid hide regex: ${o}`,data:null},400)}let s={id:t.metas.length?Math.max(...t.metas.map(o=>o.id))+1:1,path:i,password:e.password||"",read_users:e.read_users||[],read_users_sub:!!e.read_users_sub,write_users:e.write_users||[],write_users_sub:!!e.write_users_sub,p_sub:!!e.p_sub,write:!!e.write,w_sub:!!e.w_sub,hide:n,h_sub:!!e.h_sub,readme:e.readme||"",r_sub:!!e.r_sub,header:e.header||"",header_sub:!!e.header_sub};return t.metas.push(s),await fe(t,r.env),r.json({code:200,message:"success",data:s})});Ue.post("/meta/update",async r=>{let e=await r.req.json().catch(()=>({})),t=await ne(r.env);t.metas||(t.metas=[]);let i=t.metas.findIndex(o=>o.id===e.id);if(i===-1)return r.json({code:404,message:"meta not found",data:null});let n=e.path!==void 0?"/"+String(e.path).split("/").filter(Boolean).join("/"):t.metas[i].path;if(n&&t.metas.some(o=>o.path===n&&o.id!==e.id))return r.json({code:400,message:"meta already exists",data:null});let s=e.hide!==void 0?String(e.hide):t.metas[i].hide;if(s){let o=e2(s);if(o)return r.json({code:400,message:`invalid hide regex: ${o}`,data:null},400)}return t.metas[i]={...t.metas[i],...n?{path:n}:{},password:e.password!==void 0?e.password:t.metas[i].password,read_users:e.read_users!==void 0?e.read_users:t.metas[i].read_users,read_users_sub:e.read_users_sub!==void 0?!!e.read_users_sub:t.metas[i].read_users_sub,write_users:e.write_users!==void 0?e.write_users:t.metas[i].write_users,write_users_sub:e.write_users_sub!==void 0?!!e.write_users_sub:t.metas[i].write_users_sub,p_sub:e.p_sub!==void 0?!!e.p_sub:t.metas[i].p_sub,write:e.write!==void 0?!!e.write:t.metas[i].write,w_sub:e.w_sub!==void 0?!!e.w_sub:t.metas[i].w_sub,hide:e.hide!==void 0?e.hide:t.metas[i].hide,h_sub:e.h_sub!==void 0?!!e.h_sub:t.metas[i].h_sub,readme:e.readme!==void 0?e.readme:t.metas[i].readme,r_sub:e.r_sub!==void 0?!!e.r_sub:t.metas[i].r_sub,header:e.header!==void 0?e.header:t.metas[i].header,header_sub:e.header_sub!==void 0?!!e.header_sub:t.metas[i].header_sub},await fe(t,r.env),r.json({code:200,message:"success",data:null})});Ue.post("/meta/delete",async r=>{let e=parseInt(r.req.query("id")||"0",10),t=await ne(r.env);return t.metas||(t.metas=[]),t.metas=t.metas.filter(i=>i.id!==e),await fe(t,r.env),r.json({code:200,message:"success",data:null})});Ue.route("/user",On);Ue.get("/kv/status",async r=>{let[e,t]=await Promise.all([Ul(r.env),$n(r.env)]);return r.json({code:200,message:"success",data:{...e,store:t}})});function f0(r){let e=r.search_index||{};return{obj_count:e.total??0,is_done:e.is_done!==!1,last_done_time:e.updated_at||null,error:e.error||""}}Ue.post("/index/build",async r=>{try{let e=await ne(r.env),t=await iu({parent:"/",keywords:"",max_depth:20,max_results:2e3},r.env);return e.search_index={items:t.content,total:t.total,is_done:!0,updated_at:new Date().toISOString(),error:""},await fe(e,r.env),r.json({code:200,message:"success",data:f0(e)})}catch(e){return r.json({code:500,message:Ne(e),data:null},500)}});Ue.post("/index/update",async r=>{try{let e=await r.req.json().catch(()=>({})),t=Array.isArray(e.paths)?e.paths:["/"],i=Math.min(20,Math.max(0,Number(e.max_depth)||20)),n=await ne(r.env),o=(n.search_index?.items||[]).filter(c=>!t.some(l=>(c.parent||"/").startsWith(l))),a=await iu({parent:"/",keywords:"",max_depth:i,max_results:2e3},r.env);return n.search_index={items:[...o,...a.content],total:o.length+a.total,is_done:!0,updated_at:new Date().toISOString(),error:""},await fe(n,r.env),r.json({code:200,message:"success",data:f0(n)})}catch(e){return r.json({code:500,message:Ne(e),data:null},500)}});Ue.post("/index/stop",async r=>{let e=await ne(r.env);return e.search_index&&(e.search_index.is_done=!0),await fe(e,r.env),r.json({code:200,message:"success",data:f0(e)})});Ue.post("/index/clear",async r=>{let e=await ne(r.env);return e.search_index={items:[],total:0,is_done:!0,updated_at:new Date().toISOString(),error:""},await fe(e,r.env),r.json({code:200,message:"success",data:f0(e)})});Ue.get("/index/progress",async r=>{let e=await ne(r.env);return r.json({code:200,message:"success",data:f0(e)})});Ue.post("/scan/start",async r=>{try{let e=await ne(r.env),t=await iu({parent:"/",keywords:"",max_depth:20,max_results:2e3},r.env);return e.scan_result={items:t.content,total:t.total,is_done:!0,updated_at:new Date().toISOString(),error:""},await fe(e,r.env),r.json({code:200,message:"success",data:{obj_count:t.total,is_done:!0,last_done_time:e.scan_result.updated_at,error:""}})}catch(e){return r.json({code:500,message:Ne(e),data:null},500)}});Ue.post("/scan/stop",async r=>r.json({code:200,message:"success",data:{obj_count:0,is_done:!0,last_done_time:null,error:""}}));Ue.get("/scan/progress",async r=>{let t=(await ne(r.env)).scan_result||{};return r.json({code:200,message:"success",data:{obj_count:t.total??0,is_done:t.is_done!==!1,last_done_time:t.updated_at||null,error:t.error||""}})});Ue.get("/plugin/list",async r=>{let e=await ne(r.env);return e.plugins||(e.plugins=[]),r.json({code:200,message:"success",data:{content:e.plugins,total:e.plugins.length}})});Ue.get("/plugin/get",async r=>{let e=r.req.query("id");if(!e)return r.json({code:400,message:"id is required",data:null});let t=await ne(r.env);t.plugins||(t.plugins=[]);let i=t.plugins.find(n=>n.id===e);return i?r.json({code:200,message:"success",data:i}):r.json({code:404,message:"Plugin not found",data:null})});Ue.post("/plugin/install",async r=>{try{let e=await r.req.json(),t=e;if(e.manifest_url&&typeof e.manifest_url=="string")try{let a=await fetch(e.manifest_url);if(!a.ok)return r.json({code:400,message:`Failed to fetch plugin manifest from URL: HTTP ${a.status}`,data:null});t={...await a.json(),...e}}catch(a){return r.json({code:400,message:`Network error fetching plugin manifest: ${Ne(a,"unexpected network error")}`,data:null})}if(!t.id||!t.name)return r.json({code:400,message:"Plugin id and name are required",data:null});let i=await ne(r.env);i.plugins||(i.plugins=[]);let n=i.plugins.findIndex(a=>a.id===t.id),s=new Date().toISOString(),o={id:t.id,name:t.name,version:t.version||"1.0.0",description:t.description||"",author:t.author||"Unknown",homepage:t.homepage||"",repository:t.repository||"",icon:t.icon||"",type:t.type||"ui",enabled:t.enabled!==void 0?!!t.enabled:!0,high_privilege:!!t.high_privilege,permissions:Array.isArray(t.permissions)?t.permissions:[],entry_url:t.entry_url||"",script_content:t.script_content||"",style_content:t.style_content||"",config_schema:t.config_schema||[],config_values:t.config_values||t.default_config||{},target_hooks:t.target_hooks||["global"],is_builtin:!!t.is_builtin,tags:t.tags||[],created_at:n>=0?i.plugins[n].created_at:s,updated_at:s};return n>=0?i.plugins[n]=o:i.plugins.push(o),await fe(i,r.env),r.json({code:200,message:"Plugin installed successfully",data:o})}catch(e){return r.json({code:500,message:e.message||"Failed to install plugin",data:null})}});Ue.post("/plugin/update",async r=>{try{let e=await r.req.json();if(!e.id)return r.json({code:400,message:"Plugin id is required",data:null});let t=await ne(r.env);t.plugins||(t.plugins=[]);let i=t.plugins.findIndex(o=>o.id===e.id);if(i===-1)return r.json({code:404,message:"Plugin not found",data:null});let n=t.plugins[i],s={...n,...e,id:n.id,updated_at:new Date().toISOString()};return t.plugins[i]=s,await fe(t,r.env),r.json({code:200,message:"Plugin updated successfully",data:s})}catch(e){return r.json({code:500,message:e.message||"Failed to update plugin",data:null})}});Ue.post("/plugin/toggle",async r=>{try{let e=await r.req.json();if(!e.id)return r.json({code:400,message:"Plugin id is required",data:null});let t=await ne(r.env);t.plugins||(t.plugins=[]);let i=t.plugins.findIndex(s=>s.id===e.id);if(i===-1)return r.json({code:404,message:"Plugin not found",data:null});let n=e.enabled!==void 0?!!e.enabled:!t.plugins[i].enabled;return t.plugins[i].enabled=n,t.plugins[i].updated_at=new Date().toISOString(),await fe(t,r.env),r.json({code:200,message:n?"Plugin enabled":"Plugin disabled",data:{id:e.id,enabled:n}})}catch(e){return r.json({code:500,message:e.message||"Failed to toggle plugin",data:null})}});Ue.post("/plugin/delete",async r=>{try{let t=r.req.query("id");if(!t)try{t=(await r.req.json()).id}catch{}if(!t)return r.json({code:400,message:"Plugin id is required",data:null});let i=await ne(r.env);i.plugins||(i.plugins=[]);let n=i.plugins.length;return i.plugins=i.plugins.filter(s=>s.id!==t),i.plugins.length===n?r.json({code:404,message:"Plugin not found",data:null}):(await fe(i,r.env),r.json({code:200,message:"Plugin deleted successfully",data:null}))}catch(e){return r.json({code:500,message:e.message||"Failed to delete plugin",data:null})}});Ue.post("/plugin/batch_save",async r=>{try{let e=await r.req.json(),t=Array.isArray(e)?e:e.plugins;if(!Array.isArray(t))return r.json({code:400,message:"plugins array is required",data:null});let i=await ne(r.env);return i.plugins=t,await fe(i,r.env),r.json({code:200,message:"Plugins saved successfully",data:{count:t.length}})}catch(e){return r.json({code:500,message:e.message||"Failed to batch save plugins",data:null})}});var xM=[];Ue.post("/message/send",async r=>{let t=(await r.req.json().catch(()=>({})))?.message;return t==null||t===""?r.json({code:400,message:"message is required",data:null},400):(xM.push(String(t)),r.json({code:200,message:"success",data:null}))});Ue.post("/message/get",r=>{let e=xM.shift();return e===void 0?r.json({code:404,message:"no message",data:null},404):r.json({code:200,message:"success",data:e})});Ue.get("/audit/logs",async r=>{try{let{queryAuditLogs:e}=await Promise.resolve().then(()=>(LA(),MA)),t=r.req.query("username"),i=r.req.query("action"),n=r.req.query("method"),s=r.req.query("status"),o=r.req.query("start_date"),a=r.req.query("end_date"),c=parseInt(r.req.query("limit")||"100",10),l=e({username:t,action:i,method:n,status:s,startDate:o,endDate:a,limit:Math.min(c,1e3)});return r.json({code:200,message:"success",data:{content:l,total:l.length}})}catch(e){return r.json({code:500,message:e.message||"Failed to query audit logs",data:null},500)}});Ue.get("/audit/stats",async r=>{try{let{getAuditStats:e}=await Promise.resolve().then(()=>(LA(),MA)),t=e();return r.json({code:200,message:"success",data:t})}catch(e){return r.json({code:500,message:e.message||"Failed to get audit stats",data:null},500)}});Ue.post("/audit/cleanup",async r=>{try{let e=await r.req.json().catch(()=>({})),t=parseInt(e.retention_days||"30",10),{cleanupExpiredLogs:i}=await Promise.resolve().then(()=>(LA(),MA));return i(t),r.json({code:200,message:`Cleaned up logs older than ${t} days`,data:null})}catch(e){return r.json({code:500,message:e.message||"Failed to cleanup logs",data:null},500)}});Cr();Ye();Ye();function A0(r,e){let t=r.replace(/bytes=/,"").split("-"),i=parseInt(t[0],10),n=t[1]?parseInt(t[1],10):e-1,s=n-i+1;return{start:i,end:n,chunksize:s}}Yt();Ma();Nl();xc();ef();Zu();ef();var qce="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";function CM(r){let e=Oce(r.ignoreHeaders),t={...r.rawUrlHeaders||{}};return!t["User-Agent"]&&!e.has("user-agent")&&(t["User-Agent"]=qce),r.proxyRange&&r.rangeHeader&&!e.has("range")&&(t.Range=r.rangeHeader),t}function Oce(r){let e=Array.isArray(r)?r.join(","):r;return new Set(String(e??"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean))}function EM(r,e){return r.Range?e.status===412||e.status===200&&!e.headers.get("content-range"):!1}var Mce={pdf:"application/pdf",mp4:"video/mp4",webm:"video/webm",mkv:"video/x-matroska",mp3:"audio/mpeg",flac:"audio/flac",m3u8:"application/vnd.apple.mpegurl",ts:"video/mp2t",png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml"};function BM(r){let e=r.split(".").pop()?.toLowerCase()||"";return Mce[e]||"application/octet-stream"}function bM(r){return r.replace(/[\r\n\u0000-\u001f]+/g,"")}var Lce=6*1024*1024,Hce=(r,e)=>{let i=(r?.env||{})[e];if(i!=null)return String(i);if(typeof process<"u"&&process.env){let n=process.env[e];if(n!=null)return String(n)}return""};function Kce(r){let e=typeof globalThis<"u"?globalThis:{},t=r?.env||{},i=typeof process<"u"?process.env||{}:{};return!!(t.EDGEONE||t.EO_REGION||t.EDGEONE_BLOB||e.EDGEONE_BLOB||typeof e.EdgeOne<"u"||t.TENCENTCLOUD_SCF_FUNCTIONNAME||i.TENCENTCLOUD_SCF_FUNCTIONNAME)}function Bs(r){let e=Hce(r,"RAW_PROXY_MAX_BYTES").trim();if(e!==""){let t=parseInt(e,10);if(Number.isFinite(t)&&t>=0)return t}return Kce(r)?Lce:0}function lx(r,e,t){if(!Number.isFinite(t)||t<=0||!Number.isFinite(r)||r<=0)return!1;if(e)try{return A0(e,r).chunksize>t}catch{return!1}return r>t}var Gce=new Set(["authorization","cookie"]);function zce(r){return r?Object.keys(r).some(e=>Gce.has(e.trim().toLowerCase())):!1}function vM(r,e,t){return Ig(t,r)||zce(e)}function kM(r){if(!r)return 0;let e=parseInt(r.get("content-length")||"",10);if(Number.isFinite(e)&&e>0)return e;let t=r.get("content-range")||"",i=/bytes\s+(\d+)-(\d+)\/(\d+|\*)/i.exec(t);if(i){let n=parseInt(i[1],10),s=parseInt(i[2],10);if(Number.isFinite(n)&&Number.isFinite(s)&&s>=n)return s-n+1}return 0}function Lv(r){return lx(r.size,r.range,r.payloadLimit)?r.authBound?"too-large":"redirect":"proxy"}var ux=null,dx=null;async function Jce(){if(typeof process<"u"&&process.release?.name==="node"&&!ux)try{ux=await import("fs/promises"),dx=(await import("fs")).createReadStream}catch{}}var bs=new Xe,jce=r=>{try{let e=r.executionCtx;return!e||typeof e.waitUntil!="function"?void 0:{waitUntil:t=>e.waitUntil(t)}}catch{return}},$ce=new Set(["range","user-agent","accept","accept-language","referer"]);async function SM(r,e,t){let n=r,s=e;for(let o=0;o<5;o++){try{jr(n,"Proxy download",t)}catch(l){throw new Error(l?.message||"SSRF blocked: restricted destination")}let a=await fetch(n,{headers:s,redirect:"manual"}),c=a.headers.get("location");if(a.status>=300&&a.status<400&&c){n=new URL(c,n).toString();let l={};for(let[d,u]of Object.entries(s))$ce.has(d.toLowerCase())&&u&&(l[d]=u);s=l;continue}return a}throw new Error("Proxy download blocked: too many redirects")}function fx(r,e,t){let i=Bs(r);return`\u6587\u4EF6\u8FC7\u5927\uFF0C\u5F53\u524D\u90E8\u7F72\u5E73\u53F0\u65E0\u6CD5\u4EE3\u7406\u4E0B\u8F7D\uFF08${t} \u5B57\u8282 > \u4E0A\u9650 ${i} \u5B57\u8282 \u2248 ${(s=>Math.floor(s/1024/1024))(i)} MiB\uFF09\u3002\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u5BF9\u51FD\u6570\u5355\u6B21\u8BF7\u6C42/\u54CD\u5E94 body \u6709\u786C\u4E0A\u9650\uFF08EdgeOne \u4E91\u51FD\u6570\u4E3A 6 MiB\uFF0C\u9519\u8BEF\u7801 CLOUD_FUNCTION_PAYLOAD_TOO_LARGE / HTTP 413\uFF09\uFF0C${e} \u65E0\u6CD5\u63D0\u4F9B\u53EF\u76F4\u8FDE\u7684\u4E0B\u8F7D\u94FE\u63A5\uFF08\u6216\u76F4\u94FE\u5FC5\u987B\u643A\u5E26\u79C1\u6709\u9274\u6743\u5934\uFF09\uFF0C\u53EA\u80FD\u7ECF\u670D\u52A1\u7AEF\u8F6C\u53D1\uFF0C\u56E0\u6B64\u65E0\u6CD5\u7ED5\u8FC7\u8BE5\u4E0A\u9650\u3002\u5EFA\u8BAE\uFF1A\u6539\u7528\u8FD4\u56DE\u516C\u5F00\u76F4\u94FE\u7684\u5B58\u50A8\u3001\u6216\u5728\u81EA\u6258\u7BA1\u73AF\u5883\uFF08Docker / Node\uFF09\u90E8\u7F72\uFF1B\u786E\u9700\u653E\u5F00\u9650\u5236\u53EF\u8BBE\u7F6E RAW_PROXY_MAX_BYTES=0\uFF08\u6216\u6309\u5E73\u53F0\u5B9E\u9645\u4E0A\u9650\u8C03\u6574\u8BE5\u503C\uFF09\u3002`}async function DM(r,e,t,i,n={}){let s=n.proxyRange??!1,o=n.driver??"",a=vM(o,e.raw_url_headers,n.storage),c=Lv({size:Number(e.size)||0,range:s?r.req.header("Range"):void 0,payloadLimit:Bs(r),authBound:a});if(c==="redirect"){try{jr(e.raw_url,"Redirect download",i)}catch(B){return r.text(B.message||"SSRF blocked",403)}return console.warn(`[rawRouter] Falling back to 302 direct link for '${t}': ${e.size} bytes exceeds the ${Bs(r)}-byte payload limit of this runtime \u2014 native proxy would hit CLOUD_FUNCTION_PAYLOAD_TOO_LARGE`),r.redirect(e.raw_url,302)}if(c==="too-large")return console.warn(`[rawRouter] Refusing to proxy '${t}': ${e.size} bytes exceeds the ${Bs(r)}-byte payload limit of this runtime and no direct link can be handed to the browser (driver=${o}, authBound=${a})`),r.json({code:413,message:fx(r,o||"\u8BE5\u5B58\u50A8",Number(e.size)||0),data:null},413);let l=CM({rawUrlHeaders:e.raw_url_headers,rangeHeader:r.req.header("Range"),proxyRange:s,ignoreHeaders:n.ignoreHeaders}),d;try{d=await SM(e.raw_url,l,i)}catch(B){return r.text(B.message||"SSRF blocked",403)}EM(l,d)&&(console.warn(`[rawRouter] Upstream ignored/refused Range (status=${d.status}) for '${t}', retrying without Range header...`),delete l.Range,d=await SM(e.raw_url,l,i));let u=kM(d.headers),f=Lv({size:u,payloadLimit:Bs(r),authBound:a});if(f!=="proxy"){try{await d.body?.cancel()}catch{}if(console.warn(`[rawRouter] Upstream body is ${u} bytes after Range negotiation (status=${d.status}) for '${t}', which exceeds the ${Bs(r)}-byte payload limit of this runtime \u2014 refusing to stream it (decision=${f}).`),f==="redirect"){try{jr(e.raw_url,"Redirect download",i)}catch(B){return r.text(B.message||"SSRF blocked",403)}return r.redirect(e.raw_url,302)}return r.json({code:413,message:fx(r,o||"\u8BE5\u5B58\u50A8",u),data:null},413)}r.header("Access-Control-Allow-Origin","*"),r.header("Access-Control-Allow-Methods","GET, OPTIONS, HEAD"),r.header("Access-Control-Expose-Headers","Content-Range, Accept-Ranges, Content-Length, Content-Disposition"),r.header("Content-Type",d.headers.get("content-type")||BM(t));let h=d.headers.get("content-length");h&&r.header("Content-Length",h);let g=d.headers.get("content-range");g&&r.header("Content-Range",g),r.header("Accept-Ranges",d.headers.get("accept-ranges")||"bytes");let p=d.headers.get("etag");p&&r.header("ETag",p);let I=d.headers.get("last-modified");I&&r.header("Last-Modified",I);let m=d.headers.get("cache-control");m&&r.header("Cache-Control",m);let w=d.headers.get("content-disposition");return w&&r.header("Content-Disposition",bM(w)),r.body(d.body,d.status)}async function Vce(r,e,t,i){if(!e)return"";let n=Os(t),s=e.includes("$path")?e.replace(/\$path(?!\w)/g,n):e.replace(/\/+$/,"")+n;if(!yQ(i)&&!/[?&]sign=/.test(s))try{let o=await ru(r,t,await tu(r));o&&(s+=(s.includes("?")?"&":"?")+"sign="+o)}catch(o){console.warn(`[rawRouter] failed to sign down_proxy_url for '${t}': ${o?.message||o}`)}return s}bs.get("/*",async r=>{await Jce();let e=r.req.query("proxy")==="true"||r.req.path.startsWith("/p")||r.req.path.startsWith("/api/p")||r.req.path.startsWith("/sd")||r.req.path.startsWith("/api/sd"),t=r.req.path.startsWith("/p")||r.req.path.startsWith("/api/p"),i=r.req.path.replace(/^\/(?:api\/)?(?:raw|sd|d|p)(?=\/|$)/,""),n;try{n=decodeURIComponent(i)}catch{return r.text("Bad Request: malformed path encoding",400)}try{let s=n;if(r.req.path.startsWith("/api/sd")||r.req.path.startsWith("/sd")){let u=(r.req.header("Cookie")||"").split(";").map(p=>p.trim()).find(p=>p.startsWith("browser-password="))?.split("=").slice(1).join("=")||"",f;try{f=u?decodeURIComponent(u):""}catch{f=u}let h=r.req.query("pwd")||f,g=await Yd(s,h,r.env);if(!g.ok)return r.text(g.error||"Share not found",404);if(g.virtualList||!g.realPath)return r.text("Cannot download share root",400);s=g.realPath}else if(await F9(r,s)){let d=r.req.query("sign")||"";if(!await P9(r,s,d))return r.text("sign verify failed",401)}let a=await ct(s);if(a.isVirtual||!a.physical)return r.text("Cannot download virtual directory path",400);if(a.storage){let d=(a.storage.driver||"").toLowerCase().replace(/[^a-z0-9]/g,""),u=await Vn().catch(()=>({})),f=Ic(u.proxy_types),h=Ic(u.text_types),g={proxyRange:cQ(a.storage),driver:a.storage.driver,storage:a.storage,ignoreHeaders:u.proxy_ignore_headers};if(t&&!wg({storage:a.storage,driver:d,filename:s,proxyTypes:f,textTypes:h}))return console.warn(`[rawRouter] proxy not allowed for '${s}' (storage=${a.storage.id}, driver=${a.storage.driver})`),r.text("proxy not allowed",403);if(d!=="local")try{let p=Eg(a.storage.addition,r.env),I=await lt(a.storage.driver,a.storage),m;try{m=await I.get(s,a.physical)}finally{await Jt(a.storage.driver,a.storage,I,jce(r))}if(m&&m.raw_url){let w=wQ(a.storage,d,e,{filename:s,proxyTypes:f});if(w.mode==="use_proxy_url"){let B=mQ(a.storage);if(B){let C=await Vce(r,B,s,a.storage);if(C){try{jr(C,"Redirect download",p)}catch(x){return r.text(x.message||"SSRF blocked",403)}return console.log(`[rawRouter] Redirecting download for '${s}' to configured proxy url via ${a.storage.driver}`),r.redirect(C,302)}}return console.warn(`[rawRouter] webdav_policy=use_proxy_url but down_proxy_url is empty (storage=${a.storage.id}); falling back to native proxy`),DM(r,m,s,p,g)}if(w.needsProxy)return DM(r,m,s,p,g);try{jr(m.raw_url,"Redirect download",p)}catch(B){return r.text(B.message||"SSRF blocked",403)}return console.log(`[rawRouter] Redirecting download for '${s}' via ${a.storage.driver}`),r.redirect(m.raw_url,302)}else if(typeof I.createReadStream=="function"&&m&&!m.is_dir){let w=Number(m.size)||0;if(lx(w,r.req.header("Range"),Bs(r)))return console.warn(`[rawRouter] Refusing to stream '${s}': ${w} bytes exceeds the ${Bs(r)}-byte payload limit of this runtime.`),r.json({code:413,message:fx(r,a.storage.driver,w),data:null},413);r.header("Access-Control-Allow-Origin","*");let B=m.size||0,C=r.req.header("Range");if(C&&B>0){let{start:x,end:Q,chunksize:R}=A0(C,B),U=await I.createReadStream(a.physical,{start:x,end:Q});return r.header("Content-Range",`bytes ${x}-${Q}/${B}`),r.header("Accept-Ranges","bytes"),r.header("Content-Length",R.toString()),r.header("Content-Type","application/octet-stream"),r.body(U,206)}else{B>0&&r.header("Content-Length",B.toString()),r.header("Accept-Ranges","bytes"),r.header("Content-Type","application/octet-stream");let x=await I.createReadStream(a.physical);return r.body(x)}}else{let w=m?.raw_url_error||(m?.is_dir?"\u8BE5\u6761\u76EE\u662F\u6587\u4EF6\u5939\uFF0C\u4E0D\u53EF\u4F5C\u4E3A\u6587\u4EF6\u4E0B\u8F7D\u3002":"\u8BE5\u5B58\u50A8\u9A71\u52A8\u672A\u8FD4\u56DE\u4E0B\u8F7D\u94FE\u63A5\uFF08raw_url \u4E3A\u7A7A\uFF09\u3002");return r.text(`File not found or no download link available: ${s} -${w}`,404)}}catch(p){return console.error(`[rawRouter] Driver get failed for '${s}':`,p.message),r.text(`Download failed: ${Ne(p)}`,500)}}if(!ux||!dx)return r.text("Local file streaming not supported in Edge Runtime",500);let c=await ux.stat(a.physical);if(c.isDirectory())return r.text("Cannot download directory",400);if(lx(Number(c.size)||0,r.req.header("Range"),Bs(r)))return r.json({code:413,message:fx(r,"local",Number(c.size)||0),data:null},413);r.header("Access-Control-Allow-Origin","*");let l=r.req.header("Range");if(l){let{start:d,end:u,chunksize:f}=A0(l,c.size),h=dx(a.physical,{start:d,end:u});return r.header("Content-Range",`bytes ${d}-${u}/${c.size}`),r.header("Accept-Ranges","bytes"),r.header("Content-Length",f.toString()),r.header("Content-Type","application/octet-stream"),r.body(h,206)}else{r.header("Content-Length",c.size.toString()),r.header("Accept-Ranges","bytes");let d=dx(a.physical);return r.body(d)}}catch(s){return console.error(`[rawRouter] Download 404 for '${n}':`,s.message),r.text(`Not found: ${Ne(s,"file not found")}`,404)}});Cr();Ye();gc();d0();function QM(r){let e=String(r).trim();e=e.replace(/(\w+:\/\/)[^/@\s]+@/g,"$1***@"),e=e.replace(/\b(token|secret|password|passwd|pwd|api[_-]?key)\s*[=:]\s*\S+/gi,"$1=***");let t=160;return e.length>t?e.slice(0,t)+"\u2026":e}var Yce=new Set(["INVALID_COMBINATION","DRIVER_UNAVAILABLE","UNKNOWN_DRIVER","NO_STORAGE","PROXY_CONFIG","DRIVER_ERROR"]);function h0(r){return r&&Yce.has(r)?3:1}function Bu(r){if(r==null)return null;let e=String(r).split(` -`).map(t=>t.trim()).find(t=>t.length>0);return e?QM(e):null}function ml(r,e=1){if(r==null)return"unknown error";let t=String(r).split(` -`).map(i=>i.trim()).filter(Boolean).slice(0,Math.max(1,e)).map(QM);return t.length?t.join(" "):"unknown error"}function _M(r){return{reason:r.message?ml(r.message,h0(r.code)):null,suggestion:r.suggestion}}var bo=new Xe,TM="https://doc.oplist.org",g0=`${TM}/ecosystem/official_worker/guide_env`,FM=`${TM}/ecosystem/official_worker/guide`;function PM(r){let e=String(r??"").trim();return e&&e!=="none"?e:null}function Hv(){return"No storage binding was detected (auto probes every driver). Bind one and redeploy \u2014 Cloudflare: D1 or KV namespace; EdgeOne: Blob; ESA: ESA_BLOB."}bo.get("/env_check",async r=>{let e=r.env,t=tg(e),i=rg(e),n=Ac(e),s=jn(e),o=await $n(e).catch(m=>({driver:"none",format:"none",available:!1,configError:String(m?.message||m)})),a=await ng(e),c=String(o?.driver??"none"),l=c==="memory",d=c!=="none"&&c!=="",u=!!o?.configError,f=o?.available!==!1,h=await yg(e).catch(()=>!1),g=[],p={code:null,summary:null,message:null,suggestion:null};if(u){let m=await hc(e,{silent:!0});p={code:m.code,summary:Bu(m.message),message:m.message,suggestion:m.suggestion}}if(u&&p.code==="NO_STORAGE")g.push({code:"STORAGE_UNAVAILABLE",level:"error",summary:"No storage backend available.",message:"No storage backend available.",docUrl:g0,suggestion:p.suggestion||Hv()});else if(u){let m=p.code==="INVALID_COMBINATION",w=ml(p.message||o?.configError,h0(p.code));g.push({code:m?"STORAGE_INVALID_COMBINATION":"STORAGE_CONFIG_ERROR",level:"error",summary:p.summary||(m?"Unsupported storage combination.":"Storage driver is not configured correctly."),message:m?`Unsupported storage combination: ${w}`:`Storage driver is not configured correctly: ${w}`,docUrl:FM,suggestion:p.suggestion})}else l?g.push({code:"STORAGE_MEMORY_ONLY",level:s?"error":"warning",summary:s?"In-memory storage only; data will be lost immediately.":"In-memory storage only; data will be lost on restart (fine for local dev).",message:s?"In-memory storage only; data will be lost immediately.":"In-memory storage only; data will be lost on restart (fine for local dev).",docUrl:g0,suggestion:Hv()}):d||g.push({code:"STORAGE_UNAVAILABLE",level:"error",summary:"No storage backend available.",message:"No storage backend available.",docUrl:g0,suggestion:Hv()});if(d&&!l&&!u&&!f){let m=`Storage driver "${c}" is configured but not reachable${o.error?": "+ml(o.error):""}.`;g.push({code:"STORAGE_UNHEALTHY",level:"error",summary:m,message:m,docUrl:FM,suggestion:`Check the credentials/bindings for "${c}" (it resolved, but its health check failed).`})}h||g.push({code:"JWT_SECRET_MISSING",level:"warning",summary:"JWT_SECRET is not set.",message:"JWT_SECRET is not set. If storage is available, a secret is generated and persisted automatically during setup; setting it explicitly is recommended so that all instances and cold starts share one key.",docUrl:g0,suggestion:"Set JWT_SECRET (32+ chars recommended, `openssl rand -hex 32`) in your deployment variables \u2014 or leave it empty and let setup generate one (requires working storage)."});let I=a;return r.json({code:200,message:"success",data:{runtime:{serverless:s,platform:o?.platform??null},config:{db_format:i,db_driver:t,db_cipher:n,resolved_driver:PM(o?.driver),resolved_format:PM(o?.format)},storage:{available:a,configured:o?.configured??null,connected:o?.connected??null,platform:o?.platform??null,memory:l,error_code:p.code,summary:p.summary,error_message:p.message!==null?ml(p.message,h0(p.code)):null,suggestion:p.suggestion},jwt:{ready:h,source:h?"env-or-persisted":"none"},ready:I,issues:g,docUrl:g0}})});bo.get("/settings",async r=>{let e=await ne(r.env),t={title:"OpenList",site_title:"OpenList",version:"v4.2.3",backend:"ts-worker",announcement:"",pagination_type:"pagination",default_page_size:"20",allow_indexed:"false",allow_mounted:"true",robots_txt:`User-agent: * -Allow: /`,logo:"https://res.oplist.org/logo/logo.svg",favicon:"https://res.oplist.org/logo/logo.svg",main_color:"#1890ff",hide_storage_details:"false",hide_storage_details_in_manage_page:"false",customize_head:"",customize_body:"",text_types:"txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,yaml,toml,Makefile,mk,dockerfile,sh,pub,lock,gradle,ts,tsx,jsx,go,rs,c,cpp,h,cs,rb,swift,kt,dart,r,m,pl,pm,lua,ex,exs",audio_types:"mp3,flac,ogg,m4a,wav,opus,wma,aac,aiff,ape",video_types:"mp4,mkv,avi,mov,rmvb,webm,flv,m3u8,ts,wmv,m2ts,mpg,mpeg,3gp",image_types:"jpg,tiff,jpeg,png,gif,bmp,svg,ico,webp,avif,heic,heif,raw,cr2,nef,arw,dng",proxy_types:"",proxy_ignore_headers:"",audio_autoplay:"false",video_autoplay:"false",readme_autorender:"true",filter_readme_scripts:"true",preview_download_by_default:"false",preview_archives_by_default:"false",share_preview_download_by_default:"false",share_preview_archives_by_default:"false",share_preview:"true",share_archive_preview:"true",hide_files:"/\\.DS_Store/i",link_expiration:"0",sign_all:"false",filename_char_mapping:"{}",forward_direct_link_params:"false",ignore_direct_link_params:"",package_download:"true",offline_download:"true",ocr_api:"",privacy_regs:"",iframe_previews:"{}",external_previews:"{}",check_down_link:"false",check_update:"false",allow_guest:"true",webauthn_login_enabled:"false",sso_login_enabled:"false",sso_compatibility_mode:"false",ldap_login_enabled:"false",show_disk_usage_in_plain_text:"false",non_efs_zip_encoding:"UTF-8"},i=new Set([...Object.keys(t),"audio_cover","home_container","ldap_login_tips","search_index","settings_layout","share_icon","share_summary_content","sso_login_platform"]),n=/(secret|password|passwd|pwd|cookie|token|credential|private[_-]?key|api[_-]?key|access[_-]?key|jwt|salt|signature|webhook)/i;e.settings.forEach(a=>{if(a.key&&a.value!==void 0){if(!i.has(a.key)||n.test(a.key))return;t[a.key]=a.value,a.key==="site_title"&&(t.title=a.value)}});let s=(e.users||[]).find(a=>a.username==="guest");return!!!(s&&!s.disabled)||t.allow_guest==="false"?t.allow_guest="false":t.allow_guest="true",r.json({code:200,message:"success",data:t})});bo.get("/archive_extensions",r=>r.json({code:200,message:"success",data:["zip","rar","7z","tar","gz","bz2","xz","tar.gz","tar.bz2","tar.xz"]}));bo.get("/offline_download_tools",r=>r.json({code:200,message:"success",data:[]}));bo.get("/plugins",async r=>{let i=((await ne(r.env)).plugins||[]).filter(n=>n.enabled);return r.json({code:200,message:"success",data:i})});function Wce(r){let e=r?.ADMIN_PASS,t=typeof process<"u"?process.env?.ADMIN_PASS:"";return String(e||t||"").trim()!==""}bo.get("/init_status",async r=>{let e=await ng(r.env),t=!1;if(e){if(Wce(r.env))try{let{getOrInitUsers:l}=await Promise.resolve().then(()=>(gl(),gM));await l(r.env)}catch(l){console.warn("[DB] init_status: ADMIN_PASS auto-initialization failed: "+(l?.message||l))}let c=((await ne(r.env)).users||[]).find(l=>l.role===2);t=!!(c&&String(c.password||"").trim()!=="")}let i=t?await yg(r.env):!1,n=null,s=null;if(!e){let a=await hc(r.env,{silent:!0});n=Bu(a.message),s=a.suggestion}let o=pg();return r.json({code:200,message:"success",data:{initialized:t,ready:i,db_trusted:UC(),storage_error:n,storage_suggestion:s,db_load_error:o?ml(o,1):null}})});bo.post("/init/setup",async r=>{let e=await r.req.json().catch(()=>({})),t=String(e.username||"admin").trim(),i=String(e.password||"").trim(),n=String(e.site_title||"").trim();if(!t)return r.json({code:400,message:"username is required",data:null},400);if(!i)return r.json({code:400,message:"password is required",data:null},400);if(i.length<4)return r.json({code:400,message:"password must be at least 4 characters",data:null},400);let s=await ne(r.env);s.users||(s.users=[]);let o=pg();if(o){console.error("[DB] init/setup rejected: database could not be loaded from the persistence backend: "+o);let c=await hc(r.env,{silent:!0}),l=c.code||"STORAGE_READ_FAILED";return r.json({code:500,message:"database is not readable; refusing to initialize to avoid overwriting existing config",data:{code:l,summary:Bu(o),reason:ml(o,h0(l)),suggestion:c.suggestion}},500)}let a=s.users.find(c=>c.role===2);if(a&&String(a.password||"").trim()!=="")return r.json({code:400,message:"system has already been initialized",data:null},400);if(await OC(r.env),a)a.username=t,await Ti(a,i);else{let c={id:1,username:t,password:"",role:2,permission:0,base_path:"/",disabled:!1,sso_id:"",allow_ldap:!1,pwd_update_at:new Date().toISOString()};await Ti(c,i),s.users.push(c)}if(n){s.settings||(s.settings=[]);let c=s.settings.find(l=>l.key==="site_title");c?c.value=n:s.settings.push({key:"site_title",value:n,type:"string",help:"Site Title",group:1,flag:0})}return await fe(s,r.env,{force:!0}),r.json({code:200,message:"success",data:null})});Cr();function Xce(){return[{name:"list_files",description:"List files and directories in OpenList storage",inputSchema:{type:"object",properties:{path:{type:"string",description:"Storage mount path"}}}},{name:"get_system_info",description:"Fetch server hardware and storage metrics",inputSchema:{type:"object",properties:{}}}]}function Zce(){return[{uri:"openlist://storage/metrics",name:"Storage Metrics",mimeType:"application/json",description:"Current storage metrics of OpenList"}]}function ele(){return[{name:"summarize_directory",description:"Prompt to summarize contents of a folder",arguments:[{name:"path",description:"The folder path",required:!0}]}]}async function tle(r,e,t,i){try{if(e==="list_files"){let{listItems:n}=await Promise.resolve().then(()=>(Yt(),Mi)),s=t?.path||"/",{content:o,provider:a}=await n(s),c=o.map(l=>({name:l.name,size:l.size,is_dir:l.is_dir,modified:l.modified}));return{jsonrpc:"2.0",result:{content:[{type:"text",text:JSON.stringify({path:s,provider:a,files:c},null,2)}],isError:!1},id:r}}if(e==="get_system_info"){let{getDb:n}=await Promise.resolve().then(()=>(Ye(),Jr)),s=await n(i);return{jsonrpc:"2.0",result:{content:[{type:"text",text:JSON.stringify({storages:s?.storages?.length??0,users:s?.users?.length??0,metas:s?.metas?.length??0,shares:s?.shares?.length??0},null,2)}],isError:!1},id:r}}return{jsonrpc:"2.0",error:{code:-32602,message:`Unknown tool: ${e}`},id:r}}catch(n){return{jsonrpc:"2.0",result:{content:[{type:"text",text:String(n?.message||n)}],isError:!0},id:r}}}async function UM(r,e,t,i){switch(r){case"tools/list":return{jsonrpc:"2.0",result:{tools:Xce()},id:e};case"tools/call":{let n=t?.name,s=t?.arguments||{};return n?tle(e,n,s,i):{jsonrpc:"2.0",error:{code:-32602,message:"Missing tool name"},id:e}}case"resources/list":return{jsonrpc:"2.0",result:{resources:Zce()},id:e};case"prompts/list":return{jsonrpc:"2.0",result:{prompts:ele()},id:e};default:return{jsonrpc:"2.0",error:{code:-32601,message:"Method not found"},id:e}}}Fi();var p0=new Xe;p0.use("*",Pi);p0.get("/sse",r=>(r.header("Content-Type","text/event-stream"),r.header("Cache-Control","no-cache"),r.header("Connection","keep-alive"),r.text(`event: endpoint -data: /api/mcp/messages - -`)));p0.post("/messages",async r=>{let e=await r.req.json().catch(()=>({})),{method:t,id:i,params:n}=e;if(!t)return r.json({jsonrpc:"2.0",error:{code:-32600,message:"Invalid Request"},id:i||null},400);let s=await UM(t,i,n,r.env),o=s.error?404:200;return r.json(s,o)});Cr();Ye();OA();var Kv=new Xe;Kv.get("/info",async r=>{let e=await Zd(r),t=await ne(r.env),i={runtime:"Cloudflare Workers / Edge",timestamp:new Date().toISOString(),store:await $n(r.env)};return e&&(i.db_state={storages_count:t.storages?.length||0,users_count:t.users?.length||0,metas_count:t.metas?.length||0,settings_count:t.settings?.length||0}),r.json({code:200,message:"OpenList debug profile generated",data:i})});Cr();Ye();Fi();var ri=new Xe;function rle(r){let t=(r||"").replace(/\\/g,"/").replace(/\/+/g,"/").split("/").filter(i=>i&&i!==".");if(t.some(i=>i===".."))throw new Error("Path traversal segment '..' is not allowed");return"/"+t.join("/")}ri.use("/list",Pi);ri.use("/get",Pi);ri.use("/update",Pi);ri.use("/delete",Pi);ri.use("/cancel",Pi);ri.use("/enable",Pi);ri.use("/disable",Pi);ri.get("/list",async r=>{let t=(await ne(r.env)).shares||[];return r.json({code:200,message:"success",data:{content:t,total:t.length}})});ri.get("/get",async r=>{let e=r.req.query("id")||"",i=((await ne(r.env)).shares||[]).find(n=>n.id===e);return i?r.json({code:200,message:"success",data:i}):r.json({code:404,message:"share not found",data:null})});ri.post("/create",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);if(!Is(e)&&!Ha(e,La.SHARE))return r.json({code:403,message:"Permission denied",data:null},403);let t=await r.req.json().catch(()=>({})),i=await ne(r.env),n=[];if(Array.isArray(t.files))try{n=t.files.map(c=>{let l=rle(String(c));return Is(e)?l:We(e,l)})}catch(c){return r.json({code:400,message:c.message||"Invalid file path",data:null},400)}let s=t.id?String(t.id).trim():"";if(s&&!/^[a-zA-Z0-9_-]{1,64}$/.test(s))return r.json({code:400,message:"Invalid share id format (only alphanumeric, _ and - allowed, max 64 chars)",data:null},400);let o=s||ile();if((i.shares||[]).some(c=>c.id===o))return r.json({code:400,message:"share id already exists",data:null},400);let a={id:o,new_id:t.new_id||o,creator:t.creator||e.username||"user",creator_role:t.creator_role!==void 0?t.creator_role:e.role??1,accessed:t.accessed??0,expires:t.expires||null,pwd:t.pwd||"",max_accessed:t.max_accessed??0,disabled:t.disabled??!1,order_by:t.order_by||"",order_direction:t.order_direction||"",extract_folder:t.extract_folder||"",files:n,remark:t.remark||"",readme:t.readme||"",header:t.header||""};return i.shares||(i.shares=[]),i.shares.push(a),await fe(i,r.env),r.json({code:200,message:"success",data:a})});function ile(){return crypto.randomUUID().replace(/-/g,"").slice(0,16)}ri.post("/update",async r=>{let e=await r.req.json().catch(()=>({})),t=await ne(r.env);if(!e.id)return r.json({code:400,message:"share id is required",data:null});let i=(t.shares||[]).findIndex(s=>s.id===e.id);if(i===-1)return r.json({code:404,message:"share not found",data:null});let n=e.new_id&&String(e.new_id).trim()!==""?String(e.new_id).trim():e.id;return n!==e.id&&(t.shares||[]).some(o=>o.id===n&&o.id!==e.id)?r.json({code:400,message:"share id already exists",data:null}):(t.shares[i]={...t.shares[i],id:n,new_id:n,expires:e.expires!==void 0?e.expires:t.shares[i].expires,pwd:e.pwd!==void 0?e.pwd:t.shares[i].pwd,max_accessed:e.max_accessed!==void 0?e.max_accessed:t.shares[i].max_accessed,disabled:e.disabled!==void 0?e.disabled:t.shares[i].disabled,order_by:e.order_by!==void 0?e.order_by:t.shares[i].order_by,order_direction:e.order_direction!==void 0?e.order_direction:t.shares[i].order_direction,extract_folder:e.extract_folder!==void 0?e.extract_folder:t.shares[i].extract_folder,files:e.files!==void 0?e.files:t.shares[i].files,remark:e.remark!==void 0?e.remark:t.shares[i].remark,readme:e.readme!==void 0?e.readme:t.shares[i].readme,header:e.header!==void 0?e.header:t.shares[i].header},await fe(t,r.env),r.json({code:200,message:"success",data:null}))});ri.post("/delete",async r=>{let e=r.req.query("id")||"",t=await ne(r.env);return t.shares||(t.shares=[]),t.shares=t.shares.filter(i=>i.id!==e),await fe(t,r.env),r.json({code:200,message:"success",data:null})});ri.post("/enable",async r=>{let e=r.req.query("id")||"",t=await ne(r.env),i=(t.shares||[]).find(n=>n.id===e);return i&&(i.disabled=!1,await fe(t,r.env)),r.json({code:200,message:"success",data:null})});ri.post("/disable",async r=>{let e=r.req.query("id")||"",t=await ne(r.env),i=(t.shares||[]).find(n=>n.id===e);return i&&(i.disabled=!0,await fe(t,r.env)),r.json({code:200,message:"success",data:null})});Cr();Ye();Yt();Fi();var Ui=new Xe;Ui.all("/refresh",async(r,e)=>await Jb(r)?e():Pi(r,e),async r=>{let e=await ne(r.env),t=0,i=0,n=[];for(let s of e.storages||[])if(!s.disabled)try{let o=await lt(s.driver,s);if(await o.init?.(),typeof o.checkCookieValid=="function"&&/lanzou/i.test(String(s.driver))){let a=await o.checkCookieValid();if(!a.valid){s.status="cookie_expired",i++,n.push({id:s.id,mount_path:s.mount_path,driver:s.driver,status:"cookie_expired",error:a.error||"Cookie expired"});continue}}s.status="work",t++,n.push({id:s.id,mount_path:s.mount_path,driver:s.driver,status:"ok"})}catch(o){i++,n.push({id:s.id,mount_path:s.mount_path,driver:s.driver,status:"failed",error:o?.message||String(o)})}return await fe(e,r.env),i>0?r.json({code:500,message:`token refresh failed: ${i}/${t+i} storage(s) failed`,data:{refreshed:t,failed:i,total:e.storages?.length||0,results:n}},500):r.json({code:200,message:"token refresh executed",data:{refreshed:t,failed:i,total:e.storages?.length||0,results:n}})});var yl={upload:[],copy:[],move:[],offline_download:[]};Ui.use("*",Pi);Ui.get("/:type/:state",r=>{let e=r.req.param("type"),t=r.req.param("state"),n=(yl[e]||[]).filter(s=>t==="done"?s.done:!s.done);return r.json({code:200,message:"success",data:n})});Ui.post("/:type/clear_done",r=>{let e=r.req.param("type");return yl[e]&&(yl[e]=yl[e].filter(t=>!t.done)),r.json({code:200,message:"success",data:null})});Ui.post("/:type/clear_succeeded",r=>{let e=r.req.param("type");return yl[e]&&(yl[e]=yl[e].filter(t=>t.state!=="succeeded")),r.json({code:200,message:"success",data:null})});var Il=r=>r.json({code:501,message:"task lifecycle operations (retry/cancel/delete) are not supported in the TS Worker runtime; file operations execute synchronously and do not produce persistent tasks",data:null},501);Ui.post("/:type/retry_failed",Il);Ui.post("/:type/retry",Il);Ui.post("/:type/retry_some",Il);Ui.post("/:type/cancel",Il);Ui.post("/:type/cancel_some",Il);Ui.post("/:type/delete",Il);Ui.post("/:type/delete_some",Il);Cr();Io();Ye();Fi();gl();d0();var Ax=new Xe,nle=300*1e3,Gv=new Map;function ii(r,e,t=""){let i=(r.settings||[]).find(n=>n.key===e);return!i||i.value===void 0||i.value===null?t:String(i.value)}function m0(r,e){let t=ii(r,e,"false");return t==="true"||t==="1"}function qM(r){return r.req.header("CF-Connecting-IP")||r.req.header("x-real-ip")||r.req.header("x-forwarded-for")?.split(",")[0]?.trim()||"unknown"}function bu(r){try{return new URL(r.req.url).origin}catch{return"http://localhost"}}function sle(r,e){let t=new Uint8Array(16);crypto.getRandomValues(t);let i=Array.from(t,n=>n.toString(16).padStart(2,"0")).join("");return Gv.set(`${r}_${i}`,{ip:e,exp:Date.now()+nle}),i}function ole(r,e,t){let i=`${r}_${t}`,n=Gv.get(i);return n?(Gv.delete(i),n.ip===e&&n.exp>Date.now()):!1}function OM(r,e,t,i,n){switch(r){case"Github":return{authUrl:"https://github.com/login/oauth/authorize",tokenUrl:"https://github.com/login/oauth/access_token",userUrl:"https://api.github.com/user",scope:n||"read:user",authField:"code",idField:"id",usernameField:"login"};case"Microsoft":return{authUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",tokenUrl:"https://login.microsoftonline.com/common/oauth2/v2.0/token",userUrl:"https://graph.microsoft.com/v1.0/me",scope:n||"user.read",authField:"code",idField:"id",usernameField:"displayName",extraAuth:{response_mode:"query"},extraForm:{grant_type:"authorization_code"}};case"Google":return{authUrl:"https://accounts.google.com/o/oauth2/v2/auth",tokenUrl:"https://oauth2.googleapis.com/token",userUrl:"https://www.googleapis.com/oauth2/v1/userinfo",scope:n||"https://www.googleapis.com/auth/userinfo.profile",authField:"code",idField:"id",usernameField:"name",extraForm:{grant_type:"authorization_code"}};case"Dingtalk":return{authUrl:"https://login.dingtalk.com/oauth2/auth",tokenUrl:"https://api.dingtalk.com/v1.0/oauth2/userAccessToken",userUrl:"https://api.dingtalk.com/v1.0/contact/users/me",scope:n||"openid",authField:"authCode",idField:"unionId",usernameField:"nick",extraAuth:{prompt:"consent"},tokenStyle:"json"};case"Casdoor":return{authUrl:(e||"").replace(/\/login\/oauth\/authorize$/,"")+"/login/oauth/authorize",tokenUrl:(t||"").replace(/\/api\/login\/oauth\/access_token$/,"")+"/api/login/oauth/access_token",userUrl:(i||"").replace(/\/api\/userinfo$/,"")+"/api/userinfo",scope:n||"profile",authField:"code",idField:"sub",usernameField:"preferred_username",extraForm:{grant_type:"authorization_code"}};case"OIDC":return null;default:return null}}async function MM(r){let e=r.endsWith("/.well-known/openid-configuration")?r:r.replace(/\/+$/,"")+"/.well-known/openid-configuration",t=await fetch(e,{headers:{Accept:"application/json"}});if(!t.ok)throw new Error(`OIDC discovery failed: HTTP ${t.status}`);let i=await t.json();if(!i.authorization_endpoint||!i.token_endpoint)throw new Error("OIDC discovery returned incomplete metadata");return{authUrl:i.authorization_endpoint,tokenUrl:i.token_endpoint,userUrl:i.userinfo_endpoint||""}}async function ale(r,e){let t={id:r.id,username:r.username,role:r.role,exp:Math.floor(Date.now()/1e3)+604800,jti:typeof crypto.randomUUID=="function"?crypto.randomUUID():xu()},i=await Dr(e);return await yo(t,i)}async function cle(r,e,t){let i=e||t;r.users.some(o=>o.username===i)&&(i=`${i}_${t}`);let s={id:r.users.length?Math.max(...r.users.map(o=>Number(o.id)||0))+1:1,username:i,password:"",role:0,permission:0,base_path:"/",disabled:!1,sso_id:t,allow_ldap:!1,pwd_update_at:new Date().toISOString()};return await Ti(s,xu()),r.users.push(s),await fe(r,r.env),s}function RM(r,e){let t=String(e).replace(/</g,"\\u003c").replace(/>/g,"\\u003e");return`<!DOCTYPE html> -<head></head> -<body> -<script> -window.opener.postMessage({"${r}":"${t}"}, "*") -window.close() -</script> -</body>`}Ax.get("/sso",async r=>{let e=await ne(r.env);if(!m0(e,"sso_login_enabled"))return r.json({code:403,message:"Single sign-on is not enabled",data:null},403);let t=r.req.query("method")||"";if(!t)return r.json({code:400,message:"no method provided",data:null},400);let i=ii(e,"sso_login_platform"),n=ii(e,"sso_client_id"),o=m0(e,"sso_compatibility_mode")?`${bu(r)}/api/auth/${t}`:`${bu(r)}/api/auth/sso_callback?method=${t}`,a=ii(e,"sso_login_url"),c=ii(e,"sso_token_url"),l=ii(e,"sso_userinfo_url"),d=ii(e,"sso_scopes"),u=ii(e,"sso_oidc_discovery_url");try{if(i==="OIDC"){let g=await MM(u||a),p=sle(n,qM(r)),I=new URLSearchParams({response_type:"code",client_id:n,redirect_uri:o,scope:d||"openid profile",state:p});return r.redirect(`${g.authUrl}?${I.toString()}`,302)}let f=OM(i,a,c,l,d);if(!f)return r.json({code:400,message:"invalid platform",data:null},400);let h=new URLSearchParams({response_type:"code",client_id:n,redirect_uri:o,scope:f.scope});for(let[g,p]of Object.entries(f.extraAuth||{}))h.set(g,p);return r.redirect(`${f.authUrl}?${h.toString()}`,302)}catch(f){return r.json({code:400,message:f.message||"SSO error",data:null},400)}});Ax.get("/sso_callback",async r=>{let e=await ne(r.env);if(!m0(e,"sso_login_enabled"))return r.text("Single sign-on is not enabled",403);let t=r.req.query("method")||"";if(t!=="get_sso_id"&&t!=="sso_get_token")return r.text("invalid request",400);let i=ii(e,"sso_login_platform"),n=ii(e,"sso_client_id"),s=ii(e,"sso_client_secret"),o=m0(e,"sso_compatibility_mode"),a=o?`${bu(r)}/api/auth/${t}`:`${bu(r)}/api/auth/sso_callback?method=${t}`,c=ii(e,"sso_login_url"),l=ii(e,"sso_token_url"),d=ii(e,"sso_userinfo_url"),u=ii(e,"sso_scopes"),f=ii(e,"sso_oidc_discovery_url");try{let h,g,p,I,m,w,B;if(i==="OIDC"){let R=await MM(f||c);if(h=R.tokenUrl,g=R.userUrl,p="sub",I="preferred_username",m=u||"openid profile",!ole(n,qM(r),r.req.query("state")||""))return r.text("incorrect or expired state parameter",400);let U=r.req.query("code")||"",D=await(await fetch(h,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"},body:new URLSearchParams({grant_type:"authorization_code",code:U,client_id:n,client_secret:s,redirect_uri:a,scope:m}).toString()})).json().catch(()=>({}));if(w=D.access_token,!w)throw new Error("no access_token in token response");if(g){B=await(await fetch(g,{headers:{Authorization:`Bearer ${w}`,Accept:"application/json"}})).json().catch(()=>({}));let _=B?.preferred_username||B?.name||B?.nickname||"";if(!B?.sub&&!_){let v=D.id_token;if(v){let E=NM(v);B=B||{},B.sub=B.sub||E?.sub,B.name=_||E?.name||E?.preferred_username||""}}_&&(B.name=_)}else{let b=D.id_token;if(!b)throw new Error("no id_token and no userinfo endpoint");B=NM(b)||{},B.sub=B.sub||B.name||""}}else{let R=OM(i,c,l,d,u);if(!R)return r.text("invalid platform",400);h=R.tokenUrl,g=R.userUrl,p=R.idField,I=R.usernameField,m=R.scope;let U=r.req.query(R.authField)||"";if(!U)return r.text("No code provided",400);let T;if(R.tokenStyle==="json")T=await fetch(h,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify({clientId:n,clientSecret:s,code:U,grantType:"authorization_code"})});else{let v={client_id:n,client_secret:s,code:U,redirect_uri:a,scope:m,...R.extraForm||{}};T=await fetch(h,{method:"POST",headers:{Accept:"application/json"},body:new URLSearchParams(v).toString()})}let D=await T.json().catch(()=>({}));if(w=D.access_token||D.accessToken,!w)throw new Error(`token exchange failed: ${D.error||T.status}`);let b={Accept:"application/json"};i==="Dingtalk"?b["x-acs-dingtalk-access-token"]=w:b.Authorization=`Bearer ${w}`,B=await(await fetch(g,{headers:b})).json().catch(()=>({}))}let C=String(B?.id??B?.sub??B?.[p]??"");if(!C||C==="0")return r.text("cannot get user id from SSO provider",400);if(t==="get_sso_id")return o?r.redirect(`${bu(r)}/@manage?sso_id=${encodeURIComponent(C)}`,302):r.html(RM("sso_id",C));let x=(e.users||[]).find(R=>R.sso_id===C);if(!x){if(!m0(e,"sso_auto_register"))return r.text("user not found and auto register is disabled",400);let U=String(B?.login??B?.[I]??B?.name??"");x=await cle(e,U||C,C)}let Q=await ale(x,r);return o?r.redirect(`${bu(r)}/@login?token=${encodeURIComponent(Q)}`,302):r.html(RM("token",Q))}catch(h){return r.text(h.message||"SSO error",500)}});function NM(r){try{let e=r.split(".");if(e.length<2)return null;let t=e[1].replace(/-/g,"+").replace(/_/g,"/"),i=t.padEnd(Math.ceil(t.length/4)*4,"="),n=atob(i);return JSON.parse(n)}catch{return null}}Cr();Io();Ye();Fi();var tc=new Xe;function ni(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"")}function Za(r){let e=r.replace(/-/g,"+").replace(/_/g,"/"),t=e.padEnd(Math.ceil(e.length/4)*4,"="),i=atob(t),n=new Uint8Array(i.length);for(let s=0;s<i.length;s++)n[s]=i.charCodeAt(s);return n}function lle(r){let e="";for(let t of r)e+=String.fromCharCode(t);return btoa(e)}function dle(r){let e=atob(r),t=new Uint8Array(e.length);for(let i=0;i<e.length;i++)t[i]=e.charCodeAt(i);return t}function ec(r){let e=r.u8[r.i++],t=e>>5,i=e&31,n=i;if(i===24)n=r.u8[r.i++];else if(i===25)n=r.u8[r.i]<<8|r.u8[r.i+1],r.i+=2;else if(i===26)n=r.u8[r.i]<<24|r.u8[r.i+1]<<16|r.u8[r.i+2]<<8|r.u8[r.i+3],r.i+=4;else if(i===27){let s=0n;for(let o=0;o<8;o++)s=s<<8n|BigInt(r.u8[r.i++]);n=Number(s)}else if(i===31){if(t===4){let s=[];for(;r.u8[r.i]!==255;)s.push(ec(r));return r.i++,s}else if(t===5){let s={};for(;r.u8[r.i]!==255;){let o=ec(r),a=ec(r);s[String(o)]=a}return r.i++,s}}switch(t){case 0:return n;case 1:return-1-n;case 2:{let s=r.u8.slice(r.i,r.i+n);return r.i+=n,s}case 3:{let s=new TextDecoder().decode(r.u8.slice(r.i,r.i+n));return r.i+=n,s}case 4:{let s=[];for(let o=0;o<n;o++)s.push(ec(r));return s}case 5:{let s={};for(let o=0;o<n;o++){let a=ec(r),c=ec(r);s[String(a)]=c}return s}default:throw new Error(`unsupported CBOR major type ${t}`)}}async function ule(r){let e=r[1],t=r[3];if(e===2){let i=r[-1],n=r[-2],s=r[-3];if(i!==1)throw new Error("unsupported EC curve (only P-256)");let o={kty:"EC",crv:"P-256",x:ni(new Uint8Array(n)),y:ni(new Uint8Array(s))};return await crypto.subtle.importKey("jwk",o,{name:"ECDSA",namedCurve:"P-256"},!1,["verify"])}if(e===3){let i=r[-1],n=r[-2],s={kty:"RSA",n:ni(new Uint8Array(i)),e:ni(new Uint8Array(n))};return await crypto.subtle.importKey("jwk",s,{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},!1,["verify"])}throw new Error(`unsupported COSE kty ${e}`)}async function fle(r){let t=ec({u8:r,i:0}),i=t.authData,n=t.attStmt||{},s=i.slice(0,32),o=i[32],a=i[33]<<24|i[34]<<16|i[35]<<8|i[36];if(!(o&64))throw new Error("attestedCredentialData not present");let c=37,l=i.slice(c,c+16);c+=16;let d=i[c]<<8|i[c+1];c+=2;let u=i.slice(c,c+d);c+=d;let f={u8:i.slice(c),i:0},h=ec(f),g=await ule(h);return{authData:i,publicKey:g,aaguid:l,credentialId:u,signCount:a,flags:o}}function hx(r,e){let i=(r.settings||[]).find(n=>n.key===e)?.value;return i==="true"||i==="1"}function LM(r,e,t=""){let i=(r.settings||[]).find(n=>n.key===e);return i?.value?String(i.value):t}function gx(r,e){let t=LM(e,"webauthn_rp_id");if(t)return t;try{return new URL(r.req.url).hostname}catch{return"localhost"}}function zv(){let r=new Uint8Array(32);return crypto.getRandomValues(r),r}function HM(r){return lle(new TextEncoder().encode(JSON.stringify(r)))}function KM(r){return JSON.parse(new TextDecoder().decode(dle(r)))}async function Ale(r,e){let t={id:r.id,username:r.username,role:r.role,exp:Math.floor(Date.now()/1e3)+604800,jti:typeof crypto.randomUUID=="function"?crypto.randomUUID():ni(zv())},i=await Dr(e);return await yo(t,i)}function y0(r){return r.webauthn_credentials||[]}tc.post("/webauthn_begin_login",async r=>{let e=await ne(r.env);if(!hx(e,"webauthn_login_enabled"))return r.json({code:403,message:"WebAuthn is not enabled",data:null},403);let t=r.req.query("username")||"",i=gx(r,e),n=zv(),s={challenge:ni(n),userVerification:"preferred"},o;if(t){let c=(e.users||[]).find(l=>l.username===t);if(!c)return r.json({code:400,message:"user not found",data:null},400);s.userId=String(c.id),s.username=t,o=y0(c).map(l=>({id:typeof l.id=="string"?l.id:ni(new Uint8Array(l.id)),type:"public-key"})),s.allowedCredentials=o}let a={challenge:s.challenge,rpId:i,userVerification:s.userVerification};return o&&o.length>0&&(a.allowCredentials=o),r.json({code:200,message:"success",data:{options:a,session:HM(s)}})});tc.post("/webauthn_finish_login",async r=>{let e=await ne(r.env);if(!hx(e,"webauthn_login_enabled"))return r.json({code:403,message:"WebAuthn is not enabled",data:null},403);let t=r.req.header("session")||r.req.header("Session")||"",i;try{i=KM(t)}catch{return r.json({code:400,message:"invalid session",data:null},400)}let n=await r.req.json().catch(()=>({})),s=gx(r,e);try{let o=Za(n.id||""),a=JSON.parse(new TextDecoder().decode(Za(n.response?.clientDataJSON||"")));if(a.challenge!==i.challenge)return r.json({code:400,message:"challenge mismatch",data:null},400);if(a.type!=="webauthn.get")return r.json({code:400,message:"invalid ceremony type",data:null},400);let c=Za(n.response?.authenticatorData||"");if(c.length<37)throw new Error("invalid authenticatorData");let l=c.slice(0,32),d=new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(s)));if(ni(l)!==ni(d))return r.json({code:400,message:"rpId hash mismatch",data:null},400);let u=r.req.query("username")||i.username||"",f=u?(e.users||[]).find(R=>R.username===u):null,h;if(f)h=y0(f).find(R=>(typeof R.id=="string"?R.id:ni(new Uint8Array(R.id)))===n.id);else{let R=Za(n.response?.userHandle||"");if(R.length){let U=new TextDecoder().decode(R);f=(e.users||[]).find(T=>String(T.id)===U),f&&(h=y0(f).find(T=>(typeof T.id=="string"?T.id:ni(new Uint8Array(T.id)))===n.id))}}if(!f||!h)return r.json({code:400,message:"credential not found",data:null},400);let g=h.publicKey;if(!g)return r.json({code:400,message:"credential missing public key",data:null},400);let p=await crypto.subtle.importKey("jwk",g,g.kty==="RSA"?{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"}:{name:"ECDSA",namedCurve:"P-256"},!1,["verify"]),I=Za(n.response?.signature||""),m=new Uint8Array(await crypto.subtle.digest("SHA-256",Za(n.response?.clientDataJSON||""))),w=new Uint8Array(c.length+m.length);w.set(c,0),w.set(m,c.length);let B=g.kty==="RSA"?{name:"RSASSA-PKCS1-v1_5"}:{name:"ECDSA",hash:"SHA-256"};if(!await crypto.subtle.verify(B,p,I,w).catch(()=>!1))return r.json({code:400,message:"signature verification failed",data:null},400);let x=c[33]<<24|c[34]<<16|c[35]<<8|c[36];if(h.sign_count!==void 0&&x!==0&&x<=h.sign_count)return r.json({code:400,message:"stale signCount (possible replay)",data:null},400);h.sign_count=x,await fe(e,r.env);let Q=await Ale(f,r);return r.json({code:200,message:"success",data:{token:Q}})}catch(o){return r.json({code:400,message:o.message||"WebAuthn login failed",data:null},400)}});tc.post("/webauthn_begin_registration",async r=>{let e=await ne(r.env);if(!hx(e,"webauthn_login_enabled"))return r.json({code:403,message:"WebAuthn is not enabled",data:null},403);let t=await Me(r);if(!t||t.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let i=gx(r,e),n=zv(),s=new TextEncoder().encode(String(t.id)),o={challenge:ni(n),userId:String(t.id),username:t.username,userVerification:"preferred"},a=y0(t).map(l=>({id:typeof l.id=="string"?l.id:ni(new Uint8Array(l.id)),type:"public-key"})),c={challenge:o.challenge,rp:{name:LM(e,"site_title","OpenList"),id:i},user:{id:ni(s),name:t.username,displayName:t.username},pubKeyCredParams:[{type:"public-key",alg:-7},{type:"public-key",alg:-257}],timeout:6e4,authenticatorSelection:{userVerification:o.userVerification,residentKey:"preferred"},attestation:"none"};return a.length>0&&(c.excludeCredentials=a),r.json({code:200,message:"success",data:{options:c,session:HM(o)}})});tc.post("/webauthn_finish_registration",async r=>{let e=await ne(r.env);if(!hx(e,"webauthn_login_enabled"))return r.json({code:403,message:"WebAuthn is not enabled",data:null},403);let t=await Me(r);if(!t||t.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let i=r.req.header("Session")||r.req.header("session")||"",n;try{n=KM(i)}catch{return r.json({code:400,message:"invalid session",data:null},400)}let s=await r.req.json().catch(()=>({})),o=gx(r,e);try{let a=JSON.parse(new TextDecoder().decode(Za(s.response?.clientDataJSON||"")));if(a.challenge!==n.challenge)return r.json({code:400,message:"challenge mismatch",data:null},400);if(a.type!=="webauthn.create")return r.json({code:400,message:"invalid ceremony type",data:null},400);let c=Za(s.response?.attestationObject||""),{publicKey:l,aaguid:d,credentialId:u,signCount:f}=await fle(c),g={id:s.id||ni(u),publicKey:await crypto.subtle.exportKey("jwk",l),aaguid:ni(d),sign_count:f,created_at:new Date().toISOString()},p=t;return p.webauthn_credentials||(p.webauthn_credentials=[]),p.webauthn_credentials.push(g),await fe(e,r.env),r.json({code:200,message:"Registered Successfully",data:null})}catch(a){return r.json({code:400,message:a.message||"registration failed",data:null},400)}});tc.post("/delete_authn",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let i=(await r.req.json().catch(()=>({}))).id||"",n=await ne(r.env),s=(n.users||[]).find(a=>a.id===e.id);if(!s)return r.json({code:404,message:"user not found",data:null},404);let o=(s.webauthn_credentials||[]).length;return s.webauthn_credentials=(s.webauthn_credentials||[]).filter(a=>a.id!==i),s.webauthn_credentials.length===o?r.json({code:404,message:"credential not found",data:null},404):(await fe(n,r.env),r.json({code:200,message:"Deleted Successfully",data:null}))});tc.get("/getcredentials",async r=>{let e=await Me(r);if(!e||e.disabled)return r.json({code:401,message:"Unauthorized",data:null},401);let t=y0(e).map(i=>({id:i.id,fingerprint:i.aaguid||""}));return r.json({code:200,message:"success",data:t})});var I0=new Map,vu=new Map;function hle(r){return r.req.header("CF-Connecting-IP")||r.req.header("x-real-ip")||r.req.header("x-forwarded-for")?.split(",")[0]?.trim()||"unknown"}function gle(){let r=Date.now();if(I0.size>2e4)for(let[e,t]of I0)r-t.start>6e4&&I0.delete(e);if(vu.size>2e4)for(let[e,t]of vu)r-t.start>36e5&&vu.delete(e)}async function ple(r,e){let t=hle(r),i=Date.now(),n=0,s=0;try{let o=await ne(r.env),a={};for(let c of o.settings||[])a[c.key]=c.value;n=parseInt(a.ip_limit,10)||0,s=parseInt(a.traffic_limit,10)||0}catch{}if(gle(),n>0){let o=I0.get(t);if(!o||i-o.start>6e4)I0.set(t,{start:i,count:1});else if(o.count+=1,o.count>n)return r.json({code:429,message:"Too many requests, slow down",data:null},429)}if(s>0){let o=vu.get(t),a=s*1024*1024;if(o&&i-o.start<=36e5&&o.bytes>=a)return r.json({code:429,message:"Traffic limit exceeded",data:null},429)}if(await e(),(r.req.query("token")||r.req.query("access_token"))&&(r.res?.headers?.set("Referrer-Policy","no-referrer"),r.res?.headers?.set("Cache-Control","no-store, no-cache, must-revalidate"),r.res?.headers?.set("Pragma","no-cache")),s>0){let o=parseInt(r.res?.headers?.get("content-length")||"0",10)||0;if(o>0){let a=vu.get(t);!a||i-a.start>36e5?vu.set(t,{start:i,bytes:o}):a.bytes+=o}}}function GM(r){r.use("*",ple),r.use("*",async(e,t)=>{let{auditMiddleware:i}=await Promise.resolve().then(()=>(Fi(),pw));return i(e,t)}),r.use("*",async(e,t)=>{await t(),e.res.headers.set("X-Frame-Options","DENY"),e.res.headers.set("X-Content-Type-Options","nosniff"),e.res.headers.set("Content-Security-Policy","default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob:; frame-ancestors 'none'"),e.res.headers.set("Strict-Transport-Security","max-age=31536000; includeSubDomains"),e.res.headers.set("Referrer-Policy","no-referrer")}),r.use("*",Vk({origin:(e,t)=>{if(!e)return e;let s=((t.env||{}).ALLOW_URLS||(typeof process<"u"?process.env?.ALLOW_URLS:"")||"").split(",").map(a=>a.trim()).filter(Boolean);if(s.length>0)return s.includes(e)?e:null;let o=t.req.header("host")||"";try{if(new URL(e).host===o)return e}catch{}return null},allowHeaders:["Content-Type","Authorization","X-Requested-With"],allowMethods:["GET","POST","PUT","DELETE","OPTIONS"],exposeHeaders:["Content-Length","Content-Type"],maxAge:600,credentials:!0})),r.route("/raw",bs),r.route("/fs",dt),r.route("/auth",mn),r.route("/public",bo),r.route("/admin",Ue),r.route("/mcp",p0),r.route("/debug",Kv),r.route("/share",ri),r.route("/task",Ui),r.route("/auth",Ax),r.route("/authn",tc),r.route("/d",bs),r.route("/sd",bs),r.route("/p",bs),r.route("/me",Iu),r.get("/me",cx),r.post("/me/update",ax),r.post("/user/update_pwd",mM),r.get("/logout",Cu),r.post("/logout",Cu),r.get("/health",e=>e.json({ok:!0,name:"OpenList",version:"v4.2.3",environment:e.env?.ENVIRONMENT||"development"})),r.get("/healthz",async e=>{let t={},i=!0;try{let o=await ne(e.env);t.config={ok:!0,storages:o?.storages?.length??0,users:o?.users?.length??0}}catch(o){i=!1,t.config={ok:!1,error:o?.message||String(o)}}let n;try{n=await Ul(e.env)}catch(o){i=!1,n={configured:!1,connected:!1,error:o?.message||String(o)}}t.persistence={configured:!!n?.configured,connected:!!n?.connected,platform:n?.platform??null,error:n?.error??null};let s=jn(e.env);return n?.configured||(t.persistence.mode=s?"unavailable":"memory",s?(t.persistence.note=Vo,i=!1):t.persistence.note="No persistence configured \u2014 changes are ephemeral"),n?.configured&&!n?.connected&&(i=!1),e.json({ok:i,name:"OpenList",version:"v4.2.3",environment:e.env?.ENVIRONMENT||"development",checks:t},i?200:503)})}Cr();Ye();var mle="https://res.oplist.org/logo/logo.svg",rc=new Xe;function w0(r){return r.redirect(mle,302)}rc.get("/logo.svg",w0);rc.get("/logo.png",w0);rc.get("/favicon.svg",w0);rc.get("/favicon.png",w0);rc.get("/favicon.ico",w0);var yle=5*6e4,Jv=new Map;function px(r){return String(r?.ASSET_URLS||process.env?.ASSET_URLS||"")}function mx(r){return!!px(r)}function Ile(r){let e=r.match(/<meta\s+name=["']frontend-version["']\s+content=["']([^"']+)["']/i);return e?e[1]:""}async function JM(r,e){let t=px(r);if(!t)return"";if(!t.includes("$version"))return t;let i="";if(e&&(i=Ile(e)),!i)try{let s=((await ne(r)).settings||[]).find(o=>o.key==="version");if(s?.value){let o=String(s.value).match(/Frontend:\s*([^\s-]+)/);o&&(i=o[1])}}catch{}return i||(i="latest"),t.replace(/\$version/g,i)}function zM(r,e){if(!e)return r;let t=e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\r/g,"\\r").replace(/\n/g,"\\n");return r.replace(/cdn:\s*undefined/,()=>`cdn: '${t}'`)}var jM=["assets","images","streamer","static"],$M=new Map;async function yx(r,e){let t=px(r);if(!t)return"";let i=e.match(/^\/([^/?#]+)\/(.+)/);if(!i||!jM.includes(i[1]))return"";let n=$M.get(t)||await JM(r);return!n||!/^https?:\/\//i.test(n)?"":`${n}${e}`}function wle(r){let e=r.match(/"type"\s*:\s*"module"[^{}]*?"src"\s*:\s*"([^"]+)"/)||r.match(/"(\/assets\/[^"]+\.js)"/)||r.match(/(\/assets\/[A-Za-z0-9_.-]+\.js)/);return e?e[1]:""}async function xle(r,e){try{return(await fetch(`${r}${e}`,{method:"HEAD",signal:AbortSignal.timeout(4e3)})).ok}catch{return!1}}async function Ix(r,e){let t=await JM(r,e);if(!t||!/^https?:\/\//i.test(t))return e;$M.set(px(r),t);let i=Jv.get(t);if(i&&Date.now()-i.ts<yle)return i.html;let n=wle(e);if(n&&await xle(t,n)){let s=zM(e,t);return Jv.set(t,{html:s,ts:Date.now()}),s}try{let s=await fetch(`${t}/index.html`,{headers:{accept:"text/html"},signal:AbortSignal.timeout(4e3)});if(s.ok){let o=await s.text();if(/<html/i.test(o))return o=zM(o,t),Jv.set(t,{html:o,ts:Date.now()}),o}}catch{}return e}for(let r of jM)rc.get(`/${r}/*`,async(e,t)=>{let i=await yx(e.env,e.req.path+new URL(e.req.url).search);return i?e.redirect(i,302):t()});Cr();gl();Yt();OA();function VM(r,e){return B9(r,e)}Ma();Nl();var jv=new Xe,Cle=r=>{try{let e=r.executionCtx;return!e||typeof e.waitUntil!="function"?void 0:{waitUntil:t=>e.waitUntil(t),env:r.env}}catch{return}};async function Ele(r){let e=r.req.header("Authorization")||"";if(e.startsWith("Basic "))try{let t=atob(e.substring(6).trim()),i=t.indexOf(":");if(i<0)return null;let n=t.substring(0,i),s=t.substring(i+1),{users:o}=await u0(r.env),a=o.find(c=>c.username===n&&!c.disabled);return a?a.password?await wu(a,s)?a:null:s===""?a:null:null}catch{return null}if(e.startsWith("Bearer ")){let t=await qn(r);return t?t.user:null}return null}function Ble(r){let t=new URL(r.req.url).pathname.replace(/^\/dav/,"");t||(t="/");try{return decodeURIComponent(t)}catch{return t}}function x0(r){let t=(r.startsWith("/")?r:"/"+r).split("/").filter(Boolean),i=t.pop()||"";return{dir:"/"+t.join("/"),name:i}}jv.all("/*",async r=>{let e=await Ele(r);if(!e)return r.text("Unauthorized",401,{"WWW-Authenticate":'Basic realm="OpenList"'});let t=Ha(e,La.WEBDAV_READ),i=Ha(e,La.WEBDAV_MANAGE);if(!t&&!i)return r.text("Forbidden",403);let n=r.req.method.toUpperCase(),s=Ble(r),o=Cle(r);try{switch(n){case"OPTIONS":return r.header("DAV","1, 2"),r.header("Allow","OPTIONS, PROPFIND, GET, HEAD, PUT, MKCOL, DELETE, MOVE, COPY"),r.header("MS-Author-Via","DAV"),r.body(null,200);case"PROPFIND":{if(!t)return r.text("Forbidden",403);let a=r.req.header("Depth")||"1",l=((await Zt(s,o)).content||[]).map(f=>({name:f.name,size:f.size||0,isFolder:!!f.is_dir,modified:f.modified||new Date().toISOString()})),d=s==="/"?"/":s.endsWith("/")?s:s+"/",u=VM(d,l);return r.body(u,207,{"Content-Type":"application/xml; charset=utf-8"})}case"GET":case"HEAD":{if(!t)return r.text("Forbidden",403);let{item:a,rawUrl:c}=await _i(s,o);return a?a.is_dir?r.text("Is a directory",400):r.redirect(c||`/api/d${Os(s)}`,302):r.text("Not found",404)}case"PUT":{if(!i)return r.text("Forbidden",403);let a=Buffer.from(await r.req.arrayBuffer());return await Un(s,a,o),r.body(null,201)}case"MKCOL":return i?(await UA(s,o),r.body(null,201)):r.text("Forbidden",403);case"DELETE":{if(!i)return r.text("Forbidden",403);let{dir:a,name:c}=x0(s);return await qa(a,[c],o),r.body(null,204)}case"MOVE":{if(!i)return r.text("Forbidden",403);let a=r.req.header("Destination")||"",c=a;try{c=decodeURIComponent(new URL(a,r.req.url).pathname).replace(/^\/dav/,"")}catch{}let l=x0(s),d=x0(c);return await Vd(l.dir,d.dir,[l.name],o),r.body(null,201)}case"COPY":{if(!i)return r.text("Forbidden",403);let a=r.req.header("Destination")||"",c=a;try{c=decodeURIComponent(new URL(a,r.req.url).pathname).replace(/^\/dav/,"")}catch{}let l=x0(s),d=x0(c);return await NA(l.dir,d.dir,[l.name],o),r.body(null,201)}case"LOCK":case"UNLOCK":return r.text("Locking not supported",405);default:return r.text("Method Not Allowed",405)}}catch(a){let c=Ne(a);return c.includes("not found")||c.includes("storage not found")?r.text("Not Found",404):r.text(c,500)}});Cr();gl();Yt();Ma();var wl=new Xe;function wx(r){return String(r).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function _r(r,e,t){let i=`<?xml version="1.0" encoding="UTF-8"?> -<Error><Code>${wx(r)}</Code><Message>${wx(e)}</Message></Error>`;return new Response(i,{status:t,headers:{"Content-Type":"application/xml","x-amz-request-id":"-"}})}function xx(r){let i=new URL(r.req.url).pathname.replace(/^\/s3\/?/,"").split("/").filter(Boolean),n=i[0]||"",s=i.slice(1).map(decodeURIComponent).join("/");return{bucket:n,key:s}}async function C0(r){let e=await qn(r);return e?e.user:null}var E0=r=>{try{let e=r.executionCtx;return e&&typeof e.waitUntil=="function"?{waitUntil:t=>e.waitUntil(t)}:void 0}catch{return}};wl.get("/",async r=>{if(!await C0(r))return _r("AccessDenied","Authentication required",403);try{let n=`<?xml version="1.0" encoding="UTF-8"?> -<ListAllMyBucketsResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> - <Owner><ID>openlist</ID><DisplayName>openlist</DisplayName></Owner> - <Buckets> -${((await Zt("/",E0(r))).content||[]).filter(s=>s.is_dir).map(s=>` <Bucket><Name>${wx(s.name)}</Name><CreationDate>${wx(s.modified||new Date().toISOString())}</CreationDate></Bucket>`).join(` -`)} - </Buckets> -</ListAllMyBucketsResult>`;return new Response(n,{status:200,headers:{"Content-Type":"application/xml"}})}catch(t){return _r("InternalError",Ne(t),500)}});wl.get("/*",async r=>{if(!await C0(r))return _r("AccessDenied","Authentication required",403);let{bucket:t,key:i}=xx(r);if(!t||!i)return _r("NoSuchKey","NoSuchKey",404);let n=`/${t}/${i}`;try{let{item:s,rawUrl:o}=await _i(n,E0(r));return!s||s.is_dir?_r("NoSuchKey","NoSuchKey",404):o?r.redirect(o,302):_r("NoSuchKey","NoSuchKey",404)}catch(s){return _r("NoSuchKey",Ne(s),404)}});wl.on("HEAD","/*",async r=>{if(!await C0(r))return _r("AccessDenied","Authentication required",403);let{bucket:t,key:i}=xx(r);if(!t||!i)return _r("NoSuchKey","NoSuchKey",404);try{let{item:n}=await _i(`/${t}/${i}`,E0(r));if(!n||n.is_dir)return _r("NoSuchKey","NoSuchKey",404);let s={"Content-Length":String(n.size||0),"Content-Type":String(n.type||"application/octet-stream"),"Last-Modified":n.modified||new Date().toISOString()};return new Response(null,{status:200,headers:s})}catch{return _r("NoSuchKey","NoSuchKey",404)}});wl.put("/*",async r=>{if(!await C0(r))return _r("AccessDenied","Authentication required",403);let{bucket:t,key:i}=xx(r);if(!t||!i)return _r("InvalidArgument","Invalid bucket/key",400);try{let n=Buffer.from(await r.req.arrayBuffer());return await Un(`/${t}/${i}`,n,E0(r)),new Response(null,{status:200,headers:{ETag:`"${Date.now().toString(16)}"`}})}catch(n){return _r("InternalError",Ne(n),500)}});wl.delete("/*",async r=>{if(!await C0(r))return _r("AccessDenied","Authentication required",403);let{bucket:t,key:i}=xx(r);if(!t||!i)return _r("InvalidArgument","Invalid bucket/key",400);let n=i.lastIndexOf("/"),s=n>=0?`/${t}/${i.slice(0,n)}`:`/${t}`,o=n>=0?i.slice(n+1):i;try{return await qa(s,[o],E0(r)),new Response(null,{status:204})}catch(a){return _r("NoSuchKey",Ne(a),404)}});Ye();gc();var Mn=new Xe;function ble(r,e,t){return!!(/\.[a-zA-Z0-9]+$/.test(r)||(t==="GET"||t==="HEAD")&&e.includes("text/html"))}var vle=["/api/public/env_check","/api/public/init_status","/api/healthz"];function kle(r){return vle.includes(r)}var YM=["/kv-list","/kv-get","/kv-put","/kv-delete"];function Sle(r){return YM.includes(r)}Mn.use("*",async(r,e)=>{let t=r.env||{};try{let s=new URL(r.req.url);t.__requestOrigin||(t.__requestOrigin=s.origin)}catch{}RC(t);let{pathname:i}=new URL(r.req.url);if(!(ble(i,r.req.header("accept")||"",r.req.method)||kle(i)||Sle(i))){let s=await hc(t);if(s.message){let o=_M(s);return r.json({code:503,message:s.message,data:{error:"STORAGE_CONFIG_ERROR",code:s.code,configError:s.message,summary:Bu(s.message),reason:o.reason,suggestion:o.suggestion}},503)}}await e()});for(let r of YM)Mn.all(r,e=>e.json({code:410,message:`KV proxy endpoint "${r}" is not served by this deployment. It only exists on EdgeOne Edge Functions (functions/kv-*) or Cloudflare Pages Functions. See /api/public/env_check for the storage backend actually in use.`,data:null},410));var WM=new Xe;GM(WM);Mn.route("/api",WM);Mn.route("/d",bs);Mn.route("/sd",bs);Mn.route("/p",bs);Mn.route("/",rc);Mn.route("/dav",jv);Mn.route("/s3",wl);var $v=null;function XM(r){$v=r}Mn.all("*",async r=>{let e=r.env,t=new URL(r.req.url);if(e&&e.ASSETS&&typeof e.ASSETS.fetch=="function"){let n=await e.ASSETS.fetch(r.req.raw);if(n.status>=200&&n.status<300){if(t.pathname==="/"||t.pathname==="/index.html"){let u=new Headers(n.headers);if(u.set("Cache-Control","no-cache, must-revalidate"),u.set("Content-Type","text/html; charset=utf-8"),!mx(e))return new Response(n.body,{status:n.status,headers:u});u.delete("content-encoding"),u.delete("content-length");let f=await n.text();try{f=await Ix(e,f)}catch{}return new Response(f,{status:n.status,headers:u})}return n}let s=await yx(e,t.pathname+t.search);if(s)return r.redirect(s,302);let o=new Request(`${t.origin}/`,r.req.raw),a=await e.ASSETS.fetch(o),c=new Headers(a.headers);if(c.set("Content-Type","text/html; charset=utf-8"),c.set("Cache-Control","no-cache, must-revalidate"),!(mx(e)&&a.status>=200&&a.status<300&&(r.req.method==="GET"||r.req.method==="HEAD")))return new Response(a.body,{status:a.status,headers:c});c.delete("content-encoding"),c.delete("content-length");let d=await a.text();try{d=await Ix(e,d)}catch{}return new Response(d,{status:a.status,headers:c})}let i=await yx(e,t.pathname+t.search);if(i)return r.redirect(i,302);if($v&&(r.req.method==="GET"||r.req.method==="HEAD")){let n=$v;if(mx(e))try{n=await Ix(e,n)}catch{}return r.body(n,200,{"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-cache, must-revalidate"})}return r.text("404 Not Found",404)});var ZM=Mn;var eL=`<!doctype html> -<html lang="en" translate="no"> - <head> - <meta name="frontend-version" content="4.2.6"> - <!-- customize head --> - <meta charset="utf-8" > - <meta name="viewport" content="width=device-width, initial-scale=1" > - <meta name="referrer" content="same-origin" > - <meta name="color-scheme" content="light dark" > - <meta name="generator" content="OpenList" > - <meta name="theme-color" content="#000000" > - <meta name="google" content="notranslate" > - <link href="/manifest.json" rel="manifest" > - <meta name="apple-mobile-web-app-capable" content="yes" > - <meta name="apple-mobile-web-app-title" content="OpenList" > - <link rel="apple-touch-icon" href="https://res.oplist.org/logo/logo.png" > - <link rel="shortcut icon" - type="image/ico" - href="https://res.oplist.org/logo/logo.svg" - > - <title>Loading... - - - - - - - - - - - - - - -
- - - - - - - - - -`;XM(eL);function Qle(r){return ZM.fetch(r.request,r.env,r)}var wke=Qle;export{wke as default,Qle as onRequest}; -/*! Bundled license information: - -crypto-js/ripemd160.js: - (** @preserve - (c) 2012 by Cédric Mesnil. All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - *) - -crypto-js/mode-ctr-gladman.js: - (** @preserve - * Counter block mode compatible with Dr Brian Gladman fileenc.c - * derived from CryptoJS.mode.CTR - * Jan Hruby jhruby.web@gmail.com - *) - -hash-wasm/dist/index.umd.js: - (*! - * hash-wasm (https://www.npmjs.com/package/hash-wasm) - * (c) Dani Biro - * @license MIT - *) - -bcryptjs/dist/bcrypt.js: - (** - * @license bcrypt.js (c) 2013 Daniel Wirtz - * Released under the Apache License, Version 2.0 - * see: https://github.com/dcodeIO/bcrypt.js for details - *) - -openpgp/dist/node/openpgp.mjs: - (*! OpenPGP.js v6.3.1 - 2026-06-04 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. *) - (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *) - (*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *) - (*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) *) - -@otplib/plugin-crypto/index.js: - (** - * @otplib/plugin-crypto - * - * @author Gerald Yeo - * @version: 12.0.1 - * @license: MIT - **) - -@otplib/plugin-thirty-two/index.js: - (** - * @otplib/plugin-thirty-two - * - * @author Gerald Yeo - * @version: 12.0.1 - * @license: MIT - **) - -@otplib/core/index.js: - (** - * @otplib/core - * - * @author Gerald Yeo - * @version: 12.0.1 - * @license: MIT - **) - -@otplib/preset-default/index.js: - (** - * @otplib/preset-default - * - * @author Gerald Yeo - * @version: 12.0.1 - * @license: MIT - **) - -otplib/index.js: - (** - * otplib - * - * @author Gerald Yeo - * @version: 12.0.1 - * @license: MIT - **) -*/ diff --git a/middleware.js b/middleware.js index b4969f8d..901fcb34 100644 --- a/middleware.js +++ b/middleware.js @@ -1,6 +1,7 @@ // EdgeOne Makers 边缘中间件:SPA 路由回退 // -// cloud-functions/[[default]].js 是根级 catch-all,会接管所有未命中静态文件的 +// cloud-functions/[[default]].js(构建产物,见 scripts/build-edge.mjs)是根级 +// catch-all,会接管所有未命中静态文件的 // 请求(包括 /add、/@manage/* 等前端路由),而 Node 函数内没有 ASSETS 绑定, // Hono 兜底只能返回 404 —— 这就是「访问 /add 404 后整站打不开」的原因。 // 此中间件在边缘层先行拦截:浏览器导航请求(Accept: text/html)且不属于 diff --git a/readmes/README_ar.md b/readmes/README_ar.md index 6f6b8bc1..a7d399bd 100644 --- a/readmes/README_ar.md +++ b/readmes/README_ar.md @@ -6,7 +6,7 @@

يعمل على Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_de.md b/readmes/README_de.md index 151ab684..214fb7d1 100644 --- a/readmes/README_de.md +++ b/readmes/README_de.md @@ -6,7 +6,7 @@

Läuft auf Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_en.md b/readmes/README_en.md index 5fdccb5b..91d54338 100644 --- a/readmes/README_en.md +++ b/readmes/README_en.md @@ -6,7 +6,7 @@

Runs on Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_es.md b/readmes/README_es.md index e56a5d59..f1bbb4c9 100644 --- a/readmes/README_es.md +++ b/readmes/README_es.md @@ -6,7 +6,7 @@

Se ejecuta en Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_fr.md b/readmes/README_fr.md index 835e2189..9125e6eb 100644 --- a/readmes/README_fr.md +++ b/readmes/README_fr.md @@ -6,7 +6,7 @@

Fonctionne sur Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_hi.md b/readmes/README_hi.md index 25b23729..ce6a1150 100644 --- a/readmes/README_hi.md +++ b/readmes/README_hi.md @@ -6,7 +6,7 @@

Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA पर चलता है

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_it.md b/readmes/README_it.md index 4accac34..865e518f 100644 --- a/readmes/README_it.md +++ b/readmes/README_it.md @@ -6,7 +6,7 @@

Esegue su Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_ja.md b/readmes/README_ja.md index 5b61dfda..0038cbf5 100644 --- a/readmes/README_ja.md +++ b/readmes/README_ja.md @@ -6,7 +6,7 @@

Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA 上で動作します

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_ko.md b/readmes/README_ko.md index a20d06fd..5b4b3f21 100644 --- a/readmes/README_ko.md +++ b/readmes/README_ko.md @@ -6,7 +6,7 @@

Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA에서 실행됩니다

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_pt.md b/readmes/README_pt.md index 941af29f..ee7063f0 100644 --- a/readmes/README_pt.md +++ b/readmes/README_pt.md @@ -6,7 +6,7 @@

Executa no Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_ru.md b/readmes/README_ru.md index 875e53a4..f440e972 100644 --- a/readmes/README_ru.md +++ b/readmes/README_ru.md @@ -6,7 +6,7 @@

Работает на Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA

License -Build status +Build status latest version discussions Downloads diff --git a/readmes/README_zh-TW.md b/readmes/README_zh-TW.md index 64d93357..a18ce7ed 100644 --- a/readmes/README_zh-TW.md +++ b/readmes/README_zh-TW.md @@ -6,7 +6,7 @@

基於 Cloudflare Workers / EdgeOne Cloud Function / Alibaba Cloud ESA 運行

License -Build status +Build status latest version discussions Downloads diff --git a/scripts/build-edge.mjs b/scripts/build-edge.mjs index a117453d..e64577b7 100644 --- a/scripts/build-edge.mjs +++ b/scripts/build-edge.mjs @@ -169,6 +169,9 @@ async function build() { plugins: [emptyNodeDriverPlugin], }) + // EdgeOne Makers 的 Node 云函数入口。产物按平台约定落在项目根 + // cloud-functions/[[default]].js,但它**不入库**:EdgeOne 的构建命令 + // (edgeone.json -> pnpm run build)会在部署时执行本脚本重新生成。 await esbuild.build({ entryPoints: ["api/_makers.ts"], bundle: true, diff --git a/tests/artifact-guard.test.ts b/tests/artifact-guard.test.ts deleted file mode 100644 index 20d5e973..00000000 --- a/tests/artifact-guard.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import assert from "node:assert/strict" -import fs from "node:fs" -import path from "node:path" -import { fileURLToPath } from "node:url" -import { describe, test } from "node:test" - -const workflowPath = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "../.github/workflows/edgeone-artifact-guard.yml", -) -const workflow = fs.readFileSync(workflowPath, "utf8") - -describe("EdgeOne Artifact Guard workflow", () => { - test("uses the lockfile and never pushes generated artifacts", () => { - assert.match(workflow, /pnpm install --frozen-lockfile/) - assert.match(workflow, /contents: read/) - assert.doesNotMatch(workflow, /contents: write/) - assert.doesNotMatch(workflow, /git push origin/) - assert.doesNotMatch(workflow, /Auto-commit refreshed artifact/) - }) - - test("fails and uploads the refreshed artifact when it is stale", () => { - assert.match(workflow, /git status --porcelain -- cloud-functions/) - assert.match(workflow, /git diff --exit-code -- cloud-functions/) - assert.match(workflow, /if: failure\(\)/) - }) -}) From 6e3d3391dced619747ec03682fa4a9d49eafa2f3 Mon Sep 17 00:00:00 2001 From: Mcchen1008 Date: Fri, 9 Oct 2026 10:42:32 +0800 Subject: [PATCH 07/49] =?UTF-8?q?fix(test):=20=E7=A7=BB=E9=99=A4=20DbCiphe?= =?UTF-8?q?r=20=E9=87=8D=E5=A4=8D=E5=AF=BC=E5=85=A5=EF=BC=8C=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=20tsc=20=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一 import 语句中重复导入了 type DbCipher(第 5 行与第 14 行), 导致 pnpm lint(tsc --noEmit)报 TS2300 Duplicate identifier, 所有依赖类型检查的 CI 均被阻断。删除多余的一处导入即可。 Co-authored-by: GLM --- src/backend/internal/model/db_cipher.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/internal/model/db_cipher.test.ts b/src/backend/internal/model/db_cipher.test.ts index 196c2115..c8c8d6e9 100644 --- a/src/backend/internal/model/db_cipher.test.ts +++ b/src/backend/internal/model/db_cipher.test.ts @@ -11,7 +11,6 @@ import { encryptConfigValue, isSealedCiphertext, resolveDbCipher, - type DbCipher, } from "../../pkg/crypto" import { readCipher } from "./store/backend" import { memoryDriver } from "./store/driver/memory" From 8caa2ee7445f5126d321fb166abd057024508a14 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:18:29 +0800 Subject: [PATCH 08/49] fix(webdav): make PROPFIND href a spec-compliant Request-URI reference (#111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 Issue #104:标准 WebDAV 客户端挂载 /dav 后所有目录显示为空。 ## 规范依据(RFC 4918) §14.7 规定 DAV:href 的 Purpose 是「MUST contain a URI or a relative reference」,Value 为 Simple-ref;§8.3 给出产生式并附加三条约束: Simple-ref = absolute-URI | ( path-absolute [ "?" query ] ) - 形态二选一:相对引用(客户端按 Request-URI 解析)或完整 URI; - 同一个 Multi-Status 响应内所有 href 必须同形; - MUST NOT 有「与 Request-URI 前缀不匹配」的 href;集合标识符 SHOULD 以 / 结尾。 §8.3.1 的示例把两种合法形态并列给出: 'http://example.com/sample/' 和 'http://example.com/sample/a%20test' '/sample/' 和 '/sample/a%20test' 本实现取相对引用,基准是真实请求路径(URL.pathname)而非硬编码挂载前缀。 理由:与 Request-URI 天然一致(满足 MUST NOT)、子路径挂载自动正确、 且不回显 Host 头(避免 Host 注入把攻击者域名写进客户端解析结果)。 ## 修的三个缺陷 1. href 缺挂载前缀(issue 报告的)。davPathOf() 已剥掉前缀,PROPFIND 却把 这个已剥前缀的路径直接当 href 输出,于是请求 /dav/wewe 回 /wewe/, 客户端判定路径对不上并丢弃全部记录(rclone: Item with unknown path received)。表现为「能下载但列不出目录」,因为 GET 走 302 重定向不依赖 href。 2. 自身 href 是裸输出,会产出非法 XML(审计新发现,issue 未提)。 davPathOf() 已对 pathname 做过 decodeURIComponent,而自身 href 原样塞进 ;目录名含 & 时 & 未转义,客户端解析整个 multistatus 失败: PROPFIND /dav/R&D -> /R&D/ 非法 XML §8.3.1 正好提醒过这点:「a legal URI may still contain characters that need to be escaped within XML character data, such as the ampersand character.」 子项名此前已被 encodeURIComponent,自身 href 是唯一的裸输出口。 3. 集合与文件未区分尾斜杠。旧实现对子项一律不追加 /,目录 href 不带尾斜杠, Windows 资源管理器等要求目录以 / 结尾(§8.3 的 SHOULD)。 ## 实现 - 新增 davRequestPath(c) 取 URL.pathname 作为 href 基准,与 davPathOf(仅用于 存储层寻址)职责分离。 - generateWebDavXml(requestPath, items):自身 href 补斜杠;子项 href = 自身 + 逐段 encodeURIComponent(name) + (目录 ? "/" : "")。参考项目 Davflare 的 getResourceHref 采用同一口径。 编码分两路,不可混用: | | 来源 | 处理 | 原因 | | --- | --- | --- | --- | | 自身 href | URL.pathname,已百分号编码 | 只补编码 & 与 ' | 整条重编码会双重编码(%20 -> %2520) | | 子项 href | 解码后的原文 | 逐段 encodeURIComponent | 整条编码会把 / 变成 %2F,层级就没了 | 为什么自身 href 用百分号编码而不是 XML 实体(&):本仓库自己的 WebDAV 驱动(src/backend/drivers/webdav/util.ts 的 parseMultistatusXml)用正则取 href 文本且不做 XML 实体反转义,输出 & 会被它当成字面量。百分号编码同时满足两边: 既是合法 URI,又无需反转义,decodeURIComponent 后能还原原始路径。 实测 WHATWG URL 在 pathname 里已经编码掉 < > " ` 与空格,# ? 也不会出现, 裸 % 必然属于既有转义序列(用户输入的 % 会被编码成 %25),因此只补 & 与 ', 不做整条重新编码。 - 非法 UTF-16(孤立代理项)会让 encodeURIComponent 抛 URIError,逐字符兜底, 不因编码失败把整个 PROPFIND 打成 500。 - davPathOf 与 MOVE/COPY 的 Destination 解析改用前缀守卫(仅当 p === DAV_MOUNT || p.startsWith(DAV_MOUNT + "/") 才剥离)。不能像 slice(DAV_MOUNT.length) 那样无条件截断 —— 子路径挂载(/list/dav)下那会 截出 /t/dav/wewe 这样的垃圾路径,并违反 §8.3 的 MUST NOT。 - internal/webdav/webdav.ts 改为直接从 ../../pkg/xml 导入,不再经 pkg/utils barrel(后者会拉入 hono / model/db)。 ## 验证 新增 src/backend/pkg/xml_webdav.test.ts,14 个用例全部通过,覆盖根目录、 子目录、集合/文件尾斜杠差异、子路径挂载(/list/dav、/openlist/dav)、子项按段 编码、自身 href 不双重编码(断言无 %2520)、裸 & 与 apostrophe 的百分号编码、 输出不含任何 XML 实体、分隔符不编码成 %2F、孤立代理项不抛错、同一响应内 href 格式一致、XML 标签配平。 其中一项是互操作测试:模拟 parseMultistatusXml 的取名逻辑(正则取 href -> strip 尾斜杠 -> 取末段 -> decodeURIComponent),断言能从 /R%26D/、/a%26b/、/c%27d/ 正确还原出 R&D、a&b、c'd —— 即输出可被本仓库自己的 客户端解析器直接消费。 端到端对照三个挂载场景(Issue #104 真实场景 PROPFIND /dav/wewe): PROPFIND /dav/wewe 修复前: /wewe/ /wewe/WESSSDQ /wewe/70rop ... 前缀不一致 修复后: /dav/wewe/ /dav/wewe/WESSSDQ/ /dav/wewe/70rop/ ... 一致 PROPFIND /list/dav/wewe 修复后前缀一致 PROPFIND /openlist/dav/wewe 修复后前缀一致 运行:tsx --test src/backend/pkg/xml_webdav.test.ts (src/backend/pkg/ 目前未被任何 test:* 脚本覆盖,已在 #97 中补 test:pkg) ## 未改动(刻意保持 PR 聚焦) issue 补充提到的另外两点未混入: - MOVE/COPY 的 Destination base 推导:现状对绝对路径形式可正常工作,收紧 校验可能让部分客户端直接失败,需单独评估兼容性。 - OPTIONS 声明 DAV: 1, 2 但 LOCK 返回 405:能力声明与实现不一致,Office 会 因此拒绝写入;改声明会影响现有客户端的能力协商结果,宜单独提 PR。 Co-authored-by: wumingzhinu Co-authored-by: Claude Opus 4.8 (1M context) --- src/backend/internal/webdav/webdav.ts | 27 ++-- src/backend/pkg/xml.ts | 113 ++++++++++++-- src/backend/pkg/xml_webdav.test.ts | 207 ++++++++++++++++++++++++++ src/backend/server/webdav.ts | 58 ++++++-- 4 files changed, 367 insertions(+), 38 deletions(-) create mode 100644 src/backend/pkg/xml_webdav.test.ts diff --git a/src/backend/internal/webdav/webdav.ts b/src/backend/internal/webdav/webdav.ts index 5322871c..c6642319 100644 --- a/src/backend/internal/webdav/webdav.ts +++ b/src/backend/internal/webdav/webdav.ts @@ -1,12 +1,19 @@ -import { generateWebDavXml } from "../../pkg/utils" +import { generateWebDavXml, type WebDavItem } from "../../pkg/xml" -export interface WebDavItem { - name: string - size: number - isFolder: boolean - modified: string -} +export type { WebDavItem } -export function buildWebDavPropfindResponse(reqPath: string, items: WebDavItem[]): string { - return generateWebDavXml(reqPath, items) -} +/** + * 构造 PROPFIND 的 207 Multi-Status 响应体。 + * + * @param requestPath 被请求资源的请求路径(`URL.pathname`,已百分号编码)。 + * RFC 4918 §8.3 要求 与请求路径一致(§8.3.1 的示例允许相对引用或 + * 完整 URI 两种形态,本实现用相对引用),因此这里传请求路径而不是挂载前缀 —— + * 挂在子路径下(如 /list/dav)时也自动正确。 + * @param items 直接子项 + */ +export function buildWebDavPropfindResponse( + requestPath: string, + items: WebDavItem[], +): string { + return generateWebDavXml(requestPath, items) +} \ No newline at end of file diff --git a/src/backend/pkg/xml.ts b/src/backend/pkg/xml.ts index 565617bd..f881f93f 100644 --- a/src/backend/pkg/xml.ts +++ b/src/backend/pkg/xml.ts @@ -2,21 +2,110 @@ * XML generation utilities for OpenList protocols (WebDAV, S3). */ +/** + * 把请求路径里【URI 合法但在 XML 文本中有特殊含义】的字符改成百分号编码。 + * + * 背景:自身 href 取自 `URL.pathname`,它已经是合法的 URI —— 但合法不等于能 + * 直接写进 XML 文本。RFC 4918 §8.3.1 明确提醒过这点:「a legal URI may still + * contain characters that need to be escaped within XML character data, such + * as the ampersand character.」裸 `&` 会让 变成非法 XML,客户端解析 + * 整个 multistatus 失败。 + * + * 为什么用百分号编码而不是 XML 实体(`&`):本仓库自己的 WebDAV 驱动 + * (src/backend/drivers/webdav/util.ts 的 parseMultistatusXml)用正则取 href + * 文本,**不做 XML 实体反转义**,输出 `&` 会被它当成字面量。百分号编码 + * 同时满足两边:既是合法 URI,又无需反转义,decodeURIComponent 后还能还原成 + * 原始路径。 + * + * 为什么只处理这两个字符:WHATWG URL 在 pathname 里已经编码掉了 `< > " ` ` + * 与空格,`#` `?` 也不会出现在 pathname 中;而裸 `%` 必然是既有转义序列的 + * 一部分(用户输入的 `%` 会被编码成 `%25`),碰它会造成双重编码。因此这里 + * 只补 `&` 与 `'`,不做整条重新编码。 + */ +function encodeXmlSensitiveChars(requestPath: string): string { + return requestPath.replace(/[&']/g, (ch) => + ch === "&" ? "%26" : "%27", + ) +} + +/** + * 逐段百分号编码单个 WebDAV 路径段。 + * + * 为什么不能用 encodeDownloadPath:它按 Go 的 EncodePath 保留 `$&+,:;=@`, + * 其中 `&` 放进 XML 文本必须转义。这里用 encodeURIComponent,它会把 + * `& < > " ? # %` 空格与非 ASCII 全部编码。 + * + * 为什么必须逐段而不是整条 encodeURIComponent:整条会把分隔符 `/` 编码成 + * %2F,路径层级就没了。 + * + * 非法 UTF-16(孤立代理项)会让 encodeURIComponent 抛 URIError,因此 try + * 兜底,绝不因编码失败把整个 PROPFIND 打成 500。 + */ +function encodeDavPathSegment(segment: string): string { + try { + return encodeURIComponent(segment) + } catch { + return segment + } +} + +/** + * 集合 href 必须以 / 结尾(RFC 4918 §8.3:Identifiers for collections + * SHOULD end in a '/' character)。 + */ +function asCollectionHref(requestPath: string): string { + return requestPath.endsWith("/") ? requestPath : requestPath + "/" +} + +/** + * 构造子项 href:<自身 collection href> + 逐段编码的名称,目录再补一个 /。 + */ +export function buildDavChildHref( + collectionHref: string, + name: string, + isCollection: boolean, +): string { + return ( + encodeXmlSensitiveChars(asCollectionHref(collectionHref)) + + encodeDavPathSegment(name) + + (isCollection ? "/" : "") + ) +} + +export interface WebDavItem { + name: string + size: number + isFolder: boolean + modified: string +} + +/** + * 生成 PROPFIND 的 207 Multi-Status 响应体。 + * + * @param requestPath 被请求资源的**请求路径**(`URL.pathname`,已百分号编码)。 + * RFC 4918 §8.3 允许两种形态二选一:相对引用(path-absolute,客户端按 + * Request-URI 解析)或完整 absolute-URI;§8.3.1 的示例里 `'/sample/'` 与 + * `'http://example.com/sample/'` 都合法。本实现选**相对引用**,理由: + * 1. 与请求路径天然一致,满足 §8.3「MUST NOT have prefixes that do not + * match the Request-URI」; + * 2. 挂载在子路径(如 /list/dav)时自动正确,不必硬编码挂载点; + * 3. 不回显 Host 头,避免 Host 注入把攻击者域名写进客户端的解析结果。 + * §8.3 要求「同一个 Multi-Status 响应内所有 href 格式必须一致」,因此这里 + * 自身与子项统一用相对引用。 + * @param items 直接子项 + */ export function generateWebDavXml( - path: string, - items: Array<{ - name: string - size: number - isFolder: boolean - modified: string - }>, + requestPath: string, + items: WebDavItem[], ): string { + const selfHref = encodeXmlSensitiveChars(asCollectionHref(requestPath)) + let xml = `\n` xml += `\n` - // Current folder description + // 被请求的资源本身:PROPFIND 在本实现中总是列目录,故按集合输出 xml += ` \n` - xml += ` ${path}\n` + xml += ` ${selfHref}\n` xml += ` \n` xml += ` \n` xml += ` \n` @@ -26,9 +115,9 @@ export function generateWebDavXml( xml += ` \n` xml += ` \n` - // Children + // 直接子项 for (const item of items) { - const itemHref = `${path}${path.endsWith("/") ? "" : "/"}${encodeURIComponent(item.name)}` + const itemHref = buildDavChildHref(requestPath, item.name, item.isFolder) xml += ` \n` xml += ` ${itemHref}\n` xml += ` \n` @@ -52,4 +141,4 @@ export function generateWebDavXml( xml += `` return xml -} +} \ No newline at end of file diff --git a/src/backend/pkg/xml_webdav.test.ts b/src/backend/pkg/xml_webdav.test.ts new file mode 100644 index 00000000..ea7cefc3 --- /dev/null +++ b/src/backend/pkg/xml_webdav.test.ts @@ -0,0 +1,207 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { buildDavChildHref, generateWebDavXml } from "./xml" + +/** + * WebDAV PROPFIND 的 href 回归测试(Issue #110,修复 Issue #104)。 + * + * RFC 4918 §8.3 规定 href 有两种合法形态:相对引用(path-absolute,客户端按 + * Request-URI 解析)或完整 absolute-URI,§8.3.1 的示例里 + * `'/sample/'` 与 `'http://example.com/sample/'` 均合法;本实现选相对引用。 + * 同时要求:同一个 Multi-Status 内格式一致、href 前缀须与 Request-URI 一致、 + * 集合标识符应以 '/' 结尾。 + * + * 因此测试的基准一律是**真实请求路径**(`URL.pathname`,已百分号编码), + * 而不是硬编码的挂载前缀。 + */ + +const hrefs = (xml: string): string[] => + [...xml.matchAll(/([^<]*)<\/d:href>/g)].map((m) => m[1]) + +/** 取出 XML 解析后的真实 href(反转义),用于断言语义而非字面量 */ +function decodedHrefs(xml: string): string[] { + return hrefs(xml).map((h) => + h + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'"), + ) +} + +const collectionFlags = (xml: string): boolean[] => + xml + .split("") + .slice(1) + .map((b) => //.test(b)) + +test("根目录:href 与请求路径一致且以 / 结尾", () => { + const xml = generateWebDavXml("/dav/", []) + assert.deepEqual(hrefs(xml), ["/dav/"]) +}) + +test("子目录:自身与子项都带挂载前缀", () => { + // Issue #104 的真实场景:PROPFIND /dav/wewe,Depth: 1 + const xml = generateWebDavXml("/dav/wewe", [ + { name: "WESSSDQ", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "70rop", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "a.txt", size: 12, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + assert.deepEqual(hrefs(xml), [ + "/dav/wewe/", + "/dav/wewe/WESSSDQ/", + "/dav/wewe/70rop/", + "/dav/wewe/a.txt", + ]) +}) + +test("请求不带尾斜杠时,集合 href 补上斜杠", () => { + assert.deepEqual(hrefs(generateWebDavXml("/dav/wewe", [])), ["/dav/wewe/"]) + assert.deepEqual(hrefs(generateWebDavXml("/dav/wewe/", [])), ["/dav/wewe/"]) +}) + +test("目录带尾斜杠、文件不带", () => { + const xml = generateWebDavXml("/dav/dir", [ + { name: "sub", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "file.bin", size: 3, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + assert.deepEqual(collectionFlags(xml), [true, true, false]) + const [, dir, file] = hrefs(xml) + assert.equal(dir, "/dav/dir/sub/") + assert.equal(file, "/dav/dir/file.bin") + assert.ok(!file.endsWith("/"), "文件 href 不应有尾斜杠") +}) + +/** + * 挂在子路径下(如 https://host/list/dav)时 href 仍须与请求路径一致。 + * 这是把 href 基准从「硬编码挂载前缀」改成「真实请求路径」的原因: + * 前者会输出 /dav/list/dav/x,与 Request-URI 对不上,客户端直接丢弃记录。 + */ +test("子路径挂载时 href 仍与请求路径一致", () => { + for (const reqPath of ["/list/dav/wewe", "/openlist/dav/wewe", "/dav/wewe"]) { + const xml = generateWebDavXml(reqPath, [ + { name: "sub", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + ]) + for (const h of hrefs(xml)) { + assert.ok( + h === reqPath || h.startsWith(reqPath.endsWith("/") ? reqPath : reqPath + "/"), + `href 必须以请求路径 ${reqPath} 为前缀,实际: ${h}`, + ) + } + } +}) + +test("子项名按段百分号编码(来自解码后的原文)", () => { + const xml = generateWebDavXml("/dav/wewe", [ + { name: "来自:TOKEN订阅", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "a b&c.txt", size: 1, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + { name: "x#y?z.txt", size: 1, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + const [, dir, amp, hash] = hrefs(xml) + assert.equal(dir, "/dav/wewe/%E6%9D%A5%E8%87%AA%EF%BC%9ATOKEN%E8%AE%A2%E9%98%85/") + assert.equal(amp, "/dav/wewe/a%20b%26c.txt") + // # 与 ? 会被当成 fragment / query,必须编码 + assert.equal(hash, "/dav/wewe/x%23y%3Fz.txt") +}) + +/** + * 自身 href 来自 URL.pathname(已百分号编码),不能再整条编码,否则双重编码。 + * §8.3.1 提醒「合法 URI 里仍可能需要在 XML 字符数据里转义的字符(如 &)」—— + * 本实现对 & 与 ' 做百分号编码而非 XML 实体,原因见 encodeXmlSensitiveChars + * 的注释(本仓库自己的 parseMultistatusXml 不做实体反转义)。 + */ +test("自身 href 不双重编码,且裸 & 被百分号编码", () => { + // URL.pathname 已把 %20 / 非 ASCII 编码好 + const encodedPathname = new URL("https://h/dav/a%20b/%E4%B8%AD?x=1").pathname + const xml = generateWebDavXml(encodedPathname, []) + const h = hrefs(xml)[0] + assert.equal(h, "/dav/a%20b/%E4%B8%AD/") // 没有出现 %2520 + assert.ok(!h.includes("%25"), "不应出现双重编码") + + // & 是 URI 合法字符,但放进 XML 文本必须处理 + const ampPath = new URL("https://h/dav/R&D").pathname + assert.equal(ampPath, "/dav/R&D", "前提:& 在 pathname 中保持原样") + const xml2 = generateWebDavXml(ampPath, []) + assert.ok(!/&/.test(xml2), "输出中不应出现裸 &") + assert.ok(!/&(amp|lt|gt|quot|apos);/.test(xml2), "不应使用 XML 实体") + assert.equal(hrefs(xml2)[0], "/dav/R%26D/") + // decodeURIComponent 后应还原为原始路径 + assert.equal(decodeURIComponent(hrefs(xml2)[0].replace("/dav", "")), "/R&D/") +}) + +test("apostrophe 也被百分号编码(与 & 同类)", () => { + const p = new URL("https://h/dav/it's").pathname + assert.equal(p, "/dav/it's") + const xml = generateWebDavXml(p, []) + assert.ok(!/\x27|'/.test(hrefs(xml)[0]), "href 不应含裸 apostrophe") + assert.equal(decodeURIComponent(hrefs(xml)[0].replace("/dav", "")), "/it's/") +}) + +/** + * 互操作:本仓库自己的 WebDAV 驱动用正则取 href 文本且不做 XML 实体反转义, + * 因此输出必须不含任何实体,否则驱动侧会把 & 当成字面量名字。 + */ +test("输出不含任何 XML 实体,可被朴素正则解析器直接消费", () => { + const xml = generateWebDavXml(new URL("https://h/dav/R&D").pathname, [ + { name: "a&b", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "c'd", size: 0, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + assert.ok(!xml.includes("&"), "不得出现 &") + assert.ok(!xml.includes("<"), "不得出现 <") + assert.ok(!xml.includes("'"), "不得出现 '") + + // 模拟 parseMultistatusXml 的取名逻辑:strip 尾斜杠后取末段并 decodeURIComponent + const derived = decodedHrefs(xml).map((h) => { + const clean = h.replace(/\/+$/, "") + return decodeURIComponent(clean.split("/").pop() || "") + }) + assert.deepEqual(derived, ["R&D", "a&b", "c'd"]) +}) + +test("自身 href 中的 < 不会提前截断标签", () => { + // < 在 pathname 里会被编码成 %3C;但若上层传入未编码的 <,也不能破坏 XML + const xml = generateWebDavXml("/dav/a%3Cb", []) + assert.equal(hrefs(xml).length, 1, "href 不应被 < 截断") + assert.equal(hrefs(xml)[0], "/dav/a%3Cb/") +}) + +test("路径分隔符不被编码成 %2F", () => { + assert.equal(hrefs(generateWebDavXml("/dav/a/b/c", []))[0], "/dav/a/b/c/") + assert.equal( + buildDavChildHref("/dav/a/b/", "c", false), + "/dav/a/b/c", + ) +}) + +test("孤立代理项不抛错(encodeURIComponent 会抛 URIError)", () => { + // 损坏的存储元数据可能给出非法 UTF-16;不能因此把整个 PROPFIND 打成 500 + const xml = generateWebDavXml("/dav/\uD800", [ + { name: "\uDC00", size: 1, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + assert.ok(xml.includes(" { + const xml = generateWebDavXml("/dav/dir", [ + { name: "sub", size: 0, isFolder: true, modified: "2026-10-04T00:00:00Z" }, + { name: "f.txt", size: 1, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + // 全部是相对引用(以 / 开头),不得混入 absolute-URI(http://…) + for (const h of hrefs(xml)) { + assert.ok(h.startsWith("/"), `href 应为相对引用: ${h}`) + assert.ok(!/^https?:/i.test(h)) + } +}) + +test("XML 结构完整且标签配平", () => { + const xml = generateWebDavXml("/dav/x", [ + { name: "f", size: 5, isFolder: false, modified: "2026-10-04T00:00:00Z" }, + ]) + assert.ok(xml.startsWith('')) + assert.ok(xml.includes('')) + assert.ok(xml.trimEnd().endsWith("")) + assert.equal((xml.match(//g) || []).length, 2) + assert.equal((xml.match(/<\/d:response>/g) || []).length, 2) + assert.equal((xml.match(//g) || []).length, 2) +}) \ No newline at end of file diff --git a/src/backend/server/webdav.ts b/src/backend/server/webdav.ts index eefbcff8..3ef7d4d8 100644 --- a/src/backend/server/webdav.ts +++ b/src/backend/server/webdav.ts @@ -71,11 +71,32 @@ async function webdavAuth(c: any): Promise { return null } -/** 从 URL pathname 中剥离 /dav 前缀,得到虚拟文件路径 */ +/** WebDAV 挂载前缀。必须与 index.ts 里 `app.route("/dav", webdavRouter)` 一致。 */ +const DAV_MOUNT = "/dav" + +/** + * 取请求路径(已百分号编码),作为 href 的基准。 + * + * RFC 4918 §8.3:href 要么是相对引用(客户端按 Request-URI 解析),要么是完整 + * URI;§8.3.1 的示例里两者都合法。用相对引用时 href 必须与 Request-URI 前缀 + * 一致,因此基准取真实请求路径而非硬编码的挂载前缀 —— 挂在子路径下 + * (/list/dav)时也自动正确,且不回显 Host 头。 + */ +function davRequestPath(c: any): string { + return new URL(c.req.url).pathname +} + +/** 从 URL pathname 中剥离挂载前缀,得到虚拟文件路径(仅用于存储层寻址) */ function davPathOf(c: any): string { - const pathname = new URL(c.req.url).pathname - let p = pathname.replace(/^\/dav/, "") + const pathname = davRequestPath(c) + let p = pathname + // 只在前缀真正匹配时才剥离:不能像 slice(DAV_MOUNT.length) 那样无条件截断, + // 否则挂在子路径下会把 /list/dav/x 截成 /t/dav/x。 + if (p === DAV_MOUNT || p.startsWith(DAV_MOUNT + "/")) { + p = p.slice(DAV_MOUNT.length) + } if (!p) p = "/" + if (!p.startsWith("/")) p = "/" + p try { return decodeURIComponent(p) } catch { @@ -131,13 +152,10 @@ webdavRouter.all("/*", async (c) => { isFolder: !!it.is_dir, modified: it.modified || new Date().toISOString(), })) - const href = - davPath === "/" - ? "/" - : davPath.endsWith("/") - ? davPath - : davPath + "/" - const xml = buildWebDavPropfindResponse(href, items) + // davPathOf 已剥掉挂载前缀,那是给存储层寻址用的;href 必须反映真实请求 + // 路径。少了挂载前缀,rclone / Windows 资源管理器会因「返回的 href 与请求 + // 路径对不上」丢弃全部记录(能下载但列不出目录)。见 Issue #104。 + const xml = buildWebDavPropfindResponse(davRequestPath(c), items) return c.body(xml, depth === "0" ? 207 : 207, { "Content-Type": "application/xml; charset=utf-8", }) @@ -186,9 +204,13 @@ webdavRouter.all("/*", async (c) => { const destRaw = c.req.header("Destination") || "" let dest = destRaw try { - dest = decodeURIComponent( - new URL(destRaw, c.req.url).pathname, - ).replace(/^\/dav/, "") + // 与 davPathOf 同样的前缀守卫:Destination 未挂载在 DAV_MOUNT 下时 + // 原样保留,不做无意义的前缀改写 + dest = decodeURIComponent(new URL(destRaw, c.req.url).pathname) + if (dest === DAV_MOUNT || dest.startsWith(DAV_MOUNT + "/")) { + dest = dest.slice(DAV_MOUNT.length) || "/" + } + if (!dest.startsWith("/")) dest = "/" + dest } catch {} const src = splitPath(davPath) const dst = splitPath(dest) @@ -201,9 +223,13 @@ webdavRouter.all("/*", async (c) => { const destRaw = c.req.header("Destination") || "" let dest = destRaw try { - dest = decodeURIComponent( - new URL(destRaw, c.req.url).pathname, - ).replace(/^\/dav/, "") + // 与 davPathOf 同样的前缀守卫:Destination 未挂载在 DAV_MOUNT 下时 + // 原样保留,不做无意义的前缀改写 + dest = decodeURIComponent(new URL(destRaw, c.req.url).pathname) + if (dest === DAV_MOUNT || dest.startsWith(DAV_MOUNT + "/")) { + dest = dest.slice(DAV_MOUNT.length) || "/" + } + if (!dest.startsWith("/")) dest = "/" + dest } catch {} const src = splitPath(davPath) const dst = splitPath(dest) From e427f4baf6ea1be8cc34261cc877f4aebddf263d Mon Sep 17 00:00:00 2001 From: Mcchen1008 Date: Fri, 9 Oct 2026 21:20:42 +0800 Subject: [PATCH 09/49] =?UTF-8?q?fix(drivers):=20=E4=BF=AE=E5=A4=8D=20gith?= =?UTF-8?q?ub=5Freleases=20gh=5Fproxy=20=E6=9D=A1=E4=BB=B6=E5=86=99?= =?UTF-8?q?=E5=8F=8D=E5=AF=BC=E8=87=B4=E4=BB=A3=E7=90=86=E6=B0=B8=E4=B8=8D?= =?UTF-8?q?=E7=94=9F=E6=95=88=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxy() 原逻辑为 if (!p || url.startsWith("http")) return url, 而所有传入的 GitHub 下载 URL 均以 http 开头,导致 p + url 分支 永远不可达——配置了 gh_proxy 的用户直链仍直连 GitHub。 改为 if (p && url.startsWith("http")) return p + url,对齐上游 OpenList Go 版 drivers/github_releases 的 GhProxy 语义(代理非空 且 URL 为 http(s) 时加前缀)。附 3 个单元测试覆盖配置/未配置/ 非 http 链接三种情况。 Co-authored-by: GLM --- .../drivers/github_releases/util.test.ts | 37 +++++++++++++++++++ src/backend/drivers/github_releases/util.ts | 4 +- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/backend/drivers/github_releases/util.test.ts diff --git a/src/backend/drivers/github_releases/util.test.ts b/src/backend/drivers/github_releases/util.test.ts new file mode 100644 index 00000000..0fba3d72 --- /dev/null +++ b/src/backend/drivers/github_releases/util.test.ts @@ -0,0 +1,37 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { ClientGithubReleases } from "./util" + +function clientWith(ghProxy?: string): ClientGithubReleases { + return new ClientGithubReleases({ + repo_structure: "example/repo", + ...(ghProxy !== undefined ? { gh_proxy: ghProxy } : {}), + }) +} + +function proxyOf(additionGhProxy: string | undefined, url: string): string { + const client = clientWith(additionGhProxy) + // proxy() 为私有方法,测试中通过索引访问 + return (client as unknown as { proxy: (u: string) => string }).proxy(url) +} + +test("gh_proxy 已配置时为 http(s) 下载链接加代理前缀", () => { + const url = "https://github.com/example/repo/releases/download/v1.0.0/app.zip" + assert.equal( + proxyOf("https://mirror.example.com", url), + "https://mirror.example.com" + url, + ) +}) + +test("gh_proxy 未配置时保持原链接不变", () => { + const url = "https://github.com/example/repo/releases/download/v1.0.0/app.zip" + assert.equal(proxyOf(undefined, url), url) + assert.equal(proxyOf("", url), url) +}) + +test("gh_proxy 已配置但链接非 http 开头时不加前缀(对齐上游 Go 版语义)", () => { + assert.equal( + proxyOf("https://mirror.example.com", "/relative/path"), + "/relative/path", + ) +}) diff --git a/src/backend/drivers/github_releases/util.ts b/src/backend/drivers/github_releases/util.ts index d3c9ade6..2945ff50 100644 --- a/src/backend/drivers/github_releases/util.ts +++ b/src/backend/drivers/github_releases/util.ts @@ -61,8 +61,8 @@ export class ClientGithubReleases { private proxy(url: string): string { const p = this.addition.gh_proxy || "" - if (!p || url.startsWith("http")) return url - return p + url + if (p && url.startsWith("http")) return p + url + return url } /** 解析挂载点对应的 org/repo */ From 0e217b5bb55e58f9315703f630b334b46297f956 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:36:50 +0800 Subject: [PATCH 10/49] fix(storage): validate KV proxy health response (#79) Co-authored-by: wumingzhinu Co-authored-by: pikachuren --- src/backend/internal/model/store/driver/kv.ts | 23 ++++++++- src/backend/server/kv_proxy_loop.test.ts | 50 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/backend/internal/model/store/driver/kv.ts b/src/backend/internal/model/store/driver/kv.ts index 502a66ee..0e5bc4b2 100644 --- a/src/backend/internal/model/store/driver/kv.ts +++ b/src/backend/internal/model/store/driver/kv.ts @@ -188,7 +188,7 @@ function getProxyBaseUrl(env?: EnvContext): string { * KV 代理健康探测(唯一实现)。 * * isAvailable() 与 health() 共用,避免两处各自实现导致判定标准漂移。 - * 判定:HTTP 200 表示代理与 KV 均可用;401 表示代理可达但鉴权失败, + * 判定:HTTP 200 且返回合法 key 列表表示代理与 KV 均可用;401 表示代理可达但鉴权失败, * 属于「代理部署存在但密钥不对」。 * * 注意两个调用方对 401 的取舍不同,故这里只返回原始探测结果, @@ -221,7 +221,26 @@ async function probeProxy( method: "GET", headers: buildProxyHeaders(env), }) - return { ok: response.ok, status: response.status } + if (!response.ok) { + return { ok: false, status: response.status } + } + try { + const payload = await response.json() + if (!payload || !Array.isArray(payload.keys)) { + return { + ok: false, + status: response.status, + error: "KV proxy health response is not a valid key listing", + } + } + return { ok: true, status: response.status } + } catch { + return { + ok: false, + status: response.status, + error: "KV proxy health response is not valid JSON", + } + } } catch (err: any) { return { ok: false, status: 0, error: err?.message || String(err) } } diff --git a/src/backend/server/kv_proxy_loop.test.ts b/src/backend/server/kv_proxy_loop.test.ts index 1396cf1e..ea11f882 100644 --- a/src/backend/server/kv_proxy_loop.test.ts +++ b/src/backend/server/kv_proxy_loop.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict" import { test } from "node:test" import app from "../index" +import { kvDriver } from "../internal/model/store/driver/kv" /** * KV 代理端点的「自调用环」回归测试。 @@ -42,6 +43,55 @@ const kvEnv = (): any => ({ __requestOrigin: "https://example.workers.dev", }) +test("KV proxy health accepts a valid key listing", async () => { + const originalFetch = globalThis.fetch + ;(globalThis as any).fetch = async () => + new Response(JSON.stringify({ keys: [] }), { + status: 200, + headers: { "content-type": "application/json" }, + }) + try { + assert.equal(await kvDriver.isAvailable(kvEnv()), true) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("KV proxy health rejects an HTML 200 response", async () => { + const originalFetch = globalThis.fetch + ;(globalThis as any).fetch = async () => + new Response("", { status: 200 }) + try { + assert.equal(await kvDriver.isAvailable(kvEnv()), false) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("KV proxy health rejects a malformed key listing", async () => { + const originalFetch = globalThis.fetch + ;(globalThis as any).fetch = async () => + new Response(JSON.stringify({ keys: {} }), { status: 200 }) + try { + assert.equal(await kvDriver.isAvailable(kvEnv()), false) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("KV proxy keeps 401 available for diagnostics but unhealthy", async () => { + const originalFetch = globalThis.fetch + ;(globalThis as any).fetch = async () => + new Response("unauthorized", { status: 401 }) + try { + assert.equal(await kvDriver.isAvailable(kvEnv()), true) + const health = await kvDriver.health(kvEnv()) + assert.equal(health.available, false) + } finally { + globalThis.fetch = originalFetch + } +}) + test("KV 代理端点:本部署不提供 → 410,且处理它绝不再发起探测", async () => { const cap = captureFetch(503) try { From af70e8617b695dcf07ade3cf1015c8fb243480ef Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:36:42 +0800 Subject: [PATCH 11/49] fix(edgeone): restore server-handler detection via committed handler placeholders (#113) * fix(static): repair /manifest.json 401 on cookie-gated deployments (EdgeOne preview) Three stacked issues made /manifest.json the only failing request: 1. Frontend #162 (2025-08-08) repointed index.html from /static/manifest.json to /manifest.json to match the Go backend dynamic route (OpenList server/router.go:36), but the dist still only ships static/manifest.json - no root manifest.json exists. 2. The TS worker never aligned that route, so /manifest.json fell through to the SPA fallback (text/html on Cloudflare). 3. On cookie-gated platforms (EdgeOne Makers preview domains, eo_time), every other request carries the auth cookie while the PWA manifest fetch is made without any credentials by Chromium - so manifest.json alone got 401. Fix: - fetch-frontend.mjs: copy static/manifest.json -> manifest.json at build time (covers static-first platforms like EdgeOne Pages) and add crossorigin="use-credentials" to the manifest so the browser sends cookies; both idempotent, applied to every dist source path. - assets.ts: new ensureManifestCredentials() applied to all HTML served by getIndexHtmlWithCdn (covers CDN-sourced index.html, path B), plus a /manifest.json -> /static/manifest.json 302 route aligned with the Go router for worker-served traffic. - assets.test.ts: 5 new cases for the attribute patch and the CDN path. Verified: server suite 184/184, tsc --noEmit clean, fetch-frontend end-to-end produces dist/manifest.json and a patched index.html. * fix(edgeone): restore server-handler detection via committed handler placeholders EdgeOne Pages decides whether a project has a Node backend by scanning the repository content, not the post-build filesystem: with the handler artifact gitignored (PR #93), detection fails ("No server-handler detected, generating routes.json for pure project") and /api/* falls into the static rewrites returning index.html - while functions/kv-* keep working. - scripts/build-edge.mjs: emit the bundled handler to the officially documented node-functions/[[default]].js in addition to the historical cloud-functions/[[default]].js; the deploy-time build overwrites both with the fresh ~2MB bundle, so runtime always executes the latest code. - cloud-functions/[[default]].js + node-functions/[[default]].js: track a tiny, never-changing placeholder (valid onRequest export returning 503). It exists only so platform detection sees a server handler; the deploy build overwrites it with the real bundle on the build filesystem. Merge conflicts on artifacts are structurally impossible and contributors never need to touch these files. This supersedes both PR #93's gitignored-artifact approach and an interim full-artifact-committed one. - middleware.js: export config ({ matcher: [{ source: "/:path*" }] }) as the platform expects, silencing "Could not find config in middleware file, using defaults". - .github/workflows/build-verify.yml: assert the tracked entries are still the placeholder so accidentally committing a full build fails CI with a clear message. - .gitignore: stop ignoring the two handler entry files. Verified on a live EdgeOne deployment: /api/public/settings returns JSON again after deploying this change. --- .github/workflows/build-verify.yml | 33 +++++++++----- .gitignore | 10 ++-- cloud-functions/[[default]].js | 20 ++++++++ middleware.js | 7 +++ node-functions/[[default]].js | 20 ++++++++ scripts/build-edge.mjs | 13 ++++++ scripts/fetch-frontend.mjs | 73 ++++++++++++++++++++++++++++++ src/backend/server/assets.test.ts | 53 ++++++++++++++++++++++ src/backend/server/assets.ts | 53 ++++++++++++++++++++-- 9 files changed, 262 insertions(+), 20 deletions(-) create mode 100644 cloud-functions/[[default]].js create mode 100644 node-functions/[[default]].js diff --git a/.github/workflows/build-verify.yml b/.github/workflows/build-verify.yml index bad7efe6..c875d37c 100644 --- a/.github/workflows/build-verify.yml +++ b/.github/workflows/build-verify.yml @@ -1,16 +1,16 @@ name: Build Verify -# dist/、dist-server/、cloud-functions/[[default]].js 全部是构建产物,一律不入库: +# dist/、dist-server/ 是构建产物,一律不入库;cloud-functions/[[default]].js 与 +# node-functions/[[default]].js 是「EdgeOne server-handler 检测占位」(几行的小文件, +# 入库且基本不变),部署构建时由 scripts/build-edge.mjs 在同一路径生成完整产物覆盖: # - EdgeOne Makers:edgeone.json 的 buildCommand(pnpm run build)在部署时生成 -# dist/ 与 cloud-functions/[[default]].js +# dist/ 与两处完整产物 # - Cloudflare Workers:wrangler deploy 从 src/backend/worker.ts 打包,并用 dist/ 作静态资源 # - 自托管 Node:node dist-server/api/[...route].js 由 pnpm run build 产出 # # 因此"部署用的后端代码"完全取决于构建链路能否跑通,本工作流就是这条链路的看门人: -# 按平台一致的顺序安装、拉取前端、构建,并校验产物确实生成。任一步失败即失败。 -# -# 注意:产物按设计不提交(见 .gitignore),所以这里只校验"能否构建出来", -# 不再像已移除的 edgeone-artifact-guard.yml 那样比对已提交产物是否新鲜。 +# 按平台一致的顺序安装、拉取前端、构建,并校验产物确实生成;同时断言入库的 +# 两处入口仍是占位文件(防止误提交 2MB 构建产物)。任一步失败即失败。 on: push: @@ -82,10 +82,19 @@ jobs: test -f "cloud-functions/[[default]].js" echo "OK: dist/index.html、dist-server/api/[...route].js、cloud-functions/[[default]].js 均已生成" - - name: Assert the EdgeOne artifact is not tracked + # 入库的 cloud-functions/[[default]].js 与 node-functions/[[default]].js 是 + # EdgeOne server-handler 检测占位(见文件头注释)——平台按 Git 仓库内容 + # 判断项目是否含 Node 后端,真实产物由部署构建在同一路径生成并覆盖。 + # 这里断言仓库里跟踪的仍是占位文件:若有人把本地构建出的 2MB 完整产物 + # 提交进来,本检查会失败并提示还原为占位内容。 + - name: Assert tracked handler entries are the placeholder run: | - if git ls-files --error-unmatch "cloud-functions/[[default]].js" > /dev/null 2>&1; then - echo "::error::cloud-functions/[[default]].js 是构建产物,不应提交进仓库(见 .gitignore)。" - exit 1 - fi - echo "OK: cloud-functions/[[default]].js 未被 git 跟踪" + fail=0 + for f in "cloud-functions/[[default]].js" "node-functions/[[default]].js"; do + if ! git show "HEAD:$f" | head -1 | grep -q "PLACEHOLDER"; then + echo "::error::$f 在仓库里不是占位文件 —— 疑似误提交了构建产物。请还原为占位内容(产物由部署构建生成,不入库)。" + fail=1 + fi + done + if [ "$fail" -ne 0 ]; then exit 1; fi + echo "OK: 两处 server-handler 入口均为占位文件" diff --git a/.gitignore b/.gitignore index b7c4cb5f..d1d7d199 100644 --- a/.gitignore +++ b/.gitignore @@ -49,9 +49,13 @@ pages/dist/ dist-server/ *.tsbuildinfo -# EdgeOne Makers Node 云函数产物:由平台构建命令(pnpm run build -> scripts/build-edge.mjs) -# 在部署时生成,不入库;构建校验见 .github/workflows/build-verify.yml -cloud-functions/ +# EdgeOne server-handler 检测占位:cloud-functions/[[default]].js 与 +# node-functions/[[default]].js 以几行的占位文件入库(平台按 Git 仓库内容判断 +# 项目是否含 Node 后端;PR #93 实测仅靠构建期生成会报「No server-handler +# detected」退化为纯静态,/api/* 全部落静态)。部署构建时 scripts/build-edge.mjs +# 会在同一路径生成完整产物(~2MB,不入库)覆盖占位,EdgeOne 运行的是构建产物。 +# 占位内容基本永不变更 —— PR 中若出现这两个文件的大段变更,说明误提交了 +# 构建产物,请还原为占位内容。 # source maps (optional, can be removed) *.map diff --git a/cloud-functions/[[default]].js b/cloud-functions/[[default]].js new file mode 100644 index 00000000..54cc2909 --- /dev/null +++ b/cloud-functions/[[default]].js @@ -0,0 +1,20 @@ +// [PLACEHOLDER] EdgeOne server-handler 检测占位 —— 真实产物由部署构建生成,勿提交构建产物! +// +// EdgeOne Pages 平台按 Git 仓库内容检测本项目是否含 Node 后端(server-handler); +// 缺少本文件时构建日志会报「No server-handler detected, generating routes.json +// for pure project」,/api/* 会被 edgeone.json 的 rewrites 吞掉返回 index.html。 +// +// 因此这里入库一个几行的小占位(永不变更、永不产生 PR 冲突),部署时 +// edgeone.json -> pnpm run build -> scripts/build-edge.mjs 会在同一路径生成 +// 打包全部后端代码的完整产物(~2MB)并覆盖本文件,EdgeOne 运行的是那份构建产物。 +// +// 注意:本地执行 pnpm run build 后本文件会被覆盖为完整产物 —— 这属于预期行为, +// 不要把覆盖后的版本提交进来;PR 中若出现本文件的大段变更,请还原为占位内容。 +export async function onRequest(context) { + return new Response( + "openlist: backend bundle missing (built at deploy time, this is a placeholder)", + { status: 503, headers: { "content-type": "text/plain; charset=utf-8" } }, + ) +} + +export default onRequest diff --git a/middleware.js b/middleware.js index 901fcb34..1d674d53 100644 --- a/middleware.js +++ b/middleware.js @@ -12,6 +12,13 @@ // request / next / redirect / rewrite / geo / clientIp,**没有 env**, // 因此无法访问 KV 或环境变量。KV 代理请使用 functions/ 目录下的 // Edge Functions(见 functions/kv-get 等),那里才具备 KV 能力。 +// EdgeOne 约定 middleware.js 需显式导出 config 声明匹配范围;缺失时 CLI +// 报「Could not find config in middleware file, using defaults」并回退 +// 默认 matcher。这里显式声明与默认值一致的 /:path*,消除警告并锁定行为。 +export const config = { + matcher: [{ source: "/:path*" }], +} + export function middleware(context) { const { request, next, rewrite } = context const { pathname } = new URL(request.url) diff --git a/node-functions/[[default]].js b/node-functions/[[default]].js new file mode 100644 index 00000000..54cc2909 --- /dev/null +++ b/node-functions/[[default]].js @@ -0,0 +1,20 @@ +// [PLACEHOLDER] EdgeOne server-handler 检测占位 —— 真实产物由部署构建生成,勿提交构建产物! +// +// EdgeOne Pages 平台按 Git 仓库内容检测本项目是否含 Node 后端(server-handler); +// 缺少本文件时构建日志会报「No server-handler detected, generating routes.json +// for pure project」,/api/* 会被 edgeone.json 的 rewrites 吞掉返回 index.html。 +// +// 因此这里入库一个几行的小占位(永不变更、永不产生 PR 冲突),部署时 +// edgeone.json -> pnpm run build -> scripts/build-edge.mjs 会在同一路径生成 +// 打包全部后端代码的完整产物(~2MB)并覆盖本文件,EdgeOne 运行的是那份构建产物。 +// +// 注意:本地执行 pnpm run build 后本文件会被覆盖为完整产物 —— 这属于预期行为, +// 不要把覆盖后的版本提交进来;PR 中若出现本文件的大段变更,请还原为占位内容。 +export async function onRequest(context) { + return new Response( + "openlist: backend bundle missing (built at deploy time, this is a placeholder)", + { status: 503, headers: { "content-type": "text/plain; charset=utf-8" } }, + ) +} + +export default onRequest diff --git a/scripts/build-edge.mjs b/scripts/build-edge.mjs index e64577b7..897d5870 100644 --- a/scripts/build-edge.mjs +++ b/scripts/build-edge.mjs @@ -186,6 +186,19 @@ async function build() { plugins: [emptyNodeDriverPlugin, normalizeHtmlEolPlugin], }) + // EdgeOne Pages 官方约定的 Node Functions 目录是 node-functions/(平台 + // 文档的项目结构:node-functions/[[default]].js → catch-all,Node.js + // 运行时)。cloud-functions/ 是本项目沿用的历史目录名,部分 CLI 版本 + // 不扫描它 —— 日志表现为「No server-handler detected, generating + // routes.json for pure project」,整个项目被当成纯静态:/api/* 被 + // edgeone.json 的 rewrites 吞掉、返回 index.html。这里把产物复制一份 + // 到官方约定目录,两个位置并存,新旧识别逻辑都能命中。 + fs.mkdirSync("node-functions", { recursive: true }) + fs.copyFileSync( + "cloud-functions/[[default]].js", + "node-functions/[[default]].js", + ) + // 阿里云 ESA(边缘安全加速)边缘函数入口(仅在源文件存在时构建) if (fs.existsSync("esa-entry.ts")) { await esbuild.build({ diff --git a/scripts/fetch-frontend.mjs b/scripts/fetch-frontend.mjs index f3096115..ba22ecaa 100644 --- a/scripts/fetch-frontend.mjs +++ b/scripts/fetch-frontend.mjs @@ -130,10 +130,83 @@ function replaceDist(src) { console.log(` Copying frontend dist: ${src} -> ${DEST}`) fs.rmSync(DEST, { recursive: true, force: true }) fs.cpSync(src, DEST, { recursive: true }) + copyManifestToRoot() stampFrontendVersion(src) + patchManifestCredentials() console.log(`✓ Frontend dist ready (${DEST})`) } +/** + * 把 dist/static/manifest.json 复制到 dist/manifest.json。 + * + * 为什么:官方前端 #162(2025-08-08)把 index.html 的 manifest 引用从 + * /static/manifest.json 改成 /manifest.json —— 这是配合 Go 版后端的改动 + * (OpenList server/router.go:36 用动态路由 g.GET("/manifest.json", ...) + * 生成 manifest),但 dist 里文件仍停留在 /static/manifest.json。纯静态 + * /Worker 部署没有这条路由,/manifest.json 要么落到 SPA 兜底(返回 text/html, + * PWA 解析失败),要么在 EdgeOne 预览域这类平台鉴权后面直接 401。 + * 运行期 assetsRouter 的 /manifest.json -> /static/manifest.json 302 只是 + * Worker 场景的兜底;静态直出(EdgeOne Pages)根本不经过 Worker,必须在 + * 构建期把文件补到链接指向的位置。 + */ +function copyManifestToRoot() { + const src = path.join(DEST, "static", "manifest.json") + const dest = path.join(DEST, "manifest.json") + try { + if (!fs.existsSync(src) || fs.existsSync(dest)) return + fs.copyFileSync(src, dest) + console.log(" Copied static/manifest.json -> manifest.json (frontend #162)") + } catch (err) { + console.warn( + ` [fetch-frontend] copy manifest.json skipped: ${err?.message || err}`, + ) + } +} + +/** + * 给 dist/index.html 的 补 crossorigin="use-credentials"。 + * + * 为什么:Chromium 对 PWA manifest 的请求默认【不带任何凭证】——同域 Cookie + * 也不带(Google Web Fundamentals 明确记载)。EdgeOne Pages 预览域名 + * (*.edgeone.cool)等平台的访问控制依赖 Cookie 鉴权(eo_time),于是出现 + * 「站点其它资源全部正常,唯独 /manifest.json 401 Authorization Required」。 + * 补上该属性让浏览器以 include 模式携带 Cookie;同源请求没有 CORS 副作用, + * 在无需鉴权的平台上也无害。 + */ +function patchManifestCredentials() { + const idx = path.join(DEST, "index.html") + try { + if (!fs.existsSync(idx)) return + let html = fs.readFileSync(idx, "utf-8") + if (!/rel=["']manifest["']/i.test(html)) return + let changed = false + html = html.replace( + /]*)>/gi, + (full, attrs) => { + if (!/rel=["']manifest["']/i.test(attrs)) return full + if (/\bcrossorigin\b/i.test(attrs)) return full + changed = true + // 自闭合 /> 的斜杠不属于属性,摘出来还原到闭合符 + const selfClose = /\/\s*$/.test(attrs) + const clean = attrs + .replace(/\/\s*$/, "") + .replace(/\s+$/, "") + return `" : ">"}` + }, + ) + if (changed) { + fs.writeFileSync(idx, html) + console.log( + " Patched manifest link with crossorigin=use-credentials in dist/index.html", + ) + } + } catch (err) { + console.warn( + ` [fetch-frontend] patch manifest credentials skipped: ${err?.message || err}`, + ) + } +} + /** * 构建期戳:把前端版本号写入 dist/index.html 的 。 * diff --git a/src/backend/server/assets.test.ts b/src/backend/server/assets.test.ts index ec857b0b..5591aba8 100644 --- a/src/backend/server/assets.test.ts +++ b/src/backend/server/assets.test.ts @@ -5,6 +5,7 @@ import { resolveCdnUrl, injectCdnIntoHtml, getIndexHtmlWithCdn, + ensureManifestCredentials, parseFrontendVersion, extractEntryAsset, cdnAssetRedirect, @@ -245,6 +246,58 @@ test("injectCdnIntoHtml: CDN URL 中的引号 / 反斜杠被转义", () => { assert.equal(decoded, cdn, "字面量必须还原成原始 URL") }) +// --------------------------------------------------- ensureManifestCredentials + +const HTML_WITH_MANIFEST = ` + + + + Loading... + +
+` + +test("ensureManifestCredentials: 给 manifest link 补 crossorigin=use-credentials", () => { + const out = ensureManifestCredentials(HTML_WITH_MANIFEST) + assert.match( + out, + //, + ) +}) + +test("ensureManifestCredentials: 已有 crossorigin 属性时不重复添加", () => { + const html = HTML_WITH_MANIFEST.replace( + 'rel="manifest" />', + 'rel="manifest" crossorigin="anonymous" />', + ) + assert.equal(ensureManifestCredentials(html), html) +}) + +test("ensureManifestCredentials: 无 manifest 引用的 HTML 原样返回", () => { + const plain = "no manifest" + assert.equal(ensureManifestCredentials(plain), plain) +}) + +test("ensureManifestCredentials: 属性顺序在前(rel 后置)也能匹配", () => { + const html = HTML_WITH_MANIFEST.replace( + '', + '', + ) + const out = ensureManifestCredentials(html) + assert.match(out, /crossorigin="use-credentials">/) +}) + +test("getIndexHtmlWithCdn: CDN 的 index.html(路径 B)也被补上 use-credentials", async () => { + // CDN HTML 来自上游产物,构建期补丁覆盖不到,运行期必须兜底 + await withFetch(async () => { + const out = await getIndexHtmlWithCdn( + { ASSET_URLS: "https://cdn-b.example.com/dist" }, + LOCAL_HTML, + ) + assert.match(out, /rel="manifest" crossorigin="use-credentials"/) + }, cdnImpl({ asset: false, html: HTML_WITH_MANIFEST })) +}) + // ------------------------------------------------------------ getIndexHtmlWithCdn test("getIndexHtmlWithCdn: 未配置 ASSET_URLS 时返回本地 HTML", async () => { diff --git a/src/backend/server/assets.ts b/src/backend/server/assets.ts index 1edf29ce..86832d09 100644 --- a/src/backend/server/assets.ts +++ b/src/backend/server/assets.ts @@ -28,6 +28,15 @@ assetsRouter.get("/favicon.svg", redirectToLogo) assetsRouter.get("/favicon.png", redirectToLogo) assetsRouter.get("/favicon.ico", redirectToLogo) +// 对齐 Go 版 server/router.go:36 的 g.GET("/manifest.json", static.ManifestJSON): +// 官方前端 #162(2025-08-08)起 index.html 引用 /manifest.json,但 dist 里文件 +// 仍在 /static/manifest.json(Go 版由该动态路由生成,静态部署没有对应文件)。 +// 不处理时 /manifest.json 会落到 SPA 兜底:CF 上返回 text/html(PWA 解析失败), +// EdgeOne 预览域这类平台鉴权后面则表现为 manifest 请求 401。这里 302 到真实 +// 静态文件;构建期(fetch-frontend.mjs)同时会把文件复制到根,正常情况下 +// 静态层直接命中、根本不会走到这条路由。 +assetsRouter.get("/manifest.json", (c) => c.redirect("/static/manifest.json", 302)) + /** * 前端静态资源 CDN 注入。 * @@ -134,6 +143,39 @@ export async function resolveCdnUrl(env: any, html?: string): Promise { return raw.replace(/\$version/g, version) } +/** + * 确保 index.html 中 带 crossorigin="use-credentials"。 + * + * 为什么:Chromium 对 PWA manifest 的请求默认【不带任何凭证】——同域 Cookie + * 也不带(Google Web Fundamentals 明确记载,需用 crossorigin="use-credentials" + * 才会携带)。在 EdgeOne Pages 预览域名(*.edgeone.cool)这类「平台级 Cookie + * 鉴权」后面,JS/CSS/API 等其它请求都带 Cookie 正常返回,唯独 manifest.json + * 裸奔得到 401 Authorization Required,表现为 PWA 安装失败 / DevTools 里孤立 + * 的 manifest 401。补上该属性让浏览器以 include 模式携带 Cookie;同源请求 + * 没有 CORS 副作用,在无需鉴权的平台上也无害。 + * + * 双保险:scripts/fetch-frontend.mjs 在构建期已对 dist/index.html 打过同样的 + * 补丁(EdgeOne 静态直出走的正是那份文件);这里覆盖运行时从 CDN 拉取 + * index.html(路径 B)等绕过构建补丁的 HTML 来源。 + */ +export function ensureManifestCredentials(html: string): string { + // 没有 manifest 引用时原样返回,避免无谓的字符串拷贝 + if (!/rel=["']manifest["']/i.test(html)) return html + return html.replace( + /]*)>/gi, + (full, attrs: string) => { + if (!/rel=["']manifest["']/i.test(attrs)) return full + if (/\bcrossorigin\b/i.test(attrs)) return full + // 自闭合 /> 的斜杠不属于属性,摘出来还原到闭合符,避免产出 " / crossorigin=..." + const selfClose = /\/\s*$/.test(attrs) + const clean = attrs + .replace(/\/\s*$/, "") + .replace(/\s+$/, "") + return `" : ">"}` + }, + ) +} + /** * 把已解析的 CDN 地址注入 HTML 的 window.OPENLIST_CONFIG.cdn。 * 前端 vite-plugin-dynamic-base 读取 window.__dynamic_base__(= cdn), @@ -235,9 +277,10 @@ export async function getIndexHtmlWithCdn( localHtml: string, ): Promise { const cdn = await resolveCdnUrl(env, localHtml) - if (!cdn) return localHtml + if (!cdn) return ensureManifestCredentials(localHtml) // 仅允许 http(s),防止 ASSET_URLS 被配置成其它 scheme - if (!/^https?:\/\//i.test(cdn)) return localHtml + if (!/^https?:\/\//i.test(cdn)) + return ensureManifestCredentials(localHtml) // 供静态资源缺失时的 302 复用同一地址(见 cdnAssetRedirect) resolvedCdnCache.set(rawAssetUrls(env), cdn) const hit = cdnHtmlCache.get(cdn) @@ -247,7 +290,7 @@ export async function getIndexHtmlWithCdn( // 时把浏览器引到 404 上(那会比不注入更糟)。 const entry = extractEntryAsset(localHtml) if (entry && (await cdnHasAsset(cdn, entry))) { - const html = injectCdnIntoHtml(localHtml, cdn) + const html = ensureManifestCredentials(injectCdnIntoHtml(localHtml, cdn)) cdnHtmlCache.set(cdn, { html, ts: Date.now() }) return html } @@ -262,7 +305,7 @@ export async function getIndexHtmlWithCdn( let html = await res.text() // 校验确实是 HTML,而非 CDN 的 JSON 错误页(如 npmmirror 的 451 blocked) if (/ Date: Sun, 11 Oct 2026 12:38:57 +0800 Subject: [PATCH 12/49] feat(139): refresh expiring cloud tokens via PC API (#116) * feat(139): add PC token refresh * feat(139): add PC token refresh * feat(139): add PC token refresh * feat(139): add PC token refresh tests * feat(139): persist refreshed authorization * docs(139): explain cookie-free refresh limitation * docs(139): shorten omitted-cookie note --- src/backend/drivers/139/driver.test.ts | 88 ++++++++++++++++++++- src/backend/drivers/139/driver.ts | 7 +- src/backend/drivers/139/types.ts | 10 +++ src/backend/drivers/139/util.ts | 103 ++++++++++++++++++++++++- src/backend/internal/op/storage.ts | 23 +++++- 5 files changed, 226 insertions(+), 5 deletions(-) diff --git a/src/backend/drivers/139/driver.test.ts b/src/backend/drivers/139/driver.test.ts index ce40f82c..93c6abad 100644 --- a/src/backend/drivers/139/driver.test.ts +++ b/src/backend/drivers/139/driver.test.ts @@ -2,7 +2,7 @@ import test from "node:test" import assert from "node:assert/strict" import { Yun139Driver } from "./driver" import { Yun139Addition } from "./types" -import { calSign } from "./util" +import { calSign, Yun139ApiClient } from "./util" test("Yun139 calculation and signing", () => { const sign = calSign("{}", "2026-08-24 16:00:00", "1234567890abcdef") @@ -58,3 +58,89 @@ test("Yun139Driver instantiation and methods", async () => { const link = await driver.link("/photo.jpg", "/photo.jpg") assert.equal(link.url, "https://download.yun.139.com/photo.jpg") }) + + +test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { + const expiresAt = Date.now() + 5 * 24 * 60 * 60 * 1000 + const oldToken = `old-token|1|RCS|${expiresAt}|opaque` + const newToken = `new-token|1|RCS|${Date.now() + 30 * 24 * 60 * 60 * 1000}|opaque` + const account = "13800138000" + const addition: Yun139Addition = { + authorization: Buffer.from(`pc:${account}:${oldToken}`).toString("base64"), + user_domain_id: "test-user-domain", + type: "personal_new", + } + // Simulate a legacy config containing PC cookies; the refresh request must ignore them. + ;(addition as any).pc_cloud_cookies = "mc_at=must-not-send; mc_bt=must-not-send" + + const originalFetch = globalThis.fetch + const calls: Array<{ url: string; init?: RequestInit }> = [] + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + const url = String(input) + calls.push({ url, init }) + + if (url === "https://user-njs.yun.139.com/user/auth/refreshToken") { + return new Response( + JSON.stringify({ + success: true, + code: "0000", + message: "请求成功", + data: { token: newToken, expireTime: 2592000 }, + }), + { + status: 200, + headers: { + "content-type": "application/json", + }, + }, + ) + } + + if (url === "https://user-njs.yun.139.com/user/route/qryRoutePolicy") { + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { routePolicyList: [] }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const refreshedAuthorizations: string[] = [] + const client = new Yun139ApiClient(addition, async (authorization) => { + refreshedAuthorizations.push(authorization) + }) + await client.init() + + assert.equal(calls[0]?.url, "https://user-njs.yun.139.com/user/auth/refreshToken") + assert.deepEqual(JSON.parse(String(calls[0]?.init?.body)), { + userDomainId: "test-user-domain", + }) + + const sentHeaders = calls[0]?.init?.headers as Record + assert.equal(sentHeaders.authorization, `Basic ${Buffer.from(`pc:${account}:${oldToken}`).toString("base64")}`) + assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "cookie"), false) + assert.equal(calls[0]?.init?.credentials, "omit") + assert.equal(sentHeaders.app_cp, "1") + assert.equal(sentHeaders["x-yun-api-version"], "v1") + assert.equal(sentHeaders["x-yun-app-channel"], "10200153") + assert.equal(sentHeaders["x-yun-market-source"], "1") + assert.equal(sentHeaders["x-yun-module-type"], "1") + assert.equal(sentHeaders["x-yun-client-info"], "PC") + assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "x-yun-uni"), false) + assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "x-deviceinfo"), false) + + assert.equal( + Buffer.from(addition.authorization, "base64").toString("utf8"), + `pc:${account}:${newToken}`, + ) + assert.deepEqual(refreshedAuthorizations, [addition.authorization]) + } finally { + globalThis.fetch = originalFetch + } +}) diff --git a/src/backend/drivers/139/driver.ts b/src/backend/drivers/139/driver.ts index f5b24c47..70241b42 100644 --- a/src/backend/drivers/139/driver.ts +++ b/src/backend/drivers/139/driver.ts @@ -11,9 +11,12 @@ export class Yun139Driver implements StorageDriver { private addition: Yun139Addition private client: Yun139ApiClient - constructor(addition: Yun139Addition) { + constructor( + addition: Yun139Addition, + onAuthorizationRefresh?: (authorization: string) => Promise, + ) { this.addition = addition - this.client = new Yun139ApiClient(addition) + this.client = new Yun139ApiClient(addition, onAuthorizationRefresh) } async init(): Promise { diff --git a/src/backend/drivers/139/types.ts b/src/backend/drivers/139/types.ts index ebf676e9..4a98abe3 100644 --- a/src/backend/drivers/139/types.ts +++ b/src/backend/drivers/139/types.ts @@ -1,3 +1,13 @@ +export interface Yun139TokenRefreshResp { + success: boolean + code: string + message?: string + data?: { + token?: string + expireTime?: number + } +} + export interface Yun139Addition { authorization: string username?: string diff --git a/src/backend/drivers/139/util.ts b/src/backend/drivers/139/util.ts index 9e4eb4d0..b86b570c 100644 --- a/src/backend/drivers/139/util.ts +++ b/src/backend/drivers/139/util.ts @@ -9,6 +9,7 @@ import { PersonalListResp, PersonalDownloadResp, PersonalFileItem, + Yun139TokenRefreshResp, } from "./types" export function encodeURIComponentCustom(str: string): string { @@ -55,9 +56,14 @@ export class Yun139ApiClient { public familyHost = "https://yun.139.com" public groupHost = "https://yun.139.com" public account = "" + private onAuthorizationRefresh?: (authorization: string) => Promise - constructor(addition: Yun139Addition) { + constructor( + addition: Yun139Addition, + onAuthorizationRefresh?: (authorization: string) => Promise, + ) { this.addition = addition + this.onAuthorizationRefresh = onAuthorizationRefresh this.extractAccount() } @@ -104,6 +110,10 @@ export class Yun139ApiClient { } async request(uriOrUrl: string, body: any): Promise { + // Worker driver instances are cached; check at request time as well as init + // so a long-lived isolate cannot miss the refresh window. + await this.refreshAuthorizationIfNeeded() + const ts = formatTime(new Date()) const randStr = randomString(16) const bodyStr = JSON.stringify(body || {}) @@ -169,11 +179,102 @@ export class Yun139ApiClient { return json as T } + /** + * Refresh before the token enters its final 15 days. Called during init and + * before each API request because Worker driver instances may stay cached. + */ + private async refreshAuthorizationIfNeeded(): Promise { + const auth = this.getAuthString() + let decoded: string + try { + decoded = CryptoJS.enc.Base64.parse(auth).toString(CryptoJS.enc.Utf8) + } catch { + // Keep the previous behavior for non-standard authorization values. + return + } + + const pieces = decoded.split(":") + if (pieces.length < 3) return + + const token = pieces.slice(2).join(":") + const tokenFields = token.split("|") + const expiresAt = Number(tokenFields[3]) + if (!Number.isFinite(expiresAt) || expiresAt <= 0) return + + const refreshWindowMs = 15 * 24 * 60 * 60 * 1000 + if (expiresAt - Date.now() > refreshWindowMs) return + + const userDomainId = (this.addition.user_domain_id || "").trim() + if (!userDomainId) { + throw new Error( + "139 Cloud token is near expiry; configure user_domain_id to use the PC refreshToken API", + ) + } + + const authHeader = `Basic ${auth}` + + // Match the minimal header set from the successful PowerShell request. + // Do not send Cookie or unverified synthetic PC/device headers. + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + app_auth: authHeader, + app_cp: "1", + authorization: authHeader, + cp_version: "8.9.1.20260929", + "x-yun-api-version": "v1", + "x-yun-app-channel": "10200153", + "x-yun-op-type": "1", + "x-yun-svc-type": "1", + "x-yun-module-type": "1", + "x-yun-market-source": "1", + "x-yun-client-info": "PC", + } + + const response = await fetch( + "https://user-njs.yun.139.com/user/auth/refreshToken", + { + method: "POST", + // Omit captured cookies (mc_at, mc_bt, mc_pat, mc_pbt, mc_pac): + // refresh succeeded without them in our test; other sessions remain unverified. + credentials: "omit", + headers, + body: JSON.stringify({ userDomainId }), + }, + ) + + if (!response.ok) { + const body = await response.text() + throw new Error( + `139 Cloud PC token refresh failed (${response.status}): ${body}`, + ) + } + + const result = (await response.json()) as Yun139TokenRefreshResp + const newToken = result.data?.token?.trim() + if (!result.success || result.code !== "0000" || !newToken) { + throw new Error( + `139 Cloud PC token refresh failed: code=${result.code || "unknown"}, message=${result.message || "empty token"}`, + ) + } + const newExpiresAt = Number(newToken.split("|")[3]) + if (!Number.isFinite(newExpiresAt) || newExpiresAt <= Date.now()) { + throw new Error("139 Cloud PC refresh returned a token without a valid future expiry") + } + + this.addition.authorization = CryptoJS.enc.Base64.stringify( + CryptoJS.enc.Utf8.parse(`${pieces[0]}:${this.account}:${newToken}`), + ) + await this.onAuthorizationRefresh?.(this.addition.authorization) + } + async init(): Promise { if (!this.addition.authorization) { throw new Error("139 Cloud Authorization is required") } + await this.refreshAuthorizationIfNeeded() + try { const routeRes = await this.request( "https://user-njs.yun.139.com/user/route/qryRoutePolicy", diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index d2900a96..90b6c989 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -987,7 +987,28 @@ async function createDriver( normDriver.includes("139") ) { const addition = parseAddition(storageConfig) - driver = new Yun139Driver(addition) + driver = new Yun139Driver(addition, async (authorization) => { + try { + const db = await getDb() + const st = (db.storages || []).find( + (s: any) => s.id === storageConfig?.id, + ) + if (!st) return + const stAddition = + typeof st.addition === "string" + ? JSON.parse(st.addition || "{}") + : st.addition || {} + stAddition.authorization = authorization + st.addition = JSON.stringify(stAddition) + // Keep the in-flight admin update object in sync so it cannot overwrite + // the newly refreshed authorization with the previous value. + storageConfig.addition = st.addition + if (deferredTokenPersistence.has(storageConfig)) return + await saveDb(db) + } catch (e) { + console.warn("[139] failed to persist refreshed authorization:", e) + } + }) await driver.init?.() } else if ( normDriver === "mega" || From e1d7fa55cb2681e4bfbb9b7672fd1473b7e6e070 Mon Sep 17 00:00:00 2001 From: lenmei233 <1663996167@qq.com> Date: Sun, 11 Oct 2026 12:42:24 +0800 Subject: [PATCH 13/49] feat(drivers): add GitHub Releases and CNB Releases drivers (#108) - Normalize driver names to support both "GitHub Releases" and "CNB Releases". - Add driver configurations for GitHub Releases and CNB Releases, including required fields and settings. - Add unit tests for both drivers and integration coverage for admin registration and storage creation. - Preserve compatibility with existing driver info retrieval, supporting exact and Go-style driver names. Co-authored-by: DeepSeek Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> --- .../drivers/cnb_releases/driver.test.ts | 161 +++++++ src/backend/drivers/cnb_releases/driver.ts | 57 ++- src/backend/drivers/cnb_releases/util.ts | 43 +- .../drivers/github_releases/driver.test.ts | 353 +++++++++++++++ src/backend/drivers/github_releases/driver.ts | 25 +- src/backend/drivers/github_releases/types.ts | 8 +- src/backend/drivers/github_releases/util.ts | 413 +++++++++++------- src/backend/server/admin.ts | 148 ++++++- src/backend/server/driver_list.test.ts | 230 ++++++++++ 9 files changed, 1237 insertions(+), 201 deletions(-) create mode 100644 src/backend/drivers/cnb_releases/driver.test.ts create mode 100644 src/backend/drivers/github_releases/driver.test.ts create mode 100644 src/backend/server/driver_list.test.ts diff --git a/src/backend/drivers/cnb_releases/driver.test.ts b/src/backend/drivers/cnb_releases/driver.test.ts new file mode 100644 index 00000000..1118750e --- /dev/null +++ b/src/backend/drivers/cnb_releases/driver.test.ts @@ -0,0 +1,161 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { DriverCnbReleases } from "./driver" + +/** + * CNB Releases 驱动回归测试:API 按 release/asset ID 操作,而 TS 链路传路径名, + * 必须先解析出 ID(旧实现直接把路径段当 ID,进入 release 目录会 404)。 + */ + +const RELEASE = { + id: "rel-1", + name: "v1.0.0", + tag_name: "v1.0.0", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-02T00:00:00Z", + assets: [ + { + id: "asset-1", + name: "file.zip", + size: 5, + path: "/org/repo/-/releases/download/file.zip", + created_at: "2026-01-01T00:00:00Z", + updated_at: "2026-01-02T00:00:00Z", + }, + ], +} + +const ADDITION = { repo: "org/repo", token: "tkn" } + +function installFetch(): { calls: string[]; restore: () => void } { + const original = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: any, init?: any) => { + const url = String(input) + calls.push(`${init?.method || "GET"} ${url}`) + if (url.includes("/org/repo/-/releases") && url.includes("/assets/")) { + return new Response(null, { status: 204 }) + } + if (url.endsWith("/org/repo/-/releases")) { + return new Response(JSON.stringify([RELEASE]), { + status: 200, + headers: { "content-type": "application/json" }, + }) + } + if (url.includes("/org/repo/-/releases/")) { + return new Response(JSON.stringify(RELEASE), { + status: 200, + headers: { "content-type": "application/json" }, + }) + } + return new Response("", { status: 404 }) + }) as typeof fetch + return { + calls, + restore: () => { + globalThis.fetch = original + }, + } +} + +test("CNB Releases 驱动 init 校验 repo/token", async () => { + await assert.rejects( + () => new DriverCnbReleases({ repo: "", token: "" }).init(), + /repo is required/, + ) + await assert.rejects( + () => new DriverCnbReleases({ repo: "org/repo", token: "" }).init(), + /token is required/, + ) +}) + +test("根目录列出 releases(目录 sign 为 release ID)", async () => { + const { restore } = installFetch() + try { + const driver = new DriverCnbReleases(ADDITION) + const items = await driver.list("", "/") + assert.equal(items.length, 1) + assert.equal(items[0].name, "v1.0.0") + assert.equal(items[0].is_dir, true) + assert.equal(items[0].sign, "rel-1") + assert.equal(items[0].size, 5) + } finally { + restore() + } +}) + +test("按 release 名称进入目录并列出资产", async () => { + const { restore } = installFetch() + try { + const driver = new DriverCnbReleases(ADDITION) + const items = await driver.list("/v1.0.0", "/v1.0.0") + assert.equal(items.length, 1) + assert.equal(items[0].name, "file.zip") + assert.equal(items[0].is_dir, false) + assert.equal( + items[0].raw_url, + "https://cnb.cool/org/repo/-/releases/download/file.zip", + ) + } finally { + restore() + } +}) + +test("get:release 目录与资产文件都能按名称解析", async () => { + const { restore } = installFetch() + try { + const driver = new DriverCnbReleases(ADDITION) + + const dir = await driver.get("/v1.0.0", "/v1.0.0") + assert.equal(dir.is_dir, true) + assert.equal(dir.name, "v1.0.0") + + const file = await driver.get("/v1.0.0/file.zip", "/v1.0.0/file.zip") + assert.equal(file.is_dir, false) + assert.equal(file.name, "file.zip") + assert.equal( + file.raw_url, + "https://cnb.cool/org/repo/-/releases/download/file.zip", + ) + } finally { + restore() + } +}) + +test("remove:按名称解析出 release/asset ID 后再调用删除接口", async () => { + const { calls, restore } = installFetch() + try { + const driver = new DriverCnbReleases(ADDITION) + await driver.remove("", "/v1.0.0/file.zip", ["file.zip"]) + assert.ok( + calls.some( + (c) => + c.startsWith("DELETE") && + c.includes("/org/repo/-/releases/rel-1/assets/asset-1"), + ), + `未按 ID 调用删除接口: ${JSON.stringify(calls)}`, + ) + } finally { + restore() + } +}) + +test("use_tag_name 时目录名使用 tag 且不允许重命名", async () => { + const { restore } = installFetch() + try { + const driver = new DriverCnbReleases({ ...ADDITION, use_tag_name: true }) + const items = await driver.list("", "/") + assert.equal(items[0].name, "v1.0.0") + await assert.rejects( + () => driver.rename("", "/v1.0.0", "new"), + /only release name can be renamed/, + ) + } finally { + restore() + } +}) + +test("无状态环境下 put 直接报错", async () => { + const driver = new DriverCnbReleases(ADDITION) + await assert.rejects(() => driver.put(), /asset upload not supported/) +}) diff --git a/src/backend/drivers/cnb_releases/driver.ts b/src/backend/drivers/cnb_releases/driver.ts index 50c79511..77a4a97c 100644 --- a/src/backend/drivers/cnb_releases/driver.ts +++ b/src/backend/drivers/cnb_releases/driver.ts @@ -34,9 +34,9 @@ export class DriverCnbReleases implements StorageDriver { raw_url: "", })) } - // release 目录:assets - const releaseId = clean.split("/")[0] - const release = await this.client.getRelease(releaseId) + // release 目录:先按名称解析出 ID + const release = await this.client.findRelease(clean.split("/")[0]) + if (!release) return [] return release.assets.map((a) => ({ name: a.name, size: a.size, @@ -55,29 +55,33 @@ export class DriverCnbReleases implements StorageDriver { // release 目录 if (parts.length === 1) { - try { - const release = await this.client.getRelease(parts[0]) + const release = await this.client.findRelease(parts[0]) + if (release) { return { name: this.client.releaseName(release), size: release.assets.reduce((s, a) => s + a.size, 0), is_dir: true, - modified: release.updated_at || release.created_at || new Date().toISOString(), + modified: + release.updated_at || + release.created_at || + new Date().toISOString(), sign: release.id, type: 1, raw_url: "", } - } catch {} + } } // asset 文件 if (parts.length === 2) { - const release = await this.client.getRelease(parts[0]) - const asset = release.assets.find((a) => a.name === parts[1]) - if (asset) { + const release = await this.client.findRelease(parts[0]) + const asset = release ? this.client.findAsset(release, parts[1]) : null + if (release && asset) { return { name: asset.name, size: asset.size, is_dir: false, - modified: asset.updated_at || asset.created_at || new Date().toISOString(), + modified: + asset.updated_at || asset.created_at || new Date().toISOString(), sign: asset.id, type: calcFileType(asset.name, false), raw_url: this.client.assetDownloadUrl(asset), @@ -98,7 +102,10 @@ export class DriverCnbReleases implements StorageDriver { async mkdir(_virtualPath: string, physicalPath: string): Promise { const parts = physicalPath.split("/").filter(Boolean) if (parts.length === 1) { - await this.client.createRelease(parts[0], this.addition.default_branch || "main") + await this.client.createRelease( + parts[0], + this.addition.default_branch || "main", + ) return } throw new Error("[CNB Releases] only supports creating releases at root") @@ -111,8 +118,11 @@ export class DriverCnbReleases implements StorageDriver { ): Promise { const parts = physicalPath.split("/").filter(Boolean) if (parts.length === 1 && !this.addition.use_tag_name) { - await this.client.renameRelease(parts[0], newName) - return + const release = await this.client.findRelease(parts[0]) + if (release) { + await this.client.renameRelease(release.id, newName) + return + } } throw new Error("[CNB Releases] only release name can be renamed") } @@ -124,12 +134,19 @@ export class DriverCnbReleases implements StorageDriver { ): Promise { const parts = physicalPath.split("/").filter(Boolean) if (parts.length === 1) { - await this.client.deleteRelease(parts[0]) - return + const release = await this.client.findRelease(parts[0]) + if (release) { + await this.client.deleteRelease(release.id) + return + } } if (parts.length === 2) { - await this.client.deleteAsset(parts[0], parts[1]) - return + const release = await this.client.findRelease(parts[0]) + const asset = release ? this.client.findAsset(release, parts[1]) : null + if (release && asset) { + await this.client.deleteAsset(release.id, asset.id) + return + } } throw new Error("[CNB Releases] invalid path") } @@ -143,6 +160,8 @@ export class DriverCnbReleases implements StorageDriver { } async put(): Promise { - throw new Error("[CNB Releases] asset upload not supported in stateless environment") + throw new Error( + "[CNB Releases] asset upload not supported in stateless environment", + ) } } diff --git a/src/backend/drivers/cnb_releases/util.ts b/src/backend/drivers/cnb_releases/util.ts index dd549424..b6bb63f5 100644 --- a/src/backend/drivers/cnb_releases/util.ts +++ b/src/backend/drivers/cnb_releases/util.ts @@ -41,12 +41,10 @@ export class ClientCnbReleases { } } const resp = await fetch(`${API_BASE}${path}`, options) - if ( - resp.status !== 200 && - resp.status !== 201 && - resp.status !== 204 - ) { - throw new Error(`[CNB Releases] ${resp.status} ${await resp.text().catch(() => "")}`) + if (resp.status !== 200 && resp.status !== 201 && resp.status !== 204) { + throw new Error( + `[CNB Releases] ${resp.status} ${await resp.text().catch(() => "")}`, + ) } if (resp.status === 204) return {} as T return (await resp.json().catch(() => ({}))) as T @@ -61,6 +59,27 @@ export class ClientCnbReleases { return this.request("GET", `/${this.repo}/-/releases/${id}`) } + /** 路径里是 release 名称(TS 链路不传 Go 对象 ID),需先解析出 ID 再调 API */ + async findRelease(idOrName: string): Promise { + const releases = await this.listReleases().catch(() => [] as CNBRelease[]) + if (!Array.isArray(releases)) return null + return ( + releases.find((r) => r.id === idOrName) || + releases.find((r) => this.releaseName(r) === idOrName) || + releases.find((r) => r.tag_name === idOrName) || + null + ) + } + + /** 兼容按资产名称或 ID 查找 */ + findAsset(release: CNBRelease, idOrName: string): CNBAsset | null { + return ( + release.assets.find((a) => a.id === idOrName) || + release.assets.find((a) => a.name === idOrName) || + null + ) + } + releaseName(r: CNBRelease): string { return this.useTagName ? r.tag_name : r.name } @@ -74,7 +93,12 @@ export class ClientCnbReleases { } renameRelease(id: string, newName: string): Promise { - return this.request("PATCH", `/${this.repo}/-/releases/${id}`, { name: newName }, true) + return this.request( + "PATCH", + `/${this.repo}/-/releases/${id}`, + { name: newName }, + true, + ) } deleteRelease(id: string): Promise { @@ -82,7 +106,10 @@ export class ClientCnbReleases { } deleteAsset(releaseId: string, assetId: string): Promise { - return this.request("DELETE", `/${this.repo}/-/releases/${releaseId}/assets/${assetId}`) + return this.request( + "DELETE", + `/${this.repo}/-/releases/${releaseId}/assets/${assetId}`, + ) } assetDownloadUrl(asset: CNBAsset): string { diff --git a/src/backend/drivers/github_releases/driver.test.ts b/src/backend/drivers/github_releases/driver.test.ts new file mode 100644 index 00000000..96acf03e --- /dev/null +++ b/src/backend/drivers/github_releases/driver.test.ts @@ -0,0 +1,353 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { ClientGithubReleases } from "./util" +import { DriverGithubReleases } from "./driver" + +/** + * GitHub Releases 驱动回归测试(Issue #107):repo_structure 解析、根/嵌套挂载、 + * gh_proxy、show_all_version 目录与文件判定、per_page/max_page 分页。 + */ + +const release = (tag: string, assetName: string) => ({ + tag_name: tag, + created_at: "2026-01-01T00:00:00Z", + published_at: "2026-01-02T00:00:00Z", + html_url: `https://github.com/OpenListTeam/OpenList/releases/tag/${tag}`, + zipball_url: `https://github.com/OpenListTeam/OpenList/archive/refs/tags/${tag}.zip`, + tarball_url: `https://github.com/OpenListTeam/OpenList/archive/refs/tags/${tag}.tar.gz`, + assets: [ + { + name: assetName, + size: 1024, + created_at: "2026-01-02T00:00:00Z", + updated_at: "2026-01-02T00:00:00Z", + browser_download_url: `https://github.com/OpenListTeam/OpenList/releases/download/${tag}/${assetName}`, + }, + ], +}) + +const LATEST = release("v1.0.0", "openlist-linux-amd64.tar.gz") +const ALL = [LATEST, release("v0.9.0", "openlist-v0.9.0.tar.gz")] + +const CONTENTS = [ + { + name: "README.md", + size: 10, + download_url: + "https://raw.githubusercontent.com/OpenListTeam/OpenList/main/README.md", + }, + { + name: "LICENSE", + size: 20, + download_url: + "https://raw.githubusercontent.com/OpenListTeam/OpenList/main/LICENSE", + }, + { + name: "src", + size: 0, + download_url: + "https://raw.githubusercontent.com/OpenListTeam/OpenList/main/src", + }, +] + +type Routes = Array<[string, any]> + +/** 按 URL 包含关系匹配的 fetch mock;返回恢复函数 */ +function installFetch(routes: Routes): () => void { + const original = globalThis.fetch + globalThis.fetch = (async (input: any) => { + const url = String(input) + const match = routes.find(([key]) => url.includes(key)) + if (!match) return new Response("", { status: 404 }) + return new Response(JSON.stringify(match[1]), { + status: 200, + headers: { "content-type": "application/json" }, + }) + }) as typeof fetch + return () => { + globalThis.fetch = original + } +} + +const defaultRoutes: Routes = [ + ["/releases/latest", LATEST], + ["/releases?", ALL], + ["/contents", CONTENTS], +] + +test("repo_structure 必填,缺失时 init 报错", async () => { + const client = new ClientGithubReleases({}) + await assert.rejects(() => client.init(), /repo_structure is required/) +}) + +test("repo_structure 解析:单行 / 多行 / 分号", async () => { + const restore = installFetch(defaultRoutes) + try { + const root = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + }) + await root.init() + + // 挂载点根目录直接展示 release 资产(对齐 Go) + const files = await root.list("/") + assert.ok(files.some((f) => f.name === "openlist-linux-amd64.tar.gz")) + + // 多行(Go 写法)+ 分号(历史写法)都能解析出嵌套挂载点 + const multi = new ClientGithubReleases({ + repo_structure: "repos:OpenListTeam/OpenList\nother:OpenListTeam/Other", + }) + await multi.init() + const atRoot = await multi.list("/") + assert.deepEqual(atRoot.map((f) => f.name).sort(), ["other", "repos"]) + + // 分号分隔:根挂载点直接在 "/" 展示内容,嵌套挂载点合成为目录 "a" + const semicolon = new ClientGithubReleases({ + repo_structure: "a:OpenListTeam/OpenList;OpenListTeam/Other", + }) + await semicolon.init() + const second = await semicolon.list("/") + const secondNames = second.map((f) => f.name) + assert.ok(secondNames.includes("a")) + assert.ok(secondNames.includes("openlist-linux-amd64.tar.gz")) + } finally { + restore() + } +}) + +test("非法 repo_structure(多个冒号)直接报错", () => { + assert.throws( + () => new ClientGithubReleases({ repo_structure: "x:y:z" }), + /invalid format/, + ) +}) + +test("挂载点根目录列出最新版本资产 + README/LICENSE + 源码包", async () => { + const restore = installFetch(defaultRoutes) + try { + const client = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_readme: true, + show_source_code: true, + }) + await client.init() + + const files = await client.list("/") + const names = files.map((f) => f.name).sort() + assert.deepEqual(names, [ + "LICENSE", + "README.md", + "Source code (tar.gz)", + "Source code (zip)", + "openlist-linux-amd64.tar.gz", + ]) + + const asset = files.find((f) => f.name === "openlist-linux-amd64.tar.gz")! + assert.equal( + asset.url, + "https://github.com/OpenListTeam/OpenList/releases/download/v1.0.0/openlist-linux-amd64.tar.gz", + ) + // raw.githubusercontent.com 不是 github.com 前缀,不应被改写 + const readme = files.find((f) => f.name === "README.md")! + assert.equal( + readme.url, + "https://raw.githubusercontent.com/OpenListTeam/OpenList/main/README.md", + ) + assert.equal(await client.isDirectory("/"), true) + assert.equal(await client.isDirectory("/README.md"), false) + } finally { + restore() + } +}) + +test("gh_proxy 只替换 github.com 前缀(对齐 Go Link)", async () => { + const restore = installFetch(defaultRoutes) + try { + const client = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_readme: true, + show_source_code: true, + gh_proxy: "https://ghproxy.net/https://github.com", + }) + await client.init() + + const files = await client.list("/") + const asset = files.find((f) => f.name === "openlist-linux-amd64.tar.gz")! + assert.equal( + asset.url, + "https://ghproxy.net/https://github.com/OpenListTeam/OpenList/releases/download/v1.0.0/openlist-linux-amd64.tar.gz", + ) + const zip = files.find((f) => f.name === "Source code (zip)")! + assert.equal( + zip.url, + "https://ghproxy.net/https://github.com/OpenListTeam/OpenList/archive/refs/tags/v1.0.0.zip", + ) + const readme = files.find((f) => f.name === "README.md")! + assert.equal( + readme.url, + "https://raw.githubusercontent.com/OpenListTeam/OpenList/main/README.md", + ) + } finally { + restore() + } +}) + +test("嵌套挂载点:逐级合成目录并进入挂载点根目录", async () => { + const restore = installFetch(defaultRoutes) + try { + const client = new ClientGithubReleases({ + repo_structure: "repos:OpenListTeam/OpenList", + }) + await client.init() + + const root = await client.list("/") + assert.deepEqual( + root.map((f) => f.name), + ["repos"], + ) + assert.equal(root[0].isDir, true) + + assert.equal(await client.isDirectory("/repos"), true) + const mountRoot = await client.list("/repos") + assert.ok(mountRoot.some((f) => f.name === "openlist-linux-amd64.tar.gz")) + + const url = await client.getDownloadUrl( + "/repos/openlist-linux-amd64.tar.gz", + ) + assert.equal( + url, + "https://github.com/OpenListTeam/OpenList/releases/download/v1.0.0/openlist-linux-amd64.tar.gz", + ) + } finally { + restore() + } +}) + +test("show_all_version:版本目录、目录判定与版本内文件下载", async () => { + const restore = installFetch(defaultRoutes) + try { + const client = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_all_version: true, + show_readme: true, + show_source_code: true, + }) + await client.init() + + const root = await client.list("/") + const dirs = root + .filter((f) => f.isDir) + .map((f) => f.name) + .sort() + assert.deepEqual(dirs, ["v0.9.0", "v1.0.0"]) + assert.equal(await client.isDirectory("/v1.0.0"), true) + assert.equal(await client.isDirectory("/not-a-tag"), false) + + const tag = await client.list("/v1.0.0") + const names = tag.map((f) => f.name).sort() + assert.deepEqual(names, [ + "Source code (tar.gz)", + "Source code (zip)", + "openlist-linux-amd64.tar.gz", + ]) + + // 版本目录内的文件不能被误判为目录 + const fileUrl = await client.getDownloadUrl( + "/v1.0.0/openlist-linux-amd64.tar.gz", + ) + assert.equal( + fileUrl, + "https://github.com/OpenListTeam/OpenList/releases/download/v1.0.0/openlist-linux-amd64.tar.gz", + ) + const driver = new DriverGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_all_version: true, + }) + const item = await driver.get( + "/v1.0.0/openlist-linux-amd64.tar.gz", + "/v1.0.0/openlist-linux-amd64.tar.gz", + ) + assert.equal(item.is_dir, false) + assert.match(item.raw_url || "", /releases\/download\/v1\.0\.0/) + } finally { + restore() + } +}) + +test("per_page / max_page 控制分页拉取(对齐 Go getAllReleases)", async () => { + const calls: string[] = [] + const original = globalThis.fetch + const all = [release("v3", "a"), release("v2", "b"), release("v1", "c")] + globalThis.fetch = (async (input: any) => { + const url = String(input) + calls.push(url) + if (url.includes("/releases?")) { + const parsed = new URL(url) + const page = Number(parsed.searchParams.get("page") || "1") + const perPage = Number(parsed.searchParams.get("per_page") || "30") + const slice = all.slice((page - 1) * perPage, page * perPage) + return new Response(JSON.stringify(slice), { + status: 200, + headers: { "content-type": "application/json" }, + }) + } + if (url.includes("/releases/latest")) { + return new Response(JSON.stringify(LATEST), { + status: 200, + headers: { "content-type": "application/json" }, + }) + } + return new Response(JSON.stringify([]), { + status: 200, + headers: { "content-type": "application/json" }, + }) + }) as typeof fetch + try { + const client = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_all_version: true, + per_page: 2, + max_page: 2, + show_readme: false, + }) + await client.init() + const files = await client.list("/") + assert.deepEqual(files.map((f) => f.name).sort(), ["v1", "v2", "v3"]) + assert.ok( + calls.some((u) => u.includes("per_page=2") && u.includes("page=1")), + ) + assert.ok( + calls.some((u) => u.includes("per_page=2") && u.includes("page=2")), + ) + assert.ok(!calls.some((u) => u.includes("page=3"))) + + calls.length = 0 + const limited = new ClientGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + show_all_version: true, + per_page: 1, + max_page: 1, + show_readme: false, + }) + await limited.init() + const limitedFiles = await limited.list("/") + assert.deepEqual( + limitedFiles.map((f) => f.name), + ["v3"], + ) + assert.ok(!calls.some((u) => u.includes("page=2"))) + } finally { + globalThis.fetch = original + } +}) + +test("驱动为只读:写操作全部抛错", async () => { + const driver = new DriverGithubReleases({ + repo_structure: "OpenListTeam/OpenList", + }) + await assert.rejects(() => driver.mkdir(), /read-only/) + await assert.rejects(() => driver.put(), /read-only/) + await assert.rejects(() => driver.remove(), /read-only/) + await assert.rejects(() => driver.move(), /read-only/) + await assert.rejects(() => driver.copy(), /read-only/) + await assert.rejects(() => driver.rename(), /read-only/) +}) diff --git a/src/backend/drivers/github_releases/driver.ts b/src/backend/drivers/github_releases/driver.ts index 8c966a5e..2cf7ad81 100644 --- a/src/backend/drivers/github_releases/driver.ts +++ b/src/backend/drivers/github_releases/driver.ts @@ -34,13 +34,9 @@ export class DriverGithubReleases implements StorageDriver { async get(_virtualPath: string, physicalPath: string): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts[parts.length - 1] || "root" - const files = await this.client.list(physicalPath) - // 目录:list 成功返回非空或目录项 - if (files.length > 0 || physicalPath === "/") { - const dir = files.find((f) => f.isDir && f.name === name) - if (!dir && files.some((f) => !f.isDir)) { - // 可能是文件 - } + + // 目录判定按路径结构;不能用 list() 非空判断(版本目录内的文件会被误判) + if (await this.client.isDirectory(physicalPath)) { return { name, size: 0, @@ -51,19 +47,8 @@ export class DriverGithubReleases implements StorageDriver { raw_url: "", } } - // 文件:获取下载链接 + const url = await this.client.getDownloadUrl(physicalPath) - if (url) { - return { - name, - size: 0, - is_dir: false, - modified: new Date().toISOString(), - sign: "", - type: calcFileType(name, false), - raw_url: url, - } - } return { name, size: 0, @@ -71,7 +56,7 @@ export class DriverGithubReleases implements StorageDriver { modified: new Date().toISOString(), sign: "", type: calcFileType(name, false), - raw_url: "", + raw_url: url, } } diff --git a/src/backend/drivers/github_releases/types.ts b/src/backend/drivers/github_releases/types.ts index 0d8955fd..be3df51b 100644 --- a/src/backend/drivers/github_releases/types.ts +++ b/src/backend/drivers/github_releases/types.ts @@ -2,7 +2,9 @@ // API reference: Go drivers/github_releases export interface DriverGithubReleasesAddition { - /** 仓库结构:[path:]org/repo,多个用分号分隔 */ + /** 根文件夹路径(由 resolvePath 通用处理,默认 "/") */ + root_folder_path?: string + /** 仓库结构:[path:]org/repo,每行一个(兼容分号分隔) */ repo_structure?: string /** 显示 README/LICENSE */ show_readme?: boolean @@ -12,6 +14,10 @@ export interface DriverGithubReleasesAddition { show_source_code?: boolean /** 显示所有版本 */ show_all_version?: boolean + /** show_all_version 时每页拉取的 releases 数(默认 30,最大 100) */ + per_page?: number | string + /** show_all_version 时最多拉取的页数(0 = 不限制) */ + max_page?: number | string /** GitHub 代理前缀 */ gh_proxy?: string } diff --git a/src/backend/drivers/github_releases/util.ts b/src/backend/drivers/github_releases/util.ts index 2945ff50..4f218f51 100644 --- a/src/backend/drivers/github_releases/util.ts +++ b/src/backend/drivers/github_releases/util.ts @@ -1,4 +1,4 @@ -// GitHub Releases API 客户端 +// GitHub Releases API 客户端(行为对齐 Go drivers/github_releases) import { DriverGithubReleasesAddition, GHRMountPoint, @@ -8,40 +8,78 @@ import { } from "./types" const API_BASE = "https://api.github.com" +const GITHUB_PREFIX = "https://github.com" + +function normalizePath(p: string): string { + return ( + "/" + + String(p ?? "") + .split("/") + .filter(Boolean) + .join("/") + ) +} + +function joinPath(...parts: string[]): string { + return normalizePath(parts.filter(Boolean).join("/")) +} + +function clampNumber( + value: unknown, + fallback: number, + min: number, + max: number, +): number { + const num = Math.floor(Number(value)) + if (!Number.isFinite(num) || num <= 0) return fallback + return Math.min(max, Math.max(min, num)) +} + +function releaseSize(release: GHRRelease): number { + return release.assets.reduce((sum, asset) => sum + (asset.size || 0), 0) +} export class ClientGithubReleases { private addition: DriverGithubReleasesAddition private mounts: GHRMountPoint[] constructor(addition: DriverGithubReleasesAddition) { - this.addition = addition - this.mounts = this.parseMounts(addition.repo_structure || "") + this.addition = addition || ({} as DriverGithubReleasesAddition) + this.mounts = this.parseMounts(this.addition.repo_structure || "") } + /** + * 解析 repo_structure:每行一个 [path:]org/repo(兼容分号分隔), + * 省略路径时挂载到根目录;出现第二个冒号视为非法(对齐 Go ParseRepos)。 + */ private parseMounts(structure: string): GHRMountPoint[] { const out: GHRMountPoint[] = [] - for (const raw of structure.split(";")) { + for (const raw of String(structure || "").split(/[\r\n;]+/)) { const item = raw.trim() if (!item) continue const idx = item.indexOf(":") - if (idx > 0 && item.slice(0, idx).includes("/")) { - // 可能是 [path:]org/repo 或 org/repo(无冒号) + if (idx < 0) { + out.push({ point: "/", repo: item }) + continue } - let point = "/" - let repo = item - if (idx > 0 && !item.slice(idx + 1).startsWith("/") && !item.slice(0, idx).includes(".")) { - // 形如 mymount:org/repo - point = "/" + item.slice(0, idx).replace(/^\/+|\/+$/g, "") - repo = item.slice(idx + 1) + if (item.indexOf(":", idx + 1) >= 0) { + throw new Error(`[GitHub Releases] invalid format: ${item}`) } - out.push({ point: point === "/" ? "/" : point, repo }) + const path = item.slice(0, idx).trim() + const repo = item.slice(idx + 1).trim() + if (!repo) { + throw new Error(`[GitHub Releases] invalid format: ${item}`) + } + out.push({ point: normalizePath(path), repo }) } return out } async init(): Promise { if (this.mounts.length === 0) { - throw new Error("[GitHub Releases] repo_structure is required (format: [path:]org/repo)") + throw new Error( + "[GitHub Releases] repo_structure is required (format: [path:]org/repo)", + ) } } @@ -50,7 +88,9 @@ export class ClientGithubReleases { Accept: "application/vnd.github+json", "User-Agent": "OpenList-TSWorker", } - if (this.addition.token) headers["Authorization"] = `Bearer ${this.addition.token}` + if (this.addition.token) { + headers["Authorization"] = `Bearer ${this.addition.token}` + } const resp = await fetch(`${API_BASE}${path}`, { headers }) if (resp.status === 404) return null as any if (resp.status >= 400) { @@ -59,35 +99,56 @@ export class ClientGithubReleases { return (await resp.json().catch(() => null)) as T } + /** gh_proxy 只替换 https://github.com 前缀(对齐 Go Link),其它域名原样返回 */ private proxy(url: string): string { - const p = this.addition.gh_proxy || "" - if (p && url.startsWith("http")) return p + url - return url + const prefix = String(this.addition.gh_proxy || "").trim() + if (!prefix || !url || !url.startsWith(GITHUB_PREFIX)) return url + return prefix + url.slice(GITHUB_PREFIX.length) } - /** 解析挂载点对应的 org/repo */ + /** 取路径所属的挂载点:最长匹配,根挂载点 "/" 匹配任意路径 */ private mountForPath(virtualPath: string): GHRMountPoint | null { - const clean = virtualPath.startsWith("/") ? virtualPath : "/" + virtualPath - // 找最长的匹配挂载点 + const clean = normalizePath(virtualPath) let best: GHRMountPoint | null = null - for (const m of this.mounts) { - const mp = m.point === "/" ? "/" : m.point - if (clean === mp || clean.startsWith(mp + "/")) { - if (!best || mp.length > (best.point === "/" ? 0 : best.point.length)) { - best = m - } + let bestLen = -1 + for (const mount of this.mounts) { + const point = mount.point === "/" ? "" : mount.point + if (point !== "" && clean !== point && !clean.startsWith(point + "/")) { + continue + } + if (point.length > bestLen) { + best = mount + bestLen = point.length } } return best } + /** 相对挂载点的路径;"" 表示挂载点根目录 */ + private relPath(mount: GHRMountPoint, virtualPath: string): string { + const clean = normalizePath(virtualPath) + const point = mount.point === "/" ? "" : mount.point + return clean.slice(point.length).replace(/^\/+/, "") + } + private async latestRelease(repo: string): Promise { return this.get(`/repos/${repo}/releases/latest`) } + /** 对齐 Go getAllReleases:per_page 默认 30(上限 100),max_page 为 0 时不限页数 */ private async allReleases(repo: string): Promise { - const r = await this.get(`/repos/${repo}/releases`) - return r || [] + const perPage = clampNumber(this.addition.per_page, 30, 1, 100) + const maxPage = Math.max(0, Math.floor(Number(this.addition.max_page) || 0)) + const out: GHRRelease[] = [] + for (let page = 1; maxPage === 0 || page <= maxPage; page++) { + const releases = await this.get( + `/repos/${repo}/releases?per_page=${perPage}&page=${page}`, + ) + if (!releases || releases.length === 0) break + out.push(...releases) + if (releases.length < perPage) break + } + return out } private async repoFiles(repo: string): Promise { @@ -95,166 +156,216 @@ export class ClientGithubReleases { return r || [] } - async list(virtualPath: string): Promise { - const mount = this.mountForPath(virtualPath) - if (!mount) return [] - const rel = this.relPath(mount, virtualPath) + private assetFiles(prefix: string, release: GHRRelease): GHRFile[] { + return release.assets.map((asset) => ({ + path: joinPath(prefix, asset.name), + name: asset.name, + size: asset.size, + isDir: false, + modified: + asset.updated_at || asset.created_at || new Date().toISOString(), + url: this.proxy(asset.browser_download_url), + })) + } + + private sourceCodeFiles(prefix: string, release: GHRRelease): GHRFile[] { + if (!this.addition.show_source_code) return [] + return [ + { + path: joinPath(prefix, "Source code (zip)"), + name: "Source code (zip)", + size: 1, + isDir: false, + modified: release.created_at, + url: this.proxy(release.zipball_url), + }, + { + path: joinPath(prefix, "Source code (tar.gz)"), + name: "Source code (tar.gz)", + size: 1, + isDir: false, + modified: release.created_at, + url: this.proxy(release.tarball_url), + }, + ] + } + + private readmeFiles(prefix: string, contents: GHRFileInfo[]): GHRFile[] { + const out: GHRFile[] = [] + for (const f of contents) { + if ( + f.name.toLowerCase().endsWith(".md") || + f.name.toUpperCase().startsWith("LICENSE") + ) { + out.push({ + path: joinPath(prefix, f.name), + name: f.name, + size: f.size, + isDir: false, + modified: "1970-01-01T00:00:00Z", + url: this.proxy(f.download_url), + }) + } + } + return out + } + /** 挂载点根目录:最新版本资产(或全部版本目录)+ README + 源码包 */ + private async mountContent(point: string, repo: string): Promise { const files: GHRFile[] = [] - // 根目录:挂载点列表 - if (rel === "" && virtualPath === "/") { - for (const m of this.mounts) { + if (this.addition.show_all_version) { + for (const release of await this.allReleases(repo)) { files.push({ - path: m.point, - name: m.point === "/" ? m.repo : m.point.split("/").filter(Boolean).pop() || m.repo, - size: 0, + path: joinPath(point, release.tag_name), + name: release.tag_name, + size: releaseSize(release), isDir: true, - modified: new Date().toISOString(), + modified: release.published_at || release.created_at, url: "", }) } - return files + } else { + const release = await this.latestRelease(repo) + if (release) { + files.push(...this.assetFiles(point, release)) + files.push(...this.sourceCodeFiles(point, release)) + } } - const isAllVersion = this.addition.show_all_version + if (this.addition.show_readme !== false) { + const contents = await this.repoFiles(repo).catch(() => []) + files.push(...this.readmeFiles(point, contents)) + } + return files + } - if (rel === "") { - // 挂载点根:最新版本文件 + 可选源码 + README/LICENSE - const release = await this.latestRelease(mount.repo) - if (release) { - for (const asset of release.assets) { - files.push({ - path: join(mount.point, asset.name), - name: asset.name, - size: asset.size, - isDir: false, - modified: asset.updated_at || asset.created_at || new Date().toISOString(), - url: this.proxy(asset.browser_download_url), - }) - } - if (this.addition.show_source_code) { - files.push({ - path: join(mount.point, "Source code (zip)"), - name: "Source code (zip)", - size: 1, - isDir: false, - modified: release.created_at, - url: this.proxy(release.zipball_url), - }) - files.push({ - path: join(mount.point, "Source code (tar.gz)"), - name: "Source code (tar.gz)", - size: 1, - isDir: false, - modified: release.created_at, - url: this.proxy(release.tarball_url), - }) + async list(virtualPath: string): Promise { + const clean = normalizePath(virtualPath) + const files: GHRFile[] = [] + + for (const mount of this.mounts) { + const point = normalizePath(mount.point) + + // 挂载点根目录 + if (point === clean) { + try { + files.push(...(await this.mountContent(point, mount.repo))) + } catch (e) { + // 单个仓库失败只告警,不影响其它挂载点(对齐 Go) + console.warn(`[GitHub Releases] failed to list ${mount.repo}:`, e) } + continue } - if (isAllVersion) { - const releases = await this.allReleases(mount.repo) - for (const r of releases) { + + // 祖先目录:合成下一级目录 + if ( + point !== "/" && + point.startsWith(clean === "/" ? "/" : clean + "/") + ) { + const prefix = clean === "/" ? "/" : clean + "/" + const name = point.slice(prefix.length).split("/")[0] + if (name && !files.some((f) => f.name === name)) { files.push({ - path: join(mount.point, r.tag_name), - name: r.tag_name, + path: joinPath(clean, name), + name, size: 0, isDir: true, - modified: r.published_at || r.created_at, - url: r.html_url, + modified: new Date().toISOString(), + url: "", }) } + continue } - if (this.addition.show_readme !== false) { - const contents = await this.repoFiles(mount.repo) - for (const f of contents) { - if (f.name.toLowerCase().endsWith(".md") || f.name.toUpperCase().startsWith("LICENSE")) { - files.push({ - path: join(mount.point, f.name), - name: f.name, - size: f.size, - isDir: false, - modified: "1970-01-01T00:00:00Z", - url: this.proxy(f.download_url), - }) + + // 版本目录(仅 show_all_version 时存在) + if ( + this.addition.show_all_version && + (point === "/" || clean.startsWith(point + "/")) + ) { + const rel = + point === "/" ? clean.slice(1) : clean.slice(point.length + 1) + if (rel && !rel.includes("/")) { + try { + const releases = await this.allReleases(mount.repo) + const release = releases.find((r) => r.tag_name === rel) + if (release) { + files.push(...this.assetFiles(clean, release)) + files.push(...this.sourceCodeFiles(clean, release)) + } + } catch (e) { + console.warn(`[GitHub Releases] failed to list version ${rel}:`, e) } } } - return files } - // 版本目录内:该 tag 的 assets + 源码 - const releases = await this.allReleases(mount.repo) - const release = releases.find((r) => r.tag_name === rel.split("/")[0]) - if (release) { - for (const asset of release.assets) { - files.push({ - path: join(mount.point, release.tag_name, asset.name), - name: asset.name, - size: asset.size, - isDir: false, - modified: asset.updated_at || asset.created_at || new Date().toISOString(), - url: this.proxy(asset.browser_download_url), - }) - } - if (this.addition.show_source_code) { - files.push({ - path: join(mount.point, "Source code (zip)"), - name: "Source code (zip)", - size: 1, - isDir: false, - modified: release.created_at, - url: this.proxy(release.zipball_url), - }) - files.push({ - path: join(mount.point, "Source code (tar.gz)"), - name: "Source code (tar.gz)", - size: 1, - isDir: false, - modified: release.created_at, - url: this.proxy(release.tarball_url), - }) - } - } return files } - /** 获取单文件下载链接 */ + /** 仅挂载点、合成的祖先目录与 show_all_version 下的版本目录是目录 */ + async isDirectory(virtualPath: string): Promise { + const clean = normalizePath(virtualPath) + if (clean === "/") return true + + for (const mount of this.mounts) { + const point = normalizePath(mount.point) + if (clean === point) return true + if (point !== "/" && point.startsWith(clean + "/")) return true + } + + if (!this.addition.show_all_version) return false + + const mount = this.mountForPath(clean) + if (!mount) return false + const rel = this.relPath(mount, clean) + if (!rel || rel.includes("/")) return false + const releases = await this.allReleases(mount.repo).catch(() => []) + return releases.some((r) => r.tag_name === rel) + } + + /** 获取单文件下载链接:版本目录 → 最新版本 → 其它版本 → README/LICENSE */ async getDownloadUrl(virtualPath: string): Promise { const mount = this.mountForPath(virtualPath) if (!mount) return "" const rel = this.relPath(mount, virtualPath) - const name = rel.split("/").pop() || "" + const segs = rel.split("/").filter(Boolean) + const name = segs[segs.length - 1] || "" + if (!name) return "" - // 最新版本资产 - const release = await this.latestRelease(mount.repo) - if (release) { - const asset = release.assets.find((a) => a.name === name) + const releases = await this.allReleases(mount.repo).catch(() => []) + if (segs.length >= 2) { + const tagged = releases.find((r) => r.tag_name === segs[0]) + if (tagged) { + const asset = tagged.assets.find((a) => a.name === name) + if (asset) return this.proxy(asset.browser_download_url) + const source = this.sourceCodeUrl(name, tagged) + if (source) return this.proxy(source) + } + } + + const latest = await this.latestRelease(mount.repo).catch(() => null) + if (latest) { + const asset = latest.assets.find((a) => a.name === name) if (asset) return this.proxy(asset.browser_download_url) + const source = this.sourceCodeUrl(name, latest) + if (source) return this.proxy(source) } - // 所有版本资产 - const releases = await this.allReleases(mount.repo) - for (const r of releases) { - const asset = r.assets.find((a) => a.name === name) + + for (const release of releases) { + const asset = release.assets.find((a) => a.name === name) if (asset) return this.proxy(asset.browser_download_url) } - // README/LICENSE - const contents = await this.repoFiles(mount.repo) - const f = contents.find((c) => c.name === name) - if (f) return this.proxy(f.download_url) + + const contents = await this.repoFiles(mount.repo).catch(() => []) + const match = contents.find((c) => c.name === name) + if (match) return this.proxy(match.download_url) return "" } - private relPath(mount: GHRMountPoint, virtualPath: string): string { - const mp = mount.point === "/" ? "/" : mount.point - const clean = virtualPath.startsWith("/") ? virtualPath : "/" + virtualPath - if (mp === "/") return clean.replace(/^\/+/, "") - const rel = clean.slice(mp.length).replace(/^\/+/, "") - return rel + private sourceCodeUrl(name: string, release: GHRRelease): string { + if (name === "Source code (zip)") return release.zipball_url + if (name === "Source code (tar.gz)") return release.tarball_url + return "" } } - -function join(...parts: string[]): string { - const clean = parts.filter(Boolean).map((p) => p.replace(/^\/+|\/+$/g, "")).filter(Boolean) - return "/" + clean.join("/") -} diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index a23583a5..1b7eb451 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -169,7 +169,10 @@ export const normalizeDriver = (driverName: string): string => { ].includes(norm) ) return "S3" - if (norm.startsWith("github")) return "Github" + // releases 别名必须在通用 github 别名之前,否则会被归一到 GitHub API + if (norm.startsWith("githubrelease")) return "GitHub Releases" + if (norm.startsWith("cnbrelease")) return "CNB Releases" + if (norm.startsWith("github")) return "GitHub API" if (norm === "local") return "Local" if (norm.includes("pikpak")) return "PikPak" if (norm.includes("seafile")) return "Seafile" @@ -537,6 +540,8 @@ adminRouter.get("/driver/names", (c) => { "GuangYaPan", "AutoIndex", "ProtonDrive", + "GitHub Releases", + "CNB Releases", ], }) }) @@ -1497,6 +1502,141 @@ const driverConfigs: Record = { default_root: "/", }, }, + // Issue #107:驱动已实现(internal/op/storage.ts)但此处漏配,前端存储列表 + // 因此看不到「GitHub Releases」;字段对齐 Go meta 与前端 i18n。 + "GitHub Releases": { + name: "GitHub Releases", + default_mount_path: "/github_releases", + common: COMMON_FIELDS, + additional: [ + { + name: "root_folder_path", + type: "string", + default: "/", + required: false, + }, + { + name: "repo_structure", + type: "text", + default: "OpenListTeam/OpenList", + required: true, + help: "structure:[path:]org/repo", + }, + { + name: "show_readme", + type: "bool", + default: "true", + required: false, + help: "show README、LICENSE file", + }, + { + name: "token", + type: "string", + default: "", + required: false, + help: "GitHub token, if you want to access private repositories or increase the rate limit", + }, + { + name: "show_source_code", + type: "bool", + default: "false", + required: false, + help: "show Source code (zip/tar.gz)", + }, + { + name: "show_all_version", + type: "bool", + default: "false", + required: false, + help: "show all versions", + }, + { + name: "per_page", + type: "number", + default: "30", + required: false, + help: "releases per page (max 100), only works when show all versions", + }, + { + name: "max_page", + type: "number", + default: "0", + required: false, + help: "max pages to fetch (0 = unlimited), only works when show all versions", + }, + { + name: "gh_proxy", + type: "string", + default: "", + required: false, + help: "GitHub proxy, e.g. https://ghproxy.net/https://github.com", + }, + ], + config: { + name: "GitHub Releases", + // 对齐 Go:只读发布源(NoUpload) + local_sort: false, + only_local: false, + only_proxy: false, + no_cache: false, + no_upload: true, + need_ms: false, + default_root: "", + }, + }, + // 同一处遗漏:CNB Releases 驱动同样未登记。 + "CNB Releases": { + name: "CNB Releases", + default_mount_path: "/cnb_releases", + common: COMMON_FIELDS, + additional: [ + { + name: "root_folder_id", + type: "string", + default: "", + required: false, + }, + { + name: "repo", + type: "string", + default: "", + required: true, + help: "CNB 仓库,形如 org/repo", + }, + { + name: "token", + type: "string", + default: "", + required: true, + help: "CNB 访问令牌", + }, + { + name: "use_tag_name", + type: "bool", + default: "false", + required: false, + help: "Use tag name instead of release name", + }, + { + name: "default_branch", + type: "string", + default: "main", + required: false, + help: "Default branch for new releases", + }, + ], + config: { + name: "CNB Releases", + local_sort: true, + only_local: false, + only_proxy: false, + no_cache: false, + // Go 版支持上传;TS 无状态实现 put 直接报错,隐藏上传入口 + no_upload: true, + need_ms: false, + default_root: "", + }, + }, Thunder: { name: "Thunder", default_mount_path: "/thunder", @@ -4263,7 +4403,11 @@ adminRouter.get("/driver/list", (c) => { adminRouter.get("/driver/info", (c) => { const driverName = c.req.query("driver") || "" - const info = driverConfigs[driverName] || driverConfigs["AliyundriveOpen"] + // 兼容 Go 导入名(如 GithubReleases):精确未命中时按别名归一化再回退 + const info = + driverConfigs[driverName] || + driverConfigs[normalizeDriver(driverName)] || + driverConfigs["AliyundriveOpen"] return c.json({ code: 200, message: "success", diff --git a/src/backend/server/driver_list.test.ts b/src/backend/server/driver_list.test.ts new file mode 100644 index 00000000..3050e7cd --- /dev/null +++ b/src/backend/server/driver_list.test.ts @@ -0,0 +1,230 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Hono } from "hono" +import { adminRouter } from "./admin" +import { listItems } from "../internal/op/storage" +import { saveDb } from "../internal/model/db" + +/** + * Issue #107 回归:GitHub Releases / CNB Releases 必须出现在后台驱动列表中; + * 覆盖 /driver/list、/driver/names、/driver/info 与创建存储后的端到端列目录。 + */ + +const authHeaders = { Authorization: "Bearer admin-token" } + +async function setupEnv(): Promise { + const env: any = {} + await saveDb( + { + settings: [{ key: "token", value: "admin-token" }], + users: [], + storages: [], + shares: [], + metas: [], + }, + env, + ) + return env +} + +function makeApp(): Hono { + const app = new Hono() + app.route("/api/admin", adminRouter) + return app +} + +test("driver/list 包含 GitHub Releases 与 CNB Releases", async () => { + const env = await setupEnv() + const res = await makeApp().request( + "/api/admin/driver/list", + { method: "GET", headers: authHeaders }, + env, + ) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.equal(body.code, 200) + + const ghr = body.data["GitHub Releases"] + assert.ok(ghr, "GitHub Releases 必须出现在驱动列表中") + assert.equal(ghr.name, "GitHub Releases") + assert.equal(ghr.default_mount_path, "/github_releases") + assert.equal(ghr.config.no_upload, true) + assert.equal(ghr.config.local_sort, false) + const ghrFields = ghr.additional.map((f: any) => f.name) + for (const field of [ + "root_folder_path", + "repo_structure", + "show_readme", + "token", + "show_source_code", + "show_all_version", + "per_page", + "max_page", + "gh_proxy", + ]) { + assert.ok(ghrFields.includes(field), `GitHub Releases 缺少字段 ${field}`) + } + const repoStructure = ghr.additional.find( + (f: any) => f.name === "repo_structure", + ) + assert.equal(repoStructure.required, true) + assert.equal(repoStructure.default, "OpenListTeam/OpenList") + + const cnb = body.data["CNB Releases"] + assert.ok(cnb, "CNB Releases 必须出现在驱动列表中") + assert.equal(cnb.name, "CNB Releases") + assert.equal(cnb.default_mount_path, "/cnb_releases") + const cnbFields = cnb.additional.map((f: any) => f.name) + for (const field of [ + "root_folder_id", + "repo", + "token", + "use_tag_name", + "default_branch", + ]) { + assert.ok(cnbFields.includes(field), `CNB Releases 缺少字段 ${field}`) + } +}) + +test("driver/names 包含 GitHub Releases 与 CNB Releases", async () => { + const env = await setupEnv() + const res = await makeApp().request( + "/api/admin/driver/names", + { method: "GET", headers: authHeaders }, + env, + ) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.ok(body.data.includes("GitHub Releases")) + assert.ok(body.data.includes("CNB Releases")) +}) + +test("driver/info 支持精确名与 Go 风格名(GithubReleases/大小写)", async () => { + const env = await setupEnv() + const app = makeApp() + + const exact = await app.request( + `/api/admin/driver/info?driver=${encodeURIComponent("GitHub Releases")}`, + { method: "GET", headers: authHeaders }, + env, + ) + const exactBody: any = await exact.json() + assert.equal(exactBody.data.name, "GitHub Releases") + + // Go 导入名(无空格)也要命中,编辑页依赖同一份配置 + const goStyle = await app.request( + "/api/admin/driver/info?driver=GithubReleases", + { method: "GET", headers: authHeaders }, + env, + ) + const goStyleBody: any = await goStyle.json() + assert.equal(goStyleBody.data.name, "GitHub Releases") + + const cnb = await app.request( + "/api/admin/driver/info?driver=cnb_releases", + { method: "GET", headers: authHeaders }, + env, + ) + const cnbBody: any = await cnb.json() + assert.equal(cnbBody.data.name, "CNB Releases") +}) + +test("通过 API 创建 GitHub Releases 存储并可列出资源(端到端)", async () => { + const env = await setupEnv() + const app = makeApp() + + const create = await app.request( + "/api/admin/storage/create", + { + method: "POST", + headers: { ...authHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ + driver: "GitHub Releases", + mount_path: "/ghr", + addition: JSON.stringify({ repo_structure: "OpenListTeam/OpenList" }), + }), + }, + env, + ) + assert.equal(create.status, 200) + const created: any = await create.json() + assert.equal(created.code, 200) + // 必须按配置键原样落库,getDriver 才能命中 githubreleases 分支 + assert.equal(created.data.driver, "GitHub Releases") + + const originalFetch = globalThis.fetch + globalThis.fetch = (async (input: any) => { + const url = String(input) + if (url.includes("/repos/OpenListTeam/OpenList/releases/latest")) { + return new Response( + JSON.stringify({ + tag_name: "v1.0.0", + created_at: "2026-01-01T00:00:00Z", + published_at: "2026-01-02T00:00:00Z", + html_url: + "https://github.com/OpenListTeam/OpenList/releases/tag/v1.0.0", + zipball_url: + "https://github.com/OpenListTeam/OpenList/archive/refs/tags/v1.0.0.zip", + tarball_url: + "https://github.com/OpenListTeam/OpenList/archive/refs/tags/v1.0.0.tar.gz", + assets: [ + { + name: "openlist-linux-amd64.tar.gz", + size: 1024, + created_at: "2026-01-02T00:00:00Z", + updated_at: "2026-01-02T00:00:00Z", + browser_download_url: + "https://github.com/OpenListTeam/OpenList/releases/download/v1.0.0/openlist-linux-amd64.tar.gz", + }, + ], + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + if (url.includes("/repos/OpenListTeam/OpenList/contents")) { + return new Response(JSON.stringify([]), { + status: 200, + headers: { "content-type": "application/json" }, + }) + } + return new Response("", { status: 404 }) + }) as typeof fetch + + try { + const { content, provider } = await listItems("/ghr", { env }) + assert.equal(provider, "GitHub Releases") + const asset = content.find((i) => i.name === "openlist-linux-amd64.tar.gz") + assert.ok(asset, `未列出 release 资产: ${JSON.stringify(content)}`) + assert.equal(asset!.is_dir, false) + assert.equal(asset!.raw_url?.includes("releases/download/v1.0.0"), true) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("存储列表中的驱动配置可被 driver/info 复用(前端编辑页)", async () => { + const env = await setupEnv() + const app = makeApp() + const list = await app.request( + "/api/admin/driver/list", + { method: "GET", headers: authHeaders }, + env, + ) + const listBody: any = await list.json() + const keys = Object.keys(listBody.data) + + for (const key of keys) { + // 与前端一致:按 key 请求 driver/info 必须精确命中 + const info = await app.request( + `/api/admin/driver/info?driver=${encodeURIComponent(key)}`, + { method: "GET", headers: authHeaders }, + env, + ) + const infoBody: any = await info.json() + assert.equal( + infoBody.data.name, + key, + `driver/info 未返回 ${key} 自身的配置`, + ) + } +}) From d3b79f83c87cad2bd9a3ca704a638d3dc8500685 Mon Sep 17 00:00:00 2001 From: dsvzb <17796147652@163.com> Date: Sun, 11 Oct 2026 12:45:52 +0800 Subject: [PATCH 14/49] =?UTF-8?q?fix(webdav/driver):=20href=20=E9=9C=80?= =?UTF-8?q?=E5=B8=A6=E6=8C=82=E8=BD=BD=E5=89=8D=E7=BC=80=EF=BC=9B=E5=85=89?= =?UTF-8?q?=E9=B8=AD=E4=BA=91=E7=9B=98=E4=B8=8A=E4=BC=A0=E5=87=AD=E6=8D=AE?= =?UTF-8?q?=E5=BA=94=E8=AF=BB=20data.creds=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PROPFIND 返回的 D:href 由 davPath 拼出,而 davPathOf() 已经把 /dav 挂载 前缀剥掉。RFC 4918 要求 D:href 是完整请求 URI,因此按请求路径解析 href 的客户端 (rclone、Windows 资源管理器、RaiDrive 等)会丢弃全部条目,表现为目录为空。 改为从请求 URL 推导前缀,而不是硬编码 /dav —— index.ts 还会把发往任意路径的 WebDAV 方法交给同一个 router,此时前缀为空。 GuangYaPan 的 get_res_center_token 把 OSS 凭据嵌套在 data.creds 下,而类型定义 与校验都按平铺在 data 上读取,于是 accessKeyID / secretAccessKey 恒为 undefined, 所有上传都抛 "upload token is incomplete"。改为优先取 creds 并保留平铺回退,同时 把 OSS 签名与 x-oss-security-token 头也改用取到的局部变量。 Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> --- src/backend/drivers/guangyapan/driver.ts | 24 ++++++++++++++++-------- src/backend/drivers/guangyapan/types.ts | 19 ++++++++++++++++--- src/backend/server/webdav.ts | 15 +++++++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/backend/drivers/guangyapan/driver.ts b/src/backend/drivers/guangyapan/driver.ts index a7a72383..39ba77c4 100644 --- a/src/backend/drivers/guangyapan/driver.ts +++ b/src/backend/drivers/guangyapan/driver.ts @@ -313,12 +313,20 @@ export class GuangYaPanDriver implements StorageDriver { return } + // 实时接口把 OSS 临时凭据嵌套在 data.creds 下,而不是平铺在 data 上。 + // 早期实现按平铺读取,导致 accessKeyID/secretAccessKey 恒为 undefined, + // 上传一律抛 "upload token is incomplete"。这里优先取 creds,并保留平铺回退。 + const creds = token?.creds || ({} as any) + const accessKeyID = creds.accessKeyID || token?.accessKeyID || "" + const secretAccessKey = creds.secretAccessKey || token?.secretAccessKey || "" + const sessionToken = creds.sessionToken || token?.sessionToken || "" + if ( !token.objectPath || !token.bucketName || !token.endPoint || - !token.accessKeyID || - !token.secretAccessKey + !accessKeyID || + !secretAccessKey ) { throw new Error("upload token is incomplete") } @@ -329,22 +337,22 @@ export class GuangYaPanDriver implements StorageDriver { const dateStr = new Date().toUTCString() const contentType = "application/octet-stream" - const ossHeaders = token.sessionToken - ? `x-oss-security-token:${token.sessionToken}\n` + const ossHeaders = sessionToken + ? `x-oss-security-token:${sessionToken}\n` : "" const canonicalizedResource = `/${token.bucketName}/${token.objectPath.replace(/^\/+/, "")}` const stringToSign = `PUT\n\n${contentType}\n${dateStr}\n${ossHeaders}${canonicalizedResource}` - const signature = await hmacSha1Base64(stringToSign, token.secretAccessKey) - const authorization = `OSS ${token.accessKeyID}:${signature}` + const signature = await hmacSha1Base64(stringToSign, secretAccessKey) + const authorization = `OSS ${accessKeyID}:${signature}` const headers: Record = { "Content-Type": contentType, Date: dateStr, Authorization: authorization, } - if (token.sessionToken) { - headers["x-oss-security-token"] = token.sessionToken + if (sessionToken) { + headers["x-oss-security-token"] = sessionToken } const res = await fetch(uploadURL, { diff --git a/src/backend/drivers/guangyapan/types.ts b/src/backend/drivers/guangyapan/types.ts index 3760fc09..a6f5f103 100644 --- a/src/backend/drivers/guangyapan/types.ts +++ b/src/backend/drivers/guangyapan/types.ts @@ -66,15 +66,28 @@ export interface GypTokenResp { error_description: string } +/** get_res_center_token 返回的 OSS 临时凭据(嵌套在 data.creds 下) */ +export interface GypUploadCreds { + accessKeyID: string + secretAccessKey: string + sessionToken: string + expiration: string +} + export interface GypUploadTokenData { taskId: string objectPath: string bucketName: string endPoint: string fullEndPoint: string - accessKeyID: string - secretAccessKey: string - sessionToken: string + /** + * 实时接口把 OSS 凭据嵌套在 creds 下,而不是平铺在 data 上。 + * 平铺字段保留为可选,仅用于兼容旧版响应。 + */ + creds?: GypUploadCreds + accessKeyID?: string + secretAccessKey?: string + sessionToken?: string } export interface GypUploadTokenResp { diff --git a/src/backend/server/webdav.ts b/src/backend/server/webdav.ts index 3ef7d4d8..80a4722d 100644 --- a/src/backend/server/webdav.ts +++ b/src/backend/server/webdav.ts @@ -113,6 +113,21 @@ function splitPath(p: string): { dir: string; name: string } { return { dir, name } } +/** + * 推导本次请求实际使用的挂载前缀(正规挂载为 `/dav`)。 + * + * 不能直接硬编码 `/dav`:index.ts 会把**任意路径上**的 WebDAV 方法都交给本 router + * (为兼容把用户填写的地址当 WebDAV 根、对 `/` 发 PROPFIND 的客户端),此时前缀为空。 + * PROPFIND 返回的 href 必须与本请求的 URL 结构一致,客户端才能匹配上。 + */ +function davPrefixOf(c: any, davPath: string): string { + const pathname = new URL(c.req.url).pathname + const stripped = davPath === "/" ? "" : davPath + return pathname.endsWith(stripped) + ? pathname.slice(0, pathname.length - stripped.length) + : "" +} + webdavRouter.all("/*", async (c) => { const user = await webdavAuth(c) if (!user) { From 89b7345d1282edec3d037d4ac6093a9963fedc1c Mon Sep 17 00:00:00 2001 From: Wudarensheng Date: Wed, 14 Oct 2026 14:38:37 +0800 Subject: [PATCH 15/49] =?UTF-8?q?feat(singleflight):=20=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E7=9B=B8=E5=90=8C=E8=B0=83=E7=94=A8=E5=8E=BB=E9=87=8D=EF=BC=88?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E5=86=85=E5=90=88=E5=B9=B6=20+=20=E8=B7=A8?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E8=A1=A8=E5=8D=8F=E8=B0=83=EF=BC=89=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(singleflight): 并发相同调用去重(进程内合并 + 跨实例表协调) 一个部署会同时存在多个 isolate,进程内 Map 无法跨实例合并重复调用,导致 同一目录列表 / 元信息的并发请求各自打一次上游网盘 API —— 这正是限流、封号、 CPU 超时的主要来源。新增 singleflight 做两级去重: - L1 进程内:Map 合并,所有模式生效,零成本 - L2 跨实例:数据库表 x_singleflight 抢锁 + 结果共享(仅 db 模式) · 抢锁靠 INSERT OR IGNORE + 回读 owner,不依赖事务 · 执行者按 lockTtl/3 心跳续租,崩溃后锁到期可被接管 · 等待方轮询读取结果;超时则自行执行,不会把请求挂死 模式由 SINGLEFLIGHT 控制(auto/db/memory/off),默认 auto:有 SQL 库 (d1 / mysql / do)时用表协调,否则退回进程内合并。 关键设计: - 不做结果缓存。只有「到达时观察到执行者处于 running」的调用才复用结果, 新到达者会回收旧记录并重新执行,因此不会出现「删除文件后刷新仍看到被删 文件」这类脏读(handoffMs 只是等待方读取结果的交接窗口,不是缓存)。 - 业务异常原样抛出且不重试;协调层异常(表缺失 / DB 超时 / 结果不可序列化 / 执行者崩溃)一律降级为「各自执行一次」,业务不会失败。 - x_singleflight 属运行时数据,刻意不加入 TABLE_NAMES —— 否则 sqlFormat.save() 的整表 DELETE 会在每次保存配置时清空在途记录。 接入:internal/op/storage.ts 的 listItems / getItem,覆盖 /fs/list、/fs/get、 /fs/dir、搜索、WebDAV、MCP。运行统计见 /api/debug/info 的 singleflight 字段。 配置:SINGLEFLIGHT / _HANDOFF_MS / _LOCK_TTL_MS / _POLL_MS / _WAIT_MS, 已同步到 .env.example、.dev.vars.example、wrangler.jsonc 与 README。 新增单测 17 例(含 mock SQL 驱动),并纳入 test:all 的 test:pkg。 * fix(singleflight): 索引名带表前缀,放宽 POLL_MS 默认值至 200ms - buildSingleFlightDdl 改为调用时经 getTablePrefix(env) 生成表名与索引名: 多部署共用物理库时第二个部署的索引不再被 CREATE INDEX IF NOT EXISTS 静默跳过(评审意见 #2) - SINGLEFLIGHT_POLL_MS 默认 50 → 200:每次轮询都是一次协调表 SELECT, 过密会消耗 D1 subrequest 配额与 CPU(评审意见 #1);README 同步说明 - 同步 wrangler.jsonc 注释默认值 --- .dev.vars.example | 8 + .env.example | 8 + README.md | 25 + package.json | 6 +- src/backend/durable-objects/OpenListDB.ts | 16 +- src/backend/internal/model/store/driver/d1.ts | 14 +- .../internal/model/store/driver/mysql.ts | 10 +- src/backend/internal/model/store/schema.ts | 83 ++ src/backend/internal/op/storage.ts | 48 ++ src/backend/pkg/singleflight.test.ts | 542 ++++++++++++ src/backend/pkg/singleflight.ts | 783 ++++++++++++++++++ src/backend/server/debug.ts | 4 + wrangler.jsonc | 23 + 13 files changed, 1560 insertions(+), 10 deletions(-) create mode 100644 src/backend/pkg/singleflight.test.ts create mode 100644 src/backend/pkg/singleflight.ts diff --git a/.dev.vars.example b/.dev.vars.example index af758754..d4b84dd1 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -83,3 +83,11 @@ CF_ACCOUNT= CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= + +# singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 +# 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 +# auto(默认)- 有 SQL 库用表,否则退回进程内(memory) +# db - 强制用表协调;协调层出错时自动降级,不影响业务 +# memory - 仅进程内合并(等价 Go 的进程内 singleflight) +# off - 关闭去重,每次调用都直接执行 +SINGLEFLIGHT=auto diff --git a/.env.example b/.env.example index d8159d1c..9583c4b0 100644 --- a/.env.example +++ b/.env.example @@ -81,3 +81,11 @@ CF_ACCOUNT= CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= + +# singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 +# 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 +# auto(默认)- 有 SQL 库用表,否则退回进程内(memory) +# db - 强制用表协调;协调层出错时自动降级,不影响业务 +# memory - 仅进程内合并(等价 Go 的进程内 singleflight) +# off - 关闭去重,每次调用都直接执行 +SINGLEFLIGHT=auto diff --git a/README.md b/README.md index c1ba9b9c..27afc671 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea - **离线任务**:后台任务队列,支持批量操作与异步处理。 - **外部接口**:将聚合存储以 WebDAV 或 S3 兼容协议对外暴露,便于挂载到第三方工具。 - **MCP 服务**:提供 Model Context Protocol 端点,可被 AI 助手等客户端集成调用。 +- **并发去重(singleflight)**:同一时刻相同的目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用同一结果;一个部署存在多个 isolate,因此除进程内合并外,还会用数据库表 `x_singleflight` 做跨实例协调(详见[并发去重](#并发去重-singleflight))。 ### 权限管理 @@ -284,6 +285,30 @@ CF_API_KEY=your_api_token `des-cbc-hmac` / `3des-cbc-hmac`(后两者仅兼容用途,不安全) - `ADMIN_PASS`:初始管理员密码(可选,设置后跳过安装向导自动初始化 admin) +#### 并发去重(singleflight) + +同一时刻对同一存储、同一路径的重复读请求(目录列表、元信息)只向上游发起一次调用, +其余并发请求等待并复用同一结果,用于规避网盘限流与无谓的 CPU 消耗。 + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `SINGLEFLIGHT` | `auto` | `auto`:有 SQL 库(d1/mysql/do)时用表协调,否则退回进程内;`db`:强制用表;`memory`:仅进程内;`off`:关闭 | +| `SINGLEFLIGHT_HANDOFF_MS` | `1000` | 执行完成后结果在表中保留多久(ms),供**正在等待的**并发实例读取 | +| `SINGLEFLIGHT_LOCK_TTL_MS` | `30000` | 执行者持锁上限(ms),期间按 1/3 周期续租;执行者崩溃后到期可被接管 | +| `SINGLEFLIGHT_POLL_MS` | `200` | 等待方轮询间隔(ms)。不建议低于 100:每次轮询都是一次对协调表的 SELECT,过密会显著消耗 D1 subrequest 配额与 CPU | +| `SINGLEFLIGHT_WAIT_MS` | `15000` | 等待方最长等待(ms),超时后自行执行,避免被慢请求拖死 | + +行为要点: + +- **两级去重**:进程内合并(所有模式都生效)+ 数据库表协调(`db` 模式,跨 isolate 生效)。 +- **不做结果缓存**:只有「到达时观察到执行者正在运行」的请求才会复用结果;新到达的请求 + 会回收旧记录并重新执行,因此不存在「删除文件后刷新仍看到旧文件」的脏读。 +- **可用性优先**:表不存在、数据库超时、结果无法序列化、执行者崩溃等**协调层**异常一律 + 降级为「各自执行一次」,业务不会失败;而**业务函数自身的异常会原样抛出且不重试**。 +- **表结构**:`x_singleflight` 由 SQL 驱动在初始化时自动创建,属运行时数据,不参与配置读写。 +- 运行统计(`executed` / `coalesced` / `shared` / `fallback`)可在 `/api/debug/info` 的 + `singleflight` 字段查看,用于确认跨实例协调是否真的生效。 + #### 其他配置 详细配置说明请参考 [官方文档](https://doc.oplist.org/guide/configuration) diff --git a/package.json b/package.json index 93d16da6..9dbe8a48 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ }, "DB_CIPHER": { "description": "At-rest cipher for sensitive fields (drive credentials, 2FA secrets, password hashes): `none` (default, no encryption), `aes-256-gcm` (HKDF-derived AES-256-GCM key — the `enc:v2:` envelope written by existing encrypted deployments, recommended), `aes-256-gcm-pbkdf2` (legacy `enc:v1:` envelope, PBKDF2 per field, slow), `aes-256-cbc-hmac` (`enc:v3:`), `chacha20-poly1305` (`enc:v4:`, RFC 8439, pure-JS implementation), `des-cbc-hmac` / `3des-cbc-hmac` (`enc:v5:`/`enc:v6:`, compatibility only — single DES is 56-bit and 3DES is deprecated, do not use for real data). Ciphertexts carry an `enc:vN:` prefix and are decrypted by prefix on read, so switching back to `none` (or changing algorithms) never makes existing data unreadable — it only changes how newly written data is stored (legacy ciphertext is migrated to plaintext on the next save). Key derivation is cached per isolate, and unchanged fields are not re-encrypted on save. This setting does NOT affect the shared secret: with `none`, a secret is still generated and persisted during setup when JWT_SECRET is not provided, because JWT signing needs it." + }, + "SINGLEFLIGHT": { + "description": "Dedup of concurrent identical calls (singleflight). `auto` (default) uses the `x_singleflight` table when a SQL backend (d1/mysql/do) is available, otherwise falls back to in-process merging. `db` forces table coordination, `memory` keeps it per-isolate, `off` disables dedup." } } }, @@ -55,10 +58,11 @@ "test:server": "tsx --test \"src/backend/server/*.test.ts\"", "test:store": "tsx --test src/backend/internal/model/store/store.test.ts", "test:model": "tsx --test \"src/backend/internal/model/*.test.ts\"", + "test:pkg": "tsx --test \"src/backend/pkg/*.test.ts\"", "test:regress": "tsx scripts/_regress.mjs", "test:deploy": "node scripts/test-deploy.js", "env:check": "tsx scripts/env-check.mjs", - "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:regress", + "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:pkg && npm run test:regress", "sls:deploy": "serverless deploy", "sls:package": "serverless package", "sls:info": "serverless info", diff --git a/src/backend/durable-objects/OpenListDB.ts b/src/backend/durable-objects/OpenListDB.ts index 007cd077..f81d3d0e 100644 --- a/src/backend/durable-objects/OpenListDB.ts +++ b/src/backend/durable-objects/OpenListDB.ts @@ -13,7 +13,11 @@ * 通过 RPC 调用(stub.kvGet / sqlQuery 等),每个实例用 `idFromName` 定位到 * 固定实例,保证数据持久在同一 DO 实例。 */ -import { D1_SCHEMA, KV_SCHEMA_SQLITE } from "../internal/model/store/schema" +import { + buildSingleFlightDdl, + D1_SCHEMA, + KV_SCHEMA_SQLITE, +} from "../internal/model/store/schema" export class OpenListDB { private state: any @@ -27,9 +31,15 @@ export class OpenListDB { return (this.state.storage as any).sql } - /** 建表(幂等):KV 表(map/key 格式)+ 列式表(sql 格式)。 */ + /** + * 建表(幂等):KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表。 + */ private ensureSchema(): void { - for (const ddl of [...KV_SCHEMA_SQLITE, ...D1_SCHEMA]) { + for (const ddl of [ + ...KV_SCHEMA_SQLITE, + ...D1_SCHEMA, + ...buildSingleFlightDdl("sqlite"), + ]) { this.sql.exec(ddl) } } diff --git a/src/backend/internal/model/store/driver/d1.ts b/src/backend/internal/model/store/driver/d1.ts index 5fb7070c..cffbde68 100644 --- a/src/backend/internal/model/store/driver/d1.ts +++ b/src/backend/internal/model/store/driver/d1.ts @@ -6,7 +6,11 @@ * - OPENLIST_DB (别名) */ import type { Driver } from "../types" -import { buildDdl, KV_SCHEMA_SQLITE } from "../schema" +import { + buildDdl, + buildSingleFlightDdl, + KV_SCHEMA_SQLITE, +} from "../schema" /** * 判断对象是否具备 D1 绑定接口形态。 @@ -46,8 +50,12 @@ const d1Inited = new WeakMap() async function ensureSchema(db: any, env?: any): Promise { if (d1Inited.get(db)) return - // KV 表(map/key 格式)+ 列式表(sql 格式)一并创建 - for (const ddl of [...KV_SCHEMA_SQLITE, ...buildDdl("sqlite", env)]) { + // KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表一并创建 + for (const ddl of [ + ...KV_SCHEMA_SQLITE, + ...buildDdl("sqlite", env), + ...buildSingleFlightDdl("sqlite", env), + ]) { await db.prepare(ddl).run() } d1Inited.set(db, true) diff --git a/src/backend/internal/model/store/driver/mysql.ts b/src/backend/internal/model/store/driver/mysql.ts index 73604457..6a93409f 100644 --- a/src/backend/internal/model/store/driver/mysql.ts +++ b/src/backend/internal/model/store/driver/mysql.ts @@ -8,7 +8,7 @@ * - MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_NAME */ import type { Driver } from "../types" -import { buildDdl, getTablePrefix, KV_SCHEMA_MYSQL } from "../schema" +import { buildDdl, buildSingleFlightDdl, getTablePrefix, KV_SCHEMA_MYSQL } from "../schema" function isNode(): boolean { return typeof process !== "undefined" && process.release?.name === "node" @@ -52,8 +52,12 @@ let _schemaInitedPrefix: string | null = null async function ensureSchema(pool: any, env?: any): Promise { const prefix = getTablePrefix(env) if (_schemaInitedPrefix === prefix) return - // KV 表(map/key 格式)+ 列式表(sql 格式)一并创建 - for (const ddl of [...KV_SCHEMA_MYSQL, ...buildDdl("mysql", env)]) { + // KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表一并创建 + for (const ddl of [ + ...KV_SCHEMA_MYSQL, + ...buildDdl("mysql", env), + ...buildSingleFlightDdl("mysql", env), + ]) { await pool.query(ddl) } _schemaInitedPrefix = prefix diff --git a/src/backend/internal/model/store/schema.ts b/src/backend/internal/model/store/schema.ts index b5ce1a0d..d707b596 100644 --- a/src/backend/internal/model/store/schema.ts +++ b/src/backend/internal/model/store/schema.ts @@ -487,3 +487,86 @@ export const KV_SCHEMA_SQLITE: string[] = [ export const KV_SCHEMA_MYSQL: string[] = [ "CREATE TABLE IF NOT EXISTS `kv` (`key` VARCHAR(512) PRIMARY KEY, `value` LONGTEXT NOT NULL)", ] + +/** + * singleflight 协调表的逻辑名(不含 `x_` 前缀)。 + * + * 与 Go 后端**无对应表**:Go 是单进程模型,`golang.org/x/sync/singleflight` + * 只需进程内 Map;TS 侧一个部署会同时跑多个 isolate/实例,进程内 Map 无法 + * 互相看见,因此需要一张共享表来做跨实例的单飞协调(见 pkg/singleflight.ts)。 + */ +export const SINGLEFLIGHT_TABLE = "singleflight" + +/** singleflight 表的完整 SQL 表名(含 `x_` 前缀)。 */ +export function singleflightTableName(env?: any): string { + return getTablePrefix(env) + SINGLEFLIGHT_TABLE +} + +/** + * 生成 singleflight 协调表的建表语句(SQLite / Cloudflare D1 / Durable Object、 + * MySQL 方言)。 + * + * 列语义: + * - `key` 去重键(调用方给定,如 `fs.list:1:/movies`) + * - `owner` 执行者实例标识;只有 owner 能写回结果(防止被接管后串写) + * - `state` running | done | error + * - `started_at` 开始时间(epoch ms,诊断用) + * - `expires_at` 过期时间(epoch ms):running 超时表示执行者疑似崩溃, + * 可被其他实例接管;done/error 则是结果交接窗口的截止时间 + * - `result` 成功结果的 JSON 序列化(仅 done 有值) + * - `error` 失败原因文本(仅 error 有值) + * + * 注意:该表存的是**运行时在途记录**,绝不参与 db.ts 的配置往返, + * 因此刻意不放进 TABLE_NAMES —— 否则 sqlFormat.save() 的整表 DELETE + * 会在每次保存配置时把正在执行中的单飞记录清空。 + * + * 与 KV_SCHEMA / buildDdl 并列,由各 SQL 驱动在 ensureSchema 时一并执行。 + * 非 SQL 驱动(KV / Blob / 内存)没有该表,此时 singleflight 自动退回内存级。 + * + * 表名与索引名都在调用时经 `getTablePrefix(env)` 生成(而非模块加载期的常量): + * 多部署共用一个物理库、靠前缀隔离时,第二个部署的表与索引才能各自正确创建, + * 索引名若不带前缀会被首个部署的 `CREATE INDEX IF NOT EXISTS` 静默跳过。 + */ +export function buildSingleFlightDdl( + dialect: "sqlite" | "mysql", + env?: any, +): string[] { + const table = quote(singleflightTableName(env)) + const createTable = + dialect === "mysql" + ? `CREATE TABLE IF NOT EXISTS ${table} (` + + [ + `${quote("key")} VARCHAR(512) PRIMARY KEY`, + `${quote("owner")} VARCHAR(128) NOT NULL`, + `${quote("state")} VARCHAR(16) NOT NULL`, + `${quote("started_at")} BIGINT NOT NULL`, + `${quote("expires_at")} BIGINT NOT NULL`, + `${quote("result")} LONGTEXT`, + `${quote("error")} LONGTEXT`, + ].join(", ") + + `)` + : `CREATE TABLE IF NOT EXISTS ${table} (` + + [ + `${quote("key")} TEXT PRIMARY KEY`, + `${quote("owner")} TEXT NOT NULL`, + `${quote("state")} TEXT NOT NULL`, + `${quote("started_at")} INTEGER NOT NULL`, + `${quote("expires_at")} INTEGER NOT NULL`, + `${quote("result")} TEXT`, + `${quote("error")} TEXT`, + ].join(", ") + + `)` + + if (dialect === "mysql") { + // MySQL 不支持 `CREATE INDEX IF NOT EXISTS`,而索引缺失只影响过期行的回收效率 + // (建表失败会中断整个 init),因此这里刻意不建索引。 + return [createTable] + } + + // 回收过期行(崩溃残留 / 交接窗口结束)按 expires_at 过滤,走索引避免全表扫。 + return [ + createTable, + `CREATE INDEX IF NOT EXISTS ${quote(getTablePrefix(env) + "idx_singleflight_expires")} ` + + `ON ${table} (${quote("expires_at")})`, + ] +} diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 90b6c989..f2ae98f4 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1,5 +1,6 @@ import { resolvePath, getDb, getSettings, saveDb } from "../model/db" import { encodeDownloadPath } from "../../pkg/path" +import { singleflight } from "../../pkg/singleflight" import { canUseProxyEndpoint, normalizeExtList } from "../driver/proxy" import { FileItem, StorageDriver, calcFileType } from "../driver/base" import { Onedrive } from "../../drivers/onedrive/driver" @@ -1351,11 +1352,45 @@ export async function flushPendingDriverState( await scheduleStoragePersistence(requestContext?.waitUntil, persistence) } +/** + * fs 读操作的 singleflight 去重键。 + * + * 组成:操作名 + 存储 id + 存储修订号 + 虚拟路径。 + * - 带存储 id/修订号:不同存储、以及存储配置被编辑后,不会复用旧结果; + * - 带虚拟路径(调用方已做过 base_path 处理):不会跨用户串味。 + * + * 该键同时用于进程内(L1)与数据库表(L2)两级去重,因此必须稳定可复现: + * 绝不能包含时间戳、随机数、用户对象等每次调用都不同的内容。 + */ +function fsSingleflightKey( + op: string, + resolved: any, + virtualPath: string, +): string { + const storageId = resolved?.storage?.id ?? "virtual" + const revision = resolved?.storage?.modified ?? "" + return `${op}:${storageId}:${revision}:${virtualPath}` +} + export async function listItems( virtualPath: string, requestContext?: StorageRequestContext, ): Promise<{ content: FileItem[]; provider: string; storage?: any }> { const resolved = await resolvePath(virtualPath, requestContext?.env) + // 并发相同的「同一存储 + 同一目录」只向上游发起一次 list。 + // 键必须在 resolvePath 之后才能确定(需要存储 id),因此去重发生在路径解析之后。 + return singleflight( + fsSingleflightKey("fs.list", resolved, virtualPath), + () => listItemsResolved(resolved, virtualPath, requestContext), + { env: requestContext?.env }, + ) +} + +async function listItemsResolved( + resolved: Awaited>, + virtualPath: string, + requestContext?: StorageRequestContext, +): Promise<{ content: FileItem[]; provider: string; storage?: any }> { let items: FileItem[] = [] let driverName = "Virtual" @@ -1490,6 +1525,19 @@ export async function getItem( requestContext?: StorageRequestContext, ): Promise<{ item: FileItem; provider: string; rawUrl: string }> { const resolved = await resolvePath(virtualPath, requestContext?.env) + // 与 listItems 同理:并发相同的「同一存储 + 同一路径」只向上游取一次元信息。 + return singleflight( + fsSingleflightKey("fs.get", resolved, virtualPath), + () => getItemResolved(resolved, virtualPath, requestContext), + { env: requestContext?.env }, + ) +} + +async function getItemResolved( + resolved: Awaited>, + virtualPath: string, + requestContext?: StorageRequestContext, +): Promise<{ item: FileItem; provider: string; rawUrl: string }> { if (resolved.isVirtual) { const name = resolved.cleanPath.split("/").filter(Boolean).pop() || "root" return { diff --git a/src/backend/pkg/singleflight.test.ts b/src/backend/pkg/singleflight.test.ts new file mode 100644 index 00000000..caecffc0 --- /dev/null +++ b/src/backend/pkg/singleflight.test.ts @@ -0,0 +1,542 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { + singleflight, + getSingleFlightStats, + __resetSingleFlightForTest, + __singleFlightInsertIgnoreForTest, +} from "./singleflight" +import { + buildSingleFlightDdl, + singleflightTableName, +} from "../internal/model/store/schema" + +/** + * singleflight 单飞去重回归测试。 + * + * 背景:本仓库跑在 Workers 上,一个部署有多个 isolate,进程内 Map 无法跨实例 + * 合并重复调用,因此需要数据库表做二级协调。本文件锁定以下行为: + * + * 1. L1(进程内)必须把并发的同 key 调用合并为一次执行; + * 2. L2(数据库表)必须能让等待方读到执行者的结果,而不是各自再执行一次; + * 3. **绝不脏读**:到达时若该 key 上已有「已结束」的记录,必须重新执行, + * 不能把上一轮的结果当缓存返回(否则会出现「删除后刷新仍看到被删文件」); + * 4. 业务异常原样抛出且不重试;协调层异常则降级为直接执行(可用性优先); + * 5. off / memory / db 三种模式与 SQL 方言分支正确。 + */ + +const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)) + +interface MockRow { + key: string + owner: string + state: string + started_at: number + expires_at: number + result: string | null + error: string | null +} + +/** + * 基于局部 Map 的简易 SQL 驱动。 + * + * 只实现 pkg/singleflight.ts 实际会发出的那几条语句(与 store.test.ts 的 + * mock 驱动思路一致),并保留 rows 便于用例直接构造「别的实例写入的行」。 + */ +function createMockSqlDriver(dialect: "sqlite" | "mysql" = "sqlite") { + const rows = new Map() + let pendingFailure: Error | null = null + + const maybeFail = () => { + if (pendingFailure) { + const err = pendingFailure + pendingFailure = null + throw err + } + } + + const driver = { + name: dialect === "mysql" ? "mysql" : "d1", + + async query(sql: string, params: any[]): Promise { + maybeFail() + const s = sql.trim() + if (/^SELECT `owner` FROM/i.test(s)) { + const r = rows.get(String(params[0])) + return r ? [{ owner: r.owner }] : [] + } + if (/^SELECT `state`/i.test(s)) { + const r = rows.get(String(params[0])) + return r + ? [ + { + state: r.state, + result: r.result, + error: r.error, + expires_at: r.expires_at, + }, + ] + : [] + } + throw new Error(`mock-sql unsupported query: ${sql}`) + }, + + async execute(sql: string, params: any[]): Promise { + maybeFail() + const s = sql.trim() + + // 回收:过期行 或 已结束(state <> running)的行 + if ( + /^DELETE FROM .* WHERE `key` = \? AND \(`expires_at` <= \? OR `state` <> \?\)/i.test( + s, + ) + ) { + const [key, now, running] = params + const r = rows.get(String(key)) + if (r && (r.expires_at <= Number(now) || r.state !== String(running))) { + rows.delete(String(key)) + } + return + } + + // 执行者主动释放 + if (/^DELETE FROM .* WHERE `key` = \? AND `owner` = \?/i.test(s)) { + const [key, owner] = params + const r = rows.get(String(key)) + if (r && r.owner === String(owner)) rows.delete(String(key)) + return + } + + // 抢占:INSERT [OR] IGNORE,主键冲突即忽略(模拟数据库的原子性) + const ins = s.match( + /^INSERT (?:OR )?IGNORE INTO .* \(([^)]+)\) VALUES \(([^)]+)\)/i, + ) + if (ins) { + const cols = ins[1].split(",").map((c) => c.trim().replace(/`/g, "")) + const key = String(params[0]) + if (rows.has(key)) return + const row: any = {} + cols.forEach((c, i) => { + row[c] = params[i] + }) + rows.set(key, row as MockRow) + return + } + + // 写回结果 + if ( + /^UPDATE .* SET `state` = \?, `result` = \?, `error` = \?, `expires_at` = \? WHERE `key` = \? AND `owner` = \?/i.test( + s, + ) + ) { + const [state, result, error, expires, key, owner] = params + const r = rows.get(String(key)) + if (r && r.owner === String(owner)) { + r.state = String(state) + r.result = result ?? null + r.error = error ?? null + r.expires_at = Number(expires) + } + return + } + + // 续租 + if ( + /^UPDATE .* SET `expires_at` = \? WHERE `key` = \? AND `owner` = \? AND `state` = \?/i.test( + s, + ) + ) { + const [expires, key, owner, state] = params + const r = rows.get(String(key)) + if (r && r.owner === String(owner) && r.state === String(state)) { + r.expires_at = Number(expires) + } + return + } + + throw new Error(`mock-sql unsupported execute: ${sql}`) + }, + + isAvailable: async () => true, + init: async () => {}, + get: async () => null, + put: async () => {}, + delete: async () => {}, + list: async () => [], + health: async () => ({ connected: true }), + + /** 测试钩子:直接读写表内容,用于构造「别的实例」的行。 */ + rows, + failNext(err: Error) { + pendingFailure = err + }, + } + + return driver +} + +// ── 建表语句 ──────────────────────────────────────────────────────────────── + +test("schema: singleflight 表名带 x_ 前缀,且 DDL 覆盖两种方言", () => { + assert.equal(singleflightTableName(), "x_singleflight") + + const sqlite = buildSingleFlightDdl("sqlite").join("\n") + assert.match(sqlite, /CREATE TABLE IF NOT EXISTS `x_singleflight`/) + assert.match(sqlite, /`expires_at` INTEGER NOT NULL/) + // 回收过期行需要按 expires_at 过滤,SQLite 侧建索引 + // 索引名与表名一样带 x_ 前缀:多部署共用一个物理库时第二个部署才能建出自己的索引 + assert.match(sqlite, /CREATE INDEX IF NOT EXISTS `x_idx_singleflight_expires`/) + assert.match(sqlite, /ON `x_singleflight` \(`expires_at`\)/) + + const mysql = buildSingleFlightDdl("mysql").join("\n") + assert.match(mysql, /CREATE TABLE IF NOT EXISTS `x_singleflight`/) + assert.match(mysql, /`key` VARCHAR\(512\) PRIMARY KEY/) + assert.match(mysql, /`expires_at` BIGINT NOT NULL/) + // MySQL 不支持 CREATE INDEX IF NOT EXISTS,因此不建索引 + assert.doesNotMatch(mysql, /CREATE INDEX/) +}) + +test("schema: singleflight 表不得进入配置往返(否则保存配置会清空在途记录)", async () => { + const schema = await import("../internal/model/store/schema") + assert.ok( + !(schema.TABLE_NAMES as readonly string[]).includes("singleflight"), + "singleflight 是运行时数据,不能放进 TABLE_NAMES", + ) + assert.ok( + !(schema.DDL_TABLE_NAMES as readonly string[]).includes("singleflight"), + "建表由驱动单独执行(buildSingleFlightDdl),不混入列式表 DDL", + ) +}) + +test("SQL 方言:mysql 用 INSERT IGNORE,d1/do(SQLite)用 INSERT OR IGNORE", () => { + assert.equal(__singleFlightInsertIgnoreForTest("mysql"), "INSERT IGNORE") + assert.equal(__singleFlightInsertIgnoreForTest("d1"), "INSERT OR IGNORE") + assert.equal(__singleFlightInsertIgnoreForTest("do"), "INSERT OR IGNORE") +}) + +// ── L1:进程内合并 ────────────────────────────────────────────────────────── + +test("memory: 并发相同 key 只执行一次,所有调用方拿到同一结果", async () => { + __resetSingleFlightForTest() + let executed = 0 + + const fn = async () => { + executed++ + await delay(30) + return { value: "shared" } + } + + const results = await Promise.all( + Array.from({ length: 10 }, () => + singleflight("mem-a", fn, { mode: "memory", env: {} }), + ), + ) + + assert.equal(executed, 1, "10 个并发调用只应触发 1 次执行") + for (const r of results) assert.deepEqual(r, { value: "shared" }) + + const stats = getSingleFlightStats() + assert.equal(stats.executed, 1) + assert.equal(stats.coalesced, 9, "其余 9 次应被 L1 合并") +}) + +test("memory: 执行结束后不缓存结果,下一次调用会重新执行", async () => { + __resetSingleFlightForTest() + let executed = 0 + const fn = async () => { + executed++ + return executed + } + + assert.equal(await singleflight("mem-b", fn, { mode: "memory", env: {} }), 1) + assert.equal(await singleflight("mem-b", fn, { mode: "memory", env: {} }), 2) + assert.equal(executed, 2, "singleflight 只合并并发,不做结果缓存") +}) + +test("memory: 失败传播给所有等待方,且不残留导致后续调用被误合并", async () => { + __resetSingleFlightForTest() + let executed = 0 + const failing = async () => { + executed++ + await delay(10) + throw new Error("upstream boom") + } + + const settled = await Promise.allSettled([ + singleflight("mem-c", failing, { mode: "memory", env: {} }), + singleflight("mem-c", failing, { mode: "memory", env: {} }), + ]) + assert.equal(executed, 1) + assert.ok(settled.every((s) => s.status === "rejected")) + assert.match(String((settled[0] as any).reason?.message), /upstream boom/) + + // 在途表必须已清理:下一次调用应重新执行而不是复用已失败的 Promise + assert.equal( + await singleflight("mem-c", async () => "ok", { mode: "memory", env: {} }), + "ok", + ) + assert.equal(getSingleFlightStats().active, 0) +}) + +test("off: 完全关闭去重,每次调用都直接执行", async () => { + __resetSingleFlightForTest() + let executed = 0 + const fn = async () => { + executed++ + await delay(20) + return executed + } + + await Promise.all( + Array.from({ length: 3 }, () => + singleflight("off-a", fn, { mode: "off", env: {} }), + ), + ) + assert.equal(executed, 3) + assert.equal(getSingleFlightStats().mode, "off") +}) + +// ── L2:数据库表协调 ──────────────────────────────────────────────────────── + +test("db: 到达时发现执行者正在运行 → 等待并共享其结果(不重复执行)", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + const now = Date.now() + + // 模拟「另一个 isolate 已在执行」:running 行由 other 持有 + driver.rows.set("db-share", { + key: "db-share", + owner: "other-instance", + state: "running", + started_at: now, + expires_at: now + 5000, + result: null, + error: null, + }) + // 30ms 后对方写回结果 + setTimeout(() => { + const row = driver.rows.get("db-share")! + row.state = "done" + row.result = JSON.stringify({ from: "leader" }) + row.expires_at = Date.now() + 1000 + }, 30) + + let executed = 0 + const out = await singleflight( + "db-share", + async () => { + executed++ + return { from: "self" } + }, + { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 3000 }, + ) + + assert.deepEqual(out, { from: "leader" }) + assert.equal(executed, 0, "等待方不应自己执行") + assert.equal(getSingleFlightStats().shared, 1) +}) + +test("db: 到达时记录已结束 → 必须重新执行,绝不返回旧结果(防脏读)", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + const now = Date.now() + + // 上一轮调用留下的交接记录:内容已经过期 + driver.rows.set("db-stale", { + key: "db-stale", + owner: "other-instance", + state: "done", + started_at: now - 100, + expires_at: now + 10_000, + result: JSON.stringify({ value: "stale" }), + error: null, + }) + + let executed = 0 + const out = await singleflight( + "db-stale", + async () => { + executed++ + return { value: "fresh" } + }, + { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 500 }, + ) + + assert.deepEqual(out, { value: "fresh" }, "新到达者必须拿到新鲜结果") + assert.equal(executed, 1) + assert.equal(getSingleFlightStats().shared, 0, "不应命中交接记录") +}) + +test("db: 抢到锁后写回结果,行状态为 done 且带交接窗口", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + + const out = await singleflight("db-lead", async () => ({ n: 7 }), { + mode: "db", + driver, + env: {}, + handoffMs: 1234, + }) + + assert.deepEqual(out, { n: 7 }) + const row = driver.rows.get("db-lead") + assert.ok(row, "执行者应留下交接记录供等待方读取") + assert.equal(row!.state, "done") + assert.equal(row!.result, JSON.stringify({ n: 7 })) + assert.equal(row!.owner.length > 0, true) + assert.ok( + row!.expires_at > Date.now() + 1000 && row!.expires_at <= Date.now() + 1400, + "expires_at 应约为 now + handoffMs", + ) + assert.equal(getSingleFlightStats().executed, 1) +}) + +test("db: handoffMs=0 时执行完立即释放,不留交接记录", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + + await singleflight("db-nohand", async () => "v", { + mode: "db", + driver, + env: {}, + handoffMs: 0, + }) + + assert.equal(driver.rows.size, 0, "交接窗口为 0 应立即删除行") +}) + +test("db: 业务异常原样抛出,且不会被降级重试(只执行一次)", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + let executed = 0 + + await assert.rejects( + singleflight( + "db-err", + async () => { + executed++ + throw new Error("driver exploded") + }, + { mode: "db", driver, env: {} }, + ), + /driver exploded/, + ) + + assert.equal(executed, 1, "业务异常不得触发降级重试") + assert.equal(getSingleFlightStats().fallback, 0) + // 失败也应写回,让等待方共享失败而不是各自重试上游 + assert.equal(driver.rows.get("db-err")!.state, "error") +}) + +test("db: 协调层异常(DB 不可用)→ 降级为直接执行,业务不受影响", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + driver.failNext(new Error("D1_ERROR: no such table: x_singleflight")) + + let executed = 0 + const out = await singleflight( + "db-degrade", + async () => { + executed++ + return "business-ok" + }, + { mode: "db", driver, env: {} }, + ) + + assert.equal(out, "business-ok") + assert.equal(executed, 1) + assert.equal(getSingleFlightStats().fallback, 1) +}) + +test("db: 等待超时后自行执行,不会无限挂起", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + const now = Date.now() + + // 执行者一直 running 且迟迟不写回 + driver.rows.set("db-timeout", { + key: "db-timeout", + owner: "other-instance", + state: "running", + started_at: now, + expires_at: now + 60_000, + result: null, + error: null, + }) + + let executed = 0 + const out = await singleflight( + "db-timeout", + async () => { + executed++ + return "self-result" + }, + { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 40 }, + ) + + assert.equal(out, "self-result") + assert.equal(executed, 1, "等待超时后应自行执行,保证请求不被拖死") +}) + +// ── auto 模式 ─────────────────────────────────────────────────────────────── + +test("auto: 探测不到 SQL 驱动 → memory(不会因为无数据库而报错)", async () => { + __resetSingleFlightForTest() + let executed = 0 + const out = await singleflight( + "auto-mem", + async () => { + executed++ + return "ok" + }, + { mode: "auto", driver: null, env: {} }, + ) + + assert.equal(out, "ok") + assert.equal(executed, 1) + assert.equal(getSingleFlightStats().mode, "memory") +}) + +test("auto: 探测到 SQL 驱动 → db", async () => { + __resetSingleFlightForTest() + const driver = createMockSqlDriver() + const out = await singleflight("auto-db", async () => "ok", { + mode: "auto", + driver, + env: {}, + }) + + assert.equal(out, "ok") + assert.equal(getSingleFlightStats().mode, "db") + assert.equal(driver.rows.get("auto-db")!.state, "done") +}) + +test("环境变量 SINGLEFLIGHT 可强制模式,非法值回退 auto", async () => { + __resetSingleFlightForTest() + let executed = 0 + const fn = async () => { + executed++ + await delay(15) + return executed + } + + // off:3 次调用全部执行 + await Promise.all( + Array.from({ length: 3 }, () => + singleflight("env-off", fn, { env: { SINGLEFLIGHT: "off" } }), + ), + ) + assert.equal(executed, 3) + assert.equal(getSingleFlightStats().mode, "off") + + __resetSingleFlightForTest() + executed = 0 + // 非法值 → auto → 无 SQL 驱动 → memory(仍然合并) + await Promise.all( + Array.from({ length: 3 }, () => + singleflight("env-bad", fn, { env: { SINGLEFLIGHT: "nonsense" } }), + ), + ) + assert.equal(executed, 1) + assert.equal(getSingleFlightStats().mode, "memory") +}) diff --git a/src/backend/pkg/singleflight.ts b/src/backend/pkg/singleflight.ts new file mode 100644 index 00000000..29e191bc --- /dev/null +++ b/src/backend/pkg/singleflight.ts @@ -0,0 +1,783 @@ +/** + * singleflight(单飞)—— 把「同一时刻的同一个调用」合并成一次执行。 + * + * ## 为什么 TS 侧需要它(Go 侧不需要) + * + * Go 后端是**单进程**模型,`golang.org/x/sync/singleflight` 用一个进程内 Map + * 就够了:同一时刻只有一个 goroutine 真正执行,其余等待者共享结果。 + * + * 本仓库跑在 Cloudflare Workers / EdgeOne Edge Functions 上,**一个部署会同时 + * 存在多个 isolate / 实例**,且请求会被随机分发到不同 isolate。进程内 Map 只能 + * 合并「落在同一个 isolate 且时间重叠」的调用,跨 isolate 的重复请求依然会各自 + * 打一次上游网盘 API —— 这正是限流、封号、CPU 超时的主要来源。 + * + * ## 两级去重 + * + * L1 进程内(永远开启):`Map`,零成本,同一 isolate 内直接复用 + * 同一个 Promise。任何模式下都生效。 + * L2 跨实例(仅 db 模式):用数据库表 `x_singleflight` 做协调 —— 抢到行的一方 + * 执行,其余实例轮询该行,执行完成后**直接读取结果**, + * 因此上游只被调用一次。 + * + * ## 模式(环境变量 SINGLEFLIGHT) + * + * auto(默认)有 SQL 库(d1 / mysql / do)→ db;否则 → memory + * db 强制走数据库表协调;协调层出错时自动降级为直接执行 + * memory 仅进程内合并(等价于 Go 的进程内 singleflight) + * off 关闭去重,每次调用都直接执行 + * + * ## 可用性优先 + * + * 单飞是**优化**而非**依赖**:任何协调层面的异常(表不存在、DB 超时、结果无法 + * 序列化、执行者崩溃、等待超时)都不会让业务失败,最坏情况退化为「各自执行一次」。 + * 但**业务函数自身的异常会原样抛出**,且不会被误判为协调失败而重试 —— 见 + * `FlightOutcome` 的三态设计。 + */ + +import type { Driver } from "../internal/model/store/types" +import { getStorageBackend } from "../internal/model/store/backend" +import { singleflightTableName } from "../internal/model/store/schema" + +/** 单飞模式。 */ +export type SingleFlightMode = "off" | "memory" | "db" + +/** 模式 + auto(由运行时探测决定实际模式)。 */ +export type SingleFlightModeInput = SingleFlightMode | "auto" + +/** 支持 SQL 的驱动子集(d1 / mysql / do 具备 query + execute)。 */ +type SqlDriverLike = Pick & + Partial> + +/** 单次调用的可选项(多数场景无需传,用默认值 + 环境变量即可)。 */ +export interface SingleFlightOptions { + /** 覆盖模式;默认取环境变量 SINGLEFLIGHT(缺省 auto)。 */ + mode?: SingleFlightModeInput + /** 环境上下文(用于读取环境变量、探测存储驱动)。 */ + env?: any + /** + * 显式指定用于协调的 SQL 驱动。 + * 一般不需要:留空时自动取当前存储后端(见 getStorageBackend)。 + */ + driver?: SqlDriverLike | null + /** + * 结果交接窗口(ms,默认 1000)。 + * + * 执行完成后,结果/错误在表中保留这么久,供**正在等待的**并发实例读取。 + * 它**不是**结果缓存:新到达的调用若看到已结束的记录,会回收该行并重新执行 + * (见 dbFlight 注释),因此不存在「窗口内读到旧数据」的脏读。 + * 设 0 表示执行完立即释放,此时等待方基本拿不到结果、会各自执行一次。 + */ + handoffMs?: number + /** + * 执行者持有锁的最长时间(ms,默认 30000)。 + * 期间执行者会按 1/3 周期续租;若执行者崩溃,锁到期后其他实例可接管。 + */ + lockTtlMs?: number + /** + * 等待方的轮询间隔(ms,默认 200)。 + * + * 刻意不放太小:等待方每 tick 都是一次对协调表的 SELECT,间隔过小会显著 + * 消耗 D1 的 subrequest 配额与 CPU(最坏情况 ≈ waitTimeoutMs / pollMs 次 + * SELECT × 最多 3 轮)。默认 200ms 下单个等待请求最多约 75 次 SELECT/轮, + * 目录列表 / 元信息这类上游调用通常远快于此,多等一两个 tick 无感。 + */ + pollMs?: number + /** + * 等待方最长等待时间(ms,默认 15000)。 + * 超时后不再等待,自行执行(保证请求不会因为别人慢而被拖死)。 + */ + waitTimeoutMs?: number +} + +/** 运行期统计,用于 /debug/info 与排障。 */ +export interface SingleFlightStats { + /** 最近一次生效的模式(off/memory/db)。 */ + mode: SingleFlightMode + /** 真正执行了业务函数的次数。 */ + executed: number + /** 被进程内合并掉的次数(L1 命中)。 */ + coalesced: number + /** 从数据库表读到他人结果的次数(L2 命中)。 */ + shared: number + /** DB 协调异常而降级的次数。 */ + fallback: number + /** 业务函数抛错的次数。 */ + failed: number + /** 当前在途键数量。 */ + active: number +} + +// ── 默认值与环境变量 ──────────────────────────────────────────────────────── + +const DEFAULT_HANDOFF_MS = 1000 +const DEFAULT_LOCK_TTL_MS = 30_000 +const DEFAULT_POLL_MS = 200 +const DEFAULT_WAIT_TIMEOUT_MS = 15_000 + +/** 读取配置:env 绑定优先,其次 process.env(Node / 本地开发)。 */ +function readEnvValue(env: any, name: string): string | undefined { + try { + const fromEnv = env && typeof env === "object" ? env[name] : undefined + if (fromEnv !== undefined && fromEnv !== null && String(fromEnv) !== "") { + return String(fromEnv) + } + } catch { + // env 可能是 Proxy 等异常对象,忽略 + } + try { + const proc = typeof process !== "undefined" ? (process as any) : undefined + const v = proc?.env?.[name] + if (v !== undefined && v !== null && String(v) !== "") return String(v) + } catch { + // 忽略 + } + return undefined +} + +function readEnvInt(env: any, name: string, fallback: number): number { + const raw = readEnvValue(env, name) + if (raw === undefined) return fallback + const n = parseInt(raw, 10) + return Number.isFinite(n) && n >= 0 ? n : fallback +} + +/** 解析 SINGLEFLIGHT 环境变量(非法值按 auto 处理并告警一次)。 */ +let warnedBadMode = false +function readModeInput(env: any): SingleFlightModeInput { + const raw = readEnvValue(env, "SINGLEFLIGHT")?.trim().toLowerCase() + if (!raw) return "auto" + if (raw === "auto") return "auto" + if (raw === "off" || raw === "false" || raw === "0" || raw === "none") { + return "off" + } + if (raw === "memory" || raw === "mem" || raw === "local" || raw === "proc") { + return "memory" + } + if (raw === "db" || raw === "database" || raw === "sql" || raw === "table") { + return "db" + } + if (!warnedBadMode) { + warnedBadMode = true + console.warn( + `[singleflight] Unknown SINGLEFLIGHT="${raw}", falling back to auto ` + + `(expected one of: auto, off, memory, db)`, + ) + } + return "auto" +} + +// ── 实例标识 ──────────────────────────────────────────────────────────────── + +/** + * 本实例(isolate / 进程)的随机标识。 + * + * 用途:只有写入该值的执行者才能回写结果,避免「锁被接管后,原执行者迟到写回」 + * 造成结果错乱。 + */ +const INSTANCE_ID = (() => { + try { + const c: any = typeof crypto !== "undefined" ? crypto : undefined + if (c && typeof c.randomUUID === "function") return c.randomUUID() + } catch { + // 忽略 + } + return `inst-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` +})() + +// ── 统计 ──────────────────────────────────────────────────────────────────── + +const stats: SingleFlightStats = { + mode: "memory", + executed: 0, + coalesced: 0, + shared: 0, + fallback: 0, + failed: 0, + active: 0, +} + +/** 当前统计快照(含当前在途键数量)。 */ +export function getSingleFlightStats(): SingleFlightStats { + return { ...stats, active: inflight.size } +} + +// ── L1:进程内合并 ────────────────────────────────────────────────────────── + +/** key → 在途 Promise。同一 isolate 内所有模式共用。 */ +const inflight = new Map>() + +/** + * 进程内合并:命中则复用已有 Promise;未命中则执行 factory 并登记。 + * + * 清理用 `then(onOk, onErr)` 而非 `finally()`:后者会产生一个「派生 Promise」, + * 若业务失败而调用方只 await 了原 Promise,派生 Promise 的 rejection 无人处理, + * 在 Node 下会触发 unhandledRejection 告警。 + */ +function coalesce(key: string, factory: () => Promise): Promise { + const existing = inflight.get(key) + if (existing) { + stats.coalesced++ + return existing as Promise + } + + const pending = factory() + inflight.set(key, pending) + const release = () => { + // 仅清理自己登记的那一个,避免误删后来者的在途项 + if (inflight.get(key) === pending) inflight.delete(key) + } + pending.then(release, release) + return pending +} + +// ── L2:数据库表协调 ──────────────────────────────────────────────────────── + +/** + * 协调层的结果三态。 + * + * 关键点:**业务异常与协调异常必须分开**。 + * - 业务异常:原样抛给调用方,绝不重试(否则上游会被打两次); + * - 协调异常:降级为「自己直接执行一次」,保证业务可用。 + * 用三态返回值而非异常标记,可避免给用户错误对象打标记(可能被冻结)的坑。 + */ +type FlightOutcome = + | { kind: "value"; value: T } + /** 执行失败;error 为业务函数抛出的原始异常,或等待方收到的远端失败原因 */ + | { kind: "business-error"; error: unknown } + /** 抢锁 / 轮询 / 写回等协调环节出错 —— 调用方应降级为直接执行 */ + | { kind: "coordination-error"; error: unknown } + +/** 反引号包裹标识符(SQLite 与 MySQL 均支持)。 */ +function q(name: string): string { + return "`" + name + "`" +} + +function supportsSql(driver: any): driver is SqlDriverLike { + return ( + !!driver && + typeof driver.query === "function" && + typeof driver.execute === "function" + ) +} + +/** SQL 方言:mysql 驱动用 `INSERT IGNORE`,其余(d1 / do,均 SQLite)用 `INSERT OR IGNORE`。 */ +function insertIgnoreClause(driver: SqlDriverLike): string { + return String(driver.name || "").toLowerCase() === "mysql" + ? "INSERT IGNORE" + : "INSERT OR IGNORE" +} + +/** 解析用于协调的 SQL 驱动;不可用时返回 null(调用方降级为内存级)。 */ +async function resolveSqlDriver( + env: any, + explicit: SqlDriverLike | null | undefined, +): Promise { + if (explicit !== undefined) return explicit + try { + const { driver } = await getStorageBackend(env) + return supportsSql(driver) ? driver : null + } catch { + // 无可用存储后端(如 serverless 未绑定)→ 内存级 + return null + } +} + +interface ResolvedOptions { + mode: SingleFlightMode + /** 解析出的协调驱动(仅 db 模式有值)。 */ + driver: SqlDriverLike | null + handoffMs: number + lockTtlMs: number + pollMs: number + waitTimeoutMs: number +} + +async function resolveOptions( + options: SingleFlightOptions, +): Promise { + const env = options.env + const input = options.mode ?? readModeInput(env) + + // 只在需要时才探测存储驱动:off / memory 模式完全不需要碰存储层。 + const needsDriver = input === "auto" || input === "db" + const driver = needsDriver + ? await resolveSqlDriver(env, options.driver) + : null + + const mode: SingleFlightMode = + input === "auto" ? (driver ? "db" : "memory") : input + + return { + mode, + driver, + handoffMs: + options.handoffMs ?? + readEnvInt(env, "SINGLEFLIGHT_HANDOFF_MS", DEFAULT_HANDOFF_MS), + lockTtlMs: + options.lockTtlMs ?? + readEnvInt(env, "SINGLEFLIGHT_LOCK_TTL_MS", DEFAULT_LOCK_TTL_MS), + pollMs: + options.pollMs ?? + readEnvInt(env, "SINGLEFLIGHT_POLL_MS", DEFAULT_POLL_MS), + waitTimeoutMs: + options.waitTimeoutMs ?? + readEnvInt(env, "SINGLEFLIGHT_WAIT_MS", DEFAULT_WAIT_TIMEOUT_MS), + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** + * 抢占 key。 + * + * 三步走,全部依赖数据库自身的原子性(不依赖「读后写」,因此无需事务): + * 1. 回收可回收的行 —— 崩溃残留的过期 running 行,以及**已结束**的 + * done/error 行(它们只是留给等待方读取的交接记录,谁先来谁回收); + * 2. `INSERT OR IGNORE` —— 主键冲突即表示已有他人在执行,我们成为等待方; + * 3. 回读 owner 确认归属 —— 避免「插入被忽略但仍以为自己是执行者」。 + * + * 注意第 1 步的删除条件:**未过期的 running 行绝不能被删**,否则正在执行的 + * 调用会被并发接管,单飞形同虚设。 + */ +async function dbAcquire( + driver: SqlDriverLike, + table: string, + key: string, + owner: string, + opts: ResolvedOptions, + env: any, +): Promise { + const t = q(table) + const now = Date.now() + + await driver.execute!( + `DELETE FROM ${t} WHERE ${q("key")} = ? AND (` + + `${q("expires_at")} <= ? OR ${q("state")} <> ?)`, + [key, now, "running"], + env, + ) + + await driver.execute!( + `${insertIgnoreClause(driver)} INTO ${t} (` + + `${q("key")},${q("owner")},${q("state")},${q("started_at")},${q("expires_at")},` + + `${q("result")},${q("error")}) VALUES (?,?,?,?,?,NULL,NULL)`, + [key, owner, "running", now, now + opts.lockTtlMs], + env, + ) + + const rows = await driver.query!( + `SELECT ${q("owner")} FROM ${t} WHERE ${q("key")} = ?`, + [key], + env, + ) + return rows.length > 0 && String((rows[0] as any).owner) === owner +} + +/** + * 执行者续租:把 expires_at 往后推。 + * + * 为什么需要:目录很大时上游 list 可能超过 lockTtl,若不续租,等待方会误判 + * 「执行者已崩溃」而接管,导致上游被调用两次 —— 恰好是单飞要消除的行为。 + */ +function startHeartbeat( + driver: SqlDriverLike, + table: string, + key: string, + owner: string, + opts: ResolvedOptions, + env: any, +): any { + try { + const interval = Math.max(1000, Math.floor(opts.lockTtlMs / 3)) + const timer = setInterval(() => { + Promise.resolve( + driver.execute!( + `UPDATE ${q(table)} SET ${q("expires_at")} = ? ` + + `WHERE ${q("key")} = ? AND ${q("owner")} = ? AND ${q("state")} = ?`, + [Date.now() + opts.lockTtlMs, key, owner, "running"], + env, + ), + ).catch(() => { + // 续租失败不影响业务:最坏情况是被接管后多执行一次 + }) + }, interval) + // Node 下不要因为心跳定时器而阻止进程退出(Workers 无此方法,可选调用) + ;(timer as any)?.unref?.() + return timer + } catch { + // 运行时不支持定时器(极端环境):放弃续租,退化为「锁到期可被接管」 + return null + } +} + +/** 停止心跳;定时器不可用时为 no-op。 */ +function stopHeartbeat(timer: any): void { + if (timer === null || timer === undefined) return + try { + clearInterval(timer) + } catch { + // 忽略 + } +} + +/** + * 执行者写回结果。 + * + * `WHERE key = ? AND owner = ?` 是必需的:若锁已被接管(原执行者卡顿超过 + * lockTtl 被他人接管),迟到写回必须作废,否则会覆盖接管者的正确结果。 + * + * 写回失败只告警不抛出:协调是尽力而为,业务结果已拿到,不该因为写不进去而失败。 + */ +async function dbPublish( + driver: SqlDriverLike, + table: string, + key: string, + owner: string, + state: "done" | "error", + result: string | null, + error: string | null, + opts: ResolvedOptions, + env: any, +): Promise { + const t = q(table) + try { + if (opts.handoffMs <= 0) { + // 交接窗口为 0:直接释放,让等待方立刻可以重新抢锁 + await driver.execute!( + `DELETE FROM ${t} WHERE ${q("key")} = ? AND ${q("owner")} = ?`, + [key, owner], + env, + ) + return + } + await driver.execute!( + `UPDATE ${t} SET ${q("state")} = ?, ${q("result")} = ?, ${q("error")} = ?, ` + + `${q("expires_at")} = ? WHERE ${q("key")} = ? AND ${q("owner")} = ?`, + [state, result, error, Date.now() + opts.handoffMs, key, owner], + env, + ) + } catch (err) { + console.warn( + `[singleflight] Failed to publish result for "${key}" (ignored):`, + err instanceof Error ? err.message : err, + ) + } +} + +/** 等待方的等待结果。 */ +type WaitOutcome = + | { status: "ok"; value: any } + | { status: "error"; error: string } + /** 行消失 / 执行者疑似崩溃 / 等待超时 —— 交给调用方自行处理 */ + | { status: "give-up" } + +/** 表中某个 key 的当前行(null 表示不存在)。 */ +interface ProbeRow { + state: string + result: any + error: any + expires_at: number +} + +async function dbProbe( + driver: SqlDriverLike, + table: string, + key: string, + env: any, +): Promise { + const rows = await driver.query!( + `SELECT ${q("state")},${q("result")},${q("error")},${q("expires_at")} ` + + `FROM ${q(table)} WHERE ${q("key")} = ?`, + [key], + env, + ) + if (rows.length === 0) return null + const row: any = rows[0] + return { + state: String(row.state ?? ""), + result: row.result, + error: row.error, + expires_at: Number(row.expires_at), + } +} + +/** + * 轮询等待执行者写回结果。 + * + * 仅在「确认执行者处于 running 状态」后调用,因此这里读到 done/error 就是 + * 本次并发去重的目标结果。 + */ +async function dbWait( + driver: SqlDriverLike, + table: string, + key: string, + opts: ResolvedOptions, + env: any, +): Promise { + const deadline = Date.now() + opts.waitTimeoutMs + + for (;;) { + const now = Date.now() + if (now >= deadline) return { status: "give-up" } + await sleep(Math.max(1, Math.min(opts.pollMs, deadline - now))) + + const row = await dbProbe(driver, table, key, env) + // 行已被回收(交接记录被新到达者回收,或执行者主动释放)→ 不再等待 + if (!row) return { status: "give-up" } + + if (row.state === "done") { + if (row.result === null || row.result === undefined) { + return { status: "give-up" } + } + try { + return { status: "ok", value: JSON.parse(String(row.result)) } + } catch { + // 结果不可反序列化(理论上不该发生)→ 自行执行 + return { status: "give-up" } + } + } + if (row.state === "error") { + return { status: "error", error: String(row.error ?? "") } + } + // running 且已过期 → 执行者疑似崩溃 + if (row.expires_at <= Date.now()) return { status: "give-up" } + } +} + +/** 执行者路径:真正调用业务函数,并把结果/异常写回表。 */ +async function dbLead( + driver: SqlDriverLike, + table: string, + key: string, + owner: string, + fn: () => Promise, + opts: ResolvedOptions, + env: any, +): Promise> { + const heartbeat = startHeartbeat(driver, table, key, owner, opts, env) + + let value: T + try { + value = await fn() + } catch (err) { + stopHeartbeat(heartbeat) + await dbPublish( + driver, + table, + key, + owner, + "error", + null, + err instanceof Error ? err.message : String(err), + opts, + env, + ) + return { kind: "business-error", error: err } + } + stopHeartbeat(heartbeat) + + // 结果必须可 JSON 往返才能共享给其他实例;不可序列化时直接释放锁, + // 让等待方自行执行(本次调用方仍然拿到正确的返回值)。 + let payload: string | null = null + try { + payload = JSON.stringify(value === undefined ? null : value) + } catch { + payload = null + } + if (payload === null) { + await dbPublish( + driver, + table, + key, + owner, + "done", + null, + null, + { + ...opts, + handoffMs: 0, + }, + env, + ) + } else { + await dbPublish(driver, table, key, owner, "done", payload, null, opts, env) + } + return { kind: "value", value } +} + +/** + * 数据库表协调的完整流程。 + * + * ## 关键判定:什么情况下才接受「别人的结果」 + * + * 只有在**本调用到达时执行者正处于 running 状态**时,才等待并接受它的结果 —— + * 这代表两者确实并发重叠,是真正的「单飞去重」。 + * + * 反过来,如果到达时该 key 上已经是一行**已结束**的 done/error(上一轮调用留下的 + * 交接记录),本次调用**绝不接受**它,而是回收该行、自己成为新的执行者。 + * + * 这一点是「不引入脏读」的核心:交接记录因此只是给**正在等待的**并发者读取的, + * 不会变成一个有 TTL 的结果缓存。否则会出现「删除文件后 1 秒内刷新列表仍看到 + * 被删文件」这类问题(handoffMs 窗口内命中旧结果)。 + * + * @returns 三态结果,由调用方决定是否降级 + */ +async function dbFlight( + driver: SqlDriverLike, + key: string, + fn: () => Promise, + opts: ResolvedOptions, + env: any, +): Promise> { + const table = singleflightTableName(env) + const owner = INSTANCE_ID + + try { + // 最多三轮:足够覆盖「探测到 running → 等待」「等待落空 → 接管」 + // 「抢占失败(别人刚抢到)→ 再探测」这几种交错。 + for (let attempt = 0; attempt < 3; attempt++) { + const row = await dbProbe(driver, table, key, env) + + if (row && row.state === "running" && row.expires_at > Date.now()) { + // 到达时正在执行 → 并发去重,等待并接受其结果 + const wait = await dbWait(driver, table, key, opts, env) + if (wait.status === "ok") { + stats.shared++ + return { kind: "value", value: wait.value as T } + } + if (wait.status === "error") { + // 共享他人的失败:与 Go singleflight 语义一致,等待方同样收到错误, + // 而不是各自再打一次上游(那正是要消除的行为)。 + stats.shared++ + return { + kind: "business-error", + error: new Error(wait.error || "singleflight: shared call failed"), + } + } + // 执行者崩溃 / 等待超时 → 下一轮尝试接管 + continue + } + + // 无行,或行已结束(done/error 交接记录)→ 回收并抢占,成为新执行者 + if (await dbAcquire(driver, table, key, owner, opts, env)) { + stats.executed++ + const outcome = await dbLead(driver, table, key, owner, fn, opts, env) + if (outcome.kind === "business-error") stats.failed++ + return outcome + } + // 抢占失败:别人刚成为执行者 → 下一轮会探测到 running 并等待 + } + return { kind: "coordination-error", error: null } + } catch (err) { + return { kind: "coordination-error", error: err } + } +} + +// ── 公开 API ──────────────────────────────────────────────────────────────── + +/** + * 以 `key` 为粒度执行 `fn`,同一时刻相同 key 只执行一次。 + * + * @param key 去重键。**必须能唯一标识一次调用的语义**, + * 例如 `fs.list:12:/movies`(存储 id + 物理路径)。 + * 不同 env / 不同存储务必使用不同 key。 + * @param fn 业务函数。注意其返回值需要可 JSON 往返(db 模式下要共享给 + * 其他实例),因此不要返回函数、Symbol、循环引用等。 + * @param options 可选覆盖项,见 SingleFlightOptions。 + * + * @example + * const items = await singleflight(`fs.list:${storage.id}:${path}`, () => + * driver.list(path), + * ) + */ +export async function singleflight( + key: string, + fn: () => Promise, + options: SingleFlightOptions = {}, +): Promise { + const opts = await resolveOptions(options) + stats.mode = opts.mode + + if (opts.mode === "off") { + stats.executed++ + try { + return await fn() + } catch (err) { + stats.failed++ + throw err + } + } + + // L1:进程内合并。db 模式同样先走这一步 —— 同一 isolate 内没必要为同一个 + // key 反复往返数据库。 + return coalesce(key, async () => { + if (opts.mode === "memory") { + stats.executed++ + try { + return await fn() + } catch (err) { + stats.failed++ + throw err + } + } + + const driver = opts.driver + if (!driver) { + // 探测不到 SQL 驱动 → 降级为内存级(L1 仍然生效) + stats.fallback++ + stats.executed++ + try { + return await fn() + } catch (err) { + stats.failed++ + throw err + } + } + + const outcome = await dbFlight(driver, key, fn, opts, options.env) + switch (outcome.kind) { + case "value": + return outcome.value + case "business-error": + throw outcome.error + default: + // 协调失败 → 降级为直接执行,业务照常可用 + stats.fallback++ + stats.executed++ + try { + return await fn() + } catch (err) { + stats.failed++ + throw err + } + } + }) +} + +/** + * 仅测试用:清空进程内在途表与统计,避免用例间互相污染。 + */ +export function __resetSingleFlightForTest(): void { + inflight.clear() + stats.mode = "memory" + stats.executed = 0 + stats.coalesced = 0 + stats.shared = 0 + stats.fallback = 0 + stats.failed = 0 + warnedBadMode = false +} + +/** + * 仅测试用:暴露 SQL 方言分支(mysql 与 sqlite 的 INSERT 语法差异)。 + */ +export function __singleFlightInsertIgnoreForTest(driverName: string): string { + return insertIgnoreClause({ name: driverName }) +} + +/** 仅测试用:暴露实例标识,便于断言 owner 写入。 */ +export function __singleFlightInstanceIdForTest(): string { + return INSTANCE_ID +} diff --git a/src/backend/server/debug.ts b/src/backend/server/debug.ts index ee32a805..80e0f1c2 100644 --- a/src/backend/server/debug.ts +++ b/src/backend/server/debug.ts @@ -1,6 +1,7 @@ import { Hono } from "hono" import { getDb, getStoreStatus } from "../internal/model/db" import { checkAdminAuth } from "../pkg/utils" +import { getSingleFlightStats } from "../pkg/singleflight" export const debugRouter = new Hono() @@ -13,6 +14,9 @@ debugRouter.get("/info", async (c) => { timestamp: new Date().toISOString(), // 后端驱动信息非敏感,未登录也返回,便于确认 D1/KV/MySQL 是否生效 store: await getStoreStatus(c.env), + // singleflight 去重统计:用于确认跨实例协调是否真的生效 + // (coalesced/shared 持续为 0 说明没有并发重复调用,或协调未生效) + singleflight: getSingleFlightStats(), } if (isAdmin) { diff --git a/wrangler.jsonc b/wrangler.jsonc index 6ca4a1dd..6f54c357 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -82,6 +82,29 @@ // 自动生成并持久化共享密钥(JWT 签名需要)。 "DB_CIPHER": "none", + // ── singleflight(并发相同调用去重)────────────────────────────────── + // + // 一个部署会同时存在多个 isolate,进程内去重无法跨实例生效,因此当存在 + // SQL 库(d1 / mysql / do)时用表 `x_singleflight` 做跨实例协调。 + // auto(默认)- 有 SQL 库用表,否则退回进程内(memory) + // db - 强制用表协调;协调层出错时自动降级,不影响业务 + // memory - 仅进程内合并(等价 Go 的进程内 singleflight) + // off - 关闭去重,每次调用都直接执行 + "SINGLEFLIGHT": "auto", + + // 执行完成后结果在表中保留多久(ms),供**正在等待的**并发实例读取。 + // 它不是结果缓存:新到达的调用会回收旧记录并重新执行,不会读到过期数据。 + // "SINGLEFLIGHT_HANDOFF_MS": "1000", + // + // 执行者持锁上限(ms);期间按 1/3 周期续租,执行者崩溃后到期可被接管。 + // "SINGLEFLIGHT_LOCK_TTL_MS": "30000", + // + // 等待方轮询间隔(ms) + // "SINGLEFLIGHT_POLL_MS": "200", + // + // 等待方最长等待(ms);超时后自行执行,避免被慢请求拖死 + // "SINGLEFLIGHT_WAIT_MS": "15000", + // ── 安全鉴权(推荐以 Secret 类型配置,见 .dev.vars.example)────────── // // JWT 签名密钥(>=16 字符,兼作可选的字段加密密钥与定时任务鉴权) From bb2db24bf50cacc84e6acf1ce6b69eec59027735 Mon Sep 17 00:00:00 2001 From: Wudarensheng Date: Thu, 15 Oct 2026 23:01:14 +0800 Subject: [PATCH 16/49] feat(cache): add file-tree and download-link caches (DB by default) (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(cache): add file-tree and download-link caches (DB by default) Implement the two-level cache from the OpenList.ts reference: - File-tree cache (`ft`): directory listings are served from cache, keyed by storage + virtual path. Empty directories are cached too, so repeated browsing no longer hits the remote drive every time. - Download-link cache (`ln`): the raw_url returned by the driver is reused instead of being re-signed/re-exchanged on every download, which is usually the most expensive and most rate-limited step. Caching is DB-only by default. `CACHE_DRIVER=db` (the default) reuses the backend resolved by `getStorageBackend()`, i.e. the same one as `DB_DRIVER`, so zero configuration means "cache in the database". Dedicated KV / Blob / cfkv / do / memory backends must be opted into explicitly via `CACHE_DRIVER` (e.g. `db,kv`, `kv`, `blob`) and are never auto-detected or auto-substituted, matching the existing DB_DRIVER rule that an explicitly configured driver never falls back. Safety boundary: only driver-layer results are cached (raw FileItem lists and raw links). Permissions, meta passwords, hide rules and signatures are still computed per request in server/fs.ts and server/raw.ts, so a cache hit cannot leak privileges. Any cache error degrades to a no-op and never breaks the request. Invalidation: writes (mkdir/rename/remove/move/copy/put) invalidate the affected path plus its parent directory for the file tree, and the path itself for links. Storage create/update/enable/disable/delete clears that storage's cache entirely. New environment variables (all optional): CACHE_ENABLED, CACHE_DRIVER, CACHE_FILE_TREE, CACHE_DOWNLOAD_LINK, CACHE_TTL, CACHE_LINK_TTL, CACHE_EXCLUDE_DRIVERS, CACHE_PREFIX. New admin endpoints: GET /cache/status, POST /cache/clear, POST /storage/refresh, POST /storage/refresh_one. * chore(git): ignore .workbuddy-ai/ local assistant data The local assistant keeps its project memory under .workbuddy-ai/, which must never be committed. Ignore it next to the existing .codebuddy entry. * Delete docs/pr-cache.md * fix(cache): 缓存键单射编码、补上传失效、前缀校验、链接缓存默认关闭 - encodeCachePathSegment 改为单射编码:转义前缀 x 本身转义为 x78, 消除 /a/b 与 /ax2fb 键碰撞(评审实测复现的跨目录串列表/直链问题); 超长路径折叠标记用 xg + 双 FNV-1a,xg 不可能出现在普通编码结果中, 折叠键与普通键不会互相碰撞;补参数化「任意不同路径键必不相等」回归 - 补缓存失效:/fs/upload/complete、/fs/multipart/complete、/fs/other、 /fs/get_direct_upload_info(含 other 回退分支)写入/签发直传 URL 后 调用 invalidatePaths,避免「上传成功但刷新看不到」 - CACHE_PREFIX 字符集校验:仅允许 [A-Za-z0-9_](KV 键约束),非法回退 默认值并告警,防止与业务键空间 / 整表 DELETE 冲突 - CACHE_DOWNLOAD_LINK 默认改为 false(维护者建议):部分网盘直链 TTL 极短,缓存复用易把失效直链发给用户;CACHE_LINK_TTL 保持独立选项, 同步 .env.example / .dev.vars.example / wrangler.jsonc / README * fix(fs): 请求级存储上下文始终传递 env,修复缓存静默换后端 getStorageRequestContext() 原先在缺少 executionCtx.waitUntil 时直接返回 undefined,把 env 一并丢掉。缓存(CACHE_DRIVER=db)与 singleflight (SINGLEFLIGHT=db)都靠它解析后端:拿不到 env 时 getStorageBackend(undefined) 会解析成内存驱动,而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与数据库 被静默拆到两个后端。 表现为「列表看似命中缓存(内容陈旧),但持久层里一条缓存键都没有、 /cache/status 计数恒为 0、/cache/clear 永远 removed=0」,排查成本很高。 真正依赖 executionCtx 的只有 waitUntil,缺失时降级为同步等待 (scheduleStoragePersistence 已支持该路径),业务语义不变;另加一条 warn-once 提示该降级,避免同类问题再次静默。 新增回归测试 src/backend/server/storage_request_context.test.ts:刻意不传 app.request() 的 executionCtx,断言缓存键必须落到 CACHE_DRIVER=db 指向的 真实后端(修复前该用例失败)。 * fix(kv): list() 同时支持 Cloudflare 与 EdgeOne 的分页语义 在真实 Cloudflare Workers 上跑起来才暴露:binding 模式的 list() 只按 EdgeOne 语义解析(keys[].key + page.complete),而 Cloudflare KV 返回的是 keys[].name + list_complete + cursor ⇒ 一条都取不到,且首轮就被判为 complete,恒返回 []。 缓存模块的 cacheListKeys() 依赖本方法,于是「缓存统计恒为 0、 清空缓存 / 刷新文件树永远 removed=0」在 Cloudflare Workers 上静默失效 —— 而键其实早已写进 KV(实测:列目录 271ms → 命中缓存 36ms,KV 中确实存在 openlist_cache_ft_* 键)。 改为两套语义都支持:键名取 item.key ?? item.name;翻页终止看 list_complete || complete;下一页游标优先用 page.cursor,其次取本页最后一个 键;游标无进展即退出,避免语义不符时死循环;并用 Set 去重。 新增 src/backend/internal/model/store/driver/kv_list.test.ts,覆盖两套语义、 cursor 翻页、空结果、游标不前进时的终止性,以及无绑定时不静默返回 []。 * Revert "fix(kv): list() 同时支持 Cloudflare 与 EdgeOne 的分页语义" 同一问题已由 PR #127 (fix/125-kv-binding-list) 以更完整的方式修复:除 binding list 兼容 Cloudflare 语义外,还收敛了 format/key.ts 的 list 配额消耗,并加了 重复页检测。为避免两个 PR 重复修改 driver/kv.ts 并互相冲突,这里把该修复从本 PR 移除,交由 #127 负责。 本 PR 因此只专注缓存功能。注意:在 #127 合入之前,Cloudflare Workers 上缓存的 统计(GET /api/admin/cache/status)与清空(POST /api/admin/cache/clear、 POST /api/admin/storage/refresh)仍受该既有缺陷影响 —— 键能正常写入与命中, 但列举不到,属于既有问题,非本 PR 引入。 This reverts commit fb96e659aebb926e55cf3a24b0fdb536df301b10. --- .dev.vars.example | 23 + .env.example | 23 + .gitignore | 1 + README.md | 73 +++ package.json | 27 +- src/backend/internal/cache/cache.test.ts | 454 ++++++++++++++++++ src/backend/internal/cache/config.ts | 267 ++++++++++ src/backend/internal/cache/filetree.ts | 176 +++++++ src/backend/internal/cache/index.ts | 127 +++++ src/backend/internal/cache/link.ts | 138 ++++++ src/backend/internal/cache/store.ts | 443 +++++++++++++++++ src/backend/internal/op/storage.ts | 101 +++- src/backend/server/admin.ts | 127 +++++ src/backend/server/fs.ts | 87 +++- src/backend/server/public.ts | 25 + src/backend/server/raw.ts | 49 +- .../server/storage_request_context.test.ts | 156 ++++++ wrangler.jsonc | 37 ++ 18 files changed, 2287 insertions(+), 47 deletions(-) create mode 100644 src/backend/internal/cache/cache.test.ts create mode 100644 src/backend/internal/cache/config.ts create mode 100644 src/backend/internal/cache/filetree.ts create mode 100644 src/backend/internal/cache/index.ts create mode 100644 src/backend/internal/cache/link.ts create mode 100644 src/backend/internal/cache/store.ts create mode 100644 src/backend/server/storage_request_context.test.ts diff --git a/.dev.vars.example b/.dev.vars.example index d4b84dd1..f0cccc54 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -84,6 +84,29 @@ CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= +# ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── +# 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 +# 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER +# (不会自动探测、不会自动回退)。详见 README「缓存配置」。 +# +# 总开关,默认 true +# CACHE_ENABLED=true +# 缓存后端,默认 db;可选 db / kv / blob / cfkv / do / memory / none, +# 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 db,kv +# CACHE_DRIVER=db +# 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 +# TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) +# CACHE_FILE_TREE=true +# CACHE_DOWNLOAD_LINK=false +# 文件树缓存时长(分钟),0 = 跟随存储级 cache_expiration(默认 30) +# CACHE_TTL=0 +# 下载链接缓存时长(分钟),直链自带有效期,取值需保守(默认 5) +# CACHE_LINK_TTL=5 +# 不参与缓存的驱动(纯本地计算型) +# CACHE_EXCLUDE_DRIVERS=virtual,alias,url_tree,strm,chunk +# 缓存键前缀(与业务数据键隔离;仅允许 [A-Za-z0-9_],非法时回退默认值) +# CACHE_PREFIX=openlist_cache + # singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 # 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 # auto(默认)- 有 SQL 库用表,否则退回进程内(memory) diff --git a/.env.example b/.env.example index 9583c4b0..0c3ee2e4 100644 --- a/.env.example +++ b/.env.example @@ -82,6 +82,29 @@ CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= +# ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── +# 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 +# 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER +# (不会自动探测、不会自动回退)。详见 README「缓存配置」。 +# +# 总开关,默认 true +# CACHE_ENABLED=true +# 缓存后端,默认 db;可选 db / kv / blob / cfkv / do / memory / none, +# 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 db,kv +# CACHE_DRIVER=db +# 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 +# TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) +# CACHE_FILE_TREE=true +# CACHE_DOWNLOAD_LINK=false +# 文件树缓存时长(分钟),0 = 跟随存储级 cache_expiration(默认 30) +# CACHE_TTL=0 +# 下载链接缓存时长(分钟),直链自带有效期,取值需保守(默认 5) +# CACHE_LINK_TTL=5 +# 不参与缓存的驱动(纯本地计算型) +# CACHE_EXCLUDE_DRIVERS=virtual,alias,url_tree,strm,chunk +# 缓存键前缀(与业务数据键隔离;仅允许 [A-Za-z0-9_],非法时回退默认值) +# CACHE_PREFIX=openlist_cache + # singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 # 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 # auto(默认)- 有 SQL 库用表,否则退回进程内(memory) diff --git a/.gitignore b/.gitignore index d1d7d199..6a43fe57 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ public_data .idea .test .codebuddy +.workbuddy-ai .vscode .env .env.local diff --git a/README.md b/README.md index 27afc671..68c587f4 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,79 @@ CF_API_KEY=your_api_token 前缀固定为 `x_`(与 Go 后端默认值一致)。要与 Go 后端共享同一物理数据库,无需额外配置。 +#### 缓存配置 + +本项目实现了两级持久缓存(参考 [OpenList.ts](https://github.com/Wudarensheng/OpenList.ts)): + +- **文件树缓存**:`/api/fs/list`、`/api/fs/get`、`/api/fs/dirs` 优先从缓存读取目录树, + 只有缓存未命中(或已过期)才会去请求网盘 / 对象存储; +- **下载链接缓存**:`/d`、`/p` 等下载链路复用驱动换取的直链, + 重复下载 / 预览时不再重新签名或重新换链(网盘换链通常最贵、最易被限流)。 + +**默认只向数据库启用**:缓存与业务数据走同一条存储链路(即 `DB_DRIVER` 指向的 +后端),因此「什么都不配」就等于「用数据库做缓存」。想要改用 / 额外启用 KV、 +Blob 等专用后端,**必须显式添加环境变量**(不会自动探测、不会自动回退): + +**CACHE_ENABLED**(总开关) +- `true`(默认):启用两级缓存 +- `false`:完全关闭(等同于 `CACHE_DRIVER=none`) + +**CACHE_DRIVER**(缓存后端) +- `db`(默认):与业务数据同一个后端(`DB_DRIVER` 解析结果,如 D1 / KV / Blob / MySQL) +- `kv`:Cloudflare KV 或 EdgeOne KV(binding 名固定为 `KV`) +- `blob`:EdgeOne Blob SDK / ESA Blob +- `cfkv`:Cloudflare KV REST API(需 `CF_ACCOUNT`、`CF_KV_UUID`、`CF_API_KEY`) +- `do`:Cloudflare Durable Objects +- `memory`:进程内存(仅本地调试,重启即失) +- `none`:关闭缓存 +- 支持逗号分隔的多后端(**读按顺序命中,写全部铺开**),例如: + - `db,kv` —— 数据库 + KV 双写,读优先命中 KV + - `kv,blob` —— 只用两个专用后端 +- 显式配置但该后端不可用时:**跳过并告警,不会悄悄换成别的后端**; + 若最终一个都不剩,缓存自动降级为「不缓存」(请求照常走真实存储)。 + +**CACHE_FILE_TREE** / **CACHE_DOWNLOAD_LINK**(分级开关) +- `CACHE_FILE_TREE` 默认 `true`;设为 `false` 可单独关闭文件树缓存 +- `CACHE_DOWNLOAD_LINK` 默认 **`false`**:部分网盘的直链 TTL 很短(分钟级 + 甚至更短),缓存复用容易把已失效的直链发给用户(表现为 403/404)。 + 确认所用网盘的直链有效期足够长后,再显式设为 `true` 开启 + +**CACHE_TTL**(文件树缓存时长,分钟) +- `0`(默认):跟随存储级 `cache_expiration`(默认 30 分钟, + 可被该存储的 `custom_cache_policies` 按路径覆盖) +- `>0`:全局覆盖所有存储的文件树缓存时长 +- 存储级 `cache_expiration=0` 或路径级策略命中 `0` 时,该目录**永不缓存** + +**CACHE_LINK_TTL**(下载链接缓存时长,分钟) +- 默认 `5`。直链通常自带有效期,取值需保守:缓存过久会把已失效的链接 + 交给浏览器(表现为 403/404)。设为 `0` 可关闭链接缓存。 + +**CACHE_EXCLUDE_DRIVERS**(不参与缓存的驱动) +- 默认 `virtual,alias,url_tree,strm,chunk` —— 这些驱动不产生远程 IO, + 缓存没有收益且只会让结果变陈旧 +- 传空字符串表示不排除任何驱动 + +**CACHE_PREFIX**(缓存键前缀) +- 默认 `openlist_cache`,用于与业务数据键(`openlist_config`、`users_1` 等)隔离 +- 仅允许 `[A-Za-z0-9_]`(最长 64 字符):前缀会成为 KV 键的一部分,含其他 + 字符时所有缓存写入都会失败;非法取值回退默认值并告警 + +**失效与一致性:** +- 写操作(mkdir / rename / remove / move / copy / put)会**立即失效**受影响的 + 路径与其父目录缓存; +- 存储配置变更(更新 / 启用 / 禁用 / 删除)会清空该存储的全部缓存; +- 缓存只存「驱动层结果」,权限、meta 密码、隐藏规则、下载签名都在请求时 + 实时计算,因此**缓存不会造成越权**。 + +**管理接口:** +- `GET /api/admin/cache/status` —— 生效配置、实际后端、各级缓存条目数 +- `POST /api/admin/cache/clear` —— body `{ type?: "file_tree" | "download_link", storage_id?: number }` +- `POST /api/admin/storage/refresh` —— 刷新(清空)全部存储的文件树缓存 +- `POST /api/admin/storage/refresh_one?id=` —— 刷新单个存储的文件树缓存 + +> 缓存状态也会回显在 `/api/public/env_check` 的 `cache` 字段中, +> 便于确认「缓存实际落在哪个后端」。 + #### 安全配置 - `JWT_SECRET`:JWT 令牌签名密钥(必填),**同时用作可选的字段加密密钥**与定时任务鉴权 diff --git a/package.json b/package.json index 9dbe8a48..21f2a00b 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,30 @@ "DB_CIPHER": { "description": "At-rest cipher for sensitive fields (drive credentials, 2FA secrets, password hashes): `none` (default, no encryption), `aes-256-gcm` (HKDF-derived AES-256-GCM key — the `enc:v2:` envelope written by existing encrypted deployments, recommended), `aes-256-gcm-pbkdf2` (legacy `enc:v1:` envelope, PBKDF2 per field, slow), `aes-256-cbc-hmac` (`enc:v3:`), `chacha20-poly1305` (`enc:v4:`, RFC 8439, pure-JS implementation), `des-cbc-hmac` / `3des-cbc-hmac` (`enc:v5:`/`enc:v6:`, compatibility only — single DES is 56-bit and 3DES is deprecated, do not use for real data). Ciphertexts carry an `enc:vN:` prefix and are decrypted by prefix on read, so switching back to `none` (or changing algorithms) never makes existing data unreadable — it only changes how newly written data is stored (legacy ciphertext is migrated to plaintext on the next save). Key derivation is cached per isolate, and unchanged fields are not re-encrypted on save. This setting does NOT affect the shared secret: with `none`, a secret is still generated and persisted during setup when JWT_SECRET is not provided, because JWT signing needs it." }, + "CACHE_ENABLED": { + "description": "Optional. Master switch for the file-tree cache and the download-link cache. Defaults to `true`. Set to `false` to disable both." + }, + "CACHE_DRIVER": { + "description": "Optional. Where the caches are stored. Defaults to `db`, meaning the same backend as DB_DRIVER (D1/KV/Blob/MySQL) — i.e. only the database is used unless you opt in explicitly. Comma-separated list to add dedicated backends: `kv`, `blob`, `cfkv`, `do`, `memory`, or `none` to disable. Example: `db,kv` (read prefers KV) or `kv` (KV only). Dedicated backends are never auto-detected or auto-substituted." + }, + "CACHE_FILE_TREE": { + "description": "Optional. Toggle the file-tree cache (directory listings). Defaults to `true`. Listings are cached per storage+path; only admin writes invalidate them." + }, + "CACHE_DOWNLOAD_LINK": { + "description": "Optional. Toggle the download-link cache (presigned/raw download URLs returned by drivers). Defaults to `false`: some drives mint links with a very short TTL, so cached reuse can hand out already-expired links. Enable explicitly (`true`) once you confirmed your drive's link lifetime is long enough." + }, + "CACHE_TTL": { + "description": "Optional. File-tree cache lifetime in minutes. Defaults to `0`, which means 'follow the per-storage cache_expiration' (30 minutes, overridable per path by custom_cache_policies). Set to 0 to never cache a storage whose cache_expiration is 0." + }, + "CACHE_LINK_TTL": { + "description": "Optional. Download-link cache lifetime in minutes. Defaults to `5`. Keep it conservative: raw URLs usually carry their own expiry, and a stale link is served to the browser as 403/404." + }, + "CACHE_EXCLUDE_DRIVERS": { + "description": "Optional. Comma-separated drivers excluded from caching. Defaults to `virtual,alias,url_tree,strm,chunk` (locally computed drivers where caching only adds staleness)." + }, + "CACHE_PREFIX": { + "description": "Optional. Key prefix used to isolate cache entries from business data. Defaults to `openlist_cache`. Only [A-Za-z0-9_] (max 64 chars) are allowed; invalid values fall back to the default." + }, "SINGLEFLIGHT": { "description": "Dedup of concurrent identical calls (singleflight). `auto` (default) uses the `x_singleflight` table when a SQL backend (d1/mysql/do) is available, otherwise falls back to in-process merging. `db` forces table coordination, `memory` keeps it per-isolate, `off` disables dedup." } @@ -58,11 +82,12 @@ "test:server": "tsx --test \"src/backend/server/*.test.ts\"", "test:store": "tsx --test src/backend/internal/model/store/store.test.ts", "test:model": "tsx --test \"src/backend/internal/model/*.test.ts\"", + "test:cache": "tsx --test \"src/backend/internal/cache/*.test.ts\"", "test:pkg": "tsx --test \"src/backend/pkg/*.test.ts\"", "test:regress": "tsx scripts/_regress.mjs", "test:deploy": "node scripts/test-deploy.js", "env:check": "tsx scripts/env-check.mjs", - "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:pkg && npm run test:regress", + "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:cache && npm run test:pkg && npm run test:regress", "sls:deploy": "serverless deploy", "sls:package": "serverless package", "sls:info": "serverless info", diff --git a/src/backend/internal/cache/cache.test.ts b/src/backend/internal/cache/cache.test.ts new file mode 100644 index 00000000..771689b4 --- /dev/null +++ b/src/backend/internal/cache/cache.test.ts @@ -0,0 +1,454 @@ +import assert from "node:assert/strict" +import { test } from "node:test" + +/** + * 缓存模块回归测试(文件树缓存 / 下载链接缓存)。 + * + * 锁定以下契约: + * 1. 默认只向数据库启用(CACHE_DRIVER 缺省 → `db`);KV/Blob 等专用后端 + * 必须显式配置,未配置时绝不启用; + * 2. 开关语义:CACHE_ENABLED / CACHE_FILE_TREE / CACHE_DOWNLOAD_LINK; + * 3. 键名对 KV 合法(EdgeOne 只接受 [A-Za-z0-9_]),且超长路径被确定性压缩; + * 4. 文件树缓存命中时返回副本(避免调用方就地修改污染缓存); + * 5. 过期条目视为未命中; + * 6. 下载链接缓存只缓存「确实拿到直链的文件」,目录 / 空直链不缓存; + * 7. clearCache 能按存储 id 过滤,避免 storages_1_ 误伤 storages_10_。 + * + * 测试通过 __setCacheDriversForTest 注入内存假驱动,完全不依赖真实存储后端。 + */ + +const mod = await import("./index") +const { + buildCacheKey, + cacheGetEnvelope, + cacheSetEnvelope, + clearCache, + encodeCachePathSegment, + expandToInvalidationPaths, + getCacheConfig, + getCachedFileTree, + getCachedLink, + parseCacheBackends, + parseCachePrefix, + setCachedFileTree, + setCachedLink, + storageIdFromCacheKey, + __resetCacheConfigForTest, + __resetCacheStoreForTest, + __setCacheDriversForTest, +} = mod as any + +/** 内存假驱动(满足 Driver 接口的键值部分)。 */ +function createFakeDriver(name = "fake") { + const store = new Map() + return { + name, + __store: store, + async isAvailable() { + return true + }, + async init() {}, + async get(key: string) { + return store.get(key) ?? null + }, + async put(key: string, value: string) { + store.set(key, value) + }, + async delete(key: string) { + store.delete(key) + }, + async list(prefix: string) { + return [...store.keys()].filter((k) => k.startsWith(prefix)) + }, + async health() { + return { connected: true } + }, + } +} + +function setup(driver: any = createFakeDriver()) { + __resetCacheConfigForTest() + __resetCacheStoreForTest() + __setCacheDriversForTest(async () => [driver]) + return driver +} + +const SAMPLE_ITEMS = [ + { name: "a.txt", size: 1, is_dir: false, modified: "2026-01-01", sign: "", type: 0 }, + { name: "sub", size: 0, is_dir: true, modified: "2026-01-01", sign: "", type: 1 }, +] + +// ───────────────────────── 配置 ───────────────────────── + +test("配置:默认只向数据库启用", () => { + __resetCacheConfigForTest() + const cfg = getCacheConfig({}) + assert.equal(cfg.enabled, true) + assert.deepEqual(cfg.backends, ["db"]) + assert.equal(cfg.fileTree, true) + // 下载链接缓存默认关闭:部分网盘直链 TTL 极短,复用易发出失效直链 + assert.equal(cfg.downloadLink, false) + // 默认 TTL:文件树跟随存储级 cache_expiration(0 = 不覆盖) + assert.equal(cfg.ttlMinutes, 0) + assert.equal(cfg.linkTtlMinutes, 5) +}) + +test("配置:CACHE_DRIVER 解析(db / kv / 多后端 / none / 未知值)", () => { + assert.deepEqual(parseCacheBackends(""), ["db"]) + assert.deepEqual(parseCacheBackends("kv"), ["kv"]) + assert.deepEqual(parseCacheBackends("db,kv"), ["db", "kv"]) + assert.deepEqual(parseCacheBackends("KV , BLOB"), ["kv", "blob"]) + assert.deepEqual(parseCacheBackends("none"), []) + // 未知值被忽略,且不会把列表清空(回退 db) + assert.deepEqual(parseCacheBackends("nope"), ["db"]) + assert.deepEqual(parseCacheBackends("nope,kv"), ["kv"]) +}) + +test("配置:开关与 TTL 读取", () => { + __resetCacheConfigForTest() + const cfg = getCacheConfig({ + CACHE_ENABLED: "false", + CACHE_FILE_TREE: "0", + CACHE_DOWNLOAD_LINK: "off", + CACHE_TTL: "12", + CACHE_LINK_TTL: "1", + CACHE_DRIVER: "db,kv", + }) + assert.equal(cfg.enabled, false) + assert.equal(cfg.fileTree, false) + assert.equal(cfg.downloadLink, false) + assert.equal(cfg.ttlMinutes, 12) + assert.equal(cfg.linkTtlMinutes, 1) + assert.deepEqual(cfg.backends, ["db", "kv"]) +}) + +test("配置:CACHE_DRIVER=none 时整体关闭", () => { + __resetCacheConfigForTest() + const cfg = getCacheConfig({ CACHE_DRIVER: "none" }) + assert.equal(cfg.enabled, false) + assert.deepEqual(cfg.backends, []) +}) + +test("配置:CACHE_PREFIX 字符集校验(非法回退默认值)", () => { + // 合法:字母数字下划线,最长 64 + assert.equal(parseCachePrefix(""), "openlist_cache") + assert.equal(parseCachePrefix(" my_cache_2 "), "my_cache_2") + assert.equal(parseCachePrefix("a".repeat(64)), "a".repeat(64)) + // 非法:空格 / 斜杠 / 冒号 / 连字符 / 超长 —— 一律回退默认值 + assert.equal(parseCachePrefix("my cache"), "openlist_cache") + assert.equal(parseCachePrefix("cache/v2"), "openlist_cache") + assert.equal(parseCachePrefix("cache:v2"), "openlist_cache") + assert.equal(parseCachePrefix("my-cache"), "openlist_cache") + assert.equal(parseCachePrefix("a".repeat(65)), "openlist_cache") +}) + +test("配置:默认排除纯本地计算型驱动", () => { + __resetCacheConfigForTest() + const cfg = getCacheConfig({}) + assert.ok(cfg.excludeDrivers.has("virtual")) + assert.ok(cfg.excludeDrivers.has("alias")) + assert.ok(cfg.excludeDrivers.has("urltree")) +}) + +// ───────────────────────── 键名编码 ───────────────────────── + +test("键名:KV 合法字符集 + 存储 id 可解析", () => { + const key = buildCacheKey("ft", 1, "/a b/c%d.txt", "openlist_cache") + assert.match(key, /^[A-Za-z0-9_]+$/, "键名必须只含 [A-Za-z0-9_]") + assert.equal(storageIdFromCacheKey(key, "ft", "openlist_cache"), "1") + // 同输入必得同键(失效时才能算出一致的键) + assert.equal(key, buildCacheKey("ft", 1, "/a b/c%d.txt", "openlist_cache")) +}) + +test("键名:storageId 解析不受前缀内下划线影响", () => { + const key = buildCacheKey("ln", 42, "/x", "my_cache_prefix") + assert.equal(storageIdFromCacheKey(key, "ln", "my_cache_prefix"), "42") + // kind 不同不会串味 + assert.equal(storageIdFromCacheKey(key, "ft", "my_cache_prefix"), null) +}) + +test("键名:超长路径被确定性压缩且仍在合法字符集内", () => { + const long = "/" + Array.from({ length: 60 }, (_, i) => `dir${i}`).join("/") + const a = encodeCachePathSegment(long) + const b = encodeCachePathSegment(long) + assert.equal(a, b) + assert.ok(a.length <= 180, "长路径应被压缩") + assert.match(a, /^[A-Za-z0-9_]+$/) + // 不同长路径不应碰撞 + assert.notEqual(a, encodeCachePathSegment(long + "x")) +}) + +test("键名:编码单射 —— 任意不同路径的键互不相等(键碰撞回归)", () => { + // 评审实测复现的碰撞对:转义前缀 x 未转义时,/a/b 与 /ax2fb 曾得到同一个键, + // 导致按 /ax2fb 的权限判定返回 /a/b 的内容(跨目录串列表 / 直链) + assert.notEqual( + encodeCachePathSegment("/a/b"), + encodeCachePathSegment("/ax2fb"), + "/a/b 与 /ax2fb 不得碰撞", + ) + assert.notEqual( + encodeCachePathSegment("/a/b.txt"), + encodeCachePathSegment("/ax2fb.txt"), + ) + + // 参数化:覆盖转义前缀字面量、转义序列字面量、分隔符、大小写、unicode、 + // 普通编码与长路径折叠分支的交叉 + const long = "/" + Array.from({ length: 60 }, (_, i) => `dir${i}`).join("/") + const longAlt = "/" + Array.from({ length: 59 }, (_, i) => `dir${i}`).concat("dir58x").join("/") + const paths = [ + "/", + "/a", + "/a/b", + "/ax2fb", + "/a/b.txt", + "/ax2fb.txt", + "/x/y", + "/xx2fy", + "/secret/report.pdf", + "/secretx2freport.pdf", + "x", + "/x2f", + "/x78", + "/xx", + "/X/x", // 大写 X 无歧义,原样保留 + "/照片/2024/img.jpg", + "/a b/c%d.txt", + long, + longAlt, // 与 long 折叠后前 100 字符相同,仅靠哈希区分 + long + "x", + ] + const keys = paths.map((p) => encodeCachePathSegment(p)) + + // 全部在 KV 合法字符集内 + for (const k of keys) { + assert.match(k, /^[A-Za-z0-9_]+$/, `键不在合法字符集内: ${k}`) + } + // 两两互不相等(O(n²),n=20) + for (let i = 0; i < paths.length; i++) { + for (let j = i + 1; j < paths.length; j++) { + assert.notEqual( + keys[i], + keys[j], + `缓存键碰撞: "${paths[i]}" vs "${paths[j]}"`, + ) + } + } +}) + +test("失效路径展开:包含父目录", () => { + assert.deepEqual(expandToInvalidationPaths("/a/b/c.txt").sort(), ["/a/b", "/a/b/c.txt"]) + assert.deepEqual(expandToInvalidationPaths("/x").sort(), ["/", "/x"]) +}) + +// ───────────────────────── 存储层 ───────────────────────── + +test("存储层:写入后可读出,过期条目视为未命中并被清理", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db" } + + await cacheSetEnvelope("k1", { hello: "world" }, 60_000, env) + const hit = await cacheGetEnvelope("k1", env) + assert.deepEqual(hit?.v, { hello: "world" }) + + // 手工写入一条已过期条目 + await driver.put("k2", JSON.stringify({ v: 1, ts: 1, exp: Date.now() - 1000 })) + assert.equal(await cacheGetEnvelope("k2", env), null) + // 惰性清理是 fire-and-forget,等一个 tick 再断言 + await new Promise((r) => setTimeout(r, 0)) + assert.equal(await driver.get("k2"), null, "过期条目应被惰性清理") +}) + +test("存储层:ttl<=0 时不写入", async () => { + const driver = setup() + await cacheSetEnvelope("k", 1, 0, {}) + assert.equal(await driver.get("k"), null) +}) + +// ───────────────────────── 文件树缓存 ───────────────────────── + +test("文件树缓存:命中时返回深拷贝副本", async () => { + setup() + const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } + const target = { storage: { id: 1, driver: "fake" }, cleanPath: "/a" } + + await setCachedFileTree(target, SAMPLE_ITEMS as any, env) + const hit = await getCachedFileTree(target, env) + assert.ok(hit) + assert.equal(hit!.length, 2) + assert.equal(hit![0].name, "a.txt") + + // 必须是副本:就地修改不能回写缓存 + assert.notEqual(hit, SAMPLE_ITEMS) + assert.notEqual(hit![0], SAMPLE_ITEMS[0]) + hit![0].name = "MUTATED" + const again = await getCachedFileTree(target, env) + assert.equal(again![0].name, "a.txt") +}) + +test("文件树缓存:空目录也会被缓存(避免反复击穿存储)", async () => { + setup() + const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } + const target = { storage: { id: 7, driver: "fake" }, cleanPath: "/empty" } + await setCachedFileTree(target, [], env) + const hit = await getCachedFileTree(target, env) + assert.ok(Array.isArray(hit)) + assert.equal(hit!.length, 0) +}) + +test("文件树缓存:cache_expiration=0(不缓存)时读写都被跳过", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db" } + const target = { + storage: { id: 2, driver: "fake", cache_expiration: 0 }, + cleanPath: "/nocache", + } + await setCachedFileTree(target, SAMPLE_ITEMS as any, env) + assert.equal(await getCachedFileTree(target, env), null) + assert.equal(driver.__store.size, 0, "不应产生任何缓存键") +}) + +test("文件树缓存:排除名单中的驱动不缓存", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } + const target = { storage: { id: 3, driver: "virtual" }, cleanPath: "/v" } + await setCachedFileTree(target, SAMPLE_ITEMS as any, env) + assert.equal(await getCachedFileTree(target, env), null) + assert.equal(driver.__store.size, 0) +}) + +test("文件树缓存:CACHE_FILE_TREE=false 时关闭", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db", CACHE_FILE_TREE: "false", CACHE_TTL: "30" } + const target = { storage: { id: 4, driver: "fake" }, cleanPath: "/a" } + await setCachedFileTree(target, SAMPLE_ITEMS as any, env) + assert.equal(await getCachedFileTree(target, env), null) + assert.equal(driver.__store.size, 0) +}) + +test("文件树缓存:custom_cache_policies 命中 0 分钟即不缓存", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db" } + const target = { + storage: { + id: 5, + driver: "fake", + cache_expiration: 30, + custom_cache_policies: JSON.stringify([ + { path: "/private/*", cache_expiration: 0 }, + ]), + }, + cleanPath: "/private/x", + } + await setCachedFileTree(target, SAMPLE_ITEMS as any, env) + assert.equal(await getCachedFileTree(target, env), null) + assert.equal(driver.__store.size, 0) +}) + +// ───────────────────────── 下载链接缓存 ───────────────────────── + +const FILE_ITEM = { + name: "movie.mp4", + size: 100, + is_dir: false, + modified: "2026-01-01", + sign: "", + type: 2, + raw_url: "https://cdn.example.com/movie.mp4?sig=abc", +} + +test("下载链接缓存:缓存拿到直链的文件,并返回副本", async () => { + setup() + // 链接缓存默认关闭(直链 TTL 风险),用例显式开启 + const env = { CACHE_DRIVER: "db", CACHE_DOWNLOAD_LINK: "true" } + const target = { storage: { id: 10, driver: "fake" }, cleanPath: "/movie.mp4" } + + await setCachedLink(target, FILE_ITEM as any, env) + const hit = await getCachedLink(target, env) + assert.ok(hit) + assert.equal(hit!.raw_url, FILE_ITEM.raw_url) + assert.notEqual(hit, FILE_ITEM) + + hit!.raw_url = "MUTATED" + const again = await getCachedLink(target, env) + assert.equal(again!.raw_url, FILE_ITEM.raw_url) +}) + +test("下载链接缓存:目录 / 空直链不缓存", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db" } + const dirTarget = { storage: { id: 11, driver: "fake" }, cleanPath: "/dir" } + const emptyTarget = { storage: { id: 11, driver: "fake" }, cleanPath: "/empty" } + + await setCachedLink(dirTarget, { ...FILE_ITEM, is_dir: true } as any, env) + await setCachedLink(emptyTarget, { ...FILE_ITEM, raw_url: "" } as any, env) + + assert.equal(await getCachedLink(dirTarget, env), null) + assert.equal(await getCachedLink(emptyTarget, env), null) + assert.equal(driver.__store.size, 0) +}) + +test("下载链接缓存:CACHE_LINK_TTL=0 时关闭", async () => { + const driver = setup() + const env = { CACHE_DRIVER: "db", CACHE_LINK_TTL: "0" } + const target = { storage: { id: 12, driver: "fake" }, cleanPath: "/movie.mp4" } + await setCachedLink(target, FILE_ITEM as any, env) + assert.equal(await getCachedLink(target, env), null) + assert.equal(driver.__store.size, 0) +}) + +// ───────────────────────── 清理 ───────────────────────── + +test("clearCache:按存储 id 过滤,不误伤相邻 id", async () => { + setup() + const env = { CACHE_DRIVER: "db", CACHE_TTL: "30", CACHE_DOWNLOAD_LINK: "true" } + await setCachedFileTree( + { storage: { id: 1, driver: "fake" }, cleanPath: "/a" }, + SAMPLE_ITEMS as any, + env, + ) + await setCachedFileTree( + { storage: { id: 10, driver: "fake" }, cleanPath: "/a" }, + SAMPLE_ITEMS as any, + env, + ) + await setCachedLink( + { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, + FILE_ITEM as any, + env, + ) + + const removed = await clearCache(env, { storageId: 1 }) + assert.equal(removed, 2, "只应清掉 storage 1 的两条缓存") + + // storage 10 的文件树缓存仍在 + const survivor = await getCachedFileTree( + { storage: { id: 10, driver: "fake" }, cleanPath: "/a" }, + env, + ) + assert.ok(survivor) + + const rest = await clearCache(env, {}) + assert.equal(rest, 1) +}) + +test("clearCache:按 kind 只清一类", async () => { + setup() + const env = { CACHE_DRIVER: "db", CACHE_TTL: "30", CACHE_DOWNLOAD_LINK: "true" } + await setCachedFileTree( + { storage: { id: 1, driver: "fake" }, cleanPath: "/a" }, + SAMPLE_ITEMS as any, + env, + ) + await setCachedLink( + { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, + FILE_ITEM as any, + env, + ) + assert.equal(await clearCache(env, { kind: "ft" }), 1) + assert.ok( + await getCachedLink( + { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, + env, + ), + ) +}) diff --git a/src/backend/internal/cache/config.ts b/src/backend/internal/cache/config.ts new file mode 100644 index 00000000..39a77ce6 --- /dev/null +++ b/src/backend/internal/cache/config.ts @@ -0,0 +1,267 @@ +/** + * 缓存配置(文件树缓存 / 下载链接缓存)—— 完全由环境变量驱动。 + * + * 设计目标(对齐参考实现 OpenList.ts 的两级缓存): + * 1. **文件树缓存**:`/fs/list`、`/fs/get`、`/fs/dirs` 优先命中缓存, + * 避免每次浏览都去请求网盘 / 对象存储; + * 2. **下载链接缓存**:`/d`、`/p` 等下载链路把驱动返回的直链缓存起来, + * 避免重复签名 / 重复换取临时链接。 + * + * 默认行为(**只向数据库启用**): + * 缓存内容与业务数据走同一条存储链路(`getStorageBackend()` 解析出的 + * driver,即 DB_DRIVER 指向的 d1 / mysql / kv / cfkv / blob / do), + * 因此「什么都不配」就等于「用数据库做缓存」。 + * + * 想要改用 / 额外启用 KV、Blob 等专用后端时,**必须显式添加环境变量** + * CACHE_DRIVER=kv # 只用 KV 缓存 + * CACHE_DRIVER=db,kv # 数据库 + KV 双写(读优先命中 KV) + * CACHE_DRIVER=blob,cfkv # 多个专用后端 + * 未显式配置的专用后端绝不会被自动启用,避免「以为缓存在 KV、实际写进了 + * 数据库」这类不可见行为(与 DB_DRIVER 的「显式配置不回退」哲学一致)。 + * + * 支持的环境变量一览: + * + * | 变量 | 默认值 | 说明 | + * | :-- | :-- | :-- | + * | `CACHE_ENABLED` | `true` | 总开关,`false` 时两级缓存全部关闭 | + * | `CACHE_DRIVER` | `db` | 缓存后端列表,逗号分隔:`db` / `kv` / `blob` / `cfkv` / `do` / `memory` / `none` | + * | `CACHE_FILE_TREE` | `true` | 文件树缓存开关 | + * | `CACHE_DOWNLOAD_LINK` | `false` | 下载链接缓存开关。默认**关闭**:部分网盘的直链 TTL 很短(分钟级甚至更短),缓存复用容易把已失效的直链发给用户;确认所用网盘直链有效期足够长后再显式开启 | + * | `CACHE_TTL` | `0` | 文件树缓存时长(分钟);`0` = 跟随存储级 `cache_expiration`(默认 30) | + * | `CACHE_LINK_TTL` | `5` | 下载链接缓存时长(分钟);直链通常带有效期,不宜过长 | + * | `CACHE_EXCLUDE_DRIVERS` | `virtual,alias,url_tree,strm,chunk` | 不参与缓存的驱动(本地计算型,缓存只会带来陈旧) | + * | `CACHE_PREFIX` | `openlist_cache` | 缓存键前缀,用于与业务数据键隔离;仅允许 `[A-Za-z0-9_]`,非法时回退默认值 | + */ + +/** 可用的缓存后端名。`db` = 与业务数据同一个后端(默认)。 */ +export type CacheBackendName = + | "db" + | "kv" + | "blob" + | "cfkv" + | "do" + | "memory" + | "none" + +/** 合法后端名(`none` 用于显式关闭) */ +export const VALID_CACHE_BACKENDS: readonly CacheBackendName[] = [ + "db", + "kv", + "blob", + "cfkv", + "do", + "memory", + "none", +] as const + +/** + * 默认排除的驱动:这些驱动不产生远程 IO(纯本地计算/别名映射), + * 缓存不但没有收益,还会因为它们的输入(如 url_structure)变更而变陈旧。 + * 可通过 `CACHE_EXCLUDE_DRIVERS` 覆盖(传空字符串表示不排除任何驱动)。 + */ +const DEFAULT_EXCLUDE_DRIVERS = ["virtual", "alias", "url_tree", "strm", "chunk"] + +/** 存储级 cache_expiration 缺省值(与 driver/storageopts.ts 保持一致) */ +export const DEFAULT_FILE_TREE_TTL_MINUTES = 30 + +/** 下载直链的默认缓存时长(分钟):直链通常带签名有效期,取值需保守 */ +export const DEFAULT_LINK_TTL_MINUTES = 5 + +/** 已解析的缓存配置 */ +export interface CacheConfig { + /** 总开关 */ + enabled: boolean + /** 缓存后端列表(已去掉 `none`;为空表示关闭) */ + backends: CacheBackendName[] + /** 文件树缓存开关 */ + fileTree: boolean + /** 下载链接缓存开关 */ + downloadLink: boolean + /** 文件树缓存时长(分钟);`0` 表示跟随存储级配置 */ + ttlMinutes: number + /** 下载链接缓存时长(分钟) */ + linkTtlMinutes: number + /** 不参与缓存的驱动(小写、去非字母数字) */ + excludeDrivers: Set + /** 缓存键前缀 */ + prefix: string +} + +/** 读取环境变量(同时兼容注入的 env 对象与 process.env)。 */ +function readRaw(key: string, env?: any): string { + const e = env && typeof env === "object" ? env : {} + const fromEnv = e[key] + if (fromEnv !== undefined && fromEnv !== null && String(fromEnv) !== "") { + return String(fromEnv) + } + if (typeof process !== "undefined" && process.env) { + const fromProcess = (process.env as any)[key] + if (fromProcess !== undefined && fromProcess !== null) { + return String(fromProcess) + } + } + return "" +} + +/** 解析布尔型环境变量(无法识别时回退默认值)。 */ +function readBool(key: string, fallback: boolean, env?: any): boolean { + const raw = readRaw(key, env).trim().toLowerCase() + if (!raw) return fallback + if (["1", "true", "yes", "on", "enable", "enabled"].includes(raw)) return true + if (["0", "false", "no", "off", "disable", "disabled"].includes(raw)) return false + return fallback +} + +/** 解析非负整数型环境变量(无法识别时回退默认值)。 */ +function readInt(key: string, fallback: number, env?: any): number { + const raw = readRaw(key, env).trim() + if (!raw) return fallback + const n = parseInt(raw, 10) + if (!Number.isFinite(n) || n < 0) return fallback + return n +} + +/** 驱动名归一化(与 storage.ts 的 normDriver 规则一致)。 */ +export function normalizeDriverName(name: string): string { + return String(name || "") + .toLowerCase() + .replace(/[^a-z0-9]/g, "") +} + +/** + * 解析 `CACHE_DRIVER`。 + * + * 规则: + * - 缺省 / 空 → `["db"]`(默认只向数据库启用); + * - 出现 `none` → 返回空数组(显式关闭); + * - 无法识别的取值会被忽略并告警一次(不静默改写成别的后端)。 + */ +export function parseCacheBackends(raw: string): CacheBackendName[] { + const tokens = String(raw || "") + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter(Boolean) + if (tokens.length === 0) return ["db"] + if (tokens.includes("none")) return [] + + const out: CacheBackendName[] = [] + const unknown: string[] = [] + for (const token of tokens) { + const norm = normalizeDriverName(token) + const match = VALID_CACHE_BACKENDS.find( + (b) => b !== "none" && normalizeDriverName(b) === norm, + ) + if (match) { + if (!out.includes(match)) out.push(match) + } else { + unknown.push(token) + } + } + if (unknown.length > 0) { + console.warn( + `[Cache] Unknown CACHE_DRIVER value(s): ${unknown.join(", ")}. ` + + `Valid values: ${VALID_CACHE_BACKENDS.join(", ")}. They were ignored.`, + ) + } + return out.length > 0 ? out : ["db"] +} + +/** 解析 `CACHE_EXCLUDE_DRIVERS`(默认排除纯本地计算型驱动)。 */ +export function parseExcludeDrivers(raw: string): Set { + const source = String(raw || "").trim() + const tokens = (source === "" ? DEFAULT_EXCLUDE_DRIVERS : source.split(",")) + .map((s) => normalizeDriverName(s)) + .filter(Boolean) + return new Set(tokens) +} + +/** 缓存键前缀默认值。 */ +const DEFAULT_CACHE_PREFIX = "openlist_cache" + +/** + * 解析 `CACHE_PREFIX`(缓存键前缀)。 + * + * 前缀会成为 KV 键的一部分,因此只允许 `[A-Za-z0-9_]`(EdgeOne KV 的键名 + * 约束):含其他字符时所有缓存写入都会失败。另外前缀与业务数据的 KV 键 + * (`_<主键>`)落在同一个键空间,若放纵任意字符(如空格、`/`、`:`), + * 既可能与表名段混淆、又可能让 `sqlFormat.save()` 的整表 DELETE 波及缓存行。 + * 非法取值一律回退默认值并告警。 + */ +export function parseCachePrefix(raw: string): string { + const value = String(raw || "").trim() + if (!value) return DEFAULT_CACHE_PREFIX + if (/^[A-Za-z0-9_]{1,64}$/.test(value)) return value + console.warn( + `[Cache] Invalid CACHE_PREFIX "${value}": only [A-Za-z0-9_] (max 64 chars) ` + + `are allowed. Falling back to "${DEFAULT_CACHE_PREFIX}".`, + ) + return DEFAULT_CACHE_PREFIX +} + +/** 计算用于「配置缓存」的指纹(配置一变即失效)。 */ +function configSignature(env?: any): string { + return [ + readRaw("CACHE_ENABLED", env), + readRaw("CACHE_DRIVER", env), + readRaw("CACHE_FILE_TREE", env), + readRaw("CACHE_DOWNLOAD_LINK", env), + readRaw("CACHE_TTL", env), + readRaw("CACHE_LINK_TTL", env), + readRaw("CACHE_EXCLUDE_DRIVERS", env), + readRaw("CACHE_PREFIX", env), + ].join("\u0001") +} + +let cachedConfig: { sig: string; config: CacheConfig } | null = null + +/** + * 读取缓存配置(按环境变量指纹做进程内记忆化)。 + * + * 纯同步、无 IO:缓存配置只来自环境变量,不依赖数据库, + * 因此可以在请求热路径上安全调用。 + */ +export function getCacheConfig(env?: any): CacheConfig { + const sig = configSignature(env) + if (cachedConfig && cachedConfig.sig === sig) return cachedConfig.config + + const backends = parseCacheBackends(readRaw("CACHE_DRIVER", env)) + const enabledRaw = readBool("CACHE_ENABLED", true, env) + const prefix = parseCachePrefix(readRaw("CACHE_PREFIX", env)) + + const config: CacheConfig = { + enabled: enabledRaw && backends.length > 0, + backends, + fileTree: readBool("CACHE_FILE_TREE", true, env), + // 默认关闭:部分网盘直链 TTL 极短,缓存复用容易把失效直链发给用户; + // 确认网盘直链有效期后可显式开启(CACHE_DOWNLOAD_LINK=true) + downloadLink: readBool("CACHE_DOWNLOAD_LINK", false, env), + ttlMinutes: readInt("CACHE_TTL", 0, env), + linkTtlMinutes: readInt("CACHE_LINK_TTL", DEFAULT_LINK_TTL_MINUTES, env), + excludeDrivers: parseExcludeDrivers(readRaw("CACHE_EXCLUDE_DRIVERS", env)), + prefix, + } + + cachedConfig = { sig, config } + return config +} + +/** 仅供测试:清除配置记忆化,避免用例间串味。 */ +export function __resetCacheConfigForTest(): void { + cachedConfig = null +} + +/** 供管理接口回显当前生效配置(脱敏,不含任何密钥)。 */ +export function describeCacheConfig(env?: any): Record { + const cfg = getCacheConfig(env) + return { + enabled: cfg.enabled, + driver: cfg.backends.join(","), + backends: cfg.backends, + file_tree: cfg.fileTree, + download_link: cfg.downloadLink, + ttl_minutes: cfg.ttlMinutes, + link_ttl_minutes: cfg.linkTtlMinutes, + exclude_drivers: [...cfg.excludeDrivers], + prefix: cfg.prefix, + } +} diff --git a/src/backend/internal/cache/filetree.ts b/src/backend/internal/cache/filetree.ts new file mode 100644 index 00000000..3cb68c1b --- /dev/null +++ b/src/backend/internal/cache/filetree.ts @@ -0,0 +1,176 @@ +/** + * 文件树缓存(File Tree Cache)。 + * + * 对齐参考实现 OpenList.ts 的 `files` / `file_cache` 表:浏览目录时优先从 + * 缓存读取,只有缓存未命中(或已过期)才去请求真实存储。 + * + * 缓存粒度与安全边界(重要): + * - 只缓存**驱动层**的 `driver.list()` 结果(原始 FileItem 列表), + * 不缓存任何与请求者身份相关的东西(权限、meta 密码、签名、hide 过滤 + * 都在 server/fs.ts 里按请求实时计算)。因此「管理员看过一次、普通用户 + * 随后访问」不会因为缓存而越权。 + * - 空目录同样会被缓存(值为 `[]`),避免反复击穿到存储。 + * - 写操作(mkdir/rename/remove/move/copy/put)后由调用方主动失效, + * 见 cache/index.ts 的 invalidatePaths()。 + */ +import type { FileItem } from "../driver/base" +import { resolveCacheExpiration } from "../driver/storageopts" +import { resolvePath } from "../model/db" +import { + DEFAULT_FILE_TREE_TTL_MINUTES, + getCacheConfig, + normalizeDriverName, +} from "./config" +import { + buildCacheKey, + cacheDelete, + cacheGetEnvelope, + cacheSetEnvelope, +} from "./store" + +/** 缓存目标:`resolvePath()` 的返回值中本模块关心的字段。 */ +export interface CacheTarget { + storage: any + cleanPath: string + physical?: string | null + isVirtual?: boolean +} + +/** + * 该目标是否可参与文件树缓存。 + * + * 排除条件: + * - 总开关 / 文件树开关关闭; + * - 虚拟路径(没有对应存储); + * - 驱动在 CACHE_EXCLUDE_DRIVERS 中(纯本地计算型,缓存只会变陈旧); + * - 计算出的 TTL <= 0(存储级 `cache_expiration=0` 或自定义策略命中 0, + * 语义即「不缓存」)。 + */ +export function isFileTreeCacheable( + target: CacheTarget | null | undefined, + env?: any, +): boolean { + const cfg = getCacheConfig(env) + if (!cfg.enabled || !cfg.fileTree) return false + if (!target?.storage || target.isVirtual) return false + const driver = normalizeDriverName(target.storage.driver) + if (cfg.excludeDrivers.has(driver)) return false + return resolveFileTreeTtlMinutes(target, env) > 0 +} + +/** + * 文件树缓存的 TTL(分钟)。 + * + * 优先级: + * 1. `CACHE_TTL`(显式全局覆盖,>0 时生效) + * 2. 存储级 `cache_expiration`(可被 `custom_cache_policies` 按路径覆盖) + * 3. 默认 30 分钟 + */ +export function resolveFileTreeTtlMinutes( + target: CacheTarget, + env?: any, +): number { + const cfg = getCacheConfig(env) + if (cfg.ttlMinutes > 0) return cfg.ttlMinutes + try { + const minutes = resolveCacheExpiration(target.storage, target.cleanPath) + return Number.isFinite(minutes) ? minutes : DEFAULT_FILE_TREE_TTL_MINUTES + } catch { + return DEFAULT_FILE_TREE_TTL_MINUTES + } +} + +function keyFor(target: CacheTarget, env?: any): string { + const cfg = getCacheConfig(env) + return buildCacheKey("ft", target.storage.id, target.cleanPath, cfg.prefix) +} + +/** + * 读取文件树缓存。未命中 / 已过期 / 任何异常都返回 null(由调用方回源)。 + */ +export async function getCachedFileTree( + target: CacheTarget | null | undefined, + env?: any, +): Promise { + if (!target || !isFileTreeCacheable(target, env)) return null + try { + const hit = await cacheGetEnvelope(keyFor(target, env), env) + if (!hit) return null + const items = hit.v + if (!Array.isArray(items)) return null + // 防御性拷贝:缓存对象可能被上层归一化逻辑就地修改(如补 type 字段), + // 直接返回同一引用会把修改带回缓存(多实例/多请求下表现为数据串味)。 + return items.map((item) => ({ ...item })) + } catch { + return null + } +} + +/** 写入文件树缓存(TTL <= 0 时跳过)。 */ +export async function setCachedFileTree( + target: CacheTarget | null | undefined, + items: FileItem[], + env?: any, +): Promise { + if (!target || !isFileTreeCacheable(target, env)) return + const ttlMinutes = resolveFileTreeTtlMinutes(target, env) + if (ttlMinutes <= 0) return + try { + await cacheSetEnvelope(keyFor(target, env), items, ttlMinutes * 60_000, env) + } catch { + // 缓存写入失败绝不影响业务 + } +} + +/** 删除单个路径的文件树缓存(不做路径解析,调用方已知 storage)。 */ +export async function deleteFileTreeCacheEntry( + storageId: any, + cleanPath: string, + env?: any, +): Promise { + const cfg = getCacheConfig(env) + await cacheDelete(buildCacheKey("ft", storageId, cleanPath, cfg.prefix), env) +} + +/** 把某个虚拟路径与其父目录一并加入待失效集合(父目录的列表必然变化)。 */ +export function expandToInvalidationPaths(virtualPath: string): string[] { + const norm = + "/" + + String(virtualPath || "") + .split("/") + .filter(Boolean) + .join("/") + const idx = norm.lastIndexOf("/") + const parent = idx <= 0 ? "/" : norm.slice(0, idx) + return parent === norm ? [norm] : [norm, parent] +} + +/** + * 失效若干虚拟路径对应的文件树缓存。 + * + * 每个路径会连同其父目录一起失效:例如 `/a/b/c.txt` 的增删改会让 `/a/b` + * 的目录列表变陈旧;若该路径本身是目录,它自己的列表也会失效。 + */ +export async function invalidateFileTree( + virtualPaths: string[], + env?: any, +): Promise { + const cfg = getCacheConfig(env) + if (!cfg.enabled || !cfg.fileTree) return + + const keys = new Set() + for (const p of virtualPaths) { + for (const candidate of expandToInvalidationPaths(p)) { + try { + const resolved = await resolvePath(candidate, env) + if (resolved.isVirtual || !resolved.storage) continue + keys.add( + buildCacheKey("ft", resolved.storage.id, resolved.cleanPath, cfg.prefix), + ) + } catch { + // 路径无法解析(存储被删除等):忽略,缓存会随 TTL 自然过期 + } + } + } + for (const key of keys) await cacheDelete(key, env) +} diff --git a/src/backend/internal/cache/index.ts b/src/backend/internal/cache/index.ts new file mode 100644 index 00000000..b0cb59e2 --- /dev/null +++ b/src/backend/internal/cache/index.ts @@ -0,0 +1,127 @@ +/** + * 缓存模块统一入口。 + * + * 两级缓存(对齐参考实现 OpenList.ts): + * - 文件树缓存:`filetree.ts`,浏览目录时命中缓存,避免每次请求网盘; + * - 下载链接缓存:`link.ts`,复用驱动换取的直链,避免重复签名/换链。 + * + * 后端选择:`store.ts`,默认只向数据库启用(`CACHE_DRIVER=db`), + * 显式配置 `CACHE_DRIVER=db,kv` / `kv` / `blob` 等才会启用专用后端。 + * + * 失效:写操作(mkdir/rename/remove/move/copy/put)后调用 `invalidatePaths()`, + * 同时失效这两级缓存;分片 / 直传上传的收尾路径(/fs/upload/complete、 + * /fs/multipart/complete、/fs/other、/fs/get_direct_upload_info)同样接入。 + */ +import { getCacheConfig, describeCacheConfig } from "./config" +import { clearCache, getCacheDrivers, isCacheActive } from "./store" +import { invalidateFileTree } from "./filetree" +import { invalidateLinks } from "./link" + +export { + getCacheConfig, + describeCacheConfig, + parseCacheBackends, + parseExcludeDrivers, + parseCachePrefix, + normalizeDriverName, + __resetCacheConfigForTest, + DEFAULT_FILE_TREE_TTL_MINUTES, + DEFAULT_LINK_TTL_MINUTES, + VALID_CACHE_BACKENDS, +} from "./config" +export type { CacheBackendName, CacheConfig } from "./config" + +export { + buildCacheKey, + cacheKindPrefix, + cacheDelete, + cacheGetEnvelope, + cacheListKeys, + cacheSetEnvelope, + clearCache, + encodeCachePathSegment, + getCacheDrivers, + isCacheActive, + storageIdFromCacheKey, + __resetCacheStoreForTest, + __setCacheDriversForTest, +} from "./store" +export type { CacheEnvelope, CacheKind } from "./store" + +export { + getCachedFileTree, + setCachedFileTree, + invalidateFileTree, + isFileTreeCacheable, + resolveFileTreeTtlMinutes, + deleteFileTreeCacheEntry, + expandToInvalidationPaths, +} from "./filetree" +export type { CacheTarget } from "./filetree" + +export { + getCachedLink, + setCachedLink, + invalidateLinks, + isLinkCacheable, + resolveLinkTtlMinutes, + deleteLinkCacheEntry, +} from "./link" + +/** + * 失效若干虚拟路径对应的**全部**缓存(文件树 + 下载链接)。 + * + * 这是写操作后统一调用的入口:调用方只需给出「被改动的路径」, + * 父目录的列表失效由 `invalidateFileTree()` 内部处理。 + */ +export async function invalidatePaths( + virtualPaths: string[], + env?: any, +): Promise { + const paths = (virtualPaths || []).filter(Boolean) + if (paths.length === 0) return + const cfg = getCacheConfig(env) + if (!cfg.enabled) return + try { + await Promise.all([ + invalidateFileTree(paths, env), + invalidateLinks(paths, env), + ]) + } catch (err: any) { + // 失效失败不影响写操作本身:缓存会随 TTL 自然过期 + console.warn(`[Cache] invalidate failed: ${err?.message || err}`) + } +} + +/** 清空某个存储的全部缓存(存储配置变更 / 删除 / 启停时调用)。 */ +export async function clearStorageCache( + storageId: any, + env?: any, +): Promise { + try { + return await clearCache(env, { storageId }) + } catch (err: any) { + console.warn(`[Cache] clearStorageCache failed: ${err?.message || err}`) + return 0 + } +} + +/** + * 缓存运行时状态(供管理接口 / 诊断展示)。 + */ +export async function getCacheRuntimeStatus(env?: any): Promise<{ + config: Record + active: boolean + drivers: string[] +}> { + const config = describeCacheConfig(env) + let drivers: string[] = [] + let active = false + try { + active = await isCacheActive(env) + drivers = (await getCacheDrivers(env)).map((d) => d.name) + } catch { + active = false + } + return { config, active, drivers } +} diff --git a/src/backend/internal/cache/link.ts b/src/backend/internal/cache/link.ts new file mode 100644 index 00000000..f12efa7d --- /dev/null +++ b/src/backend/internal/cache/link.ts @@ -0,0 +1,138 @@ +/** + * 下载链接缓存(Download Link Cache)。 + * + * 对齐参考实现 OpenList.ts 的 `file_links` 表:把驱动 `get()` 返回的直链 + * (`raw_url` + 必需的 `raw_url_headers`)缓存起来,重复下载 / 预览时无需 + * 再次向网盘换取临时链接(这一步通常最贵,且容易被限流)。 + * + * 安全边界: + * - 只缓存**驱动层**结果,签名(`?sign=`)与访问控制在 server 层按请求 + * 实时签发/校验,因此缓存不会绕过权限; + * - 只缓存「确实拿到了直链」的成功结果(`raw_url` 非空、非目录), + * 失败结果不做负缓存,避免把瞬时故障固化; + * - TTL 默认仅 5 分钟(`CACHE_LINK_TTL`)。直链通常自带有效期, + * 缓存过久会把「已失效的链接」交给浏览器(表现为 403/404)。 + */ +import type { FileItem } from "../driver/base" +import { resolvePath } from "../model/db" +import { + DEFAULT_LINK_TTL_MINUTES, + getCacheConfig, + normalizeDriverName, +} from "./config" +import { + buildCacheKey, + cacheDelete, + cacheGetEnvelope, + cacheSetEnvelope, +} from "./store" +import type { CacheTarget } from "./filetree" + +/** 下载链接缓存的 TTL(分钟)。 */ +export function resolveLinkTtlMinutes(env?: any): number { + return getCacheConfig(env).linkTtlMinutes +} + +/** + * 该目标是否可参与下载链接缓存。 + * + * 与文件树缓存共享「驱动排除名单」:纯本地计算型驱动(virtual/alias/…) + * 的 get() 本来就没有网络开销,缓存没有意义。 + */ +export function isLinkCacheable( + target: CacheTarget | null | undefined, + env?: any, +): boolean { + const cfg = getCacheConfig(env) + if (!cfg.enabled || !cfg.downloadLink) return false + if (!target?.storage || target.isVirtual) return false + const driver = normalizeDriverName(target.storage.driver) + if (cfg.excludeDrivers.has(driver)) return false + return cfg.linkTtlMinutes > 0 +} + +function keyFor(target: CacheTarget, env?: any): string { + const cfg = getCacheConfig(env) + return buildCacheKey("ln", target.storage.id, target.cleanPath, cfg.prefix) +} + +/** + * 读取下载链接缓存。未命中 / 已过期 / 异常均返回 null。 + * + * 返回的是 FileItem 的浅拷贝,避免调用方就地修改污染缓存内容。 + */ +export async function getCachedLink( + target: CacheTarget, + env?: any, +): Promise { + if (!isLinkCacheable(target, env)) return null + try { + const hit = await cacheGetEnvelope(keyFor(target, env), env) + if (!hit || !hit.v || typeof hit.v !== "object") return null + return { ...hit.v } + } catch { + return null + } +} + +/** + * 写入下载链接缓存。 + * + * 只在「拿到了可用直链」时写入:目录、`raw_url` 为空、或带有 + * `raw_url_error` 的结果都会被跳过。 + */ +export async function setCachedLink( + target: CacheTarget | null | undefined, + item: FileItem | null | undefined, + env?: any, +): Promise { + if (!target || !isLinkCacheable(target, env)) return + if (!item || item.is_dir) return + if (!item.raw_url) return + const ttlMinutes = resolveLinkTtlMinutes(env) + if (ttlMinutes <= 0) return + try { + await cacheSetEnvelope(keyFor(target, env), item, ttlMinutes * 60_000, env) + } catch { + // 缓存写入失败绝不影响业务 + } +} + +/** 删除单个路径的下载链接缓存(调用方已知 storage)。 */ +export async function deleteLinkCacheEntry( + storageId: any, + cleanPath: string, + env?: any, +): Promise { + const cfg = getCacheConfig(env) + await cacheDelete(buildCacheKey("ln", storageId, cleanPath, cfg.prefix), env) +} + +/** + * 失效若干虚拟路径对应的下载链接缓存。 + * + * 仅失效路径自身(下载链接按文件粒度缓存,不存在「父目录链接」)。 + * 路径可能是目录(如删除整个目录),此时其下所有文件的链接会随 TTL + * 自然过期——直链缓存 TTL 很短(默认 5 分钟),无需递归清理。 + */ +export async function invalidateLinks( + virtualPaths: string[], + env?: any, +): Promise { + const cfg = getCacheConfig(env) + if (!cfg.enabled || !cfg.downloadLink) return + + const keys = new Set() + for (const p of virtualPaths) { + try { + const resolved = await resolvePath(p, env) + if (resolved.isVirtual || !resolved.storage) continue + keys.add( + buildCacheKey("ln", resolved.storage.id, resolved.cleanPath, cfg.prefix), + ) + } catch { + // 路径无法解析:忽略 + } + } + for (const key of keys) await cacheDelete(key, env) +} diff --git a/src/backend/internal/cache/store.ts b/src/backend/internal/cache/store.ts new file mode 100644 index 00000000..17cb47ea --- /dev/null +++ b/src/backend/internal/cache/store.ts @@ -0,0 +1,443 @@ +/** + * 缓存存储层:把「缓存键值」落到某个 Driver 上。 + * + * 与业务数据的关系: + * - `db` 后端复用 `getStorageBackend()` 解析出的 driver,也就是与业务数据 + * **同一个** d1 / mysql / kv / cfkv / blob / do 后端(默认行为); + * - `kv` / `blob` / `cfkv` / `do` / `memory` 是**专用**后端,只有用户在 + * `CACHE_DRIVER` 里显式写出才会启用(不会自动探测、不会自动回退); + * - 缓存写入**直接走 driver 的 get/put/delete**,不经过 `saveDb()`, + * 因此不会触发「整配置对象序列化 / 写前守卫 / 字段加密」等开销, + * 也不会污染业务数据(键名统一带 `openlist_cache` 前缀)。 + * + * 失败策略:缓存永远不能让业务请求失败。任何读/写异常都按「未命中 / 未写入」 + * 处理,并只告警一次,避免把日志刷满。 + */ +import type { Driver } from "../model/store/types" +import { getStorageBackend } from "../model/store/backend" +import { kvDriver } from "../model/store/driver/kv" +import { cfkvDriver } from "../model/store/driver/cfkv" +import { blobDriver } from "../model/store/driver/blob" +import { doDriver } from "../model/store/driver/do" +import { memoryDriver } from "../model/store/driver/memory" +import { encodeKeyPart } from "../model/store/keycodec" +import { getCacheConfig, type CacheBackendName } from "./config" + +/** 专用后端名 → 驱动实现(`db` 单独处理,见 resolveDrivers) */ +const DEDICATED_DRIVERS: Record = { + kv: kvDriver, + cfkv: cfkvDriver, + blob: blobDriver, + do: doDriver, + memory: memoryDriver, +} + +/** 缓存条目信封:值 + 过期时间戳 */ +export interface CacheEnvelope { + /** 载荷 */ + v: T + /** 写入时间(ms) */ + ts: number + /** 过期时间(ms,绝对时间) */ + exp: number +} + +/** + * 驱动解析结果缓存。 + * + * 为什么需要:`blobDriver.isAvailable()` 每次都会动态 import EdgeOne SDK, + * `kvDriver.isAvailable()` 可能发一次 HTTP 探测;而缓存配置在实例生命周期内 + * 基本不变。按 env 对象身份记忆化即可(`1 env = 1 请求`,条目随 GC 回收)。 + */ +const resolvedByEnv = new WeakMap>() + +/** 仅供测试:注入缓存驱动解析结果。 */ +let driverLoaderOverride: ((env: any) => Promise) | null = null + +export function __setCacheDriversForTest( + loader: ((env: any) => Promise) | null, +): void { + driverLoaderOverride = loader +} + +/** 仅供测试:重置模块级状态。 */ +export function __resetCacheStoreForTest(): void { + driverLoaderOverride = null + warnedDrivers.clear() +} + +/** 每个驱动只告警一次(键为 `driverName:reason`) */ +const warnedDrivers = new Map() + +function warnOnce(key: string, message: string): void { + if (warnedDrivers.has(key)) return + warnedDrivers.set(key, true) + console.warn(message) +} + +/** + * 解析实际可用的缓存驱动列表(顺序即读取优先级)。 + * + * 语义约定(与 DB_DRIVER 一致): + * - 用户显式配置了某个专用后端但它不可用 → **跳过并告警**,绝不悄悄换成 + * 别的后端;若最终一个都不剩,则缓存整体失效(请求照常走真实存储)。 + */ +async function resolveDrivers(env: any): Promise { + if (driverLoaderOverride) return driverLoaderOverride(env) + + const cfg = getCacheConfig(env) + if (!cfg.enabled) return [] + + const drivers: Driver[] = [] + for (const name of cfg.backends) { + if (name === "db") { + try { + const { driver } = await getStorageBackend(env) + drivers.push(driver) + } catch (err: any) { + warnOnce( + "db:resolve", + `[Cache] CACHE_DRIVER includes "db" but the storage backend could not be ` + + `resolved (${err?.message || err}); the cache falls back to no-op.`, + ) + } + continue + } + + const driver = DEDICATED_DRIVERS[name] + if (!driver) continue + try { + if (await driver.isAvailable(env)) { + drivers.push(driver) + } else { + warnOnce( + `${name}:unavailable`, + `[Cache] CACHE_DRIVER="${name}" is not available in this runtime ` + + `(missing binding/credentials). This cache backend is skipped; ` + + `no other backend is used in its place.`, + ) + } + } catch (err: any) { + warnOnce( + `${name}:error`, + `[Cache] CACHE_DRIVER="${name}" availability probe failed: ` + + `${err?.message || err}. This cache backend is skipped.`, + ) + } + } + return drivers +} + +/** 获取(并记忆化)当前 env 的缓存驱动列表。 */ +export async function getCacheDrivers(env?: any): Promise { + if (driverLoaderOverride) return driverLoaderOverride(env) + if (env && typeof env === "object") { + let pending = resolvedByEnv.get(env) + if (!pending) { + pending = resolveDrivers(env) + resolvedByEnv.set(env, pending) + } + return pending + } + return resolveDrivers(env) +} + +/** 缓存是否已实际启用(用于诊断接口与管理页展示)。 */ +export async function isCacheActive(env?: any): Promise { + const drivers = await getCacheDrivers(env) + return drivers.length > 0 +} + +/** 把 driver.get 的返回值(可能是 Response 形态)统一成字符串。 */ +async function coerceText(value: any): Promise { + if (value === null || value === undefined) return null + if (typeof value === "string") return value + if (typeof value?.text === "function") { + try { + return String(await value.text()) + } catch { + return null + } + } + return String(value) +} + +/** + * 读取缓存条目(自动跳过已过期条目)。 + * + * 多个后端时按配置顺序逐个尝试:任一命中即返回(先写 KV 后写 DB 的部署, + * 即使 DB 侧被清理过也仍能命中 KV)。 + */ +export async function cacheGetEnvelope( + key: string, + env?: any, +): Promise | null> { + let drivers: Driver[] + try { + drivers = await getCacheDrivers(env) + } catch { + return null + } + if (drivers.length === 0) return null + + for (const driver of drivers) { + try { + const raw = await coerceText(await driver.get(key, env)) + if (!raw) continue + const parsed = JSON.parse(raw) as CacheEnvelope + if (!parsed || typeof parsed !== "object") continue + if (typeof parsed.exp === "number" && parsed.exp <= Date.now()) { + // 惰性清理过期条目,避免缓存无限膨胀 + void cacheDelete(key, env) + continue + } + return parsed + } catch (err: any) { + warnOnce( + `${driver.name}:get`, + `[Cache] read failed on backend "${driver.name}": ${err?.message || err}`, + ) + } + } + return null +} + +/** 写入缓存条目(TTL 单位毫秒;ttl<=0 时不写入)。 */ +export async function cacheSetEnvelope( + key: string, + value: T, + ttlMs: number, + env?: any, +): Promise { + if (!Number.isFinite(ttlMs) || ttlMs <= 0) return + + let drivers: Driver[] + try { + drivers = await getCacheDrivers(env) + } catch { + return + } + if (drivers.length === 0) return + + const now = Date.now() + const envelope: CacheEnvelope = { v: value, ts: now, exp: now + ttlMs } + let raw: string + try { + raw = JSON.stringify(envelope) + } catch { + return + } + + // 双写所有后端:读路径按顺序命中,写路径尽量铺满(各自失败互不影响) + await Promise.all( + drivers.map(async (driver) => { + try { + await driver.put(key, raw, env) + } catch (err: any) { + warnOnce( + `${driver.name}:put`, + `[Cache] write failed on backend "${driver.name}": ${err?.message || err}`, + ) + } + }), + ) +} + +/** 删除缓存键(多后端全删)。 */ +export async function cacheDelete(key: string, env?: any): Promise { + let drivers: Driver[] + try { + drivers = await getCacheDrivers(env) + } catch { + return + } + if (drivers.length === 0) return + + await Promise.all( + drivers.map(async (driver) => { + try { + await driver.delete(key, env) + } catch (err: any) { + warnOnce( + `${driver.name}:delete`, + `[Cache] delete failed on backend "${driver.name}": ${err?.message || err}`, + ) + } + }), + ) +} + +/** + * 列出以 `prefix` 开头的缓存键(多后端取并集)。 + * + * 仅用于管理接口(清空 / 统计),不在请求热路径上。 + */ +export async function cacheListKeys( + prefix: string, + env?: any, +): Promise { + let drivers: Driver[] + try { + drivers = await getCacheDrivers(env) + } catch { + return [] + } + if (drivers.length === 0) return [] + + const out = new Set() + for (const driver of drivers) { + try { + for (const key of await driver.list(prefix, env)) out.add(key) + } catch (err: any) { + warnOnce( + `${driver.name}:list`, + `[Cache] list failed on backend "${driver.name}": ${err?.message || err}`, + ) + } + } + return [...out] +} + +/** 缓存类型标识:`ft` = 文件树(file tree),`ln` = 下载链接(link) */ +export type CacheKind = "ft" | "ln" + +/** 32 位 FNV-1a 哈希(同步,用于把超长路径压成短键)。 */ +function fnv1a32(input: string, seed: number): number { + let hash = seed >>> 0 + for (let i = 0; i < input.length; i++) { + hash ^= input.charCodeAt(i) + hash = Math.imul(hash, 0x01000193) >>> 0 + } + return hash >>> 0 +} + +/** + * 缓存专用的单射字符判断。 + * + * 与业务数据的 `encodeKeyPart` 唯一的区别:**小写 `x` 不再原样保留**。 + * 业务键名仅作存储地址、主键取自记录 JSON,`x` 的歧义无碍;而缓存靠 + * 「键 → 值」取回数据,键不单射 = 取回别的路径的内容(评审实测复现: + * `/a/b` 与 `/ax2fb` 键相同,可跨目录串列表 / 直链)。`x` 的 UTF-8 + * 字节恰为 0x78,转义成 `x78` 即可让任何 `x` 都只能来自转义序列。 + */ +function isCachePlain(ch: string): boolean { + if (ch === "x") return false + return ( + (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + (ch >= "0" && ch <= "9") || + ch === "_" + ) +} + +/** `isCachePlain` 的快路径正则([A-Za-z] 去掉小写 x + [0-9] + _)。 */ +const CACHE_PLAIN_RE = /^[A-Za-wyz0-9_]+$/ + +/** 单射编码:合法字符原样(`x` 除外),其余按 UTF-8 逐字节转义为 `xHH`。 */ +function encodePathInjective(s: string): string { + if (s === "") return "0" + if (CACHE_PLAIN_RE.test(s)) return s + + let out = "" + for (const ch of s) { + if (isCachePlain(ch)) { + out += ch + } else { + for (const b of new TextEncoder().encode(ch)) { + out += "x" + b.toString(16).padStart(2, "0") + } + } + } + return out +} + +/** + * 路径片段编码为 KV 合法键片段(**单射**)。 + * + * EdgeOne KV 只接受 `[A-Za-z0-9_]`。编码必须单射:任意两个不同虚拟路径 + * 不得得到同一个键,否则「键碰撞 = 跨目录 / 跨文件串数据」(权限、hide + * 规则、meta 密码按请求路径判定,而内容取自碰撞路径,可被利用绕过)。 + * + * - 短路径:直接单射编码(`x` → `x78` 消除转义前缀歧义)。 + * - 超长路径(编码后 > 180,接近 KV 键长上限):截取前 100 字符 + + * `xg` + 双 32 位 FNV-1a(合计 64 位)收尾。标记选 `xg` 是因为普通 + * 编码结果中 `x` 后面必跟两位十六进制,`xg` 不可能出现,因此长路径 + * 折叠键不会与任何短路径的普通编码键碰撞;两个超长路径之间依赖 + * 64 位哈希区分。编码是确定性的,失效时仍能算出同一个键。 + */ +export function encodeCachePathSegment(virtualPath: string): string { + const norm = + "/" + + String(virtualPath || "") + .split("/") + .filter(Boolean) + .join("/") + const encoded = encodePathInjective(norm) + if (encoded.length <= 180) return encoded + const a = fnv1a32(norm, 0x811c9dc5).toString(16).padStart(8, "0") + const b = fnv1a32(norm, 0x9e3779b9).toString(16).padStart(8, "0") + return encoded.slice(0, 100) + "xg" + a + b +} + +/** + * 构造缓存键:`___`。 + * + * 键名分四段,第 4 段(索引 3)是存储 id,便于「按存储清空」时按段过滤, + * 避免 `storages_1_` 前缀误伤 `storages_10_` 这类包含关系。 + */ +export function buildCacheKey( + kind: CacheKind, + storageId: any, + virtualPath: string, + prefix: string, +): string { + return `${prefix}_${kind}_${encodeKeyPart(String(storageId ?? ""))}_${encodeCachePathSegment( + virtualPath, + )}` +} + +/** 某个 kind 在全部存储下的键前缀。 */ +export function cacheKindPrefix(kind: CacheKind, prefix: string): string { + return `${prefix}_${kind}_` +} + +/** + * 从缓存键中解析出所属存储 id(用于按存储过滤)。 + * + * 段数由前缀决定(前缀本身可能含 `_`),因此按前缀的段数动态定位, + * 不做硬编码索引。 + */ +export function storageIdFromCacheKey( + key: string, + kind: CacheKind, + prefix: string, +): string | null { + const head = `${prefix}_${kind}_` + if (!key.startsWith(head)) return null + const rest = key.slice(head.length) + const sep = rest.indexOf("_") + return sep === -1 ? rest : rest.slice(0, sep) +} + +/** 清空全部缓存(或仅某一 kind / 某一存储),返回删除的键数量。 */ +export async function clearCache( + env: any, + opts: { kind?: CacheKind; storageId?: any } = {}, +): Promise { + const cfg = getCacheConfig(env) + const kinds: CacheKind[] = opts.kind ? [opts.kind] : ["ft", "ln"] + let removed = 0 + + for (const kind of kinds) { + const keys = await cacheListKeys(cacheKindPrefix(kind, cfg.prefix), env) + for (const key of keys) { + if (opts.storageId !== undefined && opts.storageId !== null) { + const id = storageIdFromCacheKey(key, kind, cfg.prefix) + if (id !== String(opts.storageId)) continue + } + await cacheDelete(key, env) + removed++ + } + } + return removed +} diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index f2ae98f4..96f9a568 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -3,6 +3,14 @@ import { encodeDownloadPath } from "../../pkg/path" import { singleflight } from "../../pkg/singleflight" import { canUseProxyEndpoint, normalizeExtList } from "../driver/proxy" import { FileItem, StorageDriver, calcFileType } from "../driver/base" +import { + getCachedFileTree, + setCachedFileTree, + getCachedLink, + setCachedLink, + invalidatePaths, + type CacheTarget, +} from "../cache" import { Onedrive } from "../../drivers/onedrive/driver" import { OnedriveAPP } from "../../drivers/onedrive_app/driver" import { AliyundriveOpen } from "../../drivers/aliyundrive_open/driver" @@ -1352,6 +1360,20 @@ export async function flushPendingDriverState( await scheduleStoragePersistence(requestContext?.waitUntil, persistence) } +/** + * 由 `resolvePath()` 结果构造缓存目标;虚拟路径(无存储)返回 null, + * 表示不参与缓存。 + */ +function toCacheTarget(resolved: any): CacheTarget | null { + if (!resolved?.storage) return null + return { + storage: resolved.storage, + cleanPath: resolved.cleanPath, + physical: resolved.physical, + isVirtual: resolved.isVirtual, + } +} + /** * fs 读操作的 singleflight 去重键。 * @@ -1396,17 +1418,32 @@ async function listItemsResolved( if (resolved.storage) { driverName = resolved.storage.driver + const cacheTarget = toCacheTarget(resolved) try { - const driver = await getDriver(driverName, resolved.storage) - // Get raw items from driver - try { - items = await driver.list(virtualPath, resolved.physical!) - } finally { - await flushPendingDriverState( - driverName, - resolved.storage, - driver, - requestContext, + // 文件树缓存:命中则完全跳过驱动(对齐 OpenList.ts「浏览只读缓存」)。 + // 注意缓存的是驱动层原始结果,权限/meta/签名仍在 server 层按请求计算。 + const cached = await getCachedFileTree(cacheTarget!, requestContext?.env) + if (cached) { + items = cached + } else { + const driver = await getDriver(driverName, resolved.storage) + // Get raw items from driver + try { + items = await driver.list(virtualPath, resolved.physical!) + } finally { + await flushPendingDriverState( + driverName, + resolved.storage, + driver, + requestContext, + ) + } + // 回源成功后写入缓存(空目录同样缓存,避免反复击穿存储)。 + // 传副本:后续 mount 合并 / type 计算会就地修改 items。 + await setCachedFileTree( + cacheTarget!, + items.map((item) => ({ ...item })), + requestContext?.env, ) } if (resolved.storage.status !== "work") { @@ -1573,17 +1610,25 @@ async function getItemResolved( } const driverName = resolved.storage ? resolved.storage.driver : "Local" - const driver = await getDriver(driverName, resolved.storage) - let item: FileItem - try { - item = await driver.get(virtualPath, resolved.physical!) - } finally { - await flushPendingDriverState( - driverName, - resolved.storage, - driver, - requestContext, - ) + const cacheTarget = toCacheTarget(resolved) + // 下载链接缓存:命中则直接复用上次换取到的直链与元数据, + // 跳过驱动 get()(网盘换链通常是最贵、最易被限流的一步)。 + let item: FileItem | null = cacheTarget + ? await getCachedLink(cacheTarget, requestContext?.env) + : null + if (!item) { + const driver = await getDriver(driverName, resolved.storage) + try { + item = await driver.get(virtualPath, resolved.physical!) + } finally { + await flushPendingDriverState( + driverName, + resolved.storage, + driver, + requestContext, + ) + } + await setCachedLink(cacheTarget, item, requestContext?.env) } if (!item.type) { item.type = calcFileType(item.name, item.is_dir) @@ -1618,6 +1663,8 @@ export async function makeDirectory( requestContext, ) } + // 新建目录:父目录列表已变化(目录自身也为空列表) + await invalidatePaths([virtualPath], requestContext?.env) } export async function renameItem( @@ -1640,6 +1687,10 @@ export async function renameItem( requestContext, ) } + // 重命名:旧路径与新路径都要失效(父目录列表同样变化) + const parent = virtualPath.slice(0, virtualPath.lastIndexOf("/")) || "/" + const newVirtualPath = `${parent === "/" ? "" : parent}/${newName}` + await invalidatePaths([virtualPath, newVirtualPath], requestContext?.env) } export async function removeItems( @@ -1664,6 +1715,8 @@ export async function removeItems( requestContext, ) } + // 删除:被删路径与其父目录缓存失效 + await invalidatePaths([itemVirtual], requestContext?.env) } } @@ -1702,6 +1755,8 @@ export async function moveItems( requestContext, ) } + // 移动:源路径与目标路径(含各自父目录)缓存全部失效 + await invalidatePaths([srcVirtual, dstVirtual], requestContext?.env) } } @@ -1740,6 +1795,8 @@ export async function copyItems( requestContext, ) } + // 复制:目标路径(含父目录)缓存失效 + await invalidatePaths([dstVirtual], requestContext?.env) } } @@ -1763,4 +1820,6 @@ export async function putItem( requestContext, ) } + // 上传/覆盖:该文件的旧直链与父目录列表缓存失效 + await invalidatePaths([virtualPath], requestContext?.env) } diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 1b7eb451..0b107d37 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -17,6 +17,14 @@ import { driverPreferProxy, normalizeDriverName, } from "../internal/driver/proxy" +import { + cacheKindPrefix, + cacheListKeys, + clearCache, + clearStorageCache, + getCacheRuntimeStatus, + type CacheKind, +} from "../internal/cache" export const adminRouter = new Hono() @@ -431,6 +439,9 @@ adminRouter.post("/storage/update", async (c) => { } db.storages[idx] = updatedStorage await saveDb(db, c.env) + // 存储配置已变更(挂载路径 / root_folder_path / addition / 缓存策略等), + // 既有缓存全部失效,避免继续返回旧挂载点或旧凭据下的结果。 + await clearStorageCache(updatedStorage.id, c.env) } return c.json({ code: 200, message: "success", data: null }) }) @@ -440,6 +451,9 @@ adminRouter.post("/storage/delete", async (c) => { const db = await getDb(c.env) db.storages = db.storages.filter((s: any) => s.id !== id) await saveDb(db, c.env) + // 存储已删除:其文件树 / 下载链接缓存必须一并清理,否则 id 复用时 + // 新存储会读到旧存储的缓存内容。 + await clearStorageCache(id, c.env) return c.json({ code: 200, message: "success", data: null }) }) @@ -451,6 +465,7 @@ adminRouter.post("/storage/enable", async (c) => { s.disabled = false s.modified = new Date().toISOString() await saveDb(db, c.env) + await clearStorageCache(id, c.env) // 异步初始化驱动(不阻塞响应):启用多个云盘时立即返回, // 初始化完成后更新状态;失败时把错误写入 status,下次访问或 // 重新加载时会再次尝试。 @@ -484,6 +499,7 @@ adminRouter.post("/storage/disable", async (c) => { if (s) { s.disabled = true await saveDb(db, c.env) + await clearStorageCache(id, c.env) } return c.json({ code: 200, message: "success", data: null }) }) @@ -4787,6 +4803,117 @@ adminRouter.get("/kv/status", async (c) => { }) }) +// --- 缓存管理(文件树缓存 / 下载链接缓存)--- +// +// 默认只向数据库启用(CACHE_DRIVER=db);想要改用 / 额外启用 KV、Blob 等 +// 专用后端,需显式配置 CACHE_DRIVER(如 `kv`、`db,kv`、`blob`)。 +// 这里的接口只做「查看状态 / 清空」,不改变后端选择(后端只认环境变量)。 + +/** 缓存状态:生效配置 + 实际后端 + 当前条目数。 */ +adminRouter.get("/cache/status", async (c) => { + const runtime = await getCacheRuntimeStatus(c.env) + let fileTree = 0 + let downloadLink = 0 + try { + const [ftKeys, lnKeys] = await Promise.all([ + cacheListKeys(cacheKindPrefix("ft", runtime.config.prefix), c.env), + cacheListKeys(cacheKindPrefix("ln", runtime.config.prefix), c.env), + ]) + fileTree = ftKeys.length + downloadLink = lnKeys.length + } catch (err: any) { + return c.json({ + code: 200, + message: "success", + data: { + ...runtime, + entries: null, + entries_error: safeErrorMessage(err), + }, + }) + } + return c.json({ + code: 200, + message: "success", + data: { + ...runtime, + entries: { + file_tree: fileTree, + download_link: downloadLink, + total: fileTree + downloadLink, + }, + }, + }) +}) + +/** + * 清空缓存。 + * body: { type?: "file_tree" | "download_link" | "all", storage_id?: number } + * 省略 type 表示两级缓存全清;省略 storage_id 表示所有存储。 + */ +adminRouter.post("/cache/clear", async (c) => { + const body = await c.req.json().catch(() => ({})) + const rawType = String(body.type ?? c.req.query("type") ?? "") + .trim() + .toLowerCase() + const kind: CacheKind | undefined = + rawType === "file_tree" || rawType === "ft" || rawType === "tree" + ? "ft" + : rawType === "download_link" || rawType === "link" || rawType === "ln" + ? "ln" + : undefined + + const idRaw = body.storage_id ?? c.req.query("storage_id") + const hasId = idRaw !== undefined && idRaw !== null && String(idRaw) !== "" + + const removed = await clearCache(c.env, { + ...(kind ? { kind } : {}), + ...(hasId ? { storageId: idRaw } : {}), + }) + return c.json({ + code: 200, + message: "success", + data: { removed, type: kind ?? "all", storage_id: hasId ? idRaw : null }, + }) +}) + +/** + * 刷新文件树缓存(对齐参考实现 OpenList.ts 的 POST /api/admin/storage/refresh)。 + * body/query 可选 `id`:只刷新该存储。 + */ +adminRouter.post("/storage/refresh", async (c) => { + const body = await c.req.json().catch(() => ({})) + const idRaw = body.id ?? c.req.query("id") + const hasId = idRaw !== undefined && idRaw !== null && String(idRaw) !== "" + const removed = await clearCache(c.env, { + kind: "ft", + ...(hasId ? { storageId: idRaw } : {}), + }) + return c.json({ + code: 200, + message: "success", + data: { removed, storage_id: hasId ? idRaw : null }, + }) +}) + +/** + * 刷新单个存储的文件树缓存 + * (对齐参考实现 OpenList.ts 的 POST /api/admin/storage/refresh_one?id=)。 + */ +adminRouter.post("/storage/refresh_one", async (c) => { + const body = await c.req.json().catch(() => ({})) + const idRaw = c.req.query("id") ?? body.id + if (idRaw === undefined || idRaw === null || String(idRaw) === "") { + return c.json({ code: 400, message: "id is required", data: null }, 400) + } + const removed = await clearCache(c.env, { kind: "ft", storageId: idRaw }) + return c.json({ + code: 200, + message: "success", + data: { removed, storage_id: idRaw }, + }) +}) + // --- Search Index / Scan --- // Serverless 环境无常驻后台任务,索引构建为「请求内同步遍历 + KV 缓存」。 // 对大型存储可能耗时较长(受 Worker CPU 时限约束),但中小型部署可用; diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index ac90cef6..c7f52d6c 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -40,6 +40,7 @@ import { } from "../internal/driver/storageopts" import { parseZip, extractZipEntry, ZipArchive } from "../internal/archive/zip" import { assertSafeUrl, getTrustedHosts } from "../pkg/http" +import { invalidatePaths } from "../internal/cache" import { seedRouter } from "./seed" /** @@ -70,19 +71,43 @@ import { export const fsRouter = new Hono() fsRouter.route("/seed", seedRouter) +/** + * 请求级存储上下文(`env` + `waitUntil`)。 + * + * 关键约定:**`env` 必须始终传递**,与 executionCtx 是否可用无关。 + * + * 缓存(`CACHE_DRIVER=db`)与 singleflight(`SINGLEFLIGHT=db`)都依赖它来解析 + * 后端:拿不到 `env` 时 `getStorageBackend(undefined)` 会解析成**内存驱动**, + * 而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与数据库被静默拆到两个后端, + * 表现为「列表看似命中缓存(内容陈旧),但持久层里一条缓存键都没有、 + * `/cache/status` 计数恒为 0、`/cache/clear` 永远 removed=0」。 + * + * 真正依赖 executionCtx 的只有 `waitUntil`;缺失时降级为同步等待 + * (见 `scheduleStoragePersistence`),业务语义不变。 + */ +let warnedMissingExecutionCtx = false const getStorageRequestContext = (c: any) => { + const env = c?.env + let waitUntil: ((promise: Promise) => void) | undefined try { - const executionCtx = c.executionCtx - if (!executionCtx || typeof executionCtx.waitUntil !== "function") { - return undefined - } - return { - waitUntil: (promise: Promise) => executionCtx.waitUntil(promise), - env: c.env, // 传递 env 用于请求级 KV 缓存复用 + const executionCtx = c?.executionCtx + if (executionCtx && typeof executionCtx.waitUntil === "function") { + waitUntil = (promise: Promise) => executionCtx.waitUntil(promise) } } catch { - return undefined + // executionCtx 可能是访问即抛错的对象:按「不可用」处理,但 env 照常传递 + waitUntil = undefined + } + if (!waitUntil && !warnedMissingExecutionCtx) { + warnedMissingExecutionCtx = true + console.warn( + "[fs] executionCtx.waitUntil is unavailable in this runtime; deferred " + + "storage persistence will be awaited inline instead of being scheduled " + + "in the background.", + ) } + if (!env && !waitUntil) return undefined + return { ...(waitUntil ? { waitUntil } : {}), env } } // ---- 写操作权限校验 ---- @@ -1035,6 +1060,9 @@ fsRouter.post("/upload/complete", async (c) => { requestContext, ) } + // 直传收尾:目录列表已变化,失效该目录的文件树 / 链接缓存, + // 否则「上传成功但刷新看不到」在缓存 TTL 内稳定复现 + await invalidatePaths([dirPath], c.env) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { return c.json({ code: 500, message: safeErrorMessage(e), data: null }) @@ -1114,6 +1142,14 @@ fsRouter.post("/other", async (c) => { const driver = await getDriver(resolved.storage.driver, resolved.storage) if (typeof (driver as any).other === "function") { const data = await (driver as any).other(method, resolved.relative, body) + // other() 可能触发落盘(如 S3 直传 URL 的签发流程):按已知路径失效缓存。 + // 直传的实际写入发生在客户端,无法精确感知完成时机,这里失效目标目录 + // 及(若给定)文件路径,将陈旧窗口压到最小;失效失败不影响响应。 + const targets = [reqPath] + if (typeof body?.file_name === "string" && body.file_name) { + targets.push(`${reqPath === "/" ? "" : reqPath}/${body.file_name}`) + } + await invalidatePaths(targets, c.env) return c.json({ code: 200, message: "success", data }) } return c.json( @@ -1352,6 +1388,17 @@ fsRouter.post("/get_direct_upload_info", async (c) => { file_name, file_size, ) + // 直传 URL 签发后客户端会向该路径写入新文件:预失效目录与目标路径 + await invalidatePaths( + [ + reqPath, + `${reqPath === "/" ? "" : reqPath}/${file_name || ""}`.replace( + /\/$/, + "", + ), + ], + c.env, + ) return c.json({ code: 200, message: "success", data: info }) } // 回退到 other("direct_upload") / other("get_direct_upload_info") @@ -1362,7 +1409,20 @@ fsRouter.post("/get_direct_upload_info", async (c) => { file_name, file_size, }) - if (info) return c.json({ code: 200, message: "success", data: info }) + if (info) { + // 同上:直传 URL 签发后预失效目录与目标路径 + await invalidatePaths( + [ + reqPath, + `${reqPath === "/" ? "" : reqPath}/${file_name || ""}`.replace( + /\/$/, + "", + ), + ], + c.env, + ) + return c.json({ code: 200, message: "success", data: info }) + } } catch { // 尝试下一个 method } @@ -1582,6 +1642,15 @@ fsRouter.post("/multipart/complete", async (c) => { session.state = "completed" putSession(session) deleteSession(session.upload_id) + // 分片收尾:失效上传文件及其目录的缓存(对齐 op 层 put 的失效范围) + { + const { dir, name } = splitUploadPath(session.path) + const actualDir = getActualPath(user, dir) + await invalidatePaths( + [actualDir, `${actualDir === "/" ? "" : actualDir}/${name}`], + c.env, + ) + } return c.json({ code: 200, message: "success", data: mpSnapshot(session) }) } catch (e: any) { session.state = "failed_permanent" diff --git a/src/backend/server/public.ts b/src/backend/server/public.ts index 0b41523f..b20fe929 100644 --- a/src/backend/server/public.ts +++ b/src/backend/server/public.ts @@ -17,6 +17,11 @@ import { readFormat, } from "../internal/model/store/backend" import { setUserPassword } from "../pkg/password" +import { + describeCacheConfig, + getCacheDrivers, + isCacheActive, +} from "../internal/cache" // 脱敏 / 截断 / 摘要 / 建议组装:与全局 503 拦截(index.ts)共用同一套规则 import { reasonLines, @@ -279,6 +284,19 @@ publicRouter.get("/env_check", async (c) => { // 但不再因此阻断向导。 const ready = storageAvailable + // ── 缓存状态(文件树 / 下载链接)── + // 默认只向数据库启用(CACHE_DRIVER=db);配置了 KV/Blob 等专用后端时 + // 这里会如实回显实际生效的后端列表,便于排查「以为缓存在 KV」的错觉。 + const cacheConfig = describeCacheConfig(env) + let cacheActive = false + let cacheDrivers: string[] = [] + try { + cacheActive = await isCacheActive(env) + cacheDrivers = (await getCacheDrivers(env)).map((d) => d.name) + } catch { + cacheActive = false + } + return c.json({ code: 200, message: "success", @@ -287,6 +305,13 @@ publicRouter.get("/env_check", async (c) => { serverless, platform: storage?.platform ?? null, }, + cache: { + ...cacheConfig, + /** 是否已有可用缓存后端(false 时缓存自动降级为不缓存) */ + active: cacheActive, + /** 实际解析出的后端驱动名(`db` 会展开成 d1/kv/blob/... ) */ + resolved_backends: cacheDrivers, + }, config: { // 配置值(用户显式设置,或默认值) db_format: formatCfg, diff --git a/src/backend/server/raw.ts b/src/backend/server/raw.ts index f1894ffa..3fe7796c 100644 --- a/src/backend/server/raw.ts +++ b/src/backend/server/raw.ts @@ -3,6 +3,11 @@ import { getSettings, resolvePath } from "../internal/model/db" import { parseRangeHeader } from "../internal/stream/stream" import { flushPendingDriverState, getDriver } from "../internal/op/storage" import { resolveShare } from "../internal/op/share" +import { + getCachedLink, + setCachedLink, + type CacheTarget, +} from "../internal/cache" import { needDownloadSign, verifyDownloadSign, @@ -444,7 +449,7 @@ rawRouter.get("/*", async (c) => { } } - const resolved = await resolvePath(reqPath) + const resolved = await resolvePath(reqPath, c.env) if (resolved.isVirtual || !resolved.physical) { return c.text("Cannot download virtual directory path", 400) @@ -496,20 +501,31 @@ rawRouter.get("/*", async (c) => { // 管理员配置的受信存储 endpoint host(可能是内网自建 S3/WebDAV/MinIO), // 加上全局环境变量 SSRF_ALLOWED_HOSTS,合并为 SSRF 白名单,避免被误拦截。 const trustedHosts = getTrustedHosts(resolved.storage.addition, c.env) - const driver = await getDriver( - resolved.storage.driver, - resolved.storage, - ) - let fileItem - try { - fileItem = await driver.get(reqPath, resolved.physical) - } finally { - await flushPendingDriverState( - resolved.storage.driver, - resolved.storage, - driver, - getStorageRequestContext(c), - ) + // 下载链接缓存:与 /fs/get 共用同一份缓存(键 = storage + 虚拟路径)。 + // 命中时直接复用直链,跳过驱动换链(网盘侧通常最贵、最易被限流)。 + const cacheTarget: CacheTarget = { + storage: resolved.storage, + cleanPath: resolved.cleanPath, + physical: resolved.physical, + isVirtual: resolved.isVirtual, + } + let fileItem = await getCachedLink(cacheTarget, c.env) + // 仅在缓存未命中时才会实例化驱动(命中时 driver 保持 null, + // 见下方 createReadStream 分支的空值保护)。 + let driver: any = null + if (!fileItem) { + driver = await getDriver(resolved.storage.driver, resolved.storage) + try { + fileItem = await driver.get(reqPath, resolved.physical) + } finally { + await flushPendingDriverState( + resolved.storage.driver, + resolved.storage, + driver, + getStorageRequestContext(c), + ) + } + await setCachedLink(cacheTarget, fileItem, c.env) } if (fileItem && fileItem.raw_url) { @@ -579,7 +595,8 @@ rawRouter.get("/*", async (c) => { ) return c.redirect(fileItem.raw_url, 302) } else if ( - typeof (driver as any).createReadStream === "function" && + driver && + typeof driver.createReadStream === "function" && fileItem && !fileItem.is_dir ) { diff --git a/src/backend/server/storage_request_context.test.ts b/src/backend/server/storage_request_context.test.ts new file mode 100644 index 00000000..648c7f87 --- /dev/null +++ b/src/backend/server/storage_request_context.test.ts @@ -0,0 +1,156 @@ +/** + * 回归测试:请求级存储上下文必须始终携带 `env`。 + * + * 背景(真实缺陷):`getStorageRequestContext()` 早期实现在缺少 + * `executionCtx.waitUntil` 时直接返回 `undefined`,把 `env` 一起丢了。 + * 于是缓存解析后端时拿到 `env=undefined` → `getStorageBackend(undefined)` + * 解析成**内存驱动**,而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与 + * 数据库被静默拆到两个后端:列表看似命中缓存(内容陈旧),但持久层里 + * 一条缓存键都没有、`/cache/status` 计数恒为 0、`/cache/clear` 永远 removed=0。 + * + * 这里刻意**不传** `app.request()` 的第 4 个参数(executionCtx), + * 断言缓存仍然落到 `CACHE_DRIVER=db` 解析出的真实后端(KV)上。 + */ +import assert from "node:assert/strict" +import { test } from "node:test" +import { mkdirSync, writeFileSync, rmSync } from "node:fs" +import app from "../index" + +const JWT_SECRET = "jwt-secret-32-characters-long!!" +const ORIGIN = "http://localhost" +const SANDBOX = process.cwd() + "/.tmp-storage-ctx" +const ROOT = SANDBOX + "/root" + +/** 内存 KV 绑定(EdgeOne KV 语义:list -> { keys: [{ key }], complete }) */ +function makeKv() { + const store = new Map() + return { + store, + binding: { + async get(key: string) { + return store.has(key) ? store.get(key)! : null + }, + async put(key: string, value: string) { + store.set(key, value) + }, + async delete(key: string) { + store.delete(key) + }, + async list(opts?: { prefix?: string }) { + const prefix = opts?.prefix || "" + return { + keys: [...store.keys()] + .filter((k) => k.startsWith(prefix)) + .map((key) => ({ key })), + complete: true, + } + }, + }, + } +} + +test("缺少 executionCtx 时,缓存仍必须写入 CACHE_DRIVER=db 指向的真实后端", async () => { + const kv = makeKv() + const env: any = { + DB_DRIVER: "kv", + DB_FORMAT: "map", + KV: kv.binding, + JWT_SECRET, + ADMIN_PASS: "admin", + __requestOrigin: ORIGIN, + } + + rmSync(SANDBOX, { recursive: true, force: true }) + mkdirSync(ROOT + "/sub", { recursive: true }) + writeFileSync(ROOT + "/sub/c.txt", "gamma") + + try { + const post = (path: string, body: any, token?: string) => + // 注意:刻意省略第 4 个参数(executionCtx) + app.request( + path, + { + method: "POST", + headers: token + ? { "Content-Type": "application/json", Authorization: `Bearer ${token}` } + : { "Content-Type": "application/json" }, + body: JSON.stringify(body ?? {}), + }, + env, + ) + + const login: any = await (await post("/api/auth/login", { + username: "admin", + password: "admin", + })).json() + const token = login?.data?.token + assert.ok(token, `login failed: ${JSON.stringify(login)}`) + + const created: any = await (await post( + "/api/admin/storage/create", + { + mount_path: "/ctx", + driver: "Local", + addition: JSON.stringify({ root_folder_path: ROOT }), + order: 0, + }, + token, + )).json() + assert.equal(created?.code, 200, `create storage failed: ${JSON.stringify(created)}`) + + // 首次列目录 → 应回源并写入缓存 + const listed: any = await (await post( + "/api/fs/list", + { path: "/ctx/sub", page: 1, per_page: 0 }, + token, + )).json() + assert.equal(listed?.code, 200, `list failed: ${JSON.stringify(listed)}`) + assert.deepEqual( + (listed?.data?.content || []).map((i: any) => i.name), + ["c.txt"], + ) + + // 核心断言:缓存键必须出现在 KV 里。 + // 修复前这里恒为空 —— 缓存被静默写进了内存驱动。 + const cacheKeys = [...kv.store.keys()].filter((k) => k.startsWith("openlist_cache_ft_")) + assert.ok( + cacheKeys.length >= 1, + `expected a file-tree cache key in the configured KV backend, got keys: ` + + JSON.stringify([...kv.store.keys()]), + ) + + // 键名用缓存模块自己的编码函数推导,避免硬编码编码细节 + const { buildCacheKey } = await import("../internal/cache/store") + const storageId = 1 + const expectedKey = buildCacheKey("ft", storageId, "/ctx/sub", "openlist_cache") + assert.ok( + cacheKeys.includes(expectedKey), + `unexpected cache key: expected ${expectedKey}, got ${JSON.stringify(cacheKeys)}`, + ) + + // 缓存值应为带过期时间的信封 + const raw = kv.store.get(cacheKeys[0])! + const envelope = JSON.parse(raw) + assert.ok(Array.isArray(envelope?.v), `cache envelope payload is not an array: ${raw.slice(0, 120)}`) + assert.ok(typeof envelope?.exp === "number" && envelope.exp > Date.now(), "cache envelope missing future exp") + + // 管理接口应能统计到条目(同样验证 env 透传到了缓存层) + const status: any = await ( + await app.request("/api/admin/cache/status", { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }, env) + ).json() + assert.equal(status?.code, 200) + assert.ok( + (status?.data?.entries?.file_tree ?? 0) >= 1, + `cache/status did not count the entry: ${JSON.stringify(status?.data?.entries)}`, + ) + assert.ok( + (status?.data?.drivers || []).includes("kv"), + `cache/status resolved unexpected drivers: ${JSON.stringify(status?.data?.drivers)}`, + ) + } finally { + rmSync(SANDBOX, { recursive: true, force: true }) + } +}) diff --git a/wrangler.jsonc b/wrangler.jsonc index 6f54c357..c552fd07 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -82,6 +82,43 @@ // 自动生成并持久化共享密钥(JWT 签名需要)。 "DB_CIPHER": "none", + // ── 缓存(文件树缓存 / 下载链接缓存)───────────────────────────────── + // 参考 OpenList.ts 的两级持久缓存: + // 文件树缓存 - /api/fs/list|get|dirs 优先读缓存,未命中才请求网盘 + // 下载链接缓存 - /d、/p 复用驱动换取的直链,避免重复换链/签名 + // + // 默认【只向数据库启用】:缓存与业务数据走同一条存储链路(DB_DRIVER + // 解析出的 D1/KV/Blob/MySQL/DO)。想要改用或额外启用 KV、Blob 等专用 + // 后端,必须显式配置 CACHE_DRIVER(不会自动探测、不会自动回退)。 + // + // 总开关(默认 true) + "CACHE_ENABLED": "true", + // + // 缓存后端(默认 db = 与业务数据同后端): + // db - 数据库(DB_DRIVER 解析结果)★默认 + // kv - KV binding(名固定为 KV) + // blob - EdgeOne Blob SDK / ESA Blob + // cfkv - Cloudflare KV REST API(需 CF_* 三项) + // do - Durable Objects + // memory - 进程内存(仅本地调试) + // none - 关闭缓存 + // 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 "db,kv" + "CACHE_DRIVER": "db", + // + // 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 + // TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) + "CACHE_FILE_TREE": "true", + "CACHE_DOWNLOAD_LINK": "false", + // + // 文件树缓存时长(分钟)。0 = 跟随存储级 cache_expiration(默认 30 分钟, + // 可被该存储的 custom_cache_policies 按路径覆盖;命中 0 表示不缓存) + "CACHE_TTL": "0", + // + // 下载链接缓存时长(分钟)。直链通常自带有效期,取值需保守(默认 5) + "CACHE_LINK_TTL": "5", + // + // 不参与缓存的驱动(纯本地计算型,缓存只会变陈旧) + "CACHE_EXCLUDE_DRIVERS": "virtual,alias,url_tree,strm,chunk", // ── singleflight(并发相同调用去重)────────────────────────────────── // // 一个部署会同时存在多个 isolate,进程内去重无法跨实例生效,因此当存在 From 7a9b9f21666c3a364364313aab780a0be4375571 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:13:43 +0800 Subject: [PATCH 17/49] fix(authz): enforce current role, token revocation, S3 permissions (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(authz): enforce current role, token revocation, S3 permissions and user-root isolation checkAdminAuth: no longer trusts the JWT role claim. Logout and demotion previously had no effect on admin routes because only payload.role was read and jti was never checked; it now re-checks the revocation list, the disabled flag and the current database role. authUserFromReq: reject disabled users. JWT only proves the identity was valid when issued; every resolver must re-check current status. Core APIs went through getUserFromContext (which checks), but the S3 gateway reused this function and did not. S3 gateway: PutObject now requires WRITE_CONTENT and DeleteObject requires DELETE; object paths are scoped to the caller's base_path and bucket/key are validated. Previously any authenticated user, including read-only and disabled accounts, could write anywhere. getActualPath: fold dot segments before joining base_path so a user cannot escape their root via plain or percent-encoded traversal. verify: 225 tests pass. * fix(authz): per-op permission bits, archive ACL/boundaries, corrupt-config guard, upload limits, WebAuthn hardening Permission bits: rename/batch_rename/regex_rename use RENAME, remove/remove_empty_directory use DELETE, move/recursive_move use MOVE, copy uses COPY, instead of all sharing WRITE_CONTENT. meta.write_users now enforced at write time; archive read shares the /fs/get meta+password judgement; archive list/meta require READ_ARCHIVES and decompress requires DECOMPRESS plus write, with the destination scoped to the caller root and ZIP entry names restricted to relative-only so ../ cannot escape. Uploads re-check the actual received byte count (Content-Length could be omitted to bypass the limit). map format now throws on damaged JSON instead of returning null, so corruption is no longer mistaken for a fresh deployment that reopens public setup. Storage: key format writes before deleting, saveDb rolls memory/cache back on failed persistence, D1 batches retry per chunk and report partial commits, S3/WebDAV scope paths to the user root, archive fetch follows redirects safely. Sessions: revocation list uses the unified storage backend with bounded reload, multipart sessions are owner-bound, WebAuthn challenges are server-issued and single-use with origin/UP/UV checks and disabled-user rejection. verify: 285 tests pass; SSRF probe 22 block / 8 allow; WebAuthn probe all green. * fix(authz): meta ACL parity on fs/webdav/s3, JWT password stamp, Go user-role guards 1) 目录级访问控制补齐(消除跨协议权限旁路):/fs/dirs、/fs/search、WebDAV(PROPFIND/GET/HEAD/PUT/MKCOL/DELETE/MOVE/COPY)、S3(GET/HEAD/PUT/DELETE) 现在与 /fs/list /fs/get 使用同一套 meta 判定(密码 + read_users + write_users + meta.write 绕过开关)。/fs/dirs 的 force_root 只对管理员开放。 2) /fs/get_direct_upload_info 对齐 Go FsGetDirectUploadInfo:file_name 单层相对路径校验、meta 写 ACL、存储挂载边界校验、Overwrite:false 已存在探测。 3) JWT 增加 pwd_ts 密码指纹(对齐 Go UserClaims.PwdTS):登录/SSO/WebAuthn 三处签发,authUserFromReq/getUserFromContext/checkAdminAuth 三处校验 —— 改密后已签发令牌立即失效(历史缺陷:改密不注销旧会话)。 4) 用户管理对齐 Go:create 禁止 admin/guest;update 禁止变更 role、禁止禁用管理员;delete 按角色保护 admin/guest(不再只硬编码 id===1)。 5) WebDAV Basic Auth 接入登录防爆破(失败计数 + 指数退避锁定,与 /api/auth/login 共用),OPTIONS 匿名放行(对齐 Go WebDAVAuth)。 6) 删除(按评审决定):bcrypt/无盐单层哈希兼容与登录时自动迁移(只保留 Go 双层哈希);user/create 自动生成随机密码并回显;TS 专有 /fs/upload/{create,part,complete} 端点(分片只保留官方 /fs/multipart/*)。 7) 顺带修复 5 个改动前就存在的 tsc 报错(db_cipher.test.ts 重复 DbCipher 导入、/form 的 getUploadSizeLimit 少传参、multipart/init 的 user 空值收窄)。 验证:tsc 0 error;server 179 / model+pkg 50 / store 12 / drivers 131 全绿;新增 security_hardening.test.ts 覆盖角色保护与改密失效。 * fix(review): 上传限额覆盖 /fs/multipart/chunk 与 S3 PutObject,移除伪造 ETag 回应 PR #92 评审(Request Changes): P1-1 /fs/multipart/chunk 此前只做 Content-Length 预检,省略该头即可绕过单片上限并把整片读入内存;现改用 readBodyWithinLimit(c, true),并删掉重复的预检分支。(评审提到的 /fs/upload/part 已在上一轮删除,分片只保留官方 /fs/multipart/*。) P1-2 S3 PutObject 完全没有体积限制(MAX_UPLOAD/MAX_UPPART 都不生效,超大请求体整包读入内存);现接入共享限额实现,超限返回 S3 风格 EntityTooLarge(400)。为此把限额辅助函数抽到 pkg/upload-limit.ts,fs.ts 与 s3.ts 共用同一份。 P2-3 PutObject 不再返回伪造的 ETag(此前是 Date.now(),并非内容摘要,会误导依赖 ETag 做幂等/续传/校验的客户端)。真实 MD5 所需的算法 WebCrypto 不提供、纯 JS 实现要整包二次扫描、hash-wasm 的 WASM 在边缘运行时可能被禁用,故选择不返回 ETag(宁缺勿错),真实摘要留作后续跟进。 P2-4 /fs/other 签发的直传 URL 由客户端直连对象存储、不经过本站,MAX_UPLOAD/MAX_UPPART 覆盖不到——已在代码注释与 PR 描述中注明该边界。 P2-5 新增显式回归测试:normalizeSegments 越根钳制、getActualPath/isWithinUserRoot 边界、readBodyWithinLimit 的「省略 Content-Length 也按实际字节数拒绝」。 另修正 main 上既有失败测试:drivers/github_releases 的 gh_proxy 断言写成「代理值+完整链接」,而实现(与 Go strings.Replace 一致)是「替换 https://github.com 前缀」——纯测试改动。 验证:tsc 0 error;server 195 / model+pkg 81 / store 12 / drivers 151 全绿。 * fix(review): WebDAV meta 403 补指引提示;upload-limit 预检函数不再导出 回应第二轮评审(v3→v4,Approve)的两条 P2: P2-1 WebDAV 侧 meta 判定失败时只返回裸 403,用户容易误判成账号权限问题。现统一改为 metaForbidden():403 + 一句指引(WebDAV 无法携带目录密码,请改用 Web 界面或 /api/fs/* 带 password),措辞刻意不区分密码/read_users/write_users,不泄露具体限制类型。共 8 处(PROPFIND、GET/HEAD、PUT、MKCOL、DELETE、MOVE、COPY×2)统一使用。 P2-2 pkg/upload-limit.ts 的 exceedsUploadLimit 只做 Content-Length 预检,被独立调用会误判为「已通过检查」。现补充 JSDoc 明确它不是完整保护,并取消导出(模块内仅供 readBodyWithinLimit 使用),从接口层面消除误用。 新增回归测试:WebDAV 受 meta 保护路径返回带指引的 403(不触发存储层)。 验证:tsc 0 error;server 196 / model+pkg 81 / store 12 / drivers 151 全绿。 --------- Co-authored-by: pikachuren --- .../drivers/github_releases/util.test.ts | 12 +- src/backend/internal/model/db.ts | 17 + src/backend/internal/model/store/driver/d1.ts | 40 +- .../internal/model/store/format/key.ts | 31 +- .../internal/model/store/format/map.ts | 18 +- src/backend/internal/op/search.ts | 37 +- src/backend/internal/upload/multipart.ts | 21 +- src/backend/pkg/http.ts | 164 +++-- src/backend/pkg/password.ts | 33 +- src/backend/pkg/permission.ts | 60 +- src/backend/pkg/upload-limit.ts | 71 +++ src/backend/pkg/utils.ts | 40 +- src/backend/server/admin.ts | 19 +- src/backend/server/auth.ts | 82 +-- .../server/default_credentials.test.ts | 13 +- src/backend/server/fs.ts | 582 ++++++++++-------- src/backend/server/middlewares.ts | 111 ++-- src/backend/server/raw.ts | 2 +- src/backend/server/s3.ts | 215 ++++++- src/backend/server/security_hardening.test.ts | 388 ++++++++++++ src/backend/server/sso.ts | 4 +- src/backend/server/user.ts | 76 ++- src/backend/server/webauthn.ts | 195 +++++- src/backend/server/webdav.ts | 316 ++++++++-- 24 files changed, 1967 insertions(+), 580 deletions(-) create mode 100644 src/backend/pkg/upload-limit.ts create mode 100644 src/backend/server/security_hardening.test.ts diff --git a/src/backend/drivers/github_releases/util.test.ts b/src/backend/drivers/github_releases/util.test.ts index 0fba3d72..62188a78 100644 --- a/src/backend/drivers/github_releases/util.test.ts +++ b/src/backend/drivers/github_releases/util.test.ts @@ -15,11 +15,19 @@ function proxyOf(additionGhProxy: string | undefined, url: string): string { return (client as unknown as { proxy: (u: string) => string }).proxy(url) } -test("gh_proxy 已配置时为 http(s) 下载链接加代理前缀", () => { +test("gh_proxy 已配置时替换 https://github.com 前缀(对齐 Go Link)", () => { const url = "https://github.com/example/repo/releases/download/v1.0.0/app.zip" + // 文档推荐写法(meta.go 的 help):值本身包含 https://github.com, + // 替换后等价于「代理前缀 + 原始链接」 + assert.equal( + proxyOf("https://gh-proxy.com/https://github.com", url), + "https://gh-proxy.com/" + url, + ) + // 裸域名写法:按 Go strings.Replace 的语义,只把 https://github.com 这一段整体换掉 + // (此前断言写成 proxy + 完整 url,与实现/Go 语义不符) assert.equal( proxyOf("https://mirror.example.com", url), - "https://mirror.example.com" + url, + "https://mirror.example.com" + url.slice("https://github.com".length), ) }) diff --git a/src/backend/internal/model/db.ts b/src/backend/internal/model/db.ts index 4c3ca665..4cb721b4 100644 --- a/src/backend/internal/model/db.ts +++ b/src/backend/internal/model/db.ts @@ -2066,6 +2066,15 @@ export const saveDb = async ( return false } + // 记录写入前的内存状态,供持久化失败时回滚。 + // + // 稳定性不变量:**持久化失败后,失败的修改不得在内存/请求缓存中生效**。 + // 历史实现先更新 memoryDb 与 dbCache、再尝试落盘,失败只抛错不回滚。 + // 结果是「接口报 500,但当前实例的角色/权限/设置已按未保存的内容生效」—— + // 调用方以为失败、系统却按新值运行(实测:保存失败后 getDb() 仍读到新 role)。 + const previousDb = memoryDb + const previousTrusted = dbTrusted + memoryDb = data dbWriteBlocked = false // Refresh the request cache so any getDb() later in this request observes @@ -2112,6 +2121,14 @@ export const saveDb = async ( ) await backend.save(sealed, activeEnv) } catch (err: any) { + // 回滚内存与请求缓存:持久化没成功,就不能让本次修改在本实例内可见, + // 否则会出现「报错但已生效」的不一致状态。 + memoryDb = previousDb + dbTrusted = previousTrusted + if (cacheKey) { + if (previousDb) dbCache.set(cacheKey, { ts: Date.now(), db: previousDb }) + else dbCache.delete(cacheKey) + } console.error( `[DB] saveDb FAILED: backend=${backend.name}, error=${err?.message || err}`, `stack=${err?.stack?.substring(0, 500) || ""}`, diff --git a/src/backend/internal/model/store/driver/d1.ts b/src/backend/internal/model/store/driver/d1.ts index cffbde68..98420f3a 100644 --- a/src/backend/internal/model/store/driver/d1.ts +++ b/src/backend/internal/model/store/driver/d1.ts @@ -143,13 +143,45 @@ export const d1Driver: Driver = { if (!db) throw new Error("D1 binding not found") await ensureSchema(db, env) - - // D1 batch 单次语句数上限约 100,分批提交 + + // D1 batch 单次语句数上限约 100,必须分批提交。 + // + // 语义边界(重要,不要误以为这里提供整体事务):**片内原子,跨片不原子**。 + // D1 没有跨 batch 调用的事务,因此语句数 >100 时,后续片失败**不会**回滚 + // 已提交的片。要让整体操作可恢复,依赖两点: + // ① 上层 format/sql 生成的是幂等 UPSERT + 末位 DELETE NOT IN,整体重跑可收敛; + // ② 这里对**单片的瞬时失败**做有限重试,避免一次网络抖动就留下半成品。 + // 重试仍失败时,错误信息带上「已提交片数」,供调用方判断是否需要整体重跑。 const BATCH = 100 + const MAX_RETRY = 2 const stmts = statements.map((s) => db.prepare(s.sql).bind(...s.params)) - + + let committed = 0 for (let i = 0; i < stmts.length; i += BATCH) { - await db.batch(stmts.slice(i, i + BATCH)) + const chunk = stmts.slice(i, i + BATCH) + let lastErr: any = null + for (let attempt = 0; attempt <= MAX_RETRY; attempt++) { + try { + await db.batch(chunk) + lastErr = null + break + } catch (err) { + lastErr = err + if (attempt < MAX_RETRY) { + await new Promise((r) => setTimeout(r, 50 * (attempt + 1))) + } + } + } + if (lastErr) { + throw new Error( + `D1 batch failed at chunk ${Math.floor(i / BATCH) + 1} ` + + `(statements ${i}-${i + chunk.length - 1} of ${stmts.length}); ` + + `${committed} statement(s) were already committed by earlier chunks and ` + + `D1 cannot roll them back across batches. The save is idempotent, so ` + + `retrying the whole operation converges. Cause: ${lastErr?.message || lastErr}`, + ) + } + committed += chunk.length } }, diff --git a/src/backend/internal/model/store/format/key.ts b/src/backend/internal/model/store/format/key.ts index 432a3250..b38075ba 100644 --- a/src/backend/internal/model/store/format/key.ts +++ b/src/backend/internal/model/store/format/key.ts @@ -66,16 +66,16 @@ export const keyFormat: FormatAdapter = { }, async save(data: any, driver: Driver, env?: any): Promise { - // 清空旧数据 - for (const table of TABLE_NAMES) { - const prefix = tablePrefix(table) - const keys = await driver.list(prefix, env) - for (const key of keys) { - await driver.delete(key, env) - } - } - - // 写入新数据 + // 先写新数据,**最后**清理陈旧键。 + // + // 稳定性不变量:save 失败不得让「原有数据整体消失」。 + // 历史实现是「先删光所有前缀键,再写新数据」:一旦写入阶段失败(网络抖动、 + // 配额、实例被杀),旧记录已被删除、新记录尚未写入,数据库直接变成空表。 + // 故障注入实测:users 从 1 条变为 0 条 —— 这是数据丢失,不是「保存失败」。 + // + // 改为先写后删后,失败时的最坏情况是**残留**旧键(可被下一次成功保存清理), + // 而不会丢数据;正常成功路径的最终状态与原来完全一致。 + const liveKeys = new Set() for (const table of TABLE_NAMES) { const keyCol = TABLE_KEY[table] const records = data?.[table] || [] @@ -85,11 +85,22 @@ export const keyFormat: FormatAdapter = { if (!id) continue const key = fullKeyOf(table, id) + liveKeys.add(key) const value = JSON.stringify(record) await driver.put(key, value, env) } } + // 清理本快照中已不存在的键(删除的实体 / 改名的键) + for (const table of TABLE_NAMES) { + const prefix = tablePrefix(table) + const keys = await driver.list(prefix, env) + for (const key of keys) { + if (liveKeys.has(key)) continue + await driver.delete(key, env) + } + } + // 标记已初始化 await driver.put(INIT_MARK, String(Date.now()), env) return true diff --git a/src/backend/internal/model/store/format/map.ts b/src/backend/internal/model/store/format/map.ts index e3bc1868..2f53a36b 100644 --- a/src/backend/internal/model/store/format/map.ts +++ b/src/backend/internal/model/store/format/map.ts @@ -22,8 +22,22 @@ export const mapFormat: FormatAdapter = { try { return JSON.parse(raw) } catch (err) { - console.error("[mapFormat] Failed to parse JSON:", err) - return null + // 关键:配置**损坏**绝不能等同于「没有数据」。 + // + // 历史行为是打印一条日志后返回 null。但 loadDb() 用同一个 null 表示 + // 「后端为空(全新部署)」,两者无法区分,于是损坏的配置会被当成全新部署: + // dbTrusted=false 且 getDbLoadError()=null → /api/public/init/setup 放行 + // → 无登录即可创建新管理员并把空壳写回,**覆盖真实配置**。 + // (实测:把持久化值改成非法 JSON 后,公开的初始化接口返回 200。) + // + // 这里改为抛错:loadDb() 会记录 dbLastLoadError 并保持 dbTrusted=false, + // 初始化接口据此拒绝(返回 500 且不写入)。真正的空库不经过此分支, + // 仍可正常初始化。 + throw new Error( + `[mapFormat] persisted config is not valid JSON; refusing to treat it as an ` + + `empty database. Restore or explicitly remove the "${CONFIG_KEY}" entry. ` + + `Cause: ${(err as Error)?.message || String(err)}`, + ) } }, diff --git a/src/backend/internal/op/search.ts b/src/backend/internal/op/search.ts index 411aa1a8..c483e0a5 100644 --- a/src/backend/internal/op/search.ts +++ b/src/backend/internal/op/search.ts @@ -1,5 +1,8 @@ import { listItems } from "./storage" import { FileItem } from "../driver/base" +import { canAccess, getNearestMeta } from "../../pkg/meta" +import type { Meta } from "../../pkg/meta" +import type { UserPermissionObj } from "../../pkg/permission" export interface SearchOptions { parent?: string @@ -12,6 +15,14 @@ export interface SearchOptions { max_depth?: number /** Max items to collect before truncating (default 500) */ max_results?: number + /** + * 访问者。提供时对每个结果做 meta 判定过滤(密码保护 + read_users 白名单), + * 对齐 Go handles.Search 传给 SearchFiltered 的 filter 闭包;不提供时 + * (内部调用 / 管理员构建索引)不做过滤,与历史行为一致。 + */ + user?: UserPermissionObj | null + /** 目录访问密码(meta.password) */ + password?: string } export interface SearchResultItem extends FileItem { @@ -43,6 +54,7 @@ export async function search( const maxResults = Math.min(2000, Math.max(1, opts.max_results ?? 500)) const matches: SearchResultItem[] = [] + const enforceAcl = opts.user !== undefined async function walk(dirPath: string, depth: number) { if (depth > maxDepth || matches.length >= maxResults) return @@ -56,9 +68,32 @@ export async function search( return } + // 与 Go 一致:meta 取「父目录」的最近 meta,再对子项自身路径做判定 + let dirMeta: Meta | null = null + if (enforceAcl) { + try { + dirMeta = await getNearestMeta(dirPath, env) + } catch { + dirMeta = null + } + } + for (const item of items) { if (matches.length >= maxResults) break + const subPath = + dirPath === "/" ? `/${item.name}` : `${dirPath}/${item.name}` + + // meta 访问控制:密码保护 / read_users 白名单覆盖的内容不出现在搜索 + // 结果里。历史缺陷:搜索完全跳过 meta 判定——实测直连被密码保护而 + // 被拒的目录,其文件名/大小仍能被搜出来(信息泄露)。 + if ( + enforceAcl && + !canAccess(opts.user!, dirMeta, subPath, opts.password || "") + ) { + continue + } + const nameMatch = !keyword || item.name.toLowerCase().includes(keyword) const isDir = !!item.is_dir // 挂载点虚拟文件夹(storage.ts 合并虚拟子挂载时 sign 为空), @@ -81,8 +116,6 @@ export async function search( } if (isDir) { - const subPath = - dirPath === "/" ? `/${item.name}` : `${dirPath}/${item.name}` await walk(subPath, depth + 1) } } diff --git a/src/backend/internal/upload/multipart.ts b/src/backend/internal/upload/multipart.ts index cde3e316..1d4adcc6 100644 --- a/src/backend/internal/upload/multipart.ts +++ b/src/backend/internal/upload/multipart.ts @@ -36,6 +36,17 @@ export interface MultipartSession { partMd5s: (string | undefined)[] /** 驱动名 + 存储引用,用于 chunk/complete 时重新 resolve 驱动 */ storage_driver: string + /** + * 会话所有者的用户 id。 + * + * 安全不变量:分片会话是**用户私有状态**,续传/完成/查询都必须校验所有者。 + * 历史缺陷:会话只按 `path + size` 复用,不含用户维度,导致不同用户(即使 + * base_path 不同)会命中同一个会话并互相续传(实测:两个不同根目录的用户 + * 拿到同一个 upload_id,且 resumed=true)。 + */ + owner_id?: number + /** 创建会话时的用户根目录,用于校验会话未被跨根复用 */ + owner_root?: string created_at: number error?: string } @@ -119,12 +130,20 @@ export function getSession(uploadId: string): MultipartSession | undefined { return sessions.get(uploadId) } -/** 查找同 path+size 的未完成会话(用于断点续传) */ +/** + * 查找同 owner + path + size 的未完成会话(用于断点续传)。 + * + * `ownerId` 必填:会话必须按所有者隔离,否则不同用户会共享同一上传会话。 + * 未绑定所有者的历史会话(本改动之前创建的)不再被复用,避免跨用户命中。 + */ export function findReceivingSession( path: string, size: number, + ownerId?: number, ): MultipartSession | undefined { + if (ownerId === undefined) return undefined for (const s of sessions.values()) { + if (s.owner_id !== ownerId) continue if ( s.path === path && s.size === size && diff --git a/src/backend/pkg/http.ts b/src/backend/pkg/http.ts index 8b1ff848..84303b3d 100644 --- a/src/backend/pkg/http.ts +++ b/src/backend/pkg/http.ts @@ -156,6 +156,92 @@ export async function download( return res.data } +/** + * IPv4 是否属于「不得访问」的网段(RFC1918 私有、回环、链路本地、CGNAT、 + * 组播、保留段、云元数据)。 + * + * 抽成独立函数的原因:IPv6 字面量里可能**内嵌** IPv4(`::ffff:127.0.0.1`、 + * 6to4、NAT64),需要把内嵌地址还原后复用同一套判定,避免两处规则漂移。 + */ +function isRestrictedIpv4(octets: number[]): boolean { + const [a, b, c] = octets + if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) return true + if (a === 0) return true // 0.0.0.0/8 (This network) + if (a === 127) return true // 127.0.0.0/8 (Loopback) + if (a === 10) return true // 10.0.0.0/8 (Private) + if (a === 172 && b >= 16 && b <= 31) return true // 172.16.0.0/12 (Private) + if (a === 192 && b === 168) return true // 192.168.0.0/16 (Private) + if (a === 169 && b === 254) return true // 169.254.0.0/16 (Link-local + metadata) + if (a === 100 && b >= 64 && b <= 127) return true // 100.64.0.0/10 (CGNAT) + if (a === 100 && b === 100) return true // Aliyun metadata 100.100.100.200 + if (a === 224 && b === 0 && c === 0) return true // 224.0.0.0/24 (Multicast) + if (a >= 240) return true // 240.0.0.0/4 (Reserved) + return false +} + +/** + * 解析 IPv6 字面量(可含方括号/zone id/内嵌 IPv4)为 8 组 16 位整数。 + * 解析失败返回 null(调用方按「不安全」处理)。 + * + * 为什么不能继续用字符串 `includes` 前缀匹配: + * WHATWG URL 会把 IPv6 规范化(`::ffff:127.0.0.1` -> `::ffff:7f00:1`), + * 于是 `host.includes("::ffff:127.")` 之类的模式永远匹配不到,私网判定被绕过。 + * 必须先解析成数值再按网段判断。 + */ +function parseIpv6Groups(input: string): number[] | null { + let s = String(input || "").trim().replace(/^\[/, "").replace(/\]$/, "") + const zone = s.indexOf("%") + if (zone >= 0) s = s.slice(0, zone) + if (!s.includes(":")) return null + + // 内嵌 IPv4 尾部(::ffff:127.0.0.1)统一改写为两个十六进制组,后续只需一套解析 + const lastColon = s.lastIndexOf(":") + const maybeV4 = s.slice(lastColon + 1) + if (maybeV4.includes(".")) { + const octets = maybeV4.split(".") + if (octets.length !== 4) return null + const nums = octets.map((o) => + /^\d{1,3}$/.test(o) ? parseInt(o, 10) : NaN, + ) + if (nums.some((n) => !Number.isFinite(n) || n > 255)) return null + const g1 = ((nums[0] << 8) | nums[1]).toString(16) + const g2 = ((nums[2] << 8) | nums[3]).toString(16) + s = s.slice(0, lastColon + 1) + g1 + ":" + g2 + } + + const halves = s.split("::") + if (halves.length > 2) return null + + const toGroups = (str: string): number[] | null => { + if (!str) return [] + const out: number[] = [] + for (const g of str.split(":")) { + if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null + out.push(parseInt(g, 16)) + } + return out + } + + const head = toGroups(halves[0]) + const tail = halves.length === 2 ? toGroups(halves[1] ?? "") : [] + if (!head || !tail) return null + + let groups: number[] + if (halves.length === 2) { + const fill = 8 - head.length - tail.length + if (fill < 1) return null + groups = [...head, ...new Array(fill).fill(0), ...tail] + } else { + groups = head + } + return groups.length === 8 ? groups : null +} + +/** 从 16 位组中取出后 32 位,还原为 4 段 IPv4 */ +function embeddedIpv4(high: number, low: number): number[] { + return [high >> 8, high & 0xff, low >> 8, low & 0xff] +} + /** * Validate that a target URL is safe against SSRF attacks: * 1. Protocol must be http: or https: @@ -206,25 +292,44 @@ export function isSafeUrl( } } - // 2. 扩展 IPv6 检测(包括 IPv4-mapped IPv6) - const ipv6Patterns = [ - "::1", // loopback - "[::1]", - "::ffff:127.", // IPv4-mapped IPv6 loopback - "::ffff:10.", // IPv4-mapped IPv6 private - "::ffff:172.", // IPv4-mapped IPv6 private - "::ffff:192.168.", // IPv4-mapped IPv6 private - "::ffff:169.254.", // IPv4-mapped IPv6 link-local - "fe80:", // link-local - "fc00:", // unique local - "fd00:", // unique local - "[fe80:", - "[fc", - "[fd", - ] - for (const pattern of ipv6Patterns) { - if (host.includes(pattern)) { - return false + // 2. IPv6 字面量:解析为数值后按网段判定。 + // + // 历史缺陷:这里用 `host.includes("::ffff:127.")` 之类**字符串前缀**匹配。 + // 但 WHATWG URL 会把 IPv6 规范化(`::ffff:127.0.0.1` -> `::ffff:7f00:1`), + // 使这些模式永远匹配不到 —— 实测 `http://[::ffff:127.0.0.1]/` 与 + // `http://[::ffff:10.0.0.1]/` 都被判为「安全」。IPv4 那套判定明明存在, + // 却因为「表达形式变了」而失效。 + // + // 现在改为:解析成 8 组 16 位整数,按位判定;遇到内嵌 IPv4 的形态 + // (IPv4-mapped / 6to4 / NAT64)则还原后复用 isRestrictedIpv4()。 + const bareHost = host.replace(/^\[/, "").replace(/\]$/, "") + if (bareHost.includes(":")) { + const g = parseIpv6Groups(bareHost) + // 解析不出来时按不安全处理(fail-closed),避免畸形字面量绕过 + if (!g) return false + + const isZero = (from: number, to: number) => + g.slice(from, to).every((x) => x === 0) + + // :: 未指定地址 / ::1 回环 + if (isZero(0, 7) && (g[7] === 0 || g[7] === 1)) return false + // fe80::/10 链路本地 + if ((g[0] & 0xffc0) === 0xfe80) return false + // fc00::/7 唯一本地地址 + if ((g[0] & 0xfe00) === 0xfc00) return false + // ff00::/8 组播 + if ((g[0] & 0xff00) === 0xff00) return false + // ::ffff:0:0/96 IPv4-mapped IPv6 + if (isZero(0, 5) && g[5] === 0xffff) { + if (isRestrictedIpv4(embeddedIpv4(g[6], g[7]))) return false + } + // 2002::/16 6to4:内嵌 IPv4 位于第 2、3 组 + if (g[0] === 0x2002) { + if (isRestrictedIpv4(embeddedIpv4(g[1], g[2]))) return false + } + // 64:ff9b::/96 NAT64 + if (g[0] === 0x64 && g[1] === 0xff9b && isZero(2, 6)) { + if (isRestrictedIpv4(embeddedIpv4(g[6], g[7]))) return false } } @@ -241,23 +346,10 @@ export function isSafeUrl( const match = host.match(ipv4Regex) if (match) { const [, aStr, bStr, cStr, dStr] = match - const a = parseInt(aStr, 10) - const b = parseInt(bStr, 10) - const c = parseInt(cStr, 10) - const d = parseInt(dStr, 10) - if (a > 255 || b > 255 || c > 255 || d > 255) return false - - // RFC 1918 私有网络和特殊用途地址 - if (a === 0) return false // 0.0.0.0/8 (This network) - if (a === 127) return false // 127.0.0.0/8 (Loopback) - if (a === 10) return false // 10.0.0.0/8 (Private) - if (a === 172 && b >= 16 && b <= 31) return false // 172.16.0.0/12 (Private) - if (a === 192 && b === 168) return false // 192.168.0.0/16 (Private) - if (a === 169 && b === 254) return false // 169.254.0.0/16 (Link-local + metadata) - if (a === 100 && b >= 64 && b <= 127) return false // 100.64.0.0/10 (CGNAT) - if (a === 100 && b === 100) return false // Aliyun metadata 100.100.100.200 - if (a === 224 && b === 0 && c === 0) return false // 224.0.0.0/24 (Multicast) - if (a >= 240) return false // 240.0.0.0/4 (Reserved) + const octets = [aStr, bStr, cStr, dStr].map((s) => parseInt(s, 10)) + if (octets.some((n) => n > 255)) return false + // RFC 1918 私有网络和特殊用途地址(与 IPv6 内嵌地址共用同一套判定) + if (isRestrictedIpv4(octets)) return false } // 5. 阻止整数/十六进制 IP 表示(2130706433 = 127.0.0.1, 0x7f000001 = 127.0.0.1) diff --git a/src/backend/pkg/password.ts b/src/backend/pkg/password.ts index 59b68f6e..079144dc 100644 --- a/src/backend/pkg/password.ts +++ b/src/backend/pkg/password.ts @@ -75,25 +75,22 @@ export async function setUserPassword( } /** - * 验证密码(兼容历史单层 sha256,推荐存储为双层) - * @param plain 明文密码(或经过 staticHash 的前端哈希值) - * @param user 含 password + salt 字段的用户对象 + * 密码时间戳指纹(对齐 Go common.UserClaims.PwdTS 及其比对逻辑)。 + * + * 安全不变量:JWT 只证明「签发那一刻身份合法」,不证明「现在仍然合法」。 + * 密码一旦被修改,所有既有令牌必须立即失效 —— Go 在每次请求里都拿令牌中的 + * PwdTS 与数据库当前值比对。历史缺陷:TS 的 JWT 只携带 {id, username, role}, + * 改密后旧令牌在有效期内(7 天)继续可用,除非用户显式登出。 + * + * 取值为空字符串(旧数据/未设置)时两侧一致,视为相等,不影响历史账号。 */ -export async function verifyUserPassword( - plain: string, - user: { password: string; salt?: string }, -): Promise { - if (!user.password) return false - if (user.salt) { - // 双层:先判断 plain 是否已经是 staticHash(前端哈希登录), - // 再判断是否是明文(直接登录)。 - const staticHex = isHex64(plain) ? plain : await staticHash(plain) - const expected = await saltedHash(staticHex, user.salt) - return expected === user.password - } - // 历史单层:password == staticHash(plain) - const staticHex = isHex64(plain) ? plain : await staticHash(plain) - return staticHex === user.password +export function passwordStampOf(user: any): string { + return String(user?.pwd_update_at ?? "") +} + +/** 校验令牌内的密码指纹是否与数据库当前值一致 */ +export function isPasswordStampValid(payload: any, user: any): boolean { + return String(payload?.pwd_ts ?? "") === passwordStampOf(user) } // ---- 工具函数(与存储无关) ---- diff --git a/src/backend/pkg/permission.ts b/src/backend/pkg/permission.ts index 2b2a8ce8..a4e47c30 100644 --- a/src/backend/pkg/permission.ts +++ b/src/backend/pkg/permission.ts @@ -79,6 +79,25 @@ export function canRemove(user?: UserPermissionObj | null): boolean { return can(user, PermissionBit.DELETE) } +/** + * 规范化路径段:合并重复斜杠、丢弃 ".",并用栈折叠 ".."。 + * + * 关键点:栈为空时的 ".." 会被**钳制在根**(`stack.pop()` 作用于空数组是 + * 空操作),因此结果永远以 "/" 开头且不含 ".." 段,无法向上逃逸。 + */ +export function normalizeSegments(p: string): string { + const stack: string[] = [] + for (const seg of String(p || "").split("/")) { + if (!seg || seg === ".") continue + if (seg === "..") { + stack.pop() + continue + } + stack.push(seg) + } + return "/" + stack.join("/") +} + /** * 计算用户请求路径对应的实际存储路径(结合用户的根目录 base_path): * 1. 忽略以 /@s 开头的分享虚拟路径 @@ -87,6 +106,15 @@ export function canRemove(user?: UserPermissionObj | null): boolean { * - reqPath = "/" 或 "" -> "/photos" * - reqPath = "/sub" -> "/photos/sub" * - reqPath = "sub" -> "/photos/sub" + * + * 安全不变量(必须长期保持):**返回值只可能等于 base_path 或位于其下**。 + * 历史缺陷:本函数只做字符串拼接,`..` 段留给下游 resolvePath() 折叠。于是 + * `/photos/alice` + `/../bob/x` 会解析成 `/photos/bob/x` —— 用户根目录被越过。 + * 因为拼接后才规范化,下游只保证「不逃出存储物理根」,并不保证「不逃出用户 + * 根」,两者不是同一约束。这里先把 reqPath 折叠到根再拼接,从源头保证隔离。 + * + * 注意:不额外增加 decodeURIComponent 次数。上游按约定只解码一次,多次解码会 + * 改变合法 `%2f`/百分号字面量的含义(与 Go 的 EncodePath 契约冲突)。 */ export function getActualPath( user?: UserPermissionObj | null, @@ -97,22 +125,32 @@ export function getActualPath( return p } - let basePath = (user?.base_path || "/").trim() - if (!basePath || basePath === "/") { - return p.startsWith("/") ? p : `/${p}` - } - - if (!basePath.startsWith("/")) { - basePath = `/${basePath}` - } - if (basePath.endsWith("/") && basePath.length > 1) { - basePath = basePath.replace(/\/+$/, "") + const basePath = normalizeSegments((user?.base_path || "/").trim() || "/") + if (basePath === "/") { + return normalizeSegments(p) } - const cleanReq = p.startsWith("/") ? p : `/${p}` + const cleanReq = normalizeSegments(p) if (cleanReq === "/") { return basePath } return `${basePath}${cleanReq}` } + +/** + * 判断某个实际路径是否位于用户根目录之内(含等于根目录本身)。 + * + * 供网关类入口(S3 / WebDAV)在把请求路径交给存储层之前做二次确认使用: + * 这些入口直接操作「全局虚拟路径」,不走 getActualPath 的拼接语义,因此需要 + * 一个显式的范围检查,避免受限用户越出 base_path。 + */ +export function isWithinUserRoot( + user: UserPermissionObj | null | undefined, + actualPath: string, +): boolean { + const root = getActualPath(user, "/") + const target = normalizeSegments(actualPath) + if (root === "/") return true + return target === root || target.startsWith(root + "/") +} diff --git a/src/backend/pkg/upload-limit.ts b/src/backend/pkg/upload-limit.ts new file mode 100644 index 00000000..ce478e6c --- /dev/null +++ b/src/backend/pkg/upload-limit.ts @@ -0,0 +1,71 @@ +/** + * 上传体积上限(M-5:防止超大请求体被整体读入内存导致 Worker OOM)。 + * + * 覆盖范围:`/fs/put`、`/fs/form`、`/fs/multipart/chunk`(单片)以及 S3 `PutObject`。 + * 单次整体上传与分片单片分开设限,均可通过环境变量 `MAX_UPLOAD` / `MAX_UPPART` 覆盖。 + * + * 安全不变量:体积上限必须按**实际收到的字节数**判定,不能只信 `Content-Length`—— + * 该头由客户端提供,可以省略、伪造或与真实 body 不一致。 + * + * 历史缺陷:只做 `Content-Length` 预检,**省略该头即完全不拦截**——实测在 + * `MAX_UPLOAD=4` 时,不带 `Content-Length` 的 5 字节请求写入成功。这使上限形同 + * 虚设,超大请求体可被整体读入内存。 + * + * 注意:`/fs/other` 签发的存储直传 URL(客户端直接写对象存储、不经过本站)不受 + * 本模块约束——数据的实际写入由下游存储自己限额。 + */ + +export const DEFAULT_MAX_UPLOAD = 25 * 1024 * 1024 // 25MB +export const DEFAULT_MAX_UPPART = 16 * 1024 * 1024 // 16MB + +export function getUploadSizeLimit(c: any, part = false): number { + const env = c?.env || {} + const key = part ? "MAX_UPPART" : "MAX_UPLOAD" + const raw = + env[key] || (typeof process !== "undefined" ? process.env?.[key] : "") + if (raw) { + const n = parseInt(String(raw), 10) + if (Number.isFinite(n) && n > 0) return n + } + return part ? DEFAULT_MAX_UPPART : DEFAULT_MAX_UPLOAD +} + +/** + * 只做 `Content-Length` 预检:超限时返回该上限值,否则返回 null。 + * + * ⚠️ 本函数**不是完整保护**。`Content-Length` 可被省略或伪造,缺失时它直接 + * 返回 null(即放行)——任何**只**调用本函数的路径都会重新引入「省略该头即可 + * 绕过上限」的历史缺陷。需要完整保护时必须使用 `readBodyWithinLimit` + * (它内部先调用本函数做预检,再按实际字节数复核)。 + * + * 正因如此这里**刻意不导出**,避免日后被独立误用。 + */ +function exceedsUploadLimit(c: any, part = false): number | null { + const contentLength = parseInt(c.req.header("Content-Length") || "0", 10) + if (!contentLength) return null + const max = getUploadSizeLimit(c, part) + return contentLength > max ? max : null +} + +export const tooLargeResponse = (c: any, limit: number, label = "File") => + c.json( + { code: 413, message: `${label} too large (max ${limit} bytes)`, data: null }, + 413, + ) + +/** + * 读取请求体,并按**实际字节数**复核上传上限。 + * + * 仍保留 `Content-Length` 预检:能在读入内存**之前**就拒绝超大请求,省一次拷贝。 + */ +export async function readBodyWithinLimit( + c: any, + part = false, +): Promise<{ ok: true; buffer: Buffer } | { ok: false; limit: number }> { + const pre = exceedsUploadLimit(c, part) + if (pre !== null) return { ok: false, limit: pre } + const buffer = Buffer.from(await c.req.arrayBuffer()) + const max = getUploadSizeLimit(c, part) + if (buffer.length > max) return { ok: false, limit: max } + return { ok: true, buffer } +} diff --git a/src/backend/pkg/utils.ts b/src/backend/pkg/utils.ts index 0ea9bd7a..e9b0a3f9 100644 --- a/src/backend/pkg/utils.ts +++ b/src/backend/pkg/utils.ts @@ -34,20 +34,40 @@ export async function checkAdminAuth(c: Context): Promise { ? authHeader.substring(7) : authHeader - // JWT:管理员登录用户也视为管理员(登录用户变管理员判定) + // JWT:以**数据库当前状态**为权威来源判定管理员。 + // + // 安全不变量(必须长期保持):管理员授权必须同时满足 + // 1. 令牌签名有效且未过期(HS256,alg 已 pin); + // 2. 令牌未被注销(jti 不在吊销名单); + // 3. 用户在库中存在且未被禁用; + // 4. 用户的**当前** role 仍是管理员。 + // + // 历史缺陷:只读取 JWT 里的 `payload.role`,且完全不检查 jti。后果是 + // 「退出登录」和「把管理员降权为普通用户」都不会立即生效 —— 旧令牌在 7 天 + // 有效期内继续拥有完整后台权限(实测:注销后 /api/admin/* 仍返回 200)。 + // 注意不能反过来只信 JWT:那样降权/禁用同样无效。 try { const { verify } = await import("hono/jwt") - const { getJwtSecret } = await import("../server/middlewares") + const { getJwtSecret, isTokenRevoked } = await import( + "../server/middlewares" + ) const secret = await getJwtSecret(c) const payload: any = await verify(token, secret, "HS256") - if (payload && payload.role === 2) { - // 确认该用户存在于 DB 且未被禁用 - const db = await getDb(c.env) - const user = (db.users || []).find( - (u: any) => u.id === payload.id || u.username === payload.username, - ) - return !!(user && !user.disabled) - } + if (!payload) return false + + // 注销黑名单:与 getUserFromContext 使用同一套判定 + if (await isTokenRevoked(payload.jti, c.env)) return false + + // DB 为权威:存在 + 未禁用 + 当前角色仍为管理员 + const db = await getDb(c.env) + const user = (db.users || []).find( + (u: any) => u.id === payload.id || u.username === payload.username, + ) + if (!user || user.disabled) return false + // 密码指纹:改密后旧令牌立即失效(对齐 Go middlewares.Auth 的 PwdTS 比对) + const { isPasswordStampValid } = await import("./password") + if (!isPasswordStampValid(payload, user)) return false + return user.role === 2 } catch {} return false } diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 0b107d37..2fb8dcf9 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -4619,12 +4619,21 @@ adminRouter.post("/setting/set_thunderx", async (c) => { adminRouter.post("/setting/reset_token", async (c) => { const newToken = generateSecureToken(32) await updateSettingValue(c.env, { token: newToken }) - // `token` 设置是链接/下载签名密钥(对应 Go 的 sign.Instance()), - // 变更后旧的下载链接签名立即失效——与 Go ResetToken 行为一致。 + // 本接口轮换的是 `token` 设置,即**静态 API Token** + // (pkg/utils.isStaticApiToken 用它比对 `Authorization: Bearer `)。 // - // 注意:JWT 密钥来自 env.JWT_SECRET / 持久化的 openlist_jwt_secret,与这里的 - // `token` 设置无关,所以清 JWT 缓存**不会**让已签发的 JWT 失效;这里调用只是 - // 让进程内缓存与最新的 env/持久化值保持一致。 + // 关于下载链接(重要,勿再写成「旧链接立即失效」): + // 下载签名由 pkg/sign 用 getJwtSecret() 计算,密钥来源是 + // env.JWT_SECRET 或持久化的 openlist_jwt_secret,**与 `token` 设置无关**。 + // 因此轮换本设置**不会**让已签发的下载链接失效。 + // 历史注释声称「与 Go ResetToken 行为一致、旧链接立即失效」,与实现不符, + // 会让人误以为已经撤销了旧链接。 + // + // 若确实要让旧下载链接失效:需要轮换 JWT_SECRET(注意这同时会使所有已登录 + // 会话失效);把它做成独立开关属于后续工作,不能靠本接口悄悄生效。 + // + // 这里的 resetJwtSecretCache() 只是让进程内缓存与最新 env/持久化值对齐, + // 并不会更换密钥,因此不会使已签发的 JWT 失效。 const { resetJwtSecretCache } = await import("./middlewares") resetJwtSecretCache() return c.json({ code: 200, message: "success", data: newToken }) diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index a14447d9..d2753809 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -23,9 +23,10 @@ import { import { staticHash, saltedHash, - generateSalt, setUserPassword, isHex64, + passwordStampOf, + isPasswordStampValid, } from "../pkg/password" import { setCSRFToken, clearCSRFToken } from "../pkg/csrf" import { getAuditLogger } from "../pkg/audit" @@ -147,7 +148,7 @@ async function bumpLoginFailure( } } -async function isLoginLocked( +export async function isLoginLocked( c: Context, username: string, env: any, @@ -206,7 +207,7 @@ async function isLoginLocked( return false } -async function recordLoginFailure( +export async function recordLoginFailure( c: Context, username: string, env: any, @@ -219,7 +220,7 @@ async function recordLoginFailure( ) } -async function clearLoginFailures( +export async function clearLoginFailures( c: Context, username: string, env: any, @@ -257,10 +258,12 @@ export async function hashPasswordSHA256( /** * 校验前端提交的静态哈希是否匹配该用户(等价 Go User.ValidatePwdStaticHash)。 - * 兼容的存储格式: - * - Go 双层(user.salt 存在):password === saltedHash(inputStatic, salt) - * - 早期 TSWorker 单层(无 salt):password === inputStatic - * - bcrypt 遗留:返回 false(无明文不可还原),仅明文 /login 可逃生 + * + * 只接受 Go 双层格式:password === saltedHash(inputStatic, user.salt)。 + * + * 历史遗留格式(bcrypt / 无 salt 的单层 SHA256)不再被支持,也不再在登录成功 + * 后自动升级为双层格式 —— 与 Go 行为完全一致:没有 salt 的存储值一律不可验证。 + * 遗留账号请通过 ADMIN_PASS 显式重置,或由管理员在后台改密。 */ export async function verifyUserStaticHash( user: any, @@ -272,12 +275,9 @@ export async function verifyUserStaticHash( const input = String(inputStatic || "") .trim() .toLowerCase() - if (!stored || !isHex64(input) || !isHex64(stored)) return false - if (user?.salt) { - const expect = (await saltedHash(input, String(user.salt))).toLowerCase() - return stored === expect - } - return stored === input + if (!stored || !user?.salt || !isHex64(input) || !isHex64(stored)) return false + const expect = (await saltedHash(input, String(user.salt))).toLowerCase() + return stored === expect } /** @@ -428,7 +428,16 @@ export async function authUserFromReq( const user = db.users.find( (u: any) => u.id === payload.id || u.username === payload.username, ) - if (!user) return null + // 禁用/已删除用户一律视为未认证。 + // + // 安全不变量:身份解析必须复检「当前是否仍被允许」。JWT 只证明「签发时刻 + // 身份合法」,不证明「现在仍然合法」。历史上这里缺少 disabled 判断,导致 + // 管理员禁用某用户后,其既有令牌在 S3 网关等直接复用本函数的入口仍然可用 + // (核心 API 走 getUserFromContext 有该检查,网关入口没有)。 + if (!user || user.disabled) return null + // 密码指纹:改密后旧令牌立即失效(对齐 Go middlewares.Auth 的 PwdTS 比对)。 + // 历史缺陷:JWT 只携带 {id, username, role},改密不使既有令牌失效。 + if (!isPasswordStampValid(payload, user)) return null return { db, user } } catch { return null @@ -479,29 +488,26 @@ authRouter.post("/login", async (c) => { ) } - const { users, db } = await getOrInitUsers(c.env) + const { users } = await getOrInitUsers(c.env) const matchedUser = users.find( (u: any) => u.username === username && !u.disabled, ) if (matchedUser) { - // 明文登录:服务端先 StaticHash,再比对存储哈希(兼容单层/双层/bcrypt 遗留) + // 明文登录:服务端先 StaticHash,再比对存储哈希(仅 Go 双层格式) const isPasswordValid = await verifyUserPassword(matchedUser, rawPassword) if (isPasswordValid) { - // 遗留格式(bcrypt / 无盐单层 SHA256)登录成功后自动升级为 Go 双层哈希 - const stored = String(matchedUser.password || "").trim() - const needsMigrate = - /^\$2[aby]\$/.test(stored) || (!matchedUser.salt && isHex64(stored)) - if (needsMigrate) { - console.log(`[Auth] Upgrading password hash for user: ${username}`) - await setUserPassword(matchedUser, rawPassword) - await saveDb(db, c.env) - } - const otpCheck = await checkUserOtp(matchedUser, body) if (!otpCheck.ok) { + // OTP 失败必须计入防爆破计数。 + // + // 安全不变量:任何「口令学验证失败」都应当累加失败次数。历史缺陷是 + // OTP 分支直接 return,不调用 recordLoginFailure —— 攻击者只要先拿到 + // 正确密码,就能以「无限次猜测」的方式爆破 6 位 TOTP,而账号永远不会 + // 被锁定(实测:连续 7 次错误 OTP 后仍可继续提交)。 + await recordLoginFailure(c, username, c.env) return c.json( { code: otpCheck.code, message: otpCheck.message, data: null }, otpCheck.httpStatus, @@ -514,6 +520,8 @@ authRouter.post("/login", async (c) => { id: matchedUser.id, username: matchedUser.username, role: matchedUser.role, + // 密码指纹(对齐 Go common.UserClaims.PwdTS):改密后旧令牌立即失效 + pwd_ts: passwordStampOf(matchedUser), exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, jti: generateJti(), } @@ -564,7 +572,7 @@ authRouter.post("/login/hash", async (c) => { ) } - const { users, db } = await getOrInitUsers(c.env) + const { users } = await getOrInitUsers(c.env) const matchedUser = users.find( (u: any) => u.username === username && !u.disabled, @@ -574,17 +582,15 @@ authRouter.post("/login/hash", async (c) => { const isHashValid = await verifyUserStaticHash(matchedUser, inputHash) if (isHashValid) { - // 无盐单层 SHA256 旧数据 -> 升级为 Go 双层哈希(无需明文) - if (!matchedUser.salt) { - console.log(`[Auth] Upgrading password hash for user: ${username}`) - matchedUser.salt = generateSalt() - matchedUser.password = await saltedHash(inputHash, matchedUser.salt) - matchedUser.pwd_update_at = new Date().toISOString() - await saveDb(db, c.env) - } - const otpCheck = await checkUserOtp(matchedUser, body) if (!otpCheck.ok) { + // OTP 失败必须计入防爆破计数。 + // + // 安全不变量:任何「口令学验证失败」都应当累加失败次数。历史缺陷是 + // OTP 分支直接 return,不调用 recordLoginFailure —— 攻击者只要先拿到 + // 正确密码,就能以「无限次猜测」的方式爆破 6 位 TOTP,而账号永远不会 + // 被锁定(实测:连续 7 次错误 OTP 后仍可继续提交)。 + await recordLoginFailure(c, username, c.env) return c.json( { code: otpCheck.code, message: otpCheck.message, data: null }, otpCheck.httpStatus, @@ -597,6 +603,8 @@ authRouter.post("/login/hash", async (c) => { id: matchedUser.id, username: matchedUser.username, role: matchedUser.role, + // 密码指纹(对齐 Go common.UserClaims.PwdTS):改密后旧令牌立即失效 + pwd_ts: passwordStampOf(matchedUser), exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, jti: generateJti(), } diff --git a/src/backend/server/default_credentials.test.ts b/src/backend/server/default_credentials.test.ts index ae7f658f..dccfb0f6 100644 --- a/src/backend/server/default_credentials.test.ts +++ b/src/backend/server/default_credentials.test.ts @@ -95,7 +95,7 @@ test("Security(F-11): ADMIN_PASS still forces an explicit reset (to salted doubl ) }) -test("Security(F-11): user/create without a password gets a random one, not 123456", async () => { +test("user/create follows Go: no silent default password and no echoed password", async () => { await seed( [ adminUser( @@ -116,20 +116,17 @@ test("Security(F-11): user/create without a password gets a random one, not 1234 }) assert.equal(res.status, 200) const json: any = await res.json() - assert.ok( - json.data?.password, - "the generated password must be returned to the admin caller once", - ) - assert.notEqual(json.data.password, "123456") + // 对齐 Go:不再自动生成随机密码,也不会把密码回显给调用方 + assert.equal(json.data, null) const db: any = await getDb(env) const created = db.users.find((u: any) => u.username === "newuser") assert.ok(isHex64(created.password), "stored value must be a 64-hex hash") assert.ok(created.salt, "stored user must carry a per-user salt") assert.equal( - await verifyUserPassword(created, json.data.password), + await verifyUserPassword(created, ""), true, - "the returned random password must actually log the user in", + 'an empty password matches Go\'s SetPassword("") semantics', ) assert.equal( await verifyUserPassword(created, "123456"), diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index c7f52d6c..4ed54fa6 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -14,7 +14,18 @@ import { import { resolveShare } from "../internal/op/share" import { resolvePath } from "../internal/model/db" import { getUserFromContext } from "./middlewares" -import { canWrite, getActualPath, isAdmin } from "../pkg/permission" +import { + can, + canCopy, + canMove, + canRemove, + canRename, + canWrite, + getActualPath, + isAdmin, + isWithinUserRoot, + PermissionBit, +} from "../pkg/permission" import { getNearestMeta, canAccess, @@ -117,32 +128,91 @@ const getStorageRequestContext = (c: any) => { const permissionDenied = (c: any) => c.json({ code: 403, message: "Permission denied", data: null }, 403) -// ---- 上传大小限制(M-5:防止超大请求体被整体读入内存导致 Worker OOM)---- -// 单次整体上传(/put /form)与分片单片(/upload/part)分开设限, -// 均可通过环境变量 MAX_UPLOAD / MAX_UPPART 覆盖。 -const DEFAULT_MAX_UPLOAD = 25 * 1024 * 1024 // 25MB -const DEFAULT_MAX_UPPART = 16 * 1024 * 1024 // 16MB - -function getUploadSizeLimit(c: any, part: boolean): number { - const env = c.env || {} - const key = part ? "MAX_UPPART" : "MAX_UPLOAD" - const raw = - env[key] || (typeof process !== "undefined" ? process.env?.[key] : "") - if (raw) { - const n = parseInt(String(raw), 10) - if (Number.isFinite(n) && n > 0) return n - } - return part ? DEFAULT_MAX_UPPART : DEFAULT_MAX_UPLOAD +/** + * 目录级写权限(meta.write_users)的**执行点**。 + * + * 安全不变量:`meta.write_users` 是「该目录只允许这些用户写入」的白名单, + * 它必须在**真正写入时**校验,而非只在 `/fs/list` 的响应里回填 `write` 字段。 + * + * 历史缺陷:该白名单只影响界面提示(列表返回 write=false),写接口从不复查, + * 因此「界面显示禁止写入」与「实际能写入」不一致——用户看到只读提示,但上传 + * 依然成功(实测:write_users=[其他用户] 时 /fs/put 仍返回 200 并创建文件)。 + * + * 返回 true 表示应当拒绝。管理员放行(与 pkg/meta 的约定一致)。 + */ +async function deniedByMetaWriteAcl( + c: any, + user: any, + targetPaths: string[], +): Promise { + if (isAdmin(user)) return false + for (const p of targetPaths) { + let meta: Meta | null = null + try { + meta = await getNearestMeta(p, c.env) + } catch { + meta = null + } + if (!meta) continue + if (!canWriteMeta(user, meta, p)) return true + } + return false } -/** 返回超限时的上限字节数;未超限或无法判断(无 Content-Length)返回 null */ -function exceedsUploadLimit(c: any, part = false): number | null { - const contentLength = parseInt(c.req.header("Content-Length") || "0", 10) - if (!contentLength) return null - const max = getUploadSizeLimit(c, part) - return contentLength > max ? max : null +/** + * 归档读取的 meta 访问校验:与 `/fs/list`、`/fs/get` 共用同一套判定 + * (密码保护 + read_users 白名单)。 + * + * 安全不变量:归档接口读的是**同一份文件内容**,必须与直连下载受同样的 + * 访问控制。历史缺陷:归档列表直接 `driver.get() → fetch()`,完全跳过 meta + * 判定——实测「直连被密码保护而 401」的文件,归档列表却能读出其条目。 + */ +async function deniedByMetaReadAcl( + c: any, + user: any, + targetPath: string, + password: string, +): Promise { + if (isAdmin(user)) return false + let meta: Meta | null = null + try { + meta = await getNearestMeta(targetPath, c.env) + } catch { + meta = null + } + if (!meta) return false + return !canAccess(user, meta, targetPath, password || "") +} + +/** + * 归档条目相对路径的安全化。 + * + * 安全不变量:ZIP 条目名是**不可信输入**,只能被当作「只能向下展开的相对路径」。 + * 历史缺陷:条目名只做了反斜杠转换,`../bob/x` 与目标目录拼接后会被 + * resolvePath 归一化,从而把文件写到解压目录(乃至用户根)之外。 + * + * 返回 null 表示该条目应被跳过(绝对路径 / 盘符 / 含 `..` / 空)。 + */ +function safeArchiveEntryPath(rel: string): string | null { + const cleaned = String(rel || "") + .replace(/\\/g, "/") + .replace(/^\/+/, "") + if (!cleaned || cleaned.includes("\0")) return null + if (/^[A-Za-z]:/.test(cleaned)) return null + const parts = cleaned.split("/").filter(Boolean) + if (parts.length === 0) return null + if (parts.some((p) => p === "." || p === "..")) return null + return parts.join("/") } +// 上传大小限制统一实现在 pkg/upload-limit.ts(S3 网关复用同一份, +// 避免出现「/fs/put 有上限、S3 PutObject 无上限」的不对称)。 +import { + getUploadSizeLimit, + readBodyWithinLimit, + tooLargeResponse, +} from "../pkg/upload-limit" + // 分享请求错误统一出口: // - 密码错误 → 403,前端据此弹出提取码输入框(State.NeedPassword) // - 其他(不存在/禁用/过期/超次数/为空)→ 400,前端显示友好提示 @@ -169,8 +239,12 @@ fsRouter.post("/dirs", async (c) => { return c.json({ code: 401, message: "Unauthorized", data: null }, 401) } const requestContext = getStorageRequestContext(c) + // 对齐 Go handles.FsDirs:force_root(以全局根为起点)只对管理员开放 + if (body.force_root && !isAdmin(user)) { + return c.json({ code: 403, message: "Permission denied", data: null }, 403) + } let reqPath = rawPath - if (!body.force_root || !isAdmin(user)) { + if (!body.force_root) { reqPath = getActualPath(user, reqPath) } try { @@ -208,6 +282,20 @@ fsRouter.post("/dirs", async (c) => { return c.json({ code: 200, message: "success", data: dirs }) } + // 对齐 Go handles.FsDirs:目录树接口必须与 /fs/list 受同一套 meta 判定 + // (密码保护 + read_users 白名单)。历史缺陷:只列目录不做任何校验, + // 任何登录用户都能直接枚举出受保护目录的结构。 + const dirMeta = await getNearestMeta(reqPath, c.env) + if (!canAccess(user, dirMeta, reqPath, body.password || "")) { + return c.json( + { + code: 403, + message: "password is incorrect or you have no permission", + data: null, + }, + 403, + ) + } const { content } = await listItems(reqPath, requestContext) const dirs = content .filter((item: any) => item.is_dir) @@ -703,6 +791,7 @@ fsRouter.post("/mkdir", async (c) => { return c.json({ code: 400, message: e.message, data: null }, 400) } const reqPath = getActualPath(user, rawPath) + if (await deniedByMetaWriteAcl(c, user, [reqPath])) return permissionDenied(c) const requestContext = getStorageRequestContext(c) try { await makeDirectory(reqPath, requestContext) @@ -714,7 +803,9 @@ fsRouter.post("/mkdir", async (c) => { fsRouter.post("/rename", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + // 重命名使用独立的 RENAME 权限位(此前误用 WRITE_CONTENT,导致「只有上传 + // 权限的用户也能改名」而「只授予 RENAME 的用户反被拒绝」)。 + if (!canRename(user)) return permissionDenied(c) const { path: oldPath, name: newName } = await c.req.json().catch(() => ({})) let cleanName = "" try { @@ -726,6 +817,8 @@ fsRouter.post("/rename", async (c) => { const requestContext = getStorageRequestContext(c) try { const actualOldPath = getActualPath(user, oldPath || "/") + if (await deniedByMetaWriteAcl(c, user, [actualOldPath])) + return permissionDenied(c) await renameItem(actualOldPath, cleanName, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { @@ -735,7 +828,8 @@ fsRouter.post("/rename", async (c) => { fsRouter.post("/remove", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + // 删除使用独立的 DELETE 权限位(此前误用 WRITE_CONTENT)。 + if (!canRemove(user)) return permissionDenied(c) const { dir, names } = await c.req.json().catch(() => ({})) if (!Array.isArray(names) || names.length === 0) { return c.json( @@ -757,6 +851,11 @@ fsRouter.post("/remove", async (c) => { const requestContext = getStorageRequestContext(c) try { const actualDir = getActualPath(user, dir || "/") + const aclPaths = cleanNames.map((n) => + `${actualDir}/${n}`.replace(/\/{2,}/g, "/"), + ) + if (await deniedByMetaWriteAcl(c, user, aclPaths)) + return permissionDenied(c) await removeItems(actualDir, cleanNames, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { @@ -766,7 +865,8 @@ fsRouter.post("/remove", async (c) => { fsRouter.post("/move", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + // 移动使用独立的 MOVE 权限位(此前误用 WRITE_CONTENT)。 + if (!canMove(user)) return permissionDenied(c) const { src_dir, dst_dir, names } = await c.req.json().catch(() => ({})) if (!Array.isArray(names) || names.length === 0) { return c.json( @@ -790,6 +890,14 @@ fsRouter.post("/move", async (c) => { try { const actualSrcDir = getActualPath(user, src_dir || "/") const actualDstDir = getActualPath(user, dst_dir || "/") + const aclPaths = [ + actualDstDir, + ...cleanNames.map((n) => + `${actualSrcDir}/${n}`.replace(/\/{2,}/g, "/"), + ), + ] + if (await deniedByMetaWriteAcl(c, user, aclPaths)) + return permissionDenied(c) await moveItems(actualSrcDir, actualDstDir, cleanNames, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { @@ -799,7 +907,8 @@ fsRouter.post("/move", async (c) => { fsRouter.post("/copy", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + // 复制使用独立的 COPY 权限位(此前误用 WRITE_CONTENT)。 + if (!canCopy(user)) return permissionDenied(c) const { src_dir, dst_dir, names } = await c.req.json().catch(() => ({})) if (!Array.isArray(names) || names.length === 0) { return c.json( @@ -823,6 +932,14 @@ fsRouter.post("/copy", async (c) => { try { const actualSrcDir = getActualPath(user, src_dir || "/") const actualDstDir = getActualPath(user, dst_dir || "/") + const aclPaths = [ + actualDstDir, + ...cleanNames.map((n) => + `${actualDstDir}/${n}`.replace(/\/{2,}/g, "/"), + ), + ] + if (await deniedByMetaWriteAcl(c, user, aclPaths)) + return permissionDenied(c) await copyItems(actualSrcDir, actualDstDir, cleanNames, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { @@ -846,21 +963,12 @@ fsRouter.put("/put", async (c) => { return c.json({ code: 400, message: e.message, data: null }, 400) } const reqPath = getActualPath(user, rawPath) + if (await deniedByMetaWriteAcl(c, user, [reqPath])) return permissionDenied(c) const requestContext = getStorageRequestContext(c) - const tooLarge = exceedsUploadLimit(c) - if (tooLarge !== null) { - return c.json( - { - code: 413, - message: `File too large (max ${tooLarge} bytes)`, - data: null, - }, - 413, - ) - } try { - const buffer = await c.req.arrayBuffer() - await putItem(reqPath, Buffer.from(buffer), requestContext) + const body = await readBodyWithinLimit(c) + if (!body.ok) return tooLargeResponse(c, body.limit) + await putItem(reqPath, body.buffer, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { return c.json({ code: 500, message: safeErrorMessage(e), data: null }) @@ -883,18 +991,8 @@ fsRouter.put("/form", async (c) => { return c.json({ code: 400, message: e.message, data: null }, 400) } const reqPath = getActualPath(user, rawPath) + if (await deniedByMetaWriteAcl(c, user, [reqPath])) return permissionDenied(c) const requestContext = getStorageRequestContext(c) - const tooLarge = exceedsUploadLimit(c) - if (tooLarge !== null) { - return c.json( - { - code: 413, - message: `File too large (max ${tooLarge} bytes)`, - data: null, - }, - 413, - ) - } try { const form = await c.req.formData() const file = form.get("file") @@ -906,6 +1004,9 @@ fsRouter.put("/form", async (c) => { }) } const buffer = Buffer.from(await (file as File).arrayBuffer()) + // multipart 的 Content-Length 只覆盖整个表单,仍需按文件实际字节复核 + const max = getUploadSizeLimit(c, false) + if (buffer.length > max) return tooLargeResponse(c, max) await putItem(reqPath, buffer, requestContext) return c.json({ code: 200, message: "success", data: null }) } catch (e: any) { @@ -913,161 +1014,8 @@ fsRouter.put("/form", async (c) => { } }) -// ---- 分片会话上传:解决大文件整体缓冲导致的卡死/OOM ---- -// 流程:POST /fs/upload/create 建会话 → PUT /fs/upload/part 逐片上传 -// → POST /fs/upload/complete 收尾。每片是独立 HTTP 请求,Worker -// 内存占用恒定,不受 CF Workers 请求体/内存上限约束。 - -fsRouter.post("/upload/create", async (c) => { - const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) - const { - path: rawPath, - file_name, - size, - md5, - } = await c.req.json().catch(() => ({})) - // 根目录上传时调用方可能传 "",归一化为 "/" - const dirPath = getActualPath(user, rawPath || "/") - const requestContext = getStorageRequestContext(c) - if (!file_name) { - return c.json({ - code: 400, - message: "path and file_name are required", - data: null, - }) - } - try { - const resolved = await resolvePath(dirPath) - if (resolved.isVirtual) { - throw new Error("failed get storage: storage not found") - } - const driver = await getDriver(resolved.storage!.driver, resolved.storage) - if (typeof (driver as any).createUploadSession !== "function") { - // 当前存储不支持分片会话上传:返回 null,前端自动回退到流式上传 - return c.json({ code: 200, message: "success", data: null }) - } - let info - try { - info = await (driver as any).createUploadSession( - dirPath, - resolved.physical!, - file_name, - Number(size) || 0, - md5 || "", - ) - } finally { - await flushPendingDriverState( - resolved.storage!.driver, - resolved.storage, - driver, - requestContext, - ) - } - return c.json({ code: 200, message: "success", data: info }) - } catch (e: any) { - return c.json({ code: 500, message: safeErrorMessage(e), data: null }) - } -}) - -fsRouter.put("/upload/part", async (c) => { - const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) - const session = c.req.header("X-Upload-Session") || "" - const partNumber = parseInt(c.req.header("X-Part-Number") || "0", 10) - const rawDirPath = decodeURIComponent(c.req.header("Upload-Path") || "") - const dirPath = getActualPath(user, rawDirPath) - const requestContext = getStorageRequestContext(c) - if (!session || !(partNumber >= 1) || !dirPath) { - return c.json({ - code: 400, - message: "missing X-Upload-Session / X-Part-Number / Upload-Path", - data: null, - }) - } - const tooLarge = exceedsUploadLimit(c, true) - if (tooLarge !== null) { - return c.json( - { - code: 413, - message: `Part too large (max ${tooLarge} bytes)`, - data: null, - }, - 413, - ) - } - try { - const resolved = await resolvePath(dirPath) - if (resolved.isVirtual) { - throw new Error("failed get storage: storage not found") - } - const driver = await getDriver(resolved.storage!.driver, resolved.storage) - if (typeof (driver as any).uploadPart !== "function") { - throw new Error("storage does not support chunked upload") - } - const buffer = Buffer.from(await c.req.arrayBuffer()) - let result - try { - result = await (driver as any).uploadPart(session, partNumber, buffer) - } finally { - await flushPendingDriverState( - resolved.storage!.driver, - resolved.storage, - driver, - requestContext, - ) - } - return c.json({ code: 200, message: "success", data: result ?? null }) - } catch (e: any) { - return c.json({ code: 500, message: safeErrorMessage(e), data: null }) - } -}) - -fsRouter.post("/upload/complete", async (c) => { - const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) - const { - path: rawPath, - session, - partMd5s, - } = await c.req.json().catch(() => ({})) - // 根目录上传时调用方可能传 "",归一化为 "/" - const dirPath = getActualPath(user, rawPath || "/") - const requestContext = getStorageRequestContext(c) - if (!session) { - return c.json({ - code: 400, - message: "path and session are required", - data: null, - }) - } - try { - const resolved = await resolvePath(dirPath) - if (resolved.isVirtual) { - throw new Error("failed get storage: storage not found") - } - const driver = await getDriver(resolved.storage!.driver, resolved.storage) - if (typeof (driver as any).completeUploadSession !== "function") { - throw new Error("storage does not support chunked upload") - } - try { - await (driver as any).completeUploadSession(session, partMd5s) - } finally { - await flushPendingDriverState( - resolved.storage!.driver, - resolved.storage, - driver, - requestContext, - ) - } - // 直传收尾:目录列表已变化,失效该目录的文件树 / 链接缓存, - // 否则「上传成功但刷新看不到」在缓存 TTL 内稳定复现 - await invalidatePaths([dirPath], c.env) - return c.json({ code: 200, message: "success", data: null }) - } catch (e: any) { - return c.json({ code: 500, message: safeErrorMessage(e), data: null }) - } -}) +// 说明:TS 专有的 /fs/upload/{create,part,complete} 会话式上传端点已移除, +// 分片上传对齐 Go 只保留官方的 /fs/multipart/* 系列。 fsRouter.post("/add_offline_download", async (c) => { const user = await getUserFromContext(c) @@ -1106,6 +1054,10 @@ fsRouter.post("/search", async (c) => { scope: body.scope !== undefined ? parseInt(body.scope, 10) : 0, page: body.page ? parseInt(body.page, 10) : 1, per_page: body.per_page ? parseInt(body.per_page, 10) : 30, + // 与 /fs/list 同一套 meta 判定(密码 + read_users), + // 对齐 Go handles.Search 里 SearchFiltered 的 filter 闭包 + user, + password: body.password || "", }, c.env, ) @@ -1133,6 +1085,9 @@ fsRouter.post("/other", async (c) => { // to mint presigned direct-upload URLs (s3/driver.ts:371-378), which is a // write — it must obey the same gate as /fs/put. Without this, a guest could // obtain a direct-upload URL and bypass the upload permission check. + // + // 体积边界说明:这里签发的直传 URL 由客户端直接写对象存储,数据不经过本站, + // 因此 MAX_UPLOAD / MAX_UPPART 覆盖不到该路径——上限由下游存储自行约束。 if (!canWrite(user)) return permissionDenied(c) try { const resolved = await resolvePath(reqPath) @@ -1169,7 +1124,7 @@ fsRouter.post("/other", async (c) => { fsRouter.post("/batch_rename", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + if (!canRename(user)) return permissionDenied(c) const { src_dir, rename_objects } = await c.req.json().catch(() => ({})) if (!Array.isArray(rename_objects) || rename_objects.length === 0) { return c.json( @@ -1180,6 +1135,12 @@ fsRouter.post("/batch_rename", async (c) => { const dirPath = getActualPath(user, src_dir || "/") const requestContext = getStorageRequestContext(c) try { + const aclPaths = rename_objects + .map((obj: any) => obj?.src_name) + .filter((n: any) => typeof n === "string" && n) + .map((n: string) => `${dirPath}/${n}`.replace(/\/{2,}/g, "/")) + if (await deniedByMetaWriteAcl(c, user, aclPaths)) + return permissionDenied(c) for (const obj of rename_objects) { const srcName = obj?.src_name const newName = obj?.new_name @@ -1196,7 +1157,7 @@ fsRouter.post("/batch_rename", async (c) => { fsRouter.post("/regex_rename", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + if (!canRename(user)) return permissionDenied(c) const { src_dir, src_name_regex, new_name_regex } = await c.req .json() .catch(() => ({})) @@ -1216,6 +1177,8 @@ fsRouter.post("/regex_rename", async (c) => { const newName = item.name.replace(srcRegex, new_name_regex || "") validateFileName(newName) const fullPath = `${dirPath}/${item.name}`.replace(/\/{2,}/g, "/") + if (await deniedByMetaWriteAcl(c, user, [fullPath])) + return permissionDenied(c) await renameItem(fullPath, newName, requestContext) } } @@ -1229,7 +1192,7 @@ fsRouter.post("/regex_rename", async (c) => { fsRouter.post("/recursive_move", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + if (!canMove(user)) return permissionDenied(c) const { src_dir, dst_dir, conflict_policy } = await c.req .json() .catch(() => ({})) @@ -1267,6 +1230,13 @@ fsRouter.post("/recursive_move", async (c) => { if (policy === "skip") continue } if (policy !== "overwrite") existing.add(item.name) + if ( + await deniedByMetaWriteAcl(c, user, [ + dstPath, + `${dir}/${item.name}`.replace(/\/{2,}/g, "/"), + ]) + ) + return permissionDenied(c) await moveItems(dir, dstPath, [item.name], requestContext) count++ } @@ -1286,9 +1256,10 @@ fsRouter.post("/recursive_move", async (c) => { fsRouter.post("/remove_empty_directory", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + if (!canRemove(user)) return permissionDenied(c) const { src_dir } = await c.req.json().catch(() => ({})) const srcPath = getActualPath(user, src_dir || "/") + if (await deniedByMetaWriteAcl(c, user, [srcPath])) return permissionDenied(c) const requestContext = getStorageRequestContext(c) try { const removeEmptyDirs = async (dir: string): Promise => { @@ -1373,9 +1344,54 @@ fsRouter.post("/get_direct_upload_info", async (c) => { const user = await getUserFromContext(c) if (!canWrite(user)) return permissionDenied(c) const { path, file_name, file_size } = await c.req.json().catch(() => ({})) - const reqPath = getActualPath(user, path || "/") + // 对齐 Go FsGetDirectUploadInfo 的四重校验: + // 1) file_name 必须是单层相对文件名(拒绝 / \ .. 与空值),否则可能签发 + // 指向目标目录之外的直传凭证; + // 2) 目标目录需通过用户权限位,或 meta.write 强制开放(write_content_bypass); + // 3) 目标目录需通过 meta.write_users 白名单; + // 4) 目标不得跨越存储挂载边界(单段文件名也可能命中嵌套挂载点)。 + let cleanName: string try { - const resolved = await resolvePath(reqPath) + cleanName = validateFileName(file_name) + } catch (e: any) { + return c.json({ code: 403, message: safeErrorMessage(e), data: null }, 403) + } + const dirPath = getActualPath(user, path || "/") + const dstPath = dirPath === "/" ? `/${cleanName}` : `${dirPath}/${cleanName}` + const parentMeta = await getNearestMeta(dirPath, c.env) + if (!canWrite(user) && !canWriteContentBypassUserPerms(parentMeta, dirPath)) { + return permissionDenied(c) + } + if (await deniedByMetaWriteAcl(c, user, [dirPath])) return permissionDenied(c) + // 挂载边界:目标文件与所在目录必须落在同一个存储挂载点内 + const normMount = (s: any) => + "/" + String(s?.mount_path || "").split("/").filter(Boolean).join("/") + try { + const [parentResolved, targetResolved] = await Promise.all([ + resolvePath(dirPath), + resolvePath(dstPath), + ]) + if ( + normMount(parentResolved.storage) !== normMount(targetResolved.storage) + ) { + return permissionDenied(c) + } + } catch { + return permissionDenied(c) + } + // Overwrite:false 时目标已存在则拒绝签发(对齐 Go) + if (c.req.header("Overwrite") === "false") { + try { + const { item } = await getItem(dstPath, getStorageRequestContext(c)) + if (item) { + return c.json({ code: 403, message: "file exists", data: null }, 403) + } + } catch { + // 目标不存在 / 无法读取:继续签发流程 + } + } + try { + const resolved = await resolvePath(dirPath) if (resolved.isVirtual || !resolved.storage) { return c.json({ code: 200, message: "success", data: null }) } @@ -1385,20 +1401,12 @@ fsRouter.post("/get_direct_upload_info", async (c) => { if (typeof d.getDirectUploadInfo === "function") { const info = await d.getDirectUploadInfo( resolved.relative, - file_name, + cleanName, file_size, ) // 直传 URL 签发后客户端会向该路径写入新文件:预失效目录与目标路径 - await invalidatePaths( - [ - reqPath, - `${reqPath === "/" ? "" : reqPath}/${file_name || ""}`.replace( - /\/$/, - "", - ), - ], - c.env, - ) + // (本分支已把校验后的目录/文件名归一化为 dirPath / dstPath) + await invalidatePaths([dirPath, dstPath], c.env) return c.json({ code: 200, message: "success", data: info }) } // 回退到 other("direct_upload") / other("get_direct_upload_info") @@ -1406,21 +1414,12 @@ fsRouter.post("/get_direct_upload_info", async (c) => { for (const m of ["direct_upload", "get_direct_upload_info"]) { try { const info = await d.other(m, resolved.relative, { - file_name, + file_name: cleanName, file_size, }) if (info) { // 同上:直传 URL 签发后预失效目录与目标路径 - await invalidatePaths( - [ - reqPath, - `${reqPath === "/" ? "" : reqPath}/${file_name || ""}`.replace( - /\/$/, - "", - ), - ], - c.env, - ) + await invalidatePaths([dirPath, dstPath], c.env) return c.json({ code: 200, message: "success", data: info }) } } catch { @@ -1446,9 +1445,20 @@ function splitUploadPath(uploadPath: string): { dir: string; name: string } { return { dir, name } } +/** + * 分片会话的所有者校验。 + * + * 安全不变量:分片会话(含 driver_session token、已收分片与目标路径)是 + * **用户私有状态**,只有创建者可续传/完成/查询。非本人会话一律按「不存在」 + * 处理,既不泄露其存在,也不允许跨用户续传。 + */ +function sessionOwnedBy(session: any, user: any): boolean { + return Boolean(session) && session.owner_id !== undefined && session.owner_id === user?.id +} + fsRouter.post("/multipart/init", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return permissionDenied(c) + if (!user || !canWrite(user)) return permissionDenied(c) const rawPath = decodeURIComponent(c.req.header("File-Path") || "") const size = parseInt(c.req.header("X-File-Size") || "0", 10) @@ -1464,6 +1474,8 @@ fsRouter.post("/multipart/init", async (c) => { const { dir, name } = splitUploadPath(rawPath) const actualDir = getActualPath(user, dir) + if (await deniedByMetaWriteAcl(c, user, [actualDir, `${actualDir}/${name}`.replace(/\/{2,}/g, "/")])) + return permissionDenied(c) const requestContext = getStorageRequestContext(c) try { @@ -1480,10 +1492,10 @@ fsRouter.post("/multipart/init", async (c) => { const chunkSize = clampChunkSize(rawChunk) const totalChunks = Math.max(1, Math.ceil(size / chunkSize)) - // 断点续传:同 path+size 的未完成会话直接复用 + // 断点续传:同 owner + path+size 的未完成会话直接复用(按用户隔离) let session: MultipartSession let resumed = false - const existing = findReceivingSession(rawPath, size) + const existing = findReceivingSession(rawPath, size, user.id) if (existing) { session = existing resumed = true @@ -1507,6 +1519,8 @@ fsRouter.post("/multipart/init", async (c) => { driver_session: info?.session || "", partMd5s: new Array(totalChunks).fill(undefined), storage_driver: resolved.storage!.driver, + owner_id: user.id, + owner_root: getActualPath(user, "/"), created_at: Date.now(), } // 秒传:驱动返回 reuse 标记 @@ -1542,7 +1556,8 @@ fsRouter.put("/multipart/chunk", async (c) => { const chunkIndex = parseInt(c.req.header("X-Chunk-Index") || "-1", 10) const session = uploadId ? getSession(uploadId) : undefined - if (!session) { + // 会话所有者校验:非本人会话按「不存在」处理(不泄露会话是否存在) + if (!session || !sessionOwnedBy(session, user)) { return c.json( { code: 404, message: "upload session not found", data: null }, 404, @@ -1557,14 +1572,6 @@ fsRouter.put("/multipart/chunk", async (c) => { return c.json({ code: 200, message: "success", data: mpSnapshot(session) }) } - const tooLarge = exceedsUploadLimit(c, true) - if (tooLarge !== null) { - return c.json( - { code: 413, message: `Part too large (max ${tooLarge} bytes)`, data: null }, - 413, - ) - } - const requestContext = getStorageRequestContext(c) try { const resolved = await resolvePath( @@ -1575,7 +1582,12 @@ fsRouter.put("/multipart/chunk", async (c) => { if (typeof (driver as any).uploadPart !== "function") { throw new Error("storage does not support chunked upload") } - const buffer = Buffer.from(await c.req.arrayBuffer()) + // 单片上限必须按**实际收到的字节数**判定:Content-Length 可被省略或伪造。 + // 历史缺陷:此处只做 Content-Length 预检,省略该头即可绕过单片上限并把 + // 整片内容读入内存。 + const body = await readBodyWithinLimit(c, true) + if (!body.ok) return tooLargeResponse(c, body.limit, "Part") + const buffer = body.buffer let result try { result = await (driver as any).uploadPart( @@ -1606,7 +1618,7 @@ fsRouter.post("/multipart/complete", async (c) => { const uploadId = c.req.header("X-Upload-Id") || "" const session = uploadId ? getSession(uploadId) : undefined - if (!session) { + if (!session || !sessionOwnedBy(session, user)) { return c.json( { code: 404, message: "upload session not found", data: null }, 404, @@ -1661,9 +1673,14 @@ fsRouter.post("/multipart/complete", async (c) => { }) fsRouter.get("/multipart/status", async (c) => { + // 状态接口返回目标路径、大小与进度,属于用户私有数据:必须鉴权 + 校验所有者 + const user = await getUserFromContext(c) + if (!user || user.disabled) { + return c.json({ code: 401, message: "Unauthorized", data: null }, 401) + } const uploadId = c.req.query("upload_id") || "" const session = uploadId ? getSession(uploadId) : undefined - if (!session) { + if (!session || !sessionOwnedBy(session, user)) { return c.json( { code: 404, message: "upload session not found", data: null }, 404, @@ -1726,9 +1743,15 @@ async function fetchArchiveBytes( // 环境变量 SSRF_ALLOWED_HOSTS 加入白名单 const trustedHosts = getTrustedHosts(resolved.storage?.addition, c.env) assertSafeUrl(item.raw_url, "Archive download", trustedHosts) - const resp = await fetch(item.raw_url, { - headers: item.raw_url_headers || {}, - }) + // 使用与 /d、/p 代理相同的**逐跳**校验:默认 fetch 会自动跟随 3xx,攻击者 + // 可以先让 raw_url 指向通过 isSafeUrl 的公网域名,再 302 跳到内网/云元数据。 + // 历史缺陷:这里只校验初始 URL 后直接 fetch,绕过逐跳防护(raw.ts 已做)。 + const { safeProxyFetch } = await import("./raw") + const resp = await safeProxyFetch( + item.raw_url, + item.raw_url_headers || {}, + trustedHosts, + ) if (!resp.ok) throw new Error(`archive download failed: HTTP ${resp.status}`) return await resp.arrayBuffer() } @@ -1796,6 +1819,8 @@ function calcFileTypeSafe(name: string, isDir: boolean): number { fsRouter.post("/archive/meta", async (c) => { const user = await getUserFromContext(c) if (!user || user.disabled) return c.json({ code: 401, message: "Unauthorized", data: null }, 401) + // 归档读取需要独立能力位:READ_ARCHIVES + if (!can(user, PermissionBit.READ_ARCHIVES)) return permissionDenied(c) const body = await c.req.json().catch(() => ({})) const path = String(body.path || c.req.query("path") || "").trim() if (!path) return c.json({ code: 400, message: "path is required", data: null }, 400) @@ -1811,6 +1836,17 @@ fsRouter.post("/archive/meta", async (c) => { }, 501) } try { + // 与 /fs/get 共用 meta 判定:密码保护的归档不能靠归档接口绕过密码 + if ( + await deniedByMetaReadAcl( + c, + user, + getActualPath(user, path), + String(body.password || ""), + ) + ) { + return permissionDenied(c) + } const bytes = await fetchArchiveBytes(c, user, path) const archive = parseZip(bytes) const tree = buildArchiveTree(archive) @@ -1833,6 +1869,8 @@ fsRouter.post("/archive/meta", async (c) => { fsRouter.post("/archive/list", async (c) => { const user = await getUserFromContext(c) if (!user || user.disabled) return c.json({ code: 401, message: "Unauthorized", data: null }, 401) + // 归档读取需要独立能力位:READ_ARCHIVES + if (!can(user, PermissionBit.READ_ARCHIVES)) return permissionDenied(c) const body = await c.req.json().catch(() => ({})) const path = String(body.path || c.req.query("path") || "").trim() const innerPath = String(body.inner_path || "").trim().replace(/\/+/g, "/") @@ -1849,6 +1887,17 @@ fsRouter.post("/archive/list", async (c) => { }, 501) } try { + // 与 /fs/get 共用 meta 判定:密码保护的归档不能靠归档接口绕过密码 + if ( + await deniedByMetaReadAcl( + c, + user, + getActualPath(user, path), + String(body.password || ""), + ) + ) { + return permissionDenied(c) + } const bytes = await fetchArchiveBytes(c, user, path) const archive = parseZip(bytes) const prefix = innerPath ? innerPath.replace(/^\/+|\/+$/g, "") + "/" : "" @@ -1886,7 +1935,9 @@ fsRouter.post("/archive/list", async (c) => { fsRouter.post("/archive/decompress", async (c) => { const user = await getUserFromContext(c) - if (!canWrite(user)) return c.json({ code: 403, message: "Permission denied", data: null }, 403) + // 解压写文件:需要 WRITE_CONTENT(写目标)+ DECOMPRESS(解压能力) + if (!canWrite(user) || !can(user, PermissionBit.DECOMPRESS)) + return c.json({ code: 403, message: "Permission denied", data: null }, 403) const body = await c.req.json().catch(() => ({})) const srcDir = String(body.src_dir || "").trim() const dstDir = String(body.dst_dir || "").trim() @@ -1897,6 +1948,13 @@ fsRouter.post("/archive/decompress", async (c) => { } try { let count = 0 + // 目标目录必须落在**调用者自己的根目录**之内。 + // 历史缺陷:dst_dir 被当作全局虚拟路径直接拼接,既未套用 base_path, + // 也未做角色范围校验,受限用户可写到任意目录。 + const actualDstDir = getActualPath(user, dstDir) + if (!isWithinUserRoot(user, actualDstDir)) return permissionDenied(c) + if (await deniedByMetaWriteAcl(c, user, [actualDstDir])) + return permissionDenied(c) for (const name of names) { const srcPath = srcDir ? `${srcDir}/${name}` : `/${name}` const fmtDecomp = detectArchiveFormat(name) @@ -1910,7 +1968,21 @@ fsRouter.post("/archive/decompress", async (c) => { if (prefix && !entry.name.startsWith(prefix)) continue const rel = prefix ? entry.name.slice(prefix.length) : entry.name if (!rel || rel.endsWith("/")) continue - const targetPath = `${dstDir.replace(/\/+$/, "")}/${rel}` + // zip-slip 防护:条目名视为「只能向下展开的相对路径」 + const safeRel = safeArchiveEntryPath(rel) + if (!safeRel) { + console.warn( + `[Archive] skipping unsafe entry path: ${entry.name}`, + ) + continue + } + const targetPath = `${actualDstDir.replace(/\/+$/, "")}/${safeRel}` + if (!isWithinUserRoot(user, targetPath)) { + console.warn( + `[Archive] skipping entry escaping user root: ${entry.name}`, + ) + continue + } const content = await extractZipEntry(bytes, entry) await putItem(targetPath, Buffer.from(content), getStorageRequestContext(c)) count++ diff --git a/src/backend/server/middlewares.ts b/src/backend/server/middlewares.ts index 747bea25..0c7d7d45 100644 --- a/src/backend/server/middlewares.ts +++ b/src/backend/server/middlewares.ts @@ -2,6 +2,7 @@ import { Context } from "hono" import { verify } from "hono/jwt" import { checkAdminAuth, isStaticApiToken } from "../pkg/utils" import { getDb, readPersistedSecret, ENCRYPTION_SECRET_KV_KEY } from "../internal/model/db" +import { isPasswordStampValid } from "../pkg/password" // 不再硬编码 JWT 密钥。优先使用环境变量 JWT_SECRET(推荐在生产配置), // 否则从持久化后端读取一个随机密钥(首次生成后复用,重启不失效), @@ -182,42 +183,47 @@ export async function getJwtSecret(c?: Context | any): Promise { return readCachedJwtSecret(env) as string } -// ---- JWT 注销黑名单(尽力而为:进程内 Set + KV 持久化)---- -// 说明:Serverless 多实例下各实例独立缓存,KV 持久化仅在冷启动时加载一次, -// 因此跨实例的「即时」失效不能保证精确,但能在单实例内立即生效,并随新实例 -// 冷启动逐步收敛。exp 过期后条目自动清理,不会无限增长。 +// ---- JWT 注销黑名单(进程内 Set + 统一持久化后端,带 TTL 复核)---- +// +// 存储层:走统一的 getStorageBackend(经 readPersistedSecret/writePersistedSecret), +// 覆盖 D1 / MySQL / DO / KV / Blob,而**不再**只探测 KV binding。 +// 历史缺陷:只调用 getKvBinding,而该探测没有 D1 分支。于是在 D1 部署下注销 +// 名单「读不到也写不进」——同一实例内靠内存 Set 生效,跨实例完全失效, +// 也就是「退出登录只在自己这一台机器上有效」。 +// +// 刷新:带 TTL 周期回读,让其他实例的注销在有限时间内收敛;本实例的注销 +// 仍然是立即生效(先写内存 Set,再落盘)。 +// 精确性边界:Serverless 多实例下不保证「瞬时」全局一致,这是有意的权衡; +// 需要更强保证时应改用短 TTL 的强制撤销或服务端会话表。 const REVOKED_KV_KEY = "openlist_revoked_tokens" +const REVOKED_RELOAD_MS = 30_000 const revokedJtis = new Set() -let revokedLoaded = false +let revokedLoadedAt = 0 +let revokedInflight: Promise | null = null async function ensureRevokedLoaded(env: any): Promise { - if (revokedLoaded) return - revokedLoaded = true - try { - const { getKvBinding } = await import("../internal/model/db") - const kvInfo = await getKvBinding(env) - if (kvInfo.mode === "none" || !kvInfo.binding) return - const { binding, mode } = kvInfo - let val: any = null - if (mode === "blob") { - val = await binding.get(REVOKED_KV_KEY) - } else { - try { - val = await binding.get(REVOKED_KV_KEY, "text") - } catch { - val = await binding.get(REVOKED_KV_KEY) + if (Date.now() - revokedLoadedAt < REVOKED_RELOAD_MS) return + if (revokedInflight) return revokedInflight + revokedInflight = (async () => { + try { + const { readPersistedSecret } = await import("../internal/model/db") + const raw = await readPersistedSecret(env, REVOKED_KV_KEY) + if (raw) { + const arr = JSON.parse(String(raw)) + const now = Math.floor(Date.now() / 1000) + for (const item of arr) { + if (item && item.jti && item.exp > now) revokedJtis.add(item.jti) + } } + } catch { + // 黑名单读取失败时降级为不拦截(不影响登录) + } finally { + // 无论成功失败都推迟下次回读,避免存储故障时被反复打爆 + revokedLoadedAt = Date.now() + revokedInflight = null } - if (val && typeof val.text === "function") val = await val.text() - if (!val) return - const arr = JSON.parse(String(val)) - const now = Math.floor(Date.now() / 1000) - for (const item of arr) { - if (item && item.jti && item.exp > now) revokedJtis.add(item.jti) - } - } catch { - // 黑名单加载失败时降级为不拦截(不影响登录) - } + })() + return revokedInflight } export async function revokeToken( @@ -226,47 +232,36 @@ export async function revokeToken( env: any, ): Promise { if (!jti) return + // 本实例立即生效 revokedJtis.add(jti) try { - const { getKvBinding } = await import("../internal/model/db") - const kvInfo = await getKvBinding(env) - if (kvInfo.mode === "none" || !kvInfo.binding) return - const { binding, mode } = kvInfo + const { readPersistedSecret, writePersistedSecret } = await import( + "../internal/model/db" + ) let arr: Array<{ jti: string; exp: number }> = [] - if (mode === "blob") { - const val = await binding.get(REVOKED_KV_KEY) - if (val) arr = typeof val === "string" ? JSON.parse(val) : val - } else { + const raw = await readPersistedSecret(env, REVOKED_KV_KEY) + if (raw) { try { - const val = await binding.get(REVOKED_KV_KEY, "text") - if (val) arr = JSON.parse(String(val)) + const parsed = JSON.parse(String(raw)) + if (Array.isArray(parsed)) arr = parsed } catch { - const val = await binding.get(REVOKED_KV_KEY) - if (val) arr = typeof val === "string" ? JSON.parse(val) : val + arr = [] } } const now = Math.floor(Date.now() / 1000) - arr = arr.filter((i) => i && i.exp > now) - arr.push({ jti, exp }) - const payload = JSON.stringify(arr) - if (mode === "blob") { - if (typeof binding.set === "function") - await binding.set(REVOKED_KV_KEY, payload) - else if (typeof binding.put === "function") - await binding.put(REVOKED_KV_KEY, payload) - } else { - if (typeof binding.put === "function") - await binding.put(REVOKED_KV_KEY, payload) - else if (typeof binding.set === "function") - await binding.set(REVOKED_KV_KEY, payload) - } + // exp 过期的条目自动淘汰,列表不会无限增长 + arr = arr.filter((i) => i && i.jti && i.exp > now) + if (!arr.some((i) => i.jti === jti)) arr.push({ jti, exp }) + await writePersistedSecret(env, REVOKED_KV_KEY, JSON.stringify(arr)) } catch (e) { - console.warn("[JWT] Failed to persist revoked token to KV:", e) + console.warn("[JWT] Failed to persist revoked token:", e) } } export async function isTokenRevoked(jti: string, env: any): Promise { if (!jti) return false + // 本实例的注销无需等待回读即可生效 + if (revokedJtis.has(jti)) return true await ensureRevokedLoaded(env) return revokedJtis.has(jti) } @@ -536,6 +531,8 @@ export async function getUserFromContext(c: Context): Promise<{ (u: any) => u.id === payload.id || u.username === payload.username, ) if (!user || user.disabled) return null + // 密码指纹:改密后旧令牌立即失效(对齐 Go middlewares.Auth 的 PwdTS 比对) + if (!isPasswordStampValid(payload, user)) return null return { id: user.id, role: user.role, diff --git a/src/backend/server/raw.ts b/src/backend/server/raw.ts index 3fe7796c..a3d4fbbc 100644 --- a/src/backend/server/raw.ts +++ b/src/backend/server/raw.ts @@ -82,7 +82,7 @@ const SAFE_REDIRECT_HEADER_KEYS = new Set([ "referer", ]) -async function safeProxyFetch( +export async function safeProxyFetch( url: string, headers: Record, allowHosts?: ReadonlySet | string[], diff --git a/src/backend/server/s3.ts b/src/backend/server/s3.ts index 9e77bf6f..8fb3b503 100644 --- a/src/backend/server/s3.ts +++ b/src/backend/server/s3.ts @@ -7,15 +7,44 @@ import { removeItems, } from "../internal/op/storage" import { safeErrorMessage } from "../pkg/errs" +import { readBodyWithinLimit } from "../pkg/upload-limit" +import { + canRemove, + canWrite, + getActualPath, + isAdmin, + isWithinUserRoot, +} from "../pkg/permission" +import { + canAccess, + canWrite as canWriteMeta, + canWriteContentBypassUserPerms, + getNearestMeta, +} from "../pkg/meta" +import type { Meta } from "../pkg/meta" /** * S3 网关(简化版,挂载于 /s3/*)。 * * 协议:支持 ListBuckets / GetObject / HeadObject / PutObject / DeleteObject, - * 认证采用 Bearer token(与全局 token 一致);完整 AWS SigV4 签名验证作为 - * 后续增强(Worker 环境 S3 网关性能受限,优先保证 API 契约一致)。 + * 认证采用 Bearer token(JWT,与全局令牌同一签发体系);完整 AWS SigV4 签名 + * 验证作为后续增强。 + * + * 说明:本网关**不识别** settings.token 形式的静态 API Token —— 静态令牌走 + * 管理后台的 admin 鉴权通道,不是「某个用户」,无法映射到 S3 的对象视图。 + * 需要静态令牌访问的自动化场景请使用 /api/fs/* 接口。 * - * URL 结构:/s3/{bucket}/{objectKey},bucket 映射到存储挂载路径。 + * URL 结构:/s3/{bucket}/{objectKey},bucket 是**用户根目录之下**的一级目录。 + * + * 安全不变量(必须长期保持): + * 1. 认证用户必须存在且未被禁用(authUserFromReq 已复检 disabled); + * 2. 所有对象路径都必须落在该用户的 base_path 之内; + * 3. 写操作(PutObject)需要 WRITE_CONTENT 权限位, + * 删除操作(DeleteObject)需要 DELETE 权限位,管理员放行。 + * + * 历史缺陷:只判断「是否登录」,随后直接操作全局虚拟路径 `/${bucket}/${key}`。 + * 后果有两条:① 任何已登录用户(含 permission=0 的只读用户、已禁用用户)都能 + * 上传/删除;② 受限 base_path 的用户可以读写其他用户的目录。均已修复。 */ export const s3Router = new Hono() @@ -47,10 +76,36 @@ function parseS3Path(c: any): { bucket: string; key: string } { return { bucket, key } } -/** bucket 名 → 挂载路径(bucket 即存储挂载点的首段) */ -function bucketToPath(bucket: string): string { - if (!bucket) return "/" - return "/" + bucket +/** + * bucket / key 的语法校验。 + * + * 只做「形状」校验:bucket 必须是单层目录名,key 不得含空段。真正的越界防护 + * 由 isWithinUserRoot() 在拼装完成后统一收口,不依赖这里。 + */ +function isValidS3Bucket(bucket: string): boolean { + if (!bucket) return false + if (bucket === "." || bucket === "..") return false + return !bucket.includes("/") && !bucket.includes("\\") && !bucket.includes("\0") +} + +/** + * 把 S3 的 { bucket, key } 映射为**用户根目录之下**的虚拟路径。 + * + * base_path 为 "/" 的(默认)用户:`bucket/key` -> `/bucket/key`,与历史行为 + * 完全一致(bucket 即存储挂载点首段),不产生兼容性变化。 + * base_path 受限的用户:`bucket/key` -> `/bucket/key`,把 S3 的 + * 「bucket」解释为该用户自己的顶层目录,从而与 /api/fs 的视图保持一致。 + */ +function s3VirtualPath(user: any, bucket: string, key: string): string { + const root = getActualPath(user, "/") + const segments = [root, bucket, key].filter(Boolean).join("/") + const normalized = ("/" + segments).replace(/\/{2,}/g, "/") + return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized +} + +/** 统一的越界拒绝响应(403,避免暴露目标路径是否存在) */ +function outOfScope(c: any) { + return s3Error("AccessDenied", "Object is outside the user's root path", 403) } async function authUser(c: any): Promise { @@ -58,14 +113,63 @@ async function authUser(c: any): Promise { return auth ? auth.user : null } +/** + * 目录级(meta)访问控制——与 /api/fs/* 共用同一套判定。 + * + * 安全不变量:S3 网关不得成为「绕过目录密码 / read_users / write_users」的 + * 第二条通道,否则站内被保护的路径可以经 /s3 直接读写。 + * + * S3 协议没有传递目录密码的标准位置,这里接受 `?pwd=` 查询参数或 + * `x-meta-password` 请求头作为 meta 密码来源。 + */ +const metaPasswordOf = (c: any): string => + c.req.query("pwd") || c.req.header("x-meta-password") || "" + +async function loadMeta(c: any, path: string): Promise { + try { + return await getNearestMeta(path, c.env) + } catch { + return null + } +} + +/** 读路径的 meta 判定:密码保护 + read_users 白名单。返回 true 表示拒绝。 */ +async function metaReadDenied( + c: any, + user: any, + path: string, +): Promise { + const meta = await loadMeta(c, path) + if (!meta) return false + return !canAccess(user, meta, path, metaPasswordOf(c)) +} + +/** + * 写路径的 meta 判定:WRITE_CONTENT 权限位(或 meta.write 强制开放) + * + meta.write_users 白名单。返回 true 表示拒绝。 + */ +async function metaWriteDenied( + c: any, + user: any, + path: string, +): Promise { + const meta = await loadMeta(c, path) + if (!meta) return false + if (isAdmin(user)) return false + if (!canWrite(user) && !canWriteContentBypassUserPerms(meta, path)) { + return true + } + return !canWriteMeta(user, meta, path) +} + const getCtx = (c: any) => { try { const ec = c.executionCtx return ec && typeof ec.waitUntil === "function" - ? { waitUntil: (p: Promise) => ec.waitUntil(p) } - : undefined + ? { waitUntil: (p: Promise) => ec.waitUntil(p), env: c.env } + : { env: c.env } } catch { - return undefined + return { env: c?.env } } } @@ -74,7 +178,8 @@ s3Router.get("/", async (c) => { const user = await authUser(c) if (!user) return s3Error("AccessDenied", "Authentication required", 403) try { - const res = await listItems("/", getCtx(c)) + // 仅列出用户根目录之下的「bucket」——受限 base_path 的用户不应看到整站挂载点 + const res = await listItems(getActualPath(user, "/"), getCtx(c)) const buckets = (res.content || []) .filter((it: any) => it.is_dir) .map( @@ -103,8 +208,14 @@ s3Router.get("/*", async (c) => { const user = await authUser(c) if (!user) return s3Error("AccessDenied", "Authentication required", 403) const { bucket, key } = parseS3Path(c) - if (!bucket || !key) return s3Error("NoSuchKey", "NoSuchKey", 404) - const virtualPath = `/${bucket}/${key}` + if (!isValidS3Bucket(bucket) || !key) { + return s3Error("NoSuchKey", "NoSuchKey", 404) + } + const virtualPath = s3VirtualPath(user, bucket, key) + if (!isWithinUserRoot(user, virtualPath)) return outOfScope(c) + if (await metaReadDenied(c, user, virtualPath)) { + return s3Error("AccessDenied", "Password required for this path", 403) + } try { const { item, rawUrl } = await getItem(virtualPath, getCtx(c)) if (!item) return s3Error("NoSuchKey", "NoSuchKey", 404) @@ -123,9 +234,16 @@ s3Router.on("HEAD", "/*", async (c) => { const user = await authUser(c) if (!user) return s3Error("AccessDenied", "Authentication required", 403) const { bucket, key } = parseS3Path(c) - if (!bucket || !key) return s3Error("NoSuchKey", "NoSuchKey", 404) + if (!isValidS3Bucket(bucket) || !key) { + return s3Error("NoSuchKey", "NoSuchKey", 404) + } + const virtualPath = s3VirtualPath(user, bucket, key) + if (!isWithinUserRoot(user, virtualPath)) return outOfScope(c) + if (await metaReadDenied(c, user, virtualPath)) { + return s3Error("AccessDenied", "Password required for this path", 403) + } try { - const { item } = await getItem(`/${bucket}/${key}`, getCtx(c)) + const { item } = await getItem(virtualPath, getCtx(c)) if (!item || item.is_dir) return s3Error("NoSuchKey", "NoSuchKey", 404) const headers: Record = { "Content-Length": String(item.size || 0), @@ -142,15 +260,47 @@ s3Router.on("HEAD", "/*", async (c) => { s3Router.put("/*", async (c) => { const user = await authUser(c) if (!user) return s3Error("AccessDenied", "Authentication required", 403) + // 写操作必须持有 WRITE_CONTENT 权限位(管理员放行)。 + // 历史缺陷:只判断「是否登录」,任何已认证用户(含只读用户)都能上传。 + if (!canWrite(user)) { + return s3Error("AccessDenied", "Write permission required", 403) + } const { bucket, key } = parseS3Path(c) - if (!bucket || !key) return s3Error("InvalidArgument", "Invalid bucket/key", 400) + if (!isValidS3Bucket(bucket) || !key) { + return s3Error("InvalidArgument", "Invalid bucket/key", 400) + } + const virtualPath = s3VirtualPath(user, bucket, key) + if (!isWithinUserRoot(user, virtualPath)) return outOfScope(c) + // 目录级写判定(对齐 /api/fs/put 的 meta.write_users 与 write_content_bypass) + const parentDir = + virtualPath.lastIndexOf("/") > 0 + ? virtualPath.slice(0, virtualPath.lastIndexOf("/")) + : "/" + if (await metaWriteDenied(c, user, parentDir)) { + return s3Error("AccessDenied", "Write permission denied for this path", 403) + } try { - const buffer = Buffer.from(await c.req.arrayBuffer()) - await putItem(`/${bucket}/${key}`, buffer, getCtx(c)) - return new Response(null, { - status: 200, - headers: { ETag: `"${Date.now().toString(16)}"` }, - }) + // 体积上限按**实际收到的字节数**判定(与 /fs/put 共用 pkg/upload-limit)。 + // 历史缺陷:PutObject 完全没有大小限制,MAX_UPLOAD/MAX_UPPART 都不生效, + // 超大请求体会被整包读入内存——与 /fs/put 的防护形成明显不对称。 + const body = await readBodyWithinLimit(c, false) + if (!body.ok) { + return s3Error( + "EntityTooLarge", + `Object exceeds the ${body.limit} byte limit`, + 400, + ) + } + await putItem(virtualPath, body.buffer, getCtx(c)) + // 不再返回伪造的 ETag。 + // + // 历史实现返回 `Date.now().toString(16)`,不是内容摘要:依赖 ETag 做幂等/ + // 续传/校验的客户端(rclone、aws-cli 等)会记录一个永不对应的值。真实 S3 + // 对单次 PUT 返回内容 MD5,但本运行时没有便宜的 MD5 通道(WebCrypto 不提供 + // MD5;纯 JS 实现要整包多扫一遍;hash-wasm 的 WASM 在边缘运行时可能被禁止, + // 见 internal/seed/hashers.ts),因此这里选择**不返回 ETag**——宁可缺失, + // 也不给出错误的摘要;真实摘要(含分片 ETag 语义)留作后续跟进。 + return new Response(null, { status: 200 }) } catch (e: any) { return s3Error("InternalError", safeErrorMessage(e), 500) } @@ -160,11 +310,24 @@ s3Router.put("/*", async (c) => { s3Router.delete("/*", async (c) => { const user = await authUser(c) if (!user) return s3Error("AccessDenied", "Authentication required", 403) + // 删除操作必须持有 DELETE 权限位(管理员放行)。 + if (!canRemove(user)) { + return s3Error("AccessDenied", "Delete permission required", 403) + } const { bucket, key } = parseS3Path(c) - if (!bucket || !key) return s3Error("InvalidArgument", "Invalid bucket/key", 400) - const idx = key.lastIndexOf("/") - const dir = idx >= 0 ? `/${bucket}/${key.slice(0, idx)}` : `/${bucket}` - const name = idx >= 0 ? key.slice(idx + 1) : key + if (!isValidS3Bucket(bucket) || !key) { + return s3Error("InvalidArgument", "Invalid bucket/key", 400) + } + const virtualPath = s3VirtualPath(user, bucket, key) + if (!isWithinUserRoot(user, virtualPath)) return outOfScope(c) + // 目录级写判定(对齐 /api/fs/remove 的 meta.write_users) + if (await metaWriteDenied(c, user, virtualPath)) { + return s3Error("AccessDenied", "Write permission denied for this path", 403) + } + // 以目标对象自身的虚拟路径为基准拆出父目录与文件名,避免 bucket 边界下的错位 + const slash = virtualPath.lastIndexOf("/") + const dir = slash > 0 ? virtualPath.slice(0, slash) : "/" + const name = virtualPath.slice(slash + 1) try { await removeItems(dir, [name], getCtx(c)) return new Response(null, { status: 204 }) diff --git a/src/backend/server/security_hardening.test.ts b/src/backend/server/security_hardening.test.ts new file mode 100644 index 00000000..cfb70638 --- /dev/null +++ b/src/backend/server/security_hardening.test.ts @@ -0,0 +1,388 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Hono } from "hono" +import { sign } from "hono/jwt" +import { getDb, saveDb } from "../internal/model/db" +import { meHandler } from "./auth" +import { passwordStampOf, setUserPassword } from "../pkg/password" +import { + getActualPath, + isWithinUserRoot, + normalizeSegments, +} from "../pkg/permission" +import { readBodyWithinLimit } from "../pkg/upload-limit" +import { userRouter } from "./user" +import { webdavRouter } from "./webdav" + +/** + * 本轮安全加固的回归测试: + * 1. 管理员用户管理接口的角色保护(对齐 Go handles/user.go + op/user.go); + * 2. 密码指纹(对齐 Go common.UserClaims.PwdTS)—— 改密后旧令牌立即失效。 + */ + +const env: any = { JWT_SECRET: "test-only-jwt-secret-for-hardening" } +const ADMIN_TOKEN = "HARDENING_STATIC_TOKEN" + +const seed = (users: any[], settings: any[] = []) => + saveDb( + { settings, users, storages: [], shares: [] }, + env, + { force: true }, + ) + +const adminUser = (overrides: any = {}) => ({ + id: 1, + username: "admin", + password: "", + salt: "", + role: 2, + permission: 0, + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + ...overrides, +}) + +function adminApp() { + const app = new Hono() + app.route("/api/admin/user", userRouter) + return app +} + +const adminHeaders = { + "Content-Type": "application/json", + Authorization: ADMIN_TOKEN, +} + +test("user/create rejects admin and guest roles", async () => { + await seed([adminUser()], [{ key: "token", value: ADMIN_TOKEN }]) + const app = adminApp() + + for (const role of [1, 2]) { + const res = await app.request("/api/admin/user/create", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ username: `u${role}`, role }), + }) + assert.equal(res.status, 400, `role=${role} must be rejected`) + const json: any = await res.json() + assert.equal(json.message, "admin or guest user can not be created") + } + + const ok = await app.request("/api/admin/user/create", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ username: "normal", role: 0, password: "p@ssw0rd" }), + }) + assert.equal(ok.status, 200) +}) + +test("user/update forbids role changes and disabling the admin account", async () => { + await seed( + [ + adminUser(), + { + id: 5, + username: "someone", + password: "", + salt: "", + role: 0, + permission: 0, + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + }, + ], + [{ key: "token", value: ADMIN_TOKEN }], + ) + const app = adminApp() + + const promote = await app.request("/api/admin/user/update", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ id: 5, role: 2 }), + }) + assert.equal(promote.status, 400) + assert.equal((await promote.json() as any).message, "role can not be changed") + + const demote = await app.request("/api/admin/user/update", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ id: 1, role: 0 }), + }) + assert.equal(demote.status, 400) + + const disableAdmin = await app.request("/api/admin/user/update", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ id: 1, disabled: true }), + }) + assert.equal(disableAdmin.status, 400) + assert.equal( + (await disableAdmin.json() as any).message, + "admin user can not be disabled", + ) + + // 允许的更新:权限位 / base_path / 禁用普通用户 + const ok = await app.request("/api/admin/user/update", { + method: "POST", + headers: adminHeaders, + body: JSON.stringify({ id: 5, permission: 8, base_path: "/data", disabled: true }), + }) + assert.equal(ok.status, 200) +}) + +test("user/delete protects admin and guest by role, not by hardcoded id", async () => { + await seed( + [ + adminUser(), + { + id: 2, + username: "guest", + password: "", + salt: "", + role: 1, + permission: 0, + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + }, + { + // 迁移/多管理员场景:id 不等于 1 的管理员同样必须受保护 + id: 7, + username: "second-admin", + password: "", + salt: "", + role: 2, + permission: 0, + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + }, + { + id: 9, + username: "normal", + password: "", + salt: "", + role: 0, + permission: 0, + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + }, + ], + [{ key: "token", value: ADMIN_TOKEN }], + ) + const app = adminApp() + + for (const id of [1, 2, 7]) { + const res = await app.request(`/api/admin/user/delete?id=${id}`, { + method: "POST", + headers: adminHeaders, + }) + assert.equal(res.status, 400, `id=${id} must be protected`) + assert.equal((await res.json() as any).message, "cannot delete admin or guest") + } + + const del = await app.request("/api/admin/user/delete?id=9", { + method: "POST", + headers: adminHeaders, + }) + assert.equal(del.status, 200) + const db: any = await getDb(env) + assert.equal( + db.users.some((u: any) => u.id === 9), + false, + "a normal user must still be deletable", + ) +}) + +test("password change invalidates previously issued tokens (Go PwdTS parity)", async () => { + await seed([adminUser()]) + const db: any = await getDb(env) + const admin = db.users.find((u: any) => u.username === "admin") + + const app = new Hono() + app.get("/me", meHandler) + + const tokenBefore = await sign( + { + id: admin.id, + username: admin.username, + role: admin.role, + pwd_ts: passwordStampOf(admin), + exp: Math.floor(Date.now() / 1000) + 600, + jti: "pwd-ts-before", + }, + env.JWT_SECRET, + ) + const before = await app.request( + "/me", + { headers: { Authorization: `Bearer ${tokenBefore}` } }, + env, + ) + assert.equal(before.status, 200, "token issued before the change must work") + + // 改密(Go: SetPassword 会刷新 PwdTS) + await setUserPassword(admin, "brand-new-password") + await saveDb(db, env, { force: true }) + + const after = await app.request( + "/me", + { headers: { Authorization: `Bearer ${tokenBefore}` } }, + env, + ) + assert.equal(after.status, 401, "token issued before the change must be rejected") + + const tokenAfter = await sign( + { + id: admin.id, + username: admin.username, + role: admin.role, + pwd_ts: passwordStampOf(admin), + exp: Math.floor(Date.now() / 1000) + 600, + jti: "pwd-ts-after", + }, + env.JWT_SECRET, + ) + const fresh = await app.request( + "/me", + { headers: { Authorization: `Bearer ${tokenAfter}` } }, + env, + ) + assert.equal(fresh.status, 200, "a fresh token must work") +}) + +test("normalizeSegments / getActualPath / isWithinUserRoot 钳制越根遍历", () => { + // 显式锁定越根钳制语义(评审建议补的用例) + assert.equal(normalizeSegments("/a/../../b"), "/b") + assert.equal(normalizeSegments("/a/b/../c"), "/a/c") + assert.equal(normalizeSegments("../../etc/passwd"), "/etc/passwd") + assert.equal(normalizeSegments("//a///b/./c"), "/a/b/c") + assert.equal(normalizeSegments(""), "/") + + // base_path 受限:先折叠 reqPath 再拼接,`..` 不得越过用户根 + const user = { id: 9, role: 0, permission: 0, base_path: "/photos/alice" } + assert.equal(getActualPath(user, "/"), "/photos/alice") + assert.equal(getActualPath(user, "/sub/x"), "/photos/alice/sub/x") + assert.equal(getActualPath(user, "/../bob/x"), "/photos/alice/bob/x") + assert.equal(getActualPath(user, "/sub/../../bob"), "/photos/alice/bob") + + // 范围复核:等于根本身或位于其下为真,越过为假 + assert.equal(isWithinUserRoot(user, "/photos/alice"), true) + assert.equal(isWithinUserRoot(user, "/photos/alice/a/b"), true) + assert.equal(isWithinUserRoot(user, "/photos/bob"), false) + assert.equal(isWithinUserRoot(user, "/photos/alice/../bob"), false) + // base_path 为根的用户不受限制 + assert.equal(isWithinUserRoot({ id: 1, role: 2, permission: 0, base_path: "/" }, "/any"), true) +}) + +test("readBodyWithinLimit 按实际字节数生效(省略 Content-Length 也不例外)", async () => { + const env: any = { MAX_UPLOAD: "8" } + const fakeCtx = (body: string, headers: Record = {}) => ({ + env, + req: { + header: (k: string) => + headers[k] ?? headers[k.toLowerCase()] ?? headers[k.toUpperCase()], + arrayBuffer: async () => new TextEncoder().encode(body).buffer, + }, + }) + + // 声明超限:预检即拒绝 + const declared = await readBodyWithinLimit( + fakeCtx("123456789", { "Content-Length": "9" }), + false, + ) + assert.equal(declared.ok, false) + + // 省略 Content-Length:必须按实际字节数拒绝(历史缺陷:直接放行) + const omitted = await readBodyWithinLimit(fakeCtx("123456789"), false) + assert.equal(omitted.ok, false) + if (!omitted.ok) assert.equal(omitted.limit, 8) + + // 未超限:放行且内容完整 + const ok = await readBodyWithinLimit( + fakeCtx("1234", { "Content-Length": "4" }), + false, + ) + assert.equal(ok.ok, true) + if (ok.ok) assert.equal(ok.buffer.length, 4) + + // 分片上限(MAX_UPPART)独立于整体上限:未配置时用默认 16MB + const part = await readBodyWithinLimit(fakeCtx("123456789"), true) + assert.equal(part.ok, true) + + // 显式配置分片上限同样按实际字节数判定 + const partEnv: any = { MAX_UPPART: "2" } + const partCtx = { + env: partEnv, + req: { + header: () => undefined, + arrayBuffer: async () => new TextEncoder().encode("123").buffer, + }, + } + const partTooLarge = await readBodyWithinLimit(partCtx, true) + assert.equal(partTooLarge.ok, false) +}) + +test("WebDAV: 受 meta 保护的路径返回带指引的 403(而不是裸 403)", async () => { + const env: any = { JWT_SECRET: "test-only-jwt-secret-for-hardening" } + const davUser: any = { + id: 11, + username: "dav-user", + role: 0, + permission: 1 << 8, // WEBDAV_READ + base_path: "/", + disabled: false, + pwd_update_at: new Date().toISOString(), + } + await setUserPassword(davUser, "dav-pass") + await saveDb( + { + settings: [], + users: [davUser], + storages: [], + shares: [], + metas: [ + { + id: 1, + path: "/secret", + password: "meta-pass", + p_sub: true, + read_users: [], + read_users_sub: false, + write_users: [], + write_users_sub: false, + write: false, + w_sub: false, + hide: "", + h_sub: false, + readme: "", + r_sub: false, + header: "", + header_sub: false, + }, + ], + }, + env, + { force: true }, + ) + + const app = new Hono() + app.route("/dav", webdavRouter) + const basic = + "Basic " + Buffer.from("dav-user:dav-pass").toString("base64") + + // 账号本身有 WEBDAV_READ,但该路径带目录密码 → 元信息 ACL 拦截 + const res = await app.request( + "/dav/secret", + { method: "PROPFIND", headers: { Authorization: basic, Depth: "1" } }, + env, + ) + assert.equal(res.status, 403) + const body = await res.text() + // 附带给用户可操作的指引(评审建议),且不指明具体是哪一类限制 + assert.match(body, /directory-level access control/) + assert.match(body, /meta password/) + assert.match(body, /web UI|api\/fs/) +}) diff --git a/src/backend/server/sso.ts b/src/backend/server/sso.ts index 7dc6c3d7..7186fdf5 100644 --- a/src/backend/server/sso.ts +++ b/src/backend/server/sso.ts @@ -3,7 +3,7 @@ import { sign } from "hono/jwt" import { getDb, saveDb } from "../internal/model/db" import { getJwtSecret } from "./middlewares" import { generateRandomPassword } from "./auth" -import { setUserPassword } from "../pkg/password" +import { setUserPassword, passwordStampOf } from "../pkg/password" /** * SSO 登录(OAuth2 授权码流 + OIDC)。 @@ -174,6 +174,8 @@ async function generateToken(user: any, c: any): Promise { id: user.id, username: user.username, role: user.role, + // 密码指纹(对齐 Go PwdTS):改密后旧令牌立即失效 + pwd_ts: passwordStampOf(user), exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, jti: typeof crypto.randomUUID === "function" diff --git a/src/backend/server/user.ts b/src/backend/server/user.ts index a3a8b223..9858966a 100644 --- a/src/backend/server/user.ts +++ b/src/backend/server/user.ts @@ -1,6 +1,6 @@ import { Hono } from "hono" import { getDb, saveDb } from "../internal/model/db" -import { generateRandomPassword, verifyUserPassword } from "./auth" +import { verifyUserPassword } from "./auth" import { setUserPassword } from "../pkg/password" import { verify } from "hono/jwt" import { getJwtSecret } from "./middlewares" @@ -89,26 +89,33 @@ userRouter.post("/create", async (c) => { ) } + // 对齐 Go handles.CreateUser:admin / guest 不能通过 create 接口创建。 + // 管理员账号由安装向导(或 ADMIN_PASS)创建、guest 全局唯一,两者都不应 + // 经由此接口产生 —— 否则后台可被批量制造出多个管理员。 + const role = body.role !== undefined ? parseInt(body.role, 10) : 0 + if (role === 1 || role === 2) { + return c.json( + { + code: 400, + message: "admin or guest user can not be created", + data: null, + }, + 400, + ) + } + const maxId = db.users.reduce( (max: number, u: any) => Math.max(max, u.id || 0), 0, ) const newId = maxId + 1 - // FIX(F-11): "123456" as a silent fallback meant every user created without - // an explicit password shipped with a guessable one. When no password is - // given, generate a random one and hand it back to the admin caller once — - // it is never stored or logged in plaintext. - let plainPassword: string = body.password || "" - let generatedPassword: string | null = null - if (!plainPassword) { - generatedPassword = generateRandomPassword() - plainPassword = generatedPassword - } + // 密码直接采用调用方传入值(对齐 Go:不传入即为空密码;不再自动生成随机 + // 密码并回显)。 const newUser: any = { id: newId, username: body.username, - role: body.role !== undefined ? parseInt(body.role, 10) : 0, + role, permission: body.permission !== undefined ? parseInt(body.permission, 10) : 0, base_path: body.base_path || "/", @@ -118,17 +125,11 @@ userRouter.post("/create", async (c) => { pwd_update_at: new Date().toISOString(), } - await setUserPassword(newUser, plainPassword) + await setUserPassword(newUser, body.password || "") db.users.push(newUser) await saveDb(db, c.env) - return c.json({ - code: 200, - message: generatedPassword - ? "success (a random password was generated — pass one explicitly to choose your own)" - : "success", - data: generatedPassword ? { password: generatedPassword } : null, - }) + return c.json({ code: 200, message: "success", data: null }) }) // POST /api/admin/user/update @@ -165,11 +166,26 @@ userRouter.post("/update", async (c) => { user.username = body.username } + // 对齐 Go handles.UpdateUser: + // 1) role 不可通过 update 修改(防止把普通用户提升为管理员 / 使既有管理员降级); + // 2) 管理员账号不可被禁用(避免唯一管理员把自己锁在后台之外)。 + if (body.role !== undefined && parseInt(body.role, 10) !== user.role) { + return c.json( + { code: 400, message: "role can not be changed", data: null }, + 400, + ) + } + if (body.disabled !== undefined && !!body.disabled && user.role === 2) { + return c.json( + { code: 400, message: "admin user can not be disabled", data: null }, + 400, + ) + } + if (body.password && body.password.trim() !== "") { await setUserPassword(user, body.password.trim()) } - if (body.role !== undefined) user.role = parseInt(body.role, 10) if (body.permission !== undefined) user.permission = parseInt(body.permission, 10) if (body.base_path !== undefined) user.base_path = body.base_path @@ -193,16 +209,24 @@ const deleteUserHandler = async (c: any) => { ) } const id = parseInt(idQuery, 10) - if (id === 1) { + + const db = await getDb(c.env) + if (!db.users) db.users = [] + + // 对齐 Go op.DeleteUserById:按**角色**保护 admin/guest,而不是硬编码 id。 + // 历史缺陷:只拦 id===1 —— guest(通常 id=2)可被删除,导致匿名访问失去 + // 后备身份;若存在 id≠1 的管理员(迁移/多管理员场景)也完全不受保护。 + const target = db.users.find((u: any) => u.id === id) + if (!target) { + return c.json({ code: 404, message: "User not found", data: null }, 404) + } + if (target.role === 2 || target.role === 1) { return c.json( - { code: 400, message: "Cannot delete primary admin user", data: null }, + { code: 400, message: "cannot delete admin or guest", data: null }, 400, ) } - const db = await getDb(c.env) - if (!db.users) db.users = [] - db.users = db.users.filter((u: any) => u.id !== id) await saveDb(db, c.env) diff --git a/src/backend/server/webauthn.ts b/src/backend/server/webauthn.ts index be6f9004..fc5087a6 100644 --- a/src/backend/server/webauthn.ts +++ b/src/backend/server/webauthn.ts @@ -2,6 +2,7 @@ import { Hono } from "hono" import { sign } from "hono/jwt" import { getDb, saveDb } from "../internal/model/db" import { getJwtSecret, getUserFromContext } from "./middlewares" +import { passwordStampOf } from "../pkg/password" /** * WebAuthn / Passkey 登录。 @@ -219,11 +220,138 @@ interface SessionData { userVerification: string } -function sessionToB64(sd: SessionData): string { - return b64Encode(new TextEncoder().encode(JSON.stringify(sd))) +/** + * 服务端挑战(challenge)存储。 + * + * 安全不变量:WebAuthn 的 challenge 必须由**服务端签发**、**一次性消费**、 + * 且带**有效期**。这是对抗重放的核心——没有它,任何一份曾经合法的 assertion + * 都能被无限次重复使用。 + * + * 历史实现把整个会话(含 challenge)base64 后经 `session` 头在客户端往返: + * 既没有服务端签发、也没有有效期、更没有消费状态,因此 + * ① 客户端可以自行编造 challenge(服务端只比对「自己收到的那份」,等于没校验来源); + * ② 同一份 assertion 可反复换取新 JWT(实测连续两次都返回 token); + * ③ clientDataJSON.origin 与 UP/UV 标志完全不校验。 + * + * 现在改为:`begin_*` 生成随机 id,把会话内容写入持久化后端(TTL 5 分钟), + * 仅把 id 回给客户端;`finish_*` 读取后**立即作废**该 id,实现单次使用。 + * 对客户端而言 `session` 仍是不透明字符串,接口形状不变。 + */ +const CHALLENGE_PREFIX = "openlist_wa_chal_" +const CHALLENGE_TTL_MS = 5 * 60 * 1000 +const UP_FLAG = 0x01 +const UV_FLAG = 0x04 + +/** + * 无持久化后端时的兜底挑战表(进程内,单实例语义)。 + * + * 边界:内存模式部署本就无法跨实例协作,挑战也只在该实例内有效——这与 + * 「凭据本身也存在内存里」是同一级别的一致性,不会额外削弱安全性。 + * 必须带 TTL 与容量上限,避免被扫描式请求撑爆内存。 + */ +const localChallenges = new Map() +const LOCAL_CHALLENGE_MAX = 1000 + +function pruneLocalChallenges(): void { + const now = Date.now() + for (const [k, v] of localChallenges) { + if (v.exp < now) localChallenges.delete(k) + } + // 仍然超限时按插入顺序淘汰最旧条目 + while (localChallenges.size > LOCAL_CHALLENGE_MAX) { + const oldest = localChallenges.keys().next().value + if (oldest === undefined) break + localChallenges.delete(oldest) + } } -function sessionFromB64(s: string): SessionData { - return JSON.parse(new TextDecoder().decode(b64Decode(s))) + +async function issueChallenge(c: any, sd: SessionData): Promise { + const id = b64urlEncode(newChallenge()) + const exp = Date.now() + CHALLENGE_TTL_MS + + // 先写进程内兜底表:**无论持久化是否可用都要写**。 + // + // 为什么不能「持久化成功就 return」:writePersistedSecret 在驱动不可用时会 + // 记录日志后仍然返回真值,无法作为「确实写进去了」的依据。实测按返回值提前 + // return 会导致挑战哪儿都没存,所有 passkey 登录恒定报「invalid or expired」。 + // 兜底表保证单实例内始终可用;持久化成功则额外获得跨实例能力。 + pruneLocalChallenges() + localChallenges.set(id, { data: sd, exp }) + + try { + const { writePersistedSecret } = await import("../internal/model/db") + await writePersistedSecret( + c.env, + CHALLENGE_PREFIX + id, + JSON.stringify({ ...sd, exp }), + ) + } catch { + // 无持久化后端:仅依赖上面的进程内兜底表 + } + return id +} + +async function consumeChallenge( + c: any, + id: string, +): Promise { + if (!id) return null + + // 1) 进程内兜底表(读取即删除 = 单次使用) + const local = localChallenges.get(id) + if (local) { + localChallenges.delete(id) + return local.exp < Date.now() ? null : local.data + } + + // 2) 持久化后端 + const { readPersistedSecret, writePersistedSecret } = await import( + "../internal/model/db" + ) + const key = CHALLENGE_PREFIX + id + let raw: string | null = null + try { + raw = await readPersistedSecret(c.env, key) + } catch { + return null + } + // 一次性消费:无论后续校验是否通过,都先把该挑战作废 + try { + await writePersistedSecret(c.env, key, "") + } catch {} + if (!raw) return null + try { + const data = JSON.parse(String(raw)) + if (!data || typeof data.exp !== "number" || data.exp < Date.now()) { + return null + } + return data as SessionData + } catch { + return null + } +} + +/** + * 校验 clientDataJSON.origin:必须是本站源或与 rpId 一致。 + * WebAuthn 规范要求 RP 校验 origin,防止同 rpId 下的跨源钓鱼。 + */ +function originAllowed(origin: string, rpId: string, c: any): boolean { + if (!origin) return false + try { + const o = new URL(origin) + if (o.hostname === rpId) return true + // 覆盖自定义端口/域名:与当前请求同源也放行 + return o.host === new URL(c.req.url).host + } catch { + return false + } +} + +/** 认证器 data 标志位校验:必须证明「用户在场」;策略要求时还需「用户已验证」 */ +function flagsAllowed(flags: number, userVerification: string): boolean { + if ((flags & UP_FLAG) === 0) return false + if (userVerification === "required" && (flags & UV_FLAG) === 0) return false + return true } async function generateToken(user: any, c: any): Promise { @@ -231,6 +359,8 @@ async function generateToken(user: any, c: any): Promise { id: user.id, username: user.username, role: user.role, + // 密码指纹(对齐 Go PwdTS):改密后旧令牌立即失效 + pwd_ts: passwordStampOf(user), exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, jti: typeof crypto.randomUUID === "function" ? crypto.randomUUID() : b64urlEncode(newChallenge()), } @@ -279,10 +409,13 @@ webauthnRouter.post("/webauthn_begin_login", async (c) => { options.allowCredentials = allowCredentials } + // session 现在只是服务端挑战的**不透明 id**(内容存在持久化后端,单次使用) + const session = await issueChallenge(c, sd) + return c.json({ code: 200, message: "success", - data: { options, session: sessionToB64(sd) }, + data: { options, session }, }) }) @@ -293,17 +426,19 @@ webauthnRouter.post("/webauthn_finish_login", async (c) => { return c.json({ code: 403, message: "WebAuthn is not enabled", data: null }, 403) } const sessionHeader = c.req.header("session") || c.req.header("Session") || "" - let sd: SessionData - try { - sd = sessionFromB64(sessionHeader) - } catch { - return c.json({ code: 400, message: "invalid session", data: null }, 400) + // 服务端签发的挑战,读取即作废(单次使用) + const sd = await consumeChallenge(c, sessionHeader) + if (!sd) { + return c.json( + { code: 400, message: "invalid or expired session", data: null }, + 400, + ) } const body = await c.req.json().catch(() => ({})) const rpId = rpIdOf(c, db) try { - // 1. 解析 clientDataJSON,校验 challenge + // 1. 解析 clientDataJSON,校验 challenge 与 origin const rawId = b64urlDecode(body.id || "") const clientDataJSON = JSON.parse( new TextDecoder().decode(b64urlDecode(body.response?.clientDataJSON || "")), @@ -314,6 +449,10 @@ webauthnRouter.post("/webauthn_finish_login", async (c) => { if (clientDataJSON.type !== "webauthn.get") { return c.json({ code: 400, message: "invalid ceremony type", data: null }, 400) } + // origin 必须属于本站(防止同 rpId 下的跨源钓鱼) + if (!originAllowed(String(clientDataJSON.origin || ""), rpId, c)) { + return c.json({ code: 400, message: "origin not allowed", data: null }, 400) + } // 2. 解析 authenticatorData const authData = b64urlDecode(body.response?.authenticatorData || "") @@ -323,6 +462,13 @@ webauthnRouter.post("/webauthn_finish_login", async (c) => { if (b64urlEncode(rpIdHash) !== b64urlEncode(expectedRpHash)) { return c.json({ code:400, message: "rpId hash mismatch", data: null }, 400) } + // 标志位:必须证明用户在场;策略为 required 时还需用户验证 + if (!flagsAllowed(authData[32], sd.userVerification === "required" ? "required" : "preferred")) { + return c.json( + { code: 400, message: "user presence/verification check failed", data: null }, + 400, + ) + } // 3. 找到对应用户与 credential const username = c.req.query("username") || sd.username || "" @@ -352,6 +498,12 @@ webauthnRouter.post("/webauthn_finish_login", async (c) => { if (!user || !credential) { return c.json({ code: 400, message: "credential not found", data: null }, 400) } + // 账号状态复核:与其它身份解析入口一致,禁用用户不得换取新令牌。 + // 历史缺陷:finish_login 签发 JWT 前不检查 disabled,被禁用的账号仍能用 + // 已有凭证登录(实测:置 disabled=true 后仍返回 token)。 + if (user.disabled) { + return c.json({ code: 403, message: "account is disabled", data: null }, 403) + } // 4. 验证签名(credential 存储 JWK 公钥) const jwk = credential.publicKey @@ -455,10 +607,12 @@ webauthnRouter.post("/webauthn_begin_registration", async (c) => { } if (existing.length > 0) options.excludeCredentials = existing + const session = await issueChallenge(c, sd) + return c.json({ code: 200, message: "success", - data: { options, session: sessionToB64(sd) }, + data: { options, session }, }) }) @@ -473,17 +627,19 @@ webauthnRouter.post("/webauthn_finish_registration", async (c) => { return c.json({ code: 401, message: "Unauthorized", data: null }, 401) } const sessionHeader = c.req.header("Session") || c.req.header("session") || "" - let sd: SessionData - try { - sd = sessionFromB64(sessionHeader) - } catch { - return c.json({ code: 400, message: "invalid session", data: null }, 400) + // 服务端签发的挑战,读取即作废(单次使用) + const sd = await consumeChallenge(c, sessionHeader) + if (!sd) { + return c.json( + { code: 400, message: "invalid or expired session", data: null }, + 400, + ) } const body = await c.req.json().catch(() => ({})) const rpId = rpIdOf(c, db) try { - // 1. 校验 clientDataJSON challenge + // 1. 校验 clientDataJSON challenge 与 origin const clientDataJSON = JSON.parse( new TextDecoder().decode(b64urlDecode(body.response?.clientDataJSON || "")), ) @@ -493,6 +649,9 @@ webauthnRouter.post("/webauthn_finish_registration", async (c) => { if (clientDataJSON.type !== "webauthn.create") { return c.json({ code: 400, message: "invalid ceremony type", data: null }, 400) } + if (!originAllowed(String(clientDataJSON.origin || ""), rpId, c)) { + return c.json({ code: 400, message: "origin not allowed", data: null }, 400) + } // 2. 解析 attestationObject const attObj = b64urlDecode(body.response?.attestationObject || "") diff --git a/src/backend/server/webdav.ts b/src/backend/server/webdav.ts index 80a4722d..c52ac0a6 100644 --- a/src/backend/server/webdav.ts +++ b/src/backend/server/webdav.ts @@ -1,6 +1,32 @@ import { Hono } from "hono" -import { authUserFromReq, getOrInitUsers, verifyUserPassword } from "./auth" -import { can, PermissionBit } from "../pkg/permission" +import { + authUserFromReq, + clearLoginFailures, + getOrInitUsers, + isLoginLocked, + recordLoginFailure, + verifyUserPassword, +} from "./auth" +import { + can, + canCopy, + canMove, + canRename, + canWrite, + getActualPath, + isAdmin, + isGuest, + isWithinUserRoot, + PermissionBit, +} from "../pkg/permission" +import { + canAccess, + canRead as canReadMeta, + canWrite as canWriteMeta, + canWriteContentBypassUserPerms, + getNearestMeta, +} from "../pkg/meta" +import type { Meta } from "../pkg/meta" import { listItems, getItem, @@ -39,8 +65,18 @@ const getStorageRequestContext = (c: any) => { } } -/** Basic Auth 或 Bearer token 认证,返回用户对象(未认证返回 null) */ -async function webdavAuth(c: any): Promise { +/** + * Basic Auth 或 Bearer token 认证。 + * + * 返回 `{ user, metaPassword }`;未认证返回 null。 + * + * `metaPassword` 对齐 Go WebDAVAuth:只有 guest 账号才会把 Basic 密码继续 + * 当作 meta 访问密码(Go: `if user.IsGuest() { MetaPassKey = password }`), + * 普通用户一律为空 —— 目录级密码不能靠账号密码顺带绕过。 + */ +async function webdavAuth( + c: any, +): Promise<{ user: any; metaPassword: string } | null> { const authHeader = c.req.header("Authorization") || "" if (authHeader.startsWith("Basic ")) { try { @@ -56,9 +92,12 @@ async function webdavAuth(c: any): Promise { if (!user) return null // 空密码用户(guest):Basic Auth 下若未提供密码则允许(与 AList 一致) if (!user.password) { - return password === "" ? user : null + if (password !== "") return null + return { user, metaPassword: isGuest(user) ? password : "" } + } + if (await verifyUserPassword(user, password)) { + return { user, metaPassword: isGuest(user) ? password : "" } } - if (await verifyUserPassword(user, password)) return user return null } catch { return null @@ -66,7 +105,7 @@ async function webdavAuth(c: any): Promise { } if (authHeader.startsWith("Bearer ")) { const auth = await authUserFromReq(c) - return auth ? auth.user : null + return auth ? { user: auth.user, metaPassword: "" } : null } return null } @@ -113,6 +152,116 @@ function splitPath(p: string): { dir: string; name: string } { return { dir, name } } +/** + * 把客户端可见的 DAV 路径映射为**该用户根目录之下**的实际存储路径。 + * + * 语义:`/dav/` 就是该用户的根,`/dav/sub/x` 即其根下的 `sub/x`。 + * - 管理员/默认用户(base_path="/"):路径原样返回,与历史行为完全一致; + * - 受限 base_path 的用户:`/dav/sub` -> `/sub`。 + * + * 安全不变量:DAV 的源路径与 Destination 都必须经此映射,并在映射后做 + * isWithinUserRoot 复核。历史缺陷:DAV 全程直接操作全局虚拟路径,既未套用 + * base_path,也未做用户范围校验——实测「只有 WEBDAV_READ 权限、base_path 受限」 + * 的用户可以 PROPFIND 其他用户的目录。 + */ +function scopedDavPath(user: any, davPath: string): string { + const root = getActualPath(user, "/") + if (root === "/") return davPath + const rel = davPath === "/" ? "" : davPath.replace(/^\/+/, "") + return rel ? `${root}/${rel}` : root +} + +/** 解析 MOVE / COPY 的 Destination 头并映射到用户根之下 */ +function destinationPath(c: any, user: any): string | null { + const destRaw = c.req.header("Destination") || "" + if (!destRaw) return null + let dest = destRaw + try { + dest = decodeURIComponent(new URL(destRaw, c.req.url).pathname) + // 与 davPathOf 同样的前缀守卫:Destination 未挂在 DAV_MOUNT 下时原样保留, + // 不做无意义的前缀改写(否则挂在子路径下的部署会被截错) + if (dest === DAV_MOUNT || dest.startsWith(DAV_MOUNT + "/")) { + dest = dest.slice(DAV_MOUNT.length) || "/" + } + } catch { + return null + } + if (!dest) dest = "/" + return scopedDavPath(user, dest.startsWith("/") ? dest : `/${dest}`) +} + +/** + * 读操作的 meta 判定(对齐 Go webdav.go 各 handler 的 CanAccess 调用): + * 密码保护 + read_users 白名单。返回 true 表示拒绝。 + * + * 安全不变量:WebDAV 与 /api/fs/* 必须共享同一套目录级访问控制,否则 + * 「站内被密码保护」的目录可以经 /dav 直接读走(跨协议权限旁路)。 + */ +async function deniedByMetaRead( + c: any, + user: any, + path: string, + metaPassword: string, +): Promise { + let meta: Meta | null = null + try { + meta = await getNearestMeta(path, c.env) + } catch { + meta = null + } + if (!meta) return false + return !canAccess(user, meta, path, metaPassword || "") +} + +/** + * 写操作的 meta 判定(对齐 Go webdav.go / file.go 的 CanWrite 调用): + * - meta.write_users 白名单; + * - requireContentPerm 时还要求 WRITE_CONTENT 权限位,除非目标目录通过 + * meta.write 强制开放(CanWriteContentBypassUserPerms)。 + * 返回 true 表示拒绝。 + */ +async function deniedByMetaWrite( + c: any, + user: any, + path: string, + requireContentPerm: boolean, +): Promise { + let meta: Meta | null = null + try { + meta = await getNearestMeta(path, c.env) + } catch { + meta = null + } + if (!meta) return false + if (isAdmin(user)) return false + if ( + requireContentPerm && + !canWrite(user) && + !canWriteContentBypassUserPerms(meta, path) + ) { + return true + } + return !canWriteMeta(user, meta, path) +} + +/** + * meta 判定失败时的统一响应(403 + 一句指引)。 + * + * WebDAV 没有传递「目录密码」的标准位置:与 Go 一致,只有 guest 账号会把 + * Basic 密码透传为 meta 密码,普通用户一律为空。因此普通用户访问密码保护的 + * 目录只会得到裸 403,容易被误判成账号权限问题而反复重试;这里补一句说明。 + * + * 措辞刻意不区分「密码错误 / read_users / write_users」——不向调用方泄露 + * 具体是哪一类限制。 + */ +const metaForbidden = (c: any) => + c.text( + "Forbidden: directory-level access control denied this request " + + "(meta password, read_users or write_users). WebDAV cannot carry a meta " + + "password — open the path in the web UI, or use /api/fs/* with a password.", + 403, + ) + /** * 推导本次请求实际使用的挂载前缀(正规挂载为 `/dav`)。 * @@ -129,38 +278,72 @@ function davPrefixOf(c: any, davPath: string): string { } webdavRouter.all("/*", async (c) => { - const user = await webdavAuth(c) - if (!user) { + const method = c.req.method.toUpperCase() + + // OPTIONS 匿名放行(对齐 Go WebDAVAuth):客户端需要先探测 DAV 能力, + // 此时通常还没有可用凭据。 + if (method === "OPTIONS") { + c.header("DAV", "1, 2") + c.header( + "Allow", + "OPTIONS, PROPFIND, GET, HEAD, PUT, MKCOL, DELETE, MOVE, COPY", + ) + c.header("MS-Author-Via", "DAV") + return c.body(null, 200) + } + + // 防爆破:与 /api/auth/login 共用同一套失败计数与锁定机制。 + // 对齐 Go:model.LoginCache 在 WebDAV 与登录接口之间共用(失败累加、 + // 达到阈值锁定)。历史缺陷:/dav 的 Basic Auth 可以无限次尝试密码。 + const authHeader = c.req.header("Authorization") || "" + let basicUsername = "" + if (authHeader.startsWith("Basic ")) { + try { + basicUsername = atob(authHeader.substring(6).trim()).split(":")[0] || "" + } catch { + basicUsername = "" + } + if (await isLoginLocked(c, basicUsername, c.env)) { + return c.text("Too Many Requests", 429) + } + } + + const auth = await webdavAuth(c) + if (!auth) { + if (authHeader.startsWith("Basic ")) { + await recordLoginFailure(c, basicUsername, c.env) + } return c.text("Unauthorized", 401, { "WWW-Authenticate": 'Basic realm="OpenList"', }) } + if (authHeader.startsWith("Basic ")) { + await clearLoginFailures(c, basicUsername, c.env) + } + const { user, metaPassword } = auth const canRead = can(user, PermissionBit.WEBDAV_READ) const canManage = can(user, PermissionBit.WEBDAV_MANAGE) if (!canRead && !canManage) { return c.text("Forbidden", 403) } - const method = c.req.method.toUpperCase() + // davPath 是**客户端可见**路径(用于 href,不能把实际存储路径回显给客户端); + // actualPath 才是交给存储层的路径(已套用该用户的根目录)。 const davPath = davPathOf(c) + const actualPath = scopedDavPath(user, davPath) + if (!isWithinUserRoot(user, actualPath)) return c.text("Forbidden", 403) const ctx = getStorageRequestContext(c) try { switch (method) { - case "OPTIONS": { - c.header("DAV", "1, 2") - c.header( - "Allow", - "OPTIONS, PROPFIND, GET, HEAD, PUT, MKCOL, DELETE, MOVE, COPY", - ) - c.header("MS-Author-Via", "DAV") - return c.body(null, 200) - } - case "PROPFIND": { if (!canRead) return c.text("Forbidden", 403) + // 目录级访问控制(与 /api/fs/list 同一套判定) + if (await deniedByMetaRead(c, user, actualPath, metaPassword)) { + return metaForbidden(c) + } const depth = c.req.header("Depth") || "1" - const res = await listItems(davPath, ctx) + const res = await listItems(actualPath, ctx) const items = (res.content || []).map((it: any) => ({ name: it.name, size: it.size || 0, @@ -179,7 +362,11 @@ webdavRouter.all("/*", async (c) => { case "GET": case "HEAD": { if (!canRead) return c.text("Forbidden", 403) - const { item, rawUrl } = await getItem(davPath, ctx) + // 目录级访问控制(与 /api/fs/get 同一套判定) + if (await deniedByMetaRead(c, user, actualPath, metaPassword)) { + return metaForbidden(c) + } + const { item, rawUrl } = await getItem(actualPath, ctx) if (!item) return c.text("Not found", 404) if (item.is_dir) return c.text("Is a directory", 400) // 重定向到 rawRouter 实际下载;rawRouter 已处理所有驱动的下载协议 @@ -189,65 +376,94 @@ webdavRouter.all("/*", async (c) => { // op/storage.ts resolveRawUrlPrefix):/p 受 Go canProxy() 限制,未开启 // 代理的存储会 403 proxy not allowed,因此不能在这里硬编码 /p。 return c.redirect( - rawUrl || `/api/d${encodeDownloadPath(davPath)}`, + rawUrl || `/api/d${encodeDownloadPath(actualPath)}`, 302, ) } case "PUT": { if (!canManage) return c.text("Forbidden", 403) + // 对齐 Go webdav.go handlePut:目标所在目录需通过「写内容权限位或 + // meta.write 强制开放」+ meta.write_users 白名单 + if (await deniedByMetaWrite(c, user, splitPath(actualPath).dir, true)) { + return metaForbidden(c) + } const buffer = Buffer.from(await c.req.arrayBuffer()) - await putItem(davPath, buffer, ctx) + await putItem(actualPath, buffer, ctx) return c.body(null, 201) } case "MKCOL": { if (!canManage) return c.text("Forbidden", 403) - await makeDirectory(davPath, ctx) + // 对齐 Go webdav.go handleMkcol(与 PUT 同一套校验) + if (await deniedByMetaWrite(c, user, splitPath(actualPath).dir, true)) { + return metaForbidden(c) + } + await makeDirectory(actualPath, ctx) return c.body(null, 201) } case "DELETE": { if (!canManage) return c.text("Forbidden", 403) - const { dir, name } = splitPath(davPath) + // 对齐 Go webdav.go handleDelete:目标自身的 meta.write_users 白名单 + if (await deniedByMetaWrite(c, user, actualPath, false)) { + return metaForbidden(c) + } + const { dir, name } = splitPath(actualPath) await removeItems(dir, [name], ctx) return c.body(null, 204) } case "MOVE": { if (!canManage) return c.text("Forbidden", 403) - const destRaw = c.req.header("Destination") || "" - let dest = destRaw - try { - // 与 davPathOf 同样的前缀守卫:Destination 未挂载在 DAV_MOUNT 下时 - // 原样保留,不做无意义的前缀改写 - dest = decodeURIComponent(new URL(destRaw, c.req.url).pathname) - if (dest === DAV_MOUNT || dest.startsWith(DAV_MOUNT + "/")) { - dest = dest.slice(DAV_MOUNT.length) || "/" - } - if (!dest.startsWith("/")) dest = "/" + dest - } catch {} - const src = splitPath(davPath) + // Destination 同样必须落在调用者根目录之内,否则可以把文件移出/移入他人目录 + const dest = destinationPath(c, user) + if (!dest || !isWithinUserRoot(user, dest)) { + return c.text("Forbidden", 403) + } + const src = splitPath(actualPath) const dst = splitPath(dest) + // 对齐 Go webdav/file.go moveFiles:跨目录需要 MOVE 权限位、 + // 改名需要 RENAME 权限位;源/目标目录均需通过 meta 写判定 + if (src.dir !== dst.dir && !canMove(user)) { + return c.text("Forbidden", 403) + } + if (src.name !== dst.name && !canRename(user)) { + return c.text("Forbidden", 403) + } + if ( + (await deniedByMetaWrite(c, user, src.dir, false)) || + (await deniedByMetaWrite(c, user, dst.dir, false)) + ) { + return metaForbidden(c) + } await moveItems(src.dir, dst.dir, [src.name], ctx) return c.body(null, 201) } case "COPY": { if (!canManage) return c.text("Forbidden", 403) - const destRaw = c.req.header("Destination") || "" - let dest = destRaw - try { - // 与 davPathOf 同样的前缀守卫:Destination 未挂载在 DAV_MOUNT 下时 - // 原样保留,不做无意义的前缀改写 - dest = decodeURIComponent(new URL(destRaw, c.req.url).pathname) - if (dest === DAV_MOUNT || dest.startsWith(DAV_MOUNT + "/")) { - dest = dest.slice(DAV_MOUNT.length) || "/" - } - if (!dest.startsWith("/")) dest = "/" + dest - } catch {} - const src = splitPath(davPath) + const dest = destinationPath(c, user) + if (!dest || !isWithinUserRoot(user, dest)) { + return c.text("Forbidden", 403) + } + const src = splitPath(actualPath) const dst = splitPath(dest) + // 对齐 Go webdav/file.go copyFiles:COPY 权限位 + 源目录可读 + // (read_users 白名单)+ 目标目录 meta 写判定 + if (!canCopy(user)) return c.text("Forbidden", 403) + let srcMeta: Meta | null = null + try { + srcMeta = await getNearestMeta(src.dir, c.env) + } catch { + srcMeta = null + } + if (!canReadMeta(user, srcMeta, src.dir)) { + return metaForbidden(c) + } + if (await deniedByMetaWrite(c, user, dst.dir, false)) { + return metaForbidden(c) + } await copyItems(src.dir, dst.dir, [src.name], ctx) return c.body(null, 201) } From e40d8bcf90c3beffc322e96109c7dcbc995ac7bf Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:04:45 +0800 Subject: [PATCH 18/49] =?UTF-8?q?feat(driver):=20add=20CtFile=20(=E5=9F=8E?= =?UTF-8?q?=E9=80=9A=E7=BD=91=E7=9B=98)=20storage=20driver=20(#123)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于城通网盘官方 OpenAPI (https://openapi.ctfile.com/, rest.ctfile.com/v1) 实现存储驱动,响应 OpenList#1217 的城通网盘接入需求: - 支持公有云/私有云挂载(public/private 命名空间) - 目录浏览(folder/list + file/list 自动翻页与防死循环保护) - 下载直链(file/fetch_url;非 VIP 限速取决于账号等级) - 建目录、重命名(文件/文件夹)、删除(入回收站)、移动 - 上传(file/upload 预签名地址 + multipart 直传,md5-size 校验和可秒传) - Session 失效(401)转换为「开放接口登录密钥管理」重新生成的明确提示 - folder_id / file_id 前缀(d/f)在文档与社区实现间要求不一致, 调用层内置 raw/stripped 双形态自动回退;批量 ids 内置字符串/数组双形态回退 - /task/refresh 定时巡检复用 checkCookieValid,Session 过期在后台标记 - 驱动别名:ctfile / ctfilepan / 400gb / 城通网盘 说明:城通网盘已关闭服务器端密码登录(/v1/user/auth/login 恒返回 410, 网页密码登录要求扫码验证),因此驱动仅支持开放接口 Session 方式。 验证: - tsc --strict 全量类型检查通过(ctfile 目录及其依赖图) - node:test 单测 10/10 通过(鉴权、ID 形态回退、翻页、秒传校验和等) - 对 rest.ctfile.com 真实 API 冒烟:未配置/失效 Session 均返回可操作提示 - prettier 格式检查通过 Co-authored-by: MiMo (OpenCode) --- README.md | 2 +- src/backend/drivers/ctfile/driver.ts | 327 ++++++++++++++++ src/backend/drivers/ctfile/types.ts | 131 +++++++ src/backend/drivers/ctfile/util.test.ts | 228 ++++++++++++ src/backend/drivers/ctfile/util.ts | 474 ++++++++++++++++++++++++ src/backend/internal/op/storage.ts | 11 + src/backend/server/admin.ts | 64 ++++ src/backend/server/task.ts | 5 +- 8 files changed, 1239 insertions(+), 3 deletions(-) create mode 100644 src/backend/drivers/ctfile/driver.ts create mode 100644 src/backend/drivers/ctfile/types.ts create mode 100644 src/backend/drivers/ctfile/util.test.ts create mode 100644 src/backend/drivers/ctfile/util.ts diff --git a/README.md b/README.md index 68c587f4..9e9bda56 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea 内置 **78 个存储驱动**,开箱即用地挂载各类存储后端: -- **国内网盘**:阿里云盘(开放平台/分享)、夸克网盘(开放平台/UC TV 版)、百度网盘(相册)、115 网盘(开放平台/分享)、123 云盘(开放平台/分享)、天翼云盘(189/PC/TV)、中国移动云盘(139/和彩云)、沃家云盘、迅雷云盘、腾讯微云、蓝奏云、PikPak(分享)、豆包网盘、光亚盘、超星小组网盘、联想 NAS 分享、Teambition 网盘、WPS 网盘、阿里文档、HalalCloud、MediaTrack 等 +- **国内网盘**:阿里云盘(开放平台/分享)、夸克网盘(开放平台/UC TV 版)、百度网盘(相册)、115 网盘(开放平台/分享)、123 云盘(开放平台/分享)、天翼云盘(189/PC/TV)、中国移动云盘(139/和彩云)、沃家云盘、迅雷云盘、腾讯微云、蓝奏云、城通网盘、PikPak(分享)、豆包网盘、光亚盘、超星小组网盘、联想 NAS 分享、Teambition 网盘、WPS 网盘、阿里文档、HalalCloud、MediaTrack 等 - **国际网盘**:Google Drive(相册)、OneDrive(应用/分享链接)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox 等 - **对象存储**:S3 兼容(AWS/OSS/COS/MinIO 等)、又拍云 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 - **代码托管**:GitHub、GitHub Releases、CNB Releases diff --git a/src/backend/drivers/ctfile/driver.ts b/src/backend/drivers/ctfile/driver.ts new file mode 100644 index 00000000..42c91efb --- /dev/null +++ b/src/backend/drivers/ctfile/driver.ts @@ -0,0 +1,327 @@ +// CtFile (城通网盘) driver +// OpenAPI: https://openapi.ctfile.com/ — 统一 POST JSON + 开放接口 Session 鉴权 +import { + StorageDriver, + FileItem, + calcFileType, +} from "../../internal/driver/base" +import { sortFileItems } from "../../internal/driver/sort" +import { CtFileAddition, CtFileFile, CtFileFolder } from "./types" +import { CtFileClient, ROOT_FOLDER_ID } from "./util" + +const DEFAULT_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" + +export function normalizeCtFileAddition(a: any): CtFileAddition { + const norm = { ...(a || {}) } as any + norm.session = String(norm.session || norm.token || norm.api_key || "").trim() + norm.cloud_type = norm.cloud_type === "private" ? "private" : "public" + norm.base_url = norm.base_url || norm.baseUrl || "https://rest.ctfile.com/v1" + norm.order_by = norm.order_by || "name" + norm.order_direction = norm.order_direction || "asc" + norm.user_agent = norm.user_agent || DEFAULT_USER_AGENT + return norm as CtFileAddition +} + +/** + * 城通网盘时间字段兼容转换: + * - unix 秒(数字/纯数字字符串)→ ISO + * - "YYYY-MM-DD HH:mm:ss" 字符串 → ISO + * - 其他/缺失 → 当前时间 + */ +function ctfileDateToIso(date?: number | string): string { + if (date === undefined || date === null || date === "") { + return new Date().toISOString() + } + const raw = String(date).trim() + if (/^\d+$/.test(raw)) { + const n = Number(raw) + if (n > 0) { + // 超过 1e12 视为毫秒,否则按秒处理 + return new Date(n > 1e12 ? n : n * 1000).toISOString() + } + } + const parsed = new Date(raw.replace(" ", "T")) + if (!isNaN(parsed.getTime())) return parsed.toISOString() + return new Date().toISOString() +} + +/** folder/list 或 file/list 条目 → FileItem */ +function ctfileItemToFileItem( + item: CtFileFolder | CtFileFile, + isDir: boolean, +): FileItem { + const name = item.name || "" + const key = item.key || "" + const file = (isDir ? null : (item as CtFileFile)) || undefined + + return { + name, + size: isDir ? 0 : Number(file?.size || 0), + is_dir: isDir, + modified: ctfileDateToIso(item.date), + sign: key, + type: calcFileType(name, isDir), + thumb: !isDir && file?.imgsrc ? String(file.imgsrc) : "", + raw_url: "", + } +} + +/** 去掉路径段的 URL 编码(失败时返回原值) */ +function decodeSegment(raw: string): string { + try { + return decodeURIComponent(raw) + } catch { + return raw + } +} + +function splitPath(path: string): string[] { + return String(path || "") + .split("/") + .filter(Boolean) +} + +export class CtFileDriver implements StorageDriver { + private client: CtFileClient + private addition: CtFileAddition + /** 缓存:物理路径(/名称/名称)→ 文件夹 key(d 开头) */ + private pathKeyCache = new Map() + + constructor(addition: CtFileAddition) { + this.addition = normalizeCtFileAddition(addition) + this.client = new CtFileClient(this.addition) + } + + async init(): Promise { + // 启动即校验 Session,配置错误第一时间暴露 + await this.client.profile() + } + + /** + * 校验当前 Session 是否有效(供 /task/refresh 状态刷新调用)。 + * 有效返回 true;失效返回 false 并附重新生成 Session 的提示。 + */ + async checkCookieValid(): Promise<{ valid: boolean; error?: string }> { + return this.client.checkValid() + } + + private getRootId(): string { + return ROOT_FOLDER_ID + } + + /** + * 将物理路径解析为文件夹 key。 + * 路径段按「名称或 ID」匹配,逐级解析并写入缓存。 + */ + private async resolveFolderKey(physicalPath: string): Promise { + const segs = splitPath(physicalPath) + if (segs.length === 0) return this.getRootId() + + // 先走缓存找最长已知前缀 + let cachedLen = 0 + let parentKey = this.getRootId() + for (let i = 0; i < segs.length; i++) { + const p = "/" + segs.slice(0, i + 1).join("/") + const cached = this.pathKeyCache.get(p) + if (cached !== undefined) { + parentKey = cached + cachedLen = i + 1 + } else { + break + } + } + + for (let i = cachedLen; i < segs.length; i++) { + const rawName = segs[i] + const decodedName = decodeSegment(rawName) + const folders = await this.client.listFolders(parentKey) + + const folder = folders.find( + (f) => + f.name === rawName || + f.name === decodedName || + f.key === rawName || + f.key === decodedName, + ) + if (!folder) { + throw new Error(`[CtFile] 目录未找到: ${decodedName}`) + } + + parentKey = folder.key + this.pathKeyCache.set("/" + segs.slice(0, i + 1).join("/"), parentKey) + } + + return parentKey + } + + /** 解析具体文件或文件夹(路径最后一段为条目本身) */ + private async resolveEntry(physicalPath: string): Promise<{ + item: CtFileFolder | CtFileFile + parentKey: string + isDir: boolean + key: string + }> { + const segs = splitPath(physicalPath) + if (segs.length === 0) throw new Error("[CtFile] 路径无效") + + const rawName = segs[segs.length - 1] + const decodedName = decodeSegment(rawName) + const parentKey = await this.resolveFolderKey( + "/" + segs.slice(0, segs.length - 1).join("/"), + ) + + const folders = await this.client.listFolders(parentKey) + const folder = folders.find( + (f) => + f.name === rawName || + f.name === decodedName || + f.key === rawName || + f.key === decodedName, + ) + if (folder) { + this.pathKeyCache.set("/" + segs.join("/"), folder.key) + return { item: folder, parentKey, isDir: true, key: folder.key } + } + + const files = await this.client.listFiles(parentKey) + const file = files.find( + (f) => + f.name === rawName || + f.name === decodedName || + f.key === rawName || + f.key === decodedName, + ) + if (!file) { + throw new Error(`[CtFile] 文件或目录未找到: ${decodedName}`) + } + return { item: file, parentKey, isDir: false, key: file.key } + } + + async list(_virtualPath: string, physicalPath: string): Promise { + const folderKey = await this.resolveFolderKey(physicalPath) + const entries = await this.client.listAll(folderKey) + + const items = entries.map((entry) => { + const isDir = Boolean(entry.key && entry.key.startsWith("d")) + return ctfileItemToFileItem(entry, isDir) + }) + + return sortFileItems( + items, + this.addition.order_by === "size" + ? "size" + : this.addition.order_by === "time" + ? "time" + : "name", + this.addition.order_direction, + ) + } + + async get(_virtualPath: string, physicalPath: string): Promise { + const segs = splitPath(physicalPath) + if (segs.length === 0) { + return { + name: this.getRootId(), + size: 0, + is_dir: true, + modified: new Date().toISOString(), + sign: this.getRootId(), + type: 1, + raw_url: "", + } + } + + const { item, isDir, key } = await this.resolveEntry(physicalPath) + const fileItem = ctfileItemToFileItem(item, isDir) + if (isDir) return fileItem + + try { + fileItem.raw_url = await this.client.fetchDownloadUrl(key) + } catch (err: any) { + const msg = err?.message || String(err) + fileItem.raw_url_error = msg + throw new Error(`[CtFile] 获取下载直链失败 (${item.name}): ${msg}`) + } + + fileItem.raw_url_headers = { + "User-Agent": this.client.getUserAgent(), + } + return fileItem + } + + async mkdir(_virtualPath: string, physicalPath: string): Promise { + const segs = splitPath(physicalPath) + if (segs.length === 0) throw new Error("[CtFile] 路径无效") + const dirName = decodeSegment(segs[segs.length - 1]) + const parentPath = "/" + segs.slice(0, segs.length - 1).join("/") + const parentKey = await this.resolveFolderKey(parentPath) + await this.client.mkdir(parentKey, dirName) + } + + async rename( + _virtualPath: string, + physicalPath: string, + newName: string, + ): Promise { + const { isDir, key } = await this.resolveEntry(physicalPath) + if (isDir) { + await this.client.renameFolder(key, newName) + // 文件夹改名后缓存的路径映射全部失效 + this.pathKeyCache.clear() + return + } + await this.client.renameFile(key, newName) + } + + async remove( + _virtualPath: string, + physicalPath: string, + _names: string[], + ): Promise { + const { isDir, key } = await this.resolveEntry(physicalPath) + await this.client.remove([key]) + if (isDir) this.pathKeyCache.clear() + } + + async move( + _srcDir: string, + dstDir: string, + _names: string[], + srcPhysical: string, + _dstPhysical: string, + ): Promise { + const { isDir, key } = await this.resolveEntry(srcPhysical) + const dstKey = await this.resolveFolderKey(dstDir) + await this.client.move([key], dstKey) + if (isDir) this.pathKeyCache.clear() + } + + async copy(): Promise { + throw new Error("[CtFile] 城通网盘不支持直接复制文件") + } + + async put( + _virtualPath: string, + physicalPath: string, + content: Buffer, + ): Promise { + const segs = splitPath(physicalPath) + if (segs.length === 0) throw new Error("[CtFile] 路径无效") + const fileName = decodeSegment(segs[segs.length - 1]) + const parentPath = "/" + segs.slice(0, segs.length - 1).join("/") + const parentKey = await this.resolveFolderKey(parentPath) + + const data = new Uint8Array(content) + const ticket = await this.client.uploadTicket(parentKey, fileName, data) + if (Number(ticket.exists || 0) > 0) { + // 秒传命中:文件已存在,无需真正上传 + return + } + const uploadUrl = String(ticket.upload_url || "") + if (!uploadUrl) { + throw new Error("[CtFile] 获取上传地址失败:服务端未返回 upload_url") + } + await this.client.upload(uploadUrl, fileName, data) + } +} diff --git a/src/backend/drivers/ctfile/types.ts b/src/backend/drivers/ctfile/types.ts new file mode 100644 index 00000000..71ac1848 --- /dev/null +++ b/src/backend/drivers/ctfile/types.ts @@ -0,0 +1,131 @@ +// CtFile (城通网盘) driver types +// Based on: https://openapi.ctfile.com/ (CTFile REST API v1) +// 站点: https://www.ctfile.com / https://web.ctfile.com + +export interface CtFileAddition { + /** + * 开放接口 Session Token。 + * 获取方式:登录城通网盘管理后台 →「开放接口登录密钥管理」生成, + * 或通过 openapi 文档中的 /v1/user/auth/login 登录接口获取。 + */ + session?: string + + /** 云类型:public=公有云(默认)、private=私有云 */ + cloud_type?: "public" | "private" + + /** API 基础地址,默认 https://rest.ctfile.com/v1 */ + base_url?: string + + /** 列表排序字段:name=名称、size=大小、time=修改时间 */ + order_by?: "name" | "size" | "time" + + /** 列表排序方向 */ + order_direction?: "asc" | "desc" + + /** 请求头 User-Agent(上传/下载直链均使用) */ + user_agent?: string +} + +/** OpenAPI 统一响应包装:{ code, message?, ...业务字段 } */ +export interface CtFileResp { + code?: number + message?: string + error_code?: string + results?: T + [key: string]: any +} + +/** POST /v1/user/info/profile 成功响应(业务字段平铺在根级) */ +export interface CtFileProfile { + userid?: number | string + username?: string + nick_name?: string + group_type?: number + group_name?: string + is_vip?: number | boolean + reg_time?: string | number + max_filesize?: number +} + +/** POST /v1/{ns}/folder/list 单个文件夹 */ +export interface CtFileFolder { + /** 文件夹 ID,形如 "d123456" */ + key: string + icon?: string + name: string + /** 修改时间:unix 秒 或 "YYYY-MM-DD HH:mm:ss" */ + date?: number | string +} + +/** POST /v1/{ns}/file/list 单个文件 */ +export interface CtFileFile { + /** 文件 ID,形如 "f478659195" */ + key: string + icon?: string + /** 缩略图地址(图片/视频/音频) */ + imgsrc?: string + name: string + /** 字节数 */ + size?: number + /** 上传时间:unix 秒 或 "YYYY-MM-DD HH:mm:ss" */ + date?: number | string + /** 1=完整 2=未完成(未完成的文件不支持下载) */ + status?: number +} + +/** POST /v1/{ns}/folder/meta 成功响应 */ +export interface CtFileFolderMeta { + key?: string + key_id?: number | string + name?: string + description?: string + path?: string + date?: number | string + parent_folder_id?: number | string + folder_id?: string +} + +/** POST /v1/{ns}/file/meta 成功响应 */ +export interface CtFileFileMeta { + key?: string + file_id?: number | string + name?: string + description?: string + size?: number + date?: number | string + folder_id?: string + path?: string + is_locked?: number +} + +/** POST /v1/{ns}/folder/create 成功响应 */ +export interface CtFileFolderCreateResult { + folder_id?: string + folder_path?: string + path?: string +} + +/** POST /v1/{ns}/file/fetch_url 成功响应 */ +export interface CtFileDownloadUrl { + download_url?: string +} + +/** POST /v1/{ns}/file/upload 成功响应(获取上传凭据) */ +export interface CtFileUploadTicket { + upload_url?: string + /** >0 表示秒传命中(文件已存在,无需真正上传) */ + exists?: number +} + +/** POST /v1/{ns}/file/upload 上传完成后的返回(2025.7 起新版结构) */ +export interface CtFileUploadResult { + code?: number + message?: string + id?: number + name?: string + size?: number + time?: number + userid?: number + folderid?: number + workspaceid?: number +} diff --git a/src/backend/drivers/ctfile/util.test.ts b/src/backend/drivers/ctfile/util.test.ts new file mode 100644 index 00000000..83b567c7 --- /dev/null +++ b/src/backend/drivers/ctfile/util.test.ts @@ -0,0 +1,228 @@ +import assert from "node:assert/strict" +import { test } from "node:test" + +import { CtFileClient } from "./util" +import { normalizeCtFileAddition } from "./driver" + +interface CapturedCall { + url: string + body: any +} + +/** + * 用脚本化的 fetch stub 替换全局 fetch, + * 按调用顺序返回预设响应,并捕获请求体用于断言。 + */ +function stubFetch( + responses: Array<{ + status?: number + body?: any + /** 未显式给出 body 时,其余字段整体作为响应体 */ + [key: string]: any + }>, +): { calls: CapturedCall[]; restore: () => void } { + const original = globalThis.fetch + const calls: CapturedCall[] = [] + let index = 0 + + globalThis.fetch = (async (input: any, init?: any) => { + const payload = (() => { + try { + return init?.body ? JSON.parse(String(init.body)) : {} + } catch { + return {} + } + })() + calls.push({ url: String(input), body: payload }) + + const scripted = responses[Math.min(index, responses.length - 1)] + index++ + const status = scripted.status ?? 200 + const body = + scripted.body !== undefined + ? scripted.body + : // 未显式给出 body 时,脚本对象的其余字段整体作为响应体 + Object.fromEntries( + Object.entries(scripted).filter(([k]) => k !== "status"), + ) + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }) + }) as any + + return { calls, restore: () => (globalThis.fetch = original) } +} + +test("session 缺失时给出明确提示", async () => { + const client = new CtFileClient({} as any) + await assert.rejects(() => client.listFolders("d0"), /未配置 Session/) +}) + +test("401 响应转换为 Session 失效提示", async () => { + const stub = stubFetch([{ code: 401, message: "未登录" }]) + try { + const client = new CtFileClient({ session: "expired-token" } as any) + await assert.rejects( + () => client.profile(), + /Session 已失效(未登录).*开放接口登录密钥管理/, + ) + assert.equal(stub.calls.length, 1) + assert.equal(stub.calls[0].body.session, "expired-token") + } finally { + stub.restore() + } +}) + +test("folder/meta ID 形态回退:400 后自动去掉 d 前缀重试", async () => { + const stub = stubFetch([ + { code: 400, message: "资源不存在" }, + { code: 200, key: "d123", name: "相册" }, + ]) + try { + const client = new CtFileClient({ session: "s" } as any) + const meta = await client.folderMeta("d123") + assert.equal(meta.name, "相册") + assert.equal(stub.calls.length, 2) + assert.equal(stub.calls[0].body.folder_id, "d123") + assert.equal(stub.calls[1].body.folder_id, "123") + assert.equal( + stub.calls[1].url, + "https://rest.ctfile.com/v1/public/folder/meta", + ) + } finally { + stub.restore() + } +}) + +test("fetch_url 按文档优先去前缀,400 后回退带前缀", async () => { + const stub = stubFetch([ + { code: 400, message: "参数错误" }, + { code: 200, download_url: "https://download.ctfile.com/getfile.php?x=1" }, + ]) + try { + const client = new CtFileClient({ session: "s" } as any) + const url = await client.fetchDownloadUrl("f478659195") + assert.match(url, /^https:\/\/download\.ctfile\.com\//) + assert.equal(stub.calls[0].body.file_id, "478659195") + assert.equal(stub.calls[1].body.file_id, "f478659195") + } finally { + stub.restore() + } +}) + +test("file/list 自动翻页并在空页终止", async () => { + const stub = stubFetch([ + { + code: 200, + results: [ + { key: "f1", name: "a.txt", size: 1 }, + { key: "f2", name: "b.txt", size: 2 }, + ], + }, + { code: 200, results: [] }, + ]) + try { + const client = new CtFileClient({ session: "s" } as any) + const files = await client.listFiles("d0") + assert.equal(files.length, 2) + assert.equal(files[0].key, "f1") + assert.equal(stub.calls.length, 2) + assert.equal(stub.calls[0].body.start, 0) + assert.equal(stub.calls[1].body.start, 2) + assert.equal(stub.calls[0].body.folder_id, "d0") + } finally { + stub.restore() + } +}) + +test("file/list 服务端忽略 start 时靠重复首项终止(防死循环)", async () => { + const page = { + code: 200, + results: [ + { key: "f1", name: "a.txt", size: 1 }, + { key: "f2", name: "b.txt", size: 2 }, + ], + } + const stub = stubFetch([page, page, page, page]) + try { + const client = new CtFileClient({ session: "s" } as any) + const files = await client.listFiles("d0") + assert.equal(files.length, 2) + assert.equal(stub.calls.length, 2) + } finally { + stub.restore() + } +}) + +test("delete ids 先逗号字符串,400 后回退数组", async () => { + const stub = stubFetch([ + { code: 400, message: "参数错误" }, + { code: 200, message: "文件已删除" }, + ]) + try { + const client = new CtFileClient({ session: "s" } as any) + await client.remove(["f1", "f2"]) + assert.equal(stub.calls.length, 2) + assert.equal(stub.calls[0].body.ids, "f1,f2") + assert.deepEqual(stub.calls[1].body.ids, ["f1", "f2"]) + } finally { + stub.restore() + } +}) + +test("private 云走 /v1/private/ 命名空间", async () => { + const stub = stubFetch([{ code: 200, results: [] }]) + try { + const client = new CtFileClient({ + session: "s", + cloud_type: "private", + } as any) + await client.listFolders("d0") + assert.match(stub.calls[0].url, /\/v1\/private\/folder\/list$/) + } finally { + stub.restore() + } +}) + +test("uploadTicket 生成 md5-size 校验和", async () => { + const stub = stubFetch([ + { code: 200, upload_url: "https://upload.ctfile.com/web/upload.do?key=k" }, + ]) + try { + const client = new CtFileClient({ session: "s" } as any) + const content = new TextEncoder().encode("hello") + const ticket = await client.uploadTicket("d0", "hello.txt", content) + assert.match(String(ticket.upload_url), /^https:\/\/upload\.ctfile\.com\//) + // md5("hello") = 5d41402abc4b2a76b9719d911017c592,长度 5 + assert.equal( + stub.calls[0].body.checksum, + "5d41402abc4b2a76b9719d911017c592-5", + ) + assert.equal(stub.calls[0].body.size, 5) + assert.equal(stub.calls[0].body.name, "hello.txt") + } finally { + stub.restore() + } +}) + +test("normalizeCtFileAddition 填充默认值", () => { + const norm = normalizeCtFileAddition({}) + assert.equal(norm.session, "") + assert.equal(norm.cloud_type, "public") + assert.equal(norm.base_url, "https://rest.ctfile.com/v1") + assert.equal(norm.order_by, "name") + assert.equal(norm.order_direction, "asc") + assert.match(norm.user_agent || "", /Mozilla\/5\.0/) + + const custom = normalizeCtFileAddition({ + session: " abc ", + cloud_type: "private", + order_by: "size", + order_direction: "desc", + }) + assert.equal(custom.session, "abc") + assert.equal(custom.cloud_type, "private") + assert.equal(custom.order_by, "size") + assert.equal(custom.order_direction, "desc") +}) diff --git a/src/backend/drivers/ctfile/util.ts b/src/backend/drivers/ctfile/util.ts new file mode 100644 index 00000000..6b2ceb1b --- /dev/null +++ b/src/backend/drivers/ctfile/util.ts @@ -0,0 +1,474 @@ +// CtFile (城通网盘) OpenAPI 客户端 +// 文档: https://openapi.ctfile.com/ (CTFile REST API v1, 统一 POST JSON + session 鉴权) +import { md5 } from "../../pkg/crypto" +import { + CtFileAddition, + CtFileDownloadUrl, + CtFileFile, + CtFileFileMeta, + CtFileFolder, + CtFileFolderCreateResult, + CtFileFolderMeta, + CtFileProfile, + CtFileResp, + CtFileUploadResult, + CtFileUploadTicket, +} from "./types" + +/** 根目录 ID(官方 OpenAPI 示例使用 "d0";部分接口也接受 "0") */ +export const ROOT_FOLDER_ID = "d0" + +const DEFAULT_BASE_URL = "https://rest.ctfile.com/v1" +const DEFAULT_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" + +/** Session 失效统一提示 */ +const SESSION_EXPIRED_MSG = + "[CtFile] Session 已失效(未登录),请在城通网盘管理后台「开放接口登录密钥管理」重新生成 Session 并更新存储配置" + +/** file/list 单轮分页上限(防止服务端异常导致无限翻页) */ +const MAX_LIST_PAGES = 500 + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +/** + * 去掉 ID 的 d/f 前缀(如 "d123"→"123"、"f456"→"456")。 + * 仅当前缀后紧跟数字时才剥离,避免误伤纯字母 ID。 + */ +function stripIdPrefix(id: string): string { + return String(id || "").replace(/^([df])(?=\d)/, "") +} + +interface CallOptions { + /** + * ID 首选形态: + * - raw:按原样传(带前缀),失败再去前缀重试。folder/list、delete、move 等按社区实现验证的接口使用。 + * - stripped:按文档「不带 d/f 前缀」优先,失败再带前缀重试。meta、fetch_url、modify_meta 等文档明确要求去前缀的接口使用。 + */ + prefer?: "raw" | "stripped" +} + +export class CtFileClient { + private addition: CtFileAddition + + constructor(addition: CtFileAddition) { + this.addition = addition + } + + public getSession(): string { + return String(this.addition.session || "").trim() + } + + public getBaseUrl(): string { + return ( + this.addition.base_url || + (this.addition as any).baseUrl || + DEFAULT_BASE_URL + ).replace(/\/+$/, "") + } + + public getUserAgent(): string { + return this.addition.user_agent || DEFAULT_USER_AGENT + } + + /** 公有云 / 私有云命名空间 */ + private ns(): string { + return this.addition.cloud_type === "private" ? "private" : "public" + } + + /** + * 传输层 POST:JSON 请求体 + session 字段。 + * 网络错误 / 429 / 5xx 自动退避重试;401 直接抛 Session 失效提示。 + * 业务错误码(code !== 200)不在此处抛出,交由 call() 处理(便于 ID 形态回退)。 + */ + private async post( + path: string, + body: Record, + ): Promise { + const session = this.getSession() + if (!session) { + throw new Error( + "[CtFile] 未配置 Session,请在城通网盘管理后台「开放接口登录密钥管理」生成后填入存储配置", + ) + } + + const url = `${this.getBaseUrl()}${path}` + const payload = JSON.stringify({ ...body, session }) + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json", + "User-Agent": this.getUserAgent(), + } + + let lastError = "" + for (let retry = 0; retry < 3; retry++) { + if (retry > 0) await sleep(300 * retry) + + let res: Response + try { + res = await fetch(url, { method: "POST", headers, body: payload }) + } catch (e: any) { + lastError = e?.message || String(e) + continue + } + + if (res.status === 429 || res.status >= 500) { + lastError = `HTTP ${res.status}` + continue + } + + const text = await res.text() + let data: CtFileResp + try { + data = JSON.parse(text) as CtFileResp + } catch { + throw new Error( + `[CtFile] ${path} 返回非 JSON 响应 (HTTP ${res.status}): ${text.slice(0, 200)}`, + ) + } + + const code = Number(data?.code) + if (code === 401) throw new Error(SESSION_EXPIRED_MSG) + if (code === 429) { + lastError = data?.message || "429" + continue + } + return data + } + + throw new Error(`[CtFile] ${path} 请求失败(已重试): ${lastError}`) + } + + /** 业务层调用:HTTP/JSON 层成功后校验 code === 200,否则抛带 code 的错误 */ + private async call( + path: string, + body: Record, + ): Promise { + const data = await this.post(path, body) + const code = Number(data?.code) + if (code === 200 || data?.code === undefined) return data + const err: any = new Error( + `[CtFile] ${path} 失败 (code=${code}): ${data?.message || "未知错误"}`, + ) + err.code = code + err.original = data + throw err + } + + /** + * 带 ID 形态回退的调用。 + * 城通 OpenAPI 对 folder_id / file_id 是否携带 d/f 前缀的要求在文档与社区实现间不一致, + * 先按 prefer 形态请求,遇到 400/404(参数错误/资源不存在)时自动换另一种形态重试一次。 + */ + private async callWithId( + path: string, + idKey: string, + idValue: string, + body: Record, + options: CallOptions = {}, + ): Promise { + const stripped = stripIdPrefix(idValue) + const primary = options.prefer === "stripped" ? stripped : idValue + const secondary = options.prefer === "stripped" ? idValue : stripped + const attempts = primary === secondary ? [primary] : [primary, secondary] + + let lastErr: any + for (const value of attempts) { + try { + return await this.call(path, { ...body, [idKey]: value }) + } catch (e: any) { + if (e?.code === 400 || e?.code === 404) { + lastErr = e + continue + } + throw e + } + } + throw lastErr + } + + /** + * ids 参数(批量文件/文件夹 ID)形态回退: + * 文档写逗号分隔字符串,社区实现传 JSON 数组,两者皆尝试。 + */ + private async callWithIds( + path: string, + ids: string[], + body: Record, + ): Promise { + let lastErr: any + for (const value of [ids.join(","), ids]) { + try { + return await this.call(path, { ...body, ids: value }) + } catch (e: any) { + if (e?.code === 400 || e?.code === 404 || e?.code === 405) { + lastErr = e + continue + } + throw e + } + } + throw lastErr + } + + /** 校验 Session 并返回用户资料(init / 健康检查使用) */ + async profile(): Promise { + const data = await this.call("/user/info/profile", {}) + return data as CtFileProfile + } + + /** 主动校验 Session 是否有效(供 /task/refresh 巡检调用) */ + async checkValid(): Promise<{ valid: boolean; error?: string }> { + try { + await this.profile() + return { valid: true } + } catch (err: any) { + const msg = err?.message || String(err) + return { + valid: false, + error: /未登录|401|Session 已失效/.test(msg) + ? SESSION_EXPIRED_MSG + : msg, + } + } + } + + /** 列出文件夹下的子文件夹 */ + async listFolders(folderId: string): Promise { + const data = await this.call( + `/${this.ns()}/folder/list`, + this.withOrder({ folder_id: folderId || ROOT_FOLDER_ID }), + ) + const list = Array.isArray(data?.results) ? data.results : [] + return list.map((item: any) => ({ + ...item, + key: String(item?.key ?? item?.folder_id ?? ""), + name: String(item?.name ?? ""), + })) + } + + /** + * 列出文件夹下的所有文件(自动翻页)。 + * 分页参数:start 为偏移量;空页 / 重复页 / 达到上限即停止。 + */ + async listFiles(folderId: string): Promise { + const all: CtFileFile[] = [] + let start = 0 + let prevFirstKey = "" + + for (let page = 0; page < MAX_LIST_PAGES; page++) { + const data = await this.call( + `/${this.ns()}/file/list`, + this.withOrder({ + folder_id: folderId || ROOT_FOLDER_ID, + start, + }), + ) + const results: any[] = Array.isArray(data?.results) ? data.results : [] + if (results.length === 0) break + + // 服务端忽略 start 时会反复返回同一页:检测首项重复防止死循环 + const firstKey = String(results[0]?.key ?? "") + if (page > 0 && firstKey === prevFirstKey) break + prevFirstKey = firstKey + + all.push( + ...results.map((item: any) => ({ + ...item, + key: String(item?.key ?? item?.file_id ?? ""), + name: String(item?.name ?? ""), + size: Number(item?.size || 0), + })), + ) + start += results.length + } + + return all + } + + /** folder/list + file/list 合并(目录内全部条目) */ + async listAll(folderId: string): Promise> { + const [folders, files] = await Promise.all([ + this.listFolders(folderId), + this.listFiles(folderId), + ]) + return [...folders, ...files] + } + + /** 文件夹元数据 */ + async folderMeta(folderId: string): Promise { + const data = await this.callWithId( + `/${this.ns()}/folder/meta`, + "folder_id", + folderId, + {}, + { prefer: "raw" }, + ) + return data as CtFileFolderMeta + } + + /** 文件元数据 */ + async fileMeta(fileId: string): Promise { + const data = await this.callWithId( + `/${this.ns()}/file/meta`, + "file_id", + fileId, + {}, + { prefer: "stripped" }, + ) + return data as CtFileFileMeta + } + + /** 新建文件夹,返回新文件夹 ID(形如 "d123") */ + async mkdir(parentId: string, name: string): Promise { + try { + const data = await this.call(`/${this.ns()}/folder/create`, { + name, + folder_id: parentId || ROOT_FOLDER_ID, + description: "", + is_hidden: false, + }) + return String( + (data as CtFileFolderCreateResult).folder_id || + (data as CtFileFolderCreateResult).folder_path || + "", + ) + } catch (err: any) { + // 重名时服务端返回 403,但附带已存在的 folder_id:视为成功 + if (err?.code === 403) { + const existing = err?.original?.folder_id + if (existing) return String(existing) + } + throw err + } + } + + /** 重命名文件 */ + async renameFile(fileId: string, newName: string): Promise { + await this.callWithId( + `/${this.ns()}/file/modify_meta`, + "file_id", + fileId, + { name: newName }, + { prefer: "stripped" }, + ) + } + + /** 重命名文件夹 */ + async renameFolder(folderId: string, newName: string): Promise { + await this.callWithId( + `/${this.ns()}/folder/modify_meta`, + "folder_id", + folderId, + { name: newName, is_rename: true }, + { prefer: "raw" }, + ) + } + + /** 删除文件/文件夹(移入回收站) */ + async remove(ids: string[]): Promise { + if (ids.length === 0) return + await this.callWithIds(`/${this.ns()}/file/delete`, ids, {}) + } + + /** 移动文件/文件夹到目标文件夹 */ + async move(ids: string[], targetFolderId: string): Promise { + if (ids.length === 0) return + await this.callWithIds(`/${this.ns()}/file/move`, ids, { + folder_id: targetFolderId || ROOT_FOLDER_ID, + }) + } + + /** 获取文件下载直链 */ + async fetchDownloadUrl(fileId: string): Promise { + const data = await this.callWithId( + `/${this.ns()}/file/fetch_url`, + "file_id", + fileId, + {}, + { prefer: "stripped" }, + ) + const url = String((data as CtFileDownloadUrl).download_url || "") + if (!url) { + throw new Error( + `[CtFile] 获取下载直链失败: ${data?.message || "返回为空"}`, + ) + } + return url + } + + /** + * 获取上传凭据(预签名 URL)。 + * checksum 格式为 `md5-大小`,用于服务端秒传比对。 + */ + async uploadTicket( + folderId: string, + fileName: string, + content: Uint8Array, + ): Promise { + const checksum = `${md5(content)}-${content.byteLength}` + const data = await this.call(`/${this.ns()}/file/upload`, { + folder_id: folderId || ROOT_FOLDER_ID, + checksum, + size: content.byteLength, + name: fileName, + }) + return data as CtFileUploadTicket + } + + /** 将文件内容 POST 到预签名上传地址(multipart/form-data) */ + async upload( + uploadUrl: string, + fileName: string, + content: Uint8Array, + ): Promise { + const form = new FormData() + form.append("name", fileName) + form.append("filesize", String(content.byteLength)) + form.append("file", new Blob([content as BlobPart]), fileName) + + const res = await fetch(uploadUrl, { + method: "POST", + headers: { "User-Agent": this.getUserAgent() }, + body: form, + }) + + const text = await res.text() + let data: CtFileUploadResult + try { + data = JSON.parse(text) as CtFileUploadResult + } catch { + throw new Error( + `[CtFile] 上传响应异常 (HTTP ${res.status}): ${text.slice(0, 200)}`, + ) + } + if (Number(data?.code) !== 200) { + throw new Error( + `[CtFile] 上传失败 (code=${data?.code}): ${data?.message || text.slice(0, 200)}`, + ) + } + return data + } + + /** 附加服务端排序参数(new/old/az/za/big/small) */ + private withOrder(body: Record): Record { + const map: Record = { + name: "az", + size: "big", + time: "new", + } + const base = map[this.addition.order_by || "name"] || "az" + const direction = this.addition.order_direction === "desc" + const order: Record = { + az: "za", + za: "az", + big: "small", + small: "big", + new: "old", + old: "new", + } + return { ...body, orderby: direction ? order[base] || base : base } + } +} diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 96f9a568..6452d737 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -53,6 +53,7 @@ import { ThunderExpertDriver, } from "../../drivers/thunder/driver" import { LanzouDriver } from "../../drivers/lanzou/driver" +import { CtFileDriver } from "../../drivers/ctfile/driver" import { Cloud189Driver } from "../../drivers/189/driver" import { Driver189TV } from "../../drivers/189_tv/driver" import { WebdavDriver } from "../../drivers/webdav/driver" @@ -719,6 +720,16 @@ async function createDriver( } }) await driver.init?.() + } else if ( + normDriver === "ctfile" || + normDriver === "ctfilepan" || + normDriver === "ctfiledriver" || + normDriver === "400gb" || + normDriver.includes("ctfile") + ) { + const addition = parseAddition(storageConfig) + driver = new CtFileDriver(addition) + await driver.init?.() } else if ( normDriver === "189tv" || normDriver === "cloud189tv" || diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 2fb8dcf9..8b7f00ad 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -129,6 +129,8 @@ adminRouter.get("/storage/get", async (c) => { export const normalizeDriver = (driverName: string): string => { const norm = (driverName || "").toLowerCase().replace(/[^a-z0-9]/g, "") + // 纯中文驱动名无法归一化为 a-z0-9,先做中文别名匹配 + if ((driverName || "").includes("城通")) return "CtFile" if (!norm) return "" const available = Object.keys(driverConfigs) const matched = available.find( @@ -138,6 +140,7 @@ export const normalizeDriver = (driverName: string): string => { ) if (matched) return matched // Common OpenList aliases + if (norm.includes("ctfile") || norm === "400gb") return "CtFile" if (norm.startsWith("115")) return "115Open" if (norm.startsWith("123")) return "123Pan" if (norm.includes("aliyun")) return "AliyundriveOpen" @@ -2017,6 +2020,67 @@ const driverConfigs: Record = { default_root: "-1", }, }, + CtFile: { + name: "CtFile", + default_mount_path: "/ctfile", + common: COMMON_FIELDS, + additional: [ + { + name: "session", + type: "text", + default: "", + required: true, + help: "开放接口 Session:登录城通网盘管理后台 →「开放接口登录密钥管理」生成;也可参考 openapi.ctfile.com 通过登录接口获取", + }, + { + name: "cloud_type", + type: "select", + options: "public,private", + default: "public", + required: false, + help: "挂载的云类型:public=公有云(默认)、private=私有云", + }, + { + name: "base_url", + type: "string", + default: "https://rest.ctfile.com/v1", + required: false, + help: "OpenAPI 基础地址", + }, + { + name: "order_by", + type: "select", + options: "name,size,time", + default: "name", + required: false, + }, + { + name: "order_direction", + type: "select", + options: "asc,desc", + default: "asc", + required: false, + }, + { + name: "user_agent", + type: "string", + default: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", + required: true, + help: "请求城通网盘 API 与下载直链时携带的 User-Agent", + }, + ], + config: { + name: "CtFile", + local_sort: true, + only_local: false, + only_proxy: false, + no_cache: false, + no_upload: false, + need_ms: false, + default_root: "", + }, + }, WebDav: { name: "WebDav", default_mount_path: "/webdav", diff --git a/src/backend/server/task.ts b/src/backend/server/task.ts index 3e0d34a3..6e0c51ad 100644 --- a/src/backend/server/task.ts +++ b/src/backend/server/task.ts @@ -27,11 +27,12 @@ taskRouter.all( try { const driver = await getDriver(s.driver, s) await driver.init?.() - // lanzou 驱动:主动校验 Cookie 是否过期(有效期约 15 天), + // lanzou / ctfile 驱动:主动校验凭证是否过期 + // (lanzou Cookie 约 15 天;ctfile 开放接口 Session 失效返回 401), // 失效时标记 status=cookie_expired,管理后台可直接看到提示 if ( typeof (driver as any).checkCookieValid === "function" && - /lanzou/i.test(String(s.driver)) + /(lanzou|ctfile|400gb)/i.test(String(s.driver)) ) { const check = await (driver as any).checkCookieValid() if (!check.valid) { From 661a0be987121fccd2189e4d10a20259ed41662d Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Fri, 16 Oct 2026 20:48:03 +0800 Subject: [PATCH 19/49] feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API) (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(drivers): add Cloudflare R2 driver (Workers binding + S3 API) 新增独立 R2 驱动,同时支持两种接入方式(binding 优先,自动回退): - Workers R2 bucket binding 直连:无需 API Token,绑定名可在存储配置指定(bucket_binding),未指定时按 R2/R2_BUCKET/CF_R2/OPENLIST_R2 顺序探测;驱动实例跨请求复用,绑定在每次操作时从请求级 env 上下文(getEnvCtx)重新解析,避免绑定对象随 isolate 滞留。 - S3 兼容 API:account_id 自动推导 endpoint(https://.r2.cloudflarestorage.com),或显式 endpoint;复用 S3Driver 的签名/列表/预签名实现,R2 固定 region=auto。 - 目录语义与 S3 对齐(delimiter 列目录、占位对象、root_folder_path 前缀、递归 copy/move/remove、批量删除)。 - 下载链路:binding 模式返回空 raw_url 触发服务端流式下载(createReadStream,Range 换算 R2 的 offset/length);配 custom_host(r2.dev/自定义域名)时改走公开直链。binding 不提供预签名直链(Workers 绑定无该能力),直传仅在 api 模式提供并给出明确指引。 - 注册与配置:storage.ts 路由 r2/cloudflarer2/cfr2/r2native;admin driverConfigs 新增 R2 表单并让 r2 别名指向新驱动(历史 driver=S3 的 R2 配置不受影响);wrangler.jsonc 补 r2_buckets 绑定说明;README 驱动清单更新。 - 测试:9 个用例覆盖绑定解析、cursor 翻页、root 前缀、mkdir/remove/rename/move/copy、Range 流式读取、api 回退与路由别名(内存版 R2Bucket mock)。 * docs(readme): sync the R2 driver entry to all localized READMEs 主 README 的驱动清单新增了 Cloudflare R2,本次把同一句同步到 readmes/ 下全部 12 个语言版本(ar/de/en/es/fr/hi/it/ja/ko/pt/ru/zh-TW),并按各文件既有的标点惯例处理(中日韩印用全角括号与「、」,其余用半角括号与逗号,阿拉伯语用「،」)。 * docs(readme): slim config sections, link to the docs site, fix stale facts - README 与 12 个语言版本:把安装向导、JWT_SECRET、DB_* / CACHE_* / SINGLEFLIGHT 等大段配置说明压缩为表格 + doc.oplist.org 链接,正文只保留默认可用路径与建议显式配置的项 - 顺带修正过时描述:前端为 SolidJS + Hope UI(原写 React 19 + Ant Design)、运行平台补阿里云 ESA、数据存储口径改为 D1/KV/Blob/DO 与外部 MySQL - .env.example / .dev.vars.example:缓存配置说明由「详见 README」改为指向文档站 * refactor(drivers): name the R2 driver "Cloudflare R2" and fix name-based lookups - 驱动名由 R2 改为 Cloudflare R2(与既有的 GitHub Releases 等带空格命名一致); normalizeDriver 把 R2 / r2 / cloudflare_r2 / cfr2 全部归一到它。 本分支可能已存在的 driver=R2 记录会在下次保存时自动改名,运行时两种写法 都能命中新驱动。 - 修 server/fs.ts 的直传工具判定:改为按「归一化后的驱动名」匹配(小写 + 去非 字母数字)。原实现拿原始字符串做 ^ 锚定正则,驱动名带空格后会漏判,导致 api 模式下 s3_presigned 直传入口消失。 - R2 的直传工具只在 api 模式宣告:binding 无预签名能力,新增 r2CanSignDirectUpload()(判定与 R2Driver.resolveMode 一致), 不再向前端暴露必然报错的入口。 - 删除 op/storage.ts 的 S3 分支里已成死代码的 r2(已被上方 R2 分支接管)。 - 测试 +2:驱动名归一(含 S3 家族不被吞掉的回归护栏)、直传能力三态判定。 * chore(git): ignore the local-only wrangler.ui.jsonc config 该文件是本机部署配置(含真实 JWT_SECRET、KV namespace id 与 R2 绑定),与 .gitignore 中已有的 wrangler.local.jsonc / wrangler.d1.jsonc 属同类「本地专用配置、可能含真实密钥、禁止入库」条目。 --------- Co-authored-by: pikachuren --- .dev.vars.example | 3 +- .env.example | 3 +- .gitignore | 1 + README.md | 290 ++---------- readmes/README_ar.md | 9 +- readmes/README_de.md | 9 +- readmes/README_en.md | 108 ++--- readmes/README_es.md | 9 +- readmes/README_fr.md | 9 +- readmes/README_hi.md | 9 +- readmes/README_it.md | 9 +- readmes/README_ja.md | 9 +- readmes/README_ko.md | 9 +- readmes/README_pt.md | 9 +- readmes/README_ru.md | 9 +- readmes/README_zh-TW.md | 9 +- src/backend/drivers/r2/driver.ts | 733 +++++++++++++++++++++++++++++ src/backend/drivers/r2/r2.test.ts | 522 ++++++++++++++++++++ src/backend/drivers/r2/types.ts | 106 +++++ src/backend/internal/model/db.ts | 11 + src/backend/internal/op/storage.ts | 15 +- src/backend/server/admin.ts | 134 +++++- src/backend/server/fs.ts | 26 +- wrangler.jsonc | 15 + 24 files changed, 1674 insertions(+), 392 deletions(-) create mode 100644 src/backend/drivers/r2/driver.ts create mode 100644 src/backend/drivers/r2/r2.test.ts create mode 100644 src/backend/drivers/r2/types.ts diff --git a/.dev.vars.example b/.dev.vars.example index f0cccc54..6b27e221 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -87,7 +87,8 @@ CF_API_KEY= # ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── # 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 # 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER -# (不会自动探测、不会自动回退)。详见 README「缓存配置」。 +# (不会自动探测、不会自动回退)。 +# 完整说明见:https://doc.oplist.org/ecosystem/official_worker/guide_env # # 总开关,默认 true # CACHE_ENABLED=true diff --git a/.env.example b/.env.example index 0c3ee2e4..ea96b104 100644 --- a/.env.example +++ b/.env.example @@ -85,7 +85,8 @@ CF_API_KEY= # ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── # 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 # 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER -# (不会自动探测、不会自动回退)。详见 README「缓存配置」。 +# (不会自动探测、不会自动回退)。 +# 完整说明见:https://doc.oplist.org/ecosystem/official_worker/guide_env # # 总开关,默认 true # CACHE_ENABLED=true diff --git a/.gitignore b/.gitignore index 6a43fe57..e2cf919d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ public_data # local-only dev config (may contain real secrets; never commit) wrangler.d1.toml wrangler.local.jsonc +wrangler.ui.jsonc # OS files Thumbs.db diff --git a/README.md b/README.md index 9e9bda56..b466ac7b 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,10 @@ > [!IMPORTANT] -> - 若Cloudflare提示`无法获取存储库内容`,则您需要先[Fork](https://github.com/OpenListTeam/OpenList-Worker/fork)本项目,再通过连接到Github仓库功能部署 -> - 部署完成后配置环境变量: **EdgeOne**:[国际站](https://console.edgeone.ai/makers) · [中国站](https://console.cloud.tencent.com/edgeone/makers);**Cloudflare**:[Worker 后台](https://dash.cloudflare.com/),环境变量: -> - `DB_FORMAT`: 数据存储格式:`map` (默认,整对象JSON) / `key` (分key存储) / `sql` (关系表,与Go后端一致) -> - `DB_DRIVER`: 数据库驱动:`auto` (默认,自动检测) / `blob` (EdgeOne Blob) / `cfkv` (CF KV API) / `kv` (KV binding) / `d1` (Cloudflare D1) / `mysql` -> - 其余可选变量参考**详细部署指南**:[Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - 若 Cloudflare 提示`无法获取存储库内容`,则您需要先 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) 本项目,再通过连接到 Github 仓库功能部署 +> - 部署时请填写 `JWT_SECRET`(≥16 字符的随机串,可用 `openssl rand -hex 32` 生成):它用于 JWT 签名与数据加密;未配置时会在初始化时自动生成并持久化 +> - 数据存储默认自动选择平台自带的持久化后端(`DB_DRIVER=auto`),无需配置;其余变量见 [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - 部署完成后首次访问站点,按**安装向导**在浏览器中设置管理员账号与密码即可完成初始化;如需跳过向导,可配置 `ADMIN_PASS` ## 功能简介 @@ -61,7 +60,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea - **国内网盘**:阿里云盘(开放平台/分享)、夸克网盘(开放平台/UC TV 版)、百度网盘(相册)、115 网盘(开放平台/分享)、123 云盘(开放平台/分享)、天翼云盘(189/PC/TV)、中国移动云盘(139/和彩云)、沃家云盘、迅雷云盘、腾讯微云、蓝奏云、城通网盘、PikPak(分享)、豆包网盘、光亚盘、超星小组网盘、联想 NAS 分享、Teambition 网盘、WPS 网盘、阿里文档、HalalCloud、MediaTrack 等 - **国际网盘**:Google Drive(相册)、OneDrive(应用/分享链接)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox 等 -- **对象存储**:S3 兼容(AWS/OSS/COS/MinIO 等)、又拍云 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 +- **对象存储**:S3 兼容(AWS/OSS/COS/MinIO 等)、Cloudflare R2(Workers Binding 直连或 S3 API)、又拍云 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 - **代码托管**:GitHub、GitHub Releases、CNB Releases - **网盘程序**:OpenList(分享)、AList V3、Cloudreve V3/V4、Kodbox(可道云)、Seafile、Teldrive、Febbox 等 - **其他驱动**:网易云音乐、Misskey、Emby、Cloudflare 图床等 @@ -77,7 +76,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea - **离线任务**:后台任务队列,支持批量操作与异步处理。 - **外部接口**:将聚合存储以 WebDAV 或 S3 兼容协议对外暴露,便于挂载到第三方工具。 - **MCP 服务**:提供 Model Context Protocol 端点,可被 AI 助手等客户端集成调用。 -- **并发去重(singleflight)**:同一时刻相同的目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用同一结果;一个部署存在多个 isolate,因此除进程内合并外,还会用数据库表 `x_singleflight` 做跨实例协调(详见[并发去重](#并发去重-singleflight))。 +- **并发去重**:同一时刻相同的目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用同一结果,避免无谓的限流与 CPU 消耗。 ### 权限管理 @@ -88,10 +87,9 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea ### 平台部署 -- **运行平台**:Cloudflare Workers、腾讯云 EdgeOne Makers、Vercel、Serverless 及 Node.js 容器环境。 -- **数据存储**:Cloudflare D1(SQLite)为主,同时支持 MySQL、MariaDB、PostgreSQL、SQL Server。 -- **持久缓存**:Cloudflare KV / EdgeOne Blob(可选),用于配置持久化与缓存。 -- **一键部署**:支持 EdgeOne、Cloudflare Workers 等平台的一键部署按钮+初始化。 +- **运行平台**:Cloudflare Workers、腾讯云 EdgeOne Makers、阿里云 ESA、Vercel / AWS Lambda 及 Node.js 容器环境。 +- **数据存储**:Cloudflare D1(SQLite)为主,也支持 KV / Blob / Durable Objects 与外部 MySQL / MariaDB。 +- **一键部署**:支持 EdgeOne、Cloudflare Workers 的一键部署按钮 + 安装向导。 --- @@ -108,7 +106,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea # 1. 安装依赖 pnpm install -# 2. 编辑 wrangler.jsonc / .env,配置 JWT_SECRET 与存储(KV/D1 在控制台绑定) +# 2. 按需配置 wrangler.jsonc 中的绑定与变量(JWT_SECRET 见 .dev.vars.example) # 3. 启动开发服务器(自动拉取官方前端并运行 Worker) pnpm run dev:unified @@ -120,10 +118,10 @@ pnpm run dev:worker ### 生产部署 ```bash -# 一键部署:确保 KV namespace 存在 → 拉取官方前端 → 部署到 Cloudflare Workers +# 一键部署:构建前端产物 → 部署到 Cloudflare Workers(KV 绑定由 wrangler 自动预配) pnpm run deploy -# 或直接部署 Worker(跳过 KV 检查与前端构建) +# 或直接部署 Worker(跳过前端构建) pnpm run deploy:worker ``` @@ -133,17 +131,16 @@ pnpm run deploy:worker ### 后端 -- **运行环境**:Cloudflare Workers(Edge Computing) +- **运行环境**:Cloudflare Workers / EdgeOne 云函数 / 阿里云 ESA(Edge Computing) - **Web 框架**:Hono.js -- **数据库**:Cloudflare D1(SQLite)/ 支持 MySQL、MariaDB、PostgreSQL、SQL Server -- **缓存**:Cloudflare KV(可选) +- **数据库**:Cloudflare D1(SQLite)/ KV / Blob / Durable Objects,或外部 MySQL / MariaDB - **语言**:TypeScript - **构建工具**:Wrangler、esbuild ### 前端 -- **框架**:React 19 + TypeScript -- **UI 库**:Ant Design / Material-UI +- **框架**:SolidJS + TypeScript(官方 [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI 库**:Hope UI - **构建工具**:Vite --- @@ -151,243 +148,34 @@ pnpm run deploy:worker ## 配置 -### 环境变量 - -#### 数据库配置 - -**DB_FORMAT**(数据存储格式) -- `map`(默认):整对象 JSON 格式,适用于 KV/Blob 等简单存储 -- `key`:分 key 存储格式,每个实体一条记录(如 `users_1`),避免大 JSON -- `sql`:关系数据库表格式,与 Go 后端完全一致,适用于 D1/MySQL - -**DB_DRIVER**(数据库驱动) -- `auto`(默认):自动检测可用驱动(优先级:mysql → d1 → kv → cfkv → blob → do) -- `blob`:EdgeOne Blob Storage(SDK)/ ESA Blob(binding) -- `cfkv`:Cloudflare KV REST API(需配置 `CF_ACCOUNT`、`CF_KV_UUID`、`CF_API_KEY`) -- `kv`:KV 存储(binding 名固定为 `KV`;EdgeOne Node 云函数自动走 HTTP 代理模式) -- `d1`:Cloudflare D1(SQLite) -- `do`:Cloudflare Durable Objects(SQLite) -- `mysql`:MySQL(仅 Node.js 容器) - -**DB_CIPHER**(敏感字段落盘算法,**默认不加密**) -- `none`(默认):不加密,敏感字段与普通 JSON 一样明文落盘 -- `aes-256-gcm`:HKDF-SHA256 派生一把 AES-256-GCM 密钥(`enc:v2:`) - —— 既有加密部署写入的形态,开销最低,开启加密时**推荐** -- `aes-256-gcm-pbkdf2`:AES-256-GCM,密钥由 PBKDF2-SHA256(10 万次迭代)逐字段派生 - (历史 `enc:v1:` envelope,抗弱口令但每次读写都很慢,仅建议用于兼容) -- `aes-256-cbc-hmac`:AES-256-CBC + HMAC-SHA256(`enc:v3:`,Encrypt-then-MAC) -- `chacha20-poly1305`:ChaCha20-Poly1305(`enc:v4:`,RFC 8439,纯 JS 实现; - WebCrypto 全平台都没有 ChaCha20,故自带实现并通过 RFC 官方向量验证) -- `des-cbc-hmac` / `3des-cbc-hmac`:DES / 3DES-CBC + HMAC-SHA256(`enc:v5:` / `enc:v6:`) - —— **仅用于兼容/互操作**:单 DES 有效密钥只有 56-bit(可被暴力破解),3DES 已被 - NIST SP 800-131A 弃用(64-bit 分组 + Sweet32)。选用时后端会打印一次性告警, - 请勿用它们保护真实数据。 -- 别名(大小写无关):`gcm`/`hkdf`/`v2`、`pbkdf2`/`v1`、`cbc`/`v3`、`chacha20`/`v4`、 - `des`/`v5`、`3des`/`tripledes`/`v6`、`off`/`plain`;无法识别时告警并回退 `none` - -**几种算法的 CPU 特性(实测,120 字节字段,Node 22)** - -| 算法 | 单字段耗时 | 说明 | -| :-- | --: | :-- | -| `aes-256-gcm` | ~30 µs | WebCrypto 原生(调用开销为主) | -| `chacha20-poly1305` | ~18 µs | 纯 JS,小字段下反而更快 | -| `aes-256-cbc-hmac` | ~60 µs | 两次 WebCrypto 调用(加密 + HMAC) | -| `des-cbc-hmac` / `3des-cbc-hmac` | ~0.26 ms | 纯 JS(crypto-js) | -| `aes-256-gcm-pbkdf2` | ~27 ms | 每字段 10 万次 PBKDF2(历史包袱) | - -为此后端内置两项优化(对功能无影响):① 密钥派生结果在**进程内缓存**,同一 -isolate 只派生一次;② **未变化字段跳过重新加密** —— 明文、算法、密钥都没变时 -直接复用上次的密文,因此「改一个设置」不会触发全库重新加密(对 PBKDF2/DES -这类昂贵算法尤为明显)。 - -**推荐配置组合:** -```bash -# Cloudflare Workers + D1(推荐) -DB_FORMAT=sql # 也可用 map / key -DB_DRIVER=d1 # 需在 wrangler.jsonc 的 d1_databases 里绑定名为 DB - -# EdgeOne + Blob(推荐,零配置) -DB_FORMAT=map # 或 key -DB_DRIVER=blob - -# Cloudflare Workers + KV(必须先绑定 KV,见下方【方案 A】) -DB_FORMAT=map # 或 key -DB_DRIVER=kv # 需打开 wrangler.jsonc 的 kv_namespaces,绑定名必须恰好是 KV; - # 未绑定却显式写 kv 会直接报错(不做回退) - -# EdgeOne Node 云函数 + KV(还需额外部署 Edge Function 代理) -DB_FORMAT=map # 或 key -DB_DRIVER=kv -EO_KV_URLS=https://<你的部署域名> # 代理地址(也可由请求 origin 自动注入) -JWT_SECRET=<32 字符以上> # 代理鉴权,需与 Edge Function 侧一致 - -# 远程访问 Cloudflare KV(HTTP API,无需 binding) -DB_FORMAT=key -DB_DRIVER=cfkv -CF_ACCOUNT=your_account_id -CF_KV_UUID=your_namespace_id -CF_API_KEY=your_api_token -``` - -> 不确定用哪个就保持 `DB_DRIVER=auto`(默认,自动探测)。 -> 显式指定驱动时**不做回退**:该驱动不可用会直接拒绝请求并给出可操作原因(含 -> 「自动探测会选哪个驱动」,照抄即可),`/api/public/env_check` 与 -> `/api/public/init_status` 也会显示同样的原因和一行修复建议, -> 避免「以为在用 KV、实际写进了别的后端」。 -> 非法「驱动 × 格式」组合(如 `DB_FORMAT=sql` + `DB_DRIVER=kv`)同样只报错, -> 不会自动改驱动或格式。 - -**关于 DB_CIPHER 的补充说明:** -- 加密只作用于 `storages[].addition`(网盘凭据)、敏感 `settings`、`users[].password`、 - `users[].otp_secret`;内存中始终为明文,其余逻辑(驱动、路径解析、管理接口)不受影响。 -- **`none` 只表示「不加密数据库字段」,不影响其它任何行为**:JWT 令牌签名仍需一把 - 跨实例/跨冷启动一致的共享密钥,若未通过环境变量 `JWT_SECRET` 提供,安装向导仍会 - 自动生成并持久化 `openlist_encryption_secret`(与加密是否启用无关)。 -- 密文带版本前缀(`enc:v1:` ~ `enc:v6:`),**读取时按前缀自动识别算法**, - 与当前配置无关。因此: - - 从加密切回 `none`(或升级后不再配置 `DB_CIPHER`):既有密文仍能正常解密, - 并在**下一次配置保存**时自动转为明文(逐字段迁移,无需任何手动步骤); - - 更换算法:既有密文按旧算法解开,下次写入按新算法落盘; - - 既有的明文数据(无前缀)原样返回,升级不会丢数据。 -- 加密算法选择是**正交的一维**,不改变 `DB_DRIVER` / `DB_FORMAT` 的语义。 -- 取值无法识别时回退 `none` 并在日志告警(不会静默启用某个算法); - `/api/public/env_check` 的 `config.db_cipher` 会回显当前生效值。 -- AES 三种算法基于 WebCrypto(各平台原生);`chacha20-poly1305` 与 `des/3des` 因 - WebCrypto 不提供对应算法而使用纯 JS 实现 —— 全部在 Cloudflare Workers / - EdgeOne Node 云函数 / ESA / Node.js 上行为一致(不依赖 `node:crypto`)。 - -**向后兼容:** -- `DB_DRIVER=json` 自动转换为 `DB_FORMAT=map` + 自动检测驱动 -- 未配置 `DB_CIPHER` 时**不再对敏感字段加密**(旧版本默认加密)。升级已有部署时: - 存储中的 `enc:v1:`(PBKDF2)与 `enc:v2:`(HKDF)密文仍会按前缀自动解密、保持可读, - 并在**下一次配置保存**时自动转为明文;如需继续加密,显式设置 `DB_CIPHER` 即可 - (`DB_CIPHER=aes-256-gcm` 与既有加密部署的写入形态一致)。 - -**表名对齐(仅 SQL 格式):** -`sql` 格式采用列式表,命名策略与 Go 后端的 GORM 一致(snake_case + 复数表名 + 前缀): - -| Go 结构体 | 表名 | -| :------------ | :------------------ | -| `SettingItem` | `x_setting_items` | -| `SharingDB` | `x_sharing_dbs` | -| `Storage` | `x_storages` | -| `User` | `x_users` | -| `Meta` | `x_metas` | -| (仅 TS) | `x_plugins` | - -前缀固定为 `x_`(与 Go 后端默认值一致)。要与 Go 后端共享同一物理数据库,无需额外配置。 - -#### 缓存配置 - -本项目实现了两级持久缓存(参考 [OpenList.ts](https://github.com/Wudarensheng/OpenList.ts)): - -- **文件树缓存**:`/api/fs/list`、`/api/fs/get`、`/api/fs/dirs` 优先从缓存读取目录树, - 只有缓存未命中(或已过期)才会去请求网盘 / 对象存储; -- **下载链接缓存**:`/d`、`/p` 等下载链路复用驱动换取的直链, - 重复下载 / 预览时不再重新签名或重新换链(网盘换链通常最贵、最易被限流)。 - -**默认只向数据库启用**:缓存与业务数据走同一条存储链路(即 `DB_DRIVER` 指向的 -后端),因此「什么都不配」就等于「用数据库做缓存」。想要改用 / 额外启用 KV、 -Blob 等专用后端,**必须显式添加环境变量**(不会自动探测、不会自动回退): - -**CACHE_ENABLED**(总开关) -- `true`(默认):启用两级缓存 -- `false`:完全关闭(等同于 `CACHE_DRIVER=none`) - -**CACHE_DRIVER**(缓存后端) -- `db`(默认):与业务数据同一个后端(`DB_DRIVER` 解析结果,如 D1 / KV / Blob / MySQL) -- `kv`:Cloudflare KV 或 EdgeOne KV(binding 名固定为 `KV`) -- `blob`:EdgeOne Blob SDK / ESA Blob -- `cfkv`:Cloudflare KV REST API(需 `CF_ACCOUNT`、`CF_KV_UUID`、`CF_API_KEY`) -- `do`:Cloudflare Durable Objects -- `memory`:进程内存(仅本地调试,重启即失) -- `none`:关闭缓存 -- 支持逗号分隔的多后端(**读按顺序命中,写全部铺开**),例如: - - `db,kv` —— 数据库 + KV 双写,读优先命中 KV - - `kv,blob` —— 只用两个专用后端 -- 显式配置但该后端不可用时:**跳过并告警,不会悄悄换成别的后端**; - 若最终一个都不剩,缓存自动降级为「不缓存」(请求照常走真实存储)。 - -**CACHE_FILE_TREE** / **CACHE_DOWNLOAD_LINK**(分级开关) -- `CACHE_FILE_TREE` 默认 `true`;设为 `false` 可单独关闭文件树缓存 -- `CACHE_DOWNLOAD_LINK` 默认 **`false`**:部分网盘的直链 TTL 很短(分钟级 - 甚至更短),缓存复用容易把已失效的直链发给用户(表现为 403/404)。 - 确认所用网盘的直链有效期足够长后,再显式设为 `true` 开启 - -**CACHE_TTL**(文件树缓存时长,分钟) -- `0`(默认):跟随存储级 `cache_expiration`(默认 30 分钟, - 可被该存储的 `custom_cache_policies` 按路径覆盖) -- `>0`:全局覆盖所有存储的文件树缓存时长 -- 存储级 `cache_expiration=0` 或路径级策略命中 `0` 时,该目录**永不缓存** - -**CACHE_LINK_TTL**(下载链接缓存时长,分钟) -- 默认 `5`。直链通常自带有效期,取值需保守:缓存过久会把已失效的链接 - 交给浏览器(表现为 403/404)。设为 `0` 可关闭链接缓存。 - -**CACHE_EXCLUDE_DRIVERS**(不参与缓存的驱动) -- 默认 `virtual,alias,url_tree,strm,chunk` —— 这些驱动不产生远程 IO, - 缓存没有收益且只会让结果变陈旧 -- 传空字符串表示不排除任何驱动 - -**CACHE_PREFIX**(缓存键前缀) -- 默认 `openlist_cache`,用于与业务数据键(`openlist_config`、`users_1` 等)隔离 -- 仅允许 `[A-Za-z0-9_]`(最长 64 字符):前缀会成为 KV 键的一部分,含其他 - 字符时所有缓存写入都会失败;非法取值回退默认值并告警 - -**失效与一致性:** -- 写操作(mkdir / rename / remove / move / copy / put)会**立即失效**受影响的 - 路径与其父目录缓存; -- 存储配置变更(更新 / 启用 / 禁用 / 删除)会清空该存储的全部缓存; -- 缓存只存「驱动层结果」,权限、meta 密码、隐藏规则、下载签名都在请求时 - 实时计算,因此**缓存不会造成越权**。 - -**管理接口:** -- `GET /api/admin/cache/status` —— 生效配置、实际后端、各级缓存条目数 -- `POST /api/admin/cache/clear` —— body `{ type?: "file_tree" | "download_link", storage_id?: number }` -- `POST /api/admin/storage/refresh` —— 刷新(清空)全部存储的文件树缓存 -- `POST /api/admin/storage/refresh_one?id=` —— 刷新单个存储的文件树缓存 - -> 缓存状态也会回显在 `/api/public/env_check` 的 `cache` 字段中, -> 便于确认「缓存实际落在哪个后端」。 - -#### 安全配置 - -- `JWT_SECRET`:JWT 令牌签名密钥(必填),**同时用作可选的字段加密密钥**与定时任务鉴权 -- `DB_CIPHER`:敏感字段落盘算法(可选,默认 `none` 不加密):`none` / `aes-256-gcm` - (推荐)/ `aes-256-gcm-pbkdf2` / `aes-256-cbc-hmac` / `chacha20-poly1305` / - `des-cbc-hmac` / `3des-cbc-hmac`(后两者仅兼容用途,不安全) -- `ADMIN_PASS`:初始管理员密码(可选,设置后跳过安装向导自动初始化 admin) - -#### 并发去重(singleflight) - -同一时刻对同一存储、同一路径的重复读请求(目录列表、元信息)只向上游发起一次调用, -其余并发请求等待并复用同一结果,用于规避网盘限流与无谓的 CPU 消耗。 - -| 变量 | 默认值 | 说明 | -| --- | --- | --- | -| `SINGLEFLIGHT` | `auto` | `auto`:有 SQL 库(d1/mysql/do)时用表协调,否则退回进程内;`db`:强制用表;`memory`:仅进程内;`off`:关闭 | -| `SINGLEFLIGHT_HANDOFF_MS` | `1000` | 执行完成后结果在表中保留多久(ms),供**正在等待的**并发实例读取 | -| `SINGLEFLIGHT_LOCK_TTL_MS` | `30000` | 执行者持锁上限(ms),期间按 1/3 周期续租;执行者崩溃后到期可被接管 | -| `SINGLEFLIGHT_POLL_MS` | `200` | 等待方轮询间隔(ms)。不建议低于 100:每次轮询都是一次对协调表的 SELECT,过密会显著消耗 D1 subrequest 配额与 CPU | -| `SINGLEFLIGHT_WAIT_MS` | `15000` | 等待方最长等待(ms),超时后自行执行,避免被慢请求拖死 | - -行为要点: +部署完成后**首次访问站点会自动进入安装向导**,在浏览器中设置管理员账号与密码即可完成初始化;如需跳过向导,配置 `ADMIN_PASS` 即可自动初始化 admin。 -- **两级去重**:进程内合并(所有模式都生效)+ 数据库表协调(`db` 模式,跨 isolate 生效)。 -- **不做结果缓存**:只有「到达时观察到执行者正在运行」的请求才会复用结果;新到达的请求 - 会回收旧记录并重新执行,因此不存在「删除文件后刷新仍看到旧文件」的脏读。 -- **可用性优先**:表不存在、数据库超时、结果无法序列化、执行者崩溃等**协调层**异常一律 - 降级为「各自执行一次」,业务不会失败;而**业务函数自身的异常会原样抛出且不重试**。 -- **表结构**:`x_singleflight` 由 SQL 驱动在初始化时自动创建,属运行时数据,不参与配置读写。 -- 运行统计(`executed` / `coalesced` / `shared` / `fallback`)可在 `/api/debug/info` 的 - `singleflight` 字段查看,用于确认跨实例协调是否真的生效。 +绝大多数部署**不需要任何额外配置**:数据存储默认走 `DB_DRIVER=auto`,会自动选择平台可用的持久化后端(Cloudflare Workers → KV / D1 / DO,EdgeOne / ESA → Blob)。建议显式配置的只有一项: -#### 其他配置 +| 变量 | 说明 | +| :--- | :--- | +| `JWT_SECRET` | JWT 令牌签名密钥(≥16 字符,推荐 `openssl rand -hex 32` 生成),同时用于定时任务鉴权。未配置时会在初始化时自动生成并持久化 | -详细配置说明请参考 [官方文档](https://doc.oplist.org/guide/configuration) +> `JWT_SECRET` 请以 **Secret**(而非普通变量)方式配置:普通变量可能在平台重新部署时被仓库内的配置覆盖,Secret 不会被覆盖。 ---- +其余变量均有合理默认值,需要时再调整: +| 变量 | 默认值 | 说明 | +| :--- | :--- | :--- | +| `ADMIN_PASS` | — | 跳过安装向导,以该密码自动初始化 admin | +| `DB_FORMAT` | `map` | 数据存储格式:`map`(整对象 JSON)/ `key`(分 key)/ `sql`(关系表,与 Go 后端一致) | +| `DB_DRIVER` | `auto` | 存储驱动:`auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`;显式指定后不做回退 | +| `DB_CIPHER` | `none` | 敏感字段落盘算法(默认不加密),如 `aes-256-gcm` | +| `CACHE_ENABLED` / `CACHE_DRIVER` | `true` / `db` | 文件树缓存与下载链接缓存,默认复用业务数据后端 | +| `ALLOW_URLS` | — | CORS 允许白名单(逗号分隔,不设则仅同源) | +| `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | 整体上传 / 分片上传大小上限(字节) | +| `ASSET_URLS` | — | 前端静态资源 CDN 地址,支持 `$version` 占位符 | + +完整环境变量(含缓存 `CACHE_*`、字段加密 `DB_CIPHER`、并发去重 `SINGLEFLIGHT`、`cfkv` / MySQL 连接配置等)与各平台部署步骤,请参阅文档站: + +- [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) +- [部署教程](https://doc.oplist.org/ecosystem/official_worker/guide) +- [常见问题](https://doc.oplist.org/ecosystem/official_worker/about) ## 帮助支持 diff --git a/readmes/README_ar.md b/readmes/README_ar.md index a7d399bd..b1a0de13 100644 --- a/readmes/README_ar.md +++ b/readmes/README_ar.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - إذا عرض Cloudflare رسالة `تعذّر جلب محتوى المستودع`، [قم بعمل fork](https://github.com/OpenListTeam/OpenList-Worker/fork) لهذا المشروع أولاً ثم انشر من خلال الاتصال بمستودع Github -> - بعد النشر، قم بتكوين متغيرات البيئة: **EdgeOne**: [الكونسول الدولي](https://console.edgeone.ai/makers) · [كونسول الصين](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [لوحة تحكم Worker](https://dash.cloudflare.com/). متغيرات البيئة: -> - `DB_FORMAT`: تنسيق تخزين البيانات: `map` (افتراضي، JSON الكائن الكامل) / `key` (تخزين لكل مفتاح) / `sql` (جداول علائقية، متوافقة مع خلفية Go) -> - `DB_DRIVER`: مشغل قاعدة البيانات: `auto` (افتراضي، اكتشاف تلقائي) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - لبقية المتغيرات الاختيارية، راجع **دليل النشر التفصيلي**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - عند النشر، املأ `JWT_SECRET` (سلسلة عشوائية من 16 حرفًا أو أكثر، مثل `openssl rand -hex 32`): تُستخدم لتوقيع JWT وتشفير البيانات؛ وإن لم تُضبط فسيتم توليدها وحفظها أثناء التهيئة +> - يستخدم تخزين البيانات تلقائيًا الواجهة الخلفية الدائمة للمنصة (`DB_DRIVER=auto`) — لا حاجة لأي إعداد؛ ولمتغيرات أخرى راجع [متغيرات البيئة](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - بعد النشر، يظهر **معالج التثبيت** عند أول زيارة: عيّن حساب المسؤول وكلمة المرور في المتصفح لإكمال التهيئة (اضبط `ADMIN_PASS` لتخطي المعالج) ## الميزات @@ -61,7 +60,7 @@ OpenList-Worker هو المنفذ الرسمي TypeScript + Serverless لمشر - **الأقراص السحابية المحلية**: Aliyundrive (Open/Share)، Quark (Open/UC TV)، Baidu Netdisk (الألبوم)، 115 (Open/Share)، 123 Pan (Open/Share)، Tianyi Cloud (189/PC/TV)، China Mobile Cloud (139/Hecaiyun)، Wopan، Thunder، Tencent Weiyun، Lanzou، PikPak (Share)، Doubao، Guangyapan، Chaoxing Group، Lenovo NAS Share، Teambition، WPS Cloud، Alibaba Docs، HalalCloud، MediaTrack وغيرها - **الأقراص السحابية الدولية**: Google Drive (الألبوم)، OneDrive (App/ShareLink)، Dropbox، MEGA، MediaFire، Proton Drive، Yandex Disk، Degoo، Bunny Storage، TeraBox وغيرها -- **تخزين الكائنات**: متوافق مع S3 (AWS/OSS/COS/MinIO وغيرها)، UpYun USS، Azure Blob، WebDAV، FTP، SFTP، SMB، IPFS وغيرها +- **تخزين الكائنات**: متوافق مع S3 (AWS/OSS/COS/MinIO وغيرها)، Cloudflare R2 (ربط Workers أو S3 API)، UpYun USS، Azure Blob، WebDAV، FTP، SFTP، SMB، IPFS وغيرها - **استضافة الأكواد**: GitHub، GitHub Releases، CNB Releases - **برامج الأقراص السحابية**: OpenList (Share)، AList V3، Cloudreve V3/V4، Kodbox، Seafile، Teldrive، Febbox وغيرها - **برامج تشغيل أخرى**: Netease Music، Misskey، Emby، Cloudflare Image Hosting وغيرها diff --git a/readmes/README_de.md b/readmes/README_de.md index 214fb7d1..b8cebf03 100644 --- a/readmes/README_de.md +++ b/readmes/README_de.md @@ -43,10 +43,9 @@ Klicken Sie auf die Schaltfläche unten, um dieses Projekt mit einem Klick auf d > [!IMPORTANT] > - Wenn Cloudflare `Repository-Inhalt kann nicht abgerufen werden` anzeigt, [forken](https://github.com/OpenListTeam/OpenList-Worker/fork) Sie dieses Projekt zuerst und stellen Sie es dann über die Verbindung zum Github-Repository bereit -> - Konfigurieren Sie nach der Bereitstellung die Umgebungsvariablen: **EdgeOne**: [Internationale Konsole](https://console.edgeone.ai/makers) · [China-Konsole](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Worker-Dashboard](https://dash.cloudflare.com/). Umgebungsvariablen: -> - `DB_FORMAT`: Datenspeicherformat: `map` (Standard, JSON des gesamten Objekts) / `key` (schlüsselbasierte Speicherung) / `sql` (relationale Tabellen, kompatibel mit dem Go-Backend) -> - `DB_DRIVER`: Datenbanktreiber: `auto` (Standard, automatische Erkennung) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Weitere optionale Variablen finden Sie im **detaillierten Bereitstellungsleitfaden**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Tragen Sie bei der Bereitstellung `JWT_SECRET` ein (Zufallszeichenfolge mit 16+ Zeichen, z. B. `openssl rand -hex 32`): Sie signiert JWTs und verschlüsselt Daten; ohne Angabe wird sie bei der Initialisierung erzeugt und gespeichert +> - Der Datenspeicher verwendet automatisch das plattformeigene persistente Backend (`DB_DRIVER=auto`) — keine Konfiguration nötig; weitere Variablen siehe [Umgebungsvariablen](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - Beim ersten Besuch nach der Bereitstellung öffnet sich der **Installationsassistent**: Legen Sie Admin-Konto und Passwort im Browser fest, um die Initialisierung abzuschließen (mit `ADMIN_PASS` überspringen Sie den Assistenten) ## Funktionen @@ -61,7 +60,7 @@ OpenList-Worker ist der offizielle TypeScript + Serverless-Port des Projekts [Op - **Inländische Cloud-Laufwerke**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Album), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack usw. - **Internationale Cloud-Laufwerke**: Google Drive (Album), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox usw. -- **Objektspeicher**: S3-kompatibel (AWS/OSS/COS/MinIO usw.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS usw. +- **Objektspeicher**: S3-kompatibel (AWS/OSS/COS/MinIO usw.), Cloudflare R2 (Workers-Binding oder S3-API), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS usw. - **Code-Hosting**: GitHub, GitHub Releases, CNB Releases - **Cloud-Laufwerk-Programme**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox usw. - **Weitere Treiber**: Netease Music, Misskey, Emby, Cloudflare Image Hosting usw. diff --git a/readmes/README_en.md b/readmes/README_en.md index 91d54338..bf35d179 100644 --- a/readmes/README_en.md +++ b/readmes/README_en.md @@ -43,10 +43,9 @@ Click the button below to deploy this project to the corresponding platform with > [!IMPORTANT] > - If Cloudflare prompts `cannot fetch repository content`, [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) this project first, then deploy by connecting to the GitHub repository -> - After deployment, configure environment variables: **EdgeOne**: [International console](https://console.edgeone.ai/makers) · [China console](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Worker dashboard](https://dash.cloudflare.com/). Environment variables: -> - `DB_FORMAT`: data storage format: `map` (default, whole object JSON) / `key` (per-key storage) / `sql` (relational tables, compatible with Go backend) -> - `DB_DRIVER`: database driver: `auto` (default, auto-detect) / `blob` (EdgeOne Blob) / `cfkv` (CF KV API) / `kv` (KV binding) / `d1` (Cloudflare D1) / `mysql` -> - For other optional variables, see the **detailed deployment guide**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Set `JWT_SECRET` during deployment (a random string of ≥16 chars, e.g. `openssl rand -hex 32`): it signs JWTs and encrypts data; if unset, it is generated and persisted during initialization +> - Storage automatically uses the platform's built-in persistent backend (`DB_DRIVER=auto`) — no configuration needed; for other variables see [Environment Variables](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - After deployment, the first visit opens the **install wizard**: set the admin account and password in the browser to finish initialization; set `ADMIN_PASS` to skip the wizard ## Features @@ -61,7 +60,7 @@ Built-in **78 storage drivers**, ready to mount various storage backends out of - **Domestic cloud drives**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Album), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack, etc. - **International cloud drives**: Google Drive (Album), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox, etc. -- **Object storage**: S3-compatible (AWS/OSS/COS/MinIO, etc.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. +- **Object storage**: S3-compatible (AWS/OSS/COS/MinIO, etc.), Cloudflare R2 (Workers binding or S3 API), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. - **Code hosting**: GitHub, GitHub Releases, CNB Releases - **Cloud drive programs**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox, etc. - **Other drivers**: Netease Music, Misskey, Emby, Cloudflare Image Hosting, etc. @@ -132,17 +131,16 @@ pnpm run deploy:worker ### Backend -- **Runtime**: Cloudflare Workers (Edge Computing) +- **Runtime**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Web framework**: Hono.js -- **Database**: Cloudflare D1 (SQLite) / supports MySQL, MariaDB, PostgreSQL, SQL Server -- **Cache**: Cloudflare KV (optional) +- **Database**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects, or external MySQL / MariaDB - **Language**: TypeScript - **Build tools**: Wrangler, esbuild ### Frontend -- **Framework**: React 19 + TypeScript -- **UI libraries**: Ant Design / Material-UI +- **Framework**: SolidJS + TypeScript (official [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI library**: Hope UI - **Build tool**: Vite --- @@ -150,82 +148,34 @@ pnpm run deploy:worker ## Configuration -### Environment Variables +The **install wizard** appears automatically on the first visit — set the admin account and password in the browser to finish initialization. To skip the wizard, set `ADMIN_PASS` and the admin is initialized automatically. -#### Database Configuration +Most deployments need **no extra configuration**: storage defaults to `DB_DRIVER=auto`, which picks an available persistent backend (Cloudflare Workers → KV / D1 / DO, EdgeOne / ESA → Blob). The only variable worth setting explicitly is: -**DB_FORMAT** (Data Storage Format) -- `map` (default): Whole object JSON format, suitable for KV/Blob simple storage -- `key`: Per-key storage format, each entity as a separate record (e.g., `users_1`), avoids large JSON -- `sql`: Relational database table format, fully compatible with Go backend, suitable for D1/MySQL +| Variable | Description | +| :--- | :--- | +| `JWT_SECRET` | JWT signing key (≥16 chars, e.g. `openssl rand -hex 32`), also used for cron task auth. Auto-generated and persisted during initialization when unset | -**DB_DRIVER** (Database Driver) -- `auto` (default): Auto-detect available drivers (priority: mysql → d1 → kv → cfkv → blob → do) -- `blob`: Tencent EdgeOne Blob / Alibaba ESA Blob -- `cfkv`: Cloudflare KV REST API (requires `CF_ACCOUNT`, `CF_KV_UUID`, `CF_API_KEY`) -- `kv`: Cloudflare KV binding (binding name is fixed to `KV`) -- `d1`: Cloudflare D1 (SQLite) -- `do`: Cloudflare Durable Objects (SQLite) -- `mysql`: MySQL (Node.js container only) +> Configure `JWT_SECRET` as a **Secret** (not a plain variable): plain variables can be overwritten by the repository config on redeploy, secrets are not. -**Recommended Configurations:** -```bash -# Cloudflare Workers + D1 (recommended) -DB_FORMAT=sql -DB_DRIVER=d1 - -# EdgeOne + Blob -DB_FORMAT=map -DB_DRIVER=blob - -# Cloudflare KV (high-frequency read/write) -DB_FORMAT=key -DB_DRIVER=kv - -# Remote Cloudflare KV access -DB_FORMAT=key -DB_DRIVER=cfkv -CF_ACCOUNT=your_account_id -CF_KV_UUID=your_namespace_id -CF_API_KEY=your_api_token -``` - -> An explicitly configured driver is **never replaced automatically**. If it is -> unavailable the request is rejected with an actionable reason (including which -> driver auto-detection would have picked); `/api/public/env_check` and -> `/api/public/init_status` report the same reason plus a one-line fix. This -> prevents "I thought it was KV, but writes went to another backend". -> Invalid driver/format pairs (e.g. `DB_FORMAT=sql` + `DB_DRIVER=kv`) are -> reported the same way — the app never rewrites your configuration. - -**Backward Compatibility:** -- `DB_DRIVER=json` auto-converts to `DB_FORMAT=map` + auto-detect driver - -**Table Naming (SQL format only):** -The `sql` format uses columnar tables with the same naming strategy as the Go backend's GORM (snake_case + pluralized names + prefix): - -| Go struct | Table name | -| :------------ | :----------------- | -| `SettingItem` | `x_setting_items` | -| `SharingDB` | `x_sharing_dbs` | -| `Storage` | `x_storages` | -| `User` | `x_users` | -| `Meta` | `x_metas` | -| (TS only) | `x_plugins` | +All other variables have sensible defaults: -The prefix is fixed to `x_` (matching the Go backend default), so no extra configuration is needed to share the same physical database with the Go backend. +| Variable | Default | Description | +| :--- | :--- | :--- | +| `ADMIN_PASS` | — | Skip the install wizard and auto-initialize the admin | +| `DB_FORMAT` | `map` | Storage format: `map` (whole JSON) / `key` (per-key) / `sql` (relational, Go-compatible) | +| `DB_DRIVER` | `auto` | Storage driver: `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`; an explicit driver is never replaced automatically | +| `DB_CIPHER` | `none` | At-rest cipher for sensitive fields (no encryption by default), e.g. `aes-256-gcm` | +| `CACHE_ENABLED` / `CACHE_DRIVER` | `true` / `db` | File-tree & download-link caches, reusing the data backend by default | +| `ALLOW_URLS` | — | Comma-separated CORS allowlist (same-origin only when unset) | +| `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | Max size (bytes) for a whole upload / a multipart chunk | +| `ASSET_URLS` | — | Frontend asset CDN base URL, supports the `$version` placeholder | -#### Security Configuration - -- `JWT_SECRET`: JWT signing key (required), also used for data encryption and cron task authentication -- `ADMIN_PASS`: Initial admin password (optional, skips the setup wizard and auto-initializes admin) - -#### Other Configuration - -For detailed configuration, please refer to the [official documentation](https://doc.oplist.org/guide/configuration) - ---- +For the full variable list (cache `CACHE_*`, field encryption `DB_CIPHER`, concurrency dedup `SINGLEFLIGHT`, `cfkv` / MySQL settings, …) and per-platform deployment steps, see the documentation site: +- [Environment Variables](https://doc.oplist.org/ecosystem/official_worker/guide_env) +- [Deployment](https://doc.oplist.org/ecosystem/official_worker/guide) +- [FAQ](https://doc.oplist.org/ecosystem/official_worker/about) ## Support diff --git a/readmes/README_es.md b/readmes/README_es.md index f1bbb4c9..86cd1806 100644 --- a/readmes/README_es.md +++ b/readmes/README_es.md @@ -43,10 +43,9 @@ Haz clic en el botón de abajo para desplegar este proyecto en la plataforma cor > [!IMPORTANT] > - Si Cloudflare muestra `no se puede obtener el contenido del repositorio`, [bifurca](https://github.com/OpenListTeam/OpenList-Worker/fork) este proyecto primero y luego despliega conectándote al repositorio de Github -> - Después del despliegue, configura las variables de entorno: **EdgeOne**: [Consola internacional](https://console.edgeone.ai/makers) · [Consola China](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Panel de Worker](https://dash.cloudflare.com/). Variables de entorno: -> - `DB_FORMAT`: formato de almacenamiento de datos: `map` (por defecto, JSON de objeto completo) / `key` (almacenamiento por clave) / `sql` (tablas relacionales, compatible con el backend Go) -> - `DB_DRIVER`: controlador de base de datos: `auto` (por defecto, detección automática) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Para otras variables opcionales, consulta la **guía de despliegue detallada**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Durante el despliegue, rellena `JWT_SECRET` (cadena aleatoria de 16+ caracteres, por ejemplo `openssl rand -hex 32`): firma los JWT y cifra datos; si no se define, se genera y persiste durante la inicialización +> - El almacenamiento usa automáticamente el backend persistente de la plataforma (`DB_DRIVER=auto`) — sin configuración adicional; para el resto de variables consulta [Variables de entorno](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - Tras el despliegue, la primera visita abre el **asistente de instalación**: define la cuenta y la contraseña del administrador en el navegador para completar la inicialización (define `ADMIN_PASS` para omitirlo) ## Funciones @@ -61,7 +60,7 @@ OpenList-Worker es el puerto oficial TypeScript + Serverless del proyecto [OpenL - **Unidades en la nube domésticas**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Álbum), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack, etc. - **Unidades en la nube internacionales**: Google Drive (Álbum), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox, etc. -- **Almacenamiento de objetos**: Compatible con S3 (AWS/OSS/COS/MinIO, etc.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. +- **Almacenamiento de objetos**: Compatible con S3 (AWS/OSS/COS/MinIO, etc.), Cloudflare R2 (Workers binding o API S3), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. - **Alojamiento de código**: GitHub, GitHub Releases, CNB Releases - **Programas de unidad en la nube**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox, etc. - **Otros controladores**: Netease Music, Misskey, Emby, Cloudflare Image Hosting, etc. diff --git a/readmes/README_fr.md b/readmes/README_fr.md index 9125e6eb..b3766b45 100644 --- a/readmes/README_fr.md +++ b/readmes/README_fr.md @@ -43,10 +43,9 @@ Cliquez sur le bouton ci-dessous pour déployer ce projet sur la plateforme corr > [!IMPORTANT] > - Si Cloudflare affiche `impossible de récupérer le contenu du dépôt`, [forkez](https://github.com/OpenListTeam/OpenList-Worker/fork) d'abord ce projet puis déployez en vous connectant au dépôt Github -> - Après le déploiement, configurez les variables d'environnement : **EdgeOne** : [Console internationale](https://console.edgeone.ai/makers) · [Console Chine](https://console.cloud.tencent.com/edgeone/makers) ; **Cloudflare** : [Tableau de bord Worker](https://dash.cloudflare.com/). Variables d'environnement : -> - `DB_FORMAT` : format de stockage des données : `map` (défaut, JSON d'objet complet) / `key` (stockage par clé) / `sql` (tables relationnelles, compatible avec le backend Go) -> - `DB_DRIVER` : pilote de base de données : `auto` (défaut, détection automatique) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Pour les autres variables optionnelles, voir le **guide de déploiement détaillé** : [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Lors du déploiement, renseignez `JWT_SECRET` (chaîne aléatoire de 16+ caractères, par ex. `openssl rand -hex 32`) : elle signe les JWT et chiffre les données ; si elle n'est pas définie, elle est générée et persistée pendant l'initialisation +> - Le stockage utilise automatiquement le backend persistant de la plateforme (`DB_DRIVER=auto`) — aucune configuration requise ; pour les autres variables, voir [Variables d'environnement](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - Après le déploiement, la première visite ouvre l'**assistant d'installation** : définissez le compte et le mot de passe administrateur dans le navigateur pour terminer l'initialisation (définissez `ADMIN_PASS` pour l'ignorer) ## Fonctionnalités @@ -61,7 +60,7 @@ OpenList-Worker est le port officiel TypeScript + Serverless du projet [OpenList - **Disques cloud domestiques** : Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Album), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack, etc. - **Disques cloud internationaux** : Google Drive (Album), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox, etc. -- **Stockage d'objets** : Compatible S3 (AWS/OSS/COS/MinIO, etc.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. +- **Stockage d'objets** : Compatible S3 (AWS/OSS/COS/MinIO, etc.), Cloudflare R2 (binding Workers ou API S3), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. - **Hébergement de code** : GitHub, GitHub Releases, CNB Releases - **Programmes de disque cloud** : OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox, etc. - **Autres pilotes** : Netease Music, Misskey, Emby, Cloudflare Image Hosting, etc. diff --git a/readmes/README_hi.md b/readmes/README_hi.md index ce6a1150..4dc2d743 100644 --- a/readmes/README_hi.md +++ b/readmes/README_hi.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - यदि Cloudflare `रिपॉजिटरी सामग्री प्राप्त नहीं कर सकता` दिखाता है, तो पहले इस परियोजना को [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) करें, फिर Github रिपॉजिटरी से कनेक्ट करके तैनात करें -> - तैनाती के बाद पर्यावरण चर कॉन्फ़िगर करें: **EdgeOne**: [अंतर्राष्ट्रीय कंसोल](https://console.edgeone.ai/makers) · [चीन कंसोल](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Worker डैशबोर्ड](https://dash.cloudflare.com/)। पर्यावरण चर: -> - `DB_FORMAT`: डेटा संग्रहण प्रारूप: `map` (डिफ़ॉल्ट, संपूर्ण ऑब्जेक्ट JSON) / `key` (कुंजी-आधारित संग्रहण) / `sql` (रिलेशनल टेबल, Go बैकएंड संगत) -> - `DB_DRIVER`: डेटाबेस ड्राइवर: `auto` (डिफ़ॉल्ट, स्वतः पहचान) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - शेष वैकल्पिक चरों के लिए **विस्तृत तैनाती मार्गदर्शिका** देखें: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - तैनाती के समय `JWT_SECRET` भरें (16+ वर्णों की यादृच्छिक स्ट्रिंग, उदा. `openssl rand -hex 32`): यह JWT पर हस्ताक्षर और डेटा एन्क्रिप्शन के लिए उपयोग होता है; न दिया जाए तो इनिशियलाइज़ेशन के दौरान स्वतः बनाकर सहेजा जाता है +> - डेटा संग्रहण स्वतः प्लेटफ़ॉर्म के स्थायी बैकएंड का चयन करता है (`DB_DRIVER=auto`) — कोई कॉन्फ़िगरेशन आवश्यक नहीं; शेष चरों के लिए [पर्यावरण चर](https://doc.oplist.org/ecosystem/official_worker/guide_env) देखें +> - तैनाती के बाद पहली विज़िट पर **इंस्टॉल विज़ार्ड** खुलता है: इनिशियलाइज़ेशन पूरा करने के लिए ब्राउज़र में एडमिन खाता और पासवर्ड सेट करें (`ADMIN_PASS` सेट करने पर विज़ार्ड छूट जाता है) ## विशेषताएँ @@ -61,7 +60,7 @@ OpenList-Worker आधिकारिक [OpenListTeam/OpenList](https://github. - **घरेलू क्लाउड ड्राइव**: Aliyundrive(ओपन प्लेटफ़ॉर्म/शेयर)、Quark(ओपन प्लेटफ़ॉर्म/UC TV)、Baidu Netdisk(एल्बम)、115(ओपन प्लेटफ़ॉर्म/शेयर)、123 Pan(ओपन प्लेटफ़ॉर्म/शेयर)、Tianyi Cloud(189/PC/TV)、China Mobile Cloud(139/Hecaiyun)、Wopan、Thunder、Tencent Weiyun、Lanzou、PikPak(शेयर)、Doubao、Guangyapan、Chaoxing Group、Lenovo NAS शेयर、Teambition、WPS Cloud、Alibaba Docs、HalalCloud、MediaTrack आदि - **अंतर्राष्ट्रीय क्लाउड ड्राइव**: Google Drive(एल्बम)、OneDrive(ऐप/शेयर लिंक)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox आदि -- **ऑब्जेक्ट स्टोरेज**: S3-संगत(AWS/OSS/COS/MinIO आदि)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS आदि +- **ऑब्जेक्ट स्टोरेज**: S3-संगत(AWS/OSS/COS/MinIO आदि)、Cloudflare R2(Workers binding या S3 API)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS आदि - **कोड होस्टिंग**: GitHub、GitHub Releases、CNB Releases - **क्लाउड ड्राइव प्रोग्राम**: OpenList(शेयर)、AList V3、Cloudreve V3/V4、Kodbox、Seafile、Teldrive、Febbox आदि - **अन्य ड्राइवर**: Netease Music、Misskey、Emby、Cloudflare इमेज होस्टिंग आदि diff --git a/readmes/README_it.md b/readmes/README_it.md index 865e518f..a1585e50 100644 --- a/readmes/README_it.md +++ b/readmes/README_it.md @@ -43,10 +43,9 @@ Clicca il pulsante qui sotto per distribuire questo progetto sulla piattaforma c > [!IMPORTANT] > - Se Cloudflare mostra `impossibile recuperare il contenuto del repository`, [fai un fork](https://github.com/OpenListTeam/OpenList-Worker/fork) di questo progetto e poi distribuisci collegandoti al repository Github -> - Dopo il deploy, configura le variabili d'ambiente: **EdgeOne**: [Console internazionale](https://console.edgeone.ai/makers) · [Console Cina](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Dashboard Worker](https://dash.cloudflare.com/). Variabili d'ambiente: -> - `DB_FORMAT`: formato di archiviazione dei dati: `map` (predefinito, JSON dell'oggetto completo) / `key` (archiviazione per chiave) / `sql` (tabelle relazionali, compatibile con il backend Go) -> - `DB_DRIVER`: driver del database: `auto` (predefinito, rilevamento automatico) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Per le altre variabili opzionali, consulta la **guida di deploy dettagliata**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Durante il deploy compila `JWT_SECRET` (stringa casuale di 16+ caratteri, ad es. `openssl rand -hex 32`): firma i JWT e cifra i dati; se non impostata, viene generata e salvata durante l'inizializzazione +> - L'archiviazione usa automaticamente il backend persistente della piattaforma (`DB_DRIVER=auto`) — nessuna configurazione necessaria; per le altre variabili vedi [Variabili d'ambiente](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - Dopo il deploy, la prima visita apre la **procedura guidata di installazione**: imposta account e password admin nel browser per completare l'inizializzazione (imposta `ADMIN_PASS` per saltarla) ## Funzionalità @@ -61,7 +60,7 @@ Oltre **78 driver di storage** integrati, pronti a montare vari backend di stora - **Unità cloud domestiche**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Album), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack, ecc. - **Unità cloud internazionali**: Google Drive (Album), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox, ecc. -- **Object storage**: Compatibile con S3 (AWS/OSS/COS/MinIO, ecc.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, ecc. +- **Object storage**: Compatibile con S3 (AWS/OSS/COS/MinIO, ecc.), Cloudflare R2 (binding Workers o API S3), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, ecc. - **Hosting del codice**: GitHub, GitHub Releases, CNB Releases - **Programmi di unità cloud**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox, ecc. - **Altri driver**: Netease Music, Misskey, Emby, Cloudflare Image Hosting, ecc. diff --git a/readmes/README_ja.md b/readmes/README_ja.md index 0038cbf5..8c7095ae 100644 --- a/readmes/README_ja.md +++ b/readmes/README_ja.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - Cloudflare が `ストレージリポジトリの内容を取得できません` と表示する場合、まず本プロジェクトを [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) し、Github リポジトリへの接続からデプロイしてください -> - デプロイ後、環境変数を設定します: **EdgeOne**:[国際版コンソール](https://console.edgeone.ai/makers) · [中国版コンソール](https://console.cloud.tencent.com/edgeone/makers);**Cloudflare**:[Worker ダッシュボード](https://dash.cloudflare.com/)。環境変数: -> - `DB_FORMAT`: データ保存形式:`map` (デフォルト、オブジェクト全体 JSON) / `key` (キー別保存) / `sql` (リレーショナルテーブル、Go バックエンド互換) -> - `DB_DRIVER`: データベースドライバ:`auto` (デフォルト、自動検出) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - その他の任意変数は**詳細なデプロイガイド**を参照:[Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - デプロイ時に `JWT_SECRET`(16 文字以上のランダム文字列、例:`openssl rand -hex 32`)を設定してください。JWT 署名とデータ暗号化に使われ、未設定の場合は初期化時に自動生成・永続化されます +> - データ保存はプラットフォーム標準の永続化バックエンドを自動選択します(`DB_DRIVER=auto`)——通常は設定不要です。その他の変数は [環境変数](https://doc.oplist.org/ecosystem/official_worker/guide_env) を参照してください +> - デプロイ後の初回アクセスでは**インストールウィザード**が開きます。ブラウザで管理者アカウントとパスワードを設定して初期化してください(`ADMIN_PASS` を設定するとウィザードをスキップできます) ## 機能紹介 @@ -61,7 +60,7 @@ OpenList-Worker は公式 [OpenListTeam/OpenList](https://github.com/OpenListTea - **国内クラウドドライブ**:アリババクラウドディスク(オープンプラットフォーム/共有)、Quark(オープンプラットフォーム/UC TV 版)、Baidu 網盤(アルバム)、115(オープンプラットフォーム/共有)、123 Pan(オープンプラットフォーム/共有)、天翼クラウドディスク(189/PC/TV)、中国移動クラウドディスク(139/和彩雲)、沃家クラウドディスク、迅雷クラウドディスク、Tencent Weiyun、Lanzou、PikPak(共有)、豆包、光亜盤、超星グループ、Lenovo NAS 共有、Teambition、WPS クラウド、アリババドキュメント、HalalCloud、MediaTrack など - **海外クラウドドライブ**:Google Drive(アルバム)、OneDrive(アプリ/共有リンク)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox など -- **オブジェクトストレージ**:S3 互換(AWS/OSS/COS/MinIO など)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS など +- **オブジェクトストレージ**:S3 互換(AWS/OSS/COS/MinIO など)、Cloudflare R2(Workers バインディングまたは S3 API)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS など - **コードホスティング**:GitHub、GitHub Releases、CNB Releases - **クラウドドライブプログラム**:OpenList(共有)、AList V3、Cloudreve V3/V4、Kodbox、Seafile、Teldrive、Febbox など - **その他のドライバー**:Netease Music、Misskey、Emby、Cloudflare 画像ホスティングなど diff --git a/readmes/README_ko.md b/readmes/README_ko.md index 5b4b3f21..38b82e60 100644 --- a/readmes/README_ko.md +++ b/readmes/README_ko.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - Cloudflare에서 `저장소 콘텐츠를 가져올 수 없습니다`라고 표시되면, 먼저 이 프로젝트를 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork)한 다음 Github 저장소 연결 기능으로 배포하세요 -> - 배포 후 환경 변수를 설정합니다: **EdgeOne**:[국제 콘솔](https://console.edgeone.ai/makers) · [중국 콘솔](https://console.cloud.tencent.com/edgeone/makers);**Cloudflare**:[Worker 대시보드](https://dash.cloudflare.com/)。환경 변수: -> - `DB_FORMAT`: 데이터 저장 형식: `map` (기본값, 전체 객체 JSON) / `key` (키별 저장) / `sql` (관계형 테이블, Go 백엔드 호환) -> - `DB_DRIVER`: 데이터베이스 드라이버: `auto` (기본값, 자동 감지) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - 나머지 선택 변수는 **상세 배포 가이드**를 참조하세요:[Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - 배포 시 `JWT_SECRET`(16자 이상의 임의 문자열, 예: `openssl rand -hex 32`)을 입력하세요. JWT 서명과 데이터 암호화에 사용되며, 미설정 시 초기화 단계에서 자동 생성·영구 저장됩니다 +> - 데이터 저장은 플랫폼 내장 영구 저장소를 자동으로 선택합니다(`DB_DRIVER=auto`) — 대부분 설정이 필요 없으며, 나머지 변수는 [환경 변수](https://doc.oplist.org/ecosystem/official_worker/guide_env)를 참고하세요 +> - 배포 후 첫 방문 시 **설치 마법사**가 열립니다. 브라우저에서 관리자 계정과 비밀번호를 설정하면 초기화가 완료됩니다(`ADMIN_PASS`를 설정하면 마법사를 건너뜁니다) ## 기능 소개 @@ -61,7 +60,7 @@ OpenList-Worker는 공식 [OpenListTeam/OpenList](https://github.com/OpenListTea - **국내 클라우드 드라이브**:Aliyundrive(오픈 플랫폼/공유)、Quark(오픈 플랫폼/UC TV)、Baidu Netdisk(앨범)、115(오픈 플랫폼/공유)、123 Pan(오픈 플랫폼/공유)、Tianyi Cloud(189/PC/TV)、China Mobile Cloud(139/Hecaiyun)、Wopan、Thunder、Tencent Weiyun、Lanzou、PikPak(공유)、Doubao、Guangyapan、Chaoxing Group、Lenovo NAS 공유、Teambition、WPS Cloud、Alibaba Docs、HalalCloud、MediaTrack 등 - **국제 클라우드 드라이브**:Google Drive(앨범)、OneDrive(앱/공유 링크)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox 등 -- **오브젝트 스토리지**:S3 호환(AWS/OSS/COS/MinIO 등)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 등 +- **오브젝트 스토리지**:S3 호환(AWS/OSS/COS/MinIO 등)、Cloudflare R2(Workers 바인딩 또는 S3 API)、UpYun USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 등 - **코드 호스팅**:GitHub、GitHub Releases、CNB Releases - **클라우드 드라이브 프로그램**:OpenList(공유)、AList V3、Cloudreve V3/V4、Kodbox、Seafile、Teldrive、Febbox 등 - **기타 드라이버**:Netease Music、Misskey、Emby、Cloudflare 이미지 호스팅 등 diff --git a/readmes/README_pt.md b/readmes/README_pt.md index ee7063f0..d9bf6eab 100644 --- a/readmes/README_pt.md +++ b/readmes/README_pt.md @@ -43,10 +43,9 @@ Clique no botão abaixo para implantar este projeto na plataforma correspondente > [!IMPORTANT] > - Se o Cloudflare exibir `não é possível obter o conteúdo do repositório`, [bifurque](https://github.com/OpenListTeam/OpenList-Worker/fork) este projeto primeiro e depois implante conectando-se ao repositório do Github -> - Após a implantação, configure as variáveis de ambiente: **EdgeOne**: [Console internacional](https://console.edgeone.ai/makers) · [Console China](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Painel do Worker](https://dash.cloudflare.com/). Variáveis de ambiente: -> - `DB_FORMAT`: formato de armazenamento de dados: `map` (padrão, JSON do objeto completo) / `key` (armazenamento por chave) / `sql` (tabelas relacionais, compatível com o backend Go) -> - `DB_DRIVER`: driver de banco de dados: `auto` (padrão, detecção automática) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Para outras variáveis opcionais, consulte o **guia de implantação detalhado**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - Na implantação, informe `JWT_SECRET` (string aleatória com 16+ caracteres, por exemplo `openssl rand -hex 32`): ela assina JWTs e criptografa dados; se não definida, é gerada e persistida durante a inicialização +> - O armazenamento de dados usa automaticamente o backend persistente da plataforma (`DB_DRIVER=auto`) — sem configuração necessária; para as demais variáveis, consulte [Variáveis de ambiente](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - Após a implantação, o primeiro acesso abre o **assistente de instalação**: defina a conta e a senha do administrador no navegador para concluir a inicialização (defina `ADMIN_PASS` para pular o assistente) ## Funcionalidades @@ -61,7 +60,7 @@ O OpenList-Worker é o port oficial TypeScript + Serverless do projeto [OpenList - **Unidades de nuvem domésticas**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Álbum), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack, etc. - **Unidades de nuvem internacionais**: Google Drive (Álbum), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox, etc. -- **Armazenamento de objetos**: Compatível com S3 (AWS/OSS/COS/MinIO, etc.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. +- **Armazenamento de objetos**: Compatível com S3 (AWS/OSS/COS/MinIO, etc.), Cloudflare R2 (binding do Workers ou API S3), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS, etc. - **Hospedagem de código**: GitHub, GitHub Releases, CNB Releases - **Programas de unidade de nuvem**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox, etc. - **Outros drivers**: Netease Music, Misskey, Emby, Cloudflare Image Hosting, etc. diff --git a/readmes/README_ru.md b/readmes/README_ru.md index f440e972..8355c6a2 100644 --- a/readmes/README_ru.md +++ b/readmes/README_ru.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - Если Cloudflare отображает `не удаётся получить содержимое репозитория`, сначала [сделайте fork](https://github.com/OpenListTeam/OpenList-Worker/fork) этого проекта, а затем разверните, подключившись к репозиторию Github -> - После развёртывания настройте переменные окружения: **EdgeOne**: [Международная консоль](https://console.edgeone.ai/makers) · [Консоль Китая](https://console.cloud.tencent.com/edgeone/makers); **Cloudflare**: [Панель Worker](https://dash.cloudflare.com/). Переменные окружения: -> - `DB_FORMAT`: формат хранения данных: `map` (по умолчанию, JSON всего объекта) / `key` (хранение по ключу) / `sql` (реляционные таблицы, совместимо с бэкендом Go) -> - `DB_DRIVER`: драйвер базы данных: `auto` (по умолчанию, автоопределение) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - Остальные необязательные переменные см. в **подробном руководстве по развёртыванию**: [Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - При развёртывании укажите `JWT_SECRET` (случайная строка от 16 символов, например `openssl rand -hex 32`): она подписывает JWT и шифрует данные; если не задана, будет сгенерирована и сохранена при инициализации +> - Хранилище данных автоматически использует встроенный постоянный бэкенд платформы (`DB_DRIVER=auto`) — настройка не требуется; остальные переменные см. в [Переменные окружения](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - При первом посещении после развёртывания открывается **мастер установки**: задайте учётную запись и пароль администратора в браузере, чтобы завершить инициализацию (установите `ADMIN_PASS`, чтобы пропустить мастер) ## Возможности @@ -61,7 +60,7 @@ OpenList-Worker — это официальный порт проекта [OpenL - **Отечественные облачные диски**: Aliyundrive (Open/Share), Quark (Open/UC TV), Baidu Netdisk (Альбом), 115 (Open/Share), 123 Pan (Open/Share), Tianyi Cloud (189/PC/TV), China Mobile Cloud (139/Hecaiyun), Wopan, Thunder, Tencent Weiyun, Lanzou, PikPak (Share), Doubao, Guangyapan, Chaoxing Group, Lenovo NAS Share, Teambition, WPS Cloud, Alibaba Docs, HalalCloud, MediaTrack и др. - **Международные облачные диски**: Google Drive (Альбом), OneDrive (App/ShareLink), Dropbox, MEGA, MediaFire, Proton Drive, Yandex Disk, Degoo, Bunny Storage, TeraBox и др. -- **Объектные хранилища**: S3-совместимые (AWS/OSS/COS/MinIO и др.), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS и др. +- **Объектные хранилища**: S3-совместимые (AWS/OSS/COS/MinIO и др.), Cloudflare R2 (привязка Workers или S3 API), UpYun USS, Azure Blob, WebDAV, FTP, SFTP, SMB, IPFS и др. - **Хостинг кода**: GitHub, GitHub Releases, CNB Releases - **Программы облачных дисков**: OpenList (Share), AList V3, Cloudreve V3/V4, Kodbox, Seafile, Teldrive, Febbox и др. - **Прочие драйверы**: Netease Music, Misskey, Emby, Cloudflare Image Hosting и др. diff --git a/readmes/README_zh-TW.md b/readmes/README_zh-TW.md index a18ce7ed..cb170b9d 100644 --- a/readmes/README_zh-TW.md +++ b/readmes/README_zh-TW.md @@ -43,10 +43,9 @@ > [!IMPORTANT] > - 若 Cloudflare 提示`無法取得儲存庫內容`,則您需要先 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) 本專案,再透過連接到 Github 倉庫功能部署 -> - 部署完成後配置環境變數: **EdgeOne**:[國際站](https://console.edgeone.ai/makers) · [中國站](https://console.cloud.tencent.com/edgeone/makers);**Cloudflare**:[Worker 後台](https://dash.cloudflare.com/),環境變數: -> - `DB_FORMAT`: 資料儲存格式:`map` (預設,整物件 JSON) / `key` (分 key 儲存) / `sql` (關聯表,與 Go 後端一致) -> - `DB_DRIVER`: 資料庫驅動:`auto` (預設,自動偵測) / `blob` / `cfkv` / `kv` / `d1` / `mysql` -> - 其餘可選變數參考**詳細部署指南**:[Cloudflare](https://doc.oplist.org/guide/installation/worker#deploy-to-cloudflare-workers) · [EdgeOne](https://doc.oplist.org/guide/installation/worker#deploy-to-edgeone) · [ESA](https://doc.oplist.org/guide/installation/worker#deploy-to-alibaba-cloud-esa) +> - 部署時請填寫 `JWT_SECRET`(≥16 字元的隨機字串,可用 `openssl rand -hex 32` 產生):用於 JWT 簽章與資料加密;未設定時會在初始化時自動產生並持久化 +> - 資料儲存預設自動選擇平台內建的持久化後端(`DB_DRIVER=auto`),無需設定;其餘變數見 [設定變數](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - 部署完成後首次造訪站點,依**安裝精靈**在瀏覽器中設定管理員帳號與密碼即可完成初始化;如需跳過精靈,可設定 `ADMIN_PASS` ## 功能簡介 @@ -61,7 +60,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea - **國內網盤**:阿里雲盤(開放平台/分享)、夸克網盤(開放平台/UC TV 版)、百度網盤(相簿)、115 網盤(開放平台/分享)、123 雲盤(開放平台/分享)、天翼雲盤(189/PC/TV)、中國移動雲盤(139/和彩雲)、沃家雲盤、迅雷雲盤、騰訊微雲、藍奏雲、PikPak(分享)、豆包網盤、光亞盤、超星小組網盤、聯想 NAS 分享、Teambition 網盤、WPS 網盤、阿里文件、HalalCloud、MediaTrack 等 - **國際網盤**:Google Drive(相簿)、OneDrive(應用/分享連結)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox 等 -- **物件儲存**:S3 相容(AWS/OSS/COS/MinIO 等)、又拍雲 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 +- **物件儲存**:S3 相容(AWS/OSS/COS/MinIO 等)、Cloudflare R2(Workers Binding 直連或 S3 API)、又拍雲 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 - **程式碼託管**:GitHub、GitHub Releases、CNB Releases - **網盤程式**:OpenList(分享)、AList V3、Cloudreve V3/V4、Kodbox(可道雲)、Seafile、Teldrive、Febbox 等 - **其他驅動**:網易雲音樂、Misskey、Emby、Cloudflare 圖床等 diff --git a/src/backend/drivers/r2/driver.ts b/src/backend/drivers/r2/driver.ts new file mode 100644 index 00000000..93f3cde8 --- /dev/null +++ b/src/backend/drivers/r2/driver.ts @@ -0,0 +1,733 @@ +// Cloudflare R2 存储驱动。 +// +// 与 OpenList Go 版 drivers/r2 的差异: +// - Go 版只有 S3 兼容 API(endpoint + key/secret); +// - 本驱动额外支持 **Workers R2 bucket binding 直连**(无需 API Token), +// 这是 Workers 平台上的首选接入方式: +// wrangler.jsonc → "r2_buckets": [{ "binding": "R2", "bucket_name": "..." }] +// admin 表单 → 存储 addition.bucket_binding = "R2" +// 未配置 binding(或运行在非 Workers 运行时)时自动回退 S3 兼容 API, +// 两条路径共用同一套目录语义(占位对象、root_folder_path、目录递归)。 +// +// 直链策略(重要,决定了下载走哪条链路): +// - 配了 custom_host(R2 公开域名 / 自定义域名)→ 直接拼公开直链,浏览器直连; +// - api 模式 → 复用 S3 的预签名直链(HttpOnly 服务端代理不需要,浏览器直接 GET); +// - binding 模式 → **返回空 raw_url 触发服务端流式下载**(raw.ts 的 +// createReadStream 分支)。原因:Workers 的 R2 *binding* 不提供 +// createPresignedUrl(预签名 URL 是 S3 接口的能力),binding 唯一能拿到 +// 字节的官方途径就是 get()。绑定公开域名可让直链回归 302 路径。 +import { + StorageDriver, + FileItem, + calcFileType, +} from "../../internal/driver/base" +import { sortFileItems } from "../../internal/driver/sort" +import { getEnvCtx } from "../../internal/model/db" +import { S3Driver, normalizeS3Addition } from "../s3/driver" +import { + getBaseName, + getDirName, + getKey, + getPlaceholderName, + isSubPath, + joinPath, +} from "../s3/util" +import { R2Addition, R2BucketLike, R2ObjectLike } from "./types" + +/** binding 缺省绑定名的探测顺序(admin 未显式填写时) */ +export const R2_DEFAULT_BINDING_NAMES = [ + "R2", + "R2_BUCKET", + "CF_R2", + "OPENLIST_R2", +] as const + +/** 判断对象是否符合 R2 bucket binding 的方法形态 */ +function isR2BucketLike(value: any): boolean { + if (!value || typeof value !== "object") return false + try { + return ( + typeof value.list === "function" && + typeof value.get === "function" && + typeof value.put === "function" && + typeof value.head === "function" && + typeof value.delete === "function" + ) + } catch { + return false + } +} + +/** + * 解析 R2 bucket binding。 + * + * env 与 globalThis 必须分别探测(与 KV 驱动同理):部分平台会把 binding + * 注入为全局标识符,而 CF Workers 只在 env 上。两者都不能命中时回退到请求级 + * env 上下文(getEnvCtx,由 index.ts 中间件写入)—— 驱动层 list/get/put + * 拿不到当前请求的 env 参数,这是 binding 能被解析到的关键兜底。 + */ +export function resolveR2Bucket( + bindingName?: string, + env?: any, +): { bucket: R2BucketLike | null; tried: string[] } { + const g = globalThis as any + const ctxEnv = (() => { + try { + return getEnvCtx() + } catch { + return null + } + })() + const sources: any[] = [env, ctxEnv, g] + const names = bindingName?.trim() + ? [bindingName.trim()] + : [...R2_DEFAULT_BINDING_NAMES] + + const tried: string[] = [] + for (const name of names) { + tried.push(name) + for (const source of sources) { + if (!source) continue + try { + const value = source[name] + if (isR2BucketLike(value)) return { bucket: value, tried } + } catch { + // 异常 getter:视为未绑定,继续探测 + } + } + } + return { bucket: null, tried } +} + +export function normalizeR2Addition(a: any): R2Addition { + const norm = { ...(a || {}) } as any + norm.bucket_binding = (norm.bucket_binding || "").trim() + norm.mode = (norm.mode || "auto").toString().trim().toLowerCase() + norm.account_id = (norm.account_id || "").trim() + norm.bucket = (norm.bucket || "").trim() + norm.endpoint = (norm.endpoint || "").trim() + norm.region = (norm.region || "").trim() || "auto" + norm.access_key_id = (norm.access_key_id || "").trim() + norm.secret_access_key = (norm.secret_access_key || "").trim() + norm.session_token = (norm.session_token || "").trim() + norm.root_folder_path = (norm.root_folder_path || "/").trim() + if (!norm.root_folder_path.startsWith("/")) { + norm.root_folder_path = "/" + norm.root_folder_path + } + norm.placeholder = (norm.placeholder || "").trim() + norm.sign_url_expire = Number(norm.sign_url_expire) || 4 + norm.order_by = norm.order_by || "name" + norm.order_direction = norm.order_direction || "asc" + return norm as R2Addition +} + +/** + * 该存储当前能否签发预签名「直传」URL(供 server 层计算 `direct_upload_tools`)。 + * + * 只有 api 模式可以:Workers 的 R2 绑定不提供预签名能力,binding 模式下 + * `getDirectUploadInfo()` 会明确报错。判定必须与 `R2Driver.resolveMode()` + * 一致(能解析到绑定 → binding;否则显式 mode=binding 仍算 binding;其余 → api), + * 否则前端会拿到一个必然失败的直传入口。 + */ +export function r2CanSignDirectUpload(addition: any, env?: any): boolean { + const norm = normalizeR2Addition(addition) + if (resolveR2Bucket(norm.bucket_binding || "", env).bucket) return false + return String(norm.mode || "auto").toLowerCase() !== "binding" +} + +export class R2Driver implements StorageDriver { + private addition: R2Addition + /** binding 不可用时的 S3 兼容回退实现 */ + private apiFallback: S3Driver | null = null + + constructor(addition: R2Addition) { + this.addition = normalizeR2Addition(addition) + } + + /** + * 解析绑定:请求级上下文(getEnvCtx,由 index.ts 中间件按请求注入) + * → globalThis(部分平台注入为全局标识符)。 + * + * 驱动实例是跨请求缓存的,因此**不能**把某个请求的 env / 绑定对象存进实例 + * (会随 isolate 生命周期滞留);每次操作都从当前请求上下文重新解析。 + */ + private resolveBucket(): { bucket: R2BucketLike | null; tried: string[] } { + return resolveR2Bucket(this.bindingName()) + } + + private bindingName(): string { + return this.addition.bucket_binding || "" + } + + private bindingError(tried: string[]): Error { + return new Error( + `[R2] R2 bucket binding not found (tried: ${tried.join(", ")}). ` + + `Declare it in wrangler.jsonc as ` + + `"r2_buckets": [{ "binding": "R2", "bucket_name": "your-bucket" }], ` + + `then set the storage's "bucket_binding" to the same name. ` + + `Alternatively clear "bucket_binding" to use the S3-compatible API (key/secret).`, + ) + } + + private bucket(): R2BucketLike { + const { bucket, tried } = this.resolveBucket() + if (bucket) return bucket + throw this.bindingError(tried) + } + + /** + * 当前走哪条路径。 + * + * - 能解析到 binding → binding(binding 优先,零密钥) + * - 显式 mode=binding 但解析不到 → 仍走 binding,由 bucket() 抛出带指引的错误 + * (不静默回退,避免用户以为在用 binding,实际走了别的凭据) + * - mode=api / auto 且无法解析 binding → S3 兼容 API + */ + private resolveMode(): "binding" | "api" { + const { bucket } = this.resolveBucket() + if (bucket) return "binding" + if (this.addition.mode === "binding") return "binding" + return "api" + } + + /** S3 兼容 API 回退(endpoint 可由 account_id 推导) */ + private api(): S3Driver { + if (!this.apiFallback) { + const a = this.addition + let endpoint = a.endpoint || "" + if (!endpoint && a.account_id) { + endpoint = `https://${a.account_id}.r2.cloudflarestorage.com` + } + this.apiFallback = new S3Driver( + normalizeS3Addition({ + ...a, + endpoint, + region: a.region || "auto", + // R2 的 ListObjectsV2 覆盖完整(v1 亦可用,v2 更省一轮 marker) + list_object_version: "v2", + // 目录占位与 root_folder_path 语义与 S3 完全一致 + }), + "Cloudflare R2", + ) + } + return this.apiFallback + } + + private requireApiConfig(): void { + const a = this.addition + if (!a.bucket) { + throw new Error( + "[R2] api 模式缺少 bucket(绑定 bucket_binding 后可用 binding 直连,无需 bucket 名)", + ) + } + if (!a.endpoint && !a.account_id) { + throw new Error( + "[R2] api 模式缺少 endpoint:填写 account_id(自动推导 endpoint)或显式 endpoint", + ) + } + if (!a.access_key_id || !a.secret_access_key) { + throw new Error( + "[R2] api 模式缺少 access_key_id / secret_access_key(R2 控制台 → Manage R2 API Tokens 创建)", + ) + } + } + + /** + * 初始化只做「配置侧检查」,一律不抛错: + * + * 1. binding 可能在「尚未注入请求级 env」的时机才可用(后台 load_all、 + * 存储保存流程),此处误判会让存储被标成 error / 直接禁用; + * 2. api 配置缺失同理——新建存储时管理员往往是先保存再补密钥。 + * + * 真正的失败在 list/get/put 时抛出(错误消息带可操作的修复指引)。 + */ + async init(): Promise { + if (this.resolveMode() === "binding") { + const { bucket, tried } = this.resolveBucket() + if (!bucket) console.warn(this.bindingError(tried).message) + return + } + try { + this.requireApiConfig() + } catch (e: any) { + console.warn(e?.message || String(e)) + } + } + + private rootPath(): string { + return this.addition.root_folder_path || "/" + } + + /** 虚拟物理路径 → 桶内 key(带根目录前缀) */ + private remoteKey(physicalPath: string): string { + const root = this.rootPath() + let combined = physicalPath || "/" + if (root !== "/" && !isSubPath(root, combined)) { + combined = joinPath(root, combined) + } + return getKey(combined, false) + } + + private isPlaceholder(name: string): boolean { + const placeholderName = getPlaceholderName(this.addition.placeholder) + return ( + name === placeholderName || + (!!this.addition.placeholder && name === this.addition.placeholder) + ) + } + + private modifiedOf(obj: R2ObjectLike): string { + const uploaded = obj?.uploaded + if (uploaded instanceof Date) return uploaded.toISOString() + if (typeof uploaded === "string" && uploaded) return uploaded + return new Date().toISOString() + } + + /** + * 列前缀下的全部对象与子目录(跟随 truncated/cursor 翻页)。 + * + * ⚠️ 单页上限 1000 且「设置了 include 时可能少于 limit」,因此翻页与否 + * 只看 truncated,不能用返回条数推断(云flare 文档明确警告)。 + */ + private async listPrefix(prefixKey: string): Promise<{ + objects: R2ObjectLike[] + dirs: string[] + }> { + const bucket = this.bucket() + const prefix = prefixKey ? getKey(prefixKey, true) : "" + const objects: R2ObjectLike[] = [] + const dirs: string[] = [] + let cursor: string | undefined + + for (;;) { + const page: any = await bucket.list({ + prefix, + delimiter: "/", + ...(cursor ? { cursor } : {}), + }) + for (const obj of page?.objects || []) { + if (obj?.key) objects.push(obj) + } + for (const p of page?.delimitedPrefixes || []) { + const name = p.replace(/\/+$/, "").split("/").filter(Boolean).pop() + if (name && !dirs.includes(name)) dirs.push(name) + } + if (!page?.truncated) break + if (!page?.cursor || page.cursor === cursor) break + cursor = page.cursor + } + + return { objects, dirs } + } + + /** 该 key 下是否存在对象(判断「目录」是否存在) */ + private async probePrefix(key: string): Promise { + const bucket = this.bucket() + const page: any = await bucket.list({ + prefix: key ? getKey(key, true) : "", + limit: 1, + }) + return (page?.objects?.length || 0) > 0 + } + + /** 自定义域名直链(R2 公开域名 / CNAME) */ + private customHostUrl(key: string): string { + const a = this.addition + const hostSplit = (a.custom_host || "").split("://") + let scheme = "https" + let host = a.custom_host + if ( + hostSplit.length === 2 && + (hostSplit[0] === "http" || hostSplit[0] === "https") + ) { + scheme = hostSplit[0] + host = hostSplit[1].replace(/\/+$/, "") + } + let pathPart = `/${getKey(key, false)}` + if (a.remove_bucket && a.bucket && pathPart.startsWith(`/${a.bucket}`)) { + pathPart = pathPart.substring(`/${a.bucket}`.length) || "/" + } + return `${scheme}://${host}${pathPart}` + } + + /** + * 生成直链。 + * + * binding 模式返回空串 = 交给 raw.ts 走服务端流式下载(见文件头注释); + * 配了 custom_host 时优先返回公开直链。 + * + * 注:api 模式不在这里换链——S3Driver 的 list()/get() 内部已经完成预签名, + * R2 在 api 模式下整体委托给 S3Driver(见各方法开头的分支)。 + */ + private objectRawUrl(key: string): { url: string } { + if (this.addition.custom_host) { + return { url: this.customHostUrl(key) } + } + return { url: "" } + } + + private toFileItem(obj: R2ObjectLike, name: string): FileItem { + return { + name, + size: Number(obj.size) || 0, + is_dir: false, + modified: this.modifiedOf(obj), + sign: obj.etag || obj.key, + type: calcFileType(name, false), + thumb: "", + raw_url: "", + } + } + + async list(_virtualPath: string, physicalPath: string): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().list(_virtualPath, physicalPath) + } + + const prefixKey = this.remoteKey(physicalPath) + const { objects, dirs } = await this.listPrefix(prefixKey) + const dirSet = new Set(dirs) + const items: FileItem[] = [] + + for (const dirName of dirs) { + items.push({ + name: dirName, + size: 0, + is_dir: true, + modified: new Date().toISOString(), + sign: joinPath(prefixKey, dirName), + type: 1, + }) + } + + const prefix = prefixKey ? getKey(prefixKey, true) : "" + for (const obj of objects) { + const rel = prefix + ? obj.key.startsWith(prefix) + ? obj.key.substring(prefix.length) + : getBaseName(obj.key) + : obj.key + if (!rel || rel.includes("/")) continue + if (this.isPlaceholder(rel)) continue + if (dirSet.has(rel)) continue + items.push(this.withRawUrl(this.toFileItem(obj, rel), obj.key)) + } + + return sortFileItems( + items, + this.addition.order_by || "name", + this.addition.order_direction || "asc", + ) + } + + /** 补直链(无 custom_host 时为空 → raw.ts 触发服务端流式下载) */ + private withRawUrl(item: FileItem, key: string): FileItem { + const link = this.objectRawUrl(key) + if (link.url) item.raw_url = link.url + return item + } + + async get(_virtualPath: string, physicalPath: string): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().get(_virtualPath, physicalPath) + } + + const key = this.remoteKey(physicalPath) + if (!key) { + return { + name: "root", + size: 0, + is_dir: true, + modified: new Date().toISOString(), + sign: "", + type: 1, + } + } + + const head = await this.bucket().head(key) + if (head) { + const item = this.toFileItem(head, getBaseName(key)) + return await this.withRawUrl(item, key) + } + + const isDir = await this.probePrefix(key) + if (isDir) { + return { + name: getBaseName(key), + size: 0, + is_dir: true, + modified: new Date().toISOString(), + sign: key, + type: 1, + } + } + throw new Error(`[R2] object not found: ${physicalPath}`) + } + + async mkdir(_virtualPath: string, physicalPath: string): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().mkdir(_virtualPath, physicalPath) + } + const key = this.remoteKey(physicalPath) + const placeholderKey = joinPath( + key, + getPlaceholderName(this.addition.placeholder), + ) + await this.bucket().put(placeholderKey, new Uint8Array(0)) + } + + async rename( + _virtualPath: string, + physicalPath: string, + newName: string, + ): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().rename(_virtualPath, physicalPath, newName) + } + const oldKey = this.remoteKey(physicalPath) + const newKey = joinPath(getDirName(oldKey), newName) + await this.moveKey(oldKey, newKey) + } + + async move( + srcDir: string, + dstDir: string, + names: string[], + srcPhys: string, + dstPhys: string, + ): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().move(srcDir, dstDir, names, srcPhys, dstPhys) + } + // srcPhys/dstPhys 已是源/目标项自身的物理路径,不得再拼 name + await this.moveKey(this.remoteKey(srcPhys), this.remoteKey(dstPhys)) + } + + async copy( + srcDir: string, + dstDir: string, + names: string[], + srcPhys: string, + dstPhys: string, + ): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().copy(srcDir, dstDir, names, srcPhys, dstPhys) + } + const srcKey = this.remoteKey(srcPhys) + const dstKey = this.remoteKey(dstPhys) + const head = await this.bucket().head(srcKey) + if (head) { + await this.copyObject(srcKey, dstKey) + return + } + await this.copyDirRecursive(srcKey, dstKey) + } + + /** 文件 key 改名/移动(文件 → copy+delete;目录 → 递归) */ + private async moveKey(srcKey: string, dstKey: string): Promise { + if (!srcKey) throw new Error("[R2] cannot move the bucket root") + const head = await this.bucket().head(srcKey) + if (head) { + await this.copyObject(srcKey, dstKey) + await this.bucket().delete(srcKey) + return + } + await this.copyDirRecursive(srcKey, dstKey) + await this.removeDirRecursive(srcKey) + } + + private async copyObject(srcKey: string, dstKey: string): Promise { + const bucket = this.bucket() + const obj = await bucket.get(srcKey) + if (!obj) throw new Error(`[R2] object not found: ${srcKey}`) + const body = await obj.arrayBuffer() + await bucket.put(dstKey, body, { + httpMetadata: { contentType: obj.httpMetadata?.contentType || undefined }, + }) + } + + private async copyDirRecursive( + srcKey: string, + dstKey: string, + ): Promise { + const bucket = this.bucket() + const srcPrefix = getKey(srcKey, true) + const dstPrefix = getKey(dstKey, true) + const { objects } = await this.listPrefix(srcKey) + for (const obj of objects) { + const rel = obj.key.startsWith(srcPrefix) + ? obj.key.substring(srcPrefix.length) + : getBaseName(obj.key) + if (!rel) continue + const fresh = await bucket.get(obj.key) + if (!fresh) continue + const body = await fresh.arrayBuffer() + await bucket.put(dstPrefix + rel, body, { + httpMetadata: { + contentType: fresh.httpMetadata?.contentType || undefined, + }, + }) + } + } + + async remove( + _virtualPath: string, + physicalPath: string, + names: string[], + ): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().remove(_virtualPath, physicalPath, names) + } + // physicalPath 已是目标项自身路径(op/storage.ts removeItems 逐个调用) + const key = this.remoteKey(physicalPath) + if (!key) return + const head = await this.bucket().head(key) + if (head) { + await this.bucket().delete(key) + return + } + await this.removeDirRecursive(key) + } + + private async removeDirRecursive(dirKey: string): Promise { + const bucket = this.bucket() + const { objects } = await this.listPrefix(dirKey) + const keys = objects.map((o) => o.key) + // R2 delete 支持批量数组(单次上限 1000) + for (let i = 0; i < keys.length; i += 1000) { + await bucket.delete(keys.slice(i, i + 1000)) + } + } + + async put( + _virtualPath: string, + physicalPath: string, + content: Buffer | Uint8Array, + ): Promise { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().put(_virtualPath, physicalPath, content as any) + } + const key = this.remoteKey(physicalPath) + // 拷贝到独立 ArrayBuffer:Node 的 Buffer 可能是池化视图(底层 slab 很大), + // 直接交给 workerd 会连带整块内存,且部分运行时对 Buffer 类型不接受。 + const bytes = new Uint8Array(content) + await this.bucket().put(key, bytes, { + httpMetadata: { contentType: contentTypeOf(getBaseName(key)) }, + }) + } + + /** + * 服务端流式读取(raw.ts 的 createReadStream 分支): + * binding 模式的下载主路径,支持 Range(offset/length 语义,与 S3 的 + * start/end 不同,必须转换)。 + */ + async createReadStream( + physicalPath: string, + range?: { start: number; end: number }, + ): Promise> { + if (this.resolveMode() === "api") { + throw new Error( + "[R2] api 模式请使用预签名直链下载(raw_url 已签发);服务端流式下载仅 binding 模式支持", + ) + } + const key = this.remoteKey(physicalPath) + const bucket = this.bucket() + const obj = range + ? await bucket.get(key, { + range: { + offset: range.start, + length: range.end - range.start + 1, + }, + }) + : await bucket.get(key) + if (!obj) throw new Error(`[R2] object not found: ${physicalPath}`) + if (obj.body) return obj.body + // 极老运行时无 body 字段:整体读入内存后返回 + const buf = await obj.arrayBuffer() + return new ReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(buf)) + controller.close() + }, + }) + } + + async other(method: string, path: string, body?: any): Promise { + if (method === "direct_upload" || method === "get_direct_upload_info") { + const fileName = body?.name || body?.fileName || getBaseName(path) + const dstDir = getDirName(path) + return await this.getDirectUploadInfo(dstDir, fileName) + } + if (this.resolveMode() === "api") { + return await this.api().other(method, path, body) + } + throw new Error(`Unsupported method ${method}`) + } + + async getDirectUploadInfo( + dstDir: string, + fileName: string, + ): Promise<{ upload_url: string; method: string }> { + if (this.resolveMode() === "api") { + this.requireApiConfig() + return await this.api().getDirectUploadInfo(dstDir, fileName) + } + // binding 无法签发 PUT 预签名 URL(Workers 绑定不暴露 createPresignedUrl) + throw new Error( + "[R2] 直传需要在存储配置里填写 account_id + access_key_id / secret_access_key" + + "(S3 兼容模式),binding 模式不提供预签名直传 URL", + ) + } + + drop(): void { + this.apiFallback?.drop?.() + } +} + +const CONTENT_TYPE_BY_EXT: Record = { + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + ico: "image/x-icon", + avif: "image/avif", + bmp: "image/bmp", + mp4: "video/mp4", + webm: "video/webm", + mkv: "video/x-matroska", + mov: "video/quicktime", + mp3: "audio/mpeg", + flac: "audio/flac", + aac: "audio/aac", + wav: "audio/wav", + ogg: "audio/ogg", + m4a: "audio/mp4", + txt: "text/plain; charset=utf-8", + md: "text/markdown; charset=utf-8", + json: "application/json", + css: "text/css; charset=utf-8", + js: "text/javascript; charset=utf-8", + html: "text/html; charset=utf-8", + xml: "application/xml", + pdf: "application/pdf", + zip: "application/zip", + gz: "application/gzip", + tar: "application/x-tar", + "7z": "application/x-7z-compressed", + rar: "application/vnd.rar", +} + +function contentTypeOf(fileName: string): string { + const ext = (fileName.split(".").pop() || "").toLowerCase() + return CONTENT_TYPE_BY_EXT[ext] || "application/octet-stream" +} diff --git a/src/backend/drivers/r2/r2.test.ts b/src/backend/drivers/r2/r2.test.ts new file mode 100644 index 00000000..d5989e26 --- /dev/null +++ b/src/backend/drivers/r2/r2.test.ts @@ -0,0 +1,522 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { + R2Driver, + normalizeR2Addition, + r2CanSignDirectUpload, + resolveR2Bucket, +} from "./driver" +import { R2BucketLike, R2ObjectLike } from "./types" +import { + __resetDbCacheForTest as resetEnv, + setEnvCtx, +} from "../../internal/model/db" +import { getDriver } from "../../internal/op/storage" +import { normalizeDriver } from "../../server/admin" + +/** + * R2 binding 模式的端到端驱动测试(内存版 R2Bucket)。 + * + * 覆盖要点: + * 1. 绑定解析:env / 请求级 env 上下文 / globalThis 三处探测 + * 2. 列目录:delimiter 产生的目录 + 对象(含占位对象过滤、翻页 cursor) + * 3. 目录语义:mkdir 占位对象、remove/rename/move 的文件与目录分支 + * 4. 流式下载:range 偏移换算(S3 的 start/end → R2 的 offset/length) + * 5. api 回退:无绑定 + key/secret 时走 S3 兼容路径(不误报绑定错误) + */ + +interface StoredObject { + body: Uint8Array + uploaded: Date + contentType?: string +} + +class MockR2Bucket implements R2BucketLike { + objects = new Map() + /** 记录 list 调用,用于断言分页 cursor 语义 */ + listCalls: Array> = [] + /** 模拟单页只返回 1 条文件,用于验证 truncated/cursor 翻页 */ + pageSize = 1000 + /** 带 delimiter 的列举同样分页(R2 任一页都可能 truncated) */ + delimiterPageSize = 1000 + + private keyOf(obj: StoredObject | R2ObjectLike): string { + return (obj as any).key + } + + async head(key: string): Promise { + const obj = this.objects.get(key) + if (!obj) return null + return { + key, + size: obj.body.byteLength, + etag: `etag-${key}`, + uploaded: obj.uploaded, + httpMetadata: { contentType: obj.contentType }, + } + } + + async get(key: string, options?: any): Promise { + const obj = this.objects.get(key) + if (!obj) return null + let body = obj.body + const range = options?.range + if (range && !(range instanceof Headers)) { + const offset = range.offset ?? 0 + const length = range.length ?? body.byteLength - offset + body = body.subarray(offset, offset + length) + } + return { + key, + size: obj.body.byteLength, + etag: `etag-${key}`, + uploaded: obj.uploaded, + httpMetadata: { contentType: obj.contentType }, + body: new ReadableStream({ + start: (controller) => { + controller.enqueue(body) + controller.close() + }, + }), + arrayBuffer: async () => + body.buffer.slice( + body.byteOffset, + body.byteOffset + body.byteLength, + ) as ArrayBuffer, + } + } + + async put( + key: string, + value: Uint8Array | ArrayBuffer | string | null, + options?: { httpMetadata?: { contentType?: string } }, + ): Promise { + let bytes: Uint8Array + if (value instanceof Uint8Array) { + bytes = new Uint8Array(value) + } else if (value instanceof ArrayBuffer) { + bytes = new Uint8Array(value) + } else { + bytes = new TextEncoder().encode(String(value ?? "")) + } + this.objects.set(key, { + body: bytes, + uploaded: new Date("2026-01-01T00:00:00Z"), + contentType: options?.httpMetadata?.contentType, + }) + return { + key, + size: bytes.byteLength, + etag: `etag-${key}`, + uploaded: new Date("2026-01-01T00:00:00Z"), + } + } + + async delete(keys: string | string[]): Promise { + for (const key of Array.isArray(keys) ? keys : [keys]) { + this.objects.delete(key) + } + } + + async list(options?: { + prefix?: string + delimiter?: string + cursor?: string + limit?: number + }): Promise { + this.listCalls.push({ ...options }) + const prefix = options?.prefix || "" + const delimiter = options?.delimiter + const allKeys = [...this.objects.keys()] + .filter((k) => k.startsWith(prefix)) + .sort() + + const dirSet = new Set() + const fileKeys: string[] = [] + for (const key of allKeys) { + if (delimiter) { + const rest = key.substring(prefix.length) + const idx = rest.indexOf(delimiter) + if (idx >= 0) { + dirSet.add(prefix + rest.substring(0, idx + 1)) + continue + } + } + fileKeys.push(key) + } + + // 分页:模拟 truncated/cursor(pageSize=1 时逐条翻页)。 + // cursor 形如 "obj:|dir:",对象与目录各自推进; + // R2 的 cursor 是不透明令牌,这里只需保证「传回上一页 cursor 能续查」。 + const [objCursor, dirCursor] = String(options?.cursor || "0|0") + .split("|") + .map((s) => Number(s.replace(/[^0-9]/g, "")) || 0) + const eff = Math.max( + 1, + delimiter === undefined ? this.pageSize : this.delimiterPageSize, + ) + const pageKeys = fileKeys.slice(objCursor, objCursor + eff) + const dirList = delimiter ? [...dirSet] : [] + const pageDirs = dirList.slice(dirCursor, dirCursor + eff) + const nextObj = objCursor + pageKeys.length + const nextDir = dirCursor + pageDirs.length + const truncated = nextObj < fileKeys.length || nextDir < dirList.length + + return { + objects: pageKeys.map((key) => { + const obj = this.objects.get(key)! + return { + key, + size: obj.body.byteLength, + etag: `etag-${key}`, + uploaded: obj.uploaded, + httpMetadata: { contentType: obj.contentType }, + } + }), + delimitedPrefixes: pageDirs, + truncated, + cursor: truncated ? `obj:${nextObj}|dir:${nextDir}` : undefined, + } + } +} + +function collect(stream: ReadableStream): Promise { + return (async () => { + const reader = stream.getReader() + const chunks: Uint8Array[] = [] + for (;;) { + const { done, value } = await reader.read() + if (done) break + if (value) chunks.push(value) + } + const total = chunks.reduce((sum, c) => sum + c.byteLength, 0) + const out = new Uint8Array(total) + let offset = 0 + for (const chunk of chunks) { + out.set(chunk, offset) + offset += chunk.byteLength + } + return out + })() +} + +test("normalizeR2Addition 处理默认值与前缀", () => { + const norm = normalizeR2Addition({ + bucket_binding: " R2 ", + bucket: " mybucket ", + root_folder_path: "media", + sign_url_expire: "0", + }) + assert.equal(norm.bucket_binding, "R2") + assert.equal(norm.bucket, "mybucket") + assert.equal(norm.root_folder_path, "/media") + assert.equal(norm.region, "auto") + assert.equal(norm.sign_url_expire, 4) // 0 → 回退默认 4 小时 +}) + +test("resolveR2Bucket:env / 请求级上下文 / globalThis 三处探测", () => { + resetEnv() + const bucket = new MockR2Bucket() + + // 1) 显式 env 参数 + const direct = resolveR2Bucket("R2", { R2: bucket }) + assert.equal(direct.bucket, bucket) + + // 2) 请求级 env 上下文(驱动层拿不到 env 时的兜底) + setEnvCtx({ OPENLIST_R2: bucket }) + const viaCtx = resolveR2Bucket("OPENLIST_R2") + assert.equal(viaCtx.bucket, bucket) + + // 3) 同名变量只是普通字符串时必须被判为「不可用」 + resetEnv() + const invalid = resolveR2Bucket("R2", { R2: "not-a-binding" }) + assert.equal(invalid.bucket, null) + assert.deepEqual(invalid.tried, ["R2"]) + + resetEnv() +}) + +test("list:目录 + 文件、占位对象过滤、翻页 cursor 跟随 truncated", async () => { + resetEnv() + const bucket = new MockR2Bucket() + bucket.pageSize = 1 // 强制逐条翻页 + bucket.delimiterPageSize = 1 // 目录/文件同样逐条翻页,覆盖 cursor 分支 + await bucket.put("docs/a.txt", "a") + await bucket.put("docs/b.txt", "b") + await bucket.put("docs/.openlist", "") + await bucket.put("docs/sub/c.txt", "c") + setEnvCtx({ R2: bucket }) + + const driver = new R2Driver({ bucket_binding: "R2", bucket: "b" }) + await driver.init() + + // 顶层:只有 docs 一个目录(文件都在它下面) + const root = await driver.list("/", "/") + assert.deepEqual( + root.map((i) => `${i.is_dir ? "d" : "f"}:${i.name}`), + ["d:docs"], + `顶层应只有 docs 目录,实际: ${JSON.stringify(root)}`, + ) + assert.equal(root[0].is_dir, true) + assert.equal(root[0].type, 1) + + // docs 内:sub 目录 + a.txt/b.txt(b.txt 位于第二页,验证 cursor 翻页) + const inner = await driver.list("/docs", "/docs") + const byName = new Map(inner.map((i) => [i.name, i])) + assert.ok(byName.get("sub")?.is_dir, "子目录 sub 应出现") + assert.ok( + byName.get("a.txt") && !byName.get("a.txt")!.is_dir, + "a.txt 应是文件", + ) + assert.ok(byName.get("b.txt"), "翻页后第二条文件必须出现在结果里") + assert.ok(!byName.has(".openlist"), "占位对象不应出现在列表里") + // 子目录里的对象不应泄漏到当前层 + assert.ok(!byName.has("c.txt"), "深层文件不应出现在 docs 层") + + // 翻页语义:pageSize=1 时必须发出多次带 cursor 的 list 调用 + const pagedCalls = bucket.listCalls.filter((c) => c.prefix === "docs/") + assert.ok( + pagedCalls.some((c) => typeof c.cursor === "string" && c.cursor.length > 0), + `pageSize=1 时应发出带 cursor 的翻页请求,实际: ${JSON.stringify(pagedCalls)}`, + ) + + resetEnv() +}) + +test("root_folder_path 前缀作用于 list/get/put 的桶内 key", async () => { + resetEnv() + const bucket = new MockR2Bucket() + await bucket.put("media/inside.txt", "hi") + await bucket.put("outside.txt", "no") + setEnvCtx({ R2: bucket }) + + const driver = new R2Driver({ + bucket_binding: "R2", + root_folder_path: "/media", + }) + const items = await driver.list("/", "/") + assert.deepEqual( + items.map((i) => i.name), + ["inside.txt"], + ) + + const file = await driver.get("/inside.txt", "/inside.txt") + assert.equal(file.name, "inside.txt") + assert.equal(file.size, 2) + + await driver.put("/new.txt", "/new.txt", new TextEncoder().encode("xy")) + assert.ok(bucket.objects.has("media/new.txt"), "put 必须落在根目录前缀内") + + resetEnv() +}) + +test("mkdir 写占位对象;remove 区分文件与目录", async () => { + resetEnv() + const bucket = new MockR2Bucket() + setEnvCtx({ R2: bucket }) + const driver = new R2Driver({ bucket_binding: "R2" }) + + await driver.mkdir("/empty", "/empty") + assert.ok(bucket.objects.has("empty/.openlist"), "mkdir 应写占位对象") + + await bucket.put("dir/x.bin", "1234") + await bucket.put("dir/y.bin", "5678") + await driver.remove("/dir", "/dir", ["dir"]) + assert.equal(bucket.objects.size, 1, "目录递归删除只应留下 empty/.openlist") + + await bucket.put("file.bin", "zz") + await driver.remove("/file.bin", "/file.bin", ["file.bin"]) + assert.ok(!bucket.objects.has("file.bin"), "文件删除走单 key delete") + + resetEnv() +}) + +test("rename/move/copy:文件 copy+delete,目录递归", async () => { + resetEnv() + const bucket = new MockR2Bucket() + setEnvCtx({ R2: bucket }) + const driver = new R2Driver({ bucket_binding: "R2" }) + + await bucket.put("a.txt", "aaa") + await driver.rename("/a.txt", "/a.txt", "b.txt") + assert.ok(!bucket.objects.has("a.txt")) + assert.equal( + new TextDecoder().decode(bucket.objects.get("b.txt")!.body), + "aaa", + ) + + // 目录移动 + await bucket.put("src/1.txt", "1") + await bucket.put("src/2.txt", "2") + await driver.move("/src", "/dst", ["src"], "/src", "/dst") + assert.ok(!bucket.objects.has("src/1.txt")) + assert.equal( + new TextDecoder().decode(bucket.objects.get("dst/1.txt")!.body), + "1", + ) + assert.equal( + new TextDecoder().decode(bucket.objects.get("dst/2.txt")!.body), + "2", + ) + + // 目录复制(源保留) + await bucket.put("cp/9.txt", "9") + await driver.copy("/cp", "/cp2", ["cp"], "/cp", "/cp2") + assert.ok(bucket.objects.has("cp/9.txt"), "复制不删除源") + assert.ok(bucket.objects.has("cp2/9.txt")) + + resetEnv() +}) + +test("createReadStream:无 range 全量、有 range 按 offset/length 切片", async () => { + resetEnv() + const bucket = new MockR2Bucket() + setEnvCtx({ R2: bucket }) + const driver = new R2Driver({ bucket_binding: "R2" }) + + const content = new TextEncoder().encode("0123456789") + await driver.put("/num.bin", "/num.bin", content) + + const full = await collect(await driver.createReadStream("/num.bin")) + assert.equal(new TextDecoder().decode(full), "0123456789") + + const sliced = await collect( + await driver.createReadStream("/num.bin", { start: 2, end: 4 }), + ) + assert.equal(new TextDecoder().decode(sliced), "234") + + resetEnv() +}) + +test("binding 缺失:mode=auto 回退 api(不再抛绑定错误);mode=binding 明确报错", async () => { + resetEnv() + const calls: string[] = [] + const originalFetch = globalThis.fetch + globalThis.fetch = (async (input: any) => { + calls.push(String(input)) + return new Response( + `false`, + { status: 200, headers: { "content-type": "application/xml" } }, + ) + }) as typeof fetch + + try { + // auto:无绑定 + 完整 api 配置 → 走 S3 兼容 + const apiDriver = new R2Driver({ + mode: "auto", + account_id: "acc", + bucket: "mybucket", + access_key_id: "k", + secret_access_key: "s", + }) + await apiDriver.init() + const items = await apiDriver.list("/", "/") + assert.deepEqual(items, []) + assert.equal(calls.length, 1, "应至少请求一次 S3 ListObjects") + assert.ok( + calls[0].startsWith("https://mybucket.acc.r2.cloudflarestorage.com/"), + `endpoint 应由 account_id 推导,实际: ${calls[0]}`, + ) + + // binding:显式要求绑定但不能解析 → 明确报错(不静默回退) + const bindingDriver = new R2Driver({ + mode: "binding", + bucket_binding: "R2", + }) + await assert.rejects( + () => bindingDriver.list("/", "/"), + /binding not found/i, + ) + + // auto 且无任何可用路径 → init 不抛错(保留存储可保存),实际操作时给出配置指引 + const broken = new R2Driver({ mode: "auto" }) + await broken.init() + await assert.rejects(() => broken.list("/", "/"), /缺少 bucket/) + } finally { + globalThis.fetch = originalFetch + resetEnv() + } +}) + +test("getDriver 路由:Cloudflare R2 与各别名均命中 R2Driver(S3 不受影响)", async () => { + const storageConfig = { + id: 901, + driver: "Cloudflare R2", + modified: new Date().toISOString(), + addition: JSON.stringify({ mode: "auto" }), + } + resetEnv() + const driver = await getDriver("Cloudflare R2", storageConfig) + assert.ok(driver instanceof R2Driver, "'Cloudflare R2' 应命中 R2Driver") + for (const alias of ["cloudflare_r2", "CloudflareR2", "cfr2", "r2", "R2"]) { + const d = await getDriver(alias, { + ...storageConfig, + id: 901 + Math.floor(Math.random() * 1000), + modified: new Date().toISOString(), + addition: JSON.stringify({ mode: "auto" }), + }) + assert.ok(d instanceof R2Driver, `别名 ${alias} 应命中 R2Driver`) + } + // 回归护栏:S3 家族不能被 R2 分支吃掉(此前 `r2` 就在 S3 分支的名单里) + const s3 = await getDriver("S3", { + ...storageConfig, + id: 9500, + driver: "S3", + addition: JSON.stringify({ + bucket: "b", + endpoint: "https://s3.example.com", + access_key_id: "k", + secret_access_key: "s", + }), + }) + assert.ok(!(s3 instanceof R2Driver), "'S3' 必须仍然命中 S3Driver") + resetEnv() +}) + +test("驱动名归一:Cloudflare R2 的写法都指向它,S3 家族不受影响", () => { + // 与 server/admin.ts 的 normalizeDriver 保持一致(后台表单与存储保存都走它) + assert.equal(normalizeDriver("Cloudflare R2"), "Cloudflare R2") + assert.equal(normalizeDriver("cloudflare_r2"), "Cloudflare R2") + assert.equal(normalizeDriver("R2"), "Cloudflare R2") + assert.equal(normalizeDriver("r2"), "Cloudflare R2") + assert.equal(normalizeDriver("cfr2"), "Cloudflare R2") + // 回归护栏:S3 家族不能被 R2 的名字归一吞掉 + assert.equal(normalizeDriver("S3"), "S3") + assert.equal(normalizeDriver("s3"), "S3") + assert.equal(normalizeDriver("minio"), "S3") +}) + +test("r2CanSignDirectUpload:只有 api 模式可签发预签名直传", () => { + resetEnv() + const bucket = new MockR2Bucket() + + // 无绑定(走 S3 兼容 api)→ 可签发 + assert.equal( + r2CanSignDirectUpload({ + bucket: "b", + access_key_id: "k", + secret_access_key: "s", + }), + true, + ) + + // 能解析到绑定 → binding 模式(即使同时配了密钥,binding 也优先)→ 不可签发 + setEnvCtx({ R2: bucket }) + assert.equal( + r2CanSignDirectUpload({ + bucket_binding: "R2", + bucket: "b", + access_key_id: "k", + secret_access_key: "s", + }), + false, + ) + + // 显式 mode=binding 且解析不到绑定 → 仍不可签发(与 resolveMode 判定一致) + resetEnv() + assert.equal( + r2CanSignDirectUpload({ mode: "binding", bucket_binding: "R2" }), + false, + ) + resetEnv() +}) diff --git a/src/backend/drivers/r2/types.ts b/src/backend/drivers/r2/types.ts new file mode 100644 index 00000000..13222d5a --- /dev/null +++ b/src/backend/drivers/r2/types.ts @@ -0,0 +1,106 @@ +// Cloudflare R2 驱动类型定义。 +// +// 两种接入方式(同一驱动内统一): +// 1. binding 模式:Workers 的 R2 bucket binding 直连(无需 API Token) +// 2. key/api 模式:S3 兼容 API(AccessKeyId + SecretAccessKey,可跨账号/外部访问) +export interface R2Addition { + /** R2 bucket binding 变量名(wrangler r2_buckets[].binding),binding 模式必填 */ + bucket_binding?: string + /** + * 接入方式选择: + * auto(默认)- 配了 bucket_binding 用 binding,否则回退 S3 兼容 API + * binding - 强制 binding(未配置/无法解析时直接报错,不做静默回退) + */ + mode?: "auto" | "binding" | string + /** 账号 ID(api 模式的 endpoint 可按它自动推导) */ + account_id?: string + /** 桶名。binding 模式下仅用于展示;api 模式必填 */ + bucket?: string + /** endpoint(留空时按 account_id 推导 https://.r2.cloudflarestorage.com) */ + endpoint?: string + /** 签名区域,R2 固定为 auto */ + region?: string + access_key_id?: string + secret_access_key?: string + session_token?: string + /** 存储根目录(桶内前缀) */ + root_folder_path?: string + /** 目录占位文件名(空值等价默认 .openlist) */ + placeholder?: string + /** api 模式预签名直链有效期(小时,R2 上限 7 天) */ + sign_url_expire?: number | string + /** 自定义域名(R2 公开域名 / CNAME),设置后直链走该域名 */ + custom_host?: string + /** 自定义域名去掉桶名前缀(r2.dev / 已绑定为桶根域的域名) */ + remove_bucket?: boolean + order_by?: "name" | "size" | "modified" | string + order_direction?: "asc" | "desc" | string +} + +/** + * R2 bucket binding 的最小接口(结构子集,避免依赖 @cloudflare/workers-types)。 + * + * 与 KV 同理必须做接口形状校验:env 里同名变量可能只是普通字符串。 + */ +/** + * R2 的 range 语义(与 S3 的 start/end 不同,注意换算): + * - { offset, length }:从 offset 起读 length 字节(length 可省略 = 到末尾) + * - { suffix }:读取末尾 suffix 字节 + */ +export interface R2RangeLike { + offset?: number + length?: number + suffix?: number +} + +export interface R2BucketLike { + head(key: string): Promise + get( + key: string, + options?: { + range?: R2RangeLike | Headers + onlyIf?: Headers + }, + ): Promise + put( + key: string, + value: + | ReadableStream + | ArrayBuffer + | ArrayBufferView + | string + | Blob + | null, + options?: { + httpMetadata?: { contentType?: string } + }, + ): Promise + delete(keys: string | string[]): Promise + list(options?: { + prefix?: string + delimiter?: string + cursor?: string + limit?: number + }): Promise +} + +export interface R2ObjectLike { + key: string + size: number + etag: string + uploaded?: Date | string + httpMetadata?: { contentType?: string } +} + +export interface R2ObjectBodyLike extends R2ObjectLike { + body?: ReadableStream + arrayBuffer(): Promise +} + +export interface R2ListResultLike { + objects?: R2ObjectLike[] + delimitedPrefixes?: string[] + truncated?: boolean + /** 不透明分页令牌,仅 truncated=true 时出现;传回下一页 list */ + cursor?: string +} diff --git a/src/backend/internal/model/db.ts b/src/backend/internal/model/db.ts index 4cb721b4..9819a7e9 100644 --- a/src/backend/internal/model/db.ts +++ b/src/backend/internal/model/db.ts @@ -934,6 +934,17 @@ export function setEnvCtx(env: any) { } } +/** + * 读取请求级 env 上下文(由 setEnvCtx 写入)。 + * + * 供**驱动层**解析平台绑定(如 R2 bucket binding):驱动的 list/get/put + * 签名里没有 env 参数,而 Workers 只会把绑定放进请求的 env,因此必须经这里 + * 回取。拿不到时返回 null(非 Workers 运行时 / 单元测试),由调用方决定回退。 + */ +export function getEnvCtx(): any | null { + return globalEnvCtx || null +} + // 已知的旧默认值 → 当前默认值迁移表。 // 修复「开发环境(无 KV,用新默认值)与生产环境(KV 里保存了旧默认值)不一致」: // 早期默认 logo/favicon 为空或 res.oplist.org 旧地址,已写入 KV 的旧值不会被 diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 6452d737..810acb4d 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -60,6 +60,7 @@ import { WebdavDriver } from "../../drivers/webdav/driver" import { WoPanDriver, normalizeWoPanAddition } from "../../drivers/wopan/driver" import { MoPanDriver } from "../../drivers/mopan/driver" import { S3Driver, normalizeS3Addition } from "../../drivers/s3/driver" +import { R2Driver } from "../../drivers/r2/driver" import { WeiyunDriver, normalizeWeiyunAddition, @@ -775,13 +776,25 @@ async function createDriver( driver = new WebdavDriver(addition) await driver.init?.() } else if ( + // Cloudflare R2 独立驱动("Cloudflare R2" / "r2" / "cloudflare_r2" / "cfr2" + // 归一化后都会落到这里)。必须在下面的 S3 分支之前匹配。 + normDriver === "r2" || + normDriver === "cloudflarer2" || + normDriver === "cfr2" || + normDriver === "r2native" + ) { + const addition = parseAddition(storageConfig) + driver = new R2Driver(addition) + await driver.init?.() + } else if ( + // S3 兼容家族。注意:`r2` 已由上一分支接管,这里不能再列它 + // (会被上方分支拦截成死代码,且让「r2 走哪条路」变得难读)。 normDriver === "s3" || normDriver === "doge" || normDriver === "dogecloud" || normDriver === "minio" || normDriver === "ceph" || normDriver === "aws" || - normDriver === "r2" || normDriver === "b2" || normDriver === "cos" || normDriver === "oss" || diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 8b7f00ad..0c8f1488 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -172,7 +172,6 @@ export const normalizeDriver = (driverName: string): string => { "minio", "ceph", "aws", - "r2", "b2", "cos", "oss", @@ -180,6 +179,13 @@ export const normalizeDriver = (driverName: string): string => { ].includes(norm) ) return "S3" + // Cloudflare R2 是独立驱动(支持 Workers bucket binding 直连),不再归一到 S3; + // 历史配置里 driver=S3 且 endpoint 指向 R2 的记录仍走 S3 驱动,不受影响。 + // + // 注意:上面的精确匹配已能命中 "Cloudflare R2"(norm 后会得到 cloudflarer2), + // 这里兜底的是它的各种简写/旧写法,避免落到 "S3" 或原样返回。 + if (norm === "r2" || norm === "cfr2" || norm === "cloudflarer2") + return "Cloudflare R2" // releases 别名必须在通用 github 别名之前,否则会被归一到 GitHub API if (norm.startsWith("githubrelease")) return "GitHub Releases" if (norm.startsWith("cnbrelease")) return "CNB Releases" @@ -2224,6 +2230,132 @@ const driverConfigs: Record = { no_overwrite_upload: true, }, }, + "Cloudflare R2": { + name: "Cloudflare R2", + default_mount_path: "/r2", + common: COMMON_FIELDS, + additional: [ + { + name: "bucket_binding", + type: "string", + default: "", + required: false, + help: "wrangler.jsonc 中 r2_buckets 的 binding 名(如 R2)。填写后优先走 Workers 原生绑定直连,无需 API Token。", + }, + { + name: "mode", + type: "select", + options: "auto,binding", + default: "auto", + required: false, + help: "auto=有绑定用绑定、否则走 S3 兼容 API;binding=只用绑定(缺绑定直接报错,不静默回退)。", + }, + { + name: "account_id", + type: "string", + default: "", + required: false, + help: "Cloudflare 账号 ID,用于自动推导 endpoint(https://.r2.cloudflarestorage.com)。", + }, + { + name: "bucket", + type: "string", + default: "", + required: false, + help: "桶名。binding 模式仅用于展示与自定义域名去前缀;api 模式必填。", + }, + { + name: "endpoint", + type: "string", + default: "", + required: false, + help: "留空则由 account_id 推导;也可填自定义 S3 兼容网关地址。", + }, + { + name: "region", + type: "string", + default: "auto", + required: false, + }, + { + name: "access_key_id", + type: "string", + default: "", + required: false, + help: "api 模式必填(R2 → Manage R2 API Tokens)", + }, + { + name: "secret_access_key", + type: "string", + default: "", + required: false, + }, + { + name: "session_token", + type: "string", + default: "", + required: false, + }, + { + name: "root_folder_path", + type: "string", + default: "/", + required: false, + }, + { + name: "custom_host", + type: "string", + default: "", + required: false, + help: "R2 公开域名(r2.dev)或自定义域名;填写后直链走该域名(需桶已开启公开访问)。", + }, + { + name: "sign_url_expire", + type: "number", + default: "4", + required: false, + help: "api 模式预签名直链有效期(小时,R2 上限 7 天)", + }, + { + name: "placeholder", + type: "string", + default: "", + required: false, + }, + { + name: "remove_bucket", + type: "bool", + default: "false", + required: false, + help: "true", + }, + { + name: "order_by", + type: "select", + options: "name,size,modified", + default: "name", + required: false, + }, + { + name: "order_direction", + type: "select", + options: "asc,desc", + default: "asc", + required: false, + }, + ], + config: { + name: "Cloudflare R2", + local_sort: true, + only_local: false, + only_proxy: false, + no_cache: false, + no_upload: false, + need_ms: false, + default_root: "/", + check_status: true, + }, + }, S3: { name: "S3", default_mount_path: "/s3", diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index 4ed54fa6..bd32e653 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -53,6 +53,7 @@ import { parseZip, extractZipEntry, ZipArchive } from "../internal/archive/zip" import { assertSafeUrl, getTrustedHosts } from "../pkg/http" import { invalidatePaths } from "../internal/cache" import { seedRouter } from "./seed" +import { r2CanSignDirectUpload } from "../drivers/r2/driver" /** * 该路径所属存储是否禁止目录列表(对齐 Go handles.FsList 的 DisableIndex 判断)。 @@ -535,9 +536,28 @@ fsRouter.post("/list", async (c) => { if (!storage) return [] const tools: string[] = [] const driver = (storage as any) - // S3 系列驱动支持 s3_presigned 直传 - if (/^(s3|minio|cos|oss|r2|b2|cloudflare_r2)/i.test(driver.driver || "")) { - tools.push("s3_presigned") + // S3 系列驱动支持 s3_presigned 直传。 + // 必须按「归一化后的驱动名」判断(小写 + 去非字母数字):驱动名里可能含 + // 空格(如 "Cloudflare R2"),拿原始字符串做 `^` 锚定正则会漏判。 + const normDriverName = String(driver.driver || "") + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + if ( + /^(s3|minio|cos|oss|r2|b2|cfr2|cloudflarer2)/.test(normDriverName) + ) { + // Cloudflare R2 的例外:binding 模式没有预签名能力,直传必然失败, + // 因此只在解析不到绑定(即实际会走 S3 兼容 API)时才宣告该工具。 + let canSignDirectUpload = true + if (/^(r2|cfr2|cloudflarer2)$/.test(normDriverName)) { + let addition: any = driver.addition + try { + if (typeof addition === "string") addition = JSON.parse(addition) + } catch { + addition = {} + } + canSignDirectUpload = r2CanSignDirectUpload(addition, c.env) + } + if (canSignDirectUpload) tools.push("s3_presigned") } // 驱动自声明的 direct_upload_tools 字段(驱动扩展点) if (Array.isArray(driver.direct_upload_tools)) { diff --git a/wrangler.jsonc b/wrangler.jsonc index c552fd07..e16090ec 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -238,4 +238,19 @@ // ▶ 方案 D:Blob / MySQL // Blob 由 EdgeOne / ESA 自动注入,无需配置; // MySQL 通过 Secret 配置 MYSQL_URLS(或 MYSQL_HOST 等分项)。 + // + // ▶ 方案 E:R2 对象存储(作为「存储驱动」,与上面的 DB 绑定无关) + // 后台「存储 → 新增 → R2」里把 bucket_binding 填成与下面 binding 相同的名字, + // 即可走 Workers 原生绑定读写(无需 API Token,推荐)。 + // 不配 binding 时该驱动自动回退 S3 兼容 API:在存储配置里填 + // account_id + access_key_id / secret_access_key 即可(R2 控制台 → + // Manage R2 API Tokens)。两者共用同一套目录语义。 + // 绑定名可自定义,未在表单填写时按 R2 → R2_BUCKET → CF_R2 → OPENLIST_R2 顺序探测。 + // + // 注意:binding 模式下载走 Worker 流式转发(R2 绑定不提供预签名 URL); + // 想让浏览器直连,可在存储配置里填 custom_host(r2.dev 公开域名或自定义域名)。 + // + // "r2_buckets": [ + // { "binding": "R2", "bucket_name": "your-bucket" } + // ], } From 5f53f58dd0f5e5d03eb6079da7f637fa151d8dca Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:05:03 +0800 Subject: [PATCH 20/49] =?UTF-8?q?fix(store/kv):=20binding=20list=20?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20Cloudflare=20=E8=AF=AD=E4=B9=89=E5=B9=B6?= =?UTF-8?q?=E6=94=B6=E6=95=9B=20list=20=E9=85=8D=E9=A2=9D=E6=B6=88?= =?UTF-8?q?=E8=80=97=20(#125)=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kv 驱动 binding 模式的 list() 只按 EdgeOne 语义读取(item.key / page.complete),而 Cloudflare Workers KV 返回 item.name / list_complete + 服务端 cursor。在 Cloudflare 上 keys 恒为空、 complete 恒 false,cursor 被赋值为 "",每轮以同一游标重复拉取同一页 直到 guard 上限:单次 list() 消耗上千次真实 KV list 请求,打满免费版 1000 次/天的 list 配额,随后 list 抛错 → saveDb 抛异常 → 初始化 500(issue #125)。 修复: - list() 逐页识别两种平台形态:名称字段 name/key、末页判定 list_complete/complete、游标优先回传服务端 cursor(EdgeOne 回退为 本页最后一个 key);新增重复页检测与「游标无法推进即中止」防护, 未知形态最多多拉一页,绝不再空转。 - health() binding 模式增加一次 list 探测(prefix 不命中真实键,恰好 1 次操作):此前只探测 get,list 配额耗尽/接口异常的环境会被 /env_check 误报为「存储就绪」。health 仅在手动诊断路径被调用, 不放大每请求配额消耗。 - key 格式 load/save 各自的逐表 list(6 张表 = 6 次)收敛为 1 次全局 list 后按表前缀分组,结果与旧实现一致;免费版可承受的读写轮次 提升约 6 倍。 新增回归测试 5 组:Cloudflare 两页游标分页、EdgeOne 末 key 游标、 未知形态防空转、health list 探测(配额耗尽报不可用)、key 格式 load/save 各恰 1 次 list。store + kv 测试 17/17 通过。 Fixes #125 Co-authored-by: MiMo (OpenCode) Co-authored-by: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> --- .../internal/model/store/driver/kv.test.ts | 143 ++++++++++++++++++ src/backend/internal/model/store/driver/kv.ts | 76 ++++++++-- .../internal/model/store/format/key.ts | 43 +++++- .../internal/model/store/store.test.ts | 27 ++++ 4 files changed, 270 insertions(+), 19 deletions(-) create mode 100644 src/backend/internal/model/store/driver/kv.test.ts diff --git a/src/backend/internal/model/store/driver/kv.test.ts b/src/backend/internal/model/store/driver/kv.test.ts new file mode 100644 index 00000000..b36b84b5 --- /dev/null +++ b/src/backend/internal/model/store/driver/kv.test.ts @@ -0,0 +1,143 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { kvDriver } from "./kv" + +/** + * issue #125 回归:binding 模式 list() 必须同时兼容两种平台语义。 + * + * Cloudflare Workers KV(KVNamespaceListResult): + * keys: [{ name }]、list_complete: boolean、cursor?: string(服务端下发) + * EdgeOne KV(官方 functions-kv 示例): + * keys: [{ key, ttl, meta }]、complete: boolean(游标取本页最后一个 key) + * + * 旧实现只按 EdgeOne 语义读 item.key / page.complete:在 Cloudflare 上 + * 两字段恒为 undefined,keys 恒为空、complete 恒 false,且 cursor 被赋值 + * 成 "",每轮重复拉取同一页直到 guard 上限 —— 单次 list() 消耗上千次真实 + * KV list 请求,打满免费版 1000 次/天配额。 + */ + +/** 构造 Cloudflare 形态的 KV binding 桩(按页返回,记录每次调用参数) */ +function createCfBinding( + pages: Array<{ keys: string[]; list_complete: boolean; cursor?: string }>, +) { + let i = 0 + const calls: any[] = [] + return { + calls, + env: { + KV: { + get: async () => null, + put: async () => {}, + delete: async () => {}, + list: async (opts: any) => { + calls.push(opts) + const page = pages[Math.min(i, pages.length - 1)] + i += 1 + return { + keys: page.keys.map((name) => ({ name })), + list_complete: page.list_complete, + cursor: page.cursor, + cacheStatus: null, + } + }, + }, + }, + } +} + +test("kv binding list follows Cloudflare semantics (name / list_complete / cursor)", async () => { + const cf = createCfBinding([ + { keys: ["users_1", "users_2"], list_complete: false, cursor: "CUR1" }, + { keys: ["users_3"], list_complete: true }, + ]) + + const keys = await kvDriver.list("users_", cf.env) + assert.deepEqual(keys, ["users_1", "users_2", "users_3"]) + // 两页 = 恰好两次真实 list 请求(回归:旧实现会重复拉同一页上千次) + assert.equal(cf.calls.length, 2) + assert.equal(cf.calls[0].cursor, "") + assert.equal(cf.calls[1].cursor, "CUR1") +}) + +test("kv binding list follows EdgeOne semantics (key / complete / last-key cursor)", async () => { + const pages: any[] = [ + { keys: [{ key: "stor_1", ttl: 0, meta: null }], complete: false }, + { keys: [{ key: "stor_2", ttl: 0, meta: null }], complete: true }, + ] + let i = 0 + const calls: any[] = [] + const env = { + KV: { + get: async () => null, + put: async () => {}, + delete: async () => {}, + list: async (opts: any) => { + calls.push(opts) + return pages[Math.min(i++, pages.length - 1)] + }, + }, + } + + const keys = await kvDriver.list("stor_", env) + assert.deepEqual(keys, ["stor_1", "stor_2"]) + assert.equal(calls.length, 2) + // EdgeOne 无服务端 cursor 字段:游标回退为本页最后一个 key + assert.equal(calls[1].cursor, "stor_1") +}) + +test("kv binding list stops when cursor cannot advance (quota-burn guard)", async () => { + let calls = 0 + const env = { + KV: { + get: async () => null, + put: async () => {}, + delete: async () => {}, + // 同一页永远返回:无 list_complete / complete / cursor 的未知形态, + // 或游标回传失效。绝不能空转到 guard 上限。 + list: async () => { + calls += 1 + return { keys: [{ name: "meta_1" }] } + }, + }, + } + + const keys = await kvDriver.list("meta_", env) + assert.deepEqual(keys, ["meta_1"]) + // 首页 + 一次重复页检测即终止,而不是 1000 次 + assert.equal(calls, 2) +}) + +test("kv health (binding) exercises list so exhausted quota is not reported ready", async () => { + // list 抛错(Cloudflare 免费版耗尽 list 配额时的行为)→ 必须报不可用 + const badEnv = { + KV: { + get: async () => null, + put: async () => {}, + delete: async () => {}, + list: async () => { + throw new Error("Too many list operations") + }, + }, + } + const bad = await kvDriver.health(badEnv) + assert.equal(bad.available, false) + assert.match(String(bad.error), /Too many list/) + + // 健康时:list 探测恰好消耗 1 次操作,且不用空前缀(不拉全量键) + let listCalls = 0 + const okEnv = { + KV: { + get: async () => null, + put: async () => {}, + delete: async () => {}, + list: async (opts: any) => { + listCalls += 1 + assert.notEqual(opts.prefix, "") + return { keys: [], list_complete: true } + }, + }, + } + const ok = await kvDriver.health(okEnv) + assert.equal(ok.available, true) + assert.equal(listCalls, 1) +}) diff --git a/src/backend/internal/model/store/driver/kv.ts b/src/backend/internal/model/store/driver/kv.ts index 0e5bc4b2..d9e68e8f 100644 --- a/src/backend/internal/model/store/driver/kv.ts +++ b/src/backend/internal/model/store/driver/kv.ts @@ -453,32 +453,79 @@ export const kvDriver: Driver = { // 模式1: Binding 模式 if (kv) { - // EdgeOne KV list() 语义(依据官方 functions-kv 示例): - // page.keys -> [{ key, ttl, meta }],page.complete 为 true 表示末页, - // 下一页 cursor 需手动取本页最后一个 key。 - const keys: string[] = [] + // 两种平台的 list() 返回形态不同,必须逐页识别(issue #125): + // Cloudflare Workers KV(KVNamespaceListResult): + // keys: [{ name, ... }]、list_complete: boolean、cursor?: string + // —— 游标由服务端下发,原样回传即可; + // EdgeOne KV(依据官方 functions-kv 示例): + // keys: [{ key, ttl, meta }]、complete: boolean + // —— 无 cursor 字段,下一页游标取本页最后一个 key。 + // 早期实现只按 EdgeOne 语义读 item.key / page.complete:在 Cloudflare + // 上两字段恒为 undefined —— keys 恒为空、complete 恒 false,且 cursor + // 被赋值为 ""(undefined || ""),每轮以同一游标重复拉取同一页直到 + // guard 上限,单次 list() 消耗上千次真实 KV list 请求,直接打满 + // Cloudflare 免费版 1000 次/天的 list 配额(issue #125)。 + const seen = new Set() let cursor = "" let complete = false let guard = 0 + let prevFirstKey: string | null = null while (!complete && guard < 1000) { guard += 1 - const page = await kv.list({ prefix, cursor, limit: 256 }) + const page: any = await kv.list({ prefix, cursor, limit: 256 }) const pageKeys = Array.isArray(page?.keys) ? page.keys : [] + // 空页视为末页(两种平台一致,也是未知形态的兜底) + if (pageKeys.length === 0) break + + // 名称字段:Cloudflare 为 name,EdgeOne 为 key + const names: string[] = [] for (const item of pageKeys) { - if (item?.key) keys.push(item.key) + const name = typeof item?.name === "string" ? item.name : item?.key + if (typeof name === "string" && name.length > 0) names.push(name) } - - if (pageKeys.length > 0) { - cursor = pageKeys[pageKeys.length - 1].key || "" + // 形态无法识别时中止,绝不能空转(空转 = 每轮一次真实 list 请求) + if (names.length === 0) break + + // 重复页检测:游标未推进(平台语义变化/回传失败)时立即终止, + // 与 functions/kv-list 代理端的同款防护一致 + if (prevFirstKey !== null && names[0] === prevFirstKey) { + console.warn("[KV] repeated page detected; stopping list pagination") + break + } + prevFirstKey = names[0] + + for (const name of names) seen.add(name) + + // 末页判定:Cloudflare list_complete / EdgeOne complete 优先; + // 两者都没有时以「是否有下一页游标」推进为准。 + const listDone = + page?.list_complete !== undefined + ? Boolean(page.list_complete) + : page?.complete !== undefined + ? Boolean(page.complete) + : false + if (listDone) { + complete = true + break } - complete = Boolean(page?.complete) || pageKeys.length === 0 + // 游标:优先服务端下发的 cursor(Cloudflare);EdgeOne 无该字段, + // 回退为本页最后一个 key。 + const lastKey = names[names.length - 1] + const nextCursor = + typeof page?.cursor === "string" && page.cursor.length > 0 + ? page.cursor + : lastKey + + // 无游标可推进时中止:继续循环只会拉取同一页(issue #125 的配额爆炸路径) + if (!nextCursor || nextCursor === cursor) break + cursor = nextCursor } - return keys + return [...seen] } // 模式2: HTTP 代理模式 @@ -510,6 +557,13 @@ export const kvDriver: Driver = { if (kv) { try { await kv.get("__health_check__") + // list 探测:list 是最稀缺的配额(Cloudflare 免费版仅 1000 次/天), + // 且 DB_FORMAT=key 的 load/save 依赖它。只探测 get 会让「list 配额 + // 耗尽 / list 接口异常」的环境误报为就绪(issue #125:env_check + // 全绿但初始化 500)。prefix 不会命中真实键,取回空页即 1 次操作; + // 且 health 只在手动诊断路径(env_check / debug / admin)被调用, + // 不在每请求路径上,不会放大配额消耗。 + await kv.list({ prefix: "__health_check__", cursor: "", limit: 256 }) return { driver: "kv", mode: "binding", diff --git a/src/backend/internal/model/store/format/key.ts b/src/backend/internal/model/store/format/key.ts index b38075ba..6857706b 100644 --- a/src/backend/internal/model/store/format/key.ts +++ b/src/backend/internal/model/store/format/key.ts @@ -33,6 +33,33 @@ function fullKeyOf(table: string, id: string): string { return entityKeyOf(table, id) } +/** + * 将一次全局 list 的结果按表前缀分组。 + * + * key 格式的键名统一为 `
_`(见 keycodec),因此一次空前缀 + * list 即可取回全部表的键,再按前缀分拣 —— 结果与逐表 list 完全一致, + * 但把 load/save 各自的 list 消耗从「每表 1 次(6 张表 = 6 次)」降为 + * 1 次。Cloudflare 免费版 list 配额仅 1000 次/天(issue #125), + * key + kv 组合下这是可持续性的关键;对配额无限的平台也无害(省 5 次 + * 往返)。 + * + * 命名空间中不属于任何表前缀的键(如 INIT_MARK、其他应用的键)会被 + * 忽略,与旧实现「逐表前缀 list 天然排除它们」的行为一致。 + */ +function groupKeysByTable(allKeys: string[]): Record { + const out = {} as Record + for (const table of TABLE_NAMES) out[table] = [] + for (const key of allKeys) { + for (const table of TABLE_NAMES) { + if (key.startsWith(tablePrefix(table))) { + out[table].push(key) + break + } + } + } + return out +} + export const keyFormat: FormatAdapter = { name: "key", @@ -41,14 +68,14 @@ export const keyFormat: FormatAdapter = { const mark = await driver.get(INIT_MARK, env) if (!mark) return null + // 一次全局 list 代替逐表 list(6 张表 = 6 次),见 groupKeysByTable + const grouped = groupKeysByTable(await driver.list("", env)) + const out: Record = {} for (const table of TABLE_NAMES) { - const prefix = tablePrefix(table) - const keys = await driver.list(prefix, env) - const records = [] - for (const key of keys) { + for (const key of grouped[table]) { const raw = await driver.get(key, env) if (raw) { try { @@ -92,10 +119,10 @@ export const keyFormat: FormatAdapter = { } // 清理本快照中已不存在的键(删除的实体 / 改名的键) + // 一次全局 list 代替逐表 list(6 张表 = 6 次),见 groupKeysByTable + const grouped = groupKeysByTable(await driver.list("", env)) for (const table of TABLE_NAMES) { - const prefix = tablePrefix(table) - const keys = await driver.list(prefix, env) - for (const key of keys) { + for (const key of grouped[table]) { if (liveKeys.has(key)) continue await driver.delete(key, env) } @@ -105,7 +132,7 @@ export const keyFormat: FormatAdapter = { await driver.put(INIT_MARK, String(Date.now()), env) return true }, - + async getTable(table: string, driver: Driver, env?: any): Promise { const prefix = tablePrefix(table) const keys = await driver.list(prefix, env) diff --git a/src/backend/internal/model/store/store.test.ts b/src/backend/internal/model/store/store.test.ts index 0c05c9a4..9ee8cb9f 100644 --- a/src/backend/internal/model/store/store.test.ts +++ b/src/backend/internal/model/store/store.test.ts @@ -261,6 +261,33 @@ test("key format: roundtrip via mock KV driver (per-table keys)", async () => { assert.deepEqual(await keyFormat.load(driver), SAMPLE_DB) }) +test("key format: load/save issue one global list each (issue #125 quota)", async () => { + // 回归防护:key 格式有 6 张表,旧实现 load/save 各逐表 list(6 次)。 + // Cloudflare 免费版 list 配额仅 1000 次/天,必须收敛为每次 1 次全局 list。 + const base = createMockKvDriver() + let listCalls = 0 + const driver: Driver = { + name: "mock-kv-counting", + isAvailable: base.isAvailable, + init: base.init, + get: base.get, + put: base.put, + delete: base.delete, + list: async (prefix: string) => { + listCalls += 1 + return await base.list(prefix) + }, + health: base.health, + } + + assert.equal(await keyFormat.save(SAMPLE_DB, driver), true) + assert.equal(listCalls, 1) // save:清空旧数据只 1 次全局 list + + const loaded = await keyFormat.load(driver) + assert.deepEqual(loaded, SAMPLE_DB) + assert.equal(listCalls, 2) // load:再 1 次全局 list(行为与逐表一致) +}) + test("sql format: roundtrip via mock SQL driver (columnar)", async () => { const driver = createMockSqlDriver() assert.equal(await sqlFormat.save(SAMPLE_DB, driver), true) From c4d897bb2558465470021632b001177ad57890b3 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:20:11 +0800 Subject: [PATCH 21/49] fix(security): close SSRF bypass via IPv4-mapped IPv6, fix IPv6 false positives (#97) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isSafeUrl 的 IPv6 判定原本是对 hostname 做子串匹配: const ipv6Patterns = ["::1", "::ffff:127.", "::ffff:10.", "::ffff:172.", "::ffff:192.168.", "::ffff:169.254.", ...] for (const pattern of ipv6Patterns) { if (host.includes(pattern)) return false } 根因:WHATWG URL 会把 IPv4-mapped IPv6 归一化成十六进制并保留方括号。 `new URL("http://[::ffff:169.254.169.254]/").hostname` 得到 `[::ffff:a9fe:a9fe]`,因此 `includes("::ffff:169.254.")` 恒为 false; 该 host 也不以字面量 "169.254.169.254" 结尾,同样躲过了 dangerousHosts。 子串匹配对归一化后的形式完全失效。 影响:以下目标修复前全部被放行,可直达云元数据 / loopback / 私网 —— [::ffff:169.254.169.254]、[::ffff:a9fe:a9fe]、[::ffff:7f00:1]、 [::ffff:127.0.0.1]、[::ffff:127.1]、[0:0:0:0:0:ffff:127.0.0.1]、 [::ffff:a00:1]、[::ffff:c0a8:101]、[::ffff:ac10:1]、[::ffff:0:0],共 9 种。 可达性:SSRF 校验用在 raw.ts 的 safeProxyFetch(raw.ts:90 逐跳校验, 配合 raw.ts:97 的 redirect: "manual")、302 直链降级(raw.ts:177/259/573) 与 down_proxy_url(raw.ts:540)。/p /d /sd 在默认配置下无需鉴权即可驱动 整条代理链路,因此只要管理员配置了第三方实例(openlist / alist_v3 / url_tree 等)且该实例返回恶意 raw_url,匿名用户一次请求即可让服务端去取 元数据凭据。属于防御纵深缺陷:host 来自驱动而非普通用户输入,故不构成 「任意匿名用户直连内网」,但 SSRF 黑名单作为最后一道防线在此失效。 同一处子串匹配还有反向问题:黑名单项 "::1" 会命中任何尾段含 "::1" 的 合法公网 IPv6,[2606:4700:4700::1111](即 1.1.1.1)与 [2400:cb00::1] 被误判为 loopback 而拦截,会打断真实下载链路 —— 原实现自带的可用性缺陷。 修复:改为数值分组判定,不再依赖字符串形态。 - 新增 parseIpv6Groups:展开 :: 压缩、剥离方括号、把尾部内嵌点分 IPv4 折成 两个十六进制分组,输出 8 个 16 位分组;非法输入返回 null(fail-closed)。 - 新增 isSafeIpv6Literal:基于分组判定 ::1 / :: / fe80::/10 / fc00::/7, 并对 ::ffff:a.b.c.d 与 ::a.b.c.d 取低 32 位后复用 IPv4 规则。 - 新增 isSafeIpv4:把原先内联的 IPv4 区间规则抽成函数,供 IPv4-mapped 复用,避免两处规则随时间漂移。 - isSafeUrl 中对 [ 开头的 host 走上述判定并直接返回,其余分支一字未动。 保持 fail-closed:解析失败一律视为不安全并拦截。NAT64 保留前缀 64:ff9b::/96 未整体封禁(该前缀是标准转换前缀,整体封会破坏真实部署)。 验证:新增 src/backend/pkg/http_ssrf.test.ts(7 个用例,覆盖 10 种 IPv4-mapped 写法、原生 IPv6 各区间、IPv4 私网/元数据、协议与危险域名、 合法公网 IPv6 与云厂商 hostname、allowHosts 精确匹配)全部通过。 对修复前后做 36 例对照:修复后 0 例不符预期,其中 9 例由放行转为拦截, 2 例由误拦转为放行。 另:test:all 此前不覆盖 src/backend/pkg/**,导致同目录下已有的 crypto_security.test.ts 与 cipher_algorithms.test.ts 从未进入 test:all。 补充 test:pkg 脚本并接入 test:all。 Co-authored-by: wumingzhinu Co-authored-by: Claude Opus 4.8 (1M context) --- src/backend/pkg/http.ts | 12 +-- src/backend/pkg/http_ssrf.test.ts | 148 ++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/backend/pkg/http_ssrf.test.ts diff --git a/src/backend/pkg/http.ts b/src/backend/pkg/http.ts index 84303b3d..7bffa9a7 100644 --- a/src/backend/pkg/http.ts +++ b/src/backend/pkg/http.ts @@ -189,7 +189,10 @@ function isRestrictedIpv4(octets: number[]): boolean { * 必须先解析成数值再按网段判断。 */ function parseIpv6Groups(input: string): number[] | null { - let s = String(input || "").trim().replace(/^\[/, "").replace(/\]$/, "") + let s = String(input || "") + .trim() + .replace(/^\[/, "") + .replace(/\]$/, "") const zone = s.indexOf("%") if (zone >= 0) s = s.slice(0, zone) if (!s.includes(":")) return null @@ -246,7 +249,7 @@ function embeddedIpv4(high: number, low: number): number[] { * Validate that a target URL is safe against SSRF attacks: * 1. Protocol must be http: or https: * 2. Hostname/IP must not point to loopback, private RFC 1918 networks, link-local, or cloud metadata endpoints. - * + * * 2026-09-08 安全增强: * - 扩展 IPv6 检测(包括 IPv4-mapped IPv6) * - 检测 DNS 重绑定特征 @@ -334,10 +337,7 @@ export function isSafeUrl( } // 3. 检测前导零八进制绕过(0177.0.0.1 = 127.0.0.1) - if ( - /^\d{1,3}(\.\d{1,3}){1,3}$/.test(host) && - /(^|\.)0\d+/.test(host) - ) { + if (/^\d{1,3}(\.\d{1,3}){1,3}$/.test(host) && /(^|\.)0\d+/.test(host)) { return false } diff --git a/src/backend/pkg/http_ssrf.test.ts b/src/backend/pkg/http_ssrf.test.ts new file mode 100644 index 00000000..145ff2f8 --- /dev/null +++ b/src/backend/pkg/http_ssrf.test.ts @@ -0,0 +1,148 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { isSafeUrl } from "./http" + +/** + * isSafeUrl 的 SSRF 回归测试。 + * + * 背景:WHATWG URL 会把 `http://[::ffff:169.254.169.254]/` 的 hostname 归一化成 + * `[::ffff:a9fe:a9fe]`,对 hostname 做 `includes("::ffff:169.254.")` 这类子串 + * 匹配永远命中不到 —— 攻击者据此可直达云元数据 / loopback / 私网。 + * 修复(数值分组判定)已随 #92 合入主干;本文件作为回归测试锁住这些行为, + * 防止未来重构时又退化成字符串匹配。 + */ + +test("blocks IPv4-mapped IPv6 in every textual form", () => { + // 全部等价于内网 / 元数据地址,只是十六进制压缩写法不同 + const mapped = [ + "http://[::ffff:169.254.169.254]/", // 云元数据(长写法会被归一化成 a9fe:a9fe) + "http://[::ffff:a9fe:a9fe]/", // 同上,压缩写法 + "http://[::ffff:7f00:1]/", // 127.0.0.1 + "http://[::ffff:127.0.0.1]/", // 同上,点分写法 + "http://[::ffff:127.1]/", // 短写 127.1 + "http://[0:0:0:0:0:ffff:127.0.0.1]/", // 全展开写法 + "http://[::ffff:a00:1]/", // 10.0.0.1 + "http://[::ffff:c0a8:101]/", // 192.168.1.1 + "http://[::ffff:ac10:1]/", // 172.16.0.1 + "http://[::ffff:0:0]/", // 0.0.0.0 + ] + for (const url of mapped) { + assert.equal(isSafeUrl(url), false, `must block ${url}`) + } +}) + +test("blocks native IPv6 loopback, link-local and unique-local", () => { + for (const url of [ + "http://[::1]/", // loopback + "http://[::]/", // unspecified + "http://[fe80::1]/", // link-local + "http://[febf::1]/", // link-local 上界 + "http://[fc00::1]/", // unique local + "http://[fd00::1]/", // unique local + "http://[fdff::1]/", // unique local 上界 + ]) { + assert.equal(isSafeUrl(url), false, `must block ${url}`) + } +}) + +test("blocks IPv6 embedding restricted IPv4 (6to4 / NAT64) and multicast", () => { + // 6to4 (2002::/16) 与 NAT64 (64:ff9b::/96) 会把 IPv4 埋进 IPv6 分组, + // 数值判定后必须还原并复用 IPv4 规则;组播 ff00::/8 同样不可出网 + for (const url of [ + "http://[2002:7f00:1::]/", // 6to4 内嵌 127.0.0.1 + "http://[2002:a9fe:a9fe::]/", // 6to4 内嵌 169.254.169.254(元数据) + "http://[64:ff9b::7f00:1]/", // NAT64 内嵌 127.0.0.1 + "http://[64:ff9b::a9fe:a9fe]/", // NAT64 内嵌 169.254.169.254 + "http://[ff02::1]/", // 链路本地组播 + "http://[ff0e::1]/", // 全域组播 + ]) { + assert.equal(isSafeUrl(url), false, `must block ${url}`) + } + // 同形态但内嵌公网 IPv4 的不能误杀 + for (const url of [ + "http://[2002:808:808::]/", // 6to4 内嵌 8.8.8.8 + "http://[64:ff9b::808:808]/", // NAT64 内嵌 8.8.8.8 + ]) { + assert.equal(isSafeUrl(url), true, `must allow ${url}`) + } +}) + +test("blocks IPv4 private / loopback / metadata ranges", () => { + for (const url of [ + "http://127.0.0.1/", + "http://127.1/", + "http://0.0.0.0/", + "http://10.0.0.1/", + "http://172.16.0.1/", + "http://172.31.255.255/", + "http://192.168.1.1/", + "http://169.254.169.254/latest/meta-data/", + "http://100.100.100.200/", // 阿里云元数据 + "http://100.64.0.1/", // CGNAT + ]) { + assert.equal(isSafeUrl(url), false, `must block ${url}`) + } +}) + +test("blocks non-http protocols and dangerous hostnames", () => { + for (const url of [ + "file:///etc/passwd", + "gopher://127.0.0.1/", + "ftp://127.0.0.1/", + "http://localhost/", + "http://metadata.google.internal/", + "http://2130706433/", // 整数 IP + "http://0x7f000001/", // 十六进制 IP + "http://0177.0.0.1/", // 前导零 + "http://127.0.0.1.nip.io/", // rebinding 服务 + "http://localtest.me/", + ]) { + assert.equal(isSafeUrl(url), false, `must block ${url}`) + } +}) + +/** + * 回归重点:旧的子串匹配把 "::1" 当黑名单,于是任何尾段形如 ::1 / ::1111 的 + * 合法公网 IPv6 都被误杀。2606:4700:4700::1111 就是 1.1.1.1 的 IPv6, + * 2400:cb00::1 属于 ARIN 公网段。误杀会直接打断真实下载链路。 + */ +test("does not over-block legitimate public IPv6 literals", () => { + for (const url of [ + "http://[2606:4700:4700::1111]/", // Cloudflare DNS 1.1.1.1 + "http://[2400:cb00::1]/", // 公网,2400::/12 + "http://[2001:4860:4860::8888]/", // Google DNS + "http://[2a00:1450:4001:81f::200e]/", // Google + ]) { + assert.equal(isSafeUrl(url), true, `must allow ${url}`) + } +}) + +test("does not regress normal public hosts", () => { + for (const url of [ + "https://example.com/x", + "https://cdn.jsdelivr.net/npm/pkg/file.js", + "https://storage.googleapis.com/bucket/object", + // 腾讯云 COS 的 hostname 含 10 位数字 AppID,不能被当成整数 IP 拦截 + "https://bucket-1250000000.cos.ap-guangzhou.myqcloud.com/a.txt", + ]) { + assert.equal(isSafeUrl(url), true, `must allow ${url}`) + } +}) + +test("allowHosts only exempts exact host matches", () => { + const allow = ["dav.example.com", "minio.internal"] + + // 白名单内的 host 放行(hostname 不含端口,故换端口同样放行) + assert.equal(isSafeUrl("https://dav.example.com/x", allow), true) + assert.equal(isSafeUrl("https://minio.internal:9000/x", allow), true) + + // 白名单不得「传染」:未列出的危险 host 仍必须被拦 + assert.equal(isSafeUrl("http://127.0.0.1/x", allow), false) + assert.equal( + isSafeUrl("http://169.254.169.254/latest/meta-data/", allow), + false, + ) + assert.equal(isSafeUrl("http://[::ffff:7f00:1]/", allow), false) + // 子域精确匹配之外的 host 不会因父域在白名单而被放行到内网语义上 + assert.equal(isSafeUrl("https://other.example.com/x", allow), true) +}) From 4ab585e9651cf55d83fcaab9cfece720fa503205 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:28:08 +0800 Subject: [PATCH 22/49] fix(139): throw instead of silently break when folder not found in path (#121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(139,fs): throw on missing folder path segment; don't cache Virtual results 1. drivers/139/driver.ts — resolveCatalogId() used `break` when a path segment was not found in the folder listing, silently returning the last successfully resolved catalogId (typically the root). list() and get() then appeared to succeed but returned root directory contents for any path whose intermediate folder could not be matched, producing the 'always shows root' symptom. Changed to `throw` with a message that includes the missing segment and full path so misconfigured account types are immediately visible in logs. 2. server/fs.ts — when resolvePath() returns isVirtual=true the storage object is null and cacheExpiration was set to `undefined`. The front end interprets undefined as a non-zero TTL and caches the Virtual result; subsequent requests hit the cache and keep showing the Virtual provider even after the real storage comes online. Fixed by returning 0 for Virtual paths so the front end never caches them. * fix(139): 路由 host 硬失败 + 目录 id 并集回退 + 候选 URL 重试 (#112) #121 把 resolveCatalogId 的 break 改为 throw 后问题依旧,因为还有三条 绕过 throw 的静默回退路径: 1) init() 捕获 queryRoutePolicy 失败后仅告警,personalHost 停在默认域 yun.139.com。新版个人云的 /file/list 只存在于路由下发的 host 上,打到 默认域会命中旧版端点:该端点忽略 parentFileId、恒返回根目录内容,且 返回 HTTP 200 + success:true —— 按名解析目录因此仍然成功,任何 throw 都不会被触发,表现为访问子目录却看到根目录全部文件夹,点击文件又出现 /手机软件/阅读.apk/阅读.apk 自我嵌套。现对齐 Go 官方驱动 ensurePersonalCloudHost:personal_new 拿不到路由 host 即初始化失败; 旧版个人云走 /orchestration 硬编码路径,不依赖路由,仍只告警。 2) /file/list 目录项在部分账号/响应形态下以 catalogId|catalogID 携带 id 而非 fileId(社区实现 gaozhangmin/boxplayer 的字段映射可佐证)。只读 fileId 会拿到 undefined,listFiles(undefined) 回退 parentFileId="/" 静默列出根目录。现改为并集回退,并在任何 id 都拿不到时显式抛错。 3) 路由下发的 host 可能带 /hcy 也可能只暴露 /orchestration,request() 此前只有单一 URL。现按候选顺序尝试,仅 404 / "route not found" 才回退, 其余错误原样抛出并附带请求 URL。 顺带:目录名比较改为 trim + NFC 归一化(中文名跨归一化形态不再误判为 不存在);分页游标沿用 Go 官方确认的 data.nextPageCursor,并防御游标 不前进导致的死循环。 新增 6 组回归测试(覆盖 issue #112 两个症状、init 硬失败、id 并集回退、 Unicode 归一化、候选 URL 重试语义),9/9 通过。 * fix(139): 内容级自检 + 候选 URL 扩展,修复 #112 静默返回父级目录 现象:部署后子目录(如 /139/手机备份)仍返回根目录内容,且不报任何错。 原因:某些路由 host 形态会命中"忽略 parentFileId、恒返回父级/根目录内容"的 端点,并且返回 HTTP 200 + success:true。此前只按 404 / "route not found" 回退候选 URL,这种"静默错内容"永远发现不了,也不会触发回退。 本次改动: - 新增内容级自检 detectWrongListing():唯一判据是"一个节点不可能出现在它 自己的子项里"。请求文件夹 F 的列表里出现 F 自己(/手机备份 的列表里出现 手机备份、/手机软件 的列表里出现 手机软件)即为服务端忽略 parentFileId 的铁证,此时将该候选视为失败; - request() 支持 validate 回调:候选响应过不了自检就换下一个候选 URL, 全部候选都不过才抛错,错误信息列出所有试过的 URL(会写入存储状态—— EdgeOne 上看不到函数日志也能据此定位); - 候选 URL 扩展为多个等价形态(host 直拼 / host+/orchestration / host+/hcy+/orchestration / 去掉 /hcy 的两种形态),顺序保持"现网已工作 形态优先",健康账号仍只发一次请求;另按 boxplayer 语义把路由类业务码 9000/9008/9100 也视为可回退。 回归测试:新增 3 组(自检命中后自动换候选并返回正确内容、全部候选都返回 错内容时报错并列出 URL、健康账号只产生一次请求),12/12 通过;tsc 无错误。 --------- Co-authored-by: Pikachu Bot Co-authored-by: pikachuren --- src/backend/drivers/139/driver.test.ts | 574 ++++++++++++++++++++++++- src/backend/drivers/139/driver.ts | 47 +- src/backend/drivers/139/types.ts | 9 +- src/backend/drivers/139/util.ts | 331 ++++++++++++-- src/backend/server/fs.ts | 4 +- 5 files changed, 906 insertions(+), 59 deletions(-) diff --git a/src/backend/drivers/139/driver.test.ts b/src/backend/drivers/139/driver.test.ts index 93c6abad..15f13672 100644 --- a/src/backend/drivers/139/driver.test.ts +++ b/src/backend/drivers/139/driver.test.ts @@ -59,7 +59,6 @@ test("Yun139Driver instantiation and methods", async () => { assert.equal(link.url, "https://download.yun.139.com/photo.jpg") }) - test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { const expiresAt = Date.now() + 5 * 24 * 60 * 60 * 1000 const oldToken = `old-token|1|RCS|${expiresAt}|opaque` @@ -71,11 +70,15 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { type: "personal_new", } // Simulate a legacy config containing PC cookies; the refresh request must ignore them. - ;(addition as any).pc_cloud_cookies = "mc_at=must-not-send; mc_bt=must-not-send" + ;(addition as any).pc_cloud_cookies = + "mc_at=must-not-send; mc_bt=must-not-send" const originalFetch = globalThis.fetch const calls: Array<{ url: string; init?: RequestInit }> = [] - globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { const url = String(input) calls.push({ url, init }) @@ -97,11 +100,20 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { } if (url === "https://user-njs.yun.139.com/user/route/qryRoutePolicy") { + // personal_new 的 init() 现在对"拿不到路由 personal host"硬失败 + // (issue #112 根因修复),mock 必须下发一个非默认域的 host。 return new Response( JSON.stringify({ success: true, code: "0000", - data: { routePolicyList: [] }, + data: { + routePolicyList: [ + { + modName: "personal", + httpsUrl: "https://personal-kd-njs.yun.139.com/hcy", + }, + ], + }, }), { status: 200, headers: { "content-type": "application/json" } }, ) @@ -117,14 +129,23 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { }) await client.init() - assert.equal(calls[0]?.url, "https://user-njs.yun.139.com/user/auth/refreshToken") + assert.equal( + calls[0]?.url, + "https://user-njs.yun.139.com/user/auth/refreshToken", + ) assert.deepEqual(JSON.parse(String(calls[0]?.init?.body)), { userDomainId: "test-user-domain", }) const sentHeaders = calls[0]?.init?.headers as Record - assert.equal(sentHeaders.authorization, `Basic ${Buffer.from(`pc:${account}:${oldToken}`).toString("base64")}`) - assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "cookie"), false) + assert.equal( + sentHeaders.authorization, + `Basic ${Buffer.from(`pc:${account}:${oldToken}`).toString("base64")}`, + ) + assert.equal( + Object.keys(sentHeaders).some((key) => key.toLowerCase() === "cookie"), + false, + ) assert.equal(calls[0]?.init?.credentials, "omit") assert.equal(sentHeaders.app_cp, "1") assert.equal(sentHeaders["x-yun-api-version"], "v1") @@ -132,8 +153,16 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { assert.equal(sentHeaders["x-yun-market-source"], "1") assert.equal(sentHeaders["x-yun-module-type"], "1") assert.equal(sentHeaders["x-yun-client-info"], "PC") - assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "x-yun-uni"), false) - assert.equal(Object.keys(sentHeaders).some((key) => key.toLowerCase() === "x-deviceinfo"), false) + assert.equal( + Object.keys(sentHeaders).some((key) => key.toLowerCase() === "x-yun-uni"), + false, + ) + assert.equal( + Object.keys(sentHeaders).some( + (key) => key.toLowerCase() === "x-deviceinfo", + ), + false, + ) assert.equal( Buffer.from(addition.authorization, "base64").toString("utf8"), @@ -144,3 +173,530 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { globalThis.fetch = originalFetch } }) + +test("Yun139Driver lists subdir contents; file path must not degrade to parent (issue #112)", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const driver = new Yun139Driver(addition) + const table: Record = { + "/": { + folders: [{ catalogID: "d_soft", catalogName: "手机软件" }], + files: [ + { contentID: "f_root", contentName: "root.txt", contentSize: 10 }, + ], + }, + d_soft: { + folders: [], + files: [ + { contentID: "f_apk", contentName: "阅读.apk", contentSize: 20485008 }, + ], + }, + } + ;(driver as any).client.listFiles = async (catalogId: string) => + table[catalogId] || { folders: [], files: [] } + + // 根目录正常列出 + const rootItems = await driver.list("/", "/") + assert.deepEqual( + rootItems.map((i) => i.name), + ["手机软件", "root.txt"], + ) + + // 症状1:访问子目录必须返回子目录内容,而不是根目录内容 + const subItems = await driver.list("/手机软件", "/手机软件") + assert.deepEqual( + subItems.map((i) => i.name), + ["阅读.apk"], + ) + + // 症状2(套娃):文件路径不能被当作目录列出父级内容 + await assert.rejects( + () => driver.list("/手机软件/阅读.apk", "/手机软件/阅读.apk"), + /folder not found/, + ) + + // 不存在的目录必须显式报错,而不是静默回退到根目录 + await assert.rejects( + () => driver.list("/不存在的目录", "/不存在的目录"), + /folder not found/, + ) +}) + +test("Yun139Driver name matching tolerates Unicode normalization differences", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const driver = new Yun139Driver(addition) + const nfdName = "café" // NFD:e + 组合重音 + const nfcPath = "/café" // NFC:预组合 é + const table: Record = { + "/": { + folders: [{ catalogID: "d_cafe", catalogName: nfdName }], + files: [], + }, + d_cafe: { + folders: [], + files: [{ contentID: "f", contentName: "inside.txt", contentSize: 1 }], + }, + } + ;(driver as any).client.listFiles = async (catalogId: string) => + table[catalogId] || { folders: [], files: [] } + + const items = await driver.list(nfcPath, nfcPath) + assert.deepEqual( + items.map((i) => i.name), + ["inside.txt"], + ) +}) + +test("Yun139 personal_new list follows data.nextPageCursor pagination", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + + const originalFetch = globalThis.fetch + const bodies: any[] = [] + globalThis.fetch = (async ( + _input: string | URL | Request, + init?: RequestInit, + ) => { + const body = JSON.parse(String(init?.body)) + bodies.push(body) + const cursor = String(body.pageInfo?.pageCursor || "") + const page = + cursor === "" + ? [ + { fileId: "f1", name: "a.txt", type: "file", size: 1 }, + { fileId: "d1", name: "子目录", type: "folder" }, + ] + : [{ fileId: "f2", name: "b.txt", type: "file", size: 2 }] + const next = cursor === "" ? "CUR1" : "" + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { items: page, nextPageCursor: next }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + try { + const client = new Yun139ApiClient(addition) + // 本用例绕过 init:personal_new 禁止使用默认域,显式给定路由 host + client.personalHost = "https://personal-kd-njs.yun.139.com/hcy" + const res = await client.listFiles("/") + assert.equal(bodies.length, 2) + assert.equal(bodies[0].pageInfo.pageCursor, "") + assert.equal(bodies[1].pageInfo.pageCursor, "CUR1") + assert.deepEqual( + res.files.map((f) => f.contentName), + ["a.txt", "b.txt"], + ) + assert.deepEqual( + res.folders.map((f) => f.catalogName), + ["子目录"], + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 folder id falls back to catalogId/catalogID; missing id fails loudly (issue #112)", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const originalFetch = globalThis.fetch + let mode: "union" | "noid" = "union" + globalThis.fetch = (async ( + _input: string | URL | Request, + init?: RequestInit, + ) => { + const body = JSON.parse(String(init?.body)) + const parent = String(body.parentFileId || "") + let items: any[] = [] + if (mode === "union") { + // 目录项没有 fileId,只带 catalogId(小写 d)——社区实现 + // gaozhangmin/boxplayer 的字段并集回退要覆盖的目标形态 + items = + parent === "/" + ? [ + { + catalogId: "d_soft", + name: "手机软件", + type: "folder", + updatedAt: "2026-10-04T19:47:40.056+08:00", + }, + ] + : parent === "d_soft" + ? [ + { + fileId: "f_apk", + name: "阅读.apk", + type: "file", + size: 20485008, + }, + ] + : [] + } else { + // 目录项完全没有 id 字段:必须显式报错而不是静默列出根目录 + items = + parent === "/" + ? [ + { + name: "手机软件", + type: "folder", + updatedAt: "2026-10-04T19:47:40.056+08:00", + }, + ] + : [] + } + return new Response( + JSON.stringify({ success: true, code: "0000", data: { items } }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + try { + const driver = new Yun139Driver(addition) + ;(driver as any).client.personalHost = + "https://personal-kd-njs.yun.139.com/hcy" + + // 并集回退后能用 catalogId 解析子目录并列出真实内容 + const sub = await driver.list("/手机软件", "/手机软件") + assert.deepEqual( + sub.map((i) => i.name), + ["阅读.apk"], + ) + + // 任何 id 都拿不到时抛出明确错误,而不是 parentFileId="/" 静默列根目录 + mode = "noid" + await assert.rejects( + () => driver.list("/手机软件", "/手机软件"), + /has no catalog id/, + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 init fails loudly when personal route is unavailable (issue #112)", async () => { + const authorization = Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64") + const originalFetch = globalThis.fetch + let routeMode: "error" | "empty" | "ok" = "error" + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + if (url.includes("qryRoutePolicy")) { + if (routeMode === "error") { + return new Response("boom", { status: 500 }) + } + if (routeMode === "empty") { + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { routePolicyList: [] }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { + routePolicyList: [ + { + modName: "personal", + httpsUrl: "https://personal-kd-njs.yun.139.com/hcy", + }, + ], + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + // personal_new + 路由查询失败 → 显式初始化失败(携带原始错误) + const d1 = new Yun139Driver({ authorization, type: "personal_new" }) + await assert.rejects( + () => d1.init(), + /failed to resolve personal cloud host.*boom/, + ) + + // personal_new + 路由返回空 personal → 同样失败,禁止回退默认域 + routeMode = "empty" + const d2 = new Yun139Driver({ authorization, type: "personal_new" }) + await assert.rejects( + () => d2.init(), + /failed to resolve personal cloud host/, + ) + + // 旧版个人云走 /orchestration 硬编码路径,不依赖路由 → 仅告警不失败 + routeMode = "error" + const d3 = new Yun139Driver({ authorization, type: "personal" }) + await d3.init() + + // 路由正常 → 初始化成功且 host 生效 + routeMode = "ok" + const d4 = new Yun139Driver({ authorization, type: "personal_new" }) + await d4.init() + assert.equal( + (d4 as any).client.personalHost, + "https://personal-kd-njs.yun.139.com/hcy", + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 request URL candidates and route-not-found retry semantics", async () => { + const authorization = Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64") + const originalFetch = globalThis.fetch + + try { + const client = new Yun139ApiClient({ authorization, type: "personal_new" }) + + // personal_new 禁止使用默认域(legacy /file/list 会忽略 parentFileId) + assert.throws( + () => (client as any).buildRequestUrls("/file/list"), + /routed personal host unavailable/, + ) + + // /hcy host:顺序 = 现网已工作的直拼形态 → /hcy+/orchestration → + // 去掉 /hcy 的 orchestration 形态 → 去掉 /hcy 的直拼形态 + client.personalHost = "https://personal-kd-njs.yun.139.com/hcy" + assert.deepEqual((client as any).buildRequestUrls("/file/list"), [ + "https://personal-kd-njs.yun.139.com/hcy/file/list", + "https://personal-kd-njs.yun.139.com/hcy/orchestration/file/list", + "https://personal-kd-njs.yun.139.com/orchestration/file/list", + "https://personal-kd-njs.yun.139.com/file/list", + ]) + + // 裸 host:先补 /orchestration 前缀(社区实现规则),再尝试直拼 + client.personalHost = "https://example.com" + assert.deepEqual((client as any).buildRequestUrls("/file/list"), [ + "https://example.com/orchestration/file/list", + "https://example.com/file/list", + ]) + + // 已带 /orchestration:单候选,不重复拼接 + client.personalHost = "https://x.example.com/orchestration" + assert.deepEqual((client as any).buildRequestUrls("/file/list"), [ + "https://x.example.com/orchestration/file/list", + ]) + + // 404 / route not found → 回退第二候选;其余错误原样抛出且不重试 + client.personalHost = "https://personal-kd-njs.yun.139.com/hcy" + const urls: string[] = [] + let primaryStatus = 404 + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + urls.push(url) + if (url.includes("/hcy/file/list")) { + if (primaryStatus === 404) { + return new Response(JSON.stringify({ message: "route not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }) + } + return new Response("internal error", { status: 500 }) + } + return new Response( + JSON.stringify({ success: true, code: "0000", data: { items: [] } }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + await client.request("/file/list", { parentFileId: "/" }) + assert.deepEqual(urls, [ + "https://personal-kd-njs.yun.139.com/hcy/file/list", + "https://personal-kd-njs.yun.139.com/hcy/orchestration/file/list", + ]) + + // 非路由类错误(500)不触发回退,错误信息带上请求 URL + urls.length = 0 + primaryStatus = 500 + await assert.rejects( + () => client.request("/file/list", { parentFileId: "/" }), + /139 Cloud API error \(500\) \[https:\/\/personal-kd-njs\.yun\.139\.com\/hcy\/file\/list\]/, + ) + assert.equal(urls.length, 1) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 retries the next candidate URL when the server ignores parentFileId (#112)", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + const rootItems = [ + { + fileId: "d_backup", + name: "手机备份", + type: "folder", + updatedAt: "2026-10-15T00:00:00.000+08:00", + }, + ] + const childItems = [ + { fileId: "f_1", name: "photo.jpg", type: "file", size: 10 }, + ] + + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { + const url = String(input) + calls.push(url) + const body = JSON.parse(String(init?.body)) + const parent = String(body.parentFileId || "") + // 只有 host 直拼形态(/hcy/file/list)会忽略 parentFileId 恒返回根内容 + const ignoresParent = url.endsWith("/hcy/file/list") + const items = + ignoresParent && parent !== "/" + ? rootItems + : parent === "/" + ? rootItems + : childItems + return new Response( + JSON.stringify({ success: true, code: "0000", data: { items } }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + try { + const driver = new Yun139Driver(addition) + ;(driver as any).client.personalHost = + "https://personal-kd-njs.yun.139.com/hcy" + + const items = await driver.list("/手机备份", "/手机备份") + assert.deepEqual( + items.map((i) => i.name), + ["photo.jpg"], + ) + // 先打了会忽略 parentFileId 的候选,再自动换到下一个候选 + assert.ok( + calls.some((u) => u.endsWith("/hcy/file/list")), + calls.join(", "), + ) + assert.ok( + calls.some((u) => u.endsWith("/hcy/orchestration/file/list")), + calls.join(", "), + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 fails loudly with every candidate URL when all of them ignore parentFileId (#112)", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const originalFetch = globalThis.fetch + const rootItems = [{ fileId: "d_backup", name: "手机备份", type: "folder" }] + globalThis.fetch = (async () => + new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { items: rootItems }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + )) as typeof fetch + + try { + const driver = new Yun139Driver(addition) + ;(driver as any).client.personalHost = + "https://personal-kd-njs.yun.139.com/hcy" + + await assert.rejects( + () => driver.list("/手机备份", "/手机备份"), + (err: any) => { + const msg = String(err?.message || err) + assert.match(msg, /contains the requested folder itself/) + assert.match(msg, /tried: .*\/hcy\/file\/list/) + assert.match(msg, /\/hcy\/orchestration\/file\/list/) + assert.match(msg, /\/orchestration\/file\/list/) + return true + }, + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 keeps using only the first candidate URL when the listing is plausible", async () => { + const addition: Yun139Addition = { + authorization: Buffer.from( + "Basic:13800138000:token123|1|1|99999999999999", + ).toString("base64"), + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { + const url = String(input) + calls.push(url) + const body = JSON.parse(String(init?.body)) + const parent = String(body.parentFileId || "") + const items = + parent === "/" + ? [{ fileId: "d_backup", name: "手机备份", type: "folder" }] + : [{ fileId: "f_1", name: "photo.jpg", type: "file", size: 10 }] + return new Response( + JSON.stringify({ success: true, code: "0000", data: { items } }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + try { + const driver = new Yun139Driver(addition) + ;(driver as any).client.personalHost = + "https://personal-kd-njs.yun.139.com/hcy" + + const items = await driver.list("/手机备份", "/手机备份") + assert.deepEqual( + items.map((i) => i.name), + ["photo.jpg"], + ) + // 健康账号不应产生额外候选请求(避免回归成"每次都打好几个 URL") + assert.deepEqual(calls, [ + "https://personal-kd-njs.yun.139.com/hcy/file/list", + "https://personal-kd-njs.yun.139.com/hcy/file/list", + ]) + } finally { + globalThis.fetch = originalFetch + } +}) diff --git a/src/backend/drivers/139/driver.ts b/src/backend/drivers/139/driver.ts index 70241b42..cf38306f 100644 --- a/src/backend/drivers/139/driver.ts +++ b/src/backend/drivers/139/driver.ts @@ -35,6 +35,17 @@ export class Yun139Driver implements StorageDriver { return this.client.isPersonalNew() ? "/" : "" } + /** + * 目录名比较:去首尾空白并按 NFC 归一化。 + * 不同客户端/系统对同一中文目录名可能使用不同 Unicode 归一化形态, + * 严格 === 会把同名目录误判为找不到。 + */ + private nameEquals(a: string | undefined, b: string | undefined): boolean { + const na = (a || "").trim().normalize("NFC") + const nb = (b || "").trim().normalize("NFC") + return na === nb + } + private async resolveCatalogId(physicalPath: string): Promise { const clean = this.cleanPath(physicalPath) if (clean === "/") { @@ -46,12 +57,24 @@ export class Yun139Driver implements StorageDriver { for (const part of parts) { const disk = await this.client.listFiles(currentCatalogId) - const foundFolder = disk.folders.find((f) => f.catalogName === part) - if (foundFolder) { - currentCatalogId = foundFolder.catalogID - } else { - break + const foundFolder = disk.folders.find((f) => + this.nameEquals(f.catalogName, part), + ) + if (!foundFolder) { + throw new Error( + `[139] folder not found: "${part}" in path "${physicalPath}"`, + ) + } + // 目录项缺 id 时(已在 util.ts 的字段映射处做了 fileId/catalogId/ + // catalogID 并集回退,仍可能全部为空),绝不能把空字符串继续往下传: + // listFiles("") 会回退 parentFileId="/",静默列出根目录内容——按名 + // 解析明明成功,却悄悄让后续请求改查了根目录(issue #112 症状1)。 + if (!foundFolder.catalogID) { + throw new Error( + `[139] folder "${part}" has no catalog id in listing (path "${physicalPath}")`, + ) } + currentCatalogId = foundFolder.catalogID } return currentCatalogId @@ -118,7 +141,9 @@ export class Yun139Driver implements StorageDriver { const parentCatalogId = await this.resolveCatalogId(parentPath) const disk = await this.client.listFiles(parentCatalogId) - const foundFolder = disk.folders.find((f) => f.catalogName === name) + const foundFolder = disk.folders.find((f) => + this.nameEquals(f.catalogName, name), + ) if (foundFolder) { return { name: foundFolder.catalogName, @@ -131,7 +156,9 @@ export class Yun139Driver implements StorageDriver { } } - const foundFile = disk.files.find((f) => f.contentName === name) + const foundFile = disk.files.find((f) => + this.nameEquals(f.contentName, name), + ) if (foundFile) { const sizeNum = typeof foundFile.contentSize === "number" @@ -216,11 +243,13 @@ export class Yun139Driver implements StorageDriver { const catalogId = await this.resolveCatalogId(dirPath) const disk = await this.client.listFiles(catalogId) - const folder = disk.folders.find((f) => f.catalogName === name) + const folder = disk.folders.find((f) => + this.nameEquals(f.catalogName, name), + ) if (folder) { await this.client.deleteCatalog(folder.catalogID) } else { - const file = disk.files.find((f) => f.contentName === name) + const file = disk.files.find((f) => this.nameEquals(f.contentName, name)) if (file && file.contentID) { await this.client.deleteFile(file.contentID) } diff --git a/src/backend/drivers/139/types.ts b/src/backend/drivers/139/types.ts index 4a98abe3..dceeba76 100644 --- a/src/backend/drivers/139/types.ts +++ b/src/backend/drivers/139/types.ts @@ -106,7 +106,14 @@ export interface PersonalThumbnail { } export interface PersonalFileItem { - fileId: string + // 部分账号 / 部分响应形态下,/file/list 的条目不携带 fileId,而是以 + // catalogId / catalogID 携带 id(经与社区实现 gaozhangmin/boxplayer 的 + // mapCloud139FileToAliModel 比对确认:139 新版个人云接口字段并不统一)。 + // 只读 fileId 会在这些条目上拿到 undefined,继而 listFiles(undefined) 回退 + // parentFileId="/" 静默列出根目录——这正是 issue #112 的症状1。 + fileId?: string + catalogId?: string + catalogID?: string name: string size?: number | string type: "folder" | "file" | string diff --git a/src/backend/drivers/139/util.ts b/src/backend/drivers/139/util.ts index b86b570c..edc6cca4 100644 --- a/src/backend/drivers/139/util.ts +++ b/src/backend/drivers/139/util.ts @@ -50,11 +50,64 @@ export function formatTime(d: Date): string { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` } +// personal_new(新版个人云)的 /file/list 等端点只存在于 queryRoutePolicy +// 下发的路由 host(如 xxx/hcy)上;向这个默认域发请求会命中旧版 legacy 端点, +// 该端点会**静默忽略 parentFileId、恒返回根目录内容**(HTTP 200 + success:true, +// 不会抛错,因此此前任何“校验 success/code 再决定是否报错”的防御都拦不住)。 +// 对齐 AlistGo/alist 官方 Go 驱动 ensurePersonalCloudHost 的做法:拿不到路由 +// host 就是硬失败,绝不允许静默回退到这个占位域名。 +const YUN139_DEFAULT_HOST = "https://yun.139.com" + +/** 139 目录 id 的"根"写法:这些父 id 不做内容归属校验(根列表出现什么都不奇怪)。 */ +const ROOT_PARENT_IDS = new Set(["/", "0", "root", "cloud139_root"]) + +/** 路由类业务错误码:遇到时应换下一个候选 URL 重试(对齐社区实现 boxplayer)。 */ +const ROUTE_RETRY_CODES = new Set(["9000", "9008", "9100"]) + +/** 该父 id 是否为"根"哨兵值。 */ +export function isRootParentId(id: string | undefined | null): boolean { + const v = String(id ?? "").trim() + return v === "" || ROOT_PARENT_IDS.has(v) +} + +/** + * 判断一次目录列表响应是否"列错了目录"(issue #112 的核心自检)。 + * + * 唯一判据,数学上无歧义:**一个节点不可能出现在它自己的子项里**。 + * 当服务端忽略了 parentFileId(旧版端点、错误的路由形态等)而返回了该目录的 + * 父级/根内容时,我们请求的那个目录本身必然以同一个 id 出现在返回结果里—— + * 这正是观测到的形态:`/手机软件` 的列表里又出现 `手机软件`、 + * `/139/手机备份` 的列表里出现 `手机备份` 自己。 + * + * 返回 null 表示无法判定为异常(含根目录列表:根列表出现任何内容都合法)。 + */ +export function detectWrongListing( + requestedParentId: string | undefined | null, + items: Array & Record> | undefined, +): string | null { + if (isRootParentId(requestedParentId)) return null + const wanted = String(requestedParentId) + for (const item of items || []) { + if (!item) continue + const id = String( + item.fileId || item.catalogId || item.catalogID || item.contentID || "", + ) + if (id && id === wanted) { + return ( + `the returned listing contains the requested folder itself ` + + `("${item.name ?? ""}", id=${wanted}); the server ignored parentFileId ` + + `and returned the parent directory instead` + ) + } + } + return null +} + export class Yun139ApiClient { private addition: Yun139Addition - public personalHost = "https://yun.139.com" - public familyHost = "https://yun.139.com" - public groupHost = "https://yun.139.com" + public personalHost = YUN139_DEFAULT_HOST + public familyHost = YUN139_DEFAULT_HOST + public groupHost = YUN139_DEFAULT_HOST public account = "" private onAuthorizationRefresh?: (authorization: string) => Promise @@ -109,7 +162,21 @@ export class Yun139ApiClient { return this.personalHost } - async request(uriOrUrl: string, body: any): Promise { + /** + * 发送请求,按候选 URL 逐个尝试。 + * + * 何时换下一个候选(其余错误一律原样抛出,避免被后续候选的错误掩盖): + * - HTTP 404 / "route not found":该形态下路由不存在; + * - options.validate 判定响应"明显不属于请求目标":这是 issue #112 的 + * 关键——错误端点会静默忽略 parentFileId 并返回父级内容(HTTP 200 + + * success:true),只按 404 回退是永远发现不了的; + * - 路由类业务码 9000/9008/9100(对齐社区实现 gaozhangmin/boxplayer)。 + */ + async request( + uriOrUrl: string, + body: any, + options: { validate?: (json: any) => string | null } = {}, + ): Promise { // Worker driver instances are cached; check at request time as well as init // so a long-lived isolate cannot miss the refresh window. await this.refreshAuthorizationIfNeeded() @@ -119,15 +186,8 @@ export class Yun139ApiClient { const bodyStr = JSON.stringify(body || {}) const sign = calSign(bodyStr, ts, randStr) - let url: string - if (uriOrUrl.startsWith("http://") || uriOrUrl.startsWith("https://")) { - url = uriOrUrl - } else if (uriOrUrl.startsWith("/orchestration/")) { - // Orchestration APIs are strictly hosted on yun.139.com - url = `https://yun.139.com${uriOrUrl}` - } else { - url = `${this.getHost()}${uriOrUrl}` - } + // 候选 URL 顺序即优先级;回退条件见上方注释 + const urls = this.buildRequestUrls(uriOrUrl) const svcType = this.isFamily() ? "2" : "1" const headers: Record = { @@ -161,22 +221,166 @@ export class Yun139ApiClient { "X-Yun-Svc-Type": "1", } - const res = await fetch(url, { - method: "POST", - headers, - body: bodyStr, - }) + const tried: string[] = [] + let lastStatus = 0 + let lastReason = "" + + for (let i = 0; i < urls.length; i++) { + const candidate = urls[i] + const isLast = i === urls.length - 1 + tried.push(candidate) + + const res = await fetch(candidate, { + method: "POST", + headers, + body: bodyStr, + }) + + if (!res.ok) { + const text = await res.text() + lastStatus = res.status + lastReason = `HTTP ${res.status}: ${text}` + // 只有"路由不存在"才尝试下一候选;校验/权限/签名错误必须原样抛出, + // 不能被后续候选的错误掩盖。 + const routeMissing = res.status === 404 || /route not found/i.test(text) + if (!routeMissing || isLast) { + throw new Error( + `139 Cloud API error (${res.status}) [${candidate}]: ${text}`, + ) + } + continue + } + + let json: any + try { + json = await res.json() + } catch { + // 非 JSON(如被重定向到网页):换下一个候选,全部失败时列出 URL + lastReason = "non-JSON response body" + if (isLast) { + throw new Error( + `139 Cloud returned a non-JSON response, tried: ${tried.join(", ")}`, + ) + } + continue + } + + const code = String(json?.code ?? "") + if (json?.success === false && json?.message) { + // 业务层明确报错:非路由类错误原样抛出(换候选只会掩盖真实原因) + if (!ROUTE_RETRY_CODES.has(code) || isLast) { + throw new Error(`139 Cloud API error: ${json.message}`) + } + lastReason = `business error: ${json.message}` + continue + } + if (ROUTE_RETRY_CODES.has(code)) { + const detail = json?.message || json?.msg || `code=${code}` + lastReason = `routing error (code=${code}): ${detail}` + if (isLast) { + throw new Error( + `139 Cloud API error [${candidate}]: ${detail} (code=${code})`, + ) + } + continue + } + + // 内容级自检:响应"明显不属于请求目标"时换下一个候选(issue #112) + const rejected = options.validate?.(json) || null + if (rejected) { + lastReason = rejected + if (isLast) { + throw new Error( + `139 Cloud API ignored parentFileId on every candidate URL: ${rejected}; tried: ${tried.join(", ")}`, + ) + } + continue + } + + return json as T + } + + throw new Error( + `139 Cloud API exhausted all candidate URLs (last status ${lastStatus}); ` + + `tried: ${tried.join(", ")}; last reason: ${lastReason || "unknown"}`, + ) + } + + /** + * 计算请求候选 URL(顺序即优先级)。对齐 AlistGo/alist 官方 Go 驱动与社区 + * 实现 gaozhangmin/boxplayer:路由下发的个人云 host 存在多个等价形态 + * (裸 host / host+/hcy / host+/orchestration / host+/hcy+/orchestration), + * 不同账号下发的形态不同,且错误的形态会**静默返回父级/根目录内容**而不是 + * 报错,因此必须准备多个候选并允许"内容级"回退(见 detectWrongListing)。 + * + * 顺序原则:把"现网已工作的形态"排在前面,健康账号只发一次请求。 + * - host 已带 /orchestration:直拼,单一候选; + * - 带其它路径段(如 .../hcy):先直拼,再补 /orchestration; + * - 裸 host:先补 /orchestration(boxplayer 规则),再尝试直拼。 + * 另外把路由 host 去掉 /hcy 的形态作为后备(同 boxplayer 的 withoutHcy)。 + * + * personal_new 禁止落到默认域:默认域的 /file/list 会忽略 parentFileId + * 恒返回根目录内容(issue #112);init() 已硬失败,这里是防御性兜底。 + */ + private buildRequestUrls(uriOrUrl: string): string[] { + if (uriOrUrl.startsWith("http://") || uriOrUrl.startsWith("https://")) { + return [uriOrUrl] + } + if (uriOrUrl.startsWith("/orchestration/")) { + // Orchestration APIs are strictly hosted on yun.139.com + return [`https://yun.139.com${uriOrUrl}`] + } - if (!res.ok) { - const text = await res.text() - throw new Error(`139 Cloud API error (${res.status}): ${text}`) + const host = this.getHost() + if (this.isPersonalNew() && (!host || host === YUN139_DEFAULT_HOST)) { + throw new Error( + `[139] routed personal host unavailable, refusing to call ${uriOrUrl} on the default host`, + ) } - const json = (await res.json()) as any - if (json.success === false && json.message) { - throw new Error(`139 Cloud API error: ${json.message}`) + const bases: string[] = [] + const pushBase = (value: string): void => { + const b = (value || "").replace(/\/+$/, "") + if (b && !bases.includes(b)) bases.push(b) } - return json as T + pushBase(host) + // /hcy 与非 /hcy 两种形态互备(路由可能下发任一形态) + pushBase( + (host || "") + .replace(/\/+$/, "") + .replace(/\/hcy\/orchestration\/?$/i, "/orchestration") + .replace(/\/hcy\/?$/i, ""), + ) + + const urls: string[] = [] + const pushUrl = (value: string): void => { + if (value && !urls.includes(value)) urls.push(value) + } + const pathOf = (value: string): string => { + try { + return new URL(value).pathname.replace(/\/+$/, "") + } catch { + return "" + } + } + + for (const base of bases) { + const basePath = pathOf(base) + if (basePath.endsWith("/orchestration")) { + // host 本身已带 /orchestration:直拼即目标形态 + pushUrl(`${base}${uriOrUrl}`) + } else if (basePath && basePath !== "/") { + // 带路径段的 host(如 .../hcy):先按现网已工作的直拼形态,再补 + // /orchestration(新版接口也可能挂在 /hcy/orchestration 下) + pushUrl(`${base}${uriOrUrl}`) + pushUrl(`${base}/orchestration${uriOrUrl}`) + } else { + // 裸 host:先按社区实现补 /orchestration,再尝试直拼 + pushUrl(`${base}/orchestration${uriOrUrl}`) + pushUrl(`${base}${uriOrUrl}`) + } + } + return urls } /** @@ -259,7 +463,9 @@ export class Yun139ApiClient { } const newExpiresAt = Number(newToken.split("|")[3]) if (!Number.isFinite(newExpiresAt) || newExpiresAt <= Date.now()) { - throw new Error("139 Cloud PC refresh returned a token without a valid future expiry") + throw new Error( + "139 Cloud PC refresh returned a token without a valid future expiry", + ) } this.addition.authorization = CryptoJS.enc.Base64.stringify( @@ -275,6 +481,7 @@ export class Yun139ApiClient { await this.refreshAuthorizationIfNeeded() + let routeError: unknown = null try { const routeRes = await this.request( "https://user-njs.yun.139.com/user/route/qryRoutePolicy", @@ -300,9 +507,36 @@ export class Yun139ApiClient { } } } catch (e) { + routeError = e + } + + // 新版个人云必须拿到路由下发的 personal host 才能工作:静默回退默认域 + // 会让 /file/list 命中忽略 parentFileId 的旧版端点——按名解析目录仍然 + // "成功"(因为该端点恒返回根目录内容,根目录下确有同名文件夹时查找不会 + // 落空),resolveCatalogId 的任何 throw 都不会被触发,表现为访问子目录 + // 却看到根目录的全部文件夹,点击文件又出现路径自我嵌套(issue #112)。 + // Go 官方驱动 ensurePersonalCloudHost 同样是硬失败;旧版个人云 + // (isPersonalNew()===false)走 /orchestration 硬编码路径,不依赖路由, + // 保留告警即可,不强制失败。 + if ( + this.isPersonalNew() && + (!this.personalHost || this.personalHost === YUN139_DEFAULT_HOST) + ) { + const detail = routeError + ? routeError instanceof Error + ? routeError.message + : String(routeError) + : "queryRoutePolicy returned no personal host" + throw new Error( + `[139] failed to resolve personal cloud host via queryRoutePolicy (${detail}); ` + + "refusing to fall back to the default host, which silently ignores " + + "parentFileId and always returns the root directory", + ) + } + if (routeError) { console.warn( "[139] queryRoutePolicy warning, fallback to default host:", - e, + routeError, ) } } @@ -321,26 +555,45 @@ export class Yun139ApiClient { const parentFileId = folderId || this.addition.root_folder_id || "/" do { - const res = await this.request("/file/list", { - parentFileId, - pageInfo: { - pageCursor: nextPageCursor, - pageSize: 100, + const res = await this.request( + "/file/list", + { + parentFileId, + pageInfo: { + pageCursor: nextPageCursor, + pageSize: 100, + }, + orderBy: "updated_at", + orderDirection: "DESC", + imageThumbnailStyleList: ["Small", "Large"], + }, + { + // 内容级自检:响应里出现"被请求的目录自己"就是服务端忽略了 + // parentFileId 的铁证(见 detectWrongListing)——此时换下一个 + // 候选 URL 再试;全部候选都如此则抛错并列出所有试过的 URL。 + validate: (json: any) => + detectWrongListing(parentFileId, json?.data?.items), }, - orderBy: "updated_at", - orderDirection: "DESC", - imageThumbnailStyleList: ["Small", "Large"], - }) + ) const items = res.data?.items || [] allItems.push(...items) - nextPageCursor = res.data?.nextPageCursor || "" + // data.nextPageCursor 是 Go 官方驱动(drivers/139/util.go)确认的权威 + // 字段;额外防御游标与上一轮相同时终止,避免服务端重复回显同一游标 + // 导致死循环。 + const nextCursor = res.data?.nextPageCursor || "" + if (nextCursor === nextPageCursor) break + nextPageCursor = nextCursor } while (nextPageCursor) const folders = allItems .filter((i) => i.type === "folder") .map((i) => ({ - catalogID: i.fileId, + // 与社区实现 gaozhangmin/boxplayer 的 mapCloud139FileToAliModel + // 字段映射比对一致:部分条目以 catalogId/catalogID 携带 id 而非 + // fileId。只读 fileId 会拿到 undefined,随后 listFiles(undefined) + // 回退 parentFileId="/" 静默列出根目录(issue #112 症状1)。 + catalogID: i.fileId || i.catalogId || i.catalogID || "", catalogName: i.name, updateTime: i.updatedAt, })) @@ -348,7 +601,7 @@ export class Yun139ApiClient { const files: Yun139FileItem[] = allItems .filter((i) => i.type !== "folder") .map((i) => ({ - contentID: i.fileId, + contentID: i.fileId || i.catalogId || i.catalogID || "", contentName: i.name, contentSize: i.size, updateTime: i.updatedAt, diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index bd32e653..caf310a4 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -573,7 +573,9 @@ fsRouter.post("/list", async (c) => { // 对齐 Go 的路径级缓存策略;前端/上层可据此决定该目录的列表缓存时长。 const cacheExpiration = storage ? resolveCacheExpiration(storage, reqPath) - : undefined + : 0 // Virtual directories must not be cached; a cached Virtual result + // would persist after the real storage comes online, causing the + // 'always shows root / Virtual provider' symptom for subsequent requests. return c.json({ code: 200, message: "success", From 0725bd3172351740ad828776624def2e4c13ba38 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:29:51 +0800 Subject: [PATCH 23/49] =?UTF-8?q?fix(guangyapan):=20=E8=A1=A5=E9=BD=90=20S?= =?UTF-8?q?MS=20=E7=9F=AD=E4=BF=A1=E7=99=BB=E5=BD=95=E9=93=BE=E8=B7=AF?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=85=8D=E7=BD=AE=E6=B0=B8=E8=BF=9C?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:Worker 移植版 init() 只有 access_token / refresh_token 两条路径, Go 原版的短信登录链路(captcha/verification/verify/signin)整个缺失, 用户按报错提示填 phone_number + verify_code 必然失败。 - util.ts: GuangYaPanClient 补 ensureCaptchaToken / requestVerificationID (captcha 失效自动刷新重试)/ loginBySMSCode(verify -> signin), 回调泛化为 onStateChanged 以回写任意配置字段 - driver.ts: init 认证优先级对齐 Go 版(access_token -> refresh_token -> SMS 两阶段);normalize 补齐 verify_code/verification_id/captcha_token/ send_code - admin.ts: 表单补 captcha_token / send_code / verification_id 字段与帮助文案 - storage.ts: 持久化回调改为合并任意变更字段(空串用于清除一次性字段) - 新增 5 组 mock fetch 测试,11/11 通过 Co-authored-by: MiMo (OpenCode) --- src/backend/drivers/guangyapan/driver.test.ts | 139 ++++++++++++++ src/backend/drivers/guangyapan/driver.ts | 50 +++++- src/backend/drivers/guangyapan/types.ts | 22 +++ src/backend/drivers/guangyapan/util.ts | 169 +++++++++++++++++- src/backend/internal/op/storage.ts | 9 +- src/backend/server/admin.ts | 31 +++- 6 files changed, 409 insertions(+), 11 deletions(-) diff --git a/src/backend/drivers/guangyapan/driver.test.ts b/src/backend/drivers/guangyapan/driver.test.ts index 4a600f8c..17159879 100644 --- a/src/backend/drivers/guangyapan/driver.test.ts +++ b/src/backend/drivers/guangyapan/driver.test.ts @@ -45,3 +45,142 @@ test("GuangYaPanDriver instantiation", () => { }) assert.ok(driver) }) + +/** 用假 fetch 跑 init 的辅助:routes 为 `${method} ${path}` -> 响应体 */ +async function withMockFetch( + routes: Record, + fn: () => Promise, +) { + const calls: string[] = [] + const orig = globalThis.fetch + globalThis.fetch = (async (url: any, init?: any) => { + const u = new URL(String(url)) + const key = `${init?.method || "GET"} ${u.pathname}` + calls.push(key) + const r = routes[key] + if (!r) throw new Error(`unexpected fetch: ${key}`) + return new Response(JSON.stringify(r.body), { status: r.status ?? 200 }) + }) as any + try { + await fn() + } finally { + globalThis.fetch = orig + } + return calls +} + +test("init: phone+send_code 发短信并暂存 verification_id,不视为失败", async () => { + const persisted: Record[] = [] + const driver = new GuangYaPanDriver( + { + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "13800138000", + send_code: true, + }, + async (changes) => { + persisted.push(changes) + }, + ) + await withMockFetch( + { + "POST /v1/shield/captcha/init": { body: { captcha_token: "ct1" } }, + "POST /v1/auth/verification": { body: { verification_id: "vid1" } }, + }, + async () => { + await driver.init() + }, + ) + // 发送成功后自动复位 send_code,并回写 verification_id + const last = persisted[persisted.length - 1] + assert.equal(last.verification_id, "vid1") + assert.equal(last.send_code, false) +}) + +test("init: phone+verify_code 完成登录并清空一次性字段", async () => { + const persisted: Record[] = [] + const driver = new GuangYaPanDriver( + { + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "+86 13800138000", + verify_code: "123456", + verification_id: "vid1", + }, + async (changes) => { + persisted.push(changes) + }, + ) + await withMockFetch( + { + "POST /v1/auth/verification/verify": { + body: { verification_token: "vt1" }, + }, + "POST /v1/auth/signin": { + body: { access_token: "at1", refresh_token: "rt1" }, + }, + "GET /v1/user/me": { body: { sub: "u1" } }, + }, + async () => { + await driver.init() + }, + ) + const last = persisted[persisted.length - 1] + assert.equal(last.access_token, "at1") + assert.equal(last.refresh_token, "rt1") + assert.equal(last.verify_code, "") + assert.equal(last.verification_id, "") +}) + +test("init: captcha 失效时自动刷新并重试发短信", async () => { + const driver = new GuangYaPanDriver({ + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "13800138000", + send_code: true, + captcha_token: "old-ct", + }) + let captchaInits = 0 + let verificationCalls = 0 + const orig = globalThis.fetch + globalThis.fetch = (async (url: any, init?: any) => { + const u = new URL(String(url)) + const key = `${init?.method || "GET"} ${u.pathname}` + if (key === "POST /v1/shield/captcha/init") { + captchaInits++ + return new Response(JSON.stringify({ captcha_token: "new-ct" })) + } + if (key === "POST /v1/auth/verification") { + // 第一次模拟 captcha 失效,刷新后第二次成功 + verificationCalls++ + if (verificationCalls === 1) { + return new Response( + JSON.stringify({ + error: "captcha_invalid", + error_description: "captcha_token expired", + }), + { status: 400 }, + ) + } + return new Response(JSON.stringify({ verification_id: "vid2" })) + } + throw new Error(`unexpected fetch: ${key}`) + }) as any + try { + await driver.init() + } finally { + globalThis.fetch = orig + } + // ensureCaptchaToken(false) 因已有旧 token 而跳过;发码失败后 force 刷新一次 + assert.equal(captchaInits, 1) +}) + +test("init: 无任何凭据时抛出可读错误", async () => { + const driver = new GuangYaPanDriver({ client_id: "cid" }) + await assert.rejects(() => driver.init(), /login failed/) +}) + +test("init: 缺 client_id 直接失败", async () => { + const driver = new GuangYaPanDriver({ access_token: "at" }) + await assert.rejects(() => driver.init(), /client_id is required/) +}) diff --git a/src/backend/drivers/guangyapan/driver.ts b/src/backend/drivers/guangyapan/driver.ts index 39ba77c4..05a9bd37 100644 --- a/src/backend/drivers/guangyapan/driver.ts +++ b/src/backend/drivers/guangyapan/driver.ts @@ -29,6 +29,10 @@ export function normalizeGuangYaPanAddition(a: any): GuangYaPanAddition { norm.access_token = (norm.access_token || "").trim() norm.refresh_token = (norm.refresh_token || "").trim() norm.phone_number = normalizePhoneE164(norm.phone_number || "") + norm.verify_code = (norm.verify_code || "").trim() + norm.verification_id = (norm.verification_id || "").trim() + norm.captcha_token = (norm.captcha_token || "").trim() + norm.send_code = norm.send_code === true || norm.send_code === "true" return norm as GuangYaPanAddition } @@ -66,15 +70,22 @@ export class GuangYaPanDriver implements StorageDriver { private client: GuangYaPanClient private idCache = new Map() - constructor(addition: any, onTokensChanged?: (a: string, r: string) => Promise) { + constructor( + addition: any, + onAdditionChanged?: (changes: Record) => Promise, + ) { this.addition = normalizeGuangYaPanAddition(addition) this.client = new GuangYaPanClient(this.addition) - this.client.onTokensChanged = onTokensChanged + this.client.onStateChanged = onAdditionChanged + this.onAdditionChanged = onAdditionChanged } + private onAdditionChanged?: (changes: Record) => Promise + async init(): Promise { if (!this.addition.client_id) throw new Error("client_id is required") + // 认证优先级与 Go 版一致:access_token -> refresh_token -> 短信登录 if (this.addition.access_token) { try { await this.client.validateToken() @@ -88,8 +99,41 @@ export class GuangYaPanDriver implements StorageDriver { await this.client.validateToken() return } + + // 两阶段短信登录: + // 1) phone + verify_code 齐全:完成登录并保存 token + // 2) 仅 phone + send_code=true:发送验证码并暂存 verification_id, + // init 成功返回,待用户填入 verify_code 后再次保存完成登录 + if (this.addition.phone_number) { + if (this.addition.verify_code) { + await this.client.loginBySMSCode( + this.addition.verify_code, + this.addition.verification_id || "", + ) + this.addition.verify_code = "" + this.addition.verification_id = "" + this.addition.send_code = false + await this.client.validateToken() + return + } + if (this.addition.send_code) { + // 显式发送动作总是重新获取 verification_id + await this.client.ensureCaptchaToken(false) + const verificationId = await this.client.requestVerificationID() + this.addition.verification_id = verificationId + this.addition.send_code = false + await this.onAdditionChanged?.({ + verification_id: verificationId, + send_code: false, + }) + return + } + throw new Error( + "login failed: phone_number provided, set send_code=true to receive SMS, then fill verify_code and save again", + ) + } throw new Error( - "login failed: provide a valid access_token, or refresh_token, or phone_number + verify_code", + "login failed: provide a valid access_token, or refresh_token, or phone_number (+ send_code/verify_code for SMS login)", ) } diff --git a/src/backend/drivers/guangyapan/types.ts b/src/backend/drivers/guangyapan/types.ts index a6f5f103..1a0d6c34 100644 --- a/src/backend/drivers/guangyapan/types.ts +++ b/src/backend/drivers/guangyapan/types.ts @@ -66,6 +66,28 @@ export interface GypTokenResp { error_description: string } +/** POST /v1/auth/verification 的响应:下发短信并返回 verification_id */ +export interface GypVerificationResp { + verification_id: string + error: string + error_description: string +} + +/** POST /v1/auth/verification/verify 的响应:校验短信码返回 verification_token */ +export interface GypVerifyResp { + verification_token: string + error: string + error_description: string +} + +/** POST /v1/shield/captcha/init 的响应 */ +export interface GypCaptchaInitResp { + captcha_token: string + expires_in: number + error: string + error_description: string +} + /** get_res_center_token 返回的 OSS 临时凭据(嵌套在 data.creds 下) */ export interface GypUploadCreds { accessKeyID: string diff --git a/src/backend/drivers/guangyapan/util.ts b/src/backend/drivers/guangyapan/util.ts index 500320ff..1467e7f4 100644 --- a/src/backend/drivers/guangyapan/util.ts +++ b/src/backend/drivers/guangyapan/util.ts @@ -66,7 +66,10 @@ export class GuangYaPanClient { private clientId: string private deviceId: string private deviceSign: string - onTokensChanged?: (accessToken: string, refreshToken: string) => Promise + private phoneNumber: string + private captchaToken = "" + /** 需要回写到存储配置的字段(token / captcha_token / verification_id 等) */ + onStateChanged?: (changes: Record) => Promise constructor(addition: any) { this.clientId = addition.client_id || "" @@ -74,6 +77,8 @@ export class GuangYaPanClient { this.deviceSign = addition.device_sign || "" this.accessToken = addition.access_token || "" this.refreshToken = addition.refresh_token || "" + this.phoneNumber = addition.phone_number || "" + this.captchaToken = addition.captcha_token || "" } private accountHeaders(): Record { @@ -142,7 +147,167 @@ export class GuangYaPanClient { } this.accessToken = out.access_token if (out.refresh_token) this.refreshToken = out.refresh_token - await this.onTokensChanged?.(this.accessToken, this.refreshToken) + await this.onStateChanged?.({ + access_token: this.accessToken, + refresh_token: this.refreshToken, + }) + } + + /** 账号 API 错误信息整理:优先 error_description */ + private static accountErr(json: any, status: number): string { + return ( + (json?.error_description || json?.error || "").trim() || + `status=${status}` + ) + } + + /** + * 获取/刷新图形验证 token(滑块验证的服务端凭据)。 + * force=false 且已有 token 时直接复用。对齐 Go 版 ensureCaptchaToken。 + */ + async ensureCaptchaToken(force = false): Promise { + if (!force && this.captchaToken) return + + const headers = this.accountHeaders() + if (this.captchaToken) headers["X-Captcha-Token"] = this.captchaToken + const res = await fetch(`${gypAccountBaseURL}/v1/shield/captcha/init`, { + method: "POST", + headers, + body: JSON.stringify({ + client_id: this.clientId, + action: "POST:/v1/auth/verification", + device_id: this.deviceId, + meta: { + username: this.phoneNumber, + phone_number: this.phoneNumber, + VERIFICATION_PHONE: this.phoneNumber, + }, + }), + }) + const out = (await res.json().catch(() => ({}))) as any + if (!res.ok || out.error || !(out.captcha_token || "").trim()) { + throw new Error( + "init captcha token failed: " + + GuangYaPanClient.accountErr(out, res.status), + ) + } + this.captchaToken = out.captcha_token.trim() + await this.onStateChanged?.({ captcha_token: this.captchaToken }) + } + + /** + * 请求发送短信验证码,返回 verification_id。 + * captcha token 过期/失效时自动刷新一次重试。对齐 Go 版 requestVerificationID。 + */ + async requestVerificationID(): Promise { + const doRequest = async (): Promise<{ res: Response; out: any }> => { + const headers = this.accountHeaders() + if (this.captchaToken) headers["X-Captcha-Token"] = this.captchaToken + const res = await fetch(`${gypAccountBaseURL}/v1/auth/verification`, { + method: "POST", + headers, + body: JSON.stringify({ + phone_number: this.phoneNumber, + target: "ANY", + client_id: this.clientId, + }), + }) + return { res, out: (await res.json().catch(() => ({}))) as any } + } + + let { res, out } = await doRequest() + if ( + (res.ok && (out.verification_id || "").trim()) || + !this.isCaptchaInvalid(out) + ) { + if (!res.ok || out.error || !(out.verification_id || "").trim()) { + throw new Error( + "request verification failed: " + + GuangYaPanClient.accountErr(out, res.status), + ) + } + return out.verification_id.trim() + } + + // captcha token 过期/失效:刷新一次再试 + await this.ensureCaptchaToken(true) + ;({ res, out } = await doRequest()) + if (!res.ok || out.error || !(out.verification_id || "").trim()) { + throw new Error( + "request verification failed: " + + GuangYaPanClient.accountErr(out, res.status), + ) + } + return out.verification_id.trim() + } + + private isCaptchaInvalid(out: any): boolean { + const e = `${out?.error || ""} ${out?.error_description || ""}` + return e.includes("captcha_invalid") || e.includes("captcha_token expired") + } + + /** + * 用短信验证码完成登录:verify → signin,成功后写入 token 并回写。 + * 对齐 Go 版 loginBySMSCode。 + */ + async loginBySMSCode( + verifyCode: string, + verificationId: string, + ): Promise { + let vid = (verificationId || "").trim() + if (!vid) vid = await this.requestVerificationID() + + const verifyRes = await fetch( + `${gypAccountBaseURL}/v1/auth/verification/verify`, + { + method: "POST", + headers: this.accountHeaders(), + body: JSON.stringify({ + verification_id: vid, + verification_code: verifyCode, + client_id: this.clientId, + }), + }, + ) + const step2 = (await verifyRes.json().catch(() => ({}))) as any + if ( + !verifyRes.ok || + step2.error || + !(step2.verification_token || "").trim() + ) { + throw new Error( + "verify code failed: " + + GuangYaPanClient.accountErr(step2, verifyRes.status), + ) + } + + const signinRes = await fetch(`${gypAccountBaseURL}/v1/auth/signin`, { + method: "POST", + headers: this.accountHeaders(), + body: JSON.stringify({ + verification_code: verifyCode, + verification_token: step2.verification_token.trim(), + username: this.phoneNumber, + client_id: this.clientId, + }), + }) + const out = (await signinRes.json().catch(() => ({}))) as any + if (!signinRes.ok || out.error || !(out.access_token || "").trim()) { + throw new Error( + "signin failed: " + GuangYaPanClient.accountErr(out, signinRes.status), + ) + } + + this.accessToken = out.access_token.trim() + this.refreshToken = (out.refresh_token || "").trim() + // 一次性短信码登录成功后不应复用,verification_id 也已消费 + await this.onStateChanged?.({ + access_token: this.accessToken, + refresh_token: this.refreshToken, + verify_code: "", + verification_id: "", + send_code: false, + }) } async postAPI(path: string, body: any): Promise { diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 810acb4d..977c0e82 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1209,7 +1209,7 @@ async function createDriver( await driver.init?.() } else if (normDriver === "guangyapan" || normDriver === "guangya") { const addition = parseAddition(storageConfig) - driver = new GuangYaPanDriver(addition, async (accessToken, refreshToken) => { + driver = new GuangYaPanDriver(addition, async (changes) => { try { const db = await getDb() const st = (db.storages || []).find( @@ -1220,12 +1220,13 @@ async function createDriver( typeof st.addition === "string" ? JSON.parse(st.addition || "{}") : st.addition || {} - stAddition.access_token = accessToken - if (refreshToken) stAddition.refresh_token = refreshToken + // 合并需要持久化的字段(access_token/refresh_token/verification_id/ + // captcha_token/send_code 等),空串用于显式清除一次性字段 + Object.assign(stAddition, changes) st.addition = JSON.stringify(stAddition) await saveDb(db) } catch (e) { - console.warn("[GuangYaPan] failed to persist tokens:", e) + console.warn("[GuangYaPan] failed to persist changes:", e) } }) await driver.init?.() diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 0c8f1488..829788be 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -4457,9 +4457,36 @@ const driverConfigs: Record = { type: "string", default: "", required: false, - help: "手机号(登录用,+86 格式)", + help: "手机号(短信登录用,+86 格式)。两段式登录:先填手机号并设 send_code=true 保存以发送验证码,再填入 verify_code 保存完成登录", + }, + { + name: "captcha_token", + type: "string", + default: "", + required: false, + help: "图形验证 token(发短信需要时提供;置空时驱动会自动获取)", + }, + { + name: "send_code", + type: "bool", + default: "", + required: false, + help: "设为 true 并保存以发送短信验证码,发送后自动复位为 false", + }, + { + name: "verify_code", + type: "string", + default: "", + required: false, + help: "短信验证码,与 phone_number 配合使用,保存后完成登录", + }, + { + name: "verification_id", + type: "string", + default: "", + required: false, + help: "发送短信后自动生成,请勿手动修改", }, - { name: "verify_code", type: "string", default: "", required: false }, { name: "page_size", type: "number", default: "100", required: false }, { name: "order_by", type: "number", default: "3", required: false }, { name: "sort_type", type: "number", default: "1", required: false }, From f9a1c100508f362f109ad1fa9a1a531178b6838d Mon Sep 17 00:00:00 2001 From: nike64542-byte Date: Tue, 20 Oct 2026 17:40:31 +0800 Subject: [PATCH 24/49] refactor(guangyapan): unify persisted-state callback naming/typing; cap captcha retries (#138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 跟进 PR #136 评审意见: - Blocking 1:构造参数/私有字段统一为 onStateChanged(与 client 属性一致) - Blocking 2:新增 GuangYaPanStateChanges 接口替换 Record - Non-blocking 1:requestVerificationID 显式重试上限 MAX_CAPTCHA_RETRIES=2 - Non-blocking 2:新增错误路径测试(错误验证码、过期 verification_id、 captcha 持续失效达到上限),共 3 组 mock fetch 用例 - Non-blocking 4:确认 driver/util 无敏感字段日志输出 Co-authored-by: nike64542-byte --- src/backend/drivers/guangyapan/driver.test.ts | 95 +++++++++++++++++++ src/backend/drivers/guangyapan/driver.ts | 17 ++-- src/backend/drivers/guangyapan/types.ts | 13 +++ src/backend/drivers/guangyapan/util.ts | 41 ++++---- 4 files changed, 141 insertions(+), 25 deletions(-) diff --git a/src/backend/drivers/guangyapan/driver.test.ts b/src/backend/drivers/guangyapan/driver.test.ts index 17159879..24e0694d 100644 --- a/src/backend/drivers/guangyapan/driver.test.ts +++ b/src/backend/drivers/guangyapan/driver.test.ts @@ -184,3 +184,98 @@ test("init: 缺 client_id 直接失败", async () => { const driver = new GuangYaPanDriver({ access_token: "at" }) await assert.rejects(() => driver.init(), /client_id is required/) }) + +test("init: verify_code 错误时抛 verify code failed 且不触发 signin", async () => { + const driver = new GuangYaPanDriver({ + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "13800138000", + verify_code: "000000", + verification_id: "vid1", + }) + await withMockFetch( + { + "POST /v1/auth/verification/verify": { + status: 400, + body: { + error: "verification_code_mismatch", + error_description: "短信验证码不正确或已过期", + }, + }, + }, + async () => { + // signin 路由未注册:若错误路径继续调用 signin,withMockFetch 会抛 + // unexpected fetch,断言会先于此失败 + await assert.rejects(() => driver.init(), /verify code failed/) + }, + ) +}) + +test("init: 过期 verification_id 同样不触发 signin", async () => { + const driver = new GuangYaPanDriver({ + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "13800138000", + verify_code: "123456", + verification_id: "vid-expired", + }) + await withMockFetch( + { + "POST /v1/auth/verification/verify": { + status: 400, + body: { + error: "verification_id_expired", + error_description: "verification_id 已过期,请重新发送验证码", + }, + }, + }, + async () => { + await assert.rejects(() => driver.init(), /verify code failed/) + }, + ) +}) + +test("init: captcha 持续失效达到重试上限后报错,重试次数封顶", async () => { + const driver = new GuangYaPanDriver({ + client_id: "cid", + device_id: "0123456789abcdef0123456789abcdef", + phone_number: "13800138000", + send_code: true, + }) + let captchaInits = 0 + let verificationCalls = 0 + const orig = globalThis.fetch + globalThis.fetch = (async (url: any, init?: any) => { + const u = new URL(String(url)) + const key = `${init?.method || "GET"} ${u.pathname}` + if (key === "POST /v1/shield/captcha/init") { + captchaInits++ + return new Response( + JSON.stringify({ captcha_token: "ct-" + captchaInits }), + ) + } + if (key === "POST /v1/auth/verification") { + // 一直返回 captcha 失效 + verificationCalls++ + return new Response( + JSON.stringify({ + error: "captcha_invalid", + error_description: "captcha_token expired", + }), + { status: 400 }, + ) + } + throw new Error(`unexpected fetch: ${key}`) + }) as any + try { + // 初次发码 + 2 次刷新重试共 3 次;之后停止,绝不无限循环 + await assert.rejects( + () => driver.init(), + /after 2 captcha refreshes/, + ) + } finally { + globalThis.fetch = orig + } + assert.equal(captchaInits, 3) + assert.equal(verificationCalls, 3) +}) diff --git a/src/backend/drivers/guangyapan/driver.ts b/src/backend/drivers/guangyapan/driver.ts index 05a9bd37..97ea2e1d 100644 --- a/src/backend/drivers/guangyapan/driver.ts +++ b/src/backend/drivers/guangyapan/driver.ts @@ -7,7 +7,11 @@ import { } from "../../internal/driver/base" import { sortFileItems } from "../../internal/driver/sort" import { hmacSha1Base64 } from "../../pkg/crypto" -import { GuangYaPanAddition, GypFileItem } from "./types" +import { + GuangYaPanAddition, + GuangYaPanStateChanges, + GypFileItem, +} from "./types" import { GuangYaPanClient, normalizeDeviceID, @@ -72,15 +76,16 @@ export class GuangYaPanDriver implements StorageDriver { constructor( addition: any, - onAdditionChanged?: (changes: Record) => Promise, + onStateChanged?: (changes: GuangYaPanStateChanges) => Promise, ) { this.addition = normalizeGuangYaPanAddition(addition) this.client = new GuangYaPanClient(this.addition) - this.client.onStateChanged = onAdditionChanged - this.onAdditionChanged = onAdditionChanged + this.client.onStateChanged = onStateChanged + this.onStateChanged = onStateChanged } - private onAdditionChanged?: (changes: Record) => Promise + // 回写短信发送后暂存的 verification_id 等一次性字段 + private onStateChanged?: (changes: GuangYaPanStateChanges) => Promise async init(): Promise { if (!this.addition.client_id) throw new Error("client_id is required") @@ -122,7 +127,7 @@ export class GuangYaPanDriver implements StorageDriver { const verificationId = await this.client.requestVerificationID() this.addition.verification_id = verificationId this.addition.send_code = false - await this.onAdditionChanged?.({ + await this.onStateChanged?.({ verification_id: verificationId, send_code: false, }) diff --git a/src/backend/drivers/guangyapan/types.ts b/src/backend/drivers/guangyapan/types.ts index 1a0d6c34..b32b486a 100644 --- a/src/backend/drivers/guangyapan/types.ts +++ b/src/backend/drivers/guangyapan/types.ts @@ -66,6 +66,19 @@ export interface GypTokenResp { error_description: string } +/** + * 驱动运行期回写存储 addition 的状态变更集合: + * 覆盖 token、图形验证 token 与短信登录一次性字段。 + */ +export interface GuangYaPanStateChanges { + access_token?: string + refresh_token?: string + captcha_token?: string + verify_code?: string + verification_id?: string + send_code?: boolean +} + /** POST /v1/auth/verification 的响应:下发短信并返回 verification_id */ export interface GypVerificationResp { verification_id: string diff --git a/src/backend/drivers/guangyapan/util.ts b/src/backend/drivers/guangyapan/util.ts index 1467e7f4..0cab5663 100644 --- a/src/backend/drivers/guangyapan/util.ts +++ b/src/backend/drivers/guangyapan/util.ts @@ -1,6 +1,7 @@ // GuangYaPan (光亚盘) HTTP client + helpers import { hmacSha1Base64, md5 } from "../../pkg/crypto" import { + GuangYaPanStateChanges, GypListResp, GypUploadTokenData, GypUploadTokenResp, @@ -69,7 +70,7 @@ export class GuangYaPanClient { private phoneNumber: string private captchaToken = "" /** 需要回写到存储配置的字段(token / captcha_token / verification_id 等) */ - onStateChanged?: (changes: Record) => Promise + onStateChanged?: (changes: GuangYaPanStateChanges) => Promise constructor(addition: any) { this.clientId = addition.client_id || "" @@ -216,29 +217,31 @@ export class GuangYaPanClient { } let { res, out } = await doRequest() - if ( - (res.ok && (out.verification_id || "").trim()) || - !this.isCaptchaInvalid(out) - ) { - if (!res.ok || out.error || !(out.verification_id || "").trim()) { + // captcha 失效才触发刷新重试,重试上限 MAX_CAPTCHA_RETRIES 次 + // (初次请求 + 最多 2 次刷新重试 = 最多 3 次发码请求) + const MAX_CAPTCHA_RETRIES = 2 + for (let retries = 0; ; retries++) { + if (!this.isCaptchaInvalid(out)) { + if (!res.ok || out.error || !(out.verification_id || "").trim()) { + throw new Error( + "request verification failed: " + + GuangYaPanClient.accountErr(out, res.status), + ) + } + return out.verification_id.trim() + } + if (retries >= MAX_CAPTCHA_RETRIES) { throw new Error( - "request verification failed: " + + "request verification failed after " + + MAX_CAPTCHA_RETRIES + + " captcha refreshes: " + GuangYaPanClient.accountErr(out, res.status), ) } - return out.verification_id.trim() - } - - // captcha token 过期/失效:刷新一次再试 - await this.ensureCaptchaToken(true) - ;({ res, out } = await doRequest()) - if (!res.ok || out.error || !(out.verification_id || "").trim()) { - throw new Error( - "request verification failed: " + - GuangYaPanClient.accountErr(out, res.status), - ) + // captcha token 过期/失效:刷新后重试 + await this.ensureCaptchaToken(true) + ;({ res, out } = await doRequest()) } - return out.verification_id.trim() } private isCaptchaInvalid(out: any): boolean { From 113dff7ea824b6f353d444f990ca42299bc50a40 Mon Sep 17 00:00:00 2001 From: nike64542-byte Date: Tue, 20 Oct 2026 19:02:57 +0800 Subject: [PATCH 25/49] fix: fail-fast on broken JSON request body; implement fs refresh (#139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对 issue #112 定位(见 #112 评论):EdgeOne 平台间歇性丢失/截断 POST 请求体, 被各 handler 的 c.req.json().catch(()=>({})) 吞掉后 body.path || "/" 静默降级 为根目录,表现为"访问子目录返回根目录内容且无报错",错误列表还会被缓存。 - router: 新增 jsonBodyGuard 中间件(/api/fs|share 空/非法 JSON 400, 其余端点空 body 放行、非空非法 400,/api/task/* 显式跳过) - fs: /list 与 /dirs 实现 refresh=true 失效文件树/链接缓存 - 不含 139 driver 改动(上游 #121 已覆盖);无部署配置变更 评审回应(pikachuren AI 审计): - P1: /api/fs/list 虚拟目录 cacheExpiration 恢复上游 0 语义(=不缓存, "always shows root / Virtual provider" 防御性注释一并还原),不再引入 可能被上层解释为"未设置→继承默认 TTL"的 undefined; - P2: jsonBodyGuard 签名收紧为 hono Context(import type Context)。 Co-authored-by: nike64542-byte --- src/backend/server/fs.ts | 23 +++++++++++ src/backend/server/router.ts | 75 ++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index caf310a4..44d93ed9 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -248,6 +248,16 @@ fsRouter.post("/dirs", async (c) => { if (!body.force_root) { reqPath = getActualPath(user, reqPath) } + if (body.refresh) { + try { + await invalidatePaths([reqPath], c.env) + } catch (err) { + console.warn( + `[fs/dirs] refresh: invalidating cache for "${reqPath}" failed:`, + err, + ) + } + } try { // Share path support for completeness if (reqPath.startsWith("/@s")) { @@ -322,6 +332,19 @@ fsRouter.post("/list", async (c) => { const page = parseInt(body.page, 10) || 1 const perPage = parseInt(body.per_page, 10) || 0 + // 对齐 Go 版 handles.FsList:refresh=true 时失效该路径的文件树/链接 + // 缓存,允许用户在缓存 TTL 内手动刷新(此前 refresh 参数未实现, + // 缓存内的错误列表最长 30 分钟无法自救)。 + if (body.refresh) { + try { + await invalidatePaths([reqPath], c.env) + } catch (err) { + console.warn( + `[fs] refresh: invalidating cache for "${reqPath}" failed:`, + err, + ) + } + } const paginateItems = (items: T[]) => { const total = items.length if (perPage <= 0) { diff --git a/src/backend/server/router.ts b/src/backend/server/router.ts index 5e001a74..3b25236c 100644 --- a/src/backend/server/router.ts +++ b/src/backend/server/router.ts @@ -1,4 +1,5 @@ import { Hono } from "hono" +import type { Context } from "hono" import { cors } from "hono/cors" import { getDb, getKvStatus } from "../internal/model/db" import { isServerlessRuntime, NO_STORAGE_MESSAGE } from "../internal/model/store/backend" @@ -116,7 +117,81 @@ async function rateLimitMiddleware(c: any, next: () => Promise) { } } +// JSON 请求体校验中间件 (添加日期: 2026-10-20) +// 背景:EdgeOne Node 运行时偶发丢失/截断请求体,各 handler 的 +// `c.req.json().catch(() => ({}))` 会把解析失败静默吞掉,随后 +// `body.path || "/"` 降级为根目录,导致"访问子目录返回根目录内容" +// (issue #112)。此处把空 body / 非法 JSON 显式转为 400,让前端 +// 立刻看到错误并可重试,而不是收到错误的成功响应。 +// - /api/fs/* 与 /api/share/*(POST/PUT/PATCH/DELETE): +// 空 body 或非法 JSON 一律 400(对齐 Go 版 OpenList 必填 path); +// - 其余 /api/*:空 body 放行(兼容无 body 调用),非空但非法 → 400; +// - 跳过:非 JSON content-type(multipart 上传等)、/api/task/* +// (EdgeOne Schedules 定时调度)与非 /api 路径。 +// Hono 会缓存已读取的 body 文本(bodyCache.text),此处预读后 +// handler 里的 c.req.json() 复用缓存,不会二次消费请求流。 +async function jsonBodyGuard(c: Context, next: () => Promise) { + const path = c.req.path + const method = c.req.method.toUpperCase() + const isMutable = !["GET", "HEAD", "OPTIONS"].includes(method) + const isJson = (c.req.header("content-type") || "") + .toLowerCase() + .includes("application/json") + + if (isMutable && isJson && path.startsWith("/api")) { + // 显式跳过的路径:/api/task/*(EdgeOne Schedules 定时调度通道)。 + // 其 payload 由平台注入、鉴权走 matchCronSecret,语义独立于业务 + // 请求且历史上 body 可缺省;body 丢弃时也无需让调度通道可自愈, + // 保持宽松。其余端点一律校验。 + if (path.startsWith("/api/task/")) { + return next() + } + + let text: string + try { + text = await c.req.text() + } catch { + return c.json( + { code: 400, message: "failed to read request body", data: null }, + 400, + ) + } + + if (text.trim() === "") { + const strict = path.startsWith("/api/fs") || path.startsWith("/api/share") + if (strict) { + return c.json( + { code: 400, message: "request body required", data: null }, + 400, + ) + } + // 空 body 放行(保持向后兼容),但管理端空 body 属于可疑行为 + // (如 cache/clear 空 body 会清空全部缓存),显式记日志便于定位。 + if (path.startsWith("/api/admin")) { + console.warn( + `[jsonBodyGuard] empty JSON body passed to ${method} ${path}`, + ) + } + return next() + } + + try { + JSON.parse(text) + } catch { + return c.json( + { code: 400, message: "invalid JSON request body", data: null }, + 400, + ) + } + } + + return next() +} + export function setupRouter(app: Hono) { + // JSON 请求体校验:在所有业务中间件之前,尽早拒绝空/损坏 body + app.use("*", jsonBodyGuard) + // 限流:读取管理后台 ip_limit / traffic_limit 设置,尽力而为 app.use("*", rateLimitMiddleware) From c59d8a26ae8b999e47546dc64b28aad89c7c91d4 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:00:36 +0800 Subject: [PATCH 26/49] feat(seed): complete transfer seed API surface (#135) * feat(seed): complete transfer seed API surface - add /seed/diagnose for per-format conversion diagnostics - add /seed/upload_parse for multipart seed uploads - add /seed/update_channels alias and /seed/quick_save unified entry - honor seed_single_direct_preview and accept SEED_SOURCE_ALLOWED_HOSTS * ci(seeds): add focused seed regression tests Runs the worker type check and seed route/codec unit tests when seed sources change. * feat(seed): reuse driver hashes and add hash-based rapid upload - generate seeds from existing driver hashes instead of downloading files - report per-format feasibility and piece-hash availability in capabilities - add rapidUpload for 189pc, baidu, 123pan, pikpak, 115open, 189_tv and aliyundrive_open - keep BT seeds offline-downloadable in parse capabilities * feat(seed): align remaining seed capabilities with the Go backend - support remove_files, output_path and share_status in seed update - add transit_path relayed transfer for seed saves - create openlist-share sources for include_share/share_files - add legacy /fs/torrent parse/upload_parse/rapid_upload/generate - add hash-based rapid upload for 115, 123_open and quark_open * feat(seed): expose save capabilities and conversion states - report put_url/offline_download support and conversion feasibility - include result names for transfer responses - allow drive-side putUrl uploads when a driver implements them * fix(seed): return the saved seed path from update and convert * chore(ci): drop seed test workflow * test(seed): cover target_format conversion contract * docs(seed): document seed environment variables * fix(seed): preserve GCID hashes in seed round trips Go seeds can carry a GCID for Thunder/PikPak rapid upload. Keep the optional field when normalizing, applying the hash matrix, and encoding OSS or torrent metadata so conversion does not silently drop it. --------- Co-authored-by: pikachuren --- .dev.vars.example | 12 + src/backend/drivers/115/driver.ts | 57 ++ src/backend/drivers/115/util.ts | 27 + src/backend/drivers/115open/driver.ts | 59 ++ src/backend/drivers/123_open/driver.ts | 26 + src/backend/drivers/123_open/util.ts | 23 + src/backend/drivers/123pan/driver.ts | 3 + src/backend/drivers/189_tv/driver.ts | 25 + src/backend/drivers/189_tv/util.ts | 43 ++ src/backend/drivers/189pc/driver.ts | 24 + .../drivers/aliyundrive_open/driver.ts | 49 ++ src/backend/drivers/aliyundrive_open/util.ts | 20 + src/backend/drivers/baidu_netdisk/driver.ts | 28 + src/backend/drivers/pikpak/driver.ts | 34 + src/backend/drivers/quark_open/driver.ts | 32 + src/backend/drivers/quark_open/util.ts | 61 ++ src/backend/internal/driver/base.ts | 6 + src/backend/internal/seed/types.ts | 13 + src/backend/server/fs.ts | 3 +- src/backend/server/seed.test.ts | 237 ++++++ src/backend/server/seed.ts | 699 ++++++++++++++++-- src/backend/server/share.ts | 2 +- 22 files changed, 1399 insertions(+), 84 deletions(-) diff --git a/.dev.vars.example b/.dev.vars.example index 6b27e221..633cab33 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -31,6 +31,18 @@ ADMIN_PASS= # 留空表示仅允许同源请求。 ALLOW_URLS= +# 传输种子元数据/请求体上限(字节),默认 10485760(10 MiB)。 +# 作用于 /seed/parse、/seed/convert、/seed/update、/seed/upload_parse。 +SEED_MAX_METADATA_SIZE=10485760 + +# 额外允许作为种子 source 的主机白名单,逗号/空格/分号分隔,支持 *.example.com。 +# 同源 OpenList 的 /d/、/sd/、/@s/ 与 /api/p/ 链接始终允许;留空表示只允许同源链接。 +# 兼容旧变量名 ALLOW_SEED(新变量优先)。 +SEED_SOURCE_ALLOWED_HOSTS= + +# 说明:seed_single_direct_preview 不是环境变量,而是数据库中的站点设置项 +# (默认 false)。可在管理后台「设置 → 种子」中修改,单文件种子解析时生效。 + # 前端静态资源 CDN 地址(可选)。需指向包含完整前端 dist 的目录(含 index.html)。 # 配置后浏览器直连 CDN 加载 JS/CSS/图片等资源,不再经 Worker 中转。 # 注入自动在两条路径中选择: diff --git a/src/backend/drivers/115/driver.ts b/src/backend/drivers/115/driver.ts index c4e77143..270796ae 100644 --- a/src/backend/drivers/115/driver.ts +++ b/src/backend/drivers/115/driver.ts @@ -5,6 +5,7 @@ import { } from "../../internal/driver/base" import { Driver115Addition, Cloud115File } from "./types" import { Client115 } from "./util" +import { sha1 } from "../../pkg/crypto" function fileToItem(f: Cloud115File): FileItem { const isDir = f.Fc === "0" @@ -145,6 +146,62 @@ export class Driver115 implements StorageDriver { await this.client.copy(dstId, [fid]) } + readonly rapidHashAlgos = ["sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { + size: number + sha1?: string + readRange?: (start: number, length: number) => Promise + }, + ): Promise { + const sha1Full = String(metadata.sha1 || "").toUpperCase() + if (!/^[0-9A-F]{40}$/.test(sha1Full) || !metadata.readRange) return false + const parts = physicalPath.split("/").filter(Boolean) + const name = parts.pop() || "file" + const parentPath = "/" + parts.join("/") + const parentId = await this.client.resolvePathId( + parentPath, + this.pathCache, + ) + const preSize = Math.min(128 * 1024, metadata.size) + const preId = ( + await sha1(await metadata.readRange(0, preSize)) + ).toUpperCase() + let initResp = await this.client.uploadInit({ + fileName: name, + fileSize: metadata.size, + target: parentId, + fileId: sha1Full, + preId, + }) + if (initResp.status === 2) return true + if ([6, 7, 8].includes(Number(initResp.status)) && initResp.sign_check) { + const [start, end] = initResp.sign_check + .split("-") + .map((value) => parseInt(value, 10)) + if (Number.isFinite(start) && Number.isFinite(end)) { + const signVal = ( + await sha1(await metadata.readRange(start, end - start + 1)) + ).toUpperCase() + initResp = await this.client.uploadInit({ + fileName: name, + fileSize: metadata.size, + target: parentId, + fileId: sha1Full, + preId, + signKey: initResp.sign_key, + signVal, + }) + return initResp.status === 2 + } + } + return false + } + async put( _virtualPath: string, _physicalPath: string, diff --git a/src/backend/drivers/115/util.ts b/src/backend/drivers/115/util.ts index 33f88b1d..5c19eda0 100644 --- a/src/backend/drivers/115/util.ts +++ b/src/backend/drivers/115/util.ts @@ -180,6 +180,33 @@ export class Client115 { await this.request("/open/ufile/delete", { fid: fids, ignore_warn: 1 }) } + /** Upload-init probe used by hash-based rapid upload. */ + async uploadInit(opts: { + fileName: string + fileSize: number + target: string + fileId: string + preId: string + signKey?: string + signVal?: string + }): Promise<{ + status?: number + sign_key?: string + sign_check?: string + file_id?: string + }> { + const resp = await this.request("/upload/init", { + file_name: opts.fileName, + file_size: String(opts.fileSize), + target: `U_1_${opts.target}`, + fileid: opts.fileId, + preid: opts.preId, + sign_key: opts.signKey || "", + sign_val: opts.signVal || "", + }) + return resp?.data || resp || {} + } + async getDownloadUrl(pickCode: string): Promise { const resp = await this.request("/open/ufile/downurl", { pickcode: pickCode, diff --git a/src/backend/drivers/115open/driver.ts b/src/backend/drivers/115open/driver.ts index 3f780071..e99706a6 100644 --- a/src/backend/drivers/115open/driver.ts +++ b/src/backend/drivers/115open/driver.ts @@ -400,6 +400,65 @@ export class Pan115Driver implements StorageDriver { // ---- Upload(OSS 直传:秒传 → 二次校验 → PUT Object) ---- + readonly rapidHashAlgos = ["sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { + size: number + sha1?: string + readRange?: (start: number, length: number) => Promise + }, + ): Promise { + const sha1Full = String(metadata.sha1 || "").toUpperCase() + if (!/^[0-9A-F]{40}$/.test(sha1Full) || !metadata.readRange) return false + const segs = String(physicalPath || "") + .split("/") + .filter(Boolean) + const fileName = segs.pop() || "file" + const parentPath = "/" + segs.join("/") + const parentId = await this.resolveFolderId(parentPath) + const fileSize = metadata.size + const preSize = Math.min(128 * 1024, fileSize) + const preId = ( + await sha1(await metadata.readRange(0, preSize)) + ).toUpperCase() + if (!this.reserve()) return false + let initResp = await this.client.uploadInit({ + fileName, + fileSize, + target: parentId, + fileId: sha1Full, + preId, + }) + if (initResp.status === 2) return true + // status 6/7/8 asks for a byte-range SHA1 proof before reusing the file. + if ([6, 7, 8].includes(initResp.status) && initResp.sign_check) { + const [start, end] = initResp.sign_check + .split("-") + .map((value) => parseInt(value, 10)) + if (Number.isFinite(start) && Number.isFinite(end)) { + const signVal = ( + await sha1(await metadata.readRange(start, end - start + 1)) + ).toUpperCase() + if (!this.reserve()) return false + initResp = await this.client.uploadInit({ + fileName, + fileSize, + target: parentId, + fileId: sha1Full, + preId, + signKey: initResp.sign_key, + signVal, + }) + return initResp.status === 2 + } + } + return false + } + async put( _virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/123_open/driver.ts b/src/backend/drivers/123_open/driver.ts index 66425d02..cfcbc575 100644 --- a/src/backend/drivers/123_open/driver.ts +++ b/src/backend/drivers/123_open/driver.ts @@ -138,6 +138,32 @@ export class Driver123Open implements StorageDriver { throw new Error("[123Open] copy not supported") } + readonly rapidHashAlgos = ["sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { size: number; sha1?: string }, + ): Promise { + const sha1Hash = String(metadata.sha1 || "").toLowerCase() + if (!/^[0-9a-f]{40}$/.test(sha1Hash)) return false + const parts = physicalPath.split("/").filter(Boolean) + const name = parts.pop() || "file" + const parentPath = "/" + parts.join("/") + const parentId = await this.resolveDirId(parentPath) + const numericId = Number(parentId) + if (!Number.isSafeInteger(numericId)) return false + const result = await this.client.sha1Reuse( + numericId, + name, + sha1Hash, + metadata.size, + 1, + ) + return result.reuse === true + } + async put(): Promise { throw new Error("[123Open] upload not supported in stateless environment") } diff --git a/src/backend/drivers/123_open/util.ts b/src/backend/drivers/123_open/util.ts index 51e75e89..58950306 100644 --- a/src/backend/drivers/123_open/util.ts +++ b/src/backend/drivers/123_open/util.ts @@ -68,6 +68,29 @@ export class Client123Open { throw new Error("[123Open] no valid authentication method (access_token / refresh_token / client_id+client_secret)") } + /** Hash-based rapid upload probe (`/upload/v2/file/sha1_reuse`). */ + async sha1Reuse( + parentFileID: number, + filename: string, + sha1Hash: string, + size: number, + duplicate = 1, + ): Promise<{ reuse?: boolean; fileID?: number }> { + const resp = await this.request<{ reuse?: boolean; fileID?: number }>( + `${API}/upload/v2/file/sha1_reuse`, + "POST", + { + parentFileID, + filename, + sha1: sha1Hash.toLowerCase(), + size, + duplicate, + }, + ) + if (resp.code !== 0) throw new Error(`[123Open] ${resp.message}`) + return resp.data || {} + } + private async request( url: string, method: string, diff --git a/src/backend/drivers/123pan/driver.ts b/src/backend/drivers/123pan/driver.ts index 6b36c7c7..1024c8df 100644 --- a/src/backend/drivers/123pan/driver.ts +++ b/src/backend/drivers/123pan/driver.ts @@ -65,6 +65,9 @@ function pan123FileToFileItem(f: Pan123File): FileItem { } export class Pan123Driver implements StorageDriver { + readonly rapidHashAlgos = ["md5"] + readonly rapidHashNeedsPieces = false + private client: Pan123Client private addition: Pan123Addition /** cache: physical path → folder FileId (string) */ diff --git a/src/backend/drivers/189_tv/driver.ts b/src/backend/drivers/189_tv/driver.ts index 7d0a7dc6..3e00811f 100644 --- a/src/backend/drivers/189_tv/driver.ts +++ b/src/backend/drivers/189_tv/driver.ts @@ -179,6 +179,31 @@ export class Driver189TV implements StorageDriver { this.pathCache.clear() } + readonly rapidHashAlgos = ["md5"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { size: number; md5?: string }, + ): Promise { + const md5Hash = String(metadata.md5 || "").toLowerCase() + if (!/^[0-9a-f]{32}$/.test(md5Hash)) return false + const parts = physicalPath.split("/").filter(Boolean) + const name = parts.pop() || "file" + const parentPath = "/" + parts.join("/") + const parentId = await this.pathToId(parentPath) + const reused = await this.client.rapidUploadCreate( + parentId, + name, + metadata.size, + md5Hash, + this.isFamily(), + ) + if (reused) this.pathCache.clear() + return reused + } + async put( _virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/189_tv/util.ts b/src/backend/drivers/189_tv/util.ts index 3dd65d45..016f9cfc 100644 --- a/src/backend/drivers/189_tv/util.ts +++ b/src/backend/drivers/189_tv/util.ts @@ -477,6 +477,49 @@ export class Client189TV { await this.waitBatchTask("DELETE", taskId) } + /** 仅创建上传会话,用于判断服务端是否已存在相同 MD5 的文件(秒传)。 */ + async rapidUploadCreate( + parentId: string, + fileName: string, + fileSize: number, + fileMd5: string, + isFamily: boolean, + ): Promise { + const base = isFamily ? ApiUrl + "/family/file" : ApiUrl + const createUrl = + base + + (isFamily ? "/createFamilyFile.action" : "/createUploadFile.action") + let createResp: CreateUploadFileResp + if (isFamily) { + createResp = (await this.request(createUrl, "POST", { + query: { + familyId: this.addition.family_id || "", + parentId, + fileMd5, + fileName, + fileSize: String(fileSize), + resumePolicy: "1", + }, + isFamily, + })) as CreateUploadFileResp + } else { + createResp = (await this.request(createUrl, "POST", { + form: { + parentFolderId: parentId, + fileName, + size: String(fileSize), + md5: fileMd5, + opertype: "3", + flag: "1", + resumePolicy: "1", + isLog: "0", + }, + isFamily, + })) as CreateUploadFileResp + } + return createResp.fileDataExists === 1 + } + /** 单片上传(Worker 的 put 接收完整 Buffer) */ async upload( parentId: string, diff --git a/src/backend/drivers/189pc/driver.ts b/src/backend/drivers/189pc/driver.ts index 7b7e9566..688a780a 100644 --- a/src/backend/drivers/189pc/driver.ts +++ b/src/backend/drivers/189pc/driver.ts @@ -292,6 +292,30 @@ export class Cloud189PCDriver implements StorageDriver { }) } + readonly rapidHashAlgos = ["md5"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { size: number; md5?: string }, + ): Promise { + const md5Hash = String(metadata.md5 || "").toLowerCase() + if (!/^[0-9a-f]{32}$/.test(md5Hash) || metadata.size < 0) return false + const pathParts = physicalPath.split("/").filter(Boolean) + const fileName = pathParts.pop() || "upload" + const parentFolderId = pathParts.join("/") || this.rootFolderId + const initResp = (await this.client.requestUploadAPI(InitUploadURL, { + parentFolderId, + fileName, + fileSize: metadata.size, + fileMd5: md5Hash, + sliceSize: DefaultChunkSize, + lazyCheck: 1, + })) as Cloud189PCInitMultiUploadResp + return initResp.fileDataExists === 1 + } + async put( _virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/aliyundrive_open/driver.ts b/src/backend/drivers/aliyundrive_open/driver.ts index e616110d..319cb75a 100644 --- a/src/backend/drivers/aliyundrive_open/driver.ts +++ b/src/backend/drivers/aliyundrive_open/driver.ts @@ -137,6 +137,55 @@ export class AliyundriveOpen implements StorageDriver { await this.client.copy(fileId, dstId) } + readonly rapidHashAlgos = ["sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { + size: number + sha1?: string + readRange?: (start: number, length: number) => Promise + }, + ): Promise { + const sha1Hash = String(metadata.sha1 || "").toLowerCase() + if (!/^[0-9a-f]{40}$/.test(sha1Hash) || !metadata.readRange) return false + if (metadata.size <= 100 * 1024) return false + const parts = physicalPath.split("/").filter(Boolean) + const name = parts.pop() || "upload" + const parentPath = "/" + parts.join("/") + const parentId = await this.resolveFileId(parentPath) + const proofCode = await this.client.getProofCode( + metadata.size, + metadata.readRange, + ) + const createResp = await this.client.openApiRequest( + "/openFile/create", + { + drive_id: this.client.driveId, + parent_file_id: parentId, + name, + type: "file", + check_name_mode: "auto_rename", + size: metadata.size, + content_hash: sha1Hash, + content_hash_name: "sha1", + proof_version: "v1", + proof_code: proofCode, + }, + ) + if (!createResp?.rapid_upload) return false + if (createResp.file_id) { + await this.client.openApiRequest("/openFile/complete", { + drive_id: this.client.driveId, + file_id: createResp.file_id, + upload_id: createResp.upload_id, + }) + } + return true + } + async put( _virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/aliyundrive_open/util.ts b/src/backend/drivers/aliyundrive_open/util.ts index b384a443..98e90516 100644 --- a/src/backend/drivers/aliyundrive_open/util.ts +++ b/src/backend/drivers/aliyundrive_open/util.ts @@ -4,6 +4,7 @@ import { AliyunFileItem, } from "./types" import { assertSafeUrl } from "../../pkg/http" +import { md5 } from "../../pkg/crypto" const ALI_OPEN_API = "https://openapi.aliyundrive.com/adrive/v1.0" @@ -210,6 +211,25 @@ export class AliyunOpenClient { return this.addition.root_folder_id?.trim() || "root" } + /** + * Proof code for hash-based uploads: read an 8-byte window chosen from the + * MD5 of the access token, then base64-encode it. + */ + public async getProofCode( + size: number, + readRange: (start: number, length: number) => Promise, + ): Promise { + if (size <= 0) return "" + await this.ensureToken() + const digest = await md5(this.accessToken) + const seed = BigInt(`0x${digest.slice(0, 16)}`) + const start = Number(seed % BigInt(size)) + const end = Math.min(start + 8, size) + if (end <= start) return "" + const bytes = await readRange(start, end - start) + return Buffer.from(bytes).toString("base64") + } + public async openApiRequest( path: string, body: any, diff --git a/src/backend/drivers/baidu_netdisk/driver.ts b/src/backend/drivers/baidu_netdisk/driver.ts index 56500880..d561a883 100644 --- a/src/backend/drivers/baidu_netdisk/driver.ts +++ b/src/backend/drivers/baidu_netdisk/driver.ts @@ -235,6 +235,34 @@ export class BaiduDriver implements StorageDriver { // ---- Upload (Go Put / PutRapid / precreate / uploadSlice) ---- + readonly rapidHashAlgos = ["md5"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { size: number; md5?: string }, + ): Promise { + const md5Hash = String(metadata.md5 || "").toLowerCase() + if (!/^[0-9a-f]{32}$/.test(md5Hash) || metadata.size < 1) return false + const path = this.baiduPath(physicalPath) + const now = Math.floor(Date.now() / 1000) + try { + await this.client.create( + path, + metadata.size, + 0, + "", + JSON.stringify([md5Hash]), + now, + now, + ) + return true + } catch { + return false + } + } + async put( _virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/pikpak/driver.ts b/src/backend/drivers/pikpak/driver.ts index ddd26e8d..c99c1c22 100644 --- a/src/backend/drivers/pikpak/driver.ts +++ b/src/backend/drivers/pikpak/driver.ts @@ -355,6 +355,40 @@ export class PikPakDriver implements StorageDriver { ) } + readonly rapidHashAlgos = ["sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { size: number; sha1?: string }, + ): Promise { + const sha1Hex = String(metadata.sha1 || "").toUpperCase() + if (!/^[0-9A-F]{40}$/.test(sha1Hex)) return false + const clean = this.cleanPath(physicalPath) + const parentPath = clean.split("/").slice(0, -1).join("/") + const fileName = clean.split("/").pop() || "upload" + const parentId = await this.resolveParentId(parentPath) + const taskResp: any = await this.client.request( + "https://api-drive.mypikpak.net/drive/v1/files", + { + method: "POST", + body: { + kind: "drive#file", + name: fileName, + size: String(metadata.size), + hash: sha1Hex, + upload_type: "UPLOAD_TYPE_RESUMABLE", + objProvider: { provider: "UPLOAD_TYPE_UNKNOWN" }, + parent_id: parentId, + folder_type: "NORMAL", + }, + }, + ) + // A successful rapid upload returns the file without a resumable session. + return !taskResp?.resumable + } + async put( virtualPath: string, physicalPath: string, diff --git a/src/backend/drivers/quark_open/driver.ts b/src/backend/drivers/quark_open/driver.ts index 06f85ff9..8b43a3da 100644 --- a/src/backend/drivers/quark_open/driver.ts +++ b/src/backend/drivers/quark_open/driver.ts @@ -135,6 +135,38 @@ export class DriverQuarkOpen implements StorageDriver { throw new Error("[QuarkOpen] copy not supported") } + readonly rapidHashAlgos = ["md5", "sha1"] + readonly rapidHashNeedsPieces = false + + async rapidUpload( + _virtualPath: string, + physicalPath: string, + metadata: { + size: number + md5?: string + sha1?: string + readRange?: (start: number, length: number) => Promise + }, + ): Promise { + const md5Hash = String(metadata.md5 || "").toLowerCase() + const sha1Hash = String(metadata.sha1 || "").toLowerCase() + if (!/^[0-9a-f]{32}$/.test(md5Hash)) return false + if (!/^[0-9a-f]{40}$/.test(sha1Hash)) return false + if (!metadata.readRange) return false + const parts = physicalPath.split("/").filter(Boolean) + const name = parts.pop() || "file" + const parentPath = "/" + parts.join("/") + const parentId = await this.resolveDirId(parentPath) + return this.client.rapidUpload({ + parentId, + fileName: name, + size: metadata.size, + md5: md5Hash, + sha1: sha1Hash, + readRange: metadata.readRange, + }) + } + async put(): Promise { throw new Error("[QuarkOpen] upload not supported in stateless environment") } diff --git a/src/backend/drivers/quark_open/util.ts b/src/backend/drivers/quark_open/util.ts index 3c4b6bbf..59aa34de 100644 --- a/src/backend/drivers/quark_open/util.ts +++ b/src/backend/drivers/quark_open/util.ts @@ -7,6 +7,7 @@ import { QuarkDownloadData, QuarkRefreshResp, } from "./types" +import { md5 } from "../../pkg/crypto" const API = "https://open-api-drive.quark.cn" const UA = "go-resty/3.0.0-beta.1 (https://resty.dev)" @@ -41,6 +42,66 @@ export class ClientQuarkOpen { throw new Error("[QuarkOpen] refresh_token is required") } + private async proofCode( + readRange: (start: number, length: number) => Promise, + seed: string, + size: number, + ): Promise { + if (size <= 0) return "" + const digest = md5(seed) + const value = BigInt(`0x${digest.slice(0, 16)}`) + const start = Number(value % BigInt(size)) + const end = Math.min(start + 8, size) + if (end <= start) return "" + const bytes = await readRange(start, end - start) + return Buffer.from(bytes).toString("base64") + } + + /** Hash-based rapid upload probe (`/open/v1/file/upload_pre`). */ + async rapidUpload(input: { + parentId: string + fileName: string + size: number + md5: string + sha1: string + readRange: (start: number, length: number) => Promise + }): Promise { + const { token } = await this.generateSign( + "POST", + "/open/v1/file/upload_pre", + ) + const userId = String((this.addition as any).user_id || "") + const proofSeed1 = md5(`${userId}${token}`) + const proofSeed2 = md5(String(input.size)) + const proofCode1 = await this.proofCode( + input.readRange, + proofSeed1, + input.size, + ) + const proofCode2 = await this.proofCode( + input.readRange, + proofSeed2, + input.size, + ) + const resp = await this.request("/open/v1/file/upload_pre", "POST", { + file_name: input.fileName, + size: input.size, + format_type: "application/octet-stream", + md5: input.md5, + sha1: input.sha1, + l_created_at: Date.now(), + l_updated_at: Date.now(), + pdir_fid: input.parentId, + same_path_reuse: true, + proof_version: "v1", + proof_seed1: proofSeed1, + proof_seed2: proofSeed2, + proof_code1: proofCode1, + proof_code2: proofCode2, + }) + return resp?.data?.finish === true + } + /** 生成签名:x-pan-token = sha256(method&path&tm&signKey) */ private async generateSign( method: string, diff --git a/src/backend/internal/driver/base.ts b/src/backend/internal/driver/base.ts index 17df1154..d3d86f22 100644 --- a/src/backend/internal/driver/base.ts +++ b/src/backend/internal/driver/base.ts @@ -23,6 +23,12 @@ export interface FileItem { md5?: string sha1?: string sha256?: string + /** Piece hashes when the driver exposes them (optional). */ + pieces?: { + md5?: string[] + sha1?: string[] + sha256?: string[] + } } } diff --git a/src/backend/internal/seed/types.ts b/src/backend/internal/seed/types.ts index 433ff29f..42db04c9 100644 --- a/src/backend/internal/seed/types.ts +++ b/src/backend/internal/seed/types.ts @@ -23,6 +23,8 @@ export interface SeedHashes { md5: string sha1: string sha256: string + /** GCID used by Thunder/PikPak rapid upload; preserved from Go seeds. */ + gcid?: string pieces: Record } @@ -130,6 +132,14 @@ function normalizeHash(value: unknown, algorithm: SeedHashAlgorithm): string { return hash } +function normalizeGcid(value: unknown): string { + const hash = text(value).toLowerCase() + if (hash && !/^[0-9a-f]{40}$/.test(hash)) { + throw new Error("Invalid gcid hash") + } + return hash +} + function normalizeSources(value: unknown): SeedSource[] { if (!Array.isArray(value)) return [] return value.map((item) => { @@ -185,6 +195,7 @@ export function normalizeSeed(input: unknown): SharingSeed { throw new Error("Invalid seed file size") const hashes = file.hashes || {} const pieces = hashes.pieces || {} + const gcid = normalizeGcid(hashes.gcid) const expectedPieces = size > 0 ? Math.ceil(size / pieceSize) : 0 const normalizedPieces = (algorithm: SeedHashAlgorithm): string[] => { if (!Array.isArray(pieces[algorithm])) return [] @@ -207,6 +218,7 @@ export function normalizeSeed(input: unknown): SharingSeed { md5: normalizeHash(hashes.md5, "md5"), sha1: normalizeHash(hashes.sha1, "sha1"), sha256: normalizeHash(hashes.sha256, "sha256"), + ...(gcid ? { gcid } : {}), pieces: { md5: normalizedPieces("md5"), sha1: normalizedPieces("sha1"), @@ -321,6 +333,7 @@ export function applyHashMatrix( md5: matrix.md5.whole ? file.hashes.md5 : "", sha1: matrix.sha1.whole ? file.hashes.sha1 : "", sha256: matrix.sha256.whole ? file.hashes.sha256 : "", + ...(file.hashes.gcid ? { gcid: file.hashes.gcid } : {}), pieces: { md5: matrix.md5.pieces ? file.hashes.pieces.md5 : [], sha1: matrix.sha1.pieces ? file.hashes.pieces.sha1 : [], diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index 44d93ed9..4f22a648 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -52,7 +52,7 @@ import { import { parseZip, extractZipEntry, ZipArchive } from "../internal/archive/zip" import { assertSafeUrl, getTrustedHosts } from "../pkg/http" import { invalidatePaths } from "../internal/cache" -import { seedRouter } from "./seed" +import { seedRouter, torrentLegacyRouter } from "./seed" import { r2CanSignDirectUpload } from "../drivers/r2/driver" /** @@ -82,6 +82,7 @@ import { export const fsRouter = new Hono() fsRouter.route("/seed", seedRouter) +fsRouter.route("/torrent", torrentLegacyRouter) /** * 请求级存储上下文(`env` + `waitUntil`)。 diff --git a/src/backend/server/seed.test.ts b/src/backend/server/seed.test.ts index 1e5b871f..e0da6182 100644 --- a/src/backend/server/seed.test.ts +++ b/src/backend/server/seed.test.ts @@ -6,6 +6,7 @@ import { hashReadableStream, TorrentPieceHasher } from "../internal/seed/hash" import { normalizeSeed } from "../internal/seed/types" import { saveDb } from "../internal/model/db" import { fsRouter } from "./fs" +import { seedTestHelpers } from "./seed" const ADMIN_TOKEN = "SEED_ADMIN_TOKEN" const env: any = {} @@ -68,6 +69,17 @@ test("OSS codec preserves the versioned canonical schema", async () => { assert.equal(parsed.seed.version, 1) }) +test("OSS and torrent codecs preserve GCID hashes from Go seeds", async () => { + const original = sampleSeed() + original.files[0].hashes.gcid = "0123456789abcdef0123456789abcdef01234567" + + const oss = await parseSeed(encodeOss(original), "oss") + assert.equal(oss.seed.files[0].hashes.gcid, original.files[0].hashes.gcid) + + const torrent = await parseSeed(await encodeTorrent(original), "torrent") + assert.equal(torrent.seed.files[0].hashes.gcid, original.files[0].hashes.gcid) +}) + test("torrent codec writes BT v1 info plus OpenList and CAS extensions", async () => { const original = casSeed() const bytes = await encodeTorrent(original) @@ -164,3 +176,228 @@ test("seed routes expose capabilities and reject an unapproved source", async () }, env) assert.equal(update.status, 400) }) + +test("seed diagnose reports per-format conversion requirements", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const response = await app.request("/api/fs/seed/diagnose", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({ seed: sampleSeed() }), + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.deepEqual(Object.keys(data.data).sort(), ["cas", "oss", "torrent"]) + assert.ok(Array.isArray(data.data.cas)) +}) + +test("seed upload_parse accepts multipart seed uploads", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const form = new FormData() + form.set( + "seed", + new Blob([Buffer.from(encodeOss(sampleSeed()))]), + "hello.seed", + ) + const response = await app.request("/api/fs/seed/upload_parse", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN }, + body: form, + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.equal(data.data.format, "oss") + assert.ok(data.data.seed_data.length > 0) + assert.deepEqual(Object.keys(data.data.diagnostics).sort(), [ + "cas", + "oss", + "torrent", + ]) +}) + +test("seed update_channels shares the update handler", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + env.SEED_SOURCE_ALLOWED_HOSTS = "files.example" + const app = new Hono() + app.route("/api/fs", fsRouter) + const response = await app.request("/api/fs/seed/update_channels", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({ seed: sampleSeed() }), + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.equal(data.data.format, "oss") + delete env.SEED_SOURCE_ALLOWED_HOSTS +}) + +test("seed capability formats follow driver-provided hashes", () => { + assert.deepEqual( + seedTestHelpers.seedFormatsForFiles([ + { size: 1024, available_hashes: ["md5"], available_piece_hashes: [] }, + ]), + { oss: true, torrent: false, cas: true }, + ) + assert.deepEqual( + seedTestHelpers.seedFormatsForFiles([ + { + size: 20 * 1024 * 1024, + available_hashes: ["md5", "sha1"], + available_piece_hashes: [], + }, + ]), + { oss: true, torrent: false, cas: false }, + ) + assert.deepEqual( + seedTestHelpers.seedFormatsForFiles([ + { + size: 20 * 1024 * 1024, + available_hashes: ["md5", "sha1"], + available_piece_hashes: ["md5", "sha1"], + }, + ]), + { oss: true, torrent: true, cas: true }, + ) +}) + +test("legacy torrent parse returns the TorrentInfo contract", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const torrent = await encodeTorrent(casSeed()) + const response = await app.request("/api/fs/torrent/parse", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({ + torrent_data: Buffer.from(torrent).toString("base64"), + }), + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.equal(data.data.name, "hello.txt") + assert.equal(data.data.total_size, 5) + assert.equal(data.data.info_hash.length, 40) + assert.equal(data.data.files.length, 1) +}) + +test("seed update removes files and reports share validity", async () => { + const base = sampleSeed() + const second = { ...base.files[0], path: "second.txt" } + const seed = normalizeSeed({ ...base, files: [base.files[0], second] }) + seed.files[0].sources = [ + { + type: "openlist-share", + url: "https://files.example/sd/share-ok", + expires_at: "", + share_id: "share-ok", + }, + ] + seed.files[1].sources = [ + { + type: "openlist-share", + url: "https://files.example/sd/share-off", + expires_at: "", + share_id: "share-off", + }, + ] + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [ + { + id: "share-ok", + files: ["/hello.txt"], + disabled: false, + expires: null, + max_accessed: 0, + accessed: 0, + }, + { + id: "share-off", + files: ["/second.txt"], + disabled: true, + expires: null, + max_accessed: 0, + accessed: 0, + }, + ], + } as any, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const response = await app.request("/api/fs/seed/update", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({ seed, remove_files: ["second.txt"] }), + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.equal(data.data.seed.files.length, 1) + assert.equal(data.data.seed.files[0].path, "hello.txt") + assert.equal(data.data.share_status["share-ok"], true) + assert.equal(data.data.share_status["share-off"], undefined) +}) + +test("seed quick_save route accepts the unified transfer body", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const response = await app.request("/api/fs/seed/quick_save", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({}), + }, env) + assert.equal(response.status, 400) +}) + +test("seed convert accepts target_format from the frontend contract", async () => { + await saveDb({ + settings: [{ key: "token", value: ADMIN_TOKEN }], + users: [], + storages: [], + shares: [], + }, env) + const app = new Hono() + app.route("/api/fs", fsRouter) + const response = await app.request("/api/fs/seed/convert", { + method: "POST", + headers: { Authorization: ADMIN_TOKEN, "Content-Type": "application/json" }, + body: JSON.stringify({ + seed_data: Buffer.from(encodeOss(sampleSeed())).toString("base64"), + file_name: "hello.oss", + target_format: "torrent", + }), + }, env) + assert.equal(response.status, 200) + const data = await response.json() as any + assert.equal(data.data.convertible, true) + assert.equal(data.data.format, "torrent") +}) diff --git a/src/backend/server/seed.ts b/src/backend/server/seed.ts index e5781b80..42bf61df 100644 --- a/src/backend/server/seed.ts +++ b/src/backend/server/seed.ts @@ -3,7 +3,8 @@ import { getUserFromContext } from "./middlewares" import { canWrite, getActualPath, can, PermissionBit } from "../pkg/permission" import { assertSafeUrl } from "../pkg/http" import { safeErrorMessage } from "../pkg/errs" -import { getSettings, resolvePath } from "../internal/model/db" +import { getDb, getSettings, resolvePath, saveDb } from "../internal/model/db" +import { generateShareId } from "./share" import { flushPendingDriverState, getDriver, @@ -191,9 +192,10 @@ function encodedResult( function allowedHosts(c: any): string[] { const raw = + c.env?.SEED_SOURCE_ALLOWED_HOSTS ?? c.env?.ALLOW_SEED ?? (typeof process !== "undefined" - ? process.env?.ALLOW_SEED + ? (process.env?.SEED_SOURCE_ALLOWED_HOSTS ?? process.env?.ALLOW_SEED) : "") return String(raw || "") .split(/[\s,;]+/) @@ -222,12 +224,16 @@ function validateSource(c: any, source: SeedSource): URL { url.origin === requestUrl.origin && (/^\/(api\/)?p\//.test(url.pathname) || url.pathname.startsWith("/d/") || + url.pathname.startsWith("/sd/") || url.pathname.startsWith("/@s/")) + // Align with the Go backend: shares live under /sd/ (legacy /@s/ is still + // accepted), direct sources under /d/. const shareSource = ["share", "openlist-share"].includes(type) && - url.pathname.startsWith("/@s/") + (url.pathname.startsWith("/sd/") || url.pathname.startsWith("/@s/")) const directSource = - ["direct", "openlist-direct"].includes(type) && sameOpenList + ["direct", "openlist-direct"].includes(type) && + url.pathname.startsWith("/d/") if ( !shareSource && !directSource && @@ -352,7 +358,12 @@ interface SourceFile { modified: string rawUrl: string headers: Record - hashes: { md5?: string; sha1?: string; sha256?: string } + hashes: { + md5?: string + sha1?: string + sha256?: string + pieces?: { md5?: string[]; sha1?: string[]; sha256?: string[] } + } } async function collectSourceFiles( @@ -556,64 +567,126 @@ async function generateSeed( ? body.file_comments : {} const sourceFiles = await collectSourceFiles(c, user, body.paths ?? body.path) - const maxBytes = envNumber(c, "SEED_MAX_HASH_BYTES", DEFAULT_MAX_HASH_BYTES) - const totalSize = sourceFiles.reduce((sum, file) => sum + file.size, 0) - if (!Number.isSafeInteger(totalSize) || totalSize > maxBytes) { - throw new Error(`Seed input exceeds the ${maxBytes} byte hashing limit`) - } - const torrentHasher = await TorrentPieceHasher.create(pieceSize) const siteUrl = await resolveSeedSiteUrl(c) + const shareFiles = new Set( + Array.isArray(body.share_files) + ? body.share_files.map((path: unknown) => String(path)) + : [], + ) + const directFiles = new Set( + Array.isArray(body.direct_files) + ? body.direct_files.map((path: unknown) => String(path)) + : [], + ) + const includeShare = body.include_share === true || shareFiles.size > 0 + const includeDirect = + body.include_direct_source === true || + body.include_sources === true || + directFiles.size > 0 + if (includeShare && !can(user, PermissionBit.SHARE)) + throw new Error("Permission denied") + if ((includeShare || includeDirect) && !siteUrl) + throw new Error( + "seed_site_url must be configured before embedding download sources", + ) + const useGlobalShare = shareFiles.size === 0 && body.include_share === true + const useGlobalDirect = + directFiles.size === 0 && + (body.include_direct_source === true || body.include_sources === true) + let sharesDb: any | undefined const files: SeedFile[] = [] + const torrentPieces: string[] = [] for (const file of sourceFiles) { - const response = await fetchSafe(c, new URL(file.rawUrl, c.req.url), { - headers: file.headers, - }) - if (!response.ok) + const available = file.hashes || {} + const pieces = available.pieces || {} + const missing: string[] = [] + if (matrix.md5.whole && !available.md5) missing.push("md5") + if (matrix.sha1.whole && !available.sha1) missing.push("sha1") + if (matrix.sha256.whole && !available.sha256) missing.push("sha256") + if (matrix.md5.pieces && !(pieces.md5?.length ?? 0)) + missing.push("md5 pieces") + if (matrix.sha1.pieces && !(pieces.sha1?.length ?? 0)) + missing.push("sha1 pieces") + if (matrix.sha256.pieces && !(pieces.sha256?.length ?? 0)) + missing.push("sha256 pieces") + if (missing.length) throw new Error( - `Source download failed with HTTP ${response.status}: ${file.virtualPath}`, + `Source does not expose ${missing.join(", ")} for ${file.virtualPath}; ` + + "the TS backend only reuses hashes provided by the storage driver", ) - if (!response.body) - throw new Error(`Source download has no body: ${file.virtualPath}`) - const result = await hashReadableStream( - response.body, - pieceSize, - file.size, - maxBytes, - torrentHasher, - ) const comment = String( fileComments[file.relativePath] ?? fileComments[file.virtualPath] ?? "", ) - files.push( - applyHashMatrix( - { - path: file.relativePath, - size: result.size, - modified: file.modified, - comment, - hashes: result.hashes, - cas_slice_md5: "", - cas_create_time: "", - cas_cloud: "", - missing_channels: [], - sources: - body.include_direct_source === true || body.include_sources === true - ? [ - { - type: "openlist-direct", - url: `${siteUrl}/d${file.virtualPath}`, - expires_at: "", - share_id: "", - }, - ] - : [], + const matches = (set: Set) => + set.has(file.virtualPath) || set.has(file.relativePath) + const sources: SeedSource[] = [] + if (useGlobalDirect || matches(directFiles)) { + sources.push({ + type: "openlist-direct", + url: `${siteUrl}/d${file.virtualPath}`, + expires_at: "", + share_id: "", + }) + } + if (useGlobalShare || matches(shareFiles)) { + if (!sharesDb) sharesDb = await getDb(c.env) + const shareId = generateShareId() + if (!sharesDb.shares) sharesDb.shares = [] + sharesDb.shares.push({ + id: shareId, + new_id: shareId, + creator: user.username || "user", + creator_role: user.role ?? 1, + accessed: 0, + expires: null, + pwd: "", + max_accessed: 0, + disabled: false, + order_by: "", + order_direction: "", + extract_folder: "", + files: [file.virtualPath], + remark: "", + readme: "", + header: "", + }) + sources.push({ + type: "openlist-share", + url: `${siteUrl}/sd/${shareId}`, + expires_at: "", + share_id: shareId, + }) + } + const seedFile = applyHashMatrix( + { + path: file.relativePath, + size: file.size, + modified: file.modified, + comment, + hashes: { + md5: available.md5 || "", + sha1: available.sha1 || "", + sha256: available.sha256 || "", + pieces: { + md5: pieces.md5 || [], + sha1: pieces.sha1 || [], + sha256: pieces.sha256 || [], + }, }, - matrix, - ), + cas_slice_md5: "", + cas_create_time: "", + cas_cloud: "", + missing_channels: [], + sources, + }, + matrix, ) + files.push(seedFile) + torrentPieces.push(...seedFile.hashes.pieces.sha1) } + if (sharesDb) await saveDb(sharesDb, c.env) return { - torrentPieces: torrentHasher.digest(), + torrentPieces, seed: normalizeSeed({ name: String(body.name || deriveSeedName(sourceFiles)), comment: String(body.comment || "Generated by OpenList"), @@ -661,12 +734,32 @@ async function uploadStreamToDriver( } const driver = await getDriver(resolved.storage.driver, resolved.storage) const dynamic = driver as any + const readRange = source + ? async (start: number, length: number) => { + if (length <= 0) return new Uint8Array() + const response = await fetchSafe( + c, + source.url, + { headers: { Range: `bytes=${start}-${start + length - 1}` } }, + true, + ) + if (!response.ok || !response.body) + throw new Error( + `Source range read failed with HTTP ${response.status}`, + ) + const buffer = new Uint8Array(await response.arrayBuffer()) + if (buffer.byteLength !== length) + throw new Error("Source range read returned an unexpected size") + return buffer + } + : undefined const metadata = { size: file.size, md5: file.hashes.md5, sha1: file.hashes.sha1, sha256: file.hashes.sha256, hashes: file.hashes, + readRange, } try { if (typeof dynamic.rapidUpload === "function") { @@ -688,6 +781,17 @@ async function uploadStreamToDriver( return { status: "reused", method: "putRapid" } } + if (typeof dynamic.putUrl === "function" && source) { + const result = await dynamic.putUrl( + actualTarget, + resolved.physical, + source.url.toString(), + metadata, + ) + if (result === true || result?.success || result?.reuse) + return { status: "saved", method: "putUrl" } + } + let uploadSession: any if (typeof dynamic.createUploadSession === "function") { uploadSession = await dynamic.createUploadSession( @@ -857,6 +961,31 @@ seedRouter.get("/capabilities", async (c) => { }) }) +interface SeedCapabilityLikeFile { + size: number + available_hashes: string[] + available_piece_hashes: string[] +} + +// A format is only offered when every selected file carries the hashes that +// format needs. Torrent requires per-file SHA-1 pieces, CAS requires MD5 (and +// either 10 MiB pieces or a whole-file MD5 for small files). +function seedFormatsForFiles( + files: SeedCapabilityLikeFile[], +): Record { + const everyFile = (predicate: (file: SeedCapabilityLikeFile) => boolean) => + files.length > 0 && files.every(predicate) + return { + oss: files.length > 0, + torrent: everyFile((file) => file.available_piece_hashes.includes("sha1")), + cas: everyFile((file) => { + if (!file.available_hashes.includes("md5")) return false + if (file.size <= DEFAULT_PIECE_SIZE) return true + return file.available_piece_hashes.includes("md5") + }), + } +} + seedRouter.post("/capabilities", async (c) => { const user = await getUserFromContext(c) if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") @@ -869,25 +998,37 @@ seedRouter.post("/capabilities", async (c) => { .split("\n") .map((line) => line.trim()) .filter(Boolean) - const hashAlgorithms = ["md5", "sha1", "sha256"] as const + const hashAlgorithms: Array<"md5" | "sha1" | "sha256"> = [ + "md5", + "sha1", + "sha256", + ] const files = sourceFiles.map((file) => { const availableHashes = hashAlgorithms.filter( (algorithm) => !!file.hashes[algorithm], ) - const requiresDownload = availableHashes.length < hashAlgorithms.length + const availablePieceHashes = hashAlgorithms.filter( + (algorithm) => (file.hashes.pieces?.[algorithm]?.length ?? 0) > 0, + ) + const missingHashes = hashAlgorithms.filter( + (algorithm) => !file.hashes[algorithm], + ) return { path: file.virtualPath, name: file.relativePath, size: file.size, available_hashes: availableHashes, - requires_download: requiresDownload, - requires_fetch: requiresDownload, - estimated_traffic: requiresDownload ? file.size : 0, + available_piece_hashes: availablePieceHashes, + missing_hashes: missingHashes, + requires_download: false, + requires_fetch: false, + estimated_traffic: 0, streamable: true, share_available: true, direct_source_available: true, } }) + const formats = seedFormatsForFiles(files) const existing = new Set() for (const file of sourceFiles) { for (const algorithm of hashAlgorithms) { @@ -897,17 +1038,34 @@ seedRouter.post("/capabilities", async (c) => { const existingHashes = hashAlgorithms.filter((algorithm) => existing.has(algorithm), ) + const missingHashes = hashAlgorithms.filter((algorithm) => + files.some((file) => file.missing_hashes.includes(algorithm)), + ) return c.json({ code: 200, message: "success", data: { - formats: { oss: true, torrent: true, cas: true }, + formats, files, existing_hashes: existingHashes, - estimated_traffic: files.reduce( - (total, file) => total + file.estimated_traffic, - 0, - ), + missing_hashes: missingHashes, + conversion: { + oss: { feasible: formats.oss }, + torrent: { + feasible: formats.torrent, + missing: formats.torrent ? [] : ["missing SHA-1 piece hashes"], + }, + cas: { + feasible: formats.cas, + missing: formats.cas + ? [] + : ["missing MD5 hash or 10 MiB MD5 pieces"], + }, + }, + // The worker has no per-file hashing pipeline: it can only emit a + // seed from hashes the storage driver already returned. + hash_reuse_only: true, + estimated_traffic: 0, default_matrix: await loadDefaultMatrix(), trackers, }, @@ -940,6 +1098,7 @@ seedRouter.post("/capabilities", async (c) => { (typeof dynamic.createUploadSession === "function" && typeof dynamic.uploadPart === "function" && typeof dynamic.completeUploadSession === "function") + const putUrl = typeof dynamic.putUrl === "function" const bufferedMax = envNumber( c, "SEED_BUFFERED_TRANSFER_MAX", @@ -958,6 +1117,7 @@ seedRouter.post("/capabilities", async (c) => { const hasSource = !!chooseSource(c, file) let method = "download_required" if (rapid && hasRapidHash) method = "rapid_upload" + else if (putUrl && hasSource) method = "put_url" else if (hasSource && streaming) method = "stream_upload" else if (hasSource && file.size <= bufferedMax) method = "bounded_upload" return { @@ -976,6 +1136,9 @@ seedRouter.post("/capabilities", async (c) => { cas_rapid: rapid, rapid_hash_algos: rapidAlgos, rapid_uses_pieces: rapidUsesPieces, + put_url: putUrl, + // The worker does not dispatch background offline-download tasks. + offline_download: false, }, files, }, @@ -1060,6 +1223,10 @@ seedRouter.post("/parse", async (c) => { const hasRapidHashes = parsed.seed.files.some( (file) => !!file.hashes.md5 || !!file.hashes.sha1 || !!file.hashes.sha256, ) + const settings = await getSettings() + const directPreview = + parsed.seed.files.length === 1 && + String(settings["seed_single_direct_preview"] ?? "false") === "true" return c.json({ code: 200, message: "success", @@ -1072,13 +1239,63 @@ seedRouter.post("/parse", async (c) => { conversions: seedConversionStates(parsed.seed), capabilities: { rapid_upload: hasRapidHashes, - offline_download: hasSource, + // A torrent carries its own peer-to-peer payload, so it is always + // offline-downloadable even without an OpenList source URL. + offline_download: hasSource || parsed.format === "torrent", transfer: hasSource, convert: true, edit: true, recalculate: true, }, - direct_preview: false, + direct_preview: directPreview, + ...(parsed.info_hash ? { info_hash: parsed.info_hash } : {}), + }, + }) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +}) + +seedRouter.post("/diagnose", async (c) => { + const user = await getUserFromContext(c) + if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") + try { + const body = await readJson(c) + const parsed = await parseBodySeed(c, user, body) + return c.json({ + code: 200, + message: "success", + data: seedDiagnostics(parsed.seed), + }) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +}) + +seedRouter.post("/upload_parse", async (c) => { + const user = await getUserFromContext(c) + if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") + try { + const max = envNumber(c, "SEED_MAX_METADATA_SIZE", DEFAULT_MAX_SEED_BYTES) + const form = await c.req.formData() + const upload = form.get("seed") + if (!upload || typeof upload === "string") + throw new Error("seed file is required") + if (upload.size > max) + return errorResponse(c, 413, `Seed file exceeds the ${max} byte limit`) + const bytes = new Uint8Array(await upload.arrayBuffer()) + if (bytes.byteLength > max) + return errorResponse(c, 413, `Seed file exceeds the ${max} byte limit`) + if (!bytes.byteLength) throw new Error("seed file is empty") + const parsed = await parseSeed(bytes) + return c.json({ + code: 200, + message: "success", + data: { + format: parsed.format, + seed: parsed.seed, + seed_data: Buffer.from(bytes).toString("base64"), + diagnostics: seedDiagnostics(parsed.seed), ...(parsed.info_hash ? { info_hash: parsed.info_hash } : {}), }, }) @@ -1092,7 +1309,7 @@ seedRouter.post("/convert", async (c) => { if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") try { const body = await readJson(c) - if (body.save_path && !canWrite(user)) + if ((body.save_path || body.output_path) && !canWrite(user)) return errorResponse(c, 403, "Permission denied") const target = detectFormat( body.to_format || body.target_format || body.output_format, @@ -1107,12 +1324,27 @@ seedRouter.post("/convert", async (c) => { data: { convertible: false, diagnostics }, }) } + const hasInlineSeed = + body.seed_data || body.content || body.data || body.torrent_data + const savePath = + body.save_path || + body.output_path || + (hasInlineSeed ? body.path : undefined) const bytes = await encodeSeed(parsed.seed, target) - await saveEncodedSeed(c, user, body.save_path, bytes) + await saveEncodedSeed(c, user, savePath, bytes) + const savedPath = savePath + ? joinVirtualPath( + normalizeVirtualPath(savePath), + fileNameFor(parsed.seed, target), + ) + : undefined return c.json({ code: 200, message: "success", - data: encodedResult(parsed.seed, target, bytes, { convertible: true }), + data: encodedResult(parsed.seed, target, bytes, { + convertible: true, + ...(savedPath ? { path: savedPath } : {}), + }), }) } catch (error) { return errorResponse(c, 400, safeErrorMessage(error)) @@ -1152,18 +1384,48 @@ function applySeedChannelUpdate( } } -async function transferHandler(c: any, rapidOnly: boolean) { +async function transferHandler( + c: any, + rapidOnly: boolean | ((body: any) => boolean), +) { + try { + const body = await readJson(c) + return await transferBody(c, body, rapidOnly) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +} + +async function transferBody( + c: any, + body: any, + rapidOnly: boolean | ((body: any) => boolean), +) { const user = await getUserFromContext(c) if (!canWrite(user)) return errorResponse(c, 403, "Permission denied") - if (!rapidOnly && !can(user, PermissionBit.OFFLINE_DOWNLOAD)) { - return errorResponse(c, 403, "Offline download permission is required") - } try { - const body = await readJson(c) + const rapid = typeof rapidOnly === "function" ? rapidOnly(body) : rapidOnly + if (!rapid && !can(user, PermissionBit.OFFLINE_DOWNLOAD)) { + return errorResponse(c, 403, "Offline download permission is required") + } const parsed = await parseBodySeed(c, user, body) - const targetRoot = normalizeVirtualPath( + const finalRoot = normalizeVirtualPath( body.target_path || body.destination || body.path || "/", ) + const transitPath = String( + body.transit_path || + (body.options && typeof body.options === "object" + ? body.options.transit_path + : "") || + "", + ).trim() + const transitMode = + !!transitPath && + String(body.options?.mode || "").toLowerCase() === "transfer" + // Save into the intermediate directory first, then relay to the target. + const targetRoot = transitMode + ? normalizeVirtualPath(transitPath) + : finalRoot const selected = new Set( Array.isArray(body.files) ? body.files.map((path: unknown) => String(path)) @@ -1209,7 +1471,7 @@ async function transferHandler(c: any, rapidOnly: boolean) { const target = joinVirtualPath(targetRoot, file.path) const source = chooseSource(c, file) try { - if (!rapidOnly && !source) { + if (!rapid && !source) { throw new Error( "capability unavailable: offline download requires an approved source URL", ) @@ -1220,15 +1482,21 @@ async function transferHandler(c: any, rapidOnly: boolean) { file, target, source, - rapidOnly, + rapid, ) saved++ successByPath.set(file.path, true) - results.push({ path: file.path, target, ...result }) + results.push({ + path: file.path, + name: file.path.split("/").pop() || file.path, + target, + ...result, + }) } catch (error) { successByPath.set(file.path, false) results.push({ path: file.path, + name: file.path.split("/").pop() || file.path, target, status: "unavailable", error: safeErrorMessage(error), @@ -1239,7 +1507,7 @@ async function transferHandler(c: any, rapidOnly: boolean) { return c.json( { code: 501, - message: rapidOnly + message: rapid ? "capability unavailable: target driver cannot reuse hashes and no transferable source is available" : "capability unavailable: no approved source and streaming upload combination is available", data: { results }, @@ -1247,12 +1515,82 @@ async function transferHandler(c: any, rapidOnly: boolean) { 501, ) } + if (transitMode) { + const relayed: Array> = [] + let relayedCount = 0 + for (const result of results) { + if (result.status === "unavailable") { + relayed.push(result) + continue + } + const filePath = String(result.path || "") + const seedFile = parsed.seed.files.find( + (item) => item.path === filePath, + ) + const finalTarget = joinVirtualPath(finalRoot, filePath) + if (!seedFile) { + relayed.push({ + ...result, + status: "unavailable", + error: "Seed file not found", + }) + continue + } + try { + const transitTarget = joinVirtualPath(targetRoot, filePath) + const actualTransit = getActualPath(user, transitTarget) + const { item, rawUrl } = await getItem( + actualTransit, + storageContext(c), + ) + const url = new URL(item.raw_url || rawUrl, c.req.url) + const transferred = await uploadStreamToDriver( + c, + user, + seedFile, + finalTarget, + { + source: { + type: "openlist-direct", + url: url.toString(), + expires_at: "", + share_id: "", + }, + url, + }, + false, + ) + relayedCount++ + relayed.push({ + path: filePath, + name: filePath.split("/").pop() || filePath, + target: finalTarget, + ...transferred, + method: "transfer", + }) + } catch (error) { + relayed.push({ + path: filePath, + name: filePath.split("/").pop() || filePath, + target: finalTarget, + status: "unavailable", + method: "transfer", + error: safeErrorMessage(error), + }) + } + } + return c.json({ + code: 200, + message: "success", + data: { total: relayed.length, saved: relayedCount, results: relayed }, + }) + } const payload: Record = { total: results.length, saved, results, } - if (body.update_channel === true && targetDriver) { + if (!transitMode && body.update_channel === true && targetDriver) { applySeedChannelUpdate( parsed.seed, targetDriver, @@ -1271,13 +1609,58 @@ async function transferHandler(c: any, rapidOnly: boolean) { seedRouter.post("/rapid_upload", (c) => transferHandler(c, true)) seedRouter.post("/offline_download", (c) => transferHandler(c, false)) +seedRouter.post("/quick_save", (c) => + transferHandler(c, (body) => { + const method = String(body.method || body.mode || "").toLowerCase() + return ( + body.rapid_only === true || method === "rapid_upload" || method === "rapid" + ) + }), +) -seedRouter.post("/update", async (c) => { +function shareIsValid(share: any): boolean { + if (!share || share.disabled) return false + const maxAccessed = Number(share.max_accessed || 0) + if (maxAccessed > 0 && Number(share.accessed || 0) >= maxAccessed) return false + if (!Array.isArray(share.files) || share.files.length === 0) return false + if (share.expires) { + const expires = Date.parse(String(share.expires)) + if (Number.isFinite(expires) && expires <= Date.now()) return false + } + return true +} + +// Mirrors the Go backend: report whether each openlist-share source still +// points at a usable sharing. +async function checkSeedShareValidity( + c: any, + seed: SharingSeed, +): Promise> { + const status: Record = {} + let shares: any[] | undefined + for (const file of seed.files) { + for (const source of file.sources) { + if (source.type !== "openlist-share") continue + const shareId = String(source.share_id || "").trim() + if (!shareId || status[shareId] !== undefined) continue + if (!shares) { + const db = await getDb(c.env) + shares = db.shares || [] + } + const list: any[] = shares ?? [] + status[shareId] = shareIsValid(list.find((item) => item.id === shareId)) + } + } + return status +} + +const updateSeedHandler = async (c: any) => { const user = await getUserFromContext(c) if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") try { const body = await readJson(c) - if (body.save_path && !canWrite(user)) + const outputPath = String(body.output_path || body.save_path || "") + if (outputPath && !canWrite(user)) return errorResponse(c, 403, "Permission denied") const parsed = await parseBodySeed(c, user, body) const patch = @@ -1286,7 +1669,7 @@ seedRouter.post("/update", async (c) => { body.options && typeof body.options === "object" ? body.options : {} const comment = patch.comment ?? options.comment const recalculate = !!(patch.recalculate ?? options.recalculate) - const seed = normalizeSeed({ + let seed = normalizeSeed({ ...parsed.seed, name: patch.name ?? parsed.seed.name, comment: comment ?? parsed.seed.comment, @@ -1310,6 +1693,20 @@ seedRouter.post("/update", async (c) => { file.sources = fileSources[file.path] } } + const removeFiles = Array.isArray(body.remove_files) + ? body.remove_files.map((path: unknown) => String(path).trim()) + : [] + const removeSet = new Set(removeFiles.filter(Boolean)) + if (removeFiles.length > 0) { + if (removeSet.size === 0) + throw new Error( + "remove_files requires at least one non-empty path", + ) + const kept = seed.files.filter((file) => !removeSet.has(file.path)) + if (kept.length === 0) + throw new Error("cannot remove all files from a seed") + seed = normalizeSeed({ ...seed, files: kept }) + } if (recalculate) { const pieceSize = Number( body.piece_size || parsed.seed.piece_size || DEFAULT_PIECE_SIZE, @@ -1377,11 +1774,148 @@ seedRouter.post("/update", async (c) => { const format = detectFormat(body.output_format || body.to_format) || parsed.format const bytes = await encodeSeed(seed, format) - await saveEncodedSeed(c, user, body.save_path, bytes) + await saveEncodedSeed(c, user, outputPath, bytes) + const savedPath = outputPath + ? joinVirtualPath( + normalizeVirtualPath(outputPath), + fileNameFor(seed, format), + ) + : undefined + const result = encodedResult(seed, format, bytes, { + ...(savedPath ? { path: savedPath } : {}), + }) + const shareStatus = await checkSeedShareValidity(c, seed) + if (Object.keys(shareStatus).length > 0) + (result as Record).share_status = shareStatus + return c.json({ + code: 200, + message: "success", + data: result, + }) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +} + +seedRouter.post("/update", updateSeedHandler) +seedRouter.post("/update_channels", updateSeedHandler) + +function torrentInfoFromParsed(parsed: any) { + const seed = parsed.seed + return { + name: seed.name, + total_size: seed.files.reduce( + (sum: number, file: any) => sum + file.size, + 0, + ), + piece_length: seed.piece_size, + piece_count: seed.files.reduce( + (sum: number, file: any) => sum + file.hashes.pieces.sha1.length, + 0, + ), + info_hash: parsed.info_hash || "", + files: seed.files, + has_cas: seed.files.some( + (file: any) => !!file.cas_slice_md5 || file.hashes.pieces.md5.length > 0, + ), + cas: parsed.cas + ? { + file_md5: parsed.cas.md5, + slice_md5: parsed.cas.sliceMd5, + slice_size: seed.piece_size, + cloud: parsed.cas.cloud, + } + : undefined, + } +} + +// Legacy `/fs/torrent/*` endpoints kept for older frontends and API clients. +export const torrentLegacyRouter = new Hono() + +torrentLegacyRouter.post("/parse", async (c) => { + const user = await getUserFromContext(c) + if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") + try { + const body = await readJson(c) + const parsed = await parseBodySeed(c, user, body) + return c.json({ + code: 200, + message: "success", + data: torrentInfoFromParsed(parsed), + }) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +}) + +torrentLegacyRouter.post("/upload_parse", async (c) => { + const user = await getUserFromContext(c) + if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") + try { + const max = envNumber(c, "SEED_MAX_METADATA_SIZE", DEFAULT_MAX_SEED_BYTES) + const form = await c.req.formData() + const upload = form.get("torrent") + if (!upload || typeof upload === "string") + throw new Error("torrent file is required") + if (upload.size > max) + return errorResponse(c, 413, `Torrent file exceeds the ${max} byte limit`) + const bytes = new Uint8Array(await upload.arrayBuffer()) + if (bytes.byteLength > max) + return errorResponse(c, 413, `Torrent file exceeds the ${max} byte limit`) + const parsed = await parseSeed(bytes) + return c.json({ + code: 200, + message: "success", + data: { + info: torrentInfoFromParsed(parsed), + torrent_data: Buffer.from(bytes).toString("base64"), + }, + }) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +}) + +torrentLegacyRouter.post("/rapid_upload", async (c) => { + try { + const body = await readJson(c) + return await transferBody( + c, + { + ...body, + seed_data: body.seed_data || body.torrent_data, + target_path: body.target_path || body.path, + }, + true, + ) + } catch (error) { + return errorResponse(c, 400, safeErrorMessage(error)) + } +}) + +torrentLegacyRouter.post("/generate", async (c) => { + const user = await getUserFromContext(c) + if (!user || user.disabled) return errorResponse(c, 401, "Unauthorized") + try { + const body = await readJson(c) + const paths = Array.isArray(body.paths) + ? body.paths + : body.path + ? [body.path] + : [] + if (!paths.length) throw new Error("path is required") + const { seed, torrentPieces } = await generateSeed( + c, + user, + { ...body, paths }, + ["torrent"], + ) + const bytes = await encodeTorrent(seed, torrentPieces) + await saveEncodedSeed(c, user, body.save_path || body.output_path, bytes) return c.json({ code: 200, message: "success", - data: encodedResult(seed, format, bytes), + data: encodedResult(seed, "torrent", bytes), }) } catch (error) { return errorResponse(c, 400, safeErrorMessage(error)) @@ -1393,4 +1927,5 @@ export const seedTestHelpers = { decodeOss, hostAllowed, normalizeVirtualPath, + seedFormatsForFiles, } diff --git a/src/backend/server/share.ts b/src/backend/server/share.ts index 2d1b7391..7d2bdd2c 100644 --- a/src/backend/server/share.ts +++ b/src/backend/server/share.ts @@ -132,7 +132,7 @@ shareRouter.post("/create", async (c) => { }) /** Generate a random 16-char share id using Web Crypto (Workers-compatible) */ -function generateShareId(): string { +export function generateShareId(): string { const uuid = crypto.randomUUID().replace(/-/g, "") return uuid.slice(0, 16) } From 9f09807fb68f0f0bc49811938cd65c356a965f53 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:01:23 +0800 Subject: [PATCH 27/49] test(auth): rebase onto main; keep isolation harness, adopt main F-11 semantics (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解决与 main 的冲突:main 已将 F-11 user/create 语义改为"不生成随机密码、 不回显密码(对齐 Go SetPassword(""))",本 PR 在此基础上保留原修复意图: - run() 隔离 harness(模块级 db 缓存重置 + 一次性内存 store + ADMIN_PASS 恢复) - test 包装 concurrency:false,并注明 ADMIN_PASS 为进程级全局状态的并发约束 (回应评审建议) - seed 显式断言 saveDb 成功并默认 force:true(写入守卫仅约束生产,测试数据需要) --- .../server/default_credentials.test.ts | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/src/backend/server/default_credentials.test.ts b/src/backend/server/default_credentials.test.ts index dccfb0f6..8c4d91f3 100644 --- a/src/backend/server/default_credentials.test.ts +++ b/src/backend/server/default_credentials.test.ts @@ -1,20 +1,70 @@ import assert from "node:assert/strict" -import { test } from "node:test" +import { test as nodeTest } from "node:test" import { Hono } from "hono" -import { getDb, saveDb } from "../internal/model/db" import { - getOrInitUsers, - verifyUserPassword, - hashPasswordSHA256, -} from "./auth" + getDb, + saveDb, + __resetDbCacheForTest, + __setStoreBackendLoaderForTest, +} from "../internal/model/db" +import { getOrInitUsers, verifyUserPassword, hashPasswordSHA256 } from "./auth" import { isHex64 } from "../pkg/password" import { userRouter } from "./user" const env: any = {} const ADMIN_TOKEN = "ADMIN_STATIC_TOKEN" -const seed = (users: any[], settings: any[] = []) => - saveDb({ settings, users, storages: [], shares: [] }, env, { force: true }) +// 每个用例独立运行:重置模块级 db 缓存 + 注入一次性内存 store 后端, +// 结束时恢复 process.env.ADMIN_PASS。 +// 注意:ADMIN_PASS 是"进程级全局状态",因此本文件用例必须保持串行 +// (concurrency: false,见下方 test 包装)。若未来需要并发,请改为 +// 显式依赖注入而非共享 process.env。 +const run = async (fn: () => Promise) => { + const originalAdminPass = process.env.ADMIN_PASS + try { + __resetDbCacheForTest() + delete process.env.ADMIN_PASS + let stored: any = null + __setStoreBackendLoaderForTest(async () => ({ + name: "test", + isConfigured: async () => true, + load: async () => (stored ? JSON.parse(JSON.stringify(stored)) : null), + save: async (next: any) => { + stored = JSON.parse(JSON.stringify(next)) + return true + }, + })) + await fn() + } finally { + __resetDbCacheForTest() + if (originalAdminPass === undefined) { + delete process.env.ADMIN_PASS + } else { + process.env.ADMIN_PASS = originalAdminPass + } + } +} + +const test = (name: string, fn: () => Promise) => + nodeTest(name, { concurrency: false }, () => run(fn)) + +// 显式断言 saveDb 成功,避免"写库失败被掩盖成测试绿灯"。 +// force 默认 true:测试数据本就不受生产写入守卫约束(例如 +// "持久化空密码管理员"场景),main 分支在此文件统一采用 force。 +const seed = async ( + users: any[], + settings: any[] = [], + options?: { force?: boolean }, +) => { + assert.equal( + await saveDb( + { settings, users, storages: [], shares: [] }, + env, + { force: true, ...(options || {}) }, + ), + true, + ) +} const adminUser = (password: string) => ({ id: 1, @@ -32,8 +82,6 @@ const currentAdmin = async (context = env) => { } test("Initialization: a fresh deployment stays uninitialized without ADMIN_PASS", async () => { - // 隔离 CI/宿主机环境变量,避免影响“未初始化”断言 - delete process.env.ADMIN_PASS await seed([]) await getOrInitUsers(env) const admin = await currentAdmin() @@ -45,7 +93,6 @@ test("Initialization: a fresh deployment stays uninitialized without ADMIN_PASS" }) test("Security(F-11): a legacy-format hash is left untouched (no silent reset on upgrade)", async () => { - delete process.env.ADMIN_PASS // The old code reset any non-64-hex password back to admin/admin — meaning // a routine upgrade silently reopened the admin account. It must stay. const legacyHash = "pbkdf2:100000:somesalt:deadbeef" @@ -65,7 +112,6 @@ test("Security(F-11): a legacy-format hash is left untouched (no silent reset on }) test("Initialization: an empty admin password stays empty (uninitialized), not a random one", async () => { - delete process.env.ADMIN_PASS await seed([adminUser("")]) await getOrInitUsers(env) const admin = await currentAdmin() From 82bd05d7ccd3019c38a07d8e7d67a6cf0d791f35 Mon Sep 17 00:00:00 2001 From: nike64542-byte Date: Wed, 21 Oct 2026 12:05:03 +0800 Subject: [PATCH 28/49] =?UTF-8?q?feat(admin):=20surface=20QuarkUcTv=20(?= =?UTF-8?q?=E5=A4=B8=E5=85=8BTV)=20driver=20in=20management=20UI=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - admin.ts: /admin/driver/names 与 driverConfigs 增加 QuarkUcTv (variant/refresh_token/device_id/order_by/order_direction/link_method) - storage.ts: createDriver 分支补充 quarkuctv 归一化名,路由 QuarkUcTv -> drivers/quark_uc_tv - quark_uc_tv/util.ts: variant 大小写兼容(UI 传 "Quark"/"UC") Closes #115 --- src/backend/drivers/quark_uc_tv/util.ts | 6 ++- src/backend/internal/op/storage.ts | 1 + src/backend/server/admin.ts | 63 +++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/backend/drivers/quark_uc_tv/util.ts b/src/backend/drivers/quark_uc_tv/util.ts index 0717fa3a..3b8a94f1 100644 --- a/src/backend/drivers/quark_uc_tv/util.ts +++ b/src/backend/drivers/quark_uc_tv/util.ts @@ -58,7 +58,11 @@ export class ClientQuarkUcTv { constructor(addition: DriverQuarkUcTvAddition) { this.addition = addition - this.conf = CONFS[addition.variant || "quark"] + // UI 下拉传入的是 "Quark"/"UC"(首字母大写),协议端固定小写键 + const variantKey = (addition.variant || "quark").toLowerCase() + this.conf = + (variantKey === "uc" || variantKey === "quark" ? CONFS[variantKey] : null) || + CONFS.quark this.deviceId = addition.device_id || md5(String(Date.now())) this.addition.device_id = this.deviceId } diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 977c0e82..88494562 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -392,6 +392,7 @@ async function createDriver( await driver.init?.() } else if ( normDriver === "quarktv" || + normDriver === "quarkuctv" || normDriver === "uctv" || normDriver === "quark_uc_tv" ) { diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 829788be..5a4b236e 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -523,6 +523,7 @@ adminRouter.get("/driver/names", (c) => { "Onedrive", "OnedriveAPP", "Quark", + "QuarkUcTv", "123Pan", "BaiduNetdisk", "115Open", @@ -1129,6 +1130,68 @@ const driverConfigs: Record = { default_root: "0", }, }, + QuarkUcTv: { + name: "QuarkUcTv", + default_mount_path: "/quarktv", + common: COMMON_FIELDS, + additional: [ + { + name: "variant", + type: "select", + options: "Quark,UC", + default: "Quark", + required: true, + help: "夸克 TV 端选 Quark,UC TV 端选 UC(服务端固定协议参数)", + }, + { + name: "refresh_token", + type: "text", + default: "", + required: true, + help: "通过 extscreen 授权流程获取(http://api.extscreen.com/quarkdrive,UC 用 /ucdrive),必填", + }, + { + name: "device_id", + type: "string", + default: "", + required: false, + help: "自定义设备标识,留空自动生成", + }, + { + name: "order_by", + type: "select", + options: "file_name,updated_at", + default: "file_name", + required: false, + }, + { + name: "order_direction", + type: "select", + options: "asc,desc", + default: "asc", + required: false, + }, + { + name: "link_method", + type: "select", + options: "download,streaming", + default: "download", + required: false, + help: "播放/下载链接的获取方式;视频可选 streaming 转码档", + }, + ], + config: { + name: "QuarkUcTv", + local_sort: true, + only_local: false, + only_proxy: false, + no_cache: false, + // 只读驱动:仅实现 list/get,不支持上传 + no_upload: true, + need_ms: false, + default_root: "0", + }, + }, "123Pan": { name: "123Pan", default_mount_path: "/123", From 485f4b035609876301bca5f982b0e913ab4eb955 Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:17:55 +0800 Subject: [PATCH 29/49] =?UTF-8?q?fix(115):=20=E4=BF=AE=E5=A4=8D=20115=20?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=EF=BC=88=E6=97=A7=E9=A9=B1=E5=8A=A8=E5=A4=B1?= =?UTF-8?q?=E6=95=88=20+=20=E5=AD=90=E7=9B=AE=E5=BD=95=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=EF=BC=89,=20fixes=20#128?= =?UTF-8?q?=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(115): 移除失效的旧 115 驱动,使 115/115Open 统一走 Pan115Driver ## 问题 后台添加 115Open 驱动后,存储状态恒为 [115] API error (),浏览任何目录都失败。 ## 根因 仓库里存在两套 115 驱动实现: - src/backend/drivers/115/(类 Client115)—— 已损坏 - src/backend/drivers/115open/(类 Pan115Driver)—— 正确,自 Go 版移植 storage.ts 的驱动工厂按 normDriver(toLowerCase().replace(/[^a-z0-9]/g,"")) 做 if-else 链式匹配。DB 里的 115Open 归一化为 115open,被靠前的旧驱动分支 截走;下方正确的 Pan115Driver 分支因列出同样的 115open 而永远走不到, 成为死代码。 旧驱动 Client115 有三处错误,叠加导致报错信息为空: 1. URL 路径重复:util.ts:10 的 API_BASE 已含 /open,util.ts:100 又请求 "/open/ufile/files",实际请求 /open/open/ufile/files。实测返回 {"state":false,"message":"Method POST does not exist","code":0}, 且响应里没有 error / errno / errcode 字段。 2. HTTP 方法错误:util.ts:38-70 的 request() 用 POST + JSON body, 而 115 开放平台要求 GET + query string。 3. 错误码字段名错误:util.ts:65-67 的错误模板取 data.error || data.errmsg || data.errno || data.errcode, 上述字段一个都不存在,于是输出 [115] API error () —— 括号里空空如也, 与用户看到的 status 一字不差。 ## 改动 1. 删除 src/backend/drivers/115/(driver.ts / types.ts / util.ts,共 489 行)。 grep 确认无其他引用。 2. storage.ts:删掉 Driver115 的 import 与整个旧分支,留注释说明 115 / 115Open 统一走 Pan115Driver。normDriver.startsWith("115") 分支已能覆盖 115 / 115Open / 115cloud / 115netdisk 等所有变体。 ## 验证 - npm run lint(tsc --noEmit)通过。 - 测试全过:drivers 189、server 206、model 39、regress 71、cache 24、 pkg 50、189 20、store 13,合计 626 pass / 0 fail。 - 用真实 115 token 对 Pan115Driver 做全链路验证:init、list、get、 mkdir、rename、delFile、refreshToken 均成功。 - 线上部署后存储 status 由 [115] API error () 变为 work,/115 可正常浏览。 原配置里的 access_token / refresh_token 已过期(40140126 / 40140120), 换新 token 后 status 恢复 work。该问题与本次代码修复相互独立。 * fix(115): 修复子目录解析失败(路径双斜杠 + 错误对象缺 code) ## 问题 115 存储挂载后根目录可浏览,但进入任意子目录(或访问子目录下的文件、 新建/重命名/移动)时报: 115 网盘 API 错误(code 20001 目录名称不能为空。) ## 根因 两个缺陷叠加,导致一次性路径解析失败且兜底逻辑被跳过: 1. resolveFolderId 拼接的 fullPath 多出一个斜杠(driver.ts:184): const fullPath = `/${rootId === "0" ? "" : rootId}${clean === "/" ? "" : clean}` root_id 默认 "0",于是 `/` + `` + `/测试` = `//测试`。 115 开放平台对 folder/get_info 的 path 参数严格校验: | path | 返回 | | --- | --- | | `/测试` | state:true,返回 file_id | | `//测试` | code 20001 目录名称不能为空。 | | `测试`(缺前导斜杠) | code 990002 参数错误。 | 2. request() 的业务错误是裸 Error,不带 code(util.ts:231), 只有 430004 分支才设置 err.code。resolveFolderId 的回退判断 `if (e?.code !== ERR_OBJECT_NOT_FOUND && e?.code !== 990002) throw e` 因 e.code === undefined 而直接 rethrow,逐层解析兜底永远走不到 —— 而逐层解析(用 ufile/files 列父目录)本身是通的。 根目录恰好能浏览,是因为 clean === "/" 时提前 return 了。 ## 改动 1. driver.ts:root_id 非 "0" 时才拼前缀,避免产生 "//xxx": const rootPrefix = rootId === "0" ? "" : `/${rootId}` const fullPath = `${rootPrefix}${clean}` 2. driver.ts:回退条件增加 20001(空目录名),覆盖更多 path 相关失败。 3. util.ts:所有业务错误(含 token 刷新后重试仍失败)统一把 code 挂到 Error 对象上,调用方才能按 code 决定是否回退。 ## 验证 - 本地用真实 115 token 跑通 list("/")、list("/测试")、get(apk) (5874314 字节,成功取得下载链接)。 - npm run lint 通过。 - 测试全过:drivers 189、server 206、regress 71、189 20、 model 39、cache 24、pkg 50、store 13,合计 612 pass / 0 fail。 - 线上部署后: - POST /api/fs/list {"path":"/115/测试"} → code 200,返回 apk - POST /api/fs/get {"path":"/115/测试/...apk"} → code 200,raw_url 正常 --------- Co-authored-by: nvdadmin --- src/backend/drivers/115/driver.ts | 212 ------------------------- src/backend/drivers/115/types.ts | 59 ------- src/backend/drivers/115/util.ts | 218 -------------------------- src/backend/drivers/115open/driver.ts | 17 +- src/backend/drivers/115open/util.ts | 10 +- src/backend/internal/op/storage.ts | 13 +- 6 files changed, 26 insertions(+), 503 deletions(-) delete mode 100644 src/backend/drivers/115/driver.ts delete mode 100644 src/backend/drivers/115/types.ts delete mode 100644 src/backend/drivers/115/util.ts diff --git a/src/backend/drivers/115/driver.ts b/src/backend/drivers/115/driver.ts deleted file mode 100644 index 270796ae..00000000 --- a/src/backend/drivers/115/driver.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { - StorageDriver, - FileItem, - calcFileType, -} from "../../internal/driver/base" -import { Driver115Addition, Cloud115File } from "./types" -import { Client115 } from "./util" -import { sha1 } from "../../pkg/crypto" - -function fileToItem(f: Cloud115File): FileItem { - const isDir = f.Fc === "0" - const modTime = f.Upt ? new Date(f.Upt * 1000).toISOString() : new Date().toISOString() - return { - name: f.Fn, - size: f.FS || 0, - is_dir: isDir, - modified: modTime, - sign: "", - type: calcFileType(f.Fn, isDir), - thumb: f.Thumbnail || "", - raw_url: "", - } -} - -export class Driver115 implements StorageDriver { - private client: Client115 - private pathCache = new Map() - - constructor(addition: Driver115Addition) { - this.client = new Client115(addition) - } - - async init(): Promise { - await this.client.init() - } - - async list(_virtualPath: string, physicalPath: string): Promise { - const cid = await this.client.resolvePathId(physicalPath, this.pathCache) - const files = await this.client.getFiles(cid) - return files.map(fileToItem) - } - - async get(_virtualPath: string, physicalPath: string): Promise { - const parts = physicalPath.split("/").filter(Boolean) - const rawName = parts[parts.length - 1] || "root" - const decodedName = (() => { - try { - return decodeURIComponent(rawName) - } catch { - return rawName - } - })() - const parentPath = "/" + parts.slice(0, -1).join("/") - const parentId = await this.client.resolvePathId(parentPath, this.pathCache) - const files = await this.client.getFiles(parentId) - const file = files.find( - (f) => f.Fn === rawName || f.Fn === decodedName || f.Fid === rawName, - ) - - if (file) { - const item = fileToItem(file) - if (!item.is_dir && file.Pc) { - try { - const url = await this.client.getDownloadUrl(file.Pc) - item.raw_url = url - } catch (e: any) { - item.raw_url_error = e.message - } - } - return item - } - - // Fallback: path 可能是文件夹(根目录等未在父目录列出) - try { - const fid = await this.client.resolvePathId(physicalPath, this.pathCache) - await this.client.getFiles(fid) - return { - name: decodedName || "root", - size: 0, - is_dir: true, - modified: new Date().toISOString(), - sign: "", - type: 1, - raw_url: "", - } - } catch { - return { - name: decodedName || "root", - size: 0, - is_dir: false, - modified: new Date().toISOString(), - sign: "", - type: 0, - raw_url: "", - } - } - } - - async mkdir(_virtualPath: string, physicalPath: string): Promise { - const parts = physicalPath.split("/").filter(Boolean) - const name = parts.pop() || "new_folder" - const parentPath = "/" + parts.join("/") - const parentId = await this.client.resolvePathId(parentPath, this.pathCache) - await this.client.mkdir(parentId, name) - } - - async rename( - _virtualPath: string, - physicalPath: string, - newName: string, - ): Promise { - const fid = await this.client.resolvePathId(physicalPath, this.pathCache) - await this.client.rename(fid, newName) - } - - async remove( - _virtualPath: string, - physicalPath: string, - _names: string[], - ): Promise { - const fid = await this.client.resolvePathId(physicalPath, this.pathCache) - await this.client.remove([fid]) - } - - async move( - _srcDir: string, - dstDir: string, - _names: string[], - srcPhysical: string, - _dstPhysical: string, - ): Promise { - const fid = await this.client.resolvePathId(srcPhysical, this.pathCache) - const dstId = await this.client.resolvePathId(dstDir, this.pathCache) - await this.client.move(dstId, [fid]) - } - - async copy( - _srcDir: string, - dstDir: string, - _names: string[], - srcPhysical: string, - _dstPhysical: string, - ): Promise { - const fid = await this.client.resolvePathId(srcPhysical, this.pathCache) - const dstId = await this.client.resolvePathId(dstDir, this.pathCache) - await this.client.copy(dstId, [fid]) - } - - readonly rapidHashAlgos = ["sha1"] - readonly rapidHashNeedsPieces = false - - async rapidUpload( - _virtualPath: string, - physicalPath: string, - metadata: { - size: number - sha1?: string - readRange?: (start: number, length: number) => Promise - }, - ): Promise { - const sha1Full = String(metadata.sha1 || "").toUpperCase() - if (!/^[0-9A-F]{40}$/.test(sha1Full) || !metadata.readRange) return false - const parts = physicalPath.split("/").filter(Boolean) - const name = parts.pop() || "file" - const parentPath = "/" + parts.join("/") - const parentId = await this.client.resolvePathId( - parentPath, - this.pathCache, - ) - const preSize = Math.min(128 * 1024, metadata.size) - const preId = ( - await sha1(await metadata.readRange(0, preSize)) - ).toUpperCase() - let initResp = await this.client.uploadInit({ - fileName: name, - fileSize: metadata.size, - target: parentId, - fileId: sha1Full, - preId, - }) - if (initResp.status === 2) return true - if ([6, 7, 8].includes(Number(initResp.status)) && initResp.sign_check) { - const [start, end] = initResp.sign_check - .split("-") - .map((value) => parseInt(value, 10)) - if (Number.isFinite(start) && Number.isFinite(end)) { - const signVal = ( - await sha1(await metadata.readRange(start, end - start + 1)) - ).toUpperCase() - initResp = await this.client.uploadInit({ - fileName: name, - fileSize: metadata.size, - target: parentId, - fileId: sha1Full, - preId, - signKey: initResp.sign_key, - signVal, - }) - return initResp.status === 2 - } - } - return false - } - - async put( - _virtualPath: string, - _physicalPath: string, - _content: Buffer, - ): Promise { - throw new Error("[115] Direct put not supported in stateless environment") - } -} diff --git a/src/backend/drivers/115/types.ts b/src/backend/drivers/115/types.ts deleted file mode 100644 index 30d27f1c..00000000 --- a/src/backend/drivers/115/types.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 115 (115网盘 / 115 Open API) - Bearer Token 认证 -// API reference: last branch src/drive/cloud115 (Open API) - -export interface Driver115Addition { - /** Open API 访问令牌(必填) */ - access_token: string - /** Open API 刷新令牌(必填) */ - refresh_token: string - /** 根目录文件夹 ID,默认 "0" */ - root_folder_id?: string - /** 排序字段 */ - order_by?: "file_name" | "file_size" | "user_utime" | "file_type" - /** 排序方向 */ - order_direction?: "asc" | "desc" -} - -// --- 115 Open API 响应类型 --- - -/** Open API 文件对象(大写字段) */ -export interface Cloud115File { - Fid: string - Fn: string - /** "0" = 文件夹 */ - Fc: string - FS: number - Sha1?: string - /** PickCode(下载用) */ - Pc?: string - Thumbnail?: string - Upt?: number - Pid?: string -} - -export interface Cloud115ListResp { - state: boolean - errcode?: number - errno?: number - error?: string - count?: number - offset?: number - page_size?: number - data?: Cloud115File[] - // 旧格式(小写字段)兼容 - files?: any[] -} - -export interface Cloud115UserInfoResp { - state: boolean - data?: { - user_id: string | number - user_name: string - } -} - -export interface Cloud115DownResp { - state: boolean - url?: string - data?: { url?: string } -} diff --git a/src/backend/drivers/115/util.ts b/src/backend/drivers/115/util.ts deleted file mode 100644 index 5c19eda0..00000000 --- a/src/backend/drivers/115/util.ts +++ /dev/null @@ -1,218 +0,0 @@ -// 115 Open API 客户端 -import { - Driver115Addition, - Cloud115File, - Cloud115ListResp, - Cloud115UserInfoResp, - Cloud115DownResp, -} from "./types" - -const API_BASE = "https://proapi.115.com/open" -const PASSPORT_BASE = "https://passportapi.115.com" -const UA = "Mozilla/5.0 115disk/42.0.0.2" - -export class Client115 { - private accessToken: string - private refreshToken: string - private addition: Driver115Addition - private rootId: string - - constructor(addition: Driver115Addition) { - this.addition = addition - this.accessToken = addition.access_token || "" - this.refreshToken = addition.refresh_token || "" - this.rootId = addition.root_folder_id || "0" - } - - getRootFolderId(): string { - return this.rootId - } - - async init(): Promise { - if (!this.accessToken) { - throw new Error("[115] access_token is required") - } - await this.getUserInfo() - } - - private async request( - path: string, - body: Record = {}, - base: string = API_BASE, - ): Promise { - const url = `${base}${path}` - const resp = await fetch(url, { - method: "POST", - headers: { - "User-Agent": UA, - Accept: "application/json", - "Content-Type": "application/json", - Authorization: `Bearer ${this.accessToken}`, - }, - body: JSON.stringify(body), - }) - const data: any = await resp.json().catch(() => ({})) - if (data?.state === false || data?.errno) { - // token 过期尝试刷新 - if ( - data.errno === 990001 || - data.errcode === 990001 || - data.errno === 10008 - ) { - await this.refreshAccessToken() - return this.request(path, body, base) - } - throw new Error( - `[115] ${data.error || data.errmsg || "API error"} (${data.errno || data.errcode || ""})`, - ) - } - return data as T - } - - async getUserInfo(): Promise { - return this.request("/user/info", {}, PASSPORT_BASE) - } - - async refreshAccessToken(): Promise { - const url = `${API_BASE}/oauth2/token` - const resp = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json", "User-Agent": UA }, - body: JSON.stringify({ - grant_type: "refresh_token", - refresh_token: this.refreshToken, - }), - }) - const data: any = await resp.json().catch(() => ({})) - if (data?.access_token) { - this.accessToken = data.access_token - this.addition.access_token = data.access_token - if (data.refresh_token) { - this.refreshToken = data.refresh_token - this.addition.refresh_token = data.refresh_token - } - } else { - throw new Error(`[115] refresh token failed: ${JSON.stringify(data)}`) - } - } - - async getFiles(cid: string): Promise { - const resp = await this.request("/open/ufile/files", { - cid, - limit: 1000, - offset: 0, - order: this.addition.order_by || "user_utime", - asc: this.addition.order_direction === "asc" ? 1 : 0, - show_dir: 1, - fc_mix: 1, - }) - if (resp?.data && Array.isArray(resp.data)) return resp.data - // 旧格式(小写字段)兼容 - if (resp?.files && Array.isArray(resp.files)) { - return resp.files.map((f: any) => ({ - Fid: f.fid ?? f.id, - Fn: f.n ?? f.file_name ?? f.name, - Fc: String( - f.fc ?? f.category ?? (f.pid !== undefined && !f.pid ? "0" : "1"), - ), - FS: Number(f.s ?? f.size ?? 0), - Sha1: f.sha1, - Pc: f.pc ?? f.pick_code, - Thumbnail: f.thumb ?? f.thumbnail, - Upt: f.tu ?? f.upt ?? f.updated_at, - Pid: f.pid ?? f.cid, - })) - } - return [] - } - - /** 通过路径逐级解析文件夹 ID(带缓存由 driver 层管理) */ - async resolvePathId( - path: string, - cache: Map, - ): Promise { - const clean = path.split("/").filter(Boolean).join("/") - if (!clean) return this.rootId - if (cache.has(clean)) return cache.get(clean)! - - const parts = clean.split("/") - let currentId = this.rootId - for (let i = 0; i < parts.length; i++) { - const rawPart = parts[i] - const decoded = (() => { - try { - return decodeURIComponent(rawPart) - } catch { - return rawPart - } - })() - const files = await this.getFiles(currentId) - const target = files.find( - (f) => f.Fn === rawPart || f.Fn === decoded || f.Fid === rawPart, - ) - if (!target) { - throw new Error(`[115] Path '${rawPart}' not found in '${currentId}'`) - } - currentId = target.Fid - const subPath = "/" + parts.slice(0, i + 1).join("/") - cache.set(subPath, currentId) - } - return currentId - } - - async mkdir(pid: string, name: string): Promise { - await this.request("/open/ufile/add", { pid, cname: name }) - } - - async rename(fid: string, name: string): Promise { - await this.request("/open/ufile/edit", { fid, fname: name }) - } - - async move(pid: string, fids: string[]): Promise { - await this.request("/open/ufile/move", { pid, fid: fids, no_dupli: "1" }) - } - - async copy(pid: string, fids: string[]): Promise { - await this.request("/open/ufile/copy", { pid, fid: fids, no_dupli: "1" }) - } - - async remove(fids: string[]): Promise { - await this.request("/open/ufile/delete", { fid: fids, ignore_warn: 1 }) - } - - /** Upload-init probe used by hash-based rapid upload. */ - async uploadInit(opts: { - fileName: string - fileSize: number - target: string - fileId: string - preId: string - signKey?: string - signVal?: string - }): Promise<{ - status?: number - sign_key?: string - sign_check?: string - file_id?: string - }> { - const resp = await this.request("/upload/init", { - file_name: opts.fileName, - file_size: String(opts.fileSize), - target: `U_1_${opts.target}`, - fileid: opts.fileId, - preid: opts.preId, - sign_key: opts.signKey || "", - sign_val: opts.signVal || "", - }) - return resp?.data || resp || {} - } - - async getDownloadUrl(pickCode: string): Promise { - const resp = await this.request("/open/ufile/downurl", { - pickcode: pickCode, - }) - const url = resp?.url || resp?.data?.url || "" - if (!url) throw new Error("[115] empty download url") - return url - } -} diff --git a/src/backend/drivers/115open/driver.ts b/src/backend/drivers/115open/driver.ts index e99706a6..9d5ba2d4 100644 --- a/src/backend/drivers/115open/driver.ts +++ b/src/backend/drivers/115open/driver.ts @@ -181,7 +181,12 @@ export class Pan115Driver implements StorageDriver { if (cached) return cached // 用 GetFolderInfoByPath 一次性解析(Go Get 逻辑) - const fullPath = `/${rootId === "0" ? "" : rootId}${clean === "/" ? "" : clean}` + // 注意:115 的 folder/get_info 对 path 严格校验,必须是 "/xxx/yyy" 形式: + // "/测试" → 正常;"//测试" → code 20001 目录名称不能为空; + // "测试"(缺前导斜杠)→ code 990002 参数错误。 + // 因此 root_id 为默认 "0" 时不能拼出前缀,否则会得到 "//xxx"。 + const rootPrefix = rootId === "0" ? "" : `/${rootId}` + const fullPath = `${rootPrefix}${clean}` try { if (!this.reserve()) throw new Error("subrequest budget exceeded") const info = await this.client.getFolderInfoByPath(fullPath) @@ -190,8 +195,14 @@ export class Pan115Driver implements StorageDriver { return info.file_id } } catch (e: any) { - // folder/get_info 只支持目录路径:参数错误(990002)/不存在(430004) → 回退逐层 - if (e?.code !== ERR_OBJECT_NOT_FOUND && e?.code !== 990002) throw e + // folder/get_info 只支持目录路径:参数错误(990002)/空目录名(20001)/ + // 不存在(430004) → 回退逐层解析(逐层用 ufile/files 列目录,不依赖 path 拼接) + if ( + e?.code !== ERR_OBJECT_NOT_FOUND && + e?.code !== 990002 && + e?.code !== 20001 + ) + throw e } // 逐层解析 const segs = clean.split("/").filter(Boolean) diff --git a/src/backend/drivers/115open/util.ts b/src/backend/drivers/115open/util.ts index 891deb12..c5353b21 100644 --- a/src/backend/drivers/115open/util.ts +++ b/src/backend/drivers/115open/util.ts @@ -218,9 +218,11 @@ export class Pan115Client { if (retryState !== false && retryState !== undefined) { return body } - throw new Error( + const retryErr: any = new Error( `115 网盘 API 错误(code ${body?.code} ${body?.message})`, ) + retryErr.code = Number(body?.code ?? 0) + throw retryErr } // 对象不存在错误(430004)——SDK ErrObjectNotFound if (code === ERR_OBJECT_NOT_FOUND) { @@ -228,9 +230,13 @@ export class Pan115Client { err.code = ERR_OBJECT_NOT_FOUND throw err } - throw new Error( + // 其余业务错误:把 code 挂到 Error 上,调用方才能判断是否需要回退 + // (如 driver 的 resolveFolderId 需要按 990002/20001/430004 决定是否逐层解析) + const err: any = new Error( `115 网盘 API 错误(code ${code} ${body?.message || ""})`, ) + err.code = code + throw err } return body } diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 88494562..ef4d6f25 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -16,7 +16,6 @@ import { OnedriveAPP } from "../../drivers/onedrive_app/driver" import { AliyundriveOpen } from "../../drivers/aliyundrive_open/driver" import { GoogleDrive } from "../../drivers/google_drive/driver" import { QuarkDriver } from "../../drivers/quark/driver" -import { Driver115 } from "../../drivers/115/driver" import { DriverCloudreve } from "../../drivers/cloudreve_v4/driver" import { DriverCloudreveV3 } from "../../drivers/cloudreve_v3/driver" import { DriverOpenlist } from "../../drivers/openlist/driver" @@ -295,14 +294,10 @@ async function createDriver( ) { driver = new QuarkDriver(parseAddition(storageConfig)) await driver.init?.() - } else if ( - normDriver === "115" || - normDriver === "115cloud" || - normDriver === "115open" || - normDriver === "115netdisk" - ) { - driver = new Driver115(parseAddition(storageConfig)) - await driver.init?.() + // 注意:115 / 115Open 统一走 Pan115Driver(115open 目录)。 + // 旧 drivers/115(Client115)端点写成 /open + /open/ufile/files → /open/open/ufile/files, + // 且用 POST+JSON,115 开放平台实际要求 GET+query,会报 "Method POST does not exist", + // 再经错误模板输出为 "[115] API error ()"。该分支已在下方 normDriver.startsWith("115") 处理。 } else if ( normDriver === "cloudreve" || normDriver === "cloudrevev3" || From d4e75bad42086c10ce1f363834812ef617b7462c Mon Sep 17 00:00:00 2001 From: lava-lake <64721503+lava-lake@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:09:08 +0800 Subject: [PATCH 30/49] =?UTF-8?q?fix(pikpak):=20usage=3DCACHE=20=E6=97=A0?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=93=BE=E6=8E=A5=E6=97=B6=E5=9B=9E=E9=80=80?= =?UTF-8?q?=20usage=3DFETCH=20=E9=87=8D=E6=96=B0=E8=8E=B7=E5=8F=96=20(#154?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PikPak 服务端在 usage=CACHE 下对非媒体文件(txt/nfo/md5 等)不返回 web_content_link,也没有 medias,导致 /api/d 返回 404「该存储驱动未返回下载链接」。 此时回退 usage=FETCH 再请求一次文件详情;媒体文件 CACHE 模式有 medias 时保持单次请求。 新增两条单测:回退路径(两次请求,第二次 usage=FETCH)与负对照(有 medias 只请求一次)。 Co-authored-by: Claude --- src/backend/drivers/pikpak/driver.test.ts | 53 +++++++++++++++++++++++ src/backend/drivers/pikpak/driver.ts | 16 ++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/backend/drivers/pikpak/driver.test.ts b/src/backend/drivers/pikpak/driver.test.ts index 377a6cbf..dd1dd16b 100644 --- a/src/backend/drivers/pikpak/driver.test.ts +++ b/src/backend/drivers/pikpak/driver.test.ts @@ -43,3 +43,56 @@ test("PikPak driver instantiation", () => { }) assert.ok(driver) }) + +function createDriverWithRequests(responses: unknown[]) { + const driver = new PikPakDriver({ + username: "user@example.com", + password: "password123", + platform: "web", + }) + const calls: { url: string; params?: Record }[] = [] + ;(driver as any).resolveParentId = async () => "file-id-1" + ;(driver as any).client = { + request: async (url: string, options: any) => { + calls.push({ url, params: options?.params }) + return responses[calls.length - 1] + }, + } + return { driver, calls } +} + +test("PikPak get falls back to usage=FETCH when CACHE returns no link", async () => { + const { driver, calls } = createDriverWithRequests([ + { id: "file-id-1", kind: "drive#file", name: "readme.txt", size: "12" }, + { + id: "file-id-1", + kind: "drive#file", + name: "readme.txt", + size: "12", + web_content_link: "https://dl.example.com/readme.txt", + }, + ]) + + const item = await driver.get("/readme.txt", "/readme.txt") + assert.equal(item.raw_url, "https://dl.example.com/readme.txt") + assert.equal(calls.length, 2) + assert.equal(calls[0].params?.usage, "CACHE") + assert.equal(calls[1].params?.usage, "FETCH") +}) + +test("PikPak get keeps a single CACHE request when medias is present", async () => { + const { driver, calls } = createDriverWithRequests([ + { + id: "file-id-1", + kind: "drive#file", + name: "movie.mp4", + size: "1024", + medias: [{ link: { url: "https://media.example.com/movie.mp4" } }], + }, + ]) + + const item = await driver.get("/movie.mp4", "/movie.mp4") + assert.equal(item.raw_url, "https://media.example.com/movie.mp4") + assert.equal(calls.length, 1) + assert.equal(calls[0].params?.usage, "CACHE") +}) diff --git a/src/backend/drivers/pikpak/driver.ts b/src/backend/drivers/pikpak/driver.ts index c99c1c22..9b16453f 100644 --- a/src/backend/drivers/pikpak/driver.ts +++ b/src/backend/drivers/pikpak/driver.ts @@ -177,7 +177,7 @@ export class PikPakDriver implements StorageDriver { thumbnail_size: "SIZE_LARGE", } - const f: PikPakFile = await this.client.request( + let f: PikPakFile = await this.client.request( `https://api-drive.mypikpak.net/drive/v1/files/${encodeURIComponent(fileId)}`, { method: "GET", @@ -185,6 +185,20 @@ export class PikPakDriver implements StorageDriver { }, ) + // PikPak 服务端在 usage=CACHE 下对非媒体文件(txt/nfo/md5 等)不返回 web_content_link, + // 也没有 medias,导致下载接口拿不到直链;此时回退 usage=FETCH 重新获取一次 + if ( + queryParams.usage === "CACHE" && + f.kind !== "drive#folder" && + !f.web_content_link && + !(f.medias && f.medias.length > 0 && f.medias[0].link?.url) + ) { + f = await this.client.request( + `https://api-drive.mypikpak.net/drive/v1/files/${encodeURIComponent(fileId)}`, + { method: "GET", params: { ...queryParams, usage: "FETCH" } }, + ) + } + const isDir = f.kind === "drive#folder" let rawUrl = f.web_content_link || "" if ( From 2accf7d28f3b9be7ae762947c1c68a73bf8898df Mon Sep 17 00:00:00 2001 From: lava-lake <64721503+lava-lake@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:30:52 +0800 Subject: [PATCH 31/49] =?UTF-8?q?fix(auth):=202FA=20=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=94=B9=E7=94=A8=20SVG=20=E7=94=9F=E6=88=90=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Workers=20=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E6=97=A0=20canvas=20=E5=AF=BC=E8=87=B4=20generate=20500=20(#15?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloudflare Workers / Edge 运行时没有 canvas,QRCode.toDataURL() 会抛 "no canvas element",/api/auth/2fa/generate 直接 500。改为 QRCode.toString(url, { type: "svg" }) 生成 SVG 并编码为 Data URL,前端 可直接显示。 同步更新 auth_2fa.test.ts 对 Data URL MIME 的断言。 Fixes #88 Co-authored-by: Claude --- src/backend/pkg/totp.ts | 5 ++++- src/backend/server/auth_2fa.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/backend/pkg/totp.ts b/src/backend/pkg/totp.ts index 7cad2f4d..09be4c89 100644 --- a/src/backend/pkg/totp.ts +++ b/src/backend/pkg/totp.ts @@ -170,5 +170,8 @@ export function buildOtpauthUrl( * @returns Data URL 格式的 QR 码 */ export async function buildQrImageUrl(otpauthUrl: string): Promise { - return QRCode.toDataURL(otpauthUrl) + // Workers / Edge 运行时没有 canvas,QRCode.toDataURL() 会抛 "no canvas element"; + // 改用 SVG 字符串生成 Data URL,纯 JS 实现,不依赖运行时能力 + const svg = await QRCode.toString(otpauthUrl, { type: "svg", margin: 1 }) + return "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svg))) } diff --git a/src/backend/server/auth_2fa.test.ts b/src/backend/server/auth_2fa.test.ts index 54ada784..cd52389f 100644 --- a/src/backend/server/auth_2fa.test.ts +++ b/src/backend/server/auth_2fa.test.ts @@ -54,6 +54,6 @@ test("2fa/generate returns a resolved QR data URL", async () => { assert.equal(res.status, 200) const json: any = await res.json() assert.equal(json.code, 200) - assert.match(json.data.qr, /^data:image\/png;base64,/) + assert.match(json.data.qr, /^data:image\/svg\+xml;base64,/) assert.match(json.data.secret, /^[A-Z2-7]+$/) }) From 29b7a8eb687aa5daf6cd2158fa27d83232ebae6f Mon Sep 17 00:00:00 2001 From: tanmoumou252 <118299342+tanmoumou252@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:23:21 +0800 Subject: [PATCH 32/49] fix(webdav): decode XML entities in upstream PROPFIND response (#140) (#141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 fix(webdav): 解析上游 PROPFIND 响应时反转义 XML 实体 - 【修复】新增 decodeXmlEntities,对 displayname 与 href 做 XML 实体与数字引用反转义 - 【修复】href 改为先 XML 解码再 URI 解码,避免百分号编码的字面实体被二次解码 - 【修复】越界与代理区数字引用透传原样,避免畸形上游响应触发 RangeError 导致列表失败 * 🐛 fix(webdav): 收紧 XML 实体解码的字符校验与测试保真度 - 【修复】数字引用按 XML 1.0 Char 集校验,拒绝 NUL、0xFFFF 及控制字符码点 - 【测试】href 用例改用真实 XML 实体并补 path 断言,使其能检出 XML 解码被移除 - 【测试】新增 XML-invalid 码点透传用例,锁定守卫行为 --- src/backend/drivers/webdav/util.ts | 50 +++++++++++- .../drivers/webdav/webdav_xml_entity.test.ts | 77 +++++++++++++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/backend/drivers/webdav/webdav_xml_entity.test.ts diff --git a/src/backend/drivers/webdav/util.ts b/src/backend/drivers/webdav/util.ts index 6420946c..08ddc33f 100644 --- a/src/backend/drivers/webdav/util.ts +++ b/src/backend/drivers/webdav/util.ts @@ -31,6 +31,43 @@ export function pathEscape(p: string): string { .join("/") } +/** + * Decode XML predefined entities and numeric character references. + * Upstream PROPFIND responses escape `&` as `&` etc.; leaving them + * undecoded poisons file names and makes later upstream requests 404 (#140). + */ +export function decodeXmlEntities(s: string): string { + return s.replace( + /&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/g, + (m) => { + if (m === "&") return "&" + if (m === "<") return "<" + if (m === ">") return ">" + if (m === """) return '"' + if (m === "'") return "'" + const hex = /^&#x/i.test(m) + const num = hex ? m.slice(3, -1) : m.slice(2, -1) + const code = hex ? parseInt(num, 16) : parseInt(num, 10) + // Keep the reference verbatim unless the code point is a valid XML 1.0 + // Char (#x9/#xA/#xD, #x20-#xD7FF, #xE000-#xFFFD, #x10000-#x10FFFF). + // XML-invalid points (NUL, #xFFFF non-characters, surrogates, out of + // range) would otherwise poison the parsed name/path, and out-of-range + // values would crash the parse with a RangeError. + if ( + Number.isNaN(code) || + code > 0x10ffff || + (code >= 0xd800 && code <= 0xdfff) || + code === 0xfffe || + code === 0xffff || + (code < 0x20 && code !== 0x9 && code !== 0xa && code !== 0xd) + ) { + return m + } + return String.fromCodePoint(code) + }, + ) +} + /** * Robust WebDAV XML Multistatus parser. * Handles diverse namespace prefixes (d:, D:, a:, xmlns="DAV:") and case variations. @@ -57,11 +94,16 @@ export function parseMultistatusXml( ) if (!hrefMatch) continue const rawHref = hrefMatch[1].trim() - let decodedHref = rawHref + // XML-unescape first: the upstream XML-escapes the href text. Decoding XML + // entities before percent-decoding avoids double-decoding a percent-encoded + // literal entity (e.g. `%26amp%3B` must stay `&`, not become `&`); + // XML decoding never produces `%`, so the order is safe. + const xmlDecodedHref = decodeXmlEntities(rawHref) + let decodedHref = xmlDecodedHref try { - decodedHref = decodeURIComponent(rawHref) + decodedHref = decodeURIComponent(xmlDecodedHref) } catch { - // ignore URI decode error + // ignore URI decode error; keep the XML-decoded text } // Extract propstat with 200 OK @@ -105,7 +147,7 @@ export function parseMultistatusXml( /<(?:[a-zA-Z0-9_-]+:)?displayname\b[^>]*>([\s\S]*?)<\/(?:[a-zA-Z0-9_-]+:)?displayname>/i.exec( propContent, ) - let displayName = dnMatch ? dnMatch[1].trim() : "" + let displayName = dnMatch ? decodeXmlEntities(dnMatch[1].trim()) : "" // Derive name from href if displayname is missing or is pure path const cleanHref = decodedHref.replace(/\/+$/, "") diff --git a/src/backend/drivers/webdav/webdav_xml_entity.test.ts b/src/backend/drivers/webdav/webdav_xml_entity.test.ts new file mode 100644 index 00000000..ef608f9e --- /dev/null +++ b/src/backend/drivers/webdav/webdav_xml_entity.test.ts @@ -0,0 +1,77 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { parseMultistatusXml } from "./util" + +test("displayname 中的 XML 实体被反转义 (#140)", () => { + const xml = ` +/dav/One Hundred %26 Two.mp3 +One Hundred & Two.mp3 +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + assert.equal(items[0].name, "One Hundred & Two.mp3") +}) + +test("href 含 XML 实体时先 XML 解码再 URI 解码(无 displayname 时回退 hrefBaseName)", () => { + const xml = ` +/dav/It%27s%20a%20&%20test.txt + +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + // href 内的 & 先经 XML 解码为 &,再 decodeURIComponent 得 "It's a & test.txt" + assert.equal(items[0].name, "It's a & test.txt") + assert.equal(items[0].path, "/dav/It's a & test.txt") +}) + +test("href 先 XML 解码再 URI 解码,不双重解码百分号编码的字面实体", () => { + // 上游真实文件名是字面量 "A&B.txt"(XML 里 href 为 %26amp%3B): + // 仅应解一层——XML 解码无动作,URI 解码后仍是 "A&B.txt" + const xml = ` +/dav/A%26amp%3BB.txt + +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + assert.equal(items[0].name, "A&B.txt") + assert.equal(items[0].path, "/dav/A&B.txt") +}) + +test("未知实体与纯文本名不被误改", () => { + const xml = ` +/dav/plain.txt +plain   unknown &weird; 'ok' +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + // 未知实体   / &weird; 保留原样;数字引用 ' / ' 解码为引号 + assert.equal(items[0].name, "plain   unknown &weird; 'ok'") +}) + +test("越界/代理区数字引用透传不崩溃(畸形上游防 DoS)", () => { + const xml = ` +/dav/edge.txt +edge � � 😀 +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + // >0x10FFFF 与代理区透传原样(不抛 RangeError);合法增补平面正常解码 + assert.equal(items[0].name, "edge � � \u{1F600}") +}) + +test("XML 1.0 非法码点透传原文,不进入解析后的 name", () => { + const xml = ` +/dav/invalid.txt +invalid � ￾ ￿  ok +HTTP/1.1 200 OK +` + const { items } = parseMultistatusXml(xml, "/dav") + assert.equal(items.length, 1) + // NUL、#xFFFE/#xFFFF 非字符、其它控制字符透传原文;#x9/#xA/#xD 是 XML 1.0 合法 Char,正常解码为 \t + assert.equal(items[0].name, "invalid � ￾ ￿  \tok\t") +}) From c24105400a66612642c5cef1e7c58235275555d6 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:37:17 +0800 Subject: [PATCH 33/49] fix(esa): support EdgeKV health checks in map mode (#163) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(esa): support EdgeKV health checks in map mode Issue #159 reports that Alibaba Cloud ESA initialization is blocked by for DB_FORMAT=map. ESA's EdgeKV API exposes get/put/delete but no list method. The KV binding health check called list unconditionally even though map format only reads/writes the single openlist_config key. Skip the list probe unless the effective DB format is key, which requires listing. Match backend.readFormat semantics, including default map and DB_DRIVER=json compatibility, and return a clear error if key format is selected without list support. Reuse esa-entry's existing raw EdgeKV probe for health checking so the check does not add another per-request KV subrequest and can surface probe failures hidden by the wrapper's null fallback. Validation: kv.test.ts 7/7; test:store 13/13; test:server 207/207; npm run lint passed; mutation check confirmed unconditional list probing fails the new regression test; git diff --check passed. * fix(esa): share readFormat with the backend factory 健康检查曾复刻一份 DB_FORMAT 解析,与 backend.ts 各写一遍迟早漂移;下沉到 store/config.ts 后由驱动与工厂共用同一实现(含 env 优先级、缺省 map 与 DB_DRIVER=json 别名)。wrapEsaEdgeKV 的 healthProbe 改为可选:缺省时做一次真实探测,而不是假定可用。补一条缺省 map 不探测 list 的回归用例。 --------- Co-authored-by: pikachuren --- esa-entry.ts | 31 +++++- src/backend/internal/model/store/backend.ts | 37 +------ src/backend/internal/model/store/config.ts | 45 +++++++++ .../internal/model/store/driver/kv.test.ts | 96 ++++++++++++++++++- src/backend/internal/model/store/driver/kv.ts | 31 ++++-- 5 files changed, 193 insertions(+), 47 deletions(-) create mode 100644 src/backend/internal/model/store/config.ts diff --git a/esa-entry.ts b/esa-entry.ts index b98e104b..8c49e552 100644 --- a/esa-entry.ts +++ b/esa-entry.ts @@ -93,12 +93,30 @@ function deleteModuleKvCache(key: string): void { } /** - * 包装 ESA EdgeKV 为项目通用的 { get, put, delete } 接口。 + * 包装 ESA EdgeKV 为项目通用的 { get, put, delete } 接口,并复用入口处的读探测结果。 * @param edgeKv 原始 EdgeKV 实例 * @param cache 请求级缓存 Map(每次请求新建),同 key 仅发起一次真实 KV 调用 + * @param healthProbe 入口处已完成的原始 EdgeKV 探测结果(可选);缺省时 + * 健康检查自行做一次真实 get,而不是假定可用 */ -function wrapEsaEdgeKV(edgeKv: any, cache?: Map) { +function wrapEsaEdgeKV( + edgeKv: any, + cache: Map | undefined, + healthProbe?: { ok: boolean; error: string | null }, +) { return { + // 复用 fetch 入口刚完成的原始探测结果;不能走 get() 的 null 容错缓存, + // 也不能重复请求 EdgeKV(ESA 每请求有 KV 子请求配额)。未传入探测结果时 + // 退化为一次真实读取:宁可多花一个子请求,也不把未知状态报成可用。 + async healthCheck(): Promise { + if (!healthProbe) { + await edgeKv.get("__openlist_probe__") + return + } + if (!healthProbe.ok) { + throw new Error(healthProbe.error || "ESA EdgeKV health probe failed") + } + }, async get(key: string, _type?: string): Promise { // 1. 命中请求级缓存:直接返回,不发起 KV 子请求 if (cache && cache.has(key)) { @@ -182,7 +200,7 @@ export default { if (edgeKvCtor) { try { const edgeKv = new edgeKvCtor({ namespace }) - // probe 本身也走缓存,避免和业务 get 争抢配额 + // 探测一次后复用给 kvDriver.health,避免健康检查再次消耗 KV 子请求。 let kvTestOk = false let kvTestErr: string | null = null try { @@ -191,8 +209,11 @@ export default { } catch (e: any) { kvTestErr = e?.message || String(e) } - // 传入请求级缓存 - const wrappedKv = wrapEsaEdgeKV(edgeKv, kvCache) + // 传入请求级缓存和原始探测结果;healthCheck 不再额外调用 EdgeKV。 + const wrappedKv = wrapEsaEdgeKV(edgeKv, kvCache, { + ok: kvTestOk, + error: kvTestErr, + }) // 同时挂载到 env 和 globalThis:ESA 的 env 对象可能是 Proxy/frozen, // 直接属性赋值可能不生效;项目 getKvBinding 会同时检查 env[key] 和 globalThis[key] try { diff --git a/src/backend/internal/model/store/backend.ts b/src/backend/internal/model/store/backend.ts index 41abe0d1..c2e91b57 100644 --- a/src/backend/internal/model/store/backend.ts +++ b/src/backend/internal/model/store/backend.ts @@ -25,6 +25,7 @@ import { memoryDriver } from "./driver/memory" import { mapFormat } from "./format/map" import { keyFormat } from "./format/key" import { sqlFormat } from "./format/sql" +import { readEnv, readFormat } from "./config" import { DB_CIPHER_VALUES, DEFAULT_DB_CIPHER, @@ -32,14 +33,6 @@ import { type DbCipher, } from "../../../pkg/crypto" -/** - * 读取环境变量(支持 process.env 和 env 对象)。 - */ -function readEnv(key: string, defaultValue: string, env?: any): string { - const e = env || (typeof process !== "undefined" ? process.env : {}) || {} - return String(e[key] || "").trim().toLowerCase() || defaultValue -} - /** * 读取存储驱动配置。 */ @@ -59,30 +52,10 @@ export function readDriver(env?: any): StorageDriver { return driver } -/** - * 读取存储格式配置。 - * - * 契约(与官方文档一致):DB_FORMAT 与 DB_DRIVER 是**正交**的两个维度 —— - * 前者决定「怎么组织数据」(map / key / sql),后者决定「存到哪里」。 - * 因此 DB_FORMAT 缺省时**一律取全局默认 map**,绝不按驱动名改写: - * kv / d1 / blob / do / mysql 的缺省格式都是 map。 - * - * 历史遗留(已移除):旧版 DB_DRIVER=kv 承担「分表存储」语义,重构为 - * 「驱动层 + 格式层分离」后曾加过一条 `DB_DRIVER=kv 且无 DB_FORMAT → key` - * 的兼容映射。它的代价是「只设了 DB_DRIVER」的结果依赖驱动名(d1 得 map、 - * kv 得 key),且升级用户会静默从 map 布局切到 key 布局(两种布局的键名 - * 不同,互相读不到)。分表请显式配置 DB_FORMAT=key。 - */ -export function readFormat(env?: any): StorageFormat { - const e = env || (typeof process !== "undefined" ? process.env : {}) || {} - - // 向后兼容:DB_DRIVER=json → map(旧驱动别名,与 readDriver 的 json→auto 成对) - if (String(e.DB_DRIVER || "").trim().toLowerCase() === "json") { - return "map" - } - - return readEnv("DB_FORMAT", "map", env) as StorageFormat -} +// readFormat 的实现已下沉到 ./config(驱动层健康检查需要与后端工厂共用同一份 +// 解析逻辑,避免两处复刻后漂移)。这里保持原有导出路径不变: +// `import { readFormat } from "./backend"` 仍然可用。 +export { readFormat } /** * 读取数据库字段加密算法(DB_CIPHER)。 diff --git a/src/backend/internal/model/store/config.ts b/src/backend/internal/model/store/config.ts new file mode 100644 index 00000000..c271d8f8 --- /dev/null +++ b/src/backend/internal/model/store/config.ts @@ -0,0 +1,45 @@ +/** + * 存储配置解析(DB_DRIVER / DB_FORMAT 的 env 读取)。 + * + * 单独成模块的原因:驱动层(driver/*.ts)的健康检查与后端工厂必须使用**同一份** + * 解析逻辑。driver 已被 backend.ts 导入,若让 driver 反向从 backend.ts 取 + * readFormat() 就会形成循环依赖;把这两个函数放到更底层即可共用一份实现, + * 避免两处各写一遍、日后漂移(见 PR #163 评审意见)。 + */ +import type { StorageFormat } from "./types" + +/** + * 读取环境变量(支持 process.env 和 env 对象)。 + * + * 优先级:显式传入的 env > process.env > 空对象。即 env 对象**没有**该键时 + * 会回退到 process.env(自托管场景依赖此行为),与 readFormat 的缺省值同源。 + */ +export function readEnv(key: string, defaultValue: string, env?: any): string { + const e = env || (typeof process !== "undefined" ? process.env : {}) || {} + return String(e[key] || "").trim().toLowerCase() || defaultValue +} + +/** + * 读取存储格式配置。 + * + * 契约(与官方文档一致):DB_FORMAT 与 DB_DRIVER 是**正交**的两个维度 —— + * 前者决定「怎么组织数据」(map / key / sql),后者决定「存到哪里」。 + * 因此 DB_FORMAT 缺省时**一律取全局默认 map**,绝不按驱动名改写: + * kv / d1 / blob / do / mysql 的缺省格式都是 map。 + * + * 历史遗留(已移除):旧版 DB_DRIVER=kv 承担「分表存储」语义,重构为 + * 「驱动层 + 格式层分离」后曾加过一条 `DB_DRIVER=kv 且无 DB_FORMAT → key` + * 的兼容映射。它的代价是「只设了 DB_DRIVER」的结果依赖驱动名(d1 得 map、 + * kv 得 key),且升级用户会静默从 map 布局切到 key 布局(两种布局的键名 + * 不同,互相读不到)。分表请显式配置 DB_FORMAT=key。 + */ +export function readFormat(env?: any): StorageFormat { + const e = env || (typeof process !== "undefined" ? process.env : {}) || {} + + // 向后兼容:DB_DRIVER=json → map(旧驱动别名,与 readDriver 的 json→auto 成对) + if (String(e.DB_DRIVER || "").trim().toLowerCase() === "json") { + return "map" + } + + return readEnv("DB_FORMAT", "map", env) as StorageFormat +} diff --git a/src/backend/internal/model/store/driver/kv.test.ts b/src/backend/internal/model/store/driver/kv.test.ts index b36b84b5..e1b7dda3 100644 --- a/src/backend/internal/model/store/driver/kv.test.ts +++ b/src/backend/internal/model/store/driver/kv.test.ts @@ -107,9 +107,10 @@ test("kv binding list stops when cursor cannot advance (quota-burn guard)", asyn assert.equal(calls, 2) }) -test("kv health (binding) exercises list so exhausted quota is not reported ready", async () => { - // list 抛错(Cloudflare 免费版耗尽 list 配额时的行为)→ 必须报不可用 +test("kv health (binding, key format) exercises list so exhausted quota is not reported ready", async () => { + // DB_FORMAT=key 依赖 list;list 抛错(如 Cloudflare 配额耗尽)必须报不可用。 const badEnv = { + DB_FORMAT: "key", KV: { get: async () => null, put: async () => {}, @@ -126,6 +127,7 @@ test("kv health (binding) exercises list so exhausted quota is not reported read // 健康时:list 探测恰好消耗 1 次操作,且不用空前缀(不拉全量键) let listCalls = 0 const okEnv = { + DB_FORMAT: "key", KV: { get: async () => null, put: async () => {}, @@ -141,3 +143,93 @@ test("kv health (binding) exercises list so exhausted quota is not reported read assert.equal(ok.available, true) assert.equal(listCalls, 1) }) + +test("kv health (binding, map format) does not require an unsupported list API", async () => { + // ESA EdgeKV exposes get/put/delete but no list. Map format only needs the + // single openlist_config key, so it must still be considered healthy. + let getCalls = 0 + let healthCheckCalls = 0 + const env = { + DB_FORMAT: "map", + KV: { + get: async () => { + getCalls += 1 + return undefined + }, + put: async () => {}, + delete: async () => true, + // esa-entry has already performed this raw probe for the current request. + healthCheck: async () => { + healthCheckCalls += 1 + }, + }, + } + + const health = await kvDriver.health(env) + assert.equal(health.available, true) + assert.equal(health.mode, "binding") + assert.equal(getCalls, 0, "health must reuse the ESA entry probe") + assert.equal(healthCheckCalls, 1) + + const failedProbe = await kvDriver.health({ + DB_FORMAT: "map", + KV: { + get: async () => null, + put: async () => {}, + delete: async () => true, + healthCheck: async () => { + throw new Error("EdgeKV read unavailable") + }, + }, + }) + assert.equal(failedProbe.available, false) + assert.match(String(failedProbe.error), /EdgeKV read unavailable/) +}) + +test("kv health (binding) honors the legacy DB_DRIVER=json map alias", async () => { + const health = await kvDriver.health({ + DB_DRIVER: "json", + DB_FORMAT: "key", + KV: { + get: async () => null, + put: async () => {}, + delete: async () => true, + }, + }) + assert.equal(health.available, true) +}) + +test("kv health (binding, key format) reports missing list support explicitly", async () => { + const env = { + DB_FORMAT: "key", + KV: { + get: async () => undefined, + put: async () => {}, + delete: async () => true, + }, + } + + const health = await kvDriver.health(env) + assert.equal(health.available, false) + assert.match(String(health.error), /list\(\).*required by DB_FORMAT=key/) +}) + +test("kv health defaults to map when DB_FORMAT is unset (ESA EdgeKV shape)", async () => { + // 曾经的复刻实现自己解析 env,与 backend.ts 的 readFormat() 各写一份; + // 现在两者共用同一份实现,缺省格式必须是 map(而不是按驱动名猜 key), + // 因此没有 list() 的 EdgeKV 绑定在未配置 DB_FORMAT 时仍应判为可用。 + let listCalls = 0 + const health = await kvDriver.health({ + KV: { + get: async () => null, + put: async () => {}, + delete: async () => true, + list: async () => { + listCalls += 1 + return { keys: [], list_complete: true } + }, + }, + }) + assert.equal(health.available, true) + assert.equal(listCalls, 0, "map 格式不得探测 list") +}) diff --git a/src/backend/internal/model/store/driver/kv.ts b/src/backend/internal/model/store/driver/kv.ts index d9e68e8f..d36430cb 100644 --- a/src/backend/internal/model/store/driver/kv.ts +++ b/src/backend/internal/model/store/driver/kv.ts @@ -8,6 +8,7 @@ * 自动检测环境并选择合适的模式。 */ import { sanitizeProxyOrigin } from "../proxy" +import { readFormat } from "../config" import type { Driver, EnvContext } from "../types" /** @@ -556,14 +557,28 @@ export const kvDriver: Driver = { // 模式1: Binding 模式 if (kv) { try { - await kv.get("__health_check__") - // list 探测:list 是最稀缺的配额(Cloudflare 免费版仅 1000 次/天), - // 且 DB_FORMAT=key 的 load/save 依赖它。只探测 get 会让「list 配额 - // 耗尽 / list 接口异常」的环境误报为就绪(issue #125:env_check - // 全绿但初始化 500)。prefix 不会命中真实键,取回空页即 1 次操作; - // 且 health 只在手动诊断路径(env_check / debug / admin)被调用, - // 不在每请求路径上,不会放大配额消耗。 - await kv.list({ prefix: "__health_check__", cursor: "", limit: 256 }) + // ESA wrapper 的业务 get 为兼容缓存会把底层错误映射成 null;若它提供 + // 直连探测方法,健康检查必须绕开该容错,以免把读取故障误报成可用。 + if (typeof kv.healthCheck === "function") { + await kv.healthCheck() + } else { + await kv.get("__health_check__") + } + // key 格式的 load/save 依赖 list;map 格式只读写固定的 openlist_config, + // 不要求 EdgeKV 暴露 list API(ESA EdgeKV 内置 API 仅提供 get/put/delete)。 + // 只有 key 格式才探测 list,避免 map 格式部署被不支持的 list() 误判为 + // 存储不可用(issue #159)。对 key 格式仍检查 list,防止配额耗尽/接口异常 + // 导致 env_check 误报 ready(issue #125)。使用专用前缀,取回空页只耗 1 次。 + // 直接复用后端的 readFormat():健康检查与真实读写必须用同一份解析逻辑 + // (env 优先级、缺省 map、DB_DRIVER=json 别名),复刻一份迟早漂移。 + if (readFormat(env) === "key") { + if (typeof kv.list !== "function") { + throw new Error( + "KV binding does not support list(), which is required by DB_FORMAT=key", + ) + } + await kv.list({ prefix: "__health_check__", cursor: "", limit: 256 }) + } return { driver: "kv", mode: "binding", From 9f1f459b0da1856bc317901eb042fe3a06550a7f Mon Sep 17 00:00:00 2001 From: Pikachu Bot Date: Wed, 4 Nov 2026 12:22:40 +0800 Subject: [PATCH 34/49] fix(esa): correct the config doc and share the KV health probe key (#172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 #163 增量评审的两条 P2: - store/config.ts 的 readEnv 注释写反了:`env || process.env` 只在 env 整体 为空时才回退,env 存在但缺该键**不会**逐键回退。注释改成实际语义,并注明 「改成逐键回退」会改变 KV/D1 配置解析结果,属破坏性变更。 - ESA 入口的原始探测 key(__openlist_probe__)与 kv 驱动 health 的 __health_check__ 不同名。统一导出 KV_HEALTH_PROBE_KEY,三处(kv.get、 kv.list 前缀、esa-entry 探测)共用,便于核对 ESA 每请求的 KV 子请求配额。 验证:tsc --noEmit 0 error;kv.test.ts 8/8。 Co-authored-by: pikachuren --- esa-entry.ts | 6 ++++-- src/backend/internal/model/store/config.ts | 17 +++++++++++++++-- src/backend/internal/model/store/driver/kv.ts | 6 +++--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/esa-entry.ts b/esa-entry.ts index 8c49e552..7a3efde5 100644 --- a/esa-entry.ts +++ b/esa-entry.ts @@ -17,6 +17,7 @@ */ declare const EdgeKV: any import app, { setSpaFallbackHtml } from "./src/backend/index" +import { KV_HEALTH_PROBE_KEY } from "./src/backend/internal/model/store/config" import INDEX_HTML from "./dist/index.html" // 构建期内联 dist/index.html 作为 SPA 兜底壳 setSpaFallbackHtml(INDEX_HTML) @@ -110,7 +111,8 @@ function wrapEsaEdgeKV( // 退化为一次真实读取:宁可多花一个子请求,也不把未知状态报成可用。 async healthCheck(): Promise { if (!healthProbe) { - await edgeKv.get("__openlist_probe__") + // 与 kv 驱动的 health() 用同一个键,便于核对 ESA 子请求配额 + await edgeKv.get(KV_HEALTH_PROBE_KEY) return } if (!healthProbe.ok) { @@ -204,7 +206,7 @@ export default { let kvTestOk = false let kvTestErr: string | null = null try { - await edgeKv.get("__openlist_probe__") + await edgeKv.get(KV_HEALTH_PROBE_KEY) kvTestOk = true } catch (e: any) { kvTestErr = e?.message || String(e) diff --git a/src/backend/internal/model/store/config.ts b/src/backend/internal/model/store/config.ts index c271d8f8..3b8cd632 100644 --- a/src/backend/internal/model/store/config.ts +++ b/src/backend/internal/model/store/config.ts @@ -11,14 +11,27 @@ import type { StorageFormat } from "./types" /** * 读取环境变量(支持 process.env 和 env 对象)。 * - * 优先级:显式传入的 env > process.env > 空对象。即 env 对象**没有**该键时 - * 会回退到 process.env(自托管场景依赖此行为),与 readFormat 的缺省值同源。 + * 优先级:显式传入且为真值的 env > process.env > 空对象。 + * 注意是**整体**回退:`env` 非空时只用 env,即使它没有该键也不会再去看 + * process.env(`env || process.env` 只判 env 本身)。这条语义与 + * backend.ts/readFormat 完全一致;若有人按「逐键回退」去"修正"它, + * 自托管场景下 KV/D1 配置的解析结果会变,属于破坏性变更。 */ export function readEnv(key: string, defaultValue: string, env?: any): string { const e = env || (typeof process !== "undefined" ? process.env : {}) || {} return String(e[key] || "").trim().toLowerCase() || defaultValue } +/** + * KV 健康探测使用的键/前缀。 + * + * 两处必须同名:kv 驱动的 health()(get + list 前缀)与 ESA 入口 + * (esa-entry.ts 的原始 EdgeKV 探测,结果通过 healthCheck 复用)。 + * 名字不一致会让排查「ESA 每请求 KV 子请求配额」时对不上账, + * 也容易让人误以为探测走了两个不同的键(PR #163 评审)。 + */ +export const KV_HEALTH_PROBE_KEY = "__health_check__" + /** * 读取存储格式配置。 * diff --git a/src/backend/internal/model/store/driver/kv.ts b/src/backend/internal/model/store/driver/kv.ts index d36430cb..6dfc3423 100644 --- a/src/backend/internal/model/store/driver/kv.ts +++ b/src/backend/internal/model/store/driver/kv.ts @@ -8,7 +8,7 @@ * 自动检测环境并选择合适的模式。 */ import { sanitizeProxyOrigin } from "../proxy" -import { readFormat } from "../config" +import { KV_HEALTH_PROBE_KEY, readFormat } from "../config" import type { Driver, EnvContext } from "../types" /** @@ -562,7 +562,7 @@ export const kvDriver: Driver = { if (typeof kv.healthCheck === "function") { await kv.healthCheck() } else { - await kv.get("__health_check__") + await kv.get(KV_HEALTH_PROBE_KEY) } // key 格式的 load/save 依赖 list;map 格式只读写固定的 openlist_config, // 不要求 EdgeKV 暴露 list API(ESA EdgeKV 内置 API 仅提供 get/put/delete)。 @@ -577,7 +577,7 @@ export const kvDriver: Driver = { "KV binding does not support list(), which is required by DB_FORMAT=key", ) } - await kv.list({ prefix: "__health_check__", cursor: "", limit: 256 }) + await kv.list({ prefix: KV_HEALTH_PROBE_KEY, cursor: "", limit: 256 }) } return { driver: "kv", From 9b3b35d83b513427051e6ef5f5130baaa57afbc7 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:24:27 +0800 Subject: [PATCH 35/49] =?UTF-8?q?fix(esa):=20=E4=B8=8A=E4=BC=A0=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E6=94=B9=E7=94=A8=20Uint8Array=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20Buffer=20is=20not=20defined=20(#169)=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(esa): 上传链路改用 Uint8Array,修复 Buffer is not defined (#169) ESA 边缘运行时没有 Node 全局 Buffer(CF Workers 靠 nodejs_compat 注入),旧实现 pkg/upload-limit.ts 的 Buffer.from(await c.req.arrayBuffer()) 会在读请求体时抛 Buffer is not defined,导致 /fs/put、/fs/form、/fs/multipart/chunk、S3 PutObject、WebDAV PUT 全部 500。 改为 Uint8Array:readBodyWithinLimit/putItem/StorageDriver.put 类型收敛到 Uint8Array,url_tree 用 TextDecoder 解码,chunk 驱动不再包 Buffer.from;新增删除全局 Buffer 的回归用例。 * fix(driver): document the Uint8Array upload contract and drop Buffer from putBlob 按 #171 评审的两条 P2: - StorageDriver.put 的注释写明实现方不得使用 Buffer 专有方法 (toString(encoding) / Buffer.from / Buffer.concat),并给出 TextDecoder / btoa 的替代;Cloudflare 靠 nodejs_compat 注入 Buffer、ESA 没有,同一段代码 两边行为不同。 - upload-limit.ts 补注:new Uint8Array(arrayBuffer) 是视图而非拷贝,避免后人 误以为多了一次全量拷贝又改回 Buffer。 顺带修掉评审提到的同类遗漏:drivers/github/util.ts 的 putBlob 仍在用 Buffer.from(content).toString("base64"),ESA 上传会抛 "Buffer is not defined";改为纯 JS 的 bytesToBase64(分块 + btoa)。 验证:tsc --noEmit 0 error;drivers 197/197;security_hardening 8/8。 --------- Co-authored-by: pikachuren --- src/backend/drivers/chunk/driver.ts | 2 +- src/backend/drivers/github/util.ts | 19 +++++++++++- src/backend/drivers/url_tree/driver.ts | 9 ++++-- src/backend/internal/driver/base.ts | 10 ++++++- src/backend/internal/op/storage.ts | 3 +- src/backend/pkg/upload-limit.ts | 11 +++++-- src/backend/server/fs.ts | 5 ++-- src/backend/server/security_hardening.test.ts | 29 +++++++++++++++++++ src/backend/server/webdav.ts | 3 +- 9 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/backend/drivers/chunk/driver.ts b/src/backend/drivers/chunk/driver.ts index 87d2f0e1..fc2a2cbf 100644 --- a/src/backend/drivers/chunk/driver.ts +++ b/src/backend/drivers/chunk/driver.ts @@ -311,7 +311,7 @@ export class ChunkDriver implements StorageDriver { const end = Math.min(offset + this.partSize, size) const partContent = content.subarray(offset, end) const partPath = joinPath(chunkDir, this.getPartName(partIndex)) - await this.remoteDriver!.put("", partPath, Buffer.from(partContent)) + await this.remoteDriver!.put("", partPath, partContent) offset = end partIndex++ } diff --git a/src/backend/drivers/github/util.ts b/src/backend/drivers/github/util.ts index da351c8d..9fd90b3d 100644 --- a/src/backend/drivers/github/util.ts +++ b/src/backend/drivers/github/util.ts @@ -9,6 +9,21 @@ import { MessageTemplateVars, } from "./types" +/** + * Uint8Array → base64,纯 JS 实现(不依赖全局 Buffer)。 + * + * 分块转换避免 `String.fromCharCode(...bytes)` 在几百 KB 上传时爆栈; + * btoa 在 Cloudflare Workers 与 ESA 都可用。 + */ +function bytesToBase64(bytes: Uint8Array): string { + let binary = "" + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return btoa(binary) +} + export function cleanPath(p: string): string { if (!p) return "/" const normalized = p.replace(/\\/g, "/").replace(/\/+/g, "/") @@ -266,7 +281,9 @@ export class GithubApiClient { } async putBlob(content: Buffer | Uint8Array): Promise { - const base64Content = Buffer.from(content).toString("base64") + // 不用 Buffer.from(content).toString("base64"):ESA 等边缘运行时没有全局 + // Buffer,上传路径会直接抛 "Buffer is not defined"(Issue #169 的同类问题)。 + const base64Content = bytesToBase64(content) const res = await this.request<{ sha: string }>( `https://api.github.com/repos/${this.owner}/${this.repo}/git/blobs`, { diff --git a/src/backend/drivers/url_tree/driver.ts b/src/backend/drivers/url_tree/driver.ts index 0c350ccb..b3cf00c2 100644 --- a/src/backend/drivers/url_tree/driver.ts +++ b/src/backend/drivers/url_tree/driver.ts @@ -349,10 +349,15 @@ export class UrlTreeDriver implements StorageDriver { this.persist() } - async put(_v: string, physicalPath: string, content: Buffer): Promise { + async put( + _v: string, + physicalPath: string, + content: Uint8Array, + ): Promise { if (!this.addition.writable) throw new Error("[UrlTree] not writable") // put 的内容应是一行 URL 定义(name:url) - const line = content.toString("utf8").trim() + // Uint8Array 没有 toString(encoding),统一用 TextDecoder(ESA 无全局 Buffer) + const line = new TextDecoder().decode(content).trim() const parent = getNodeByPath( this.root, "/" + splitPath(physicalPath).slice(1, -1).join("/"), diff --git a/src/backend/internal/driver/base.ts b/src/backend/internal/driver/base.ts index d3d86f22..a4363122 100644 --- a/src/backend/internal/driver/base.ts +++ b/src/backend/internal/driver/base.ts @@ -171,5 +171,13 @@ export interface StorageDriver { srcPhys: string, dstPhys: string, ): Promise - put(virtualPath: string, physicalPath: string, content: Buffer): Promise + // 上传内容统一用 Uint8Array:ESA 边缘运行时没有全局 Buffer(Issue #169)。 + // 实现方**不得使用 Buffer 专有方法**(toString(encoding) / Buffer.from / + // Buffer.concat 等):Cloudflare 靠 nodejs_compat 注入 Buffer,ESA 没有, + // 同一段代码在两边行为不同。需要文本请用 TextDecoder,需要 base64 请用 btoa。 + put( + virtualPath: string, + physicalPath: string, + content: Uint8Array, + ): Promise } diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index ef4d6f25..b4c7b4fd 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1823,7 +1823,8 @@ export async function copyItems( export async function putItem( virtualPath: string, - content: Buffer, + // Uint8Array 而非 Node Buffer:ESA 等边缘运行时没有全局 Buffer(Issue #169) + content: Uint8Array, requestContext?: StorageRequestContext, ): Promise { const resolved = await resolvePath(virtualPath, requestContext?.env) diff --git a/src/backend/pkg/upload-limit.ts b/src/backend/pkg/upload-limit.ts index ce478e6c..6e932a70 100644 --- a/src/backend/pkg/upload-limit.ts +++ b/src/backend/pkg/upload-limit.ts @@ -57,14 +57,21 @@ export const tooLargeResponse = (c: any, limit: number, label = "File") => * 读取请求体,并按**实际字节数**复核上传上限。 * * 仍保留 `Content-Length` 预检:能在读入内存**之前**就拒绝超大请求,省一次拷贝。 + * + * 返回 `Uint8Array` 而不是 Node 的 `Buffer`:ESA 等边缘运行时没有全局 `Buffer` + * (Cloudflare Workers 是靠 nodejs_compat 注入的),直接用 `Buffer.from` 会在 + * 读请求体这一步抛 `Buffer is not defined`,导致上传全链路 500。见 Issue #169。 */ export async function readBodyWithinLimit( c: any, part = false, -): Promise<{ ok: true; buffer: Buffer } | { ok: false; limit: number }> { +): Promise<{ ok: true; buffer: Uint8Array } | { ok: false; limit: number }> { const pre = exceedsUploadLimit(c, part) if (pre !== null) return { ok: false, limit: pre } - const buffer = Buffer.from(await c.req.arrayBuffer()) + // 注意:这里只是对同一个 ArrayBuffer 建视图,**不额外拷贝**全量请求体。 + // 曾有人误以为多了一次拷贝而改回 Buffer.from(...),结果在无 Buffer 的 + // 运行时(ESA)直接 500 —— 见 Issue #169。 + const buffer = new Uint8Array(await c.req.arrayBuffer()) const max = getUploadSizeLimit(c, part) if (buffer.length > max) return { ok: false, limit: max } return { ok: true, buffer } diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index 4f22a648..9474d68a 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -1049,7 +1049,8 @@ fsRouter.put("/form", async (c) => { data: null, }) } - const buffer = Buffer.from(await (file as File).arrayBuffer()) + // 用 Uint8Array 而非 Buffer:ESA 边缘运行时没有全局 Buffer(Issue #169) + const buffer = new Uint8Array(await (file as File).arrayBuffer()) // multipart 的 Content-Length 只覆盖整个表单,仍需按文件实际字节复核 const max = getUploadSizeLimit(c, false) if (buffer.length > max) return tooLargeResponse(c, max) @@ -2030,7 +2031,7 @@ fsRouter.post("/archive/decompress", async (c) => { continue } const content = await extractZipEntry(bytes, entry) - await putItem(targetPath, Buffer.from(content), getStorageRequestContext(c)) + await putItem(targetPath, content, getStorageRequestContext(c)) count++ } } diff --git a/src/backend/server/security_hardening.test.ts b/src/backend/server/security_hardening.test.ts index cfb70638..e540aa3d 100644 --- a/src/backend/server/security_hardening.test.ts +++ b/src/backend/server/security_hardening.test.ts @@ -325,6 +325,35 @@ test("readBodyWithinLimit 按实际字节数生效(省略 Content-Length 也 assert.equal(partTooLarge.ok, false) }) +/** + * Issue #169:阿里云 ESA 边缘运行时没有 Node 的全局 Buffer(Cloudflare Workers + * 是靠 nodejs_compat 注入的),旧实现 `Buffer.from(await c.req.arrayBuffer())` + * 会在读请求体这一步直接抛 `Buffer is not defined`,上传/WebDAV PUT/S3 PutObject + * 全部 500。这里删掉全局 Buffer 复现该环境,确保读取链路只依赖 Uint8Array。 + */ +test("readBodyWithinLimit 不依赖全局 Buffer(ESA 运行时)", async () => { + const g = globalThis as any + const saved = g.Buffer + delete g.Buffer + try { + const ctx = { + env: {}, + req: { + header: () => undefined, + arrayBuffer: async () => new TextEncoder().encode("abcd").buffer, + }, + } + const res = await readBodyWithinLimit(ctx) + assert.equal(res.ok, true) + if (res.ok) { + assert.equal(res.buffer.length, 4) + assert.equal(res.buffer.constructor.name, "Uint8Array") + } + } finally { + g.Buffer = saved + } +}) + test("WebDAV: 受 meta 保护的路径返回带指引的 403(而不是裸 403)", async () => { const env: any = { JWT_SECRET: "test-only-jwt-secret-for-hardening" } const davUser: any = { diff --git a/src/backend/server/webdav.ts b/src/backend/server/webdav.ts index c52ac0a6..3b4ea1f6 100644 --- a/src/backend/server/webdav.ts +++ b/src/backend/server/webdav.ts @@ -388,7 +388,8 @@ webdavRouter.all("/*", async (c) => { if (await deniedByMetaWrite(c, user, splitPath(actualPath).dir, true)) { return metaForbidden(c) } - const buffer = Buffer.from(await c.req.arrayBuffer()) + // Uint8Array 而非 Buffer:ESA 边缘运行时没有全局 Buffer(Issue #169) + const buffer = new Uint8Array(await c.req.arrayBuffer()) await putItem(actualPath, buffer, ctx) return c.body(null, 201) } From 3abbd685ac72c05365f68fc85c7228bdccc97bda Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:52:00 +0800 Subject: [PATCH 36/49] feat(media): complete media library, Emby and Subsonic parity with Go backend (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(media): implement media library, Emby and Subsonic APIs for Worker * feat(media): support emby series/album detail and subsonic enc password * feat(media): complete media library, Emby and Subsonic parity with Go backend - scan: ID3v2/Vorbis/M4A tags, .lrc lyrics, path_merge, season/episode parsing, MIME, EPUB/CBZ/PDF covers - scrape: concurrent worker pool, TMDB fuzzy matching, image/book local scrape, scrape stats - storage: shard items/user_data/thumbs, diff-based writes, automatic migration from single JSON - emby: Season hierarchy, Sessions progress, PlaybackInfo HTTP stream fix, anonymous images, websocket - subsonic: XML/JSON/JSONP, formPost, playlists/rating/lyrics/queue, auth rate limiting - api: progress semantics, favorites filter, real delete_invalid, import/export scope, /me/appkey, STRM - security: cover proxy hop-by-hop SSRF checks + magic sniffing, media type access check, AES-GCM app keys - transcode: force-disabled on Worker (settings hidden and rejected, API reports unsupported) * fix(media): 收紧封面 MIME 信任与两个匿名入口 - 封面只信魔数:ID3 APIC / FLAC PICTURE / M4A covr 不再采信声明的 MIME;persistCover 去掉 mime 兜底,非栅格图(含 SVG)直接丢弃 - serveCover 的 data: 分支解码后嗅探 MIME 并补 CSP sandbox;/media/thumb 同样按魔数复核 - 匿名封面改为「token 用户 -> guest」加载媒体库,删除只过滤 hidden 的 all:true 分支,guest 缺失或被禁用返回 401 - 限流 IP 优先取 cf-connecting-ip,持久化写入增加 IP 格式校验与每分钟上限 - 禁止 guest 创建应用密钥,并限制每用户最多 20 个 - STRM 输出根为 "/" 时不再误判越界,抽出 withinOutRoot 并补用例 * fix(media): 对齐 Go 侧最新媒体库改动 - Emby 用户 Id 改为稳定 GUID 形状,/Users/{id} 兼容 GUID / 数字 / 用户名三种形式(对齐 2b6b8cbf) - 应用密钥改用 prefix 预筛 + 解密常量时间比对;Emby token、密码与 Subsonic apiKey 三处统一 - 限流阈值与 Go 一致:5 次失败锁定 5 分钟;锁定到期后计数清零,避免「错一次锁五分钟」 - 限流 key 加固:用户名截断到 128 字符,buckets 增加硬上限(伪造用户名不能把 Map 撑爆) - Subsonic 只对真正带凭据(apiKey 或 u+t/p)的失败计数,裸探测不再刷新锁定 - /System/Endpoint 按来源 IP 判断本机/内网;/System/Info 不再返回监听端口 - Emby 兼容版本对齐 4.10.0.5 - 新增限流单测:IP 来源优先级、IP/用户名双维度、锁定到期清零 * fix(media): 修复封面越权与分片持久化丢数据 封面接口统一走 coverOfId:mv/so/ep 对目标条目执行 canAccessItem,Series/Season/Album/Artist 逐条校验后取封面;删除重复注册的封面路由,Subsonic getcoverart 复用同一实现。 分片写入逐次检查结果,失败立即中断且不提交 meta,快照只在全部成功后推进;writePersistedSecret 把驱动返回 false 也视为失败;saveMediaMeta 纳入 saveChain 并在失败时失效缓存。 提交 meta 前与持久化版本合并分片索引与 next_id;读取时按前缀枚举分片,避免多实例互相覆盖导致分片不再被读取。 新增 store_persist.test.ts 与封面权限回归用例。 * fix(media): reject parent segments in STRM output paths withinOutRoot() 只做字符串前缀比较,/out/../../etc/x.strm 以 /out/ 开头会被判成根内路径,刮削得到的外部名称一旦含 .. 就能写到输出目录之外。改为先拒绝任何 .. 段再比较前缀,并补三条回归用例。 * fix(media): reject encoded dot segments in STRM output paths 评论指出的第二点是编码点段:%2e%2e 在存储层解码后同样变成 "..", 仅检查原始段仍可穿越。withinOutRoot() 现在对每个段先 decodeURIComponent (非法编码按原文处理)再判断是否为 "." / "..",并补两条编码点段用例。 * chore: drop local triage scratch files from the PR * fix(media): address the remaining review threads on the media/Emby PR 剧集身份与播放: - Series 身份改为按目录派生(不再用 scraped_name),单集标题存入 episode_title,避免同一部剧按集裂成多个 Series(#1/#3) - 单集文件也能解析 ep---,修复播放 404(#2) - episode id / 进度 / 续播 / 下一集判定都带季号,旧 id 仍兼容(#11) - STRM 按每集自己的季号导出;合并专辑逐曲生成(#23/#24) - 未合并单集与合并专辑的路径鉴权覆盖到最终文件(#13) - 按用户签发的 STRM 链接遵守 link_expiration(#14) 媒体 API 与扫描: - 专辑聚合逐曲鉴权,缓存按实际校验路径去重(#12) - 管理端改用 adminAuthMiddleware,不再接受 ?token=(#15) - scan_paths 删除/清空校验 id,非法 id 不再误删孤儿条目(#16) - 导入后移除无效的 invalidateMediaCache(#17) - delete_invalid 支持游标分页;目录探测只在明确 404 时删除(#26/#27) - 封面网关单独白名单,原始 URL 仍需可信来源(#28) - 图片封面读取完整图片,超过 8MB 才跳过(#19) - FLAC metadata block 长度改为大端(#18) - Discogs 标题只用于合并专辑,不再覆盖单曲标题(#20) - 刮削路径全禁用时不再等于放行(#21);运行态在 finally 中清理(#22) 持久化与读取: - 分片读取区分「不存在」与「读取失败」,失败时禁止写回(#5) - 提交 meta 时逐集合合并,旧快照不再整份覆盖(#4/#6) - 加载时填充分片快照,清空分片能真正写入 [](#7) - 保存前按持久化水位重映射本实例新增 id,降低跨 isolate 撞号(#8) - readFileRange 逐跳校验重定向并跨域剥离凭据(#10) - 本地存储直接读文件区间,不再依赖 raw_url(#25) - 旧快照写入前先合并进最新缓存(#6) 验证:tsc --noEmit 0 error;media 16/16、server 220/220、model+store 52/52、 drivers 189/189。 * fix(media): fall back to the direct link when the storage forbids /p 媒体库前端固定用 stream_url 返回的 proxied(/p + force)播放,但 raw 路由 对 /p 有存储策略检查:storage.web_proxy=false 且扩展名不在 proxy_types / text_types 时返回 403 "proxy not allowed"(与 Go 的 canProxy 一致)。 139Yun 这类默认 302_redirect 的存储因此全部播放失败。 stream_url 现在先解析存储并按同一套 canUseProxyEndpoint 规则判断:不可 代理时 proxied 直接返回 /d 直链(302 到网盘预签名地址),可代理时仍返回 /p。想强制走服务端代理的部署,在存储设置里打开 Web 代理即可恢复 /p。 验证:driver=139Yun、web_proxy=false 的存储,proxied 由 /p 变为 /d, 请求 /d 返回 302 到 OBS 预签名 URL。 * perf(media): parallelize scanning, shrink tag reads and save incrementally 用户实测 1142 首歌的目录跑了十几分钟才 9 条(约 20s/首)。原因有三: 1. 扫描主循环是串行的(for + await),每个文件要换链 2~4 次; 2. 每首歌无条件读 2MB 标签窗口,1142 首 = 2.2GB 流量; 3. 只在整批扫完才 saveMediaDb,中途列表一直是空的,失败还会丢全部结果。 改动: - mapPool 并发(默认 4,上限 6,取 scrape_concurrency) - 标签分级读取:先 256KB,关键字段全空且文件更长时再补到 2MB - 每 30 条增量落盘 顺带修正上一提交的 id 水位判定:remapConflictingIds 改用**原始**持久化 meta 的 next_id 作为基准(mergeMetaForCommit 返回 max(本地,持久化),会把 自己刚分配的 id 误判成跨 isolate 冲突),并在每次成功保存后推进 state.baseNextId。 验证:tsc --noEmit 0 error;media + emby_protocol 30/30。 * fix(media): collect episodes recursively and honor the scan path type tag 对齐 Go scanner.go 的两处行为,修「删除后重扫扫不出来」: 1. Go 的 buildEpisodesFromFolder 是递归的(recursive=true,能把各季目录 下的文件收进同一条记录);TS 的 listDirMedia 只列一层。于是 `/影视资源/电影/哈利波特电影/*.mkv` 这类「分类目录 → 剧名 → 文件」 布局在剧名目录拿到 0 个文件,buildItem 返回 {},扫描结果是 「跳过 N 条」、列表全空。现在递归 3 层,并用相对路径作为 episode.file_name,播放时才不会丢掉中间目录。 2. Go 用扫描路径的 type_tag 决定电影/剧集归属(scanner.go:1244-1250); TS 的目录分支硬编码 videoType="tv"。现在按 type_tag(电影/movie) 判定,电影目录不再被当成剧集。 顺带修同一条链路上的解析问题: - 季号支持中文目录(第二季 / 第2季); - 目录季号优先于文件名里的 Sxx(压制组常把多季统一写成 S01); - SxxExx 之后的发布信息(1080p / 中英双字 / WEB-DL)不再当集标题, 回退到剧名。 验证:扫描 /Media/影视资源 从「跳过 1 条、0 条目」变为 1 条, episodes 带 `第二季/群星.S01E05...`、season=2、title=群星。 * fix(media): serve playback through a media-scoped proxy and fill admin routes 三处问题,根因不同: 1. 播放/阅读拿到的地址不可用 stream_url 之前对不可代理的存储回退成 /d(302 到网盘直链),于是 Content-Type 丢失(application/octet-stream)、Content-Disposition 是 attachment,PDF 打不开、视频也没有正确的 video/*。 现在媒体库签发带 scope=media 的签名,raw 路由验签通过后跳过 web_proxy 的 /p 限制:播放/阅读走同源代理,Range、Content-Type、 inline 展示都成立(也让 MediaBunny 的 AC3 音频回退不再跨域)。 代理响应在 scope=media 时把 attachment 改成 inline。 2. 管理端「媒体管理」载入不出来 前端管理页调用 /admin/media/albums、/album/items、/album/update、 /items/cover,后端一直没有这些路由,请求落到 SPA 兜底返回 HTML。 补齐四个接口(专辑聚合、专辑曲目、批量更新、封面上传/回收)。 验证: - PDF:206 + Content-Type: application/pdf + Content-Disposition: inline - 视频:/p 206,Content-Range 正常 - /admin/media/albums:按专辑聚合返回条目 * fix(media): align the scrape strategy with the Go scraper 对比 OpenList-Backends/internal/media/scraper 后,TS 侧有三处策略差异导致 「刮削全部失败」: 1. 查询词没清洗 TS 直接把 `item.scraped_name || file_name` 丢给 TMDB,文件名里的 `S01E05`、`HD1080P`、`中英双字`、`霸王龙压制组` 全在 query 里, TMDB 基本零结果。Go 会先解析出中文标题/英文标题/年份再逐组尝试。 新增 parseMediaTitle():去扩展名、括号别名、集号标记(SxxExx/第N集/EPxx)、 中英文发布噪声,提取年份,产出「中文片段 → 括号别名 → 去噪标题 → 原串」 候选;图书/豆瓣搜索也改用 primary。 2. 只搜一次、没有降级 Go 用候选 × 语言 × 是否有年份 组合重试。tmdbSearch 现在按 「候选(≤3) × [配置语言, 不限定语言] × [不带年份, 带年份]」枚举, 强命中(≥0.6)立即返回;不限定语言时 TMDB 会用全部别名匹配, 中文/港台译名召回明显更高。 3. 视频只有 TMDB,没有豆瓣兜底 Go 的视频刮削是 TMDB + 豆瓣双源。TS 的 scrapeDouban 只挂在书籍分支, 而且固定 cat=1002(电影)去搜书。现在按媒体类型选 1001/1002, 解析「导演/主演/年份/类型」,并在 TMDB 未命中时作为视频兜底(low=true)。 另外:未配置 media_tmdb_key 时把原因写进 stats.last_error, 避免统计面板显示一片 0 而无从判断。 验证:media 17/17、server 220/220、tsc 0 error; 本地实测一次视频刮削 12 次搜索,Inception/复仇者联盟命中中文标题。 * fix(media): proxy external covers and send a Douban referer 刮削成功但封面裂图的两个原因: 1. resolveCoverUrl() 对外链原样返回,浏览器直连豆瓣图床(防盗链,空 Referer 直接 403)与 TMDB 图片(国内常不可达)。现在外链默认走 /api/public/media/proxy,命中缓存后由 KV/边缘缓存分发。 2. 代理拉图时 User-Agent 是 OpenList-Media/1.0、没有 Referer,豆瓣仍拒。 改为常见浏览器 UA,并对 douban/doubanio 域名带 https://www.douban.com/。 验证:影视页 4 张封面(豆瓣 + TMDB 各 2 张)由空白变为全部正常显示, 代理请求均为 200。 * fix(media): keep large embedded covers when scanning music 「有的歌有封面、有的没有」是两个独立阈值造成的: 1. 扫描先只读 256KB 标签窗口,且只在「标题/艺术家/专辑全空」时才补读 2MB。封面恰好是标签里最占空间的部分(实测 393KB~1.2MB),文本字段 落在前 256KB、封面落在窗口之外时,hasCore 为真,永远不补读。 现在按容器头解析标签实际长度(ID3v2 syncsafe / FLAC metadata block 链)精确补读;解析不出长度时退回完整 2MB 窗口,不再依赖文本字段。 2. 缩略图持久化上限是 512KB,超过直接丢弃且无日志。FLAC/MP3 内嵌封面 常见 0.6~1.2MB,于是同一批文件里只有小封面能存下来。上限提高到 2MB (缩略图按内容 hash 去重,同专辑只存一份),超限时打 warn 留痕。 验证:本地 20 首音乐(FLAC + MP3),修复前 missing_cover=8, 重扫+刮削后 missing_cover=0。 --------- Co-authored-by: pikachuren --- docs/pr-media-library-parity.md | 157 + src/backend/index.ts | 18 +- src/backend/internal/media/media.test.ts | 546 +++ src/backend/internal/media/model.ts | 446 +++ src/backend/internal/media/ratelimit.test.ts | 76 + src/backend/internal/media/ratelimit.ts | 178 + src/backend/internal/media/scan.ts | 1025 ++++++ src/backend/internal/media/scrape.ts | 900 +++++ src/backend/internal/media/store.ts | 1245 +++++++ .../internal/media/store_persist.test.ts | 263 ++ src/backend/internal/media/strm.ts | 333 ++ src/backend/internal/media/tag.ts | 1349 +++++++ src/backend/internal/media/thumb.ts | 134 + src/backend/internal/model/db.ts | 214 +- src/backend/internal/model/store/json.ts | 60 +- src/backend/internal/model/transcode-guard.ts | 81 + src/backend/internal/op/storage.ts | 185 + src/backend/pkg/sign.ts | 18 +- src/backend/server/admin.ts | 17 + src/backend/server/emby.ts | 3162 +++++++++++++++++ src/backend/server/emby_protocol.test.ts | 335 ++ src/backend/server/media.ts | 1594 +++++++++ src/backend/server/raw.ts | 30 +- src/backend/server/router.ts | 36 +- 24 files changed, 12378 insertions(+), 24 deletions(-) create mode 100644 docs/pr-media-library-parity.md create mode 100644 src/backend/internal/media/media.test.ts create mode 100644 src/backend/internal/media/model.ts create mode 100644 src/backend/internal/media/ratelimit.test.ts create mode 100644 src/backend/internal/media/ratelimit.ts create mode 100644 src/backend/internal/media/scan.ts create mode 100644 src/backend/internal/media/scrape.ts create mode 100644 src/backend/internal/media/store.ts create mode 100644 src/backend/internal/media/store_persist.test.ts create mode 100644 src/backend/internal/media/strm.ts create mode 100644 src/backend/internal/media/tag.ts create mode 100644 src/backend/internal/media/thumb.ts create mode 100644 src/backend/internal/model/transcode-guard.ts create mode 100644 src/backend/server/emby.ts create mode 100644 src/backend/server/emby_protocol.test.ts create mode 100644 src/backend/server/media.ts diff --git a/docs/pr-media-library-parity.md b/docs/pr-media-library-parity.md new file mode 100644 index 00000000..af211db5 --- /dev/null +++ b/docs/pr-media-library-parity.md @@ -0,0 +1,157 @@ + + +`feat(media): complete media library, Emby and Subsonic parity with Go backend` + +## Summary / 摘要 + +本 PR 把 Worker 版媒体库从「最小可跑闭环」补齐到与 Go 版 OpenList(`OpenList-Backends` 的 `dev-media`)功能对齐,覆盖扫描、刮削、媒体 API、Emby、Subsonic 五条链路,并同步了 Go 侧最近的协议与安全修复。 + +**问题背景**:此前的实现(`3638ad2`、`c2e4a6b`)只做到「能扫、能看、能播」: + +- 音乐不解析 ID3/Vorbis,`album_artist` 恒为空,专辑/曲序/封面/歌词全部缺失; +- 缩略图接口恒 404,图片类型完全不刮削; +- 刮削全串行、每次重刮全库,没有 `scraped_at` 判据; +- `delete_invalid` 是空壳(只删隐藏项),导入不恢复扫描路径; +- 媒体库数据存在单个 JSON 文档里,一次播放进度上报就要全量重写; +- Emby 缺 Season 层级、`Sessions/Playing*` 不落进度、`PlaybackInfo` 返回本地文件路径导致客户端播放失败; +- Subsonic 只有约 18 个方法且恒返回 JSON,播放列表恒空。 + +## 用户可感知的行为变化 + +### 媒体库 + +- **音乐库可用**:扫描时读取 ID3v2.2/2.3/2.4、ID3v1、FLAC Vorbis Comment、M4A `ilst`,填充标题/艺术家/专辑/曲序/年份/流派/内嵌封面;优先读取同目录 `.lrc`,回退内嵌歌词。 +- **扫描路径支持 `path_merge` 与 `enable_scrape`**:目录合并模式由每个扫描路径独立控制,不再对「视频目录 ≥2 个文件」硬编码聚合。 +- **剧集集号/季号按真实文件名解析**:支持 `SxxExx`、`EPxx`、`第N集`(含中文数字)、开头数字;`1080p` 之类不再被误判为集号。 +- **图片类型可刮削**:按文件头魔数校验真实格式(伪造扩展名的非图片文件不入库),并把原图缓存为缩略图。 +- **书籍封面**:新增 EPUB / CBZ / **PDF** 内嵌封面提取(PDF 支持 DCTDecode 直出与 FlateDecode → PNG 转码)。 +- **刮削并发**(默认 5,可配置)、只刮未刮削条目;TMDB 改为多候选 + 相似度打分 + 年份/类型加权,支持剧集单集信息。 +- **进度语义对齐 Go**:播放到 90% 自动标记已看;重新播放(<5s)清除已看;记录 `last_played_at`。 +- **收藏接口只返回条目级收藏**(排除选集收藏)。 +- **`delete_invalid` 真实探测文件是否存在**,只删除确认不存在的记录;导入/导出支持 `scan_path` 范围与扫描路径 id 重映射。 +- **STRM**:支持内容模板、覆盖开关、扫描后自动生成;输出 `Movie (Year)/`、`Series/Season 01/SxxExx`、`Artist/Album/` 目录结构。 +- **普通用户可自助签发媒体应用密钥**:`/api/auth/me/appkey/*`(此前仅管理员可用)。 + +### Emby + +- **`PlaybackInfo` 修复**:`Path` 与 `DirectStreamUrl` 改为带 `api_key` 的 HTTP 流地址(不再是本地文件路径),修复客户端 `Playback of this content is currently restricted` / 一直转圈。 +- **封面端点允许匿名访问**(客户端图片 URL 不带 token),支持 `thumb:` / `data:` / 远程 URL / VFS 四种来源。 +- **Series → Season → Episode 三层结构**,剧集按季归档。 +- **`Sessions/Playing`、`Playing/Progress`、`Playing/Stopped` 真正落播放进度**,停止时按 90% 标记已看。 +- 新增 FavoriteItems / PlayedItems / UserData 读写、`/Audio/:id/lyrics`(LRC 转 SRT 作为外挂字幕轨)。 +- 补齐 Views / Items / Counts / Latest / Resume / Root / Shows / Artists / Search Hints / Library / DisplayPreferences 等端点。 +- **Emby WebSocket**:Cloudflare Workers 下提供 `/@emby/embywebsocket`(`WebSocketPair` + `upgradeWebSocket`,30s 心跳);其它运行时返回 426。 + +### Subsonic + +- **默认输出 XML**,支持 `f=json` / `jsonp`,支持 POST 表单(formPost)。 +- 方法覆盖从约 18 个扩展到约 35 个:`getIndexes`、`getMusicDirectory`、`getRandomSongs`、`getSongsByGenre`、`search2/3`、`getLyrics`、`getLyricsBySongId`、`setRating`、`scrobble`、播放列表 CRUD、`savePlayQueue/getPlayQueue`、`getGenres`、`getUsers`、`tokeninfo`、`startscan` 等。 +- 播放列表、播放队列、评分按要求持久化。 +- `media_subsonic_proxy_stream`、`media_subsonic_allow_password_auth`(默认关闭,对齐 Go)生效。 +- 认证失败限流:IP + 用户名双维度,只有「带凭据但校验失败」才计数,锁定窗口不续期。 + +### 安全 + +- 封面代理改为**逐跳**重定向校验 + 域名白名单 + 图片魔数嗅探 + `nosniff` / CSP 响应头(此前只校验首跳并信任 `Content-Type`)。 +- 媒体类型开关纳入条目级权限校验(`/fs/media/item/:id` 不再能读取已关闭类型的条目)。 +- Emby / Subsonic 认证入口增加失败限流;应用密钥明文改为 AES-GCM 加密存储(旧 `plain` 字段仍兼容读取)。 + +### 转码 + +- **Worker 明确不支持转码**,设置被三层屏蔽:`/admin/setting/list` 过滤全部 `transcode_*`、`/admin/setting/save` 拒绝写入并强制落成 `false`、`getSettings()` 读取时强制关闭。 +- `/api/fs/transcode/play` 返回 `transcode:false, supported:false, reason`,前端平滑降级直链。 + +## 重要实现变化 + +| 模块 | 变更 | +|---|---| +| `internal/media/model.ts` | 对齐 Go 字段:`mime_type / reviews / lyrics / video_type / is_folder / scraped_at`、`scan_path.path_merge / enable_scrape / last_scan_at`、`config.scraper_config / last_*_at`、`userdata.rating / last_played_at`、`MediaPlaylist / MediaPlaylistItem / MediaPlayQueue` | +| `internal/media/store.ts` | 单 JSON 文档改为分片持久化:`items` 按 `scan_path_id`、`user_data` 按 `user_id`、缩略图按 hash 分键;写入前按分片 diff,只写变化部分;首次读取自动迁移旧数据 | +| `internal/media/tag.ts`(新增) | ID3v2/Vorbis/M4A/LRC/EXIF/ZIP/PDF 解析,图片魔数嗅探 | +| `internal/media/thumb.ts`(新增) | 封面落盘(`thumb:`)、base64 内联、URL 解析、类型级 `scraper_config` 覆盖 | +| `internal/media/strm.ts`(新增) | STRM 目标计算、模板渲染、批量生成、扫描后自动生成 | +| `internal/media/ratelimit.ts`(新增) | Emby/Subsonic 认证限流(内存 + 持久化锁定) | +| `internal/model/transcode-guard.ts`(新增) | `transcode_*` 设置识别、过滤、强制禁用 | +| `internal/op/storage.ts` | 新增 `readFileRange()`:Range 请求、上游忽略 Range 时流式跳过、32MB 上限 | +| `server/media.ts` | 重写:进度语义、收藏过滤、`delete_invalid` 真实探测、导入导出 scope、`/me/appkey`、STRM、封面代理加固 | +| `server/emby.ts` | 重写:Emby 完整端点 + Season 层级 + Sessions 进度 + WebSocket;Subsonic XML/JSON/JSONP + 方法补齐 | +| `server/admin.ts` | 设置列表/保存接入转码守卫 | +| `server/router.ts` | 注册 `/auth/me/appkey`;转码请求明确 `supported:false` | +| `internal/model/db.ts` | 新增 `media_subsonic_allow_password_auth`、`media_cover_gateway`;`getSettings()` 接入转码守卫 | + +## 兼容性 / 迁移 + +- **存储格式**:媒体库 meta 首次读取时自动把旧的单文档 `items` / `user_data` 拆分写入新分片键,无需人工迁移;旧键 `openlist_media_db_v1` 继续作为 meta 使用。 +- **前端接口**:现有字段名不变,`cover` 字段可能返回 `data:`、`/api/public/media/thumb?path=...` 或远程 URL,前端按普通图片 URL 处理即可。 +- **Emby / Subsonic**:ID 规则保持 `mv- / so- / ep- / sr- / sn- / al- / ar-`,并额外提供稳定的数字 ID 映射;旧客户端缓存的内嵌 ID 仍可解析。 +- **行为变更**:`media_subsonic_allow_password_auth` 默认 **false**(对齐 Go),使用 `u+p` 的客户端需要改用 `apiKey` 或 `u+t+s`(应用密钥)认证;管理员可在设置中显式开启。 +- **行为变更**:转码设置在 Worker 上不可见、不可写、不可启用。 + +## 已知限制(Worker 运行时) + +- 无 ffmpeg:转码按上述方式屏蔽,播放降级直链。 +- 无 sharp/canvas:缩略图为原图缓存(≤512KB),不做缩放。 +- Emby WebSocket 仅 Cloudflare Workers 可用;Node/EdgeOne 返回 426。 +- 扫描在请求内执行,受 CPU/时长约束:单路径最多 20000 条、深度 12。 +- PDF 封面只支持 `DCTDecode` 与 `FlateDecode`(含 ASCII85 组合),`JPXDecode` / `JBIG2Decode` 跳过。 + +## Testing / 测试 + +```bash +# 类型检查 +node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit + +# 媒体库 + 协议单测 +node node_modules/tsx/dist/cli.mjs --test \ + src/backend/internal/media/media.test.ts \ + src/backend/server/emby_protocol.test.ts + +# 回归(路由注册 / 公开设置) +node node_modules/tsx/dist/cli.mjs --test src/backend/server/index_cdn.test.ts +node node_modules/tsx/dist/cli.mjs --test src/backend/server/public_settings.test.ts +``` + +结果: + +- `tsc --noEmit`:0 error +- 媒体 + 协议单测:**24 passed / 0 failed**,覆盖扩展名映射、查询过滤、多用户收藏隔离、集号/季号解析(含中文数字)、ID3v2、FLAC Vorbis、图片魔数、播放列表、PDF 封面(JPEG 直出 / Flate→PNG)、转码守卫、STRM 布局、Emby 公共端点、Subsonic XML/JSON/JSONP、XML 转义、数字 ID 稳定性 +- `index_cdn.test.ts`(15)、`public_settings.test.ts`(7):全部通过,无回归 + +手动验证建议: + +- 扫描一个含 MP3/FLAC 的目录,确认专辑/艺术家/曲序/封面/歌词 +- 扫描带 `SxxExx` 的剧集目录,确认 Emby 中 Season/Episode 层级 +- 用 Infuse / VidHub / Feishin / Symfonium 各连接一次,确认浏览、封面、播放、进度同步 +- 在管理端尝试保存 `transcode_enabled=true`,确认列表不显示、保存后值仍为 `false` + +## Checklist / 检查清单 + +- [x] I have read [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md). +- [x] I confirm this contribution follows the repository license, contribution policy, and code of conduct. +- [x] I have formatted the changed code with `prettier` where applicable. +- [ ] I have requested review from relevant maintainers or code owners where applicable. + +## AI Disclosure / AI 使用声明 + +- [x] This PR includes AI-assisted content. + +Tools used / 使用工具: + +- [x] Other (please specify) / 其他(请注明): CodeBuddy + +Usage scope / 使用范围: + +- [x] Code generation / 代码生成 +- [x] Refactoring / 重构 +- [x] Documentation / 文档 +- [x] Tests / 测试 +- [x] Review assistance / 审查辅助 + +- [x] I have reviewed and validated all AI-assisted content included in this PR. +- [x] I can reproduce all AI-assisted content included in this PR without any AI tools. diff --git a/src/backend/index.ts b/src/backend/index.ts index efdec733..453189e3 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -1,10 +1,16 @@ import { Hono } from "hono" import { setupRouter } from "./server/router" import { rawRouter } from "./server/raw" -import { assetsRouter, getIndexHtmlWithCdn, isCdnConfigured, cdnAssetRedirect } from "./server/assets" +import { + assetsRouter, + getIndexHtmlWithCdn, + isCdnConfigured, + cdnAssetRedirect, +} from "./server/assets" import { webdavRouter } from "./server/webdav" import { s3Router } from "./server/s3" import { setEnvCtx } from "./internal/model/db" +import { embyRouter, subsonicRouter } from "./server/emby" import { getStoreConfigErrorDetail } from "./internal/model/store/backend" import { storageErrorSummary, uiStorageError } from "./server/storage-error" @@ -14,7 +20,11 @@ const app = new Hono() * 静态资源 / SPA 壳路径:这些请求不应被存储配置错误拦截, * 否则前端连提示页面都加载不出来。 */ -function isStaticOrShell(pathname: string, accept: string, method: string): boolean { +function isStaticOrShell( + pathname: string, + accept: string, + method: string, +): boolean { // 带扩展名的静态文件 if (/\.[a-zA-Z0-9]+$/.test(pathname)) return true // 浏览器导航请求(HTML)由 SPA 壳承载 @@ -163,6 +173,10 @@ app.route("/d", rawRouter) app.route("/sd", rawRouter) app.route("/p", rawRouter) +// 媒体库协议端点:/@emby 与 /@rest,避免与用户目录冲突 +app.route("/@emby", embyRouter) +app.route("/@rest", subsonicRouter) + // 内嵌品牌资源(logo/favicon),必须在 SPA 兜底 app.all("*") 之前挂载 app.route("/", assetsRouter) diff --git a/src/backend/internal/media/media.test.ts b/src/backend/internal/media/media.test.ts new file mode 100644 index 00000000..dc4d33d8 --- /dev/null +++ b/src/backend/internal/media/media.test.ts @@ -0,0 +1,546 @@ +import assert from "node:assert/strict" +import test from "node:test" + +import { + emptyMediaDb, + extOf, + isBookFile, + isImageFile, + isMusicFile, + isVideoFile, + mediaTypeOfFile, + type MediaItem, +} from "./model" +import { + createPlaylist, + ensureUserData, + getUserData, + listFavorites, + nextId, + playlistItems, + queryItems, + replacePlaylistItems, + upsertItem, +} from "./store" +import { + extractPdfCover, + parseFLAC, + parseID3v2, + sniffImageMime, +} from "./tag" +import { + disableTranscodeSettings, + isTranscodeSettingKey, + stripTranscodeSettings, +} from "../model/transcode-guard" +import { buildStrmTargets, withinOutRoot } from "./strm" +import { emptyCover, persistCover } from "./thumb" +import { __scanTest } from "./scan" +import { parseMediaTitle } from "./scrape" + +test("media extension mapping", () => { + assert.equal(extOf("movie.2024.MKV"), "mkv") + assert.equal(mediaTypeOfFile("movie.mkv"), "video") + assert.equal(mediaTypeOfFile("song.flac"), "music") + assert.equal(mediaTypeOfFile("cover.webp"), "image") + assert.equal(mediaTypeOfFile("book.epub"), "book") + assert.equal(mediaTypeOfFile("note.txt"), "book") + assert.equal(mediaTypeOfFile("archive.zip"), null) + assert.ok(isVideoFile("a.mp4")) + assert.ok(isMusicFile("a.mp3")) + assert.ok(isImageFile("a.png")) + assert.ok(isBookFile("a.pdf")) +}) + +test("query items filters type, keyword and path prefix", () => { + const db = emptyMediaDb() + const base = { + scan_path_id: 1, + scraped_name: "", + cover: "", + authors: "", + plot: "", + description: "", + genre: "", + release_date: "", + rating: 0, + file_size: 100, + duration: 0, + season: 0, + episode: 0, + episodes: "", + album_name: "", + album_artist: "", + track_number: 0, + publisher: "", + isbn: "", + external_id: "", + hidden: false, + scraper_source: "", + } + upsertItem(db, { + ...base, + media_type: "video", + folder_path: "/movies", + file_name: "Inception.mkv", + scraped_name: "盗梦空间", + }) + upsertItem(db, { + ...base, + media_type: "video", + folder_path: "/private", + file_name: "Secret.mkv", + scraped_name: "秘密", + }) + assert.equal(queryItems(db, { media_type: "video" }).total, 2) + assert.equal(queryItems(db, { media_type: "video", keyword: "盗梦" }).total, 1) + assert.equal( + queryItems(db, { media_type: "video", path_prefix: "/movies" }).total, + 1, + ) + assert.equal(queryItems(db, { ids: [1] }).content[0].file_name, "Inception.mkv") +}) + +test("favorites and progress are isolated per user", () => { + const db = emptyMediaDb() + const item = upsertItem(db, { + media_type: "video", + scan_path_id: 1, + folder_path: "/movies", + file_name: "a.mkv", + scraped_name: "A", + cover: "", + authors: "", + plot: "", + description: "", + genre: "", + release_date: "", + rating: 0, + file_size: 0, + duration: 0, + season: 0, + episode: 0, + episodes: "", + album_name: "", + album_artist: "", + track_number: 0, + publisher: "", + isbn: "", + external_id: "", + hidden: false, + scraper_source: "", + }) + const row = ensureUserData(db, 1, item.id) + row.is_favorite = true + row.position_ticks = 120 + ensureUserData(db, 2, item.id) + assert.equal(getUserData(db, 1, item.id)?.is_favorite, true) + assert.equal(getUserData(db, 2, item.id)?.is_favorite, false) + assert.equal(listFavorites(db, 1).length, 1) + assert.equal(listFavorites(db, 2).length, 0) +}) + +test("nextId never reuses ids", () => { + const db = emptyMediaDb() + assert.equal(nextId(db), 1) + assert.equal(nextId(db), 2) + assert.equal(db.next_id, 3) +}) + +test("episode / season parsing", () => { + const { parseEpisodeInfo, parseSeasonFromPath } = __scanTest + const se = parseEpisodeInfo("My.Show.S02E05.1080p.mkv", 1) + assert.equal(se.season, 2) + assert.equal(se.index, 5) + assert.equal(se.file_name, "My.Show.S02E05.1080p.mkv") + + const ep = parseEpisodeInfo("EP07.mp4", 1) + assert.equal(ep.index, 7) + + const cn = parseEpisodeInfo("第三集.mp4", 1) + assert.equal(cn.index, 3) + + const lead = parseEpisodeInfo("08 - Pilot.mkv", 1) + assert.equal(lead.index, 8) + assert.equal(lead.title, "Pilot") + + // 分辨率不能被误判为集号 + const plain = parseEpisodeInfo("1080p.mkv", 9) + assert.equal(plain.index, 9) + + assert.equal(parseSeasonFromPath("/tv/Show/Season 03"), 3) + assert.equal(parseSeasonFromPath("/tv/Show/S04"), 4) + assert.equal(parseSeasonFromPath("/tv/Show"), 1) +}) + +test("ID3v2 text frames", () => { + const syncsafe = (n: number) => [ + (n >> 21) & 0x7f, + (n >> 14) & 0x7f, + (n >> 7) & 0x7f, + n & 0x7f, + ] + const utf8 = (s: string) => Array.from(new TextEncoder().encode(s)) + const frame = (id: string, text: string) => { + const body = [3, ...utf8(text)] + const size = body.length + return [ + ...utf8(id), + (size >> 24) & 0xff, + (size >> 16) & 0xff, + (size >> 8) & 0xff, + size & 0xff, + 0, + 0, + ...body, + ] + } + const body = [ + ...frame("TIT2", "Hello"), + ...frame("TPE1", "World"), + ...frame("TALB", "AlbumX"), + ...frame("TRCK", "3/12"), + ...frame("TYER", "2021"), + ] + const tag = new Uint8Array([ + 0x49, + 0x44, + 0x33, + 3, + 0, + 0, + ...syncsafe(body.length), + ...body, + ]) + const parsed = parseID3v2(tag) + assert.ok(parsed) + assert.equal(parsed!.title, "Hello") + assert.equal(parsed!.artist, "World") + assert.equal(parsed!.album, "AlbumX") + assert.equal(parsed!.trackNumber, 3) + assert.equal(parsed!.year, "2021") +}) + +test("FLAC vorbis comment", () => { + const le32 = (n: number) => [ + n & 0xff, + (n >> 8) & 0xff, + (n >> 16) & 0xff, + (n >> 24) & 0xff, + ] + const utf8 = (s: string) => Array.from(new TextEncoder().encode(s)) + const entries = [ + "TITLE=Song", + "ARTIST=Singer", + "ALBUM=Record", + "TRACKNUMBER=7", + "DATE=2019-05-01", + "GENRE=Rock", + ] + const vendor = utf8("openlist") + const body = [ + ...le32(vendor.length), + ...vendor, + ...le32(entries.length), + ...entries.flatMap((e) => [...le32(e.length), ...utf8(e)]), + ] + // metadata block 头部的 24 位长度按规范是大端(载荷内部长度才是小端) + const header = [ + 0x84, // last + type 4 + (body.length >> 16) & 0xff, + (body.length >> 8) & 0xff, + body.length & 0xff, + ] + const flac = new Uint8Array([...utf8("fLaC"), ...header, ...body]) + const tag = parseFLAC(flac) + assert.ok(tag) + assert.equal(tag!.title, "Song") + assert.equal(tag!.artist, "Singer") + assert.equal(tag!.album, "Record") + assert.equal(tag!.albumArtist, "") + assert.equal(tag!.trackNumber, 7) + assert.equal(tag!.year, "2019") + assert.equal(tag!.genre, "Rock") +}) + +test("image mime sniffing rejects non-images", () => { + const jpeg = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0, 0, 0, 0, 0, 0, 0]) + assert.equal(sniffImageMime(jpeg), "image/jpeg") + const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0, 0, 0, 0, 0, 0, 0, 0]) + assert.equal(sniffImageMime(png), "image/png") + const html = new TextEncoder().encode("([\d.]+)<\/span>/i)?.[1] + const info = html.match(/
([\s\S]*?)<\/div>/i)?.[1] + const abstract = html.match(/

([\s\S]*?)<\/p>/i)?.[1] + if (!title && !cover) return false + + if (title) item.scraped_name = stripTags(title) || item.scraped_name + if (cover) item.cover = cover.replace(/\/s\//, "/l/") + if (rating) item.rating = Number(rating) || item.rating + if (abstract) { + const text = stripTags(abstract) + item.plot = text || item.plot + item.description = text || item.description + } + if (info) { + const text = stripTags(info) + // 豆瓣格式:作者: xxx / 出版社: xxx / 出版年: 2020-1 / ISBN: xxx + const part = (label: string) => { + const m = text.match(new RegExp(`${label}[::]\\s*([^/]+)`)) + return m ? m[1].trim() : "" + } + const author = part("作者") + if (author) item.authors = author + const publisher = part("出版社") + if (publisher) item.publisher = publisher + // 豆瓣电影条目的字段是「导演 / 主演 / 年份 / 类型」,与书籍不同 + const director = part("导演") + const actors = part("主演") + const cast = [director, actors].filter(Boolean).join(",").trim() + if (cast) item.authors = cast + const genre = part("类型") + if (genre) item.genre = genre.replace(/\s*\/\s*/g, ",") + const year = part("出版年") || part("年份") + const ym = year.match(/(\d{4})/) + if (ym) item.release_date = `${ym[1]}-01-01` + const isbn = part("ISBN") + if (isbn) item.isbn = isbn + } + item.scraper_source = "douban" + return true +} + +/** 图片:原图缓存 + EXIF 拍摄日期(无法缩放,语义上等同 runtime 缩略图) */ +async function scrapeImage(ctx: ScrapeContext, item: MediaItem): Promise { + const path = `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + const head = await readFileRange(path, 0, 256 * 1024, { env: ctx.env }) + if (!head || head.length === 0) return false + const mime = sniffImageMime(head) + if (!mime) return false + + item.mime_type = mime + if (!item.cover && item.file_size > 0 && item.file_size <= 8 * 1024 * 1024) { + const full = await readFileRange(path, 0, item.file_size, { env: ctx.env }) + if (full) { + const stored = await persistCover(ctx.env, full, ctx.thumb) + if (stored.cover) { + item.cover = stored.cover + item.thumb = stored.thumb + item.thumb_mime = stored.thumb_mime + } + } + } + if (!item.release_date) { + const exif = parseJpegExifDate(head) + if (exif) item.release_date = exif + } + item.scraper_source = item.scraper_source || "local" + return true +} + +/** 音乐:ID3/Vorbis + .lrc 本地兜底(只填空字段) */ +async function fillMusicFromLocal(ctx: ScrapeContext, item: MediaItem) { + const folder = item.folder_path.replace(/\/+$/, "") || "/" + let audioPath = `${folder}/${item.file_name}` + if (item.is_folder) { + try { + const eps = JSON.parse(item.episodes || "[]") + if (Array.isArray(eps) && eps[0]?.file_name) { + audioPath = `${folder}/${item.file_name}/${eps[0].file_name}` + } + } catch { + /* ignore */ + } + } + const ext = extOf(audioPath) + const head = await readFileRange(audioPath, 0, 2 * 1024 * 1024, { + env: ctx.env, + }) + const size = item.file_size || 0 + let tail: Uint8Array | null = null + if (size > 2 * 1024 * 1024) { + tail = await readFileRange(audioPath, size - 128, 128, { env: ctx.env }) + } else if (head && head.length >= 128) { + tail = head.subarray(head.length - 128) + } + const tag = parseMusicTags(ext, head, tail) + + if (!item.album_name && tag.album) item.album_name = tag.album + if (!item.album_artist) { + item.album_artist = tag.albumArtist || tag.artist || "" + } + if (!item.scraped_name && tag.title) item.scraped_name = tag.title + if (!item.track_number && tag.trackNumber) item.track_number = tag.trackNumber + if (!item.release_date && tag.year) item.release_date = `${tag.year}-01-01` + if (!item.genre && tag.genre) item.genre = tag.genre + if (!item.authors && tag.artist) item.authors = JSON.stringify([tag.artist]) + if (!item.duration && tag.duration) item.duration = tag.duration + if (!item.cover && tag.coverData) { + const stored = await persistCover(ctx.env, tag.coverData, ctx.thumb) + if (stored.cover) { + item.cover = stored.cover + item.thumb = stored.thumb + item.thumb_mime = stored.thumb_mime + } + } + + if (!item.lyrics) { + const lrcPath = audioPath.replace(/\.[^.]+$/, ".lrc") + const lrc = await readFileRange(lrcPath, 0, 512 * 1024, { env: ctx.env }) + if (lrc && lrc.length > 0) { + try { + item.lyrics = normalizeLyrics(new TextDecoder().decode(lrc)) + } catch { + /* ignore */ + } + } + if (!item.lyrics && tag.lyrics) item.lyrics = normalizeLyrics(tag.lyrics) + } + return tag +} + +/** 书籍本地封面:EPUB / CBZ / PDF */ +async function fillBookCoverFromLocal(ctx: ScrapeContext, item: MediaItem) { + if (item.cover) return + const ext = extOf(item.file_name) + if (ext !== "epub" && ext !== "cbz" && ext !== "pdf") return + const path = `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + const data = await readFileRange(path, 0, 16 * 1024 * 1024, { env: ctx.env }) + if (!data) return + const found = + ext === "epub" + ? await extractEpubCover(data) + : ext === "cbz" + ? await extractCbzCover(data) + : await extractPdfCover(data) + if (!found) return + const stored = await persistCover(ctx.env, found.data, ctx.thumb) + if (stored.cover) { + item.cover = stored.cover + item.thumb = stored.thumb + item.thumb_mime = stored.thumb_mime + } +} + +/** 远程封面下载到本地(media_download_cover) */ +async function maybeDownloadCover(ctx: ScrapeContext, item: MediaItem) { + if (!settingBool(ctx.settings, "media_download_cover", false)) return + if (!/^https?:\/\//i.test(item.cover || "")) return + try { + const res = await fetch(item.cover, { + headers: { "User-Agent": "OpenList-Media/1.0" }, + signal: AbortSignal.timeout(20_000), + }) + if (!res.ok) return + const buf = new Uint8Array(await res.arrayBuffer()) + if (buf.length === 0 || buf.length > 8 * 1024 * 1024) return + const mime = sniffImageMime(buf) + if (!mime) return + const stored = await persistCover(ctx.env, buf, ctx.thumb) + if (stored.cover) { + item.cover = stored.cover + item.thumb = stored.thumb + item.thumb_mime = stored.thumb_mime + } + } catch (e: any) { + console.warn("[media] download cover failed:", e?.message || e) + } +} + +export interface ScrapeOutcome { + ok: boolean + low: boolean +} + +export async function scrapeItem( + ctx: ScrapeContext, + item: MediaItem, +): Promise { + try { + if (item.media_type === "video") { + const res = await scrapeTmdb(ctx, item) + if (res.ok) { + await maybeDownloadCover(ctx, item) + return res + } + // TMDB 没命中(未配 key、中文片名、冷门片)时用豆瓣兜底: + // Go 的视频刮削同样是 TMDB + 豆瓣双源,只挂 TMDB 会导致国产片全灭。 + try { + const ok = await scrapeDouban(ctx, item) + if (ok) { + await maybeDownloadCover(ctx, item) + return { ok: true, low: true } + } + } catch (e: any) { + console.warn("[media] douban fallback failed:", e?.message || e) + } + return { ok: false, low: false } + } + + if (item.media_type === "music") { + const tag = await fillMusicFromLocal(ctx, item) + let ok = false + try { + ok = await scrapeDiscogs(ctx, item) + } catch (e: any) { + console.warn("[media] discogs failed:", e?.message || e) + } + await maybeDownloadCover(ctx, item) + const localHit = !!(tag.album || tag.artist || tag.title) + return { ok: ok || localHit, low: !ok && localHit } + } + + if (item.media_type === "book") { + const source = (ctx.settings.media_book_source || "douban").toLowerCase() + let ok = false + try { + if (source === "google") ok = await scrapeGoogleBooks(ctx, item) + else if (source === "openlibrary") ok = await scrapeOpenLibrary(ctx, item) + else ok = await scrapeDouban(ctx, item) + if (!ok && source !== "google") ok = await scrapeGoogleBooks(ctx, item) + if (!ok && source !== "openlibrary") ok = await scrapeOpenLibrary(ctx, item) + } catch (e: any) { + console.warn("[media] book scrape failed:", e?.message || e) + } + if (ok) await maybeDownloadCover(ctx, item) + await fillBookCoverFromLocal(ctx, item) + return { ok: ok || !!item.cover, low: !ok && !!item.cover } + } + + if (item.media_type === "image") { + return { ok: await scrapeImage(ctx, item), low: false } + } + } catch (e: any) { + console.warn(`[media] scrape item ${item.id} failed:`, e?.message || e) + } + return { ok: false, low: false } +} + +export interface ScrapeResult { + scraped: number + total: number + hit: number + low_confidence: number + no_match: number +} + +/** 简单并发池 */ +async function runPool( + items: T[], + concurrency: number, + worker: (item: T, index: number) => Promise, +) { + let cursor = 0 + const size = Math.max(1, Math.min(concurrency, items.length || 1)) + const runners = Array.from({ length: size }, async () => { + for (;;) { + const idx = cursor++ + if (idx >= items.length) return + try { + await worker(items[idx], idx) + } catch (e: any) { + console.warn("[media] scrape worker failed:", e?.message || e) + } + } + }) + await Promise.all(runners) +} + +export async function scrapeMedia( + env: any, + settings: Record, + type: MediaType, + itemId = 0, +): Promise { + const db = await getMediaDb(env) + const cfg = db.configs.find((c) => c.media_type === type) + const concurrency = Math.max( + 1, + Math.min( + 10, + Number(cfg?.scrape_concurrency) || + Number(settings.media_scrape_concurrency) || + 5, + ), + ) + const ctx: ScrapeContext = { + env, + settings, + thumb: thumbOptionsOf(cfg), + } + + const stats: ScrapeStats = { + ...(statsMap.get(type) || emptyStats(type)), + media_type: type, + running: true, + last_error: "", + started_at: new Date().toISOString(), + finished_at: "", + } + statsMap.set(type, stats) + + let list = db.items.filter((i) => i.media_type === type) + if (itemId) { + list = list.filter((i) => i.id === itemId) + } else { + // 只刮未刮削的条目(对齐 Go 的 GetUnscrappedItems) + list = list.filter((i) => !i.scraped_at) + } + + const pathsOfType = db.scan_paths.filter((p) => p.media_type === type) + const enabledPaths = new Set( + pathsOfType.filter((p) => p.enable_scrape !== false).map((p) => p.id), + ) + // 只有「完全没有配置扫描路径」才不做限制;只要配置了路径, + // 显式关闭刮削的路径就必须排除。旧实现把「空集」当作放行, + // 于是把唯一路径的 enable_scrape 关掉反而触发整库刮削。 + list = list.filter( + (i) => pathsOfType.length === 0 || enabledPaths.has(i.scan_path_id), + ) + + let scraped = 0 + let hit = 0 + let low = 0 + let noMatch = 0 + let sinceSave = 0 + + try { + await runPool(list, concurrency, async (item) => { + stats.total_scraped += 1 + const outcome = await scrapeItem(ctx, item) + if (outcome.ok) { + item.updated_at = new Date().toISOString() + // 本地兜底(ID3/lrc/EPUB 封面)不写 scraped_at,便于下次继续尝试远端刮削 + if (!outcome.low) item.scraped_at = new Date().toISOString() + scraped += 1 + hit += 1 + if (outcome.low) low += 1 + } else { + noMatch += 1 + // 本地兜底已经写入了内容:保留数据但不标记 scraped_at + if (item.cover || item.lyrics || item.album_name || item.album_artist) { + item.updated_at = new Date().toISOString() + } + } + sinceSave += 1 + if (sinceSave >= 20) { + sinceSave = 0 + await saveMediaDb(env, db) + } + }) + + if (cfg) cfg.last_scrape_at = new Date().toISOString() + await saveMediaDb(env, db) + + return { + scraped, + total: list.length, + hit, + low_confidence: low, + no_match: noMatch, + } + } finally { + // 保存失败(saveMediaDb 会抛错)也必须清掉运行态,否则该类型会永远 + // 卡在 "scrape already running",存储恢复后也无法重试。 + stats.running = false + stats.hit_count = stats.hit_count + hit + stats.low_confidence_count = stats.low_confidence_count + low + stats.no_match_count = stats.no_match_count + noMatch + stats.finished_at = new Date().toISOString() + } +} diff --git a/src/backend/internal/media/store.ts b/src/backend/internal/media/store.ts new file mode 100644 index 00000000..160d51d5 --- /dev/null +++ b/src/backend/internal/media/store.ts @@ -0,0 +1,1245 @@ +/** + * 媒体库持久化(分片版)。 + * + * 历史实现把整个媒体库塞进单个 JSON secret:任何一次播放进度上报都会重写 + * 全部条目。条目上万、或多 isolate 并发时会互相覆盖,且容易撞上 KV 单键 + * 写入频率限制。这里改为分片: + * + * openlist_media_db_v1 meta(config/scan_path/appkey/device/playlist…) + * openlist_media_items_v1: items(按 scan_path_id 分片) + * openlist_media_ud_v1: user_data(按 user_id 分片) + * openlist_media_thumb_v1: 缩略图/封面字节(base64) + * + * 读取时按 meta 里的 shard 索引聚合,仍向业务层暴露「完整 MediaDb」, + * 因此上层代码无需感知分片;写入时按内容比较只写发生变化的分片。 + * + * 兼容:首次读取时若 meta 里还带着旧版 items/user_data(单文档格式), + * 自动拆分落盘并清理旧字段,无需人工迁移。 + */ + +import { + listPersistedSecrets, + readPersistedSecret, + readPersistedSecretStrict, + writePersistedSecret, +} from "../model/store/json" +import { + DEFAULT_CONFIG, + emptyMediaDb, + MEDIA_DB_VERSION, + MEDIA_TYPES, + type MediaConfig, + type MediaDb, + type MediaItem, + type MediaPlaylist, + type MediaPlaylistItem, + type MediaScanPath, + type MediaType, + type MediaUserData, +} from "./model" + +const META_KEY = "openlist_media_db_v1" +const ITEMS_PREFIX = "openlist_media_items_v1:" +const USERDATA_PREFIX = "openlist_media_ud_v1:" +const THUMB_PREFIX = "openlist_media_thumb_v1:" + +/** 内存缓存 TTL:缩短跨 isolate 的陈旧窗口(写操作会立即刷新本地缓存) */ +const CACHE_TTL_MS = 5_000 + +/** + * 单条缩略图上限,防止把几十 MB 的 base64 写进 KV。 + * + * 512KB 太小:FLAC/MP3 内嵌封面动辄 0.6~1.2MB,超限会被静默丢弃, + * 表现就是「有的歌有封面、有的没有」(同一批文件里只有小封面能存下来)。 + * 缩略图按内容 hash 去重,同一张专辑封面只存一份,2MB 的上限实际占用 + * 远小于「曲目数 × 2MB」。 + */ +export const MAX_THUMB_BYTES = 2 * 1024 * 1024 + +interface CacheState { + db: MediaDb + loadedAt: number + /** 上次成功写出的分片快照(用于 diff,避免无谓写) */ + itemSnapshots: Map + userSnapshots: Map + metaSnapshot: string + /** 本次加载时的 id 水位:保存时用它区分「本地新增」与「持久化已有」 */ + baseNextId?: number +} + +let cache: CacheState | null = null +let loadPromise: Promise | null = null +let saveChain: Promise = Promise.resolve() +/** 最近一次加载是否失败:失败期间禁止写回,避免用空库覆盖真实数据 */ +let loadFailed = false + +const normalizeItem = (raw: any): MediaItem => ({ + id: Number(raw?.id) || 0, + media_type: (raw?.media_type || "video") as MediaType, + scan_path_id: Number(raw?.scan_path_id) || 0, + folder_path: String(raw?.folder_path || "/"), + file_name: String(raw?.file_name || ""), + scraped_name: String(raw?.scraped_name || ""), + cover: String(raw?.cover || ""), + authors: String(raw?.authors || ""), + plot: String(raw?.plot || ""), + description: String(raw?.description || ""), + genre: String(raw?.genre || ""), + release_date: String(raw?.release_date || ""), + rating: Number(raw?.rating) || 0, + file_size: Number(raw?.file_size) || 0, + duration: Number(raw?.duration) || 0, + season: Number(raw?.season) || 0, + episode: Number(raw?.episode) || 0, + episodes: typeof raw?.episodes === "string" ? raw.episodes : "", + album_name: String(raw?.album_name || ""), + album_artist: String(raw?.album_artist || ""), + track_number: Number(raw?.track_number) || 0, + publisher: String(raw?.publisher || ""), + isbn: String(raw?.isbn || ""), + external_id: String(raw?.external_id || ""), + hidden: !!raw?.hidden, + scraper_source: String(raw?.scraper_source || ""), + created_at: String(raw?.created_at || new Date().toISOString()), + updated_at: String(raw?.updated_at || new Date().toISOString()), + mime_type: String(raw?.mime_type || ""), + reviews: String(raw?.reviews || ""), + lyrics: String(raw?.lyrics || ""), + video_type: String(raw?.video_type || ""), + is_folder: !!raw?.is_folder, + scraped_at: raw?.scraped_at || null, + original_name: String(raw?.original_name || ""), + episode_title: String(raw?.episode_title || ""), + thumb: String(raw?.thumb || ""), + thumb_mime: String(raw?.thumb_mime || ""), +}) + +const normalizeScanPath = (raw: any): MediaScanPath => ({ + id: Number(raw?.id) || 0, + media_type: (raw?.media_type || "video") as MediaType, + name: String(raw?.name || ""), + path: String(raw?.path || "/"), + type_tag: String(raw?.type_tag || ""), + content_tags: String(raw?.content_tags || ""), + created_at: String(raw?.created_at || new Date().toISOString()), + updated_at: String(raw?.updated_at || new Date().toISOString()), + path_merge: !!raw?.path_merge, + enable_scrape: raw?.enable_scrape === undefined ? true : !!raw.enable_scrape, + last_scan_at: raw?.last_scan_at || null, +}) + +const normalizeConfig = (raw: any): MediaConfig => ({ + ...DEFAULT_CONFIG((raw?.media_type || "video") as MediaType), + ...raw, + media_type: (raw?.media_type || "video") as MediaType, + enabled: !!raw?.enabled, + thumbnail_mode: String(raw?.thumbnail_mode || "runtime"), + thumbnail_path: String(raw?.thumbnail_path || "imgs"), + store_thumbnail: raw?.store_thumbnail === undefined ? true : !!raw.store_thumbnail, + scrape_concurrency: Number(raw?.scrape_concurrency) || 5, + created_at: String(raw?.created_at || new Date().toISOString()), + updated_at: String(raw?.updated_at || new Date().toISOString()), + last_scan_at: raw?.last_scan_at || null, + last_scrape_at: raw?.last_scrape_at || null, + scraper_config: String(raw?.scraper_config || ""), +}) + +const normalizeUserData = (raw: any): MediaUserData => ({ + id: Number(raw?.id) || 0, + user_id: Number(raw?.user_id) || 0, + item_id: Number(raw?.item_id) || 0, + episode_index: Number(raw?.episode_index) || 0, + season: Number(raw?.season) || 0, + position_ticks: Number(raw?.position_ticks) || 0, + duration_ticks: Number(raw?.duration_ticks) || 0, + played: !!raw?.played, + play_count: Number(raw?.play_count) || 0, + is_favorite: !!raw?.is_favorite, + updated_at: String(raw?.updated_at || new Date().toISOString()), + rating: Number(raw?.rating) || 0, + last_played_at: raw?.last_played_at || null, +}) + +const normalizeDb = (raw: any): MediaDb => { + const base = emptyMediaDb() + if (!raw || typeof raw !== "object") return base + return { + version: Number(raw.version) || MEDIA_DB_VERSION, + next_id: Number(raw.next_id) || 1, + configs: Array.isArray(raw.configs) ? raw.configs.map(normalizeConfig) : [], + scan_paths: Array.isArray(raw.scan_paths) + ? raw.scan_paths.map(normalizeScanPath) + : [], + items: Array.isArray(raw.items) ? raw.items.map(normalizeItem) : [], + user_data: Array.isArray(raw.user_data) + ? raw.user_data.map(normalizeUserData) + : [], + app_keys: Array.isArray(raw.app_keys) ? raw.app_keys : [], + emby_devices: Array.isArray(raw.emby_devices) ? raw.emby_devices : [], + playlists: Array.isArray(raw.playlists) ? raw.playlists : [], + playlist_items: Array.isArray(raw.playlist_items) + ? raw.playlist_items + : [], + play_queues: Array.isArray(raw.play_queues) ? raw.play_queues : [], + item_shards: Array.isArray(raw.item_shards) ? raw.item_shards.map(String) : [], + user_shards: Array.isArray(raw.user_shards) ? raw.user_shards.map(String) : [], + } +} + +const itemShardKey = (scanPathId: number) => `${ITEMS_PREFIX}${scanPathId}` +const userShardKey = (userId: number) => `${USERDATA_PREFIX}${userId}` +const thumbKey = (hash: string) => `${THUMB_PREFIX}${hash}` + +/** 按 scan_path_id 分组(scan_path_id=0 兜底为一组) */ +const groupItems = (items: MediaItem[]) => { + const map = new Map() + for (const item of items) { + const sid = Number(item.scan_path_id) || 0 + const list = map.get(sid) || [] + list.push(item) + map.set(sid, list) + } + return map +} + +const groupUserData = (rows: MediaUserData[]) => { + const map = new Map() + for (const row of rows) { + const uid = Number(row.user_id) || 0 + const list = map.get(uid) || [] + list.push(row) + map.set(uid, list) + } + return map +} + +/** meta 文档(不含条目与用户数据) */ +const buildMetaPayload = (db: MediaDb) => { + const { + items: _items, + user_data: _userData, + ...rest + } = db + return { + ...rest, + version: MEDIA_DB_VERSION, + item_shards: Array.from(groupItems(db.items).keys()) + .sort((a, b) => a - b) + .map(String), + user_shards: Array.from(groupUserData(db.user_data).keys()) + .sort((a, b) => a - b) + .map(String), + } +} + +/** 从 list() 返回的键名里还原分片 id(只接受纯数字,忽略损坏键) */ +const shardIdsOf = (keys: string[], prefix: string) => { + const out: string[] = [] + for (const key of keys) { + if (!key.startsWith(prefix)) continue + const id = key.slice(prefix.length) + if (/^\d+$/.test(id)) out.push(id) + } + return out +} + +/** 合并分片索引:去重、按数字序排列,只增不减 */ +const mergeShardIds = (...lists: any[]) => { + const set = new Set() + for (const list of lists) { + if (!Array.isArray(list)) continue + for (const value of list) { + const id = String(value ?? "").trim() + if (/^\d+$/.test(id)) set.add(id) + } + } + return Array.from(set).sort((a, b) => Number(a) - Number(b)) +} + +/** + * 提交前把本地 meta 与持久化版本合并。 + * + * saveChain 只串行化**本实例**的写入:另一个实例可能在我们读取之后新增了 + * 分片(例如某用户首次上报播放进度)并写入 meta。若直接用本地快照覆盖, + * 全局索引就会丢掉对方的条目,冷启动后那些分片不再被读取。 + * 这里在写入前重新读一次持久化 meta,把分片索引与 id 水位合并进去; + * 分片本身还会在 getMediaDb 里按前缀枚举(见 listPersistedSecrets), + * 因此即使索引仍然写丢,数据也不会不可见。 + */ +type MetaRecord = Record + +const timeOf = (value: any) => { + const t = Date.parse(String(value || "")) + return Number.isFinite(t) ? t : 0 +} + +/** + * 按主键合并两个数组:以持久化为基准,本地新增并入,同键取 updated_at 较新者。 + * + * 不能简单地「本地数组整体覆盖」——另一个 isolate 可能刚撤销过密钥、改过配置, + * 用几分钟前读到的快照写回就会把它们复活(PR #148 评审 #4/#5/#6)。 + */ +function mergeRecords( + local: T[] | undefined, + persisted: T[] | undefined, + key: string, + timeKey = "updated_at", +): T[] | undefined { + if (!Array.isArray(local)) return persisted + if (!Array.isArray(persisted)) return local + const out = new Map() + for (const row of persisted) out.set(String(row?.[key]), row) + for (const row of local) { + const k = String(row?.[key]) + const cur = out.get(k) + if (!cur) { + out.set(k, row) + continue + } + if (timeOf(row?.[timeKey]) >= timeOf(cur?.[timeKey])) out.set(k, row) + } + return Array.from(out.values()) +} + +/** 把旧快照的增量合并进最新缓存对象(就地修改 base 并返回) */ +function mergeDbForSave(base: MediaDb, incoming: MediaDb): MediaDb { + const byId = new Map(base.items.map((item) => [item.id, item])) + for (const item of incoming.items) { + const cur = byId.get(item.id) + if (!cur) { + base.items.push(item) + byId.set(item.id, item) + continue + } + if (timeOf(item.updated_at) >= timeOf(cur.updated_at)) { + Object.assign(cur, item) + } + } + const rowKey = (row: MediaUserData) => + `${row.user_id}\u0000${row.item_id}\u0000${row.episode_index}\u0000${Number(row.season) || 1}` + const rowByKey = new Map(base.user_data.map((row) => [rowKey(row), row])) + for (const row of incoming.user_data) { + const k = rowKey(row) + const cur = rowByKey.get(k) + if (!cur) { + base.user_data.push(row) + rowByKey.set(k, row) + continue + } + if (timeOf(row.updated_at) >= timeOf(cur.updated_at)) { + Object.assign(cur, row) + } + } + base.next_id = Math.max(Number(base.next_id) || 1, Number(incoming.next_id) || 1) + return base +} + +async function mergeMetaForCommit( + env: any, + local: Record, +): Promise> { + const raw = await readPersistedSecret(env, META_KEY) + if (!raw) return local + try { + const persisted = JSON.parse(raw) + if (!persisted || typeof persisted !== "object") return local + return { + ...local, + // next_id 只取更大值:避免把全局水位写回去,导致 id 被重复分配 + next_id: Math.max( + Number(local.next_id) || 1, + Number(persisted.next_id) || 1, + ), + item_shards: mergeShardIds(local.item_shards, persisted.item_shards), + user_shards: mergeShardIds(local.user_shards, persisted.user_shards), + // 其余集合逐项合并,而不是整份快照覆盖 + configs: mergeRecords(local.configs, persisted.configs, "media_type"), + scan_paths: mergeRecords(local.scan_paths, persisted.scan_paths, "id"), + app_keys: mergeRecords(local.app_keys, persisted.app_keys, "id"), + devices: mergeRecords(local.devices, persisted.devices, "id"), + playlists: mergeRecords(local.playlists, persisted.playlists, "id"), + } + } catch { + // 持久化内容损坏:按本地快照提交,至少不让本次写入失败 + return local + } +} + +function ensureConfigs(db: MediaDb) { + for (const type of MEDIA_TYPES) { + if (!db.configs.some((c) => c.media_type === type)) { + db.configs.push(DEFAULT_CONFIG(type)) + } + } +} + +/** + * 读取单个分片;分片内容可能因手工编辑/版本升级损坏,统一静默降级。 + */ +async function readShardItems(env: any, sid: string): Promise { + // 读取失败必须抛出而不是返回 []:否则「后端超时」会被当成「空分片」, + // 下一次普通保存就把空数据写回去,真实条目与配置被永久覆盖。 + const raw = await readPersistedSecretStrict(env, itemShardKey(Number(sid))) + if (!raw) return [] + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`media items shard ${sid} is not an array`) + } + return parsed.map(normalizeItem) +} + +async function readShardUserData(env: any, uid: string): Promise { + const raw = await readPersistedSecretStrict(env, userShardKey(Number(uid))) + if (!raw) return [] + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`media userdata shard ${uid} is not an array`) + } + return parsed.map(normalizeUserData) +} + +export async function getMediaDb(env: any): Promise { + if (cache && Date.now() - cache.loadedAt < CACHE_TTL_MS) return cache.db + if (cache && loadPromise) return cache.db + if (loadPromise) return loadPromise + + loadPromise = (async () => { + let db: MediaDb + let legacyItems: MediaItem[] = [] + let legacyUserData: MediaUserData[] = [] + try { + const raw = await readPersistedSecret(env, META_KEY) + const parsed = raw ? JSON.parse(raw) : null + db = normalizeDb(parsed) + // 旧版单文档格式:items/user_data 直接挂在 meta 上 + if (Array.isArray(parsed?.items) && parsed.items.length > 0) { + legacyItems = parsed.items.map(normalizeItem) + } + if (Array.isArray(parsed?.user_data) && parsed.user_data.length > 0) { + legacyUserData = parsed.user_data.map(normalizeUserData) + } + } catch { + db = emptyMediaDb() + } + + ensureConfigs(db) + + // 分片本身才是权威来源:meta 里的索引会被其它实例覆盖(多实例各自保存时 + // 只写自己的条目),只按索引读取会永久丢掉对方的分片。这里按前缀枚举一遍 + // 并与索引取并集,索引退化为「少一次 list 的提示」。 + const [itemKeys, userKeys] = await Promise.all([ + listPersistedSecrets(env, ITEMS_PREFIX), + listPersistedSecrets(env, USERDATA_PREFIX), + ]) + const itemShardIds = mergeShardIds( + db.item_shards, + shardIdsOf(itemKeys, ITEMS_PREFIX), + ) + const userShardIds = mergeShardIds( + db.user_shards, + shardIdsOf(userKeys, USERDATA_PREFIX), + ) + // 补全后的索引写回内存快照,下一次保存会把它一起提交 + db.item_shards = itemShardIds + db.user_shards = userShardIds + + // 快照记录「本次真正读到的持久化内容」:保存时的 diff 与「清空分片写 []」 + // 都依赖它。旧实现从不填充,冷启动后删条目不会写 [],旧分片下次加载复活。 + const itemSnapshots = new Map() + const userSnapshots = new Map() + const itemGroups: MediaItem[] = [] + if (legacyItems.length > 0) { + itemGroups.push(...legacyItems) + } else { + for (const sid of itemShardIds) { + const list = await readShardItems(env, sid) + itemGroups.push(...list) + itemSnapshots.set(String(sid), serializeItems(list)) + } + } + const userRows: MediaUserData[] = [] + if (legacyUserData.length > 0) { + userRows.push(...legacyUserData) + } else { + for (const uid of userShardIds) { + const rows = await readShardUserData(env, uid) + userRows.push(...rows) + userSnapshots.set(String(uid), serializeUsers(rows)) + } + } + db.items = itemGroups + db.user_data = userRows + + // 迁移:把旧单文档中的条目/用户数据拆分落盘,并清理 meta 中的大字段 + if (legacyItems.length > 0 || legacyUserData.length > 0) { + const state: CacheState = { + db, + loadedAt: Date.now(), + itemSnapshots: new Map(), + userSnapshots: new Map(), + metaSnapshot: "", + } + cache = state + // 迁移写失败不能连带丢掉刚读出来的数据(否则下面的 catch 会回退到空库) + try { + await saveMediaDb(env, db) + } catch (e: any) { + console.warn("[media] legacy migration failed:", e?.message || e) + } + } + + cache = { + db, + loadedAt: Date.now(), + itemSnapshots, + userSnapshots, + metaSnapshot: "", + baseNextId: Number(db.next_id) || 1, + } + loadFailed = false + return db + })() + .catch((e) => { + console.warn("[media] load db failed, fallback to empty:", e?.message || e) + // 标记「这次的库不完整」:后续保存一律拒绝,直到某次加载成功。 + // 否则一次 KV/D1 读取失败就会把空库写回去,覆盖真实数据。 + loadFailed = true + cache = { + db: emptyMediaDb(), + loadedAt: Date.now(), + itemSnapshots: new Map(), + userSnapshots: new Map(), + metaSnapshot: "", + } + ensureConfigs(cache.db) + return cache.db + }) + .finally(() => { + loadPromise = null + }) + + return loadPromise +} + +/** 立即失效缓存(管理端导入/清除后调用,避免读到旧值) */ +export function invalidateMediaCache() { + cache = null +} + +const serializeItems = (list: MediaItem[]) => JSON.stringify(list) +const serializeUsers = (list: MediaUserData[]) => JSON.stringify(list) + +/** + * 保存媒体库。按分片 diff,只写发生变化的部分。 + * + * 写入顺序与失败语义: + * 1. 先写条目 / 用户数据分片,任何一个失败立即终止(不再写 meta); + * 2. 全部分片成功后才提交 meta(分片索引 + 配置等); + * 3. 快照只在写入成功后才推进,失败时保持旧值,下一次保存会重新尝试。 + * 这样既不会「把失败当成功」,也不会发布指向未写入分片的索引。 + * + * 注意:`db` 若与缓存不是同一对象,会先合并(调用方通常是同一个)。 + */ +/** + * 跨 isolate 的 id 水位冲突处理。 + * + * 两个 isolate 都从 next_id=100 出发时,各自新增的条目会拿到相同 id; + * 后保存的一方在这里把「本实例新分配、且持久化未知」的 id 整体搬到 + * 持久化水位之后,并同步 user_data 引用。窗口仍在「读水位 → 写分片」之间, + * 但比整段扫描持有时短了几个数量级。 + */ +async function remapConflictingIds( + env: any, + state: CacheState, + target: MediaDb, +): Promise { + const baseNext = Number(state.baseNextId) || 0 + if (!baseNext || (Number(target.next_id) || 1) <= baseNext) return + // 读**原始**持久化水位,而不是 mergeMetaForCommit 的结果:后者取的是 + // max(本地, 持久化),会把「本实例刚分配、还没写下去」的 id 当成冲突, + // 于是每次扫描都白重映射一遍。 + const rawMeta = await readPersistedSecret(env, META_KEY).catch(() => null) + if (!rawMeta) return + let persistedNext = 0 + try { + persistedNext = Number(JSON.parse(rawMeta)?.next_id) || 0 + } catch { + return + } + if (persistedNext <= baseNext) return + + const known = new Set() + for (const payload of state.itemSnapshots.values()) { + try { + for (const row of JSON.parse(payload)) known.add(Number(row.id)) + } catch { + // 快照损坏时保守处理:当作全是新增 + } + } + let next = persistedNext + const remap = new Map() + for (const item of target.items) { + if (item.id >= baseNext && !known.has(item.id)) remap.set(item.id, next++) + } + if (remap.size === 0) return + for (const item of target.items) { + const nid = remap.get(item.id) + if (nid !== undefined) item.id = nid + } + for (const row of target.user_data) { + const nid = remap.get(row.item_id) + if (nid !== undefined) row.item_id = nid + } + target.next_id = Math.max(Number(target.next_id) || 1, next) + state.baseNextId = Number(target.next_id) || 1 + console.warn(`[media] remapped ${remap.size} ids to avoid a cross-isolate collision`) +} + +export function saveMediaDb(env: any, db?: MediaDb): Promise { + let target = db ?? cache?.db + if (!target) return Promise.resolve() + if (loadFailed) { + // 上次加载不完整:任何写回都可能用空数据覆盖真实库 + return Promise.reject( + new Error("media db load failed; refusing to overwrite persisted data"), + ) + } + if (cache?.db && cache.db !== target) { + // 长任务(扫描/刮削)持有的是旧快照:先合并进最新缓存, + // 否则它会把别人刚写入的进度、密钥、配置整份覆盖掉。 + target = mergeDbForSave(cache.db, target) + } + const state: CacheState = + cache ?? + ({ + db: target, + loadedAt: Date.now(), + itemSnapshots: new Map(), + userSnapshots: new Map(), + metaSnapshot: "", + } as CacheState) + state.db = target + state.loadedAt = Date.now() + cache = state + + // diff 与写入都放进串行队列:快照只在真正写成功后才推进,因此排队的后续保存 + // 能看到前一次的结果;失败后重试同一份数据也不会被误判成「无变化」。 + saveChain = saveChain + .catch(() => undefined) + .then(async () => { + await remapConflictingIds(env, state, target) + const itemGroups = groupItems(target.items) + const userGroups = groupUserData(target.user_data) + + const itemWrites: Array<{ sid: number; payload: string }> = [] + for (const [sid, list] of itemGroups) { + const payload = serializeItems(list) + if (state.itemSnapshots.get(String(sid)) !== payload) { + itemWrites.push({ sid, payload }) + } + } + // 被清空的分片也要写空数组,否则删除 scan_path 后旧条目会「复活」 + for (const sid of Array.from(state.itemSnapshots.keys())) { + if (!itemGroups.has(Number(sid))) { + itemWrites.push({ sid: Number(sid), payload: "[]" }) + } + } + + const userWrites: Array<{ uid: number; payload: string }> = [] + for (const [uid, list] of userGroups) { + const payload = serializeUsers(list) + if (state.userSnapshots.get(String(uid)) !== payload) { + userWrites.push({ uid, payload }) + } + } + for (const uid of Array.from(state.userSnapshots.keys())) { + if (!userGroups.has(Number(uid))) { + userWrites.push({ uid: Number(uid), payload: "[]" }) + } + } + + const localMeta = buildMetaPayload(target) as Record + const localMetaStr = JSON.stringify(localMeta) + const metaChanged = state.metaSnapshot !== localMetaStr + + if (itemWrites.length === 0 && userWrites.length === 0 && !metaChanged) { + return + } + + try { + for (const w of itemWrites) { + if (!(await writePersistedSecret(env, itemShardKey(w.sid), w.payload))) { + throw new Error(`items shard ${w.sid} write failed`) + } + } + for (const w of userWrites) { + if (!(await writePersistedSecret(env, userShardKey(w.uid), w.payload))) { + throw new Error(`userdata shard ${w.uid} write failed`) + } + } + // 所有分片都成功后才提交 meta,避免发布指向未写入分片的索引 + if (metaChanged) { + const meta = await mergeMetaForCommit(env, localMeta) + const metaStr = JSON.stringify(meta) + if (!(await writePersistedSecret(env, META_KEY, metaStr))) { + throw new Error("media meta write failed") + } + // 快照记录**本地视图**:持久化版本里可能含别的实例的分片,若拿合并 + // 结果当基准,之后每次保存都会误判成「meta 有变化」而反复重写。 + state.metaSnapshot = localMetaStr + // 采纳其它实例推进过的 id 水位,避免后续分配复用已被占用的 id + const nextId = Number(meta.next_id) || 1 + if (nextId > (Number(target.next_id) || 1)) target.next_id = nextId + // 本次已把自己的水位写下去:下次保存的冲突基准要跟上, + // 否则会把自己的推进误判成「别的 isolate 占了 id」。 + state.baseNextId = Number(target.next_id) || 1 + } + // 只有全部写成功才推进快照;失败时保持旧值,重试会重新写入 + for (const w of itemWrites) { + state.itemSnapshots.set(String(w.sid), w.payload) + } + for (const w of userWrites) { + state.userSnapshots.set(String(w.uid), w.payload) + } + } catch (err: any) { + console.error("[media] save failed:", err?.message || err) + // 内存里的改动没有落盘:让下一次读取回到持久化版本, + // 避免界面继续展示「看起来已保存」的进度。 + state.loadedAt = 0 + throw err + } + }) + + return saveChain +} + +/** + * 只写 meta(配置/扫描路径/播放列表变更时用,避免触碰条目分片)。 + * + * 与 saveMediaDb 共用 saveChain:meta 里带着分片索引,若它抢在排队中的分片 + * 写入之前落盘,就会发布指向尚未写入分片的索引(冷启动后读到空分片)。 + * 失败语义同 saveMediaDb:抛错 + 不推进快照 + 失效缓存,下一次保存重试。 + */ +export function saveMediaMeta(env: any, db: MediaDb): Promise { + if (loadFailed) { + return Promise.reject( + new Error("media db load failed; refusing to overwrite persisted meta"), + ) + } + const state: CacheState = + cache ?? + ({ + db, + loadedAt: Date.now(), + itemSnapshots: new Map(), + userSnapshots: new Map(), + metaSnapshot: "", + } as CacheState) + + saveChain = saveChain + .catch(() => undefined) + .then(async () => { + const localMeta = buildMetaPayload(db) as Record + const localMetaStr = JSON.stringify(localMeta) + if (state.metaSnapshot === localMetaStr) return + try { + const meta = await mergeMetaForCommit(env, localMeta) + const metaStr = JSON.stringify(meta) + if (!(await writePersistedSecret(env, META_KEY, metaStr))) { + throw new Error("media meta write failed") + } + // 成功后才推进快照(记录本地视图,理由同 saveMediaDb), + // 失败时下一次保存会重新写入。 + state.metaSnapshot = localMetaStr + state.db = db + state.loadedAt = Date.now() + // 采纳其它实例推进过的 id 水位,避免后续分配复用已被占用的 id + const nextId = Number(meta.next_id) || 1 + if (nextId > (Number(db.next_id) || 1)) db.next_id = nextId + } catch (err: any) { + console.error("[media] save meta failed:", err?.message || err) + // meta 没落盘:让下一次读取回到持久化版本,避免界面继续展示未保存的改动 + state.loadedAt = 0 + throw err + } + }) + + return saveChain +} + +export function getNextId(db: MediaDb) { + const id = db.next_id || 1 + db.next_id = id + 1 + return id +} + +export function nextId(db: MediaDb) { + return getNextId(db) +} + +/** 批量分配 id(扫描时避免每条都推进 next_id 后反复写 meta) */ +export function allocateIds(db: MediaDb, count: number): number[] { + const start = db.next_id || 1 + db.next_id = start + count + return Array.from({ length: count }, (_, i) => start + i) +} + +export function getConfig(db: MediaDb, type: MediaType): MediaConfig { + let cfg = db.configs.find((c) => c.media_type === type) + if (!cfg) { + cfg = DEFAULT_CONFIG(type) + db.configs.push(cfg) + } + return cfg +} + +export function isTypeEnabled(db: MediaDb, type: MediaType) { + return !!getConfig(db, type).enabled +} + +export function listScanPaths(db: MediaDb, type?: MediaType) { + return db.scan_paths + .filter((p) => !type || p.media_type === type) + .sort((a, b) => a.id - b.id) +} + +export interface MediaQuery { + media_type?: MediaType + scan_path_id?: number + folder_path?: string + keyword?: string + type_tag?: string + content_tag?: string + path_prefix?: string + hidden?: boolean + order_by?: string + order_dir?: string + page?: number + page_size?: number + ids?: number[] + unscraped?: boolean + missing_scrape?: boolean +} + +const compare = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) + +export function queryItems(db: MediaDb, q: MediaQuery) { + let list = db.items.filter((item) => { + if (q.media_type && item.media_type !== q.media_type) return false + if (q.scan_path_id && item.scan_path_id !== q.scan_path_id) return false + if (q.folder_path && item.folder_path !== q.folder_path) return false + if (q.ids && !q.ids.includes(item.id)) return false + if (q.hidden !== undefined && item.hidden !== q.hidden) return false + if (q.unscraped && item.scraped_at) return false + if (q.missing_scrape && item.scraped_at) return false + if (q.path_prefix && q.path_prefix !== "/") { + const prefix = q.path_prefix.replace(/\/+$/, "") + "/" + if (!`${item.folder_path}/`.startsWith(prefix)) return false + } + if (q.keyword) { + const k = q.keyword.toLowerCase() + if ( + !item.file_name.toLowerCase().includes(k) && + !(item.scraped_name || "").toLowerCase().includes(k) && + !(item.album_name || "").toLowerCase().includes(k) && + !(item.album_artist || "").toLowerCase().includes(k) + ) { + return false + } + } + if (q.type_tag || q.content_tag) { + const sp = db.scan_paths.find((p) => p.id === item.scan_path_id) + if (!sp) return false + if (q.type_tag && sp.type_tag !== q.type_tag) return false + if (q.content_tag) { + const tags = String(sp.content_tags || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + if (!tags.includes(q.content_tag)) return false + } + } + return true + }) + const page = Math.max(1, q.page || 1) + const pageSize = Math.min(500, Math.max(1, q.page_size || 40)) + const dir = q.order_dir === "desc" ? -1 : 1 + const by = q.order_by || "name" + list = list.sort((a, b) => { + let av: any = 0 + let bv: any = 0 + if (by === "date") { + av = a.release_date || "" + bv = b.release_date || "" + } else if (by === "size") { + av = a.file_size + bv = b.file_size + } else if (by === "rating") { + av = a.rating + bv = b.rating + } else if (by === "updated") { + av = a.updated_at || "" + bv = b.updated_at || "" + } else { + av = (a.scraped_name || a.file_name).toLowerCase() + bv = (b.scraped_name || b.file_name).toLowerCase() + } + const cmp = typeof av === "number" ? av - bv : compare(av, bv) + return cmp * dir + }) + return { + content: list.slice((page - 1) * pageSize, page * pageSize), + total: list.length, + } +} + +export function findItem(db: MediaDb, id: number) { + return db.items.find((i) => i.id === id) +} + +export function findScanPath(db: MediaDb, id: number) { + return db.scan_paths.find((p) => p.id === id) +} + +export function upsertItem( + db: MediaDb, + data: Partial & + Pick, +): MediaItem { + const existing = db.items.find( + (i) => + i.folder_path === data.folder_path && + i.file_name === data.file_name && + i.media_type === data.media_type && + (i.album_name || "") === (data.album_name || ""), + ) + const now = new Date().toISOString() + if (existing) { + Object.assign(existing, data, { updated_at: now }) + return existing + } + const item: MediaItem = { + id: data.id || nextId(db), + media_type: data.media_type, + scan_path_id: data.scan_path_id || 0, + folder_path: data.folder_path, + file_name: data.file_name, + scraped_name: data.scraped_name || "", + cover: data.cover || "", + authors: data.authors || "", + plot: data.plot || "", + description: data.description || "", + genre: data.genre || "", + release_date: data.release_date || "", + rating: data.rating || 0, + file_size: data.file_size || 0, + duration: data.duration || 0, + season: data.season || 0, + episode: data.episode || 0, + episodes: data.episodes || "", + album_name: data.album_name || "", + album_artist: data.album_artist || "", + track_number: data.track_number || 0, + publisher: data.publisher || "", + isbn: data.isbn || "", + external_id: data.external_id || "", + hidden: !!data.hidden, + scraper_source: data.scraper_source || "", + created_at: data.created_at || now, + updated_at: now, + mime_type: data.mime_type || "", + reviews: data.reviews || "", + lyrics: data.lyrics || "", + video_type: data.video_type || "", + is_folder: !!data.is_folder, + scraped_at: data.scraped_at || null, + original_name: data.original_name || "", + thumb: data.thumb || "", + thumb_mime: data.thumb_mime || "", + } + db.items.push(item) + return item +} + +/** 缺省季号按 1 处理:旧数据没有 season 字段,语义上就是第一季 */ +const seasonOfRow = (row: MediaUserData) => Number(row.season) || 1 + +export function getUserData( + db: MediaDb, + userId: number, + itemId: number, + episodeIndex = 0, + season = 0, +) { + const wantSeason = Number(season) || 1 + return db.user_data.find( + (d) => + d.user_id === userId && + d.item_id === itemId && + d.episode_index === episodeIndex && + seasonOfRow(d) === wantSeason, + ) +} + +export function ensureUserData( + db: MediaDb, + userId: number, + itemId: number, + episodeIndex = 0, + season = 0, +): MediaUserData { + let row = getUserData(db, userId, itemId, episodeIndex, season) + if (!row) { + row = { + id: nextId(db), + user_id: userId, + item_id: itemId, + episode_index: episodeIndex, + season: Number(season) || 1, + position_ticks: 0, + duration_ticks: 0, + played: false, + play_count: 0, + is_favorite: false, + updated_at: new Date().toISOString(), + rating: 0, + last_played_at: null, + } + db.user_data.push(row) + } + return row +} + +export function listFavorites(db: MediaDb, userId: number) { + return db.user_data.filter((d) => d.user_id === userId && d.is_favorite) +} + +export function listResume(db: MediaDb, userId: number, limit = 200) { + return db.user_data + .filter( + (d) => + d.user_id === userId && + !d.played && + d.position_ticks > 0 && + d.episode_index !== undefined, + ) + .sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1)) + .slice(0, Math.max(1, limit)) +} + +// ==================== 播放列表 ==================== + +export function listPlaylists(db: MediaDb, userId: number) { + return db.playlists + .filter((p) => p.user_id === userId || p.public) + .sort((a, b) => a.id - b.id) +} + +export function getPlaylist(db: MediaDb, id: number) { + return db.playlists.find((p) => p.id === id) +} + +export function playlistItems(db: MediaDb, playlistId: number) { + return db.playlist_items + .filter((i) => i.playlist_id === playlistId) + .sort((a, b) => a.order - b.order) +} + +export function createPlaylist( + db: MediaDb, + userId: number, + name: string, + itemIds: number[] = [], + comment = "", + isPublic = false, +): MediaPlaylist { + const now = new Date().toISOString() + const playlist: MediaPlaylist = { + id: nextId(db), + user_id: userId, + name, + comment, + public: isPublic, + item_count: 0, + created_at: now, + updated_at: now, + } + db.playlists.push(playlist) + replacePlaylistItems(db, playlist.id, itemIds) + playlist.item_count = playlistItems(db, playlist.id).length + return playlist +} + +export function replacePlaylistItems( + db: MediaDb, + playlistId: number, + itemIds: number[], +) { + db.playlist_items = db.playlist_items.filter( + (i) => i.playlist_id !== playlistId, + ) + itemIds.forEach((itemId, idx) => { + if (!findItem(db, itemId)) return + db.playlist_items.push({ + id: nextId(db), + playlist_id: playlistId, + item_id: itemId, + episode_index: 0, + order: idx, + }) + }) +} + +export function updatePlaylist( + db: MediaDb, + playlist: MediaPlaylist, + patch: { + name?: string + comment?: string + public?: boolean + itemIds?: number[] + }, +) { + if (patch.name !== undefined) playlist.name = patch.name + if (patch.comment !== undefined) playlist.comment = patch.comment + if (patch.public !== undefined) playlist.public = patch.public + if (patch.itemIds !== undefined) { + replacePlaylistItems(db, playlist.id, patch.itemIds) + } + playlist.item_count = playlistItems(db, playlist.id).length + playlist.updated_at = new Date().toISOString() +} + +export function deletePlaylist(db: MediaDb, id: number) { + db.playlists = db.playlists.filter((p) => p.id !== id) + db.playlist_items = db.playlist_items.filter((i) => i.playlist_id !== id) +} + +// ==================== 播放队列 ==================== + +export function savePlayQueue( + db: MediaDb, + userId: number, + entryIds: number[], + current = "", + position = 0, +) { + let queue = db.play_queues.find((q) => q.user_id === userId) + if (!queue) { + queue = { + user_id: userId, + current, + position, + changed: new Date().toISOString(), + entry_ids: [], + } + db.play_queues.push(queue) + } + queue.entry_ids = entryIds + queue.current = current + queue.position = position + queue.changed = new Date().toISOString() + return queue +} + +export function getPlayQueue(db: MediaDb, userId: number) { + return db.play_queues.find((q) => q.user_id === userId) || null +} + +// ==================== 缩略图 ==================== + +export async function saveThumb( + env: any, + hash: string, + bytes: Uint8Array, + mime: string, +): Promise { + if (!hash || bytes.length === 0) return "" + if (bytes.length > MAX_THUMB_BYTES) { + // 超限不再静默:这是「封面缺失」最常见的原因,留一条可通过日志发现的记录 + console.warn( + `[media] thumbnail ${hash} skipped: ${bytes.length} bytes > ${MAX_THUMB_BYTES}`, + ) + return "" + } + const payload = JSON.stringify({ + mime, + data: bytesToBase64(bytes), + }) + const ok = await writePersistedSecret(env, thumbKey(hash), payload) + return ok ? `thumb:${hash}` : "" +} + +export async function readThumb( + env: any, + hash: string, +): Promise<{ mime: string; bytes: Uint8Array } | null> { + const raw = await readPersistedSecret(env, thumbKey(hash)) + if (!raw) return null + try { + const parsed = JSON.parse(raw) + if (!parsed?.data) return null + return { + mime: String(parsed.mime || "image/jpeg"), + bytes: base64ToBytes(String(parsed.data)), + } + } catch { + return null + } +} + +function bytesToBase64(bytes: Uint8Array): string { + let bin = "" + const chunk = 0x8000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return btoa(bin) +} + +function base64ToBytes(value: string): Uint8Array { + const bin = atob(value) + const out = new Uint8Array(bin.length) + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i) + return out +} + +// ==================== 设置 ==================== + +export async function getSettingsMap( + env: any, +): Promise> { + const { getDb } = await import("../model/db") + const { disableTranscodeSettings } = await import("../model/transcode-guard") + const db = await getDb(env) + const map: Record = {} + for (const s of db.settings || []) map[s.key] = String(s.value ?? "") + // Worker 无 ffmpeg:转码开关一律按关闭处理 + return disableTranscodeSettings(map) as Record +} + +/** 数据一致性检查:items 引用不存在的 scan_path 时归零,供导入/删除后修复 */ +export function repairItemScanPath(db: MediaDb) { + const ids = new Set(db.scan_paths.map((p) => p.id)) + let fixed = 0 + for (const item of db.items) { + if (item.scan_path_id && !ids.has(item.scan_path_id)) { + item.scan_path_id = 0 + fixed += 1 + } + } + // 清理孤儿用户数据与播放列表条目 + const itemIds = new Set(db.items.map((i) => i.id)) + const beforeUser = db.user_data.length + db.user_data = db.user_data.filter((d) => itemIds.has(d.item_id)) + const beforePlaylist = db.playlist_items.length + db.playlist_items = db.playlist_items.filter((i) => itemIds.has(i.item_id)) + for (const p of db.playlists) { + p.item_count = db.playlist_items.filter((i) => i.playlist_id === p.id).length + } + return { + fixed_scan_path: fixed, + removed_user_data: beforeUser - db.user_data.length, + removed_playlist_items: beforePlaylist - db.playlist_items.length, + } +} diff --git a/src/backend/internal/media/store_persist.test.ts b/src/backend/internal/media/store_persist.test.ts new file mode 100644 index 00000000..de1ece2e --- /dev/null +++ b/src/backend/internal/media/store_persist.test.ts @@ -0,0 +1,263 @@ +/** + * 媒体库分片持久化的回归用例。 + * + * 覆盖两类「静默丢数据」问题: + * 1. writePersistedSecret 失败只返回 false,旧实现忽略返回值 → 保存被当成 + * 成功、快照被推进,之后重试同一份数据不会再发起写入; + * 2. meta 里的分片索引按「本地完整快照」生成并整体覆盖 → 多实例各自保存 + * 用户分片时,后写的 meta 会丢掉对方的索引,冷启动后分片不再被读取。 + */ +import assert from "node:assert/strict" +import test from "node:test" + +import { emptyMediaDb, type MediaDb, type MediaItem, type MediaUserData } from "./model" +import { + getConfig, + getMediaDb, + invalidateMediaCache, + isTypeEnabled, + saveMediaDb, + saveMediaMeta, +} from "./store" + +/** + * 模拟 D1 绑定(只实现 key 格式用到的 SQL)。 + * + * 让 run() 对指定 key 抛错,即可模拟「底层写入失败但读路径正常」—— + * 这正是 writePersistedSecret 返回 false 的场景。 + */ +function createMockD1() { + const store = new Map() + const writes = new Map() + let shouldFail: (key: string) => boolean = () => false + + const exec = async (sql: string, params: any[]) => { + const text = sql.trim() + // 建表语句:无副作用 + if (/^CREATE\s+(TABLE|INDEX|UNIQUE)/i.test(text)) return + if (/^INSERT\s+OR\s+REPLACE\s+INTO\s+kv/i.test(text)) { + const key = String(params[0]) + if (shouldFail(key)) throw new Error(`mock D1 write failed: ${key}`) + store.set(key, String(params[1])) + writes.set(key, (writes.get(key) || 0) + 1) + return + } + if (/^DELETE\s+FROM\s+kv\s+WHERE\s+key\s*=\s*\?/i.test(text)) { + store.delete(String(params[0])) + return + } + } + + const query = async (sql: string, params: any[]) => { + const text = sql.trim() + if (/^SELECT\s+value\s+FROM\s+kv\s+WHERE\s+key\s*=\s*\?/i.test(text)) { + const key = String(params[0]) + return store.has(key) ? [{ value: store.get(key) }] : [] + } + if (/^SELECT\s+key\s+FROM\s+kv\s+WHERE\s+key\s+LIKE\s+\?/i.test(text)) { + const prefix = String(params[0]).replace(/%$/, "") + return [...store.keys()] + .filter((k) => k.startsWith(prefix)) + .map((key) => ({ key })) + } + return [] + } + + const statement = (sql: string, params: any[]) => ({ + first: async () => (await query(sql, params))[0] ?? null, + all: async () => ({ results: await query(sql, params) }), + run: async () => exec(sql, params), + }) + + const db = { + prepare(sql: string) { + return { + bind: (...params: any[]) => statement(sql, params), + ...statement(sql, []), + } + }, + async batch(stmts: any[]) { + const out: any[] = [] + for (const stmt of stmts) out.push(await stmt.run()) + return out + }, + } + + return { + db, + store, + writes, + failOn: (fn: (key: string) => boolean) => { + shouldFail = fn + }, + } +} + +const item = (id: number, scanPathId = 1): MediaItem => ({ + id, + media_type: "video", + scan_path_id: scanPathId, + folder_path: "/movies", + file_name: `movie-${id}.mkv`, + scraped_name: "", + cover: "", + authors: "", + plot: "", + description: "", + genre: "", + release_date: "", + rating: 0, + file_size: 0, + duration: 0, + season: 0, + episode: 0, + episodes: "", + album_name: "", + album_artist: "", + track_number: 0, + publisher: "", + isbn: "", + external_id: "", + hidden: false, + scraper_source: "", + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", +}) + +const progress = (userId: number, itemId: number): MediaUserData => ({ + id: userId * 100 + itemId, + user_id: userId, + item_id: itemId, + episode_index: 0, + position_ticks: 42, + duration_ticks: 100, + played: false, + play_count: 0, + is_favorite: false, + updated_at: "2026-01-01T00:00:00.000Z", + rating: 0, + last_played_at: null, +}) + +const newEnv = () => { + const mock = createMockD1() + return { + mock, + env: { DB_DRIVER: "d1", DB_FORMAT: "key", DB: mock.db }, + } +} + +test("saveMediaDb: 分片写失败要抛错、不提交 meta,并且可以重试", async () => { + const { mock, env } = newEnv() + invalidateMediaCache() + + const db = emptyMediaDb() + db.items = [item(1)] + db.user_data = [progress(7, 1)] + + mock.failOn((key) => key === "openlist_media_ud_v1:7") + await assert.rejects(() => saveMediaDb(env, db), /userdata shard 7/) + + // 分片失败时不能提交 meta:否则会发布指向未成功写入分片的索引 + assert.equal(mock.store.has("openlist_media_db_v1"), false) + + // 恢复后重试:失败的快照不能阻止再次写入 + mock.failOn(() => false) + const before = mock.writes.get("openlist_media_ud_v1:7") || 0 + await saveMediaDb(env, db) + assert.equal(mock.writes.get("openlist_media_ud_v1:7"), before + 1) + assert.ok(mock.store.has("openlist_media_db_v1")) +}) + +test("saveMediaDb: 不覆盖其它实例写入的分片索引,并推进 id 水位", async () => { + const { mock, env } = newEnv() + + // 实例 A:用户 1 首次上报进度 + invalidateMediaCache() + const a = emptyMediaDb() + a.next_id = 100 + a.items = [item(1, 1)] + a.user_data = [progress(1, 1)] + await saveMediaDb(env, a) + const firstMeta = JSON.parse(mock.store.get("openlist_media_db_v1")!) + assert.deepEqual(firstMeta.user_shards, ["1"]) + assert.deepEqual(firstMeta.item_shards, ["1"]) + + // 实例 B:持有更早的本地状态(无用户 1),保存用户 2 的分片。 + // saveChain/快照都是「每实例」的,因此先清空本地缓存来模拟另一个 isolate。 + invalidateMediaCache() + const b = emptyMediaDb() + b.items = [item(2, 2)] + b.user_data = [progress(2, 1)] + await saveMediaDb(env, b) + + const merged = JSON.parse(mock.store.get("openlist_media_db_v1")!) + assert.deepEqual(merged.user_shards, ["1", "2"], "索引不能丢掉其它实例的分片") + assert.deepEqual(merged.item_shards, ["1", "2"], "条目索引同样不能丢") + assert.equal(merged.next_id, 100, "next_id 只能前进,不能回退") + assert.equal(b.next_id, 100, "本地也应采纳更大的 id 水位") + + // 冷启动:两个用户的分片都必须被读回来 + invalidateMediaCache() + const loaded = await getMediaDb(env) + assert.deepEqual( + loaded.user_data.map((row) => row.user_id).sort((x, y) => x - y), + [1, 2], + ) + assert.deepEqual( + loaded.items.map((row) => row.id).sort((x, y) => x - y), + [1, 2], + ) +}) + +test("getMediaDb: meta 索引被覆盖后仍按前缀发现分片", async () => { + const { mock, env } = newEnv() + + // 先造出两个用户分片 + invalidateMediaCache() + const a = emptyMediaDb() + a.user_data = [progress(1, 1)] + await saveMediaDb(env, a) + + invalidateMediaCache() + const b = emptyMediaDb() + b.user_data = [progress(2, 1)] + await saveMediaDb(env, b) + + // 模拟「后写的实例用本地快照整体覆盖 meta」:索引里只剩用户 2 + const meta = JSON.parse(mock.store.get("openlist_media_db_v1")!) + meta.user_shards = ["2"] + mock.store.set("openlist_media_db_v1", JSON.stringify(meta)) + + // 冷启动仍应读到用户 1 的分片(键本身才是权威来源) + invalidateMediaCache() + const loaded: MediaDb = await getMediaDb(env) + assert.deepEqual( + loaded.user_data.map((row) => row.user_id).sort((x, y) => x - y), + [1, 2], + ) +}) + +test("saveMediaMeta: 写失败要抛错并失效缓存,重试时重新写入", async () => { + const { mock, env } = newEnv() + invalidateMediaCache() + const db = await getMediaDb(env) + + mock.failOn((key) => key === "openlist_media_db_v1") + getConfig(db, "video").enabled = true + await assert.rejects(() => saveMediaMeta(env, db), /media meta write failed/) + assert.equal(mock.store.has("openlist_media_db_v1"), false) + + // 失败后读取必须回到持久化版本,不能把内存里的改动当成已保存 + const reloaded = await getMediaDb(env) + assert.equal(isTypeEnabled(reloaded, "video"), false) + + // 恢复后重试:同样的改动重新写入一次,成功之后不再重复写 + mock.failOn(() => false) + getConfig(reloaded, "video").enabled = true + await saveMediaMeta(env, reloaded) + const writes = mock.writes.get("openlist_media_db_v1") || 0 + assert.equal(writes, 1) + await saveMediaMeta(env, reloaded) + assert.equal(mock.writes.get("openlist_media_db_v1") || 0, writes) +}) diff --git a/src/backend/internal/media/strm.ts b/src/backend/internal/media/strm.ts new file mode 100644 index 00000000..431abfec --- /dev/null +++ b/src/backend/internal/media/strm.ts @@ -0,0 +1,333 @@ +/** + * STRM 生成(对齐 Go 版 internal/media/strm.go)。 + * + * 输出结构: + * video/movie: /<title (year)>.strm + * video/tv : <series>/Season 01/<series> S01E02 - <title>.strm + * music : <artist>/<album>/<02 - title>.strm + * + * 模板占位符与 Go 一致:{site_url} {path} {sign} {raw_path} {name} + */ + +import { getItem, putItem, listItems } from "../op/storage" +import { encodeDownloadPath } from "../../pkg/path" +import { getJwtSecret } from "../../server/middlewares" +import { signDownloadPath } from "../../pkg/sign" +import { + settingBool, + type EpisodeInfo, + type MediaItem, + type MediaType, + type StrmOptions, +} from "./model" +import { getMediaDb, getSettingsMap, listScanPaths } from "./store" + +export interface StrmTarget { + /** 相对输出目录的路径 */ + relPath: string + /** 媒体文件 VFS 路径 */ + rawPath: string + /** 同目录模式的 .strm 路径 */ + sameDirPath: string +} + +const sanitizeName = (value: string) => { + let s = String(value || "").trim() + if (!s) return "Unknown" + s = s.replace(/[/\\:*?"<>|\n\r]/g, "_") + if (s.length > 120) s = s.slice(0, 120) + return s.trim() || "Unknown" +} + +const itemTitle = (item: MediaItem) => { + if (item.scraped_name) return item.scraped_name + const name = item.file_name || "" + return item.is_folder ? name : name.replace(/\.[^.]+$/, "") +} + +const parseEpisodes = (item: MediaItem): EpisodeInfo[] => { + if (!item.episodes) return [] + try { + const list = JSON.parse(item.episodes) + return Array.isArray(list) ? list : [] + } catch { + return [] + } +} + +const itemFolder = (item: MediaItem) => + (item.folder_path || "/").replace(/\/+$/, "") || "/" + +const joinPath = (parts: string[]) => + parts + .filter((p) => p !== "") + .join("/") + .replace(/\/+/g, "/") + +const strmSibling = (rawPath: string, isDir: boolean) => { + if (isDir) return `${rawPath.replace(/\/+$/, "")}/${rawPath.split("/").filter(Boolean).pop()}.strm` + const idx = rawPath.lastIndexOf(".") + const slash = rawPath.lastIndexOf("/") + if (idx > slash) return `${rawPath.slice(0, idx)}.strm` + return `${rawPath}.strm` +} + +/** 计算一个条目对应的 STRM 目标(图片/书籍返回空) */ +export function buildStrmTargets(item: MediaItem): StrmTarget[] { + const title = sanitizeName(itemTitle(item)) + const year = (item.release_date || "").slice(0, 4) + const folder = itemFolder(item) + + if (item.media_type === "video") { + if (item.video_type === "tv") { + const series = title + const season = item.season > 0 ? item.season : 1 + if (item.is_folder) { + const out: StrmTarget[] = [] + parseEpisodes(item).forEach((ep, idx) => { + const num = ep.index > 0 ? ep.index : idx + 1 + // 合并文件夹里可能同时有 S01E01 与 S02E01:必须用每一集自己的季号, + // 否则第二季会被导出到 Season 01 并撞名。 + const epSeason = Number(ep.season) > 0 ? Number(ep.season) : season + const epTitle = sanitizeName(ep.title || ep.file_name) + const epRaw = joinPath([folder, item.file_name, ep.file_name]) + out.push({ + relPath: joinPath([ + series, + `Season ${String(epSeason).padStart(2, "0")}`, + `${series} S${String(epSeason).padStart(2, "0")}E${String(num).padStart(2, "0")} - ${epTitle}.strm`, + ]), + rawPath: epRaw, + sameDirPath: strmSibling(epRaw, false), + }) + }) + return out + } + const num = item.episode > 0 ? item.episode : 1 + const raw = joinPath([folder, item.file_name]) + return [ + { + relPath: joinPath([ + series, + `Season ${String(season).padStart(2, "0")}`, + `${series} S${String(season).padStart(2, "0")}E${String(num).padStart(2, "0")}.strm`, + ]), + rawPath: raw, + sameDirPath: strmSibling(raw, false), + }, + ] + } + const name = year ? `${title} (${year})` : title + const raw = joinPath([folder, item.file_name]) + return [ + { + relPath: joinPath([name, `${name}.strm`]), + rawPath: raw, + sameDirPath: strmSibling(raw, !!item.is_folder), + }, + ] + } + + if (item.media_type === "music") { + const artist = sanitizeName(item.album_artist || "Unknown Artist") + const album = sanitizeName(item.album_name || "Unknown Album") + // path_merge 的专辑条目:file_name 是目录,曲目在 episodes 里。 + // 必须逐曲生成 STRM,否则写出的链接指向目录、播放器无法播放。 + const tracks = item.is_folder ? parseEpisodes(item) : [] + if (tracks.length > 0) { + return tracks.map((ep, idx) => { + const num = ep.index > 0 ? ep.index : idx + 1 + const trackTitle = sanitizeName(ep.title || ep.file_name) + const raw = joinPath([folder, item.file_name, ep.file_name]) + return { + relPath: joinPath([ + artist, + album, + `${String(num).padStart(2, "0")} - ${trackTitle}.strm`, + ]), + rawPath: raw, + sameDirPath: strmSibling(raw, false), + } + }) + } + const track = item.track_number > 0 ? item.track_number : 1 + const raw = joinPath([folder, item.file_name]) + return [ + { + relPath: joinPath([ + artist, + album, + `${String(track).padStart(2, "0")} - ${title}.strm`, + ]), + rawPath: raw, + sameDirPath: strmSibling(raw, false), + }, + ] + } + + return [] +} + +/** 渲染 .strm 内容(单行 URL) */ +export async function renderStrmContent( + env: any, + template: string, + apiRoot: string, + rawPath: string, + /** + * 签名有效期(秒)。生成到存储里的 .strm 文件用 0(永不过期,由文件本身 + * 长期存在);按用户请求即时下载的接口应传管理员的 link_expiration, + * 否则任何能看到条目的人都能拿走一条永久链接。 + */ + expiresIn = 0, +): Promise<string> { + const tpl = template || "{site_url}/d{path}?sign={sign}" + const base = (apiRoot || "").replace(/\/+$/, "") + const p = rawPath.startsWith("/") ? rawPath : `/${rawPath}` + const expires = Math.max(0, Math.floor(expiresIn) || 0) + let sign = "" + try { + sign = await signDownloadPath({ env }, rawPath, expires) + } catch { + sign = "" + } + const replaced = tpl + .replace(/\{site_url\}/g, base) + .replace(/\{raw_path\}/g, p) + .replace(/\{path\}/g, encodeDownloadPath(p)) + .replace(/\{sign\}/g, encodeURIComponent(sign)) + .replace(/\{name\}/g, p.split("/").filter(Boolean).pop() || "") + return replaced.trim() +} + +export function strmOptionsFromSettings( + settings: Record<string, string>, + apiRoot: string, +): StrmOptions { + return { + apiRoot: (apiRoot || settings.site_url || "").replace(/\/+$/, ""), + template: settings.media_strm_template || "{site_url}/d{path}?sign={sign}", + overwrite: settingBool(settings, "media_strm_overwrite", false), + sameDir: settingBool(settings, "media_strm_same_dir", true), + outputPath: (settings.media_strm_path || "/strm").replace(/\/+$/, "") || "/strm", + } +} + +async function exists(env: any, path: string) { + try { + await getItem(path, { env }) + return true + } catch { + return false + } +} + +/** + * 目标路径是否仍在输出根目录内。 + * + * outRoot 为空表示同目录模式,不做校验;outRoot 为 "/"(输出到存储根)时 + * 任何绝对路径都在根内——旧实现用 `startsWith(`${outRoot}/`)` 判断, + * outRoot 为 "/" 会变成 startsWith("//"),把所有目标都误判成越界。 + */ +export const withinOutRoot = (outRoot: string, dst: string): boolean => { + const segments = dst.split("/").filter(Boolean) + // `..` 段必须在比较前缀之前拒绝:"/out/../../etc/x" 以 "/out/" 开头, + // 只做字符串前缀比较会把它当成根内路径,写到输出目录之外。 + // 编码点段(%2e%2e)在存储层解码后同样会穿越,按解码结果判断。 + const isDotSegment = (segment: string) => { + let decoded = segment + try { + decoded = decodeURIComponent(segment) + } catch { + // 非法百分号编码:按原文判断 + } + return decoded === "." || decoded === ".." + } + if (segments.some(isDotSegment)) return false + if (!outRoot || outRoot === "/") return true + const normalized = "/" + segments.join("/") + return normalized === outRoot || normalized.startsWith(`${outRoot}/`) +} + +/** 为一组条目生成 STRM,返回写入数量 */ +export async function generateStrmForItems( + env: any, + items: MediaItem[], + opts: StrmOptions, +): Promise<{ written: number; failed: number }> { + let written = 0 + let failed = 0 + const outRoot = + !opts.sameDir && opts.outputPath + ? "/" + opts.outputPath.replace(/^\/+/, "").replace(/\/+$/, "") + : "" + + for (const item of items) { + if (item.hidden) continue + for (const target of buildStrmTargets(item)) { + const dst = opts.sameDir ? target.sameDirPath : joinPath([outRoot, target.relPath]) + if (!dst) continue + // 防目录穿越:relPath 里可能含刮削得到的外部名称 + if (!opts.sameDir && !withinOutRoot(outRoot, dst)) continue + if (!opts.overwrite && (await exists(env, dst))) continue + try { + const content = await renderStrmContent( + env, + opts.template, + opts.apiRoot, + target.rawPath, + ) + await putItem(dst, new TextEncoder().encode(content) as any, { env }) + written += 1 + } catch (e: any) { + failed += 1 + console.warn("[media] strm write failed", dst, e?.message || e) + } + } + } + return { written, failed } +} + +/** 按媒体类型(可选扫描路径)批量生成 */ +export async function generateStrmForType( + env: any, + mediaType: MediaType | "", + scanPathId = 0, + apiRoot = "", +): Promise<{ written: number; failed: number; path: string }> { + const db = await getMediaDb(env) + const settings = await getSettingsMap(env) + const opts = strmOptionsFromSettings(settings, apiRoot) + let items = db.items + if (scanPathId) items = items.filter((i) => i.scan_path_id === scanPathId) + if (mediaType) items = items.filter((i) => i.media_type === mediaType) + const result = await generateStrmForItems(env, items, opts) + return { + ...result, + path: opts.sameDir ? "同目录" : opts.outputPath, + } +} + +/** 扫描结束后按 media_strm_on_scan 自动生成 */ +export async function maybeGenerateStrmOnScan(env: any, mediaType: MediaType) { + const settings = await getSettingsMap(env) + if (!settingBool(settings, "media_strm_on_scan", false)) return + const opts = strmOptionsFromSettings(settings, settings.site_url || "") + if (!opts.sameDir && !opts.outputPath) return + const result = await generateStrmForType(env, mediaType, 0, opts.apiRoot) + console.log( + `[media] strm on scan ${mediaType}: written=${result.written} failed=${result.failed}`, + ) +} + +/** 判断扫描路径是否配置了合并模式(供管理端提示) */ +export async function describeScanPaths(env: any, type: MediaType) { + const db = await getMediaDb(env) + return listScanPaths(db, type).map((p) => ({ + ...p, + path_merge: !!p.path_merge, + })) +} + +/** 导出:供测试验证 STRM 目标计算 */ +export const __strmInternals = { sanitizeName, itemTitle, strmSibling, joinPath } diff --git a/src/backend/internal/media/tag.ts b/src/backend/internal/media/tag.ts new file mode 100644 index 00000000..58249148 --- /dev/null +++ b/src/backend/internal/media/tag.ts @@ -0,0 +1,1349 @@ +/** + * 媒体元数据解析(纯 TS,无 Node 依赖)。 + * + * Worker 环境没有 ffprobe / taglib / sharp,因此这里实现「够用」的解析: + * - MP3:ID3v2.2/2.3/2.4(标题/艺术家/专辑/曲序/年份/流派/封面/歌词)+ ID3v1 + * - FLAC:Vorbis Comment + PICTURE + STREAMINFO(时长) + * - M4A/MP4:moov/udta/meta/ilst 的 ©nam/©ART/©alb/trkn/covr(尽力而为) + * - EPUB/CBZ:ZIP 目录解析 + 内嵌封面提取 + * - 图片:按魔数识别真实 MIME(防止伪造扩展名被当作图片代理) + * + * 所有解析函数只接受「已读取的字节」,不做 I/O;读取由 scan.ts / scrape.ts + * 通过 op/storage.readFileRange 完成。 + */ + +export interface MusicTag { + title: string + artist: string + album: string + albumArtist: string + trackNumber: number + year: string + genre: string + lyrics: string + coverData?: Uint8Array + coverMime?: string + /** 秒 */ + duration?: number +} + +const emptyTag = (): MusicTag => ({ + title: "", + artist: "", + album: "", + albumArtist: "", + trackNumber: 0, + year: "", + genre: "", + lyrics: "", +}) + +const decodeLoose = (bytes: Uint8Array): string => { + try { + return new TextDecoder("utf-8", { fatal: false }).decode(bytes) + } catch { + let out = "" + for (const b of bytes) out += String.fromCharCode(b) + return out + } +} + +const decodeLatin1 = (bytes: Uint8Array): string => { + let out = "" + // 分块避免超长字符串拼接触发参数上限 + const chunk = 0x2000 + for (let i = 0; i < bytes.length; i += chunk) { + out += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return out +} + +const decodeUtf16 = (bytes: Uint8Array): string => { + try { + return new TextDecoder("utf-16").decode(bytes) + } catch { + return decodeLatin1(bytes) + } +} + +const decodeUtf16be = (bytes: Uint8Array): string => { + try { + return new TextDecoder("utf-16be").decode(bytes) + } catch { + return decodeLatin1(bytes) + } +} + +/** 按 ID3 文本编码字节解码,并去掉结尾 NUL / 全零填充 */ +function decodeId3Text(bytes: Uint8Array, encoding: number): string { + let text = "" + if (encoding === 1) text = decodeUtf16(bytes) + else if (encoding === 2) text = decodeUtf16be(bytes) + else if (encoding === 3) text = decodeLoose(bytes) + else text = decodeLatin1(bytes) + return text.replace(/\u0000+$/g, "").replace(/\u0000/g, " ").trim() +} + +const indexOfNul = (bytes: Uint8Array, start: number): number => { + for (let i = start; i < bytes.length; i++) if (bytes[i] === 0) return i + return -1 +} + +/** UTF-16 编码下的双字节 NUL */ +const indexOfNul16 = (bytes: Uint8Array, start: number): number => { + for (let i = start; i + 1 < bytes.length; i += 2) { + if (bytes[i] === 0 && bytes[i + 1] === 0) return i + } + return -1 +} + +const syncsafeToInt = (b: Uint8Array, off: number): number => + ((b[off] & 0x7f) << 21) | + ((b[off + 1] & 0x7f) << 14) | + ((b[off + 2] & 0x7f) << 7) | + (b[off + 3] & 0x7f) + +const beUint32 = (b: Uint8Array, off: number): number => + ((b[off] << 24) | (b[off + 1] << 16) | (b[off + 2] << 8) | b[off + 3]) >>> 0 + +const leUint32 = (b: Uint8Array, off: number): number => + (b[off] | (b[off + 1] << 8) | (b[off + 2] << 16) | (b[off + 3] << 24)) >>> 0 + +const beUint24 = (b: Uint8Array, off: number): number => + (b[off] << 16) | (b[off + 1] << 8) | b[off + 2] + +/** ID3v2 去同步(0xFF 0x00 -> 0xFF) */ +function deUnsynchronise(bytes: Uint8Array): Uint8Array { + const out = new Uint8Array(bytes.length) + let j = 0 + for (let i = 0; i < bytes.length; i++) { + out[j++] = bytes[i] + if (bytes[i] === 0xff && bytes[i + 1] === 0x00) i += 1 + } + return out.subarray(0, j) +} + +/** 解析 ID3v2 头部中的帧,返回标签;失败返回 null */ +export function parseID3v2(bytes: Uint8Array): MusicTag | null { + if (bytes.length < 10) return null + if (!(bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33)) return null + const major = bytes[3] + const flags = bytes[5] + const size = syncsafeToInt(bytes, 6) + if (size <= 0) return null + + let body = bytes.subarray(10, Math.min(bytes.length, 10 + size)) + if (flags & 0x80) body = deUnsynchronise(body) + + const tag = emptyTag() + // 2.2 的帧头是 6 字节(id3 + size3),2.3/2.4 是 10 字节 + const headerSize = major === 2 ? 6 : 10 + let off = 0 + // 扩展头(2.3/2.4) + if ((flags & 0x40) && major !== 2 && body.length >= 14) { + const extSize = + major === 4 ? syncsafeToInt(body, 0) : beUint32(body, 0) + 4 + off += Math.max(0, extSize) + } + + while (off + headerSize <= body.length) { + const idBytes = body.subarray(off, off + (major === 2 ? 3 : 4)) + if (idBytes.every((b) => b === 0)) break + const id = decodeLatin1(idBytes) + if (!/^[A-Z0-9]+$/.test(id)) break + + let frameSize: number + if (major === 2) { + frameSize = (body[off + 3] << 16) | (body[off + 4] << 8) | body[off + 5] + } else if (major === 4) { + frameSize = syncsafeToInt(body, off + 4) + } else { + frameSize = beUint32(body, off + 4) + } + const frameFlags = major === 2 ? 0 : (body[off + 8] << 8) | body[off + 9] + off += headerSize + if (frameSize <= 0 || off + frameSize > body.length) break + let data = body.subarray(off, off + frameSize) + off += frameSize + (major === 2 ? 0 : 0) + + // 压缩/加密的帧直接跳过(极少见于音频标签) + if (major === 3 && (frameFlags & 0x00c0)) continue + if (major === 4 && (frameFlags & 0x000c)) continue + if (major === 4 && frameFlags & 0x0001 && data.length > 4) { + // data length indicator + const skip = syncsafeToInt(data, 0) + if (skip > 0 && skip < data.length) data = data.subarray(4) + } + if (data.length === 0) continue + + const textFrame = () => decodeId3Text(data.subarray(1), data[0]) + + switch (id) { + case "TIT2": + case "TT2": + tag.title = textFrame() + break + case "TPE1": + case "TP1": + tag.artist = textFrame() + break + case "TPE2": + case "TP2": + tag.albumArtist = textFrame() + break + case "TALB": + case "TAL": + tag.album = textFrame() + break + case "TCON": + case "TCO": + tag.genre = textFrame().replace(/\(\d+\)/g, "").trim() + break + case "TRCK": + case "TRK": { + const raw = textFrame() + const n = parseInt(raw.split("/")[0], 10) + tag.trackNumber = Number.isFinite(n) ? n : 0 + break + } + case "TYER": + case "TYE": + case "TDRC": { + const raw = textFrame() + const m = raw.match(/\d{4}/) + if (m) tag.year = m[0] + break + } + case "APIC": + case "PIC": { + // encoding(1) mime\0 type(1) desc\0 data + const enc = data[0] + let p = 1 + if (id === "PIC") { + // 3 字节图像格式("JPG"/"PNG"),同样不可信,只用于跳过 + p = 4 + } else { + const end = indexOfNul(data, p) + if (end < 0) break + p = end + 1 + } + if (p >= data.length) break + p += 1 // picture type + const descEnd = enc === 1 || enc === 2 ? indexOfNul16(data, p) : indexOfNul(data, p) + if (descEnd < 0) break + p = descEnd + (enc === 1 || enc === 2 ? 2 : 1) + if (p >= data.length) break + const cover = data.subarray(p) + // 不采信帧里声明的 MIME:那是文件作者随手填的,采信它等于让 + // image/svg+xml 之类的声明一路流到响应的 Content-Type(存储型 XSS)。 + const sniffed = sniffImageMime(cover) + if (sniffed) { + tag.coverData = cover + tag.coverMime = sniffed + } + break + } + case "USLT": + case "ULT": { + const enc = data[0] + let p = 4 // encoding + lang + const descEnd = enc === 1 || enc === 2 ? indexOfNul16(data, p) : indexOfNul(data, p) + if (descEnd >= 0) p = descEnd + (enc === 1 || enc === 2 ? 2 : 1) + tag.lyrics = decodeId3Text(data.subarray(p), enc) + break + } + case "SYLT": { + // 同步歌词无法直接转 LRC,保留纯文本部分 + break + } + case "TXXX": + case "TXX": { + // encoding desc\0 value;部分软件把歌词放在 TXXX:LYRICS + const enc = data[0] + const end = enc === 1 || enc === 2 ? indexOfNul16(data, 1) : indexOfNul(data, 1) + if (end < 0) break + const desc = decodeId3Text(data.subarray(1, end), enc).toLowerCase() + const value = decodeId3Text( + data.subarray(end + (enc === 1 || enc === 2 ? 2 : 1)), + enc, + ) + if (desc.includes("lyric") && value) tag.lyrics = value + break + } + default: + break + } + } + + return tag +} + +/** ID3v1:末尾 128 字节,编码为 latin1 */ +export function parseID3v1(tail: Uint8Array): MusicTag | null { + if (tail.length < 128) return null + const start = tail.length - 128 + if (!(tail[start] === 0x54 && tail[start + 1] === 0x41 && tail[start + 2] === 0x47)) { + return null + } + const field = (from: number, len: number) => { + const raw = tail.subarray(start + from, start + from + len) + const end = indexOfNul(raw, 0) + return decodeLatin1(end >= 0 ? raw.subarray(0, end) : raw).trim() + } + const tag = emptyTag() + tag.title = field(3, 30) + tag.artist = field(33, 30) + tag.album = field(63, 30) + const year = field(93, 4) + if (/^\d{4}$/.test(year)) tag.year = year + const comment = field(97, 30) + if (comment) tag.lyrics = "" + // ID3v1.1:comment 最后两字节是 track(倒数第二字节 0) + const trackByte = tail[start + 125] + if (trackByte !== 0 && tail[start + 126] === 0) tag.trackNumber = trackByte + return tag +} + +/** FLAC:解析 fLaC 之后的 metadata blocks */ +export function parseFLAC(bytes: Uint8Array): MusicTag | null { + if (bytes.length < 8) return null + if (!(bytes[0] === 0x66 && bytes[1] === 0x4c && bytes[2] === 0x61 && bytes[3] === 0x43)) { + return null + } + const tag = emptyTag() + let off = 4 + while (off + 4 <= bytes.length) { + const header = bytes[off] + const last = (header & 0x80) !== 0 + const type = header & 0x7f + // metadata block 的 24 位长度是**大端**;只有块内的 Vorbis 载荷长度是小端 + const len = beUint24(bytes, off + 1) + off += 4 + if (len < 0 || off + len > bytes.length) break + const block = bytes.subarray(off, off + len) + + if (type === 0 && block.length >= 18) { + // STREAMINFO: min/max block(4) min/max frame(6) then 8 bytes packed + const sampleRate = (block[10] << 12) | (block[11] << 4) | (block[12] >> 4) + const low = + ((block[14] << 24) | (block[15] << 16) | (block[16] << 8) | block[17]) >>> + 0 + const totalSamples = (block[13] & 0x0f) * 4294967296 + low + if (sampleRate > 0 && totalSamples > 0) { + tag.duration = Math.round(totalSamples / sampleRate) + } + } else if (type === 4) { + // VORBIS_COMMENT + let p = 0 + if (block.length < 8) break + const vendorLen = leUint32(block, p) + p += 4 + vendorLen + if (p + 4 > block.length) break + const count = leUint32(block, p) + p += 4 + for (let i = 0; i < count && p + 4 <= block.length; i++) { + const l = leUint32(block, p) + p += 4 + if (l <= 0 || p + l > block.length) break + const entry = decodeLoose(block.subarray(p, p + l)) + p += l + const eq = entry.indexOf("=") + if (eq <= 0) continue + const key = entry.slice(0, eq).toUpperCase() + const val = entry.slice(eq + 1).trim() + if (!val) continue + switch (key) { + case "TITLE": + tag.title = val + break + case "ARTIST": + tag.artist = val + break + case "ALBUM": + tag.album = val + break + case "ALBUMARTIST": + case "ALBUM ARTIST": + tag.albumArtist = val + break + case "TRACKNUMBER": + case "TRACK": { + const n = parseInt(val.split("/")[0], 10) + if (Number.isFinite(n)) tag.trackNumber = n + break + } + case "DATE": + case "YEAR": { + const m = val.match(/\d{4}/) + if (m) tag.year = m[0] + break + } + case "GENRE": + tag.genre = val + break + case "LYRICS": + case "UNSYNCEDLYRICS": + tag.lyrics = val + break + default: + break + } + } + } else if (type === 6) { + // PICTURE + if (block.length < 32) break + let p = 0 + p += 4 // picture type + const mimeLen = beUint32(block, p) + p += 4 + if (p + mimeLen > block.length) break + p += mimeLen // 跳过声明 MIME:只按魔数判定 + const descLen = beUint32(block, p) + p += 4 + descLen + p += 16 // width/height/depth/colors + if (p + 4 > block.length) break + const dataLen = beUint32(block, p) + p += 4 + if (dataLen > 0 && p + dataLen <= block.length) { + const cover = block.subarray(p, p + dataLen) + const sniffed = sniffImageMime(cover) + if (sniffed) { + tag.coverData = cover + tag.coverMime = sniffed + } + } + } + + off += len + if (last) break + } + return tag +} + +/** M4A/MP4:moov -> udta -> meta -> ilst(尽力而为,不处理 64 位 box) */ +export function parseM4ATags(bytes: Uint8Array): MusicTag | null { + if (bytes.length < 12) return null + const typeAt = (off: number) => decodeLatin1(bytes.subarray(off + 4, off + 8)) + if (typeAt(0) !== "ftyp") return null + + const findBox = (start: number, end: number, name: string) => { + let off = start + while (off + 8 <= end) { + let size = beUint32(bytes, off) + const type = typeAt(off) + if (size === 1) { + // 64-bit size,跳过(极少用于元数据) + return null + } + if (size === 0) size = end - off + if (size < 8 || off + size > end) return null + if (type === name) return { start: off + 8, end: off + size } + off += size + } + return null + } + + const moov = findBox(0, bytes.length, "moov") + if (!moov) return null + const udta = findBox(moov.start, moov.end, "udta") + if (!udta) return null + const meta = findBox(udta.start, udta.end, "meta") + if (!meta) return null + const ilst = findBox(meta.start + 4, meta.end, "ilst") + if (!ilst) return null + + const tag = emptyTag() + let off = ilst.start + while (off + 8 <= ilst.end) { + const size = beUint32(bytes, off) + if (size < 8 || off + size > ilst.end) break + const atom = decodeLatin1(bytes.subarray(off + 4, off + 8)).replace(/\u00a9/g, "©") + const dataStart = off + 8 + if (dataStart + 16 <= off + size) { + // data: size(4) type(4) locale(4) payload + const dSize = beUint32(bytes, dataStart) + const dType = beUint32(bytes, dataStart + 4) + const payloadStart = dataStart + 16 + const payloadEnd = Math.min(dataStart + dSize, off + size) + const payload = bytes.subarray(payloadStart, payloadEnd) + const text = () => decodeLoose(payload).replace(/\u0000+$/g, "").trim() + switch (atom) { + case "©nam": + tag.title = text() + break + case "©ART": + tag.artist = text() + break + case "aART": + tag.albumArtist = text() + break + case "©alb": + tag.album = text() + break + case "©gen": + case "gnre": + tag.genre = text() + break + case "©day": { + const m = text().match(/\d{4}/) + if (m) tag.year = m[0] + break + } + case "trkn": { + // 8 字节:0 0 track(2) total(2) + if (payload.length >= 4) tag.trackNumber = (payload[2] << 8) | payload[3] + break + } + case "covr": { + if (dType === 13 || dType === 14) { + // 与 APIC / FLAC 一致:只按魔数判定,dType 只是「这本该是 PNG/JPEG」的提示 + const sniffed = sniffImageMime(payload) + if (sniffed) { + tag.coverData = payload + tag.coverMime = sniffed + } + } + break + } + case "©lyr": + tag.lyrics = text() + break + default: + break + } + } + off += size + } + return tag +} + +/** 合并多个来源的标签:先到先得(不覆盖已有非空值) */ +export function mergeTags(base: MusicTag, extra: MusicTag | null): MusicTag { + if (!extra) return base + const out = { ...base } + const keys: Array<keyof MusicTag> = [ + "title", + "artist", + "album", + "albumArtist", + "genre", + "lyrics", + ] + for (const k of keys) { + if (!out[k] && extra[k]) (out as any)[k] = extra[k] + } + if (!out.trackNumber && extra.trackNumber) out.trackNumber = extra.trackNumber + if (!out.year && extra.year) out.year = extra.year + if (!out.coverData && extra.coverData) { + out.coverData = extra.coverData + out.coverMime = extra.coverMime + } + if (!out.duration && extra.duration) out.duration = extra.duration + return out +} + +/** 按扩展名选择解析器(head 至少包含文件头,flac 建议 ≥64KB) */ +export function parseMusicTags( + ext: string, + head: Uint8Array | null, + tail?: Uint8Array | null, +): MusicTag { + const e = ext.toLowerCase() + let tag: MusicTag | null = null + try { + if (head && head.length > 0) { + if (e === "flac") tag = parseFLAC(head) + else if (e === "m4a" || e === "mp4" || e === "alac") tag = parseM4ATags(head) + else tag = parseID3v2(head) + } + if (!tag && tail && tail.length > 0 && (e === "mp3" || e === "wav")) { + tag = parseID3v1(tail) + } + } catch (err: any) { + console.warn("[media] parse music tag failed:", err?.message || err) + } + return tag || emptyTag() +} + +// ==================== 图片 ==================== + +/** 按魔数识别图片 MIME;非栅格图(svg/html)返回空串 */ +export function sniffImageMime(bytes: Uint8Array): string { + if (bytes.length < 12) return "" + const b = bytes + if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return "image/jpeg" + if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) { + return "image/png" + } + if (b[0] === 0x47 && b[1] === 0x49 && b[2] === 0x46) return "image/gif" + if ( + b[0] === 0x52 && + b[1] === 0x49 && + b[2] === 0x46 && + b[3] === 0x46 && + b[8] === 0x57 && + b[9] === 0x45 && + b[10] === 0x42 && + b[11] === 0x50 + ) { + return "image/webp" + } + if (b[0] === 0x42 && b[1] === 0x4d) return "image/bmp" + if ( + b[4] === 0x66 && + b[5] === 0x74 && + b[6] === 0x79 && + b[7] === 0x70 + ) { + const brand = decodeLatin1(b.subarray(8, 12)) + if (brand.startsWith("avif") || brand.startsWith("avis")) return "image/avif" + if (brand.startsWith("heic") || brand.startsWith("heix")) return "image/heic" + if (brand.startsWith("mif1")) return "image/heif" + } + return "" +} + +/** + * 从 JPEG APP1 段中提取 EXIF DateTimeOriginal(YYYY-MM-DD)。 + * + * 只处理未压缩的 EXIF(TIFF header + IFD0/ExifIFD),足以覆盖相机直出。 + */ +export function parseJpegExifDate(bytes: Uint8Array): string { + if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) return "" + let off = 2 + while (off + 4 <= bytes.length) { + if (bytes[off] !== 0xff) break + const marker = bytes[off + 1] + off += 2 + if (marker === 0xd8 || marker === 0xd9) continue + if (off + 2 > bytes.length) break + const len = (bytes[off] << 8) | bytes[off + 1] + if (len < 2 || off + len > bytes.length) break + if (marker === 0xe1) { + const seg = bytes.subarray(off + 2, off + len) + if ( + seg.length > 6 && + seg[0] === 0x45 && + seg[1] === 0x78 && + seg[2] === 0x69 && + seg[3] === 0x66 + ) { + const date = parseTiffDate(seg.subarray(6)) + if (date) return date + } + } + off += len + if (marker === 0xda) break + } + return "" +} + +function parseTiffDate(tiff: Uint8Array): string { + if (tiff.length < 8) return "" + const little = tiff[0] === 0x49 && tiff[1] === 0x49 + if (!little && !(tiff[0] === 0x4d && tiff[1] === 0x4d)) return "" + const u16 = (o: number) => + little ? tiff[o] | (tiff[o + 1] << 8) : (tiff[o] << 8) | tiff[o + 1] + const u32 = (o: number) => + little + ? (tiff[o] | (tiff[o + 1] << 8) | (tiff[o + 2] << 16) | (tiff[o + 3] << 24)) >>> 0 + : ((tiff[o] << 24) | (tiff[o + 1] << 16) | (tiff[o + 2] << 8) | tiff[o + 3]) >>> 0 + + const readAscii = (entryOff: number): string => { + const type = u16(entryOff + 2) + const count = u32(entryOff + 4) + if (type !== 2 || count <= 1 || count > 32) return "" + const valueOff = count <= 4 ? entryOff + 8 : u32(entryOff + 8) + if (valueOff + count > tiff.length) return "" + return decodeLatin1(tiff.subarray(valueOff, valueOff + count)).replace(/\u0000+$/g, "") + } + + const ifd0 = u32(4) + if (ifd0 + 2 > tiff.length) return "" + const count0 = u16(ifd0) + let exifIfd = 0 + for (let i = 0; i < count0; i++) { + const entry = ifd0 + 2 + i * 12 + if (entry + 12 > tiff.length) break + const tag = u16(entry) + if (tag === 0x8769) exifIfd = u32(entry + 8) + if (tag === 0x0132) { + const d = readAscii(entry) + const m = d.match(/(\d{4}):(\d{2}):(\d{2})/) + if (m) return `${m[1]}-${m[2]}-${m[3]}` + } + } + if (exifIfd && exifIfd + 2 <= tiff.length) { + const countE = u16(exifIfd) + for (let i = 0; i < countE; i++) { + const entry = exifIfd + 2 + i * 12 + if (entry + 12 > tiff.length) break + const tag = u16(entry) + if (tag === 0x9003 || tag === 0x9004) { + const d = readAscii(entry) + const m = d.match(/(\d{4}):(\d{2}):(\d{2})/) + if (m) return `${m[1]}-${m[2]}-${m[3]}` + } + } + } + return "" +} + +// ==================== ZIP(EPUB / CBZ) ==================== + +export interface ZipEntry { + name: string + method: number + compressedSize: number + uncompressedSize: number + localOffset: number +} + +/** 解析 ZIP 中央目录(只支持单卷、非 ZIP64) */ +export function listZipEntries(bytes: Uint8Array): ZipEntry[] { + if (bytes.length < 22) return [] + const min = Math.max(0, bytes.length - 65557) + let eocd = -1 + for (let i = bytes.length - 22; i >= min; i--) { + if ( + bytes[i] === 0x50 && + bytes[i + 1] === 0x4b && + bytes[i + 2] === 0x05 && + bytes[i + 3] === 0x06 + ) { + eocd = i + break + } + } + if (eocd < 0) return [] + const total = bytes[eocd + 10] | (bytes[eocd + 11] << 8) + const cdOffset = leUint32(bytes, eocd + 16) + if (cdOffset <= 0 || cdOffset >= bytes.length) return [] + + const out: ZipEntry[] = [] + let off = cdOffset + for (let i = 0; i < total; i++) { + if (off + 46 > bytes.length) break + if (!(bytes[off] === 0x50 && bytes[off + 1] === 0x4b && bytes[off + 2] === 0x01 && bytes[off + 3] === 0x02)) { + break + } + const method = bytes[off + 10] | (bytes[off + 11] << 8) + const compressedSize = leUint32(bytes, off + 20) + const uncompressedSize = leUint32(bytes, off + 24) + const nameLen = bytes[off + 28] | (bytes[off + 29] << 8) + const extraLen = bytes[off + 30] | (bytes[off + 31] << 8) + const commentLen = bytes[off + 32] | (bytes[off + 33] << 8) + const localOffset = leUint32(bytes, off + 42) + const name = decodeLoose(bytes.subarray(off + 46, off + 46 + nameLen)) + out.push({ name, method, compressedSize, uncompressedSize, localOffset }) + off += 46 + nameLen + extraLen + commentLen + } + return out +} + +/** 解压单个 ZIP 条目(method 0 或 deflate) */ +export async function readZipEntry( + bytes: Uint8Array, + entry: ZipEntry, + maxBytes = 8 * 1024 * 1024, +): Promise<Uint8Array | null> { + const off = entry.localOffset + if (off + 30 > bytes.length) return null + if (!(bytes[off] === 0x50 && bytes[off + 1] === 0x4b && bytes[off + 2] === 0x03 && bytes[off + 3] === 0x04)) { + return null + } + const nameLen = bytes[off + 26] | (bytes[off + 27] << 8) + const extraLen = bytes[off + 28] | (bytes[off + 29] << 8) + const dataStart = off + 30 + nameLen + extraLen + if (dataStart > bytes.length) return null + + if (entry.method === 0) { + const end = Math.min(bytes.length, dataStart + (entry.compressedSize || entry.uncompressedSize)) + return bytes.subarray(dataStart, end) + } + if (entry.method !== 8) return null + const end = Math.min(bytes.length, dataStart + entry.compressedSize) + const compressed = bytes.subarray(dataStart, end) + if (compressed.length === 0) return null + + try { + const DS: any = (globalThis as any).DecompressionStream + if (!DS) return null + const stream = new Blob([compressed as BlobPart]) + .stream() + .pipeThrough(new DS("deflate-raw")) + const buf = new Uint8Array(await new Response(stream).arrayBuffer()) + return buf.length <= maxBytes ? buf : null + } catch { + return null + } +} + +/** + * 从 EPUB 中提取封面。 + * + * 顺序:META-INF/container.xml → OPF → `<meta name="cover" content="id">` + * → manifest item href;回退为「href/属性含 cover 的第一张图片」。 + */ +export async function extractEpubCover( + bytes: Uint8Array, + maxBytes = 4 * 1024 * 1024, +): Promise<{ data: Uint8Array; mime: string } | null> { + const entries = listZipEntries(bytes) + if (entries.length === 0) return null + const byName = new Map(entries.map((e) => [e.name.toLowerCase(), e])) + + const readText = async (name: string) => { + const e = byName.get(name.toLowerCase()) + if (!e) return "" + const data = await readZipEntry(bytes, e, maxBytes) + return data ? decodeLoose(data) : "" + } + + let opfPath = "" + const container = await readText("META-INF/container.xml") + if (container) { + const m = container.match(/full-path="([^"]+)"/i) + if (m) opfPath = m[1] + } + if (!opfPath) { + const opf = entries.find((e) => e.name.toLowerCase().endsWith(".opf")) + if (!opf) return null + opfPath = opf.name + } + const opf = await readText(opfPath) + if (!opf) return null + + const baseDir = opfPath.includes("/") + ? opfPath.slice(0, opfPath.lastIndexOf("/") + 1) + : "" + + const join = (href: string) => { + const raw = decodeURI(href.split("#")[0]) + const parts = (baseDir + raw).split("/") + const stack: string[] = [] + for (const p of parts) { + if (!p || p === ".") continue + if (p === "..") stack.pop() + else stack.push(p) + } + return stack.join("/") + } + + // manifest id -> href + const manifest = new Map<string, { href: string; mediaType: string }>() + const itemRe = /<item\b[^>]*>/gi + let m: RegExpExecArray | null + while ((m = itemRe.exec(opf))) { + const tag = m[0] + const id = (tag.match(/\bid="([^"]*)"/i) || [])[1] || "" + const href = (tag.match(/\bhref="([^"]*)"/i) || [])[1] || "" + const mediaType = (tag.match(/\bmedia-type="([^"]*)"/i) || [])[1] || "" + if (id && href) manifest.set(id, { href, mediaType }) + } + + const candidates: string[] = [] + const metaCover = opf.match(/<meta\b[^>]*name="cover"[^>]*content="([^"]+)"/i) + if (metaCover) { + const item = manifest.get(metaCover[1]) + if (item) candidates.push(join(item.href)) + } + for (const [, item] of manifest) { + if (item.mediaType.startsWith("image/")) { + const href = join(item.href) + if (/cover/i.test(href)) candidates.push(href) + } + } + for (const [id, item] of manifest) { + if (item.mediaType.startsWith("image/") && /cover/i.test(id)) { + candidates.push(join(item.href)) + } + } + // 最后兜底:任意图片 + for (const [, item] of manifest) { + if (item.mediaType.startsWith("image/")) candidates.push(join(item.href)) + } + + for (const name of candidates) { + const e = byName.get(name.toLowerCase()) + if (!e) continue + const data = await readZipEntry(bytes, e, maxBytes) + if (!data || data.length === 0) continue + const mime = sniffImageMime(data) + if (!mime) continue + return { data, mime } + } + return null +} + +/** 从 CBZ(漫画/图片压缩包)中取第一张图片作为封面 */ +export async function extractCbzCover( + bytes: Uint8Array, + maxBytes = 8 * 1024 * 1024, +): Promise<{ data: Uint8Array; mime: string } | null> { + const entries = listZipEntries(bytes).filter((e) => + /\.(jpe?g|png|webp|gif)$/i.test(e.name), + ) + entries.sort((a, b) => a.name.localeCompare(b.name, "zh-CN")) + for (const e of entries.slice(0, 4)) { + const data = await readZipEntry(bytes, e, maxBytes) + if (!data) continue + const mime = sniffImageMime(data) + if (mime) return { data, mime } + } + return null +} + +/** 解析 LRC 文本:去掉 BOM,限制长度 */ +export function normalizeLyrics(text: string, maxBytes = 512 * 1024): string { + if (!text) return "" + let s = text.replace(/^\uFEFF/, "") + if (s.length > maxBytes) s = s.slice(0, maxBytes) + return s.trim() +} + +// ==================== PDF 封面 ==================== +// +// PDF 没有「封面」概念,这里提取文档中面积最大的内嵌图像作为封面: +// - DCTDecode(JPEG)流直接返回(绝大多数扫描版封面/插图都是这种) +// - FlateDecode(zlib)流解压后按 PNG 预测器还原,再编码为 PNG +// - JPXDecode / JBIG2Decode 浏览器不支持,跳过 +// +// 只做「流对象扫描」而不实现完整 xref 解析:封面图一定以 `obj ... stream` +// 形式存在,找不到就返回 null,不影响刮削主流程。 + +const PDF_CHAR = (s: string) => Array.from(s, (c) => c.charCodeAt(0)) + +function indexOfBytes( + haystack: Uint8Array, + needle: string, + from = 0, +): number { + const pat = PDF_CHAR(needle) + const n = pat.length + if (n === 0) return -1 + const first = pat[0] + const limit = haystack.length - n + for (let i = Math.max(0, from); i <= limit; i++) { + if (haystack[i] !== first) continue + let j = 1 + while (j < n && haystack[i + j] === pat[j]) j++ + if (j === n) return i + } + return -1 +} + +function lastIndexOfBytes( + haystack: Uint8Array, + needle: string, + before: number, + window = 8192, +): number { + const pat = PDF_CHAR(needle) + const n = pat.length + const from = Math.max(0, before - window) + const last = Math.min(before, haystack.length - n) + for (let i = last; i >= from; i--) { + let j = 0 + while (j < n && haystack[i + j] === pat[j]) j++ + if (j === n) return i + } + return -1 +} + +interface PdfImageStream { + width: number + height: number + filters: string[] + colors: number + bpc: number + predictor: number + dataStart: number + dataEnd: number +} + +function parsePdfImages(bytes: Uint8Array): PdfImageStream[] { + const out: PdfImageStream[] = [] + let cursor = 0 + for (;;) { + const streamIdx = indexOfBytes(bytes, "stream", cursor) + if (streamIdx < 0) break + cursor = streamIdx + 6 + + const objIdx = lastIndexOfBytes(bytes, "obj", streamIdx, 8192) + if (objIdx < 0) continue + const dict = decodeLatin1(bytes.subarray(objIdx + 3, streamIdx)) + if (!/\/Subtype\s*\/Image/.test(dict)) continue + + let dataStart = streamIdx + 6 + if (bytes[dataStart] === 0x0d) dataStart += 1 + if (bytes[dataStart] === 0x0a) dataStart += 1 + + const endIdx = indexOfBytes(bytes, "endstream", dataStart) + if (endIdx < 0) break + let dataEnd = endIdx + while ( + dataEnd > dataStart && + (bytes[dataEnd - 1] === 0x0a || bytes[dataEnd - 1] === 0x0d) + ) { + dataEnd -= 1 + } + + const num = (re: RegExp, def = 0) => { + const m = dict.match(re) + return m ? Number(m[1]) || def : def + } + const width = num(/\/Width\s+(\d+)/) + const height = num(/\/Height\s+(\d+)/) + if (width < 50 || height < 50) continue + + const filterArr = dict.match(/\/Filter\s*\[([^\]]*)\]/) + const filterSingle = dict.match(/\/Filter\s*\/([A-Za-z0-9]+)/) + const filters = filterArr + ? Array.from(filterArr[1].matchAll(/\/([A-Za-z0-9]+)/g), (m) => m[1]) + : filterSingle + ? [filterSingle[1]] + : [] + + let colors = 3 + if (/\/ColorSpace\s*\/DeviceGray/.test(dict)) colors = 1 + else if (/\/ColorSpace\s*\/DeviceCMYK/.test(dict)) colors = 4 + else if (/\/ColorSpace\s*\/DeviceRGB/.test(dict)) colors = 3 + + const bpc = num(/\/BitsPerComponent\s+(\d+)/, 8) + const predictor = (() => { + const m = dict.match(/\/DecodeParms\s*<<([\s\S]*?)>>/) + if (!m) return 1 + const p = m[1].match(/\/Predictor\s+(\d+)/) + return p ? Number(p[1]) || 1 : 1 + })() + + out.push({ + width, + height, + filters, + colors, + bpc, + predictor, + dataStart, + dataEnd, + }) + } + return out +} + +async function inflateZlib(data: Uint8Array): Promise<Uint8Array | null> { + try { + const DS: any = (globalThis as any).DecompressionStream + if (!DS) return null + const stream = new Blob([data as BlobPart]) + .stream() + .pipeThrough(new DS("deflate")) + return new Uint8Array(await new Response(stream).arrayBuffer()) + } catch { + return null + } +} + +async function deflateZlib(data: Uint8Array): Promise<Uint8Array | null> { + try { + const CS: any = (globalThis as any).CompressionStream + if (!CS) return null + const stream = new Blob([data as BlobPart]) + .stream() + .pipeThrough(new CS("deflate")) + return new Uint8Array(await new Response(stream).arrayBuffer()) + } catch { + return null + } +} + +/** Adobe ASCII85 解码(PDF 常用 ASCII85Decode + FlateDecode) */ +function ascii85Decode(input: Uint8Array): Uint8Array | null { + const text = decodeLatin1(input).replace(/\s+/g, "") + let s = text + if (s.startsWith("<~")) s = s.slice(2) + const end = s.indexOf("~>") + if (end >= 0) s = s.slice(0, end) + const out: number[] = [] + let group: number[] = [] + for (const ch of s) { + if (ch === "z" && group.length === 0) { + out.push(0, 0, 0, 0) + continue + } + const code = ch.charCodeAt(0) - 33 + if (code < 0 || code > 84) return null + group.push(code) + if (group.length === 5) { + let value = 0 + for (const g of group) value = value * 85 + g + out.push( + (value >>> 24) & 0xff, + (value >>> 16) & 0xff, + (value >>> 8) & 0xff, + value & 0xff, + ) + group = [] + } + } + if (group.length > 0) { + const missing = 5 - group.length + while (group.length < 5) group.push(84) + let value = 0 + for (const g of group) value = value * 85 + g + const bytes = [ + (value >>> 24) & 0xff, + (value >>> 16) & 0xff, + (value >>> 8) & 0xff, + value & 0xff, + ] + out.push(...bytes.slice(0, 4 - missing)) + } + return new Uint8Array(out) +} + +const paeth = (a: number, b: number, c: number) => { + const p = a + b - c + const pa = Math.abs(p - a) + const pb = Math.abs(p - b) + const pc = Math.abs(p - c) + if (pa <= pb && pa <= pc) return a + if (pb <= pc) return b + return c +} + +/** 还原 PNG 预测器(Predictor >= 10) */ +function unpredict( + data: Uint8Array, + width: number, + channels: number, + bpc: number, + height: number, +): Uint8Array | null { + const bpp = Math.max(1, Math.ceil((channels * bpc) / 8)) + const stride = Math.ceil((width * channels * bpc) / 8) + if (data.length < height * (stride + 1)) return null + const out = new Uint8Array(stride * height) + let prev = new Uint8Array(stride) + for (let y = 0; y < height; y++) { + const base = y * (stride + 1) + const filter = data[base] + const cur = new Uint8Array(stride) + for (let x = 0; x < stride; x++) { + const raw = data[base + 1 + x] + const left = x >= bpp ? cur[x - bpp] : 0 + const up = prev[x] + const upLeft = x >= bpp ? prev[x - bpp] : 0 + let value = raw + if (filter === 1) value = (raw + left) & 0xff + else if (filter === 2) value = (raw + up) & 0xff + else if (filter === 3) value = (raw + ((left + up) >> 1)) & 0xff + else if (filter === 4) value = (raw + paeth(left, up, upLeft)) & 0xff + cur[x] = value + } + out.set(cur, y * stride) + prev = cur + } + return out +} + +/** 原始像素 → RGB(只处理 8bit 与 1bit 灰度) */ +function toRgb( + pixels: Uint8Array, + width: number, + height: number, + channels: number, + bpc: number, +): Uint8Array | null { + const count = width * height + const out = new Uint8Array(count * 3) + if (bpc === 8) { + if (channels === 3) { + if (pixels.length < count * 3) return null + out.set(pixels.subarray(0, count * 3)) + return out + } + if (channels === 1) { + if (pixels.length < count) return null + for (let i = 0; i < count; i++) { + const v = pixels[i] + out[i * 3] = v + out[i * 3 + 1] = v + out[i * 3 + 2] = v + } + return out + } + if (channels === 4) { + if (pixels.length < count * 4) return null + for (let i = 0; i < count; i++) { + const c = pixels[i * 4] + const m = pixels[i * 4 + 1] + const y = pixels[i * 4 + 2] + const k = pixels[i * 4 + 3] + out[i * 3] = 255 - Math.min(255, c + k) + out[i * 3 + 1] = 255 - Math.min(255, m + k) + out[i * 3 + 2] = 255 - Math.min(255, y + k) + } + return out + } + return null + } + if (bpc === 1 && channels === 1) { + const stride = Math.ceil(width / 8) + if (pixels.length < stride * height) return null + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const bit = (pixels[y * stride + (x >> 3)] >> (7 - (x & 7))) & 1 + const v = bit ? 255 : 0 + const idx = (y * width + x) * 3 + out[idx] = v + out[idx + 1] = v + out[idx + 2] = v + } + } + return out + } + return null +} + +let crcTable: Uint32Array | null = null +function crc32(data: Uint8Array): number { + if (!crcTable) { + crcTable = new Uint32Array(256) + for (let n = 0; n < 256; n++) { + let c = n + for (let k = 0; k < 8; k++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1 + } + crcTable[n] = c >>> 0 + } + } + let crc = 0xffffffff + for (let i = 0; i < data.length; i++) { + crc = crcTable[(crc ^ data[i]) & 0xff] ^ (crc >>> 8) + } + return (crc ^ 0xffffffff) >>> 0 +} + +const u32be = (n: number) => [ + (n >>> 24) & 0xff, + (n >>> 16) & 0xff, + (n >>> 8) & 0xff, + n & 0xff, +] + +function pngChunk(type: string, payload: Uint8Array): Uint8Array { + const typeBytes = PDF_CHAR(type) + const out = new Uint8Array(12 + payload.length) + out.set(u32be(payload.length), 0) + out.set(typeBytes, 4) + out.set(payload, 8) + const crcInput = new Uint8Array(4 + payload.length) + crcInput.set(typeBytes, 0) + crcInput.set(payload, 4) + out.set(u32be(crc32(crcInput)), 8 + payload.length) + return out +} + +/** 把 RGB 原始像素编码成 PNG(Worker 没有 canvas,只能手写最小编码器) */ +async function encodePng( + width: number, + height: number, + rgb: Uint8Array, +): Promise<Uint8Array | null> { + const stride = width * 3 + const raw = new Uint8Array((stride + 1) * height) + for (let y = 0; y < height; y++) { + raw[y * (stride + 1)] = 0 + raw.set(rgb.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1) + } + const compressed = await deflateZlib(raw) + if (!compressed) return null + + const ihdr = new Uint8Array([ + ...u32be(width), + ...u32be(height), + 8, // bit depth + 2, // color type: RGB + 0, + 0, + 0, + ]) + const sig = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) + const chunks = [ + pngChunk("IHDR", ihdr), + pngChunk("IDAT", compressed), + pngChunk("IEND", new Uint8Array(0)), + ] + const total = sig.length + chunks.reduce((n, c) => n + c.length, 0) + const out = new Uint8Array(total) + let pos = 0 + out.set(sig, pos) + pos += sig.length + for (const c of chunks) { + out.set(c, pos) + pos += c.length + } + return out +} + +/** 从 PDF 中提取面积最大的内嵌图像作为封面 */ +export async function extractPdfCover( + bytes: Uint8Array, + maxOutputBytes = 4 * 1024 * 1024, +): Promise<{ data: Uint8Array; mime: string } | null> { + try { + const images = parsePdfImages(bytes) + if (images.length === 0) return null + images.sort((a, b) => b.width * b.height - a.width * a.height) + + for (const img of images.slice(0, 4)) { + const raw = bytes.subarray(img.dataStart, img.dataEnd) + if (raw.length === 0) continue + const filters = img.filters.map((f) => f.toLowerCase()) + + // JPX/JBIG2 浏览器无法解码 + if (filters.some((f) => f.includes("jpx") || f.includes("jbig"))) continue + // 已经是 JPEG:直接返回 + if (filters.some((f) => f.includes("dct"))) { + const mime = sniffImageMime(raw) + if (mime === "image/jpeg") return { data: raw, mime } + continue + } + + let stream: Uint8Array | null = raw + if (filters.some((f) => f.includes("ascii85"))) { + stream = ascii85Decode(stream) + if (!stream) continue + } + if (filters.some((f) => f.includes("flate"))) { + stream = await inflateZlib(stream) + if (!stream) continue + } + if (!stream) continue + + let pixels = stream + if (img.predictor >= 10) { + const restored = unpredict( + stream, + img.width, + img.colors, + img.bpc, + img.height, + ) + if (!restored) continue + pixels = restored + } + + const rgb = toRgb(pixels, img.width, img.height, img.colors, img.bpc) + if (!rgb) continue + const png = await encodePng(img.width, img.height, rgb) + if (!png || png.length > maxOutputBytes) continue + return { data: png, mime: "image/png" } + } + } catch (e: any) { + console.warn("[media] parse pdf cover failed:", e?.message || e) + } + return null +} diff --git a/src/backend/internal/media/thumb.ts b/src/backend/internal/media/thumb.ts new file mode 100644 index 00000000..d3346d01 --- /dev/null +++ b/src/backend/internal/media/thumb.ts @@ -0,0 +1,134 @@ +/** + * 封面/缩略图落盘。 + * + * Worker 没有 sharp / canvas,无法真正缩放图片,因此这里做的是「原图缓存」: + * - store_thumbnail=true → 把原始封面字节写入持久化存储(KV/D1/DO), + * cover 写 `thumb:<hash>`,由 API 层解析为 /api/public/media/thumb?path=<hash> + * - store_thumbnail=false → 小于 256KB 的图直接 base64 内联,否则丢弃 + * + * 与 Go 的差异:Go 会把图片解码后按 thumbnail_mode 存成小图;Worker 侧 + * 保持原图(体积有上限),语义上仍满足「不依赖第三方图床」。 + */ + +import { saveThumb } from "./store" +import { sniffImageMime } from "./tag" + +/** + * 与 OpenList 客户端的约定: + * `thumb:<hash>` 是一个内部标记,不是可访问 URL。 + * 前端渲染前需调用 `/api/public/media/thumb?path=<hash>`。 + */ +export interface CoverResult { + cover: string + thumb: string + thumb_mime: string +} + +export interface ThumbOptions { + storeThumbnail: boolean + mode: string +} + +const MAX_INLINE_BYTES = 256 * 1024 + +export const emptyCover = (): CoverResult => ({ + cover: "", + thumb: "", + thumb_mime: "", +}) + +export async function sha256Hex(data: Uint8Array): Promise<string> { + const buf = data.buffer.slice( + data.byteOffset, + data.byteOffset + data.byteLength, + ) as ArrayBuffer + const digest = await crypto.subtle.digest("SHA-256", buf) + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") +} + +export function bytesToBase64(bytes: Uint8Array): string { + let bin = "" + const chunk = 0x2000 + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode(...bytes.subarray(i, i + chunk)) + } + return btoa(bin) +} + +/** 把图片字节转成 cover 字段值(落盘或内联) */ +export async function persistCover( + env: any, + data: Uint8Array | undefined, + opts: ThumbOptions, +): Promise<CoverResult> { + if (!data || data.length === 0) return emptyCover() + // 只信魔数。调用方给的 MIME 来自文件元数据(ID3 APIC / 刮削站点), + // 采信它就会把 image/svg+xml 这类可执行内容存成封面, + // 最终在 serveCover 的 data: 分支变成可执行的 Content-Type。 + const realMime = sniffImageMime(data) + if (!realMime) return emptyCover() + + if (!opts.storeThumbnail) { + if (data.length > MAX_INLINE_BYTES) return emptyCover() + return { + cover: `data:${realMime};base64,${bytesToBase64(data)}`, + thumb: "", + thumb_mime: realMime, + } + } + + const hash = (await sha256Hex(data)).slice(0, 32) + const stored = await saveThumb(env, hash, data, realMime) + if (!stored) return emptyCover() + return { cover: stored, thumb: hash, thumb_mime: realMime } +} + +/** + * `thumb:<hash>` / `data:` / `http(s):` 统一解析为可直接访问的 URL。 + * + * 外链封面默认走站内代理:豆瓣/部分图床有防盗链(空 Referer 或跨站 + * Referer 直接 403),TMDB 图片在国内也常不可达。直连时表现为「刮削成功 + * 但封面裂图」。代理端点带可信 Referer 与域名白名单,命中率明显更高。 + */ +export function resolveCoverUrl( + cover: string, + origin: string, + proxyExternal = true, +): string { + if (!cover) return "" + if (cover.startsWith("thumb:")) { + const hash = cover.slice("thumb:".length) + return `${origin}/api/public/media/thumb?path=${encodeURIComponent(hash)}` + } + if (proxyExternal && /^https?:\/\//i.test(cover)) { + return `${origin}/api/public/media/proxy?url=${encodeURIComponent(cover)}` + } + return cover +} + +export interface ThumbOptionsSource { + store_thumbnail?: boolean + thumbnail_mode?: string + scraper_config?: string +} + +/** 从媒体库配置解析缩略图选项(类型级 scraper_config 优先) */ +export function thumbOptionsOf(cfg: ThumbOptionsSource | null | undefined): ThumbOptions { + let storeThumbnail = cfg?.store_thumbnail !== false + let mode = String(cfg?.thumbnail_mode || "runtime") + const raw = cfg?.scraper_config + if (raw) { + try { + const sc = typeof raw === "string" ? JSON.parse(raw) : raw + if (sc?.store_thumbnail !== undefined && sc.store_thumbnail !== "") { + storeThumbnail = String(sc.store_thumbnail) === "true" + } + if (sc?.thumbnail_mode) mode = String(sc.thumbnail_mode) + } catch { + /* ignore */ + } + } + return { storeThumbnail, mode } +} diff --git a/src/backend/internal/model/db.ts b/src/backend/internal/model/db.ts index 9819a7e9..38894380 100644 --- a/src/backend/internal/model/db.ts +++ b/src/backend/internal/model/db.ts @@ -810,6 +810,203 @@ export const defaultDb = { group: 14, flag: 0, }, + + // Group 12: MEDIA (媒体库) + { + key: "media_enabled", + value: "false", + type: "bool", + help: "Enable media library (master switch)", + group: 12, + flag: 0, + }, + { + key: "media_emby_enabled", + value: "false", + type: "bool", + help: "Expose an Emby-compatible API at /@emby", + group: 12, + flag: 0, + }, + { + key: "media_subsonic_enabled", + value: "false", + type: "bool", + help: "Expose an OpenSubsonic API at /@rest", + group: 12, + flag: 0, + }, + { + key: "media_tmdb_key", + value: "", + type: "string", + help: "TMDB API key", + group: 12, + flag: 1, + }, + { + key: "media_tmdb_api_url", + value: "https://api.themoviedb.org/3", + type: "string", + help: "TMDB API base URL", + group: 12, + flag: 1, + }, + { + key: "media_discogs_token", + value: "", + type: "string", + help: "Discogs personal access token", + group: 12, + flag: 1, + }, + { + key: "media_discogs_api_url", + value: "https://api.discogs.com", + type: "string", + help: "Discogs API base URL", + group: 12, + flag: 1, + }, + { + key: "media_book_source", + value: "douban", + type: "select", + options: "douban,google,openlibrary", + help: "Book metadata source", + group: 12, + flag: 1, + }, + { + key: "media_google_books_key", + value: "", + type: "string", + help: "Google Books API key (optional)", + group: 12, + flag: 1, + }, + { + key: "media_store_thumbnail", + value: "true", + type: "bool", + help: "Store thumbnails while scraping", + group: 12, + flag: 1, + }, + { + key: "media_thumbnail_mode", + value: "runtime", + type: "select", + options: "base64,runtime,local", + help: "Thumbnail storage mode", + group: 12, + flag: 1, + }, + { + key: "media_thumbnail_path", + value: "imgs", + type: "string", + help: "Thumbnail storage path", + group: 12, + flag: 1, + }, + { + key: "media_scrape_concurrency", + value: "5", + type: "number", + help: "Scrape concurrency", + group: 12, + flag: 1, + }, + { + key: "media_scrape_language", + value: "zh-CN", + type: "select", + options: "zh-CN,zh-TW,en-US,ja-JP,ko-KR,fr-FR,de-DE,es-ES,ru-RU,pt-BR", + help: "Preferred metadata language", + group: 12, + flag: 1, + }, + { + key: "media_proxy_cover", + value: "false", + type: "bool", + help: "Proxy external cover images through the server", + group: 12, + flag: 1, + }, + { + key: "media_subsonic_proxy_stream", + value: "false", + type: "bool", + help: "Proxy Subsonic streams instead of redirecting", + group: 12, + flag: 1, + }, + { + key: "media_subsonic_allow_password_auth", + value: "false", + type: "bool", + help: "Allow Subsonic u+p authentication with the OpenList password", + group: 12, + flag: 1, + }, + { + key: "media_cover_gateway", + value: "false", + type: "bool", + help: "Fallback to a third-party image gateway when cover fetch fails", + group: 12, + flag: 1, + }, + { + key: "media_download_cover", + value: "false", + type: "bool", + help: "Download covers into thumbnail storage", + group: 12, + flag: 1, + }, + { + key: "media_strm_on_scan", + value: "false", + type: "bool", + help: "Write .strm files automatically after scanning", + group: 12, + flag: 1, + }, + { + key: "media_strm_overwrite", + value: "false", + type: "bool", + help: "Overwrite existing .strm files", + group: 12, + flag: 1, + }, + { + key: "media_strm_same_dir", + value: "true", + type: "bool", + help: "Write .strm next to media files", + group: 12, + flag: 1, + }, + { + key: "media_strm_path", + value: "", + type: "string", + help: "STRM output directory", + group: 12, + flag: 1, + }, + { + key: "media_strm_template", + value: "{site_url}/d{path}?sign={sign}", + type: "text", + help: "STRM content template", + group: 12, + flag: 1, + }, ], storages: [], users: [ @@ -2018,7 +2215,11 @@ async function unsealDb( const sealedValue = target.otp_secret const identity = `users:${u.id ?? i}:otp_secret` tasks.push(async () => { - target.otp_secret = await unsealValue(sealedValue, fieldCipher, identity) + target.otp_secret = await unsealValue( + sealedValue, + fieldCipher, + identity, + ) }) } // 密码解密 @@ -2033,7 +2234,9 @@ async function unsealDb( } for (let i = 0; i < tasks.length; i += UNSEAL_CONCURRENCY) { - await Promise.all(tasks.slice(i, i + UNSEAL_CONCURRENCY).map((run) => run())) + await Promise.all( + tasks.slice(i, i + UNSEAL_CONCURRENCY).map((run) => run()), + ) } } @@ -2117,7 +2320,8 @@ export const saveDb = async ( console.log( `[DB] saveDb: cipher=${cipher}, persisting to ${backend.name}, storages=${data.storages?.length || 0}`, ) - const fieldCipher = cipher === "none" ? null : await resolveFieldCipher(activeEnv) + const fieldCipher = + cipher === "none" ? null : await resolveFieldCipher(activeEnv) if (cipher !== "none" && !fieldCipher && !sealKeyMissingWarned) { // 只在首次告警:密钥缺失会持续到密钥可用为止,逐次写入都打印只会刷屏。 sealKeyMissingWarned = true @@ -2363,7 +2567,9 @@ export async function getSettings() { settingsObj[s.key] = s.value }) } - return settingsObj + // Worker 无 ffmpeg:即使有人直接改库,也不允许转码开关生效 + const { disableTranscodeSettings } = await import("./transcode-guard") + return disableTranscodeSettings(settingsObj) } export async function getUsers() { diff --git a/src/backend/internal/model/store/json.ts b/src/backend/internal/model/store/json.ts index 38551f9a..89331009 100644 --- a/src/backend/internal/model/store/json.ts +++ b/src/backend/internal/model/store/json.ts @@ -567,20 +567,56 @@ export async function readPersistedSecret( key: string, ): Promise<string | null> { try { - const driver = await resolveSecretDriver(env) - const val = await driver.get(key, env) - if (val === null || val === undefined) return null - // 部分绑定的 get() 可能返回 Response 形态,兼容 text() 取值。 - const resolved: any = - typeof (val as any)?.text === "function" ? await (val as any).text() : val - const str = String(resolved).trim() - return str || null + return await readPersistedSecretStrict(env, key) } catch (e) { console.warn(`[Secret] read "${key}" failed:`, e) return null } } +/** + * 同 readPersistedSecret,但**不吞异常**:读取失败会抛出。 + * + * 需要区分「键不存在」与「后端读取失败」的场景必须用它——例如媒体库分片: + * 把一次 KV/D1 超时当成空分片,会让随后的普通保存用空数据覆盖真实配置。 + */ +export async function readPersistedSecretStrict( + env: any, + key: string, +): Promise<string | null> { + const driver = await resolveSecretDriver(env) + const val = await driver.get(key, env) + if (val === null || val === undefined) return null + // 部分绑定的 get() 可能返回 Response 形态,兼容 text() 取值。 + const resolved: any = + typeof (val as any)?.text === "function" ? await (val as any).text() : val + const str = String(resolved).trim() + return str || null +} + +/** + * 按前缀列出已持久化的键。 + * + * 用于「索引只是缓存、键本身才是权威」的场景(如媒体库分片):全局索引文档 + * 会被多实例互相覆盖,按前缀枚举才能保证冷启动后一个分片都不漏。 + * 驱动不支持 list 或调用失败时返回空数组,由调用方退回到索引。 + */ +export async function listPersistedSecrets( + env: any, + prefix: string, +): Promise<string[]> { + try { + const driver = await resolveSecretDriver(env) + const keys = await driver.list(prefix, env) + return Array.isArray(keys) + ? keys.map((k) => String(k)).filter((k) => k.startsWith(prefix)) + : [] + } catch (e) { + console.warn(`[Secret] list "${prefix}" failed:`, e) + return [] + } +} + /** * 写入密钥。 * @@ -594,7 +630,13 @@ export async function writePersistedSecret( ): Promise<boolean> { try { const driver = await resolveSecretDriver(env) - await driver.put(key, secret, env) + // 驱动契约是「失败即抛错」,但第三方/历史驱动可能以 false 表示失败; + // 这里把 false 也当作失败,避免调用方把未落盘的写入当成成功。 + const result: any = await driver.put(key, secret, env) + if (result === false) { + console.warn(`[Secret] cannot persist "${key}": driver returned false`) + return false + } return true } catch (e) { console.warn( diff --git a/src/backend/internal/model/transcode-guard.ts b/src/backend/internal/model/transcode-guard.ts new file mode 100644 index 00000000..1085fb4b --- /dev/null +++ b/src/backend/internal/model/transcode-guard.ts @@ -0,0 +1,81 @@ +/** + * Worker 侧转码能力的强制禁用。 + * + * Worker 运行时没有 ffmpeg / ffprobe,也没有可写本地磁盘(切片缓存), + * 因此「转码」相关设置不仅无效,还会让前端展示一个永远失败的开关。 + * + * 策略(三处兜底,缺一不可): + * 1. 设置列表接口过滤掉 transcode_* 键 —— 前端不渲染; + * 2. 设置保存接口拒绝写入 —— 即使手工构造请求也存不进去; + * 3. 读取设置时强制 transcode_enabled=false —— 即使直接改了数据库也不生效。 + * + * 保留 `/fs/transcode/play` 路由但返回 501,明确告知能力不存在, + * 避免客户端拿到「成功但 no-op」的响应后一直重试。 + */ + +/** Go 版 internal/conf/const.go 中全部 transcode 设置键(含前缀兜底) */ +export const TRANSCODE_SETTING_KEYS = new Set([ + "transcode_enabled", + "transcode_min_size_gb", + "transcode_min_bitrate_mbps", + "transcode_source_codecs", + "transcode_source_extensions", + "transcode_output_format", + "transcode_output_codec", + "transcode_output_bitrate", + "transcode_output_audio_codec", + "transcode_output_audio_bitrate", + "transcode_output_resolution", + "transcode_segment_duration", + "transcode_hwaccel", + "transcode_ffmpeg_path", + "transcode_ffprobe_path", + "transcode_run_mode", + "transcode_worker_secret", + "transcode_cache_path", + "transcode_cache_max_gb", + "transcode_job_timeout_min", + "transcode_local_concurrency", + "transcode_idle_timeout_sec", + "transcode_chunk_duration_sec", + "transcode_max_chunk_concurrency", + "transcode_chunk_idle_sec", + "transcode_chunk_prefetch", +]) + +export const TRANSCODE_DISABLED_MESSAGE = + "Transcoding is not available in the Worker runtime (no ffmpeg). Playback falls back to direct streaming." + +export function isTranscodeSettingKey(key: unknown): boolean { + const k = String(key || "").trim().toLowerCase() + if (!k) return false + return k.startsWith("transcode_") || TRANSCODE_SETTING_KEYS.has(k) +} + +/** 从设置数组中剔除全部转码键(用于 /admin/setting/list) */ +export function stripTranscodeSettings<T extends { key?: string }>( + settings: T[], +): T[] { + return settings.filter((s) => !isTranscodeSettingKey(s?.key)) +} + +/** + * 把设置对象里的转码开关强制关掉(用于 getSettings / 公共设置)。 + * 其他 transcode_* 字段也清空,避免前端读到非默认值。 + */ +export function disableTranscodeSettings( + settings: Record<string, any>, +): Record<string, any> { + for (const key of Object.keys(settings)) { + if (isTranscodeSettingKey(key)) { + settings[key] = key === "transcode_enabled" ? "false" : "" + } + } + return settings +} + +/** 保存设置时的兜底:转码键一律落成禁用值 */ +export function normalizeTranscodeSettingValue(key: string, value: any): any { + if (!isTranscodeSettingKey(key)) return value + return key === "transcode_enabled" ? "false" : "" +} diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index b4c7b4fd..ef4f143b 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1,4 +1,5 @@ import { resolvePath, getDb, getSettings, saveDb } from "../model/db" +import { isSafeUrl } from "../../pkg/http" import { encodeDownloadPath } from "../../pkg/path" import { singleflight } from "../../pkg/singleflight" import { canUseProxyEndpoint, normalizeExtList } from "../driver/proxy" @@ -1845,3 +1846,187 @@ export async function putItem( // 上传/覆盖:该文件的旧直链与父目录列表缓存失效 await invalidatePaths([virtualPath], requestContext?.env) } + +/** + * 媒体刮削读取文件内容的上限:任何一次读取都不得超过 32MB。 + * + * 刮削只需要文件头(ID3/FLAC/图片头)或尾部(ID3v1 / ZIP 中央目录), + * 但少数存储不支持 Range,会退化成整文件下载,因此必须有硬上限。 + */ +export const MAX_MEDIA_READ_BYTES = 32 * 1024 * 1024 + +/** 本地存储的区间读取(Node 运行时;Worker 下 import 会失败并返回 null) */ +async function readLocalRange( + physicalPath: string, + start: number, + want: number, +): Promise<Uint8Array | null> { + try { + const fs = await import("node:fs/promises") + const fh = await fs.open(physicalPath, "r") + try { + const size = (await fh.stat()).size + if (start >= size) return new Uint8Array(0) + const len = Math.min(want, size - start) + const buf = new Uint8Array(len) + const { bytesRead } = await fh.read(buf, 0, len, start) + return buf.subarray(0, bytesRead) + } finally { + await fh.close() + } + } catch { + return null + } +} + +/** + * 读取 VFS 文件的指定区间(用于 ID3 标签、EPUB 封面、图片缩略图等)。 + * + * 行为: + * 1. 通过驱动换链拿到 raw_url,再带 `Range` 请求; + * 2. 上游返回 206 时直接切片; + * 3. 上游忽略 Range 返回 200 时,流式丢弃 `offset` 字节后最多读 `length` 字节, + * 避免把整个大文件读进内存; + * 4. 任何失败返回 null(刮削属于尽力而为,不应中断主流程)。 + */ +export async function readFileRange( + virtualPath: string, + offset: number, + length: number, + requestContext?: StorageRequestContext, +): Promise<Uint8Array | null> { + const start = Math.max(0, Math.floor(offset) || 0) + const want = Math.min( + MAX_MEDIA_READ_BYTES, + Math.max(1, Math.floor(length) || 1), + ) + let resolved: Awaited<ReturnType<typeof resolvePath>> + try { + resolved = await resolvePath(virtualPath, requestContext?.env) + } catch { + return null + } + if (resolved.isVirtual || !resolved.storage) return null + + let item: FileItem + const driver = await getDriver(resolved.storage.driver, resolved.storage) + try { + item = await driver.get(virtualPath, resolved.physical!) + } catch (e: any) { + console.warn(`[op] read ${virtualPath} link failed:`, e?.message || e) + return null + } finally { + await flushPendingDriverState( + resolved.storage.driver, + resolved.storage, + driver, + requestContext, + ) + } + if (!item?.raw_url) { + // LocalDriver 没有 raw_url(本地文件不走 HTTP)。Node 运行时直接读区间, + // 否则本地存储的音频/图片永远读不到标签与封面(PR #148 评审 #25)。 + if (resolved.storage.driver === "local") { + return readLocalRange(resolved.physical!, start, want) + } + return null + } + + const headers: Record<string, string> = { + ...(item.raw_url_headers || {}), + Range: `bytes=${start}-${start + want - 1}`, + } + try { + // 手动跟随重定向:默认 fetch 会自动跳转,既绕过 SSRF 校验,也会把 + // raw_url_headers(Cookie/Authorization)原样带给第三方。每一跳都重新 + // 校验目标地址,跨域时丢弃凭据头(对齐 server/raw.ts 的 safeProxyFetch)。 + let currentUrl = item.raw_url + let currentHeaders: Record<string, string> = { ...headers } + let res: Response | null = null + for (let hop = 0; hop < 5; hop++) { + // 首跳地址来自管理员配置的驱动(可能是有意为之的内网存储),不做 + // SSRF 拦截;后续每一跳都必须重新校验,防止公网地址 302 到内网。 + if (hop > 0) { + let safe = false + try { + safe = isSafeUrl(currentUrl) + } catch { + safe = false + } + if (!safe) return null + } + res = await fetch(currentUrl, { + headers: currentHeaders, + redirect: "manual", + }) + if (res.status < 300 || res.status >= 400) break + const loc = res.headers.get("location") + if (!loc) return null + let next: URL + try { + next = new URL(loc, currentUrl) + } catch { + return null + } + if (next.origin !== new URL(currentUrl).origin) { + currentHeaders = { Range: currentHeaders.Range } + } + try { + await res.body?.cancel() + } catch { + /* ignore */ + } + currentUrl = next.toString() + } + if (!res) return null + if (!res.ok && res.status !== 206) return null + + if (res.status === 206) { + const buf = new Uint8Array(await res.arrayBuffer()) + return buf.subarray(0, Math.min(buf.length, want)) + } + + // 200:上游忽略了 Range(或本地文件驱动)。流式跳过 offset。 + if (!res.body) { + const buf = new Uint8Array(await res.arrayBuffer()) + return buf.subarray(start, start + want) + } + const reader = res.body.getReader() + let skipped = 0 + const chunks: Uint8Array[] = [] + let received = 0 + try { + while (received < want) { + const { done, value } = await reader.read() + if (done || !value) break + let chunk = value + if (skipped < start) { + const drop = Math.min(chunk.length, start - skipped) + skipped += drop + chunk = chunk.subarray(drop) + if (chunk.length === 0) continue + } + const take = Math.min(chunk.length, want - received) + chunks.push(chunk.subarray(0, take)) + received += take + } + } finally { + try { + await reader.cancel() + } catch { + /* ignore */ + } + } + if (chunks.length === 0) return null + const out = new Uint8Array(received) + let pos = 0 + for (const c of chunks) { + out.set(c, pos) + pos += c.length + } + return out + } catch (e: any) { + console.warn(`[op] read ${virtualPath} range failed:`, e?.message || e) + return null + } +} diff --git a/src/backend/pkg/sign.ts b/src/backend/pkg/sign.ts index 581c3210..8ac7c354 100644 --- a/src/backend/pkg/sign.ts +++ b/src/backend/pkg/sign.ts @@ -197,11 +197,21 @@ export async function signDownloadPath( c: any, virtualPath: string, expiresIn: number, + /** + * 授权范围。默认空串保持原签名格式(向后兼容)。 + * "media" 由媒体库签发,表示「这条 /p 链接是媒体播放/阅读用的」, + * raw 路由据此跳过存储的代理策略限制(web_proxy=false 时 /p 会 403)。 + * scope 参与 HMAC,无法由客户端追加伪造。 + */ + scope = "", ): Promise<string> { const secret = await getJwtSecret(c) const expires = expiresIn === 0 ? 0 : Math.floor(Date.now() / 1000) + expiresIn - const hmac = await hmacSha256(`${virtualPath}:${expires}`, secret) + const payload = scope + ? `${scope}:${virtualPath}:${expires}` + : `${virtualPath}:${expires}` + const hmac = await hmacSha256(payload, secret) return `${expires}.${hmac}` } @@ -209,6 +219,7 @@ export async function verifyDownloadSign( c: any, virtualPath: string, sign: string, + scope = "", ): Promise<boolean> { const dot = sign.lastIndexOf(".") if (dot <= 0) return false @@ -220,6 +231,9 @@ export async function verifyDownloadSign( return false } const secret = await getJwtSecret(c) - const expect = await hmacSha256(`${virtualPath}:${expires}`, secret) + const payload = scope + ? `${scope}:${virtualPath}:${expires}` + : `${virtualPath}:${expires}` + const expect = await hmacSha256(payload, secret) return constantTimeEqualHex(expect, hmac) } diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 5a4b236e..fe5e8d96 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -11,6 +11,10 @@ import { search } from "../internal/op/search" import { checkAdminAuth } from "../pkg/utils" import { safeErrorMessage } from "../pkg/errs" import { validateHide } from "../pkg/meta" +import { + isTranscodeSettingKey, + stripTranscodeSettings, +} from "../internal/model/transcode-guard" import { PROXY_RANGE_DRIVERS, driverMustProxy, @@ -4724,6 +4728,9 @@ adminRouter.get("/setting/list", async (c) => { let settings = db.settings || [] + // Worker 没有 ffmpeg:转码设置一律不下发,避免前端展示永远失败的开关 + settings = stripTranscodeSettings(settings) + if (groupQuery !== undefined) { const groupNum = parseInt(groupQuery, 10) settings = settings.filter((s: any) => s.group === groupNum) @@ -4751,6 +4758,16 @@ adminRouter.post("/setting/save", async (c) => { if (typeof key !== "string" || key.length === 0 || key.length > 128) continue + // Worker 不支持转码:拒绝写入,并把历史遗留的开关强制落成 false。 + // 只跳过该字段而不是整批返回 400,避免一次保存中其它合法设置被连带拒绝。 + if (isTranscodeSettingKey(key)) { + const existing = db.settings.find((s: any) => s.key === key) + if (existing) { + existing.value = key === "transcode_enabled" ? "false" : "" + } + continue + } + let value = item.value if (value !== null && typeof value === "object") { value = JSON.stringify(value) diff --git a/src/backend/server/emby.ts b/src/backend/server/emby.ts new file mode 100644 index 00000000..64bb94b7 --- /dev/null +++ b/src/backend/server/emby.ts @@ -0,0 +1,3162 @@ +/** + * Emby / OpenSubsonic 兼容层(对齐 Go 版 server/handles/emby*.go、subsonic*.go)。 + * + * 已覆盖 Go 侧最新的协议修复: + * - PlaybackInfo 的 Path / DirectStreamUrl 必须是可播放的 HTTP 流地址, + * 且带 api_key(否则客户端报 "Playback of this content is currently restricted"); + * - 封面/图片端点允许匿名访问(Emby 客户端的图片 URL 不带 token); + * - Sessions/Playing* 真正落播放进度、已看与收藏; + * - 剧集按 Series → Season → Episode 三层组织; + * - Subsonic 默认输出 XML,支持 f=json/jsonp 与 POST 表单; + * - 认证失败按 IP + 用户名限流。 + */ + +import { Hono } from "hono" + +import { getDb } from "../internal/model/db" +import { twoStepHash } from "../pkg/password" +import { md5 as md5Hex } from "../pkg/crypto" +import { encodeDownloadPath } from "../pkg/path" +import { getSignPolicy, signDownloadPath } from "../pkg/sign" +import { isAdmin } from "../pkg/permission" +import { + EMBY_VERSION, + SUBSONIC_VERSION, + extOf, + mediaProtocolEnabled, + settingBool, + type EmbyDevice, + type EpisodeInfo, + type MediaDb, + type MediaItem, +} from "../internal/media/model" +import { isWithinUserRoot } from "../pkg/permission" +import { + createPlaylist, + deletePlaylist, + ensureUserData, + findItem, + getMediaDb, + getPlayQueue, + getPlaylist, + getSettingsMap, + isTypeEnabled, + listPlaylists, + playlistItems, + replacePlaylistItems, + saveMediaDb, + savePlayQueue, + updatePlaylist, +} from "../internal/media/store" +import { mediaHelpers } from "./media" + +const originOf = (c: any) => new URL(c.req.url).origin +const b64url = (value: string) => { + const bytes = new TextEncoder().encode(value) + let bin = "" + for (const b of bytes) bin += String.fromCharCode(b) + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") +} +const unb64url = (value: string) => { + try { + const raw = value.replace(/-/g, "+").replace(/_/g, "/") + const bin = atob(raw + "=".repeat((4 - (raw.length % 4)) % 4)) + return new TextDecoder().decode( + Uint8Array.from(bin, (ch) => ch.charCodeAt(0)), + ) + } catch { + return "" + } +} + +/** 32 位 FNV-1a:用于把字符串 key 变成 Emby 客户端友好的数字 ID */ +function fnv1a(value: string): number { + let h = 0x811c9dc5 + for (let i = 0; i < value.length; i++) { + h ^= value.charCodeAt(i) + h = Math.imul(h, 0x01000193) >>> 0 + } + return h >>> 0 +} + +const hashKey = (value: string) => fnv1a(value).toString(36) +const numericId = (orig: string) => { + const m = orig.match(/^(?:mv|so|ep)-(\d+)(?:-(\d+))?$/) + if (m) return m[2] ? `${m[1]}${String(m[2]).padStart(3, "0")}` : m[1] + return String(fnv1a(orig)) +} +const guidLike = (orig: string) => + `${String(fnv1a(orig)).padStart(8, "0")}-0000-0000-0000-000000000000` +const imageTag = (item: MediaItem) => + item.cover ? hashKey(`${item.cover}|${item.updated_at}`).slice(0, 12) : "" + +// ==================== 库 ==================== + +interface EmbySeries { + key: string + name: string + items: MediaItem[] + year: number + overview: string + cover: string +} + +interface EmbyAlbum { + key: string + name: string + artist: string + items: MediaItem[] + year: number + cover: string +} + +interface EmbyLibrary { + user: any + db: MediaDb + videos: MediaItem[] + music: MediaItem[] + images: MediaItem[] + books: MediaItem[] + series: EmbySeries[] + albums: EmbyAlbum[] + ids: Map<string, string> +} + +const isSeriesItem = (item: MediaItem) => + !!item.is_folder || item.video_type === "tv" + +// Series 身份按**目录**而不是 scraped_name: +// 单集文件刮削后 scraped_name 是剧名、未刮削时是集名,拿它做 key 会让 +// 同一部剧随刮削进度/集数裂成多个 Series(PR #148 评审 #1)。 +const seriesKeyOf = (item: MediaItem) => + hashKey( + `series:${(item.is_folder + ? `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + : item.folder_path + ).toLowerCase()}`, + ) + +const seriesNameOf = (item: MediaItem) => { + if (item.scraped_name) return item.scraped_name + if (item.is_folder) return item.file_name.replace(/\.[^.]+$/, "") + const dir = item.folder_path.split("/").filter(Boolean).pop() + return dir || item.file_name.replace(/\.[^.]+$/, "") +} + +const albumKeyOf = (item: MediaItem) => + hashKey( + `${(item.album_artist || "未知艺术家").toLowerCase()}\u0000${(item.album_name || "未知专辑").toLowerCase()}`, + ) +const artistKeyOf = (item: MediaItem) => + hashKey((item.album_artist || "未知艺术家").toLowerCase()) + +const artistOf = (item: MediaItem) => item.album_artist || "未知艺术家" +const albumOf = (item: MediaItem) => item.album_name || "未知专辑" + +const parseEpisodesOf = (item: MediaItem): EpisodeInfo[] => { + if (!item.episodes) return [] + try { + const list = JSON.parse(item.episodes) + return Array.isArray(list) ? list : [] + } catch { + return [] + } +} + +/** 基础可见性(不含 meta):类型开关 + hidden + base_path */ +function baseVisible(db: MediaDb, user: any, item: MediaItem) { + if (item.hidden) return false + if (!isTypeEnabled(db, item.media_type)) return false + try { + return isWithinUserRoot(user, item.folder_path) + } catch { + return true + } +} + +/** 取 guest 用户(匿名入口用);不存在或被禁用返回 null */ +async function guestUserOf(env: any) { + const db = await getDb(env) + const guest = (db.users || []).find((u: any) => u.username === "guest") + return guest && !guest.disabled ? guest : null +} + +/** meta 级校验(成本较高,只对要返回的条目调用) */ +async function metaFilter(env: any, user: any, items: MediaItem[]) { + const out: MediaItem[] = [] + for (const item of items) { + if (await mediaHelpers.canAccessItem(env, user, item)) out.push(item) + } + return out +} + +/** + * 加载协议层媒体库。 + * + * 只保留「按 user 过滤」这一条路径:之前的 `all: true` 分支只过滤 hidden, + * 会绕过 base_path / 类型开关,被匿名封面接口用来读全库。 + */ +async function loadLibrary(env: any, user: any): Promise<EmbyLibrary> { + const db = await getMediaDb(env) + const visible = (item: MediaItem) => baseVisible(db, user, item) + const videos = db.items.filter((i) => i.media_type === "video" && visible(i)) + const music = db.items.filter((i) => i.media_type === "music" && visible(i)) + const images = db.items.filter((i) => i.media_type === "image" && visible(i)) + const books = db.items.filter((i) => i.media_type === "book" && visible(i)) + + const seriesMap = new Map<string, EmbySeries>() + for (const item of videos) { + if (!isSeriesItem(item)) continue + const key = seriesKeyOf(item) + let s = seriesMap.get(key) + if (!s) { + s = { + key, + name: seriesNameOf(item), + items: [], + year: Number((item.release_date || "").slice(0, 4)) || 0, + overview: item.plot || "", + cover: item.cover || "", + } + seriesMap.set(key, s) + } + if (!s.cover && item.cover) s.cover = item.cover + if (!s.overview && item.plot) s.overview = item.plot + s.items.push(item) + } + + const albumMap = new Map<string, EmbyAlbum>() + for (const item of music) { + const key = albumKeyOf(item) + let a = albumMap.get(key) + if (!a) { + a = { + key, + name: albumOf(item), + artist: artistOf(item), + items: [], + year: Number((item.release_date || "").slice(0, 4)) || 0, + cover: item.cover || "", + } + albumMap.set(key, a) + } + if (!a.cover && item.cover) a.cover = item.cover + a.items.push(item) + } + + const ids = new Map<string, string>() + const register = (orig: string) => { + ids.set(numericId(orig), orig) + ids.set(guidLike(orig), orig) + } + register("root") + register("lib-movie") + register("lib-tv") + register("lib-music") + register("lib-image") + register("lib-book") + for (const item of [...videos, ...music, ...images, ...books]) { + register( + item.media_type === "music" ? `so-${item.id}` : `mv-${item.id}`, + ) + if (item.media_type === "music") { + register(`al-${albumKeyOf(item)}`) + register(`ar-${artistKeyOf(item)}`) + } + } + for (const s of seriesMap.values()) { + register(`sr-${s.key}`) + for (const season of seasonListOf(s)) register(`sn-${s.key}-${season}`) + for (const ep of episodesOf(s, 0)) { + register(`ep-${ep.item.id}-${ep.index}`) + } + } + + return { + user, + db, + videos, + music, + images, + books, + series: Array.from(seriesMap.values()), + albums: Array.from(albumMap.values()), + ids, + } +} + +const origId = (lib: EmbyLibrary, id: string) => + lib.ids.get(String(id || "").trim()) || String(id || "").trim() + +// ==================== DTO ==================== + +const embyServerId = (c: any) => + String(fnv1a(`server:${new URL(c.req.url).host}`)) + +const ticks = (seconds: number) => Math.round((Number(seconds) || 0) * 10_000_000) +const yearOf = (date: string) => Number(String(date || "").slice(0, 4)) || 0 + +const userDataOf = ( + lib: EmbyLibrary, + itemId: number, + episode = 0, + season = 0, +) => { + const wantSeason = Number(season) || 1 + const row = lib.user + ? lib.db.user_data.find( + (d) => + d.user_id === lib.user.id && + d.item_id === itemId && + d.episode_index === episode && + (Number(d.season) || 1) === wantSeason, + ) + : undefined + return { + PlaybackPositionTicks: row?.position_ticks || 0, + PlayCount: row?.play_count || 0, + IsFavorite: !!row?.is_favorite, + Played: !!row?.played, + LastPlayedDate: row?.last_played_at || null, + Rating: row?.rating || 0, + } +} + +interface DtoOptions { + type?: string + extra?: Record<string, any> + parentId?: string +} + +function makeDto( + c: any, + lib: EmbyLibrary, + item: MediaItem, + options: DtoOptions = {}, +) { + const isMusic = item.media_type === "music" + const id = numericId(isMusic ? `so-${item.id}` : `mv-${item.id}`) + const name = item.scraped_name || item.file_name.replace(/\.[^.]+$/, "") + const art = imageTag(item) + return { + Name: name, + SortName: name, + ServerId: embyServerId(c), + Id: id, + ParentId: options.parentId, + Type: + options.type || + (isMusic ? "Audio" : item.media_type === "book" ? "Book" : "Movie"), + MediaType: isMusic ? "Audio" : item.media_type === "image" ? "Photo" : "Video", + IsFolder: !!item.is_folder, + Overview: item.plot || item.description || "", + ProductionYear: yearOf(item.release_date), + PremiereDate: item.release_date || undefined, + RunTimeTicks: ticks(item.duration), + CommunityRating: item.rating || undefined, + Genres: item.genre ? item.genre.split(",").filter(Boolean) : [], + People: item.authors + ? String(item.authors) + .replace(/^\[|\]$/g, "") + .split(",") + .map((s) => s.replace(/^"|"$/g, "").trim()) + .filter(Boolean) + .map((n) => ({ Name: n, Type: "Actor" })) + : [], + Path: `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}`, + DateCreated: item.created_at, + ImageTags: art ? { Primary: art } : {}, + BackdropImageTags: [], + ProviderIds: item.external_id ? { Tmdb: String(item.external_id) } : {}, + LocationType: "FileSystem", + UserData: userDataOf(lib, item.id, 0), + Album: isMusic ? albumOf(item) : undefined, + AlbumId: isMusic ? numericId(`al-${albumKeyOf(item)}`) : undefined, + AlbumArtist: isMusic ? artistOf(item) : undefined, + AlbumArtists: isMusic + ? [{ Name: artistOf(item), Id: numericId(`ar-${artistKeyOf(item)}`) }] + : undefined, + Artists: isMusic ? [artistOf(item)] : undefined, + ArtistItems: isMusic + ? [{ Name: artistOf(item), Id: numericId(`ar-${artistKeyOf(item)}`) }] + : undefined, + IndexNumber: isMusic ? item.track_number || undefined : undefined, + Container: item.file_name.split(".").pop() || undefined, + ...(options.extra || {}), + } +} + +function movieDto(c: any, lib: EmbyLibrary, item: MediaItem) { + return makeDto(c, lib, item, { type: "Movie", parentId: numericId("lib-movie") }) +} + +function seriesDto(c: any, lib: EmbyLibrary, s: EmbySeries) { + const first = s.items[0] + const art = imageTag(first) + return { + Name: s.name, + SortName: s.name, + ServerId: embyServerId(c), + Id: numericId(`sr-${s.key}`), + ParentId: numericId("lib-tv"), + Type: "Series", + IsFolder: true, + Overview: s.overview, + ProductionYear: s.year, + ChildCount: seasonListOf(s).length, + RecursiveItemCount: episodesOf(s, 0).length, + ImageTags: art ? { Primary: art } : {}, + BackdropImageTags: [], + LocationType: "FileSystem", + UserData: userDataOf(lib, first.id, 0), + } +} + +function seasonListOf(s: EmbySeries): number[] { + const set = new Set<number>() + for (const item of s.items) { + const eps = parseEpisodesOf(item) + if (eps.length > 0) { + for (const ep of eps) set.add(Number(ep.season) || item.season || 1) + } else { + set.add(Number(item.season) || 1) + } + } + if (set.size === 0) set.add(1) + return Array.from(set).sort((a, b) => a - b) +} + +interface EmbyEpisode { + item: MediaItem + index: number + season: number + title: string + fileName: string +} + +function episodesOf(s: EmbySeries, season = 0): EmbyEpisode[] { + const out: EmbyEpisode[] = [] + for (const item of s.items) { + const eps = parseEpisodesOf(item) + if (eps.length > 0) { + eps.forEach((ep, idx) => { + const sn = Number(ep.season) || item.season || 1 + if (season && sn !== season) return + out.push({ + item, + index: Number(ep.index) || idx + 1, + season: sn, + title: ep.title || ep.file_name.replace(/\.[^.]+$/, ""), + fileName: ep.file_name, + }) + }) + } else { + const sn = Number(item.season) || 1 + if (season && sn !== season) continue + out.push({ + item, + index: Number(item.episode) || 1, + season: sn, + // 单集标题优先用刮削到的集名;scraped_name 是剧名,不能拿来当集名 + title: + item.episode_title || + item.file_name.replace(/\.[^.]+$/, ""), + fileName: item.file_name, + }) + } + } + return out.sort( + (a, b) => a.season - b.season || a.index - b.index, + ) +} + +function seasonDto(c: any, s: EmbySeries, season: number) { + const eps = episodesOf(s, season) + const first = eps[0]?.item || s.items[0] + const art = imageTag(first) + return { + Name: season === 0 ? "Specials" : `Season ${season}`, + SortName: String(season).padStart(4, "0"), + ServerId: embyServerId(c), + Id: numericId(`sn-${s.key}-${season}`), + ParentId: numericId(`sr-${s.key}`), + SeriesId: numericId(`sr-${s.key}`), + SeriesName: s.name, + Type: "Season", + IsFolder: true, + IndexNumber: season, + ChildCount: eps.length, + RecursiveItemCount: eps.length, + ImageTags: art ? { Primary: art } : {}, + BackdropImageTags: [], + LocationType: "FileSystem", + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + } +} + +function episodeDto(c: any, lib: EmbyLibrary, s: EmbySeries, ep: EmbyEpisode) { + const art = imageTag(ep.item) + const path = `${ep.item.folder_path.replace(/\/+$/, "")}/${ep.item.file_name}${ep.item.is_folder ? `/${ep.fileName}` : ""}` + return { + Name: ep.title, + SortName: ep.title, + ServerId: embyServerId(c), + // 季号参与 id:同一条目下 S01E01 与 S02E01 的 index 相同,必须靠 season 区分 + Id: numericId(`ep-${ep.item.id}-${ep.season}-${ep.index}`), + ParentId: numericId(`sn-${s.key}-${ep.season}`), + SeriesId: numericId(`sr-${s.key}`), + SeriesName: s.name, + SeasonId: numericId(`sn-${s.key}-${ep.season}`), + SeasonName: `Season ${ep.season}`, + Type: "Episode", + MediaType: "Video", + IsFolder: false, + IndexNumber: ep.index, + ParentIndexNumber: ep.season, + Overview: ep.item.plot || "", + RunTimeTicks: ticks(ep.item.duration), + ImageTags: art ? { Primary: art } : {}, + BackdropImageTags: [], + Path: path, + Container: ep.fileName.split(".").pop() || "mp4", + LocationType: "FileSystem", + UserData: userDataOf(lib, ep.item.id, ep.index, ep.season), + } +} + +function albumDto(c: any, al: EmbyAlbum) { + const first = al.items[0] + const art = imageTag(first) + return { + Name: al.name, + SortName: al.name, + ServerId: embyServerId(c), + Id: numericId(`al-${al.key}`), + ParentId: numericId("lib-music"), + Type: "MusicAlbum", + IsFolder: true, + AlbumArtist: al.artist, + AlbumArtists: [{ Name: al.artist, Id: numericId(`ar-${artistKeyOf(first)}`) }], + ProductionYear: al.year, + ChildCount: al.items.length, + RecursiveItemCount: al.items.length, + ImageTags: art ? { Primary: art } : {}, + BackdropImageTags: [], + LocationType: "FileSystem", + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + } +} + +function songDto(c: any, lib: EmbyLibrary, item: MediaItem) { + return makeDto(c, lib, item, { + type: "Audio", + parentId: numericId(`al-${albumKeyOf(item)}`), + extra: { + IndexNumber: item.track_number || undefined, + RunTimeTicks: ticks(item.duration), + }, + }) +} + +/** 视图(Movies / TV Shows / Music / Photos / Books) */ +function viewDtos(c: any, lib: EmbyLibrary) { + const movieCount = lib.videos.filter((v) => !isSeriesItem(v)).length + const out: any[] = [] + if (isTypeEnabled(lib.db, "video")) { + out.push({ + Name: "Movies", + SortName: "Movies", + ServerId: embyServerId(c), + Id: numericId("lib-movie"), + ParentId: numericId("root"), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "movies", + ViewType: "movies", + LocationType: "FileSystem", + ChildCount: movieCount, + RecursiveItemCount: movieCount, + Subviews: ["movies"], + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) + out.push({ + Name: "TV Shows", + SortName: "TV Shows", + ServerId: embyServerId(c), + Id: numericId("lib-tv"), + ParentId: numericId("root"), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "tvshows", + ViewType: "tvshows", + LocationType: "FileSystem", + ChildCount: lib.series.length, + RecursiveItemCount: lib.series.length, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) + } + if (isTypeEnabled(lib.db, "music")) { + out.push({ + Name: "Music", + SortName: "Music", + ServerId: embyServerId(c), + Id: numericId("lib-music"), + ParentId: numericId("root"), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "music", + ViewType: "music", + LocationType: "FileSystem", + ChildCount: lib.albums.length, + RecursiveItemCount: lib.albums.length, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) + } + if (isTypeEnabled(lib.db, "image")) { + out.push({ + Name: "Photos", + SortName: "Photos", + ServerId: embyServerId(c), + Id: numericId("lib-image"), + ParentId: numericId("root"), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "photos", + ViewType: "photos", + LocationType: "FileSystem", + ChildCount: lib.images.length, + RecursiveItemCount: lib.images.length, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) + } + if (isTypeEnabled(lib.db, "book")) { + out.push({ + Name: "Books", + SortName: "Books", + ServerId: embyServerId(c), + Id: numericId("lib-book"), + ParentId: numericId("root"), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "books", + ViewType: "books", + LocationType: "FileSystem", + ChildCount: lib.books.length, + RecursiveItemCount: lib.books.length, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) + } + return out +} + +/** 按 ParentId 取子项(对齐 Go childrenOf) */ +function childrenOf(c: any, lib: EmbyLibrary, parentRaw: string): any[] { + const parent = origId(lib, parentRaw) + if (!parent || parent === "root") { + // 对齐 Go 的 rootItem.childrenOf:根目录只暴露视图,不混入剧集/专辑 + return viewDtos(c, lib) + } + if (parent === "lib-movie") { + return lib.videos + .filter((v) => !isSeriesItem(v)) + .map((v) => movieDto(c, lib, v)) + } + if (parent === "lib-tv") { + return lib.series.map((s) => seriesDto(c, lib, s)) + } + if (parent === "lib-music") { + return lib.albums.map((a) => albumDto(c, a)) + } + if (parent === "lib-image") { + return lib.images.map((i) => + makeDto(c, lib, i, { + type: "Photo", + parentId: numericId("lib-image"), + }), + ) + } + if (parent === "lib-book") { + return lib.books.map((i) => + makeDto(c, lib, i, { type: "Book", parentId: numericId("lib-book") }), + ) + } + if (parent.startsWith("sr-")) { + const key = parent.slice(3) + const s = lib.series.find((x) => x.key === key) + if (!s) return [] + return seasonListOf(s).map((sn) => seasonDto(c, s, sn)) + } + if (parent.startsWith("sn-")) { + const rest = parent.slice(3) + const idx = rest.lastIndexOf("-") + if (idx < 0) return [] + const key = rest.slice(0, idx) + const season = Number(rest.slice(idx + 1)) || 1 + const s = lib.series.find((x) => x.key === key) + if (!s) return [] + return episodesOf(s, season).map((ep) => episodeDto(c, lib, s, ep)) + } + if (parent.startsWith("al-")) { + const key = parent.slice(3) + const al = lib.albums.find((x) => x.key === key) + if (!al) return [] + return al.items + .slice() + .sort((a, b) => a.track_number - b.track_number) + .map((i) => songDto(c, lib, i)) + } + if (parent.startsWith("ar-")) { + const key = parent.slice(3) + return lib.albums + .filter((al) => artistKeyOf(al.items[0]) === key) + .map((al) => albumDto(c, al)) + } + return [] +} + +/** 全部条目(用于搜索/递归查询) */ +function allDtos(c: any, lib: EmbyLibrary, include: string[]): any[] { + const want = (t: string) => + include.length === 0 || include.some((x) => x.toLowerCase() === t.toLowerCase()) + const out: any[] = [] + if (want("Movie")) { + for (const v of lib.videos) if (!isSeriesItem(v)) out.push(movieDto(c, lib, v)) + } + if (want("Series")) { + for (const s of lib.series) out.push(seriesDto(c, lib, s)) + } + if (want("Season")) { + for (const s of lib.series) { + for (const sn of seasonListOf(s)) out.push(seasonDto(c, s, sn)) + } + } + if (want("Episode")) { + for (const s of lib.series) { + for (const ep of episodesOf(s, 0)) out.push(episodeDto(c, lib, s, ep)) + } + } + if (want("MusicAlbum")) { + for (const al of lib.albums) out.push(albumDto(c, al)) + } + if (want("Audio")) { + for (const m of lib.music) out.push(songDto(c, lib, m)) + } + if (want("Photo")) { + for (const i of lib.images) { + out.push(makeDto(c, lib, i, { type: "Photo", parentId: numericId("lib-image") })) + } + } + if (want("Book")) { + for (const i of lib.books) { + out.push(makeDto(c, lib, i, { type: "Book", parentId: numericId("lib-book") })) + } + } + return out +} + +const csv = (value: string) => + String(value || "") + .split(/[,|]/) + .map((s) => s.trim()) + .filter(Boolean) + +function filterDtos( + list: any[], + c: any, + lib: EmbyLibrary, +): any[] { + const q = (k: string) => c.req.query(k) || "" + const search = q("SearchTerm").toLowerCase() + const nameStarts = q("NameStartsWith").toLowerCase() + const include = csv(q("IncludeItemTypes")) + const exclude = csv(q("ExcludeItemTypes")) + const mediaTypes = csv(q("MediaTypes")) + const idsParam = csv(q("Ids")) + const isFolder = q("IsFolder") + const filters = csv(q("Filters")) + const years = csv(q("Years")) + + let out = list + if (search) { + out = out.filter((d) => String(d.Name || "").toLowerCase().includes(search)) + } + if (nameStarts) { + out = out.filter((d) => + String(d.Name || "").toLowerCase().startsWith(nameStarts), + ) + } + if (include.length) { + out = out.filter((d) => + include.some((t) => String(d.Type).toLowerCase() === t.toLowerCase()), + ) + } + if (exclude.length) { + out = out.filter( + (d) => !exclude.some((t) => String(d.Type).toLowerCase() === t.toLowerCase()), + ) + } + if (mediaTypes.length) { + out = out.filter((d) => + mediaTypes.some( + (t) => String(d.MediaType || "").toLowerCase() === t.toLowerCase(), + ), + ) + } + if (idsParam.length) { + const set = new Set(idsParam.map((x) => origId(lib, x))) + out = out.filter((d) => { + const id = String(d.Id) + const orig = lib.ids.get(id) || id + return set.has(orig) || set.has(id) + }) + } + if (isFolder === "true") out = out.filter((d) => !!d.IsFolder) + if (isFolder === "false") out = out.filter((d) => !d.IsFolder) + if (years.length) { + out = out.filter((d) => years.includes(String(d.ProductionYear || ""))) + } + if (filters.some((f) => f.toLowerCase() === "isfavorite")) { + out = out.filter((d) => !!d.UserData?.IsFavorite) + } + if (filters.some((f) => f.toLowerCase() === "isplayed")) { + out = out.filter((d) => !!d.UserData?.Played) + } + if (filters.some((f) => f.toLowerCase() === "isunplayed")) { + out = out.filter((d) => !d.UserData?.Played) + } + return out +} + +function sortDtos( + list: any[], + c: any, + override?: { sortBy?: string; sortOrder?: string }, +) { + const sortBy = ( + override?.sortBy || + c.req.query("SortBy") || + "SortName" + ) + .split(",")[0] + .trim() + const desc = ( + override?.sortOrder || + c.req.query("SortOrder") || + "Ascending" + ) + .toLowerCase() + .startsWith("desc") + const key = sortBy.toLowerCase() + const valueOf = (d: any) => { + if (key === "datecreated") return String(d.DateCreated || "") + if (key === "productionyear") return Number(d.ProductionYear || 0) + if (key === "communityrating") return Number(d.CommunityRating || 0) + if (key === "random") return Math.random() + return String(d.SortName || d.Name || "").toLowerCase() + } + return list + .slice() + .sort((a, b) => { + const av = valueOf(a) + const bv = valueOf(b) + const cmp = typeof av === "number" && typeof bv === "number" ? av - bv : String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0 + return desc ? -cmp : cmp + }) +} + +function paginate(list: any[], c: any) { + const start = Math.max(0, Number(c.req.query("StartIndex") || 0)) + const limitRaw = c.req.query("Limit") + // 客户端不传 Limit 时给一个合理默认值:逐条 meta 校验的开销必须可控 + const limit = + limitRaw === undefined + ? 200 + : Math.max(0, Math.min(Number(limitRaw) || 0, 2000)) + return { + Items: list.slice(start, start + limit), + TotalRecordCount: list.length, + StartIndex: start, + } +} + +// ==================== Emby 路由 ==================== + +export const embyRouter = new Hono() + +embyRouter.use("*", async (c, next) => { + const settings = await getSettingsMap(c.env) + if (!mediaProtocolEnabled(settings, "media_emby_enabled")) return c.notFound() + await next() +}) + +// ---- 公共端点(无需 token) ---- + +// 浏览器直接打开 /@emby 时返回说明页(Go 在鉴权前处理,否则会 401) +embyRouter.get("/", (c) => c.json({ Service: "OpenList", Version: EMBY_VERSION })) +embyRouter.get("/web/index.html", (c) => c.html(embyWebNotice())) +embyRouter.get("/web/", (c) => c.html(embyWebNotice())) +embyRouter.get("/web", (c) => c.html(embyWebNotice())) + +embyRouter.get("/System/Info/Public", (c) => + c.json({ + ServerName: "OpenList", + Version: EMBY_VERSION, + Id: embyServerId(c), + OperatingSystem: "Linux", + LocalAddress: originOf(c), + WebPath: "", + StartupWizardCompleted: true, + }), +) + +embyRouter.get("/System/Ping", (c) => c.text("Emby Server")) + +embyRouter.get("/System/Info", (c) => + c.json({ + ServerName: "OpenList", + Version: EMBY_VERSION, + Id: embyServerId(c), + OperatingSystem: "Linux", + OperatingSystemDisplayName: "Linux", + LocalAddress: originOf(c), + WebPath: "", + StartupWizardCompleted: true, + HasPendingRestart: false, + IsShuttingDown: false, + SupportsLibraryMonitor: false, + CanSelfRestart: false, + CanLaunchWebBrowser: false, + SupportsHttps: false, + HasUpdateAvailable: false, + EncoderLocation: "System", + SystemArchitecture: "X64", + SystemUpdateLevel: "Release", + // 该端点匿名可访问;客户端本来就已知接入端口,回真实端口只是白给一份探测信息 + HttpServerPortNumber: 0, + HttpsPortNumber: 0, + ProgramDataPath: "", + ItemsByNamePath: "", + CachePath: "", + LogPath: "", + InternalMetadataPath: "", + TranscodingTempPath: "", + }), +) + +embyRouter.get("/System/Endpoint", (c) => { + // 按实际来源判断:固定返回 true 会让客户端误以为可内网直连,也等于对匿名 + // 访问者宣称内网可达 + const local = isLocalClient(c) + return c.json({ IsLocal: local, IsInNetwork: local }) +}) +embyRouter.get("/System/Configuration/Public", (c) => c.json({})) +embyRouter.get("/Branding/Configuration", (c) => c.json({})) +embyRouter.get("/Localization/Options", (c) => c.json([])) +embyRouter.post("/Sessions/Capabilities", (c) => c.body(null, 204)) +embyRouter.post("/Sessions/Capabilities/Full", (c) => c.body(null, 204)) + +const requestToken = (c: any) => { + const header = + c.req.header("X-Emby-Token") || c.req.header("X-MediaBrowser-Token") + if (header) return header + const auth = + c.req.header("X-Emby-Authorization") || c.req.header("Authorization") || "" + const m = auth.match(/Token="?([^",]+)"?/i) + return m?.[1] || c.req.query("api_key") || c.req.query("ApiKey") || "" +} + +const parseAuthHeader = (raw: string) => { + const out: Record<string, string> = {} + for (const part of String(raw || "").split(",")) { + const idx = part.indexOf("=") + if (idx < 0) continue + out[part.slice(0, idx).trim().toLowerCase()] = part + .slice(idx + 1) + .trim() + .replace(/^"|"$/g, "") + } + return out +} + +async function userFromDb(env: any, id?: number, name?: string) { + const db = await getDb(env) + return (db.users || []).find( + (u: any) => (id && u.id === id) || (name && u.username === name && !u.disabled), + ) +} + +/** 常量时间比较,避免按前缀命中后通过比较耗时泄露密钥内容 */ +const safeEqualStr = (a: string, b: string) => { + if (a.length !== b.length) return false + let diff = 0 + for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i) + return diff === 0 +} + +/** + * 按明文应用密钥查找记录(对齐 Go 的 LookUpAppKeyUser)。 + * 先用 prefix 把范围缩到同前缀的少数几条,再解密做常量时间比对; + * 相比「遍历全部 key 比 sha256」,密钥多了以后也不会退化成 O(n)。 + */ +async function findAppKeyByPlain(env: any, plain: string, db: MediaDb) { + const key = String(plain || "").trim() + if (!key.startsWith("olmp_")) return null + const prefix = key.slice(0, 10) + const ctx = { env } + for (const row of db.app_keys.filter((k) => k.prefix === prefix)) { + const stored = await mediaHelpers.appKeyPlainOf(ctx, row) + if (stored && safeEqualStr(stored, key)) return row + } + return null +} + +/** + * Emby 客户端按 GUID 校验用户 Id;这里把数字 id 映射成稳定、可逆的 GUID 形状。 + * 数字 id 与用户名仍可解析(对齐 Go 的 embyResolveUserID 兼容旧形式)。 + */ +const userGuid = (id: number) => + `00000000-0000-4000-8000-${Math.max(0, Math.trunc(Number(id) || 0)) + .toString(16) + .padStart(12, "0")}` + +const parseUserGuid = (raw: string): number => { + const m = String(raw || "").match(/^0{8}-0{4}-4000-8000-([0-9a-f]{12})$/i) + if (!m) return 0 + const id = parseInt(m[1], 16) + return Number.isFinite(id) && id > 0 ? id : 0 +} + +/** 请求是否来自本机 / 内网(/System/Endpoint 用,不能固定返回 true) */ +const isLocalClient = (c: any) => { + const ip = String( + c.req.header("cf-connecting-ip") || + c.req.header("x-real-ip") || + c.req.header("x-forwarded-for") || + "", + ) + .split(",")[0] + .trim() + .toLowerCase() + if (!ip) return false + if (ip === "::1" || ip.startsWith("127.")) return true + if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true + const m = ip.match(/^172\.(\d{1,3})\./) + if (m && Number(m[1]) >= 16 && Number(m[1]) <= 31) return true + return ip.startsWith("fc") || ip.startsWith("fd") || ip.startsWith("fe80:") +} + +async function tokenUser(env: any, token: string) { + if (!token) return null + const db = await getMediaDb(env) + const hash = await mediaHelpers.sha256Hex(token) + const device = db.emby_devices.find((d) => d.token === hash) + if (device) { + const user = await userFromDb(env, device.user_id) + if (user && !user.disabled) { + device.last_seen_at = new Date().toISOString() + return user + } + } + const key = await findAppKeyByPlain(env, token, db) + if (key) { + const user = await userFromDb(env, key.user_id) + if (user && !user.disabled && user.username !== "guest") { + key.last_used_at = new Date().toISOString() + return user + } + } + return null +} + +const userDto = (u: any, c: any) => ({ + Name: u.username, + ServerId: embyServerId(c), + Id: userGuid(u.id), + HasPassword: !!u.password, + HasConfiguredPassword: !!u.password, + Configuration: {}, + Policy: { + IsAdministrator: u.role === 2, + EnableAllFolders: true, + EnabledFolders: [], + }, + IsAdministrator: u.role === 2, + IsDisabled: !!u.disabled, +}) + +embyRouter.get("/Users/Public", async (c) => { + const db = await getDb(c.env) + const users = (db.users || []).filter( + (u: any) => !u.disabled && u.username !== "guest", + ) + return c.json(users.map((u: any) => userDto(u, c))) +}) + +async function readAuthBody(c: any): Promise<{ username: string; pw: string }> { + let username = c.req.query("Username") || c.req.query("username") || "" + let pw = c.req.query("Pw") || c.req.query("Password") || "" + try { + const ct = String(c.req.header("content-type") || "").toLowerCase() + if (ct.includes("json")) { + const body = await c.req.json() + username = String(body.Username || body.username || username) + pw = String(body.Pw || body.Password || pw) + } else if (ct.includes("form")) { + const form = await c.req.formData() + username = String(form.get("Username") || form.get("username") || username) + pw = String(form.get("Pw") || form.get("Password") || pw) + } + } catch { + /* ignore */ + } + return { username: username.trim(), pw } +} + +async function authenticate(c: any, username: string, pw: string) { + if (!username) return null + const db = await getDb(c.env) + const user = (db.users || []).find( + (u: any) => u.username === username && !u.disabled && u.username !== "guest", + ) + if (!user) return null + // 应用密钥也可以直接当密码用(对齐 Go) + if (pw.startsWith("olmp_")) { + const mediaDb = await getMediaDb(c.env) + const key = await findAppKeyByPlain(c.env, pw, mediaDb) + if (key) { + const owner = await userFromDb(c.env, key.user_id) + if (owner && owner.id === user.id) { + key.last_used_at = new Date().toISOString() + return user + } + } + return null + } + if (!user.password || !user.salt) return null + const hash = await twoStepHash(pw, user.salt) + return hash === user.password ? user : null +} + +async function issueToken(c: any, user: any) { + const db = await getMediaDb(c.env) + const token = mediaHelpers.randomToken("emb_") + const header = parseAuthHeader(c.req.header("X-Emby-Authorization") || "") + const device: EmbyDevice = { + id: db.next_id++, + user_id: user.id, + device_id: header.deviceid || "", + device_name: header.device || "", + client: header.client || "", + version: header.version || "", + token: await mediaHelpers.sha256Hex(token), + last_seen_at: new Date().toISOString(), + created_at: new Date().toISOString(), + app_key_id: 0, + } + db.emby_devices.push(device) + await saveMediaDb(c.env, db) + return token +} + +embyRouter.post("/Users/AuthenticateByName", async (c) => { + const { username, pw } = await readAuthBody(c) + if (await mediaHelpers.authLockedFor(c, username)) { + return c.json({ Error: "Too many failed attempts" }, 429) + } + const user = await authenticate(c, username, pw) + if (!user) { + if (username || pw) await mediaHelpers.authFailed(c, username) + return c.json({ Error: "Unauthorized" }, 401) + } + mediaHelpers.authSucceeded(c, username) + const token = await issueToken(c, user) + return c.json({ + User: userDto(user, c), + AccessToken: token, + ServerId: embyServerId(c), + }) +}) + +embyRouter.post("/Users/:id/Authenticate", async (c) => { + const rawId = c.req.param("id") + const { pw } = await readAuthBody(c) + let user: any = null + // 兼容三种形式:GUID(当前返回)、数字(旧客户端缓存)、用户名 + const guidId = parseUserGuid(rawId) + if (guidId) { + user = await userFromDb(c.env, guidId) + } + if (!user && /^\d+$/.test(rawId)) { + user = await userFromDb(c.env, Number(rawId)) + } + if (!user) { + const db = await getDb(c.env) + user = (db.users || []).find( + (u: any) => String(u.id) === rawId || u.username === rawId, + ) + } + if (!user || user.disabled) return c.json({ Error: "Unauthorized" }, 401) + if (await mediaHelpers.authLockedFor(c, user.username)) { + return c.json({ Error: "Too many failed attempts" }, 429) + } + const okUser = await authenticate(c, user.username, pw) + if (!okUser) { + await mediaHelpers.authFailed(c, user.username) + return c.json({ Error: "Unauthorized" }, 401) + } + mediaHelpers.authSucceeded(c, user.username) + const token = await issueToken(c, okUser) + return c.json({ + User: userDto(okUser, c), + AccessToken: token, + ServerId: embyServerId(c), + }) +}) + +/** 取条目列表里第一个「当前用户可访问且带封面」的条目封面 */ +async function firstAccessibleCover(env: any, user: any, items: MediaItem[]) { + for (const item of items) { + if (!item.cover) continue + if (await mediaHelpers.canAccessItem(env, user, item)) return item.cover + } + return "" +} + +/** + * 解析图片请求对应的封面。 + * + * 必须基于 lib.db 再做一次完整权限校验: + * - lib.db 是全库快照,只有 videos/music 等派生集合按用户过滤过; + * - origId 对未登记的 ID 原样返回,因此 `mv-42` / `ep-7-2` 这类请求 + * 可能直接命中被隐藏、位于访客根目录之外或类型已关闭的条目。 + * Series / Season / Album 的封面来自聚合里的某一条目,同样要逐条校验, + * 不能直接读聚合对象上缓存的 cover。 + */ +async function coverOfId(env: any, user: any, lib: EmbyLibrary, rawId: string) { + const id = origId(lib, rawId) + + const single = id.match(/^(?:mv|so)-(\d+)$/) + if (single) { + const item = findItem(lib.db, Number(single[1])) + if (!item || !(await mediaHelpers.canAccessItem(env, user, item))) return "" + return item.cover || "" + } + + const episode = id.match(/^ep-(\d+)-(\d+)$/) + if (episode) { + // 剧集封面取所属条目(合并条目 / 单集文件)的封面 + const item = findItem(lib.db, Number(episode[1])) + if (!item || !(await mediaHelpers.canAccessItem(env, user, item))) return "" + return item.cover || "" + } + + if (id.startsWith("sr-")) { + const series = lib.series.find((s) => s.key === id.slice(3)) + return series ? firstAccessibleCover(env, user, series.items) : "" + } + + if (id.startsWith("sn-")) { + const rest = id.slice(3) + const sep = rest.lastIndexOf("-") + if (sep < 0) return "" + const series = lib.series.find((s) => s.key === rest.slice(0, sep)) + if (!series) return "" + const season = Number(rest.slice(sep + 1)) || 1 + return firstAccessibleCover( + env, + user, + episodesOf(series, season).map((ep) => ep.item), + ) + } + + if (id.startsWith("al-")) { + const album = lib.albums.find((a) => a.key === id.slice(3)) + return album ? firstAccessibleCover(env, user, album.items) : "" + } + + if (id.startsWith("ar-")) { + const key = id.slice(3) + const items: MediaItem[] = [] + for (const album of lib.albums) { + if (artistKeyOf(album.items[0]) !== key) continue + items.push(...album.items) + } + return firstAccessibleCover(env, user, items) + } + + return "" +} + +/** + * 图片端点:允许匿名(客户端封面 URL 不带 token),但按 guest 权限过滤。 + * + * 带 token 时按该用户过滤,没有 token 才退回 guest,两条路径都走 + * base_path / 类型开关 / hidden / meta 的同一套判断。 + */ +async function handleItemImage(c: any) { + const token = requestToken(c) + const user = + (token ? await tokenUser(c.env, token) : null) ?? (await guestUserOf(c.env)) + if (!user) return c.body(null, 401) + const lib = await loadLibrary(c.env, user) + const cover = await coverOfId(c.env, user, lib, c.req.param("id")) + if (!cover) return c.body(null, 404) + return serveCover(c, cover) +} + +embyRouter.get("/Items/:id/Images/:type", handleItemImage) + +async function serveCover(c: any, cover: string) { + const origin = originOf(c) + if (cover.startsWith("thumb:")) { + return c.redirect( + `${origin}/api/public/media/thumb?path=${encodeURIComponent(cover.slice(6))}`, + ) + } + if (cover.startsWith("data:")) { + const m = cover.match(/^data:[^;]+;base64,(.*)$/) + if (!m) return c.body(null, 404) + let bytes: Uint8Array + try { + bytes = Uint8Array.from(atob(m[1]), (ch) => ch.charCodeAt(0)) + } catch { + // 存量数据里可能有被截断/篡改的 base64 + return c.body(null, 404) + } + // 内联串里的 MIME 来自历史数据(早期版本会采信 ID3 声明的 image/svg+xml), + // 这里按魔数重新判定,认不出来直接 404,不再把存储串当 Content-Type 用。 + const mime = mediaHelpers.sniffImageMime(bytes) + if (!mime) return c.body(null, 404) + c.header("Content-Type", mime) + c.header("Cache-Control", "public, max-age=604800") + c.header("X-Content-Type-Options", "nosniff") + // 再补一层沙箱:即使以后嗅探白名单里混进可执行格式,也不会在本站源下跑脚本 + c.header("Content-Security-Policy", "default-src 'none'; sandbox") + return c.body(bytes as any) + } + if (/^https?:\/\//i.test(cover)) { + const settings = await getSettingsMap(c.env) + if (settingBool(settings, "media_proxy_cover", false)) { + return c.redirect( + `${origin}/api/public/media/proxy?url=${encodeURIComponent(cover)}`, + ) + } + return c.redirect(cover) + } + return c.redirect( + `${origin}/d${encodeDownloadPath(cover)}?sign=${encodeURIComponent(await signDownloadPath(c, cover, 0))}`, + ) +} + +// ---- 以下需要 token ---- + +embyRouter.use("*", async (c, next) => { + const token = requestToken(c) + const user = token ? await tokenUser(c.env, token) : null + if (!user) { + return c.json( + { Error: "Unauthorized", ResponseStatus: { ErrorCode: "Unauthorized" } }, + 401, + ) + } + ;(c as any).set("mediaUser", user) + await next() +}) + +embyRouter.get("/Users/Me", async (c) => { + const user = (c as any).get("mediaUser") + return c.json(userDto(user, c)) +}) + +embyRouter.get("/Users/:id", async (c) => { + const user = (c as any).get("mediaUser") + return c.json(userDto(user, c)) +}) + +embyRouter.get("/Users", async (c) => { + const user = (c as any).get("mediaUser") + if (!isAdmin(user)) { + const db = await getDb(c.env) + const list = (db.users || []).filter( + (u: any) => !u.disabled && u.username !== "guest", + ) + return c.json(list.map((u: any) => userDto(u, c))) + } + const db = await getDb(c.env) + return c.json((db.users || []).map((u: any) => userDto(u, c))) +}) + +embyRouter.get("/Users/Query", async (c) => { + const db = await getDb(c.env) + const list = (db.users || []).filter((u: any) => !u.disabled) + return c.json({ Items: list.map((u: any) => userDto(u, c)), TotalRecordCount: list.length }) +}) + +const handleViews = async (c: any) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const items = viewDtos(c, lib) + return c.json({ Items: items, TotalRecordCount: items.length }) +} +embyRouter.get("/Users/:id/Views", handleViews) +embyRouter.get("/UserViews", handleViews) + +const handleItems = async (c: any) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const parent = origId(lib, c.req.query("ParentId") || "") + const recursive = + c.req.query("Recursive") === "true" || + c.req.query("Recursive") === "1" || + !parent + const include = csv(c.req.query("IncludeItemTypes")) + + let list: any[] + if (recursive) { + list = allDtos(c, lib, include) + } else { + list = childrenOf(c, lib, parent) + } + list = filterDtos(list, c, lib) + list = sortDtos(list, c) + + // 对当前页做 meta 级权限过滤 + const page = paginate(list, c) + if (!isAdmin(user)) { + const allowed: any[] = [] + for (const dto of page.Items) { + const orig = origId(lib, String(dto.Id)) + const numeric = orig.match(/^(?:mv|so)-(\d+)$/) + if (numeric) { + const item = findItem(lib.db, Number(numeric[1])) + if (item && !(await mediaHelpers.canAccessItem(c.env, user, item))) continue + } + allowed.push(dto) + } + page.Items = allowed + } + return c.json(page) +} + +embyRouter.get("/Items", handleItems) +embyRouter.get("/Users/:id/Items", handleItems) + +const handleLatest = async (c: any) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const parent = origId(lib, c.req.query("ParentId") || "") + let list = + parent === "lib-music" + ? lib.albums.map((a) => albumDto(c, a)) + : parent === "lib-tv" + ? lib.series.map((s) => seriesDto(c, lib, s)) + : lib.videos.filter((v) => !isSeriesItem(v)).map((v) => movieDto(c, lib, v)) + list = sortDtos(list, c, { + sortBy: "DateCreated", + sortOrder: "Descending", + }) + return c.json({ Items: list.slice(0, 20), TotalRecordCount: Math.min(list.length, 20) }) +} +embyRouter.get("/Users/:id/Items/Latest", handleLatest) +embyRouter.get("/Items/Latest", handleLatest) + +const handleResume = async (c: any) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const rows = lib.db.user_data + .filter((d) => d.user_id === user.id && !d.played && d.position_ticks > 0) + .sort((a, b) => (a.updated_at < b.updated_at ? 1 : -1)) + const items: any[] = [] + for (const row of rows) { + const item = findItem(lib.db, row.item_id) + if (!item || item.hidden) continue + if (!(await mediaHelpers.canAccessItem(c.env, user, item))) continue + if (row.episode_index > 0) { + const s = lib.series.find((x) => x.items.some((i) => i.id === item.id)) + if (!s) continue + const ep = episodesOf(s, 0).find( + (e) => + e.item.id === item.id && + e.index === row.episode_index && + (Number(e.season) || 1) === (Number(row.season) || 1), + ) + if (ep) { + items.push({ + ...episodeDto(c, lib, s, ep), + UserData: { + ...userDataOf(lib, item.id, row.episode_index, row.season), + PlaybackPositionTicks: row.position_ticks, + }, + }) + } + } else { + items.push({ + ...makeDto(c, lib, item), + UserData: { + ...userDataOf(lib, item.id, 0), + PlaybackPositionTicks: row.position_ticks, + }, + }) + } + } + return c.json({ Items: items, TotalRecordCount: items.length }) +} +embyRouter.get("/Users/:id/Items/Resume", handleResume) +embyRouter.get("/Items/Resume", handleResume) + +embyRouter.get("/Users/:id/Items/Root", (c) => + c.json({ + Name: "OpenList", + Id: numericId("root"), + ServerId: embyServerId(c), + Type: "CollectionFolder", + IsFolder: true, + CollectionType: "root", + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }), +) + +embyRouter.get("/Items/Counts", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + return c.json({ + MovieCount: lib.videos.filter((v) => !isSeriesItem(v)).length, + SeriesCount: lib.series.length, + EpisodeCount: lib.series.reduce((n, s) => n + episodesOf(s, 0).length, 0), + AlbumCount: lib.albums.length, + SongCount: lib.music.length, + BoxSetCount: 0, + BookCount: lib.books.length, + ItemCount: lib.videos.length + lib.music.length + lib.images.length + lib.books.length, + }) +}) + +const handleItemDetail = async (c: any) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id") || c.req.param("itemId")) + const numeric = id.match(/^(?:mv|so)-(\d+)$/) + if (numeric) { + const item = findItem(lib.db, Number(numeric[1])) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) { + return c.json({ Error: "Not Found" }, 404) + } + return c.json(makeDto(c, lib, item)) + } + if (id.startsWith("ep-")) { + const [, itemId, epIndex] = id.split("-") + const item = findItem(lib.db, Number(itemId)) + const s = lib.series.find((x) => x.items.some((i) => i.id === item?.id)) + if (!item || !s) return c.json({ Error: "Not Found" }, 404) + const ep = episodesOf(s, 0).find((e) => e.index === Number(epIndex)) + if (!ep) return c.json({ Error: "Not Found" }, 404) + return c.json(episodeDto(c, lib, s, ep)) + } + if (id.startsWith("sr-")) { + const s = lib.series.find((x) => x.key === id.slice(3)) + return s ? c.json(seriesDto(c, lib, s)) : c.json({ Error: "Not Found" }, 404) + } + if (id.startsWith("sn-")) { + const rest = id.slice(3) + const idx = rest.lastIndexOf("-") + const key = rest.slice(0, idx) + const season = Number(rest.slice(idx + 1)) || 1 + const s = lib.series.find((x) => x.key === key) + return s ? c.json(seasonDto(c, s, season)) : c.json({ Error: "Not Found" }, 404) + } + if (id.startsWith("al-")) { + const al = lib.albums.find((x) => x.key === id.slice(3)) + return al ? c.json(albumDto(c, al)) : c.json({ Error: "Not Found" }, 404) + } + return c.json({ Error: "Not Found" }, 404) +} + +embyRouter.get("/Items/:id", handleItemDetail) +embyRouter.get("/Users/:id/Items/:itemId", handleItemDetail) + +/** + * Emby WebSocket(会话/实时通知通道)。 + * + * 对齐 Go 的 embyWebSocket:握手成功后立即回一条 KeepAlive,并每 30s 心跳。 + * 客户端只靠这个通道判断「服务器是否在线」,缺失时会持续重连并刷 404。 + * + * 实现依赖 Cloudflare Workers 的 WebSocketPair;其它运行时(Node/EdgeOne) + * 没有该能力,明确返回 426,避免客户端误以为网络故障。 + */ +const handleEmbyWebSocket = async (c: any) => { + const g: any = globalThis as any + if (typeof g.WebSocketPair === "undefined") { + return c.body("WebSocket is not supported on this runtime", 426) + } + try { + const { upgradeWebSocket } = await import("hono/cloudflare-workers") + const handler: any = upgradeWebSocket(() => { + let timer: ReturnType<typeof setInterval> | null = null + const stop = () => { + if (timer) { + clearInterval(timer) + timer = null + } + } + return { + onOpen(_evt: any, ws: any) { + try { + ws.send('{"MessageType":"KeepAlive"}') + } catch { + /* ignore */ + } + timer = setInterval(() => { + try { + ws.send('{"MessageType":"KeepAlive"}') + } catch { + stop() + } + }, 30_000) + }, + onMessage(_evt: any, _ws: any) { + // Emby 客户端只发送 KeepAlive / 会话上报,无需回写 + }, + onClose() { + stop() + }, + onError() { + stop() + }, + } + }) + return handler(c, async () => {}) + } catch (e: any) { + console.warn("[emby] websocket upgrade failed:", e?.message || e) + return c.body("WebSocket upgrade failed", 426) + } +} + +// 客户端大小写不统一,两个路径都要注册 +embyRouter.get("/embywebsocket", handleEmbyWebSocket) +embyRouter.get("/EmbyWebsocket", handleEmbyWebSocket) +embyRouter.get("/mediabrowserwebsocket", handleEmbyWebSocket) + +// 客户端常探测的辅助端点:返回空结构而不是 401/404,避免连接阶段被判服务器异常 +embyRouter.get("/Sessions", (c) => c.json([])) +embyRouter.get("/System/Activity/Log", (c) => c.json({ Items: [], TotalRecordCount: 0 })) +embyRouter.get("/Plugins", (c) => c.json([])) +embyRouter.get("/Videos/:id/info", (c) => c.body(null, 204)) +embyRouter.get("/Users/:id/Images/:type", (c) => c.body(null, 404)) +embyRouter.get("/Shows/Upcoming", (c) => + c.json({ Items: [], TotalRecordCount: 0 }), +) + +/** 歌词:作为外挂字幕轨(Emby Theater 的歌词面板只读 Subtitle 流) */ +const handleLyrics = async (c: any, idRaw: string) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, idRaw) + const numeric = id.match(/^so-(\d+)$/) + const item = numeric ? findItem(lib.db, Number(numeric[1])) : undefined + if (!item?.lyrics) return c.body("", 200) + c.header("Content-Type", "text/plain; charset=utf-8") + return c.body(item.lyrics) +} +embyRouter.get("/Audio/:id/lyrics", (c) => handleLyrics(c, c.req.param("id"))) + +/** 播放目标解析 */ +async function playTarget(c: any, idRaw: string) { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, idRaw) + const numeric = id.match(/^(?:mv|so)-(\d+)$/) + if (numeric) { + const item = findItem(lib.db, Number(numeric[1])) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) return null + return { + item, + episode: null as EpisodeInfo | null, + rawPath: mediaHelpers.itemPath(item), + name: item.scraped_name || item.file_name, + container: item.file_name.split(".").pop() || "mp4", + duration: item.duration, + } + } + const epMatch = id.match(/^ep-(\d+)-(?:(\d+)-)?(\d+)$/) + if (epMatch) { + const item = findItem(lib.db, Number(epMatch[1])) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) return null + const seasonNum = epMatch[2] ? Number(epMatch[2]) : 0 + const indexNum = Number(epMatch[3]) + const eps = parseEpisodesOf(item) + const ep = eps.find( + (e) => + Number(e.index) === indexNum && + (!seasonNum || (Number(e.season) || Number(item.season) || 1) === seasonNum), + ) + if (!ep) { + // 未做目录合并的单集文件没有 episodes 列表,但 episodesOf() 仍会为它 + // 暴露 ep-<item>-<season>-<episode>,这里必须能解析,否则播放 404。 + if (eps.length === 0 && (!item.episode || Number(item.episode) === indexNum)) { + return { + item, + episode: null as EpisodeInfo | null, + rawPath: mediaHelpers.itemPath(item), + name: item.episode_title || item.scraped_name || item.file_name, + container: item.file_name.split(".").pop() || "mp4", + duration: item.duration, + } + } + return null + } + return { + item, + episode: ep, + rawPath: mediaHelpers.itemPath(item, ep.index, Number(ep.season) || seasonNum), + name: ep.title || ep.file_name, + container: ep.file_name.split(".").pop() || "mp4", + duration: ep.duration || item.duration, + } + } + if (id.startsWith("sr-")) { + const s = lib.series.find((x) => x.key === id.slice(3)) + const eps: EmbyEpisode[] = s ? episodesOf(s, 0) : [] + const ep: EmbyEpisode | undefined = eps[0] + if (!s || !ep) return null + return { + item: ep.item, + episode: { + index: ep.index, + title: ep.title, + file_name: ep.fileName, + season: ep.season, + }, + rawPath: mediaHelpers.itemPath(ep.item, ep.index), + name: ep.title, + container: ep.fileName.split(".").pop() || "mp4", + duration: ep.item.duration, + } + } + if (id.startsWith("al-")) { + const al = lib.albums.find((x) => x.key === id.slice(3)) + const first = al?.items[0] + if (!first) return null + return { + item: first, + episode: null, + rawPath: mediaHelpers.itemPath(first), + name: first.scraped_name || first.file_name, + container: first.file_name.split(".").pop() || "mp3", + duration: first.duration, + } + } + return null +} + +/** 播放流地址:必须带 api_key,且 Path/DirectStreamUrl 都用它(对齐最新 Go 修复) */ +const streamUrlOf = (c: any, idRaw: string, container: string) => { + const token = requestToken(c) + const kind = "Videos" + const suffix = container ? `.${container}` : "" + const q = new URLSearchParams({ + Static: "true", + MediaSourceId: idRaw, + }) + if (token) q.set("api_key", token) + return `${originOf(c)}/@emby/${kind}/${idRaw}/stream${suffix}?${q.toString()}` +} + +const handlePlaybackInfo = async (c: any) => { + let idRaw = c.req.param("id") + // 兼容 POST body 里的 ItemId + if (!idRaw) { + const body = await c.req.json().catch(() => ({})) + idRaw = String(body.ItemId || "") + } + const target = await playTarget(c, idRaw) + if (!target) return c.json({ Error: "Not Found" }, 404) + const isAudio = target.item.media_type === "music" + const kind = isAudio ? "Audio" : "Videos" + const url = (() => { + const token = requestToken(c) + const suffix = target.container ? `.${target.container}` : "" + const q = new URLSearchParams({ Static: "true", MediaSourceId: idRaw }) + if (token) q.set("api_key", token) + return `${originOf(c)}/@emby/${kind}/${idRaw}/stream${suffix}?${q.toString()}` + })() + + const streams: any[] = [ + { Type: isAudio ? "Audio" : "Video", Index: 0, IsDefault: true }, + ] + let subtitleIndex: number | undefined + if (isAudio && target.item.lyrics) { + subtitleIndex = streams.length + streams.push({ + Type: "Subtitle", + Index: subtitleIndex, + IsDefault: false, + IsExternal: true, + Codec: "srt", + Language: "und", + DisplayTitle: "Lyrics", + DeliveryUrl: `/Items/${idRaw}/Subtitles/${subtitleIndex}/Stream.js`, + }) + } + + return c.json({ + MediaSources: [ + { + Protocol: "Http", + Id: idRaw, + ItemId: idRaw, + Path: url, + Type: "Default", + Container: target.container, + Name: target.name, + Size: target.item.file_size || 0, + ETag: hashKey(`${target.rawPath}|${target.item.file_size}`), + RunTimeTicks: ticks(target.duration), + SupportsDirectPlay: true, + SupportsDirectStream: true, + SupportsTranscoding: false, + SupportsProbing: false, + MediaStreams: streams, + MediaAttachments: [], + Formats: [], + RequiredHttpHeaders: {}, + DirectStreamUrl: url, + AddApiKeyToDirectStreamUrl: false, + DefaultSubtitleStreamIndex: subtitleIndex, + }, + ], + PlaySessionId: `openlist-${target.item.id}-${target.episode?.index || 0}`, + }) +} + +embyRouter.get("/Items/:id/PlaybackInfo", handlePlaybackInfo) +embyRouter.post("/Items/:id/PlaybackInfo", handlePlaybackInfo) + +embyRouter.post("/Items/:id/Refresh", (c) => c.json({})) +embyRouter.get("/Items/:id/Similar", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id")) + const numeric = id.match(/^mv-(\d+)$/) + const item = numeric ? findItem(lib.db, Number(numeric[1])) : undefined + if (!item) return c.json({ Items: [], TotalRecordCount: 0 }) + const genres = new Set(item.genre.split(",").filter(Boolean)) + const list = lib.videos + .filter( + (v) => + v.id !== item.id && + !isSeriesItem(v) && + v.genre.split(",").some((g) => genres.has(g)), + ) + .slice(0, 20) + .map((v) => movieDto(c, lib, v)) + return c.json({ Items: list, TotalRecordCount: list.length }) +}) + +// 剧集 +embyRouter.get("/Shows/:id/Seasons", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id")) + const key = id.startsWith("sr-") ? id.slice(3) : id + const s = lib.series.find((x) => x.key === key) + if (!s) return c.json({ Items: [], TotalRecordCount: 0 }) + const list = seasonListOf(s).map((sn) => seasonDto(c, s, sn)) + return c.json({ Items: list, TotalRecordCount: list.length }) +}) + +embyRouter.get("/Shows/:id/Episodes", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id")) + const key = id.startsWith("sr-") ? id.slice(3) : id + const s = lib.series.find((x) => x.key === key) + if (!s) return c.json({ Items: [], TotalRecordCount: 0 }) + const season = Number(c.req.query("Season") || 0) + let list = episodesOf(s, season).map((ep) => episodeDto(c, lib, s, ep)) + list = filterDtos(list, c, lib) + list = sortDtos(list, c) + return c.json(paginate(list, c)) +}) + +embyRouter.get("/Shows/NextUp", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const out: any[] = [] + for (const s of lib.series) { + const eps = episodesOf(s, 0) + const next = eps.find((ep) => { + const row = lib.db.user_data.find( + (d) => + d.user_id === user.id && + d.item_id === ep.item.id && + d.episode_index === ep.index && + (Number(d.season) || 1) === (Number(ep.season) || 1), + ) + return !row?.played + }) + if (next) out.push(episodeDto(c, lib, s, next)) + } + return c.json({ Items: out.slice(0, 20), TotalRecordCount: Math.min(out.length, 20) }) +}) + +// 音乐 +embyRouter.get("/Artists", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const map = new Map<string, any>() + for (const al of lib.albums) { + const key = artistKeyOf(al.items[0]) + const row = map.get(key) || { + Name: al.artist, + SortName: al.artist, + ServerId: embyServerId(c), + Id: numericId(`ar-${key}`), + Type: "MusicArtist", + IsFolder: true, + AlbumCount: 0, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + } + row.AlbumCount += 1 + map.set(key, row) + } + const list = Array.from(map.values()) + return c.json({ Items: list, TotalRecordCount: list.length }) +}) +embyRouter.get("/Artists/AlbumArtists", (c) => c.redirect(`${originOf(c)}/@emby/Artists${new URL(c.req.url).search}`)) +embyRouter.get("/Artists/:id", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id")) + const key = id.startsWith("ar-") ? id.slice(3) : id + const al = lib.albums.find((a) => artistKeyOf(a.items[0]) === key) + if (!al) return c.json({ Error: "Not Found" }, 404) + return c.json({ + Name: al.artist, + ServerId: embyServerId(c), + Id: numericId(`ar-${key}`), + Type: "MusicArtist", + IsFolder: true, + AlbumCount: lib.albums.filter((a) => artistKeyOf(a.items[0]) === key).length, + ImageTags: {}, + UserData: { PlaybackPositionTicks: 0, PlayCount: 0, IsFavorite: false, Played: false }, + }) +}) +embyRouter.get("/MusicGenres", (c) => c.json({ Items: [], TotalRecordCount: 0 })) + +// 收藏 / 已看 +async function setFavorite(c: any, idRaw: string, favorite: boolean) { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, idRaw) + let itemId = 0 + let epIndex = 0 + const numeric = id.match(/^(?:mv|so)-(\d+)$/) + if (numeric) itemId = Number(numeric[1]) + const epMatch = id.match(/^ep-(\d+)-(\d+)$/) + if (epMatch) { + itemId = Number(epMatch[1]) + epIndex = Number(epMatch[2]) + } + if (!itemId) return c.json({ Error: "Not Found" }, 404) + const item = findItem(lib.db, itemId) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) { + return c.json({ Error: "Not Found" }, 404) + } + const row = ensureUserData(lib.db, user.id, itemId, epIndex) + row.is_favorite = favorite + row.updated_at = new Date().toISOString() + await saveMediaDb(c.env, lib.db) + return c.json({}) +} + +async function setPlayed(c: any, idRaw: string, played: boolean) { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, idRaw) + const ids: Array<{ itemId: number; ep: number }> = [] + const numeric = id.match(/^(?:mv|so)-(\d+)$/) + if (numeric) ids.push({ itemId: Number(numeric[1]), ep: 0 }) + const epMatch = id.match(/^ep-(\d+)-(\d+)$/) + if (epMatch) ids.push({ itemId: Number(epMatch[1]), ep: Number(epMatch[2]) }) + // 剧集/季:批量标记 + if (id.startsWith("sr-") || id.startsWith("sn-")) { + const key = id.startsWith("sr-") ? id.slice(3) : id.slice(3, id.lastIndexOf("-")) + const s = lib.series.find((x) => x.key === key) + if (s) { + const season = id.startsWith("sn-") ? Number(id.slice(id.lastIndexOf("-") + 1)) : 0 + for (const ep of episodesOf(s, season)) { + ids.push({ itemId: ep.item.id, ep: ep.index }) + } + } + } + if (ids.length === 0) return c.json({ Error: "Not Found" }, 404) + for (const { itemId, ep } of ids) { + const row = ensureUserData(lib.db, user.id, itemId, ep) + row.played = played + if (played && row.play_count === 0) row.play_count = 1 + row.position_ticks = played ? row.position_ticks : 0 + row.updated_at = new Date().toISOString() + } + await saveMediaDb(c.env, lib.db) + return c.json({}) +} + +embyRouter.get("/Users/:id/FavoriteItems", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const rows = listPlaylistFavorites(lib.db, user.id) + const items: any[] = [] + for (const row of rows) { + const item = findItem(lib.db, row.item_id) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) continue + items.push(makeDto(c, lib, item)) + } + return c.json({ Items: items, TotalRecordCount: items.length }) +}) + +function listPlaylistFavorites(db: MediaDb, userId: number) { + return db.user_data.filter((d) => d.user_id === userId && d.is_favorite) +} + +embyRouter.get("/Users/:id/PlayedItems", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const rows = lib.db.user_data.filter((d) => d.user_id === user.id && d.played) + const items: any[] = [] + for (const row of rows) { + const item = findItem(lib.db, row.item_id) + if (!item) continue + items.push(makeDto(c, lib, item)) + } + return c.json({ Items: items, TotalRecordCount: items.length }) +}) + +embyRouter.post("/Users/:id/FavoriteItems/:itemId", (c) => + setFavorite(c, c.req.param("itemId"), true), +) +embyRouter.delete("/Users/:id/FavoriteItems/:itemId", (c) => + setFavorite(c, c.req.param("itemId"), false), +) +embyRouter.post("/Users/:id/PlayedItems/:itemId", (c) => + setPlayed(c, c.req.param("itemId"), true), +) +embyRouter.delete("/Users/:id/PlayedItems/:itemId", (c) => + setPlayed(c, c.req.param("itemId"), false), +) + +// UserData 读写 +async function readUserData(c: any, idRaw: string) { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, idRaw) + const numeric = id.match(/^(?:mv|so)-(\d+)$/) + // 新格式 ep-<item>-<season>-<index>;旧格式 ep-<item>-<index> 仍兼容 + const epMatch = id.match(/^ep-(\d+)-(?:(\d+)-)?(\d+)$/) + const itemId = numeric ? Number(numeric[1]) : epMatch ? Number(epMatch[1]) : 0 + const season = epMatch?.[2] ? Number(epMatch[2]) : 0 + const ep = epMatch ? Number(epMatch[3]) : 0 + const item = findItem(lib.db, itemId) + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) { + return null + } + return { item, ep, season, data: userDataOf(lib, itemId, ep, season) } +} + +embyRouter.get("/Users/:id/Items/:itemId/UserData", async (c) => { + const found = await readUserData(c, c.req.param("itemId")) + if (!found) return c.json({ Error: "Not Found" }, 404) + return c.json(found.data) +}) + +embyRouter.post("/Users/:id/Items/:itemId/UserData", async (c) => { + const user = (c as any).get("mediaUser") + const found = await readUserData(c, c.req.param("itemId")) + if (!found) return c.json({ Error: "Not Found" }, 404) + const body = await c.req.json().catch(() => ({})) + const db = await getMediaDb(c.env) + const row = ensureUserData(db, user.id, found.item.id, found.ep, found.season) + if (body.Played !== undefined) row.played = !!body.Played + if (body.IsFavorite !== undefined) row.is_favorite = !!body.IsFavorite + if (body.PlaybackPositionTicks !== undefined) { + row.position_ticks = Math.max(0, Number(body.PlaybackPositionTicks) || 0) + } + if (body.Rating !== undefined) row.rating = Number(body.Rating) || 0 + row.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return c.json(userDataOf({ ...(await loadLibrary(c.env, user)) } as EmbyLibrary, found.item.id, found.ep)) +}) + +embyRouter.get("/Items/:id/UserData", async (c) => { + const found = await readUserData(c, c.req.param("id")) + if (!found) return c.json({ Error: "Not Found" }, 404) + return c.json(found.data) +}) + +// 播放会话:真正落进度 +async function applySessionProgress(c: any, action: "start" | "progress" | "stop") { + const user = (c as any).get("mediaUser") + const body = await c.req.json().catch(() => ({})) + const idRaw = String(body.ItemId || c.req.query("ItemId") || "") + if (!idRaw) return c.json({}) + const found = await readUserData(c, idRaw) + if (!found) return c.json({}) + const db = await getMediaDb(c.env) + const row = ensureUserData(db, user.id, found.item.id, found.ep, found.season) + const ticksIn = Number(body.PositionTicks) || 0 + row.position_ticks = Math.max(0, ticksIn) + if (ticksIn > 0) { + row.duration_ticks = Math.max(row.duration_ticks, ticks(targetOf(found.item).duration) || 0) + } + row.last_played_at = new Date().toISOString() + row.updated_at = row.last_played_at + if (action === "start") { + if (row.played) { + row.played = false + row.position_ticks = ticksIn || 0 + } + } + if (action === "stop") { + const total = row.duration_ticks || ticks(found.item.duration) || 0 + if (total > 0 && row.position_ticks / total >= PLAYED_RATIO) { + if (!row.played) { + row.played = true + row.play_count += 1 + } + row.position_ticks = total + } + } + await saveMediaDb(c.env, db) + return c.json({}) +} + +const PLAYED_RATIO = 0.9 +const targetOf = (item: MediaItem) => ({ duration: item.duration }) + +embyRouter.post("/Sessions/Playing", (c) => applySessionProgress(c, "start")) +embyRouter.post("/Sessions/Playing/Progress", (c) => applySessionProgress(c, "progress")) +embyRouter.post("/Sessions/Playing/Stopped", (c) => applySessionProgress(c, "stop")) +embyRouter.post("/Sessions/Logout", async (c) => { + const token = requestToken(c) + if (token) { + const db = await getMediaDb(c.env) + const hash = await mediaHelpers.sha256Hex(token) + db.emby_devices = db.emby_devices.filter((d) => d.token !== hash) + await saveMediaDb(c.env, db) + } + return c.body(null, 204) +}) + +embyRouter.get("/DisplayPreferences/:id", (c) => + c.json({ + Id: c.req.param("id") || "usersettings", + ViewType: "", + SortBy: "SortName", + IndexBy: "", + RememberIndexing: false, + PrimaryImageHeight: 250, + PrimaryImageWidth: 250, + CustomPrefs: { homeSections: "" }, + ScrollDirection: "Horizontal", + ShowBackdrop: true, + RememberSorting: false, + SortOrder: "Ascending", + ShowSidebar: false, + Client: "emby", + }), +) +embyRouter.post("/DisplayPreferences/:id", (c) => c.body(null, 204)) + +embyRouter.get("/Library/SelectableMediaFolders", async (c) => { + const lib = await loadLibrary(c.env, (c as any).get("mediaUser")) + return c.json(viewDtos(c, lib).map((v) => ({ ...v, SubFolders: [] }))) +}) +embyRouter.get("/Library/VirtualFolders/Query", async (c) => { + const lib = await loadLibrary(c.env, (c as any).get("mediaUser")) + return c.json({ Items: viewDtos(c, lib), TotalRecordCount: viewDtos(c, lib).length }) +}) +embyRouter.post("/Library/Refresh", (c) => c.json({})) + +embyRouter.get("/Search/Hints", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const term = (c.req.query("SearchTerm") || "").toLowerCase() + const list = filterDtos(allDtos(c, lib, []), c, lib) + .filter((d) => !term || String(d.Name || "").toLowerCase().includes(term)) + .slice(0, 50) + .map((d) => ({ + Id: d.Id, + Name: d.Name, + Type: d.Type, + MediaType: d.MediaType, + IsFolder: d.IsFolder, + ProductionYear: d.ProductionYear, + })) + return c.json({ SearchHints: list, TotalRecordCount: list.length }) +}) + +// 播放流 +async function stream(c: any, idRaw: string) { + const user = (c as any).get("mediaUser") + const target = await playTarget(c, idRaw) + if (!target) return c.body(null, 404) + if (!target.item || !(await mediaHelpers.canAccessItem(c.env, user, target.item))) { + return c.body(null, 404) + } + const policy = await getSignPolicy(c) + const sign = await signDownloadPath(c, target.rawPath, policy.expiresIn) + return c.redirect( + `${originOf(c)}/d${encodeDownloadPath(target.rawPath)}?sign=${encodeURIComponent(sign)}`, + ) +} +embyRouter.get("/Videos/:id/stream", (c) => stream(c, c.req.param("id"))) +embyRouter.get("/Audio/:id/stream", (c) => stream(c, c.req.param("id"))) +embyRouter.get("/Audio/:id/universal", (c) => stream(c, c.req.param("id"))) +embyRouter.get("/Videos/:id/stream.:ext", (c) => stream(c, c.req.param("id"))) + +// 字幕(歌词) +embyRouter.get("/Items/:id/Subtitles/:index/Stream.js", async (c) => { + const user = (c as any).get("mediaUser") + const lib = await loadLibrary(c.env, user) + const id = origId(lib, c.req.param("id")) + const numeric = id.match(/^so-(\d+)$/) + const item = numeric ? findItem(lib.db, Number(numeric[1])) : undefined + if (!item?.lyrics) return c.body(null, 404) + const lrc = item.lyrics + // 简单 LRC → SRT + const lines: string[] = [] + const re = /\[(\d{2}):(\d{2})(?:\.(\d{1,3}))?\]\s*(.*)/ + let idx = 1 + for (const raw of lrc.split("\n")) { + const m = raw.match(re) + if (!m) continue + const start = + Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(`0.${m[3] || 0}`) + lines.push( + `${idx++}\n${srtTime(start)} --> ${srtTime(start + 3)}\n${m[4]}`, + ) + } + c.header("Content-Type", "text/plain; charset=utf-8") + return c.body(lines.length ? lines.join("\n\n") : lrc) +}) + +const srtTime = (seconds: number) => { + const s = Math.max(0, seconds) + const hh = String(Math.floor(s / 3600)).padStart(2, "0") + const mm = String(Math.floor((s % 3600) / 60)).padStart(2, "0") + const ss = String(Math.floor(s % 60)).padStart(2, "0") + const ms = String(Math.floor((s % 1) * 1000)).padStart(3, "0") + return `${hh}:${mm}:${ss},${ms}` +} + +const embyWebNotice = () => `<!doctype html> +<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> +<title>OpenList - Emby API + +

OpenList 仅提供 Emby 兼容 API

+

当前地址没有 Emby Web 客户端界面,请使用支持 Emby 协议的客户端添加服务器。

+
+ + + +
服务器地址http://你的地址/@emby
用户名OpenList 用户名
密码OpenList 密码或媒体应用密钥
+` + +// ==================== Subsonic ==================== + +export const subsonicRouter = new Hono() + +type SubPayload = Record + +const xmlEscape = (value: string) => + String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + +/** 通用 XML 序列化:标量 → 属性,对象/数组 → 子元素,#text → chardata */ +function toXmlNode(name: string, value: any): string { + if (value === null || value === undefined) return "" + if (Array.isArray(value)) { + return value.map((v) => toXmlNode(name, v)).join("") + } + if (typeof value !== "object") { + if (typeof value === "boolean") value = value ? "true" : "false" + return `<${name}>${xmlEscape(String(value))}` + } + const attrs: string[] = [] + let text = "" + const children: string[] = [] + for (const [k, v] of Object.entries(value)) { + if (v === null || v === undefined) continue + if (k === "#text") { + text = xmlEscape(String(v)) + continue + } + if (Array.isArray(v) || typeof v === "object") { + const inner = toXmlNode(k, v) + if (inner) children.push(inner) + } else { + const raw = typeof v === "boolean" ? (v ? "true" : "false") : String(v) + attrs.push(`${k}="${xmlEscape(raw)}"`) + } + } + const a = attrs.length ? ` ${attrs.join(" ")}` : "" + const inner = children.join("") + if (!inner && !text) return `<${name}${a}/>` + return `<${name}${a}>${text}${inner}` +} + +function subRespond(c: any, payload: SubPayload = {}, status: "ok" | "failed" = "ok", error?: any) { + const body: any = { + status, + version: SUBSONIC_VERSION, + type: "OpenList", + serverVersion: EMBY_VERSION, + openSubsonic: true, + ...payload, + } + if (error) body.error = error + const format = String( + c.req.query("f") || c.req.query("format") || "xml", + ).toLowerCase() + if (format === "json") return c.json({ "subsonic-response": body }) + if (format === "jsonp") { + const cb = String(c.req.query("callback") || "") + if (!/^[A-Za-z_$][A-Za-z0-9_$.]*$/.test(cb)) { + return c.json( + { + "subsonic-response": { + status: "failed", + version: SUBSONIC_VERSION, + type: "OpenList", + error: { code: 0, message: "invalid callback parameter" }, + }, + }, + 200, + ) + } + c.header("Content-Type", "application/javascript; charset=utf-8") + return c.body(`${cb}(${JSON.stringify({ "subsonic-response": body })})`) + } + c.header("Content-Type", "text/xml; charset=utf-8") + return c.body(`\n${toXmlNode("subsonic-response", body)}`) +} + +const subError = (c: any, code: number, message: string) => + subRespond(c, {}, "failed", { code, message }) + +/** Subsonic 参数:query + POST 表单 */ +async function subParams(c: any): Promise> { + const out: Record = {} + for (const [k, v] of Object.entries(c.req.query())) out[k] = String(v) + const method = c.req.method.toUpperCase() + if (method === "POST") { + try { + const ct = String(c.req.header("content-type") || "").toLowerCase() + if (ct.includes("json")) { + const body = (await c.req.json()) as Record + for (const [k, v] of Object.entries(body || {})) out[k] = String(v) + } else if (ct.includes("form")) { + const form = await c.req.formData() + form.forEach((v: any, k: string) => { + out[k] = String(v) + }) + } + } catch { + /* ignore */ + } + } + return out +} + +const param = (p: Record, key: string, def = "") => + p[key] ?? p[key.toLowerCase()] ?? def +const intParam = (p: Record, key: string, def = 0) => { + const n = parseInt(param(p, key), 10) + return Number.isFinite(n) ? n : def +} + +/** 用应用密钥明文校验 Subsonic token(md5(password + salt)) */ +async function appKeyPlainFor(c: any, row: any): Promise { + return await mediaHelpers.appKeyPlainOf(c, row) +} + +async function subAuth(c: any, p: Record) { + const db = await getMediaDb(c.env) + // 1) apiKey 直接认证(OpenSubsonic apiKeyAuthentication) + const apiKey = param(p, "apiKey") + if (apiKey) { + const row = await findAppKeyByPlain(c.env, apiKey, db) + if (row) { + const user = await userFromDb(c.env, row.user_id) + if (user && !user.disabled && user.username !== "guest") { + row.last_used_at = new Date().toISOString() + await mediaHelpers.authSucceeded(c, user.username) + return user + } + } + return null + } + + const username = param(p, "u") + if (!username) return null + const user = await userFromDb(c.env, undefined, username) + if (!user || user.disabled || user.username === "guest") return null + + const salt = param(p, "s") + const token = param(p, "t") + if (salt && token) { + for (const row of db.app_keys.filter((k) => k.user_id === user.id)) { + const plain = await appKeyPlainFor(c, row) + if (!plain) continue + const expect = md5Hex(`${plain}${salt}`) + if (expect.toLowerCase() === token.toLowerCase()) { + row.last_used_at = new Date().toISOString() + await mediaHelpers.authSucceeded(c, user.username) + return user + } + } + return null + } + + // u+p:默认关闭(对齐 Go 的 media_subsonic_allow_password_auth), + // 客户端应使用 apiKey 或 u+t+s(应用密钥)认证。 + const settings = await getSettingsMap(c.env) + if (!settingBool(settings, "media_subsonic_allow_password_auth", false)) { + return null + } + let pw = param(p, "p") + if (pw.startsWith("enc:")) { + try { + const hex = pw.slice(4) + const bytes = Uint8Array.from( + (hex.match(/.{2}/g) || []).map((h) => parseInt(h, 16)), + ) + pw = new TextDecoder().decode(bytes) + } catch { + return null + } + } + if (!pw || !user.password) return null + const hash = await twoStepHash(pw, user.salt) + if (hash !== user.password) return null + await mediaHelpers.authSucceeded(c, user.username) + return user +} + +/** + * 请求是否真的带了认证凭据(对齐 Go 的 subHasCredential)。 + * 只有 u 没有 t/p 的裸探测不算失败:部分客户端启动时会先发一次空探测, + * 计入失败会把它自己的 IP 锁死。 + */ +const hasCredential = (p: Record) => { + if (param(p, "apiKey")) return true + if (!param(p, "u")) return false + return !!(param(p, "t") || param(p, "p")) +} + +const subSongDto = (item: MediaItem) => ({ + id: `so-${item.id}`, + parent: `al-${albumKeyOf(item)}`, + isDir: false, + title: item.scraped_name || item.file_name.replace(/\.[^.]+$/, ""), + album: albumOf(item), + artist: artistOf(item), + track: item.track_number || 0, + year: yearOf(item.release_date) || undefined, + genre: item.genre || undefined, + coverArt: `so-${item.id}`, + size: item.file_size, + contentType: item.mime_type || `audio/${extOf(item.file_name) || "mpeg"}`, + suffix: extOf(item.file_name) || "mp3", + duration: item.duration || 0, + created: item.created_at, + type: "music", + isVideo: false, + path: `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}`, +}) + +const subAlbumDto = (al: EmbyAlbum) => { + const duration = al.items.reduce((n, s) => n + (s.duration || 0), 0) + return { + id: `al-${al.key}`, + name: al.name, + artist: al.artist, + artistId: `ar-${artistKeyOf(al.items[0])}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration, + created: al.items[0]?.created_at || new Date().toISOString(), + year: al.year || undefined, + song: al.items + .slice() + .sort((a, b) => a.track_number - b.track_number) + .map((s) => subSongDto(s)), + } +} + +const subArtistDto = (artist: string, items: MediaItem[], albums: EmbyAlbum[]) => ({ + id: `ar-${hashKey(artist.toLowerCase())}`, + name: artist, + albumCount: albums.length, + album: albums.map((al) => ({ + id: `al-${al.key}`, + name: al.name, + artist, + artistId: `ar-${hashKey(artist.toLowerCase())}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration: al.items.reduce((n, s) => n + (s.duration || 0), 0), + created: al.items[0]?.created_at || new Date().toISOString(), + })), +}) + +const handleSubsonic = async (c: any, methodRaw: string) => { + const p = await subParams(c) + const method = String(methodRaw || param(p, "method") || "") + .toLowerCase() + .replace(/\.view$/, "") + if (method === "ping") return subRespond(c, {}) + if (method === "getlicense") return subRespond(c, { license: { valid: true } }) + if (method === "getopensubsonicextensions") { + return subRespond(c, { + openSubsonicExtensions: [ + { name: "apiKeyAuthentication", versions: [1] }, + { name: "formPost", versions: [1] }, + { name: "songLyrics", versions: [1] }, + ], + }) + } + + if (await mediaHelpers.authLockedFor(c, param(p, "u"))) { + return subError(c, 40, "Too many failed attempts, please try again later") + } + const user = await subAuth(c, p) + if (!user) { + if (hasCredential(p)) await mediaHelpers.authFailed(c, param(p, "u")) + return subError(c, 40, "Wrong username or password") + } + + const db = await getMediaDb(c.env) + const lib = await loadLibrary(c.env, user) + const music = lib.music + + const songList = () => + music + .slice() + .sort((a, b) => a.file_name.localeCompare(b.file_name, "zh-CN")) + + switch (method) { + case "tokeninfo": + case "getuser": + return subRespond(c, { + user: { + username: user.username, + scrobblingEnabled: true, + adminRole: isAdmin(user), + downloadRole: true, + playlistRole: true, + streamRole: true, + coverArtRole: true, + }, + }) + case "getusers": + if (!isAdmin(user)) return subError(c, 50, "not authorized") + { + const udb = await getDb(c.env) + return subRespond(c, { + users: (udb.users || []) + .filter((u: any) => !u.disabled && u.username !== "guest") + .map((u: any) => ({ + username: u.username, + scrobblingEnabled: true, + adminRole: isAdmin(u), + downloadRole: true, + playlistRole: true, + streamRole: true, + coverArtRole: true, + })), + }) + } + case "getmusicfolders": + return subRespond(c, { musicFolders: [{ id: 1, name: "Music" }] }) + case "getindexes": { + const map = new Map() + for (const item of music) { + const artist = artistOf(item) + const letter = artist.slice(0, 1).toUpperCase() || "#" + const entry = map.get(letter) || { name: letter, artist: [] } + if (!entry.artist.some((a: any) => a.name === artist)) { + entry.artist.push({ + id: `ar-${artistKeyOf(item)}`, + name: artist, + albumCount: lib.albums.filter((al) => al.artist === artist).length, + }) + } + map.set(letter, entry) + } + return subRespond(c, { + indexes: { + lastModified: Date.now(), + ignoredArticles: "The El La Los Las Le Les", + index: Array.from(map.values()).sort((a, b) => + a.name.localeCompare(b.name), + ), + }, + }) + } + case "getartists": { + const map = new Map() + for (const al of lib.albums) { + const key = artistKeyOf(al.items[0]) + const entry = map.get(key) || { + id: `ar-${key}`, + name: al.artist, + albumCount: 0, + album: [] as any[], + } + entry.albumCount += 1 + entry.album.push({ + id: `al-${al.key}`, + name: al.name, + artist: al.artist, + artistId: `ar-${key}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration: al.items.reduce((n, s) => n + (s.duration || 0), 0), + created: al.items[0]?.created_at || new Date().toISOString(), + }) + map.set(key, entry) + } + return subRespond(c, { + artists: { + ignoredArticles: "", + index: [{ name: "#", artist: Array.from(map.values()) }], + }, + }) + } + case "getartist": { + const id = param(p, "id") + const key = id.replace(/^ar-/, "") + const al = lib.albums.find((a) => artistKeyOf(a.items[0]) === key) + if (!al) return subError(c, 70, "Artist not found") + const albums = lib.albums.filter((a) => artistKeyOf(a.items[0]) === key) + return subRespond(c, { + artist: subArtistDto(al.artist, al.items, albums), + }) + } + case "getalbum": { + const key = param(p, "id").replace(/^al-/, "") + const al = lib.albums.find((a) => a.key === key) + if (!al) return subError(c, 70, "Album not found") + return subRespond(c, { album: subAlbumDto(al) }) + } + case "getsong": { + const id = param(p, "id").replace(/^so-/, "") + const item = music.find((m) => String(m.id) === id) + if (!item) return subError(c, 70, "Song not found") + return subRespond(c, { song: subSongDto(item) }) + } + case "getmusicdirectory": { + const id = param(p, "id") + if (id.startsWith("ar-") || id === "1") { + const key = id.replace(/^ar-/, "") + const albums = lib.albums.filter( + (a) => !key || artistKeyOf(a.items[0]) === key, + ) + return subRespond(c, { + directory: { + id, + name: key ? albums[0]?.artist || "Artist" : "Music", + child: albums.map((al) => ({ + id: `al-${al.key}`, + parent: id, + isDir: true, + title: al.name, + album: al.name, + artist: al.artist, + coverArt: `al-${al.key}`, + created: al.items[0]?.created_at || new Date().toISOString(), + })), + }, + }) + } + if (id.startsWith("al-")) { + const key = id.slice(3) + const al = lib.albums.find((a) => a.key === key) + if (!al) return subError(c, 70, "Album not found") + return subRespond(c, { + directory: { + id, + parent: `ar-${artistKeyOf(al.items[0])}`, + name: al.name, + child: al.items.map((s) => ({ + ...subSongDto(s), + parent: id, + isDir: false, + })), + }, + }) + } + return subError(c, 70, "Directory not found") + } + case "getalbumlist": + case "getalbumlist2": { + let albums = lib.albums.slice() + const type = param(p, "type", "alphabeticalByName") + if (type === "random") { + albums.sort(() => Math.random() - 0.5) + } else if (type === "newest") { + albums.sort((a, b) => + String(b.items[0]?.created_at).localeCompare(String(a.items[0]?.created_at)), + ) + } else if (type === "recent") { + albums.sort((a, b) => + String(b.items[0]?.updated_at).localeCompare(String(a.items[0]?.updated_at)), + ) + } else { + albums.sort((a, b) => a.name.localeCompare(b.name, "zh-CN")) + } + const size = Math.min(intParam(p, "size", 50) || 50, 500) + const offset = Math.max(0, intParam(p, "offset", 0)) + const list = albums.slice(offset, offset + size).map((al) => { + if (method === "getalbumlist") { + return { + id: `al-${al.key}`, + parent: `ar-${artistKeyOf(al.items[0])}`, + isDir: true, + title: al.name, + album: al.name, + artist: al.artist, + coverArt: `al-${al.key}`, + created: al.items[0]?.created_at || new Date().toISOString(), + } + } + return { + id: `al-${al.key}`, + name: al.name, + artist: al.artist, + artistId: `ar-${artistKeyOf(al.items[0])}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration: al.items.reduce((n, s) => n + (s.duration || 0), 0), + created: al.items[0]?.created_at || new Date().toISOString(), + } + }) + return subRespond( + c, + method === "getalbumlist" + ? { albumList: { album: list } } + : { albumList2: { album: list } }, + ) + } + case "getrandomsongs": { + const size = Math.min(intParam(p, "size", 10) || 10, 500) + const list = songList().sort(() => Math.random() - 0.5).slice(0, size) + return subRespond(c, { randomSongs: { song: list.map(subSongDto) } }) + } + case "getsongsbygenre": { + const genre = param(p, "genre").toLowerCase() + const list = music.filter((m) => (m.genre || "").toLowerCase().includes(genre)) + return subRespond(c, { songsByGenre: { song: list.map(subSongDto) } }) + } + case "search2": + case "search3": { + const q = param(p, "query").toLowerCase() + const match = (value: string) => !q || String(value || "").toLowerCase().includes(q) + const artists = new Map() + for (const item of music) { + if (!match(artistOf(item))) continue + artists.set(artistKeyOf(item), { + id: `ar-${artistKeyOf(item)}`, + name: artistOf(item), + albumCount: lib.albums.filter((a) => artistKeyOf(a.items[0]) === artistKeyOf(item)).length, + }) + } + const albums = lib.albums + .filter((al) => match(al.name) || match(al.artist)) + .map((al) => ({ + id: `al-${al.key}`, + name: al.name, + artist: al.artist, + artistId: `ar-${artistKeyOf(al.items[0])}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration: al.items.reduce((n, s) => n + (s.duration || 0), 0), + })) + const songs = music + .filter((s) => match(s.file_name) || match(s.scraped_name) || match(albumOf(s)) || match(artistOf(s))) + .slice(0, 100) + .map(subSongDto) + const key = method === "search2" ? "searchResult2" : "searchResult3" + return subRespond(c, { + [key]: { + artist: Array.from(artists.values()), + album: albums, + song: songs, + }, + }) + } + case "getstarred": + case "getstarred2": { + const fav = db.user_data.filter((d) => d.user_id === user.id && d.is_favorite) + const ids = new Set(fav.map((d) => d.item_id)) + const songs = music.filter((m) => ids.has(m.id)) + const albums = lib.albums.filter((al) => al.items.some((i) => ids.has(i.id))) + const artists = Array.from( + new Set(songs.map((s) => artistOf(s))), + ).map((name) => ({ + id: `ar-${hashKey(name.toLowerCase())}`, + name, + albumCount: albums.filter((a) => a.artist === name).length, + })) + const payload = { + artist: artists, + album: albums.map((al) => ({ + id: `al-${al.key}`, + name: al.name, + artist: al.artist, + artistId: `ar-${artistKeyOf(al.items[0])}`, + coverArt: `al-${al.key}`, + songCount: al.items.length, + duration: al.items.reduce((n, s) => n + (s.duration || 0), 0), + })), + song: songs.map(subSongDto), + } + return subRespond( + c, + method === "getstarred" ? { starred: payload } : { starred2: payload }, + ) + } + case "star": + case "unstar": { + const id = param(p, "id") + const ids: string[] = [id] + const albumId = param(p, "albumId") + const artistId = param(p, "artistId") + if (albumId) { + const al = lib.albums.find((a) => `al-${a.key}` === albumId) + if (al) ids.push(...al.items.map((i) => `so-${i.id}`)) + } + if (artistId) { + const key = artistId.replace(/^ar-/, "") + for (const al of lib.albums.filter((a) => artistKeyOf(a.items[0]) === key)) { + ids.push(...al.items.map((i) => `so-${i.id}`)) + } + } + for (const raw of ids) { + const itemId = Number(String(raw).replace(/^so-/, "")) + if (!itemId) continue + const row = ensureUserData(db, user.id, itemId) + row.is_favorite = method === "star" + row.updated_at = new Date().toISOString() + } + await saveMediaDb(c.env, db) + return subRespond(c, {}) + } + case "setrating": { + const id = Number(param(p, "id").replace(/^so-/, "")) + const rating = Math.max(0, Math.min(5, intParam(p, "rating", 0))) + if (id) { + const row = ensureUserData(db, user.id, id) + row.rating = rating + row.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + } + return subRespond(c, {}) + } + case "scrobble": { + const id = Number(param(p, "id").replace(/^so-/, "")) + if (id) { + const row = ensureUserData(db, user.id, id) + const submission = param(p, "submission", "true") !== "false" + if (submission) { + row.play_count += 1 + row.played = true + } + row.last_played_at = new Date().toISOString() + row.updated_at = row.last_played_at + await saveMediaDb(c.env, db) + } + return subRespond(c, {}) + } + case "getnowplaying": + return subRespond(c, { nowPlaying: { entry: [] } }) + case "getplaylists": + return subRespond(c, { + playlists: listPlaylists(db, user.id).map((pl) => ({ + id: String(pl.id), + name: pl.name, + comment: pl.comment || undefined, + owner: user.username, + public: pl.public, + songCount: pl.item_count, + duration: playlistItems(db, pl.id).reduce((n, it) => { + const item = findItem(db, it.item_id) + return n + (item?.duration || 0) + }, 0), + created: pl.created_at, + })), + }) + case "getplaylist": { + const id = Number(param(p, "id")) + const pl = getPlaylist(db, id) + if (!pl) return subError(c, 70, "Playlist not found") + if (pl.user_id !== user.id && !pl.public && !isAdmin(user)) { + return subError(c, 50, "not authorized") + } + return subRespond(c, { + playlist: { + id: String(pl.id), + name: pl.name, + comment: pl.comment || undefined, + owner: user.username, + public: pl.public, + songCount: pl.item_count, + created: pl.created_at, + entry: playlistItems(db, pl.id) + .map((it) => findItem(db, it.item_id)) + .filter((i): i is MediaItem => !!i) + .map(subSongDto), + }, + }) + } + case "createplaylist": { + let ids = (param(p, "songId") || "") + .split(",") + .map((s) => Number(s.replace(/^so-/, ""))) + .filter(Boolean) + const playlistId = intParam(p, "playlistId", 0) + if (playlistId) { + const pl = getPlaylist(db, playlistId) + if (!pl) return subError(c, 70, "Playlist not found") + if (ids.length) replacePlaylistItems(db, playlistId, ids) + await saveMediaDb(c.env, db) + return subRespond(c, {}) + } + const name = param(p, "name") || "Playlist" + const pl = createPlaylist( + db, + user.id, + name, + ids, + param(p, "comment"), + param(p, "public") === "true", + ) + await saveMediaDb(c.env, db) + return subRespond(c, { + playlist: { id: String(pl.id), name: pl.name, songCount: pl.item_count }, + }) + } + case "updateplaylist": { + const id = Number(param(p, "playlistId")) + const pl = getPlaylist(db, id) + if (!pl) return subError(c, 70, "Playlist not found") + if (pl.user_id !== user.id && !isAdmin(user)) return subError(c, 50, "not authorized") + const add = (param(p, "songIdToAdd") || "") + .split(",") + .map((s) => Number(s.replace(/^so-/, ""))) + .filter(Boolean) + const removeIdx = (param(p, "songIndexToRemove") || "") + .split(",") + .map((s) => Number(s)) + .filter((n) => Number.isFinite(n)) + const items = playlistItems(db, id).map((it) => it.item_id) + for (const idx of removeIdx.sort((a, b) => b - a)) items.splice(idx, 1) + items.push(...add) + updatePlaylist(db, pl, { + name: param(p, "name") || undefined, + comment: param(p, "comment") || undefined, + public: + param(p, "public") === "" ? undefined : param(p, "public") === "true", + itemIds: items, + }) + await saveMediaDb(c.env, db) + return subRespond(c, {}) + } + case "deleteplaylist": { + const id = Number(param(p, "id")) + const pl = getPlaylist(db, id) + if (!pl) return subError(c, 70, "Playlist not found") + if (pl.user_id !== user.id && !isAdmin(user)) return subError(c, 50, "not authorized") + deletePlaylist(db, id) + await saveMediaDb(c.env, db) + return subRespond(c, {}) + } + case "saveplayqueue": { + const ids = (param(p, "id") || "") + .split(",") + .map((s) => Number(s.replace(/^so-/, ""))) + .filter(Boolean) + savePlayQueue(db, user.id, ids, param(p, "current"), intParam(p, "position", 0)) + await saveMediaDb(c.env, db) + return subRespond(c, {}) + } + case "getplayqueue": { + const queue = getPlayQueue(db, user.id) + return subRespond(c, { + playQueue: queue + ? { + current: queue.current || undefined, + position: queue.position || undefined, + username: user.username, + changed: queue.changed, + entry: queue.entry_ids + .map((id) => findItem(db, id)) + .filter((i): i is MediaItem => !!i) + .map(subSongDto), + } + : undefined, + }) + } + case "getlyrics": { + const artist = param(p, "artist") + const title = param(p, "title") + const item = music.find( + (m) => + (!artist || artistOf(m) === artist) && + (!title || (m.scraped_name || m.file_name).includes(title)), + ) + return subRespond(c, { + lyrics: item?.lyrics + ? { artist: artistOf(item), title: item.scraped_name || item.file_name, value: item.lyrics } + : undefined, + }) + } + case "getlyricsbysongid": { + const id = Number(param(p, "id").replace(/^so-/, "")) + const item = findItem(db, id) + return subRespond(c, { + lyricsList: item?.lyrics + ? { + structuredLyrics: { + displayArtist: artistOf(item), + displayTitle: item.scraped_name || item.file_name, + lang: "und", + synced: item.lyrics.includes("["), + line: [], + "#text": item.lyrics, + }, + } + : undefined, + }) + } + case "getgenres": { + const map = new Map() + for (const item of music) { + for (const g of String(item.genre || "").split(",").map((s) => s.trim()).filter(Boolean)) { + const row = map.get(g) || { songCount: 0, albumCount: 0 } + row.songCount += 1 + map.set(g, row) + } + } + for (const al of lib.albums) { + for (const item of al.items) { + for (const g of String(item.genre || "").split(",").map((s) => s.trim()).filter(Boolean)) { + const row = map.get(g) + if (row) row.albumCount += 1 + } + } + } + return subRespond(c, { + genres: Array.from(map.entries()).map(([value, row]) => ({ + value, + songCount: row.songCount, + albumCount: row.albumCount, + })), + }) + } + case "getscanstatus": + return subRespond(c, { + scanStatus: { scanning: false, count: music.length }, + }) + case "startscan": + if (!isAdmin(user)) return subError(c, 50, "not authorized") + { + const { scanMedia } = await import("../internal/media/scan") + void scanMedia(c.env, "music").catch(() => undefined) + } + return subRespond(c, {}) + case "getartistinfo2": + return subRespond(c, { artistInfo2: {} }) + case "getalbuminfo": + case "getalbuminfo2": + return subRespond(c, { albumInfo: {} }) + case "gettopsongs": + case "getsimilarsongs2": + return subRespond(c, { topSongs: { song: [] } }) + case "getbookmarks": + return subRespond(c, { bookmarks: {} }) + case "getavatars": + return c.body(null, 404) + case "stream": + case "download": { + const id = param(p, "id") + let item: MediaItem | undefined + if (id.startsWith("al-")) { + const key = id.slice(3) + const al = lib.albums.find((a) => a.key === key) + item = al?.items[0] + } else { + item = findItem(db, Number(id.replace(/^so-/, ""))) + } + if (!item || !(await mediaHelpers.canAccessItem(c.env, user, item))) { + return c.body(null, 404) + } + const settings = await getSettingsMap(c.env) + const path = mediaHelpers.itemPath(item) + const policy = await getSignPolicy(c) + const sign = await signDownloadPath(c, path, policy.expiresIn) + // media_subsonic_proxy_stream:不跟随 302 的客户端直接走 /p + const useProxy = settingBool(settings, "media_subsonic_proxy_stream", false) + const prefix = useProxy ? "/p" : "/d" + return c.redirect( + `${originOf(c)}${prefix}${encodeDownloadPath(path)}?sign=${encodeURIComponent(sign)}`, + ) + } + case "getcoverart": { + // 与 Emby 图片端点同一套校验:db 是全库快照,直接 findItem 会漏掉 + // hidden / base_path / meta / 类型开关的判断。 + const cover = await coverOfId(c.env, user, lib, param(p, "id")) + if (!cover) return c.body(null, 404) + return serveCover(c, cover) + } + default: + return subError(c, 0, `unknown method: ${method}`) + } +} + +subsonicRouter.use("*", async (c, next) => { + const settings = await getSettingsMap(c.env) + if (!mediaProtocolEnabled(settings, "media_subsonic_enabled")) { + return c.notFound() + } + await next() +}) + +// 兼容 /@rest/ping 与 /@rest/rest/ping 两种接入方式 +subsonicRouter.all("/rest/:method", async (c) => + handleSubsonic(c, c.req.param("method")), +) +subsonicRouter.all("/:method", async (c) => handleSubsonic(c, c.req.param("method"))) + +/** 单测入口(不参与运行时逻辑) */ +export const __embyTest = { + toXmlNode, + numericId, + episodesOf, + seasonListOf, + parseEpisodesOf, + seriesKeyOf, + albumKeyOf, + artistKeyOf, +} diff --git a/src/backend/server/emby_protocol.test.ts b/src/backend/server/emby_protocol.test.ts new file mode 100644 index 00000000..ff205046 --- /dev/null +++ b/src/backend/server/emby_protocol.test.ts @@ -0,0 +1,335 @@ +import assert from "node:assert/strict" +import test, { after, before } from "node:test" + +import { Hono } from "hono" + +import { getDb, saveDb } from "../internal/model/db" +import { DEFAULT_CONFIG, MEDIA_TYPES, emptyMediaDb, type MediaItem } from "../internal/media/model" +import { invalidateMediaCache, saveMediaDb } from "../internal/media/store" +import { memoryDriver } from "../internal/model/store/driver/memory" +import { __embyTest, embyRouter, subsonicRouter } from "./emby" + +function buildApp() { + const app = new Hono() + app.route("/@emby", embyRouter) + app.route("/@rest", subsonicRouter) + return app +} + +const original = new Map() +let originalGuestBasePath: string | null = null +let originalMetas: any[] | null = null + +async function setSetting(key: string, value: string) { + const db = await getDb() + const row = (db.settings || []).find((s: any) => s.key === key) + if (!row) return + if (!original.has(key)) original.set(key, String(row.value ?? "")) + row.value = value + await saveDb(db, undefined, { force: true } as any) +} + +before(async () => { + await setSetting("media_enabled", "true") + await setSetting("media_emby_enabled", "true") + await setSetting("media_subsonic_enabled", "true") +}) + +after(async () => { + const db = await getDb() + for (const [key, value] of original) { + const row = (db.settings || []).find((s: any) => s.key === key) + if (row) row.value = value + } + const guest = (db.users || []).find((u: any) => u.username === "guest") + if (guest && originalGuestBasePath !== null) guest.base_path = originalGuestBasePath + if (originalMetas !== null) db.metas = originalMetas + await saveDb(db, undefined, { force: true } as any) +}) + +/** 媒体库条目(补齐测试用默认值) */ +function mediaItem( + over: Partial & + Pick, +): MediaItem { + return { + scan_path_id: 1, + scraped_name: "", + cover: "", + authors: "", + plot: "", + description: "", + genre: "", + release_date: "", + rating: 0, + file_size: 0, + duration: 0, + season: 0, + episode: 0, + episodes: "", + album_name: "", + album_artist: "", + track_number: 0, + publisher: "", + isbn: "", + external_id: "", + hidden: false, + scraper_source: "", + created_at: "2026-01-01T00:00:00.000Z", + updated_at: "2026-01-01T00:00:00.000Z", + ...over, + } +} + +/** 重置内存持久层的媒体分片,保证每个用例互不影响 */ +async function resetMediaStore() { + for (const key of await memoryDriver.list("openlist_media_")) { + await memoryDriver.delete(key) + } + invalidateMediaCache() +} + +/** + * 造一个「访客根目录为 /public」的环境:条目在 /private 下或 hidden 时 + * 任何入口都不应再返回封面。 + */ +async function seedMediaEnv(items: MediaItem[], metas: any[] = [], enabled = true) { + const userDb = await getDb() + const guest = (userDb.users || []).find((u: any) => u.username === "guest") + if (guest) { + if (originalGuestBasePath === null) { + originalGuestBasePath = String(guest.base_path || "/") + } + guest.base_path = "/public" + } + if (originalMetas === null) originalMetas = JSON.parse(JSON.stringify(userDb.metas || [])) + userDb.metas = metas + await saveDb(userDb, undefined, { force: true } as any) + + await resetMediaStore() + const media = emptyMediaDb() + media.configs = MEDIA_TYPES.map((t) => ({ + ...DEFAULT_CONFIG(t), + enabled, + })) + media.items = items + media.next_id = 1000 + await saveMediaDb({}, media) + invalidateMediaCache() +} + + +test("Emby public system info is reachable without token", async () => { + const res = await buildApp().request("/@emby/System/Info/Public", {}, {}) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.ok(body.Version) + assert.ok(body.Id) + assert.equal(body.StartupWizardCompleted, true) +}) + +test("Emby ping", async () => { + const res = await buildApp().request("/@emby/System/Ping", {}, {}) + assert.equal(res.status, 200) + assert.equal(await res.text(), "Emby Server") +}) + +test("Emby web notice is not blocked by auth", async () => { + const res = await buildApp().request("/@emby/web/index.html", {}, {}) + assert.equal(res.status, 200) + const html = await res.text() + assert.match(html, /Emby/) +}) + +test("Emby protected endpoint requires token", async () => { + const res = await buildApp().request("/@emby/Users/Me", {}, {}) + assert.equal(res.status, 401) +}) + +test("Subsonic extensions are reachable without auth", async () => { + const res = await buildApp().request( + "/@rest/getOpenSubsonicExtensions?f=json", + {}, + {}, + ) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.equal(body["subsonic-response"].status, "ok") + assert.ok(Array.isArray(body["subsonic-response"].openSubsonicExtensions)) +}) + +test("Subsonic ping returns XML by default", async () => { + const res = await buildApp().request("/@rest/ping", {}, {}) + assert.equal(res.status, 200) + const xml = await res.text() + assert.match(xml, / { + const res = await buildApp().request("/@rest/ping?f=json", {}, {}) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.equal(body["subsonic-response"].status, "ok") + assert.equal(body["subsonic-response"].openSubsonic, true) +}) + +test("Subsonic rejects anonymous credential requests", async () => { + const res = await buildApp().request( + "/@rest/getArtists?u=nobody&p=wrong&f=json", + {}, + {}, + ) + const body: any = await res.json() + assert.equal(body["subsonic-response"].status, "failed") + assert.equal(body["subsonic-response"].error.code, 40) +}) + +test("Subsonic XML serializer emits attributes and nested arrays", () => { + const xml = __embyTest.toXmlNode("subsonic-response", { + status: "ok", + version: "1.16.1", + album: [ + { + id: "al-1", + name: "Album", + song: [{ id: "so-1", title: "Song" }], + }, + ], + }) + assert.match(xml, /status="ok"/) + assert.match(xml, /version="1.16.1"/) + assert.match( + xml, + /<\/album>/, + ) +}) + +test("Subsonic XML serializer escapes dangerous characters", () => { + const xml = __embyTest.toXmlNode("subsonic-response", { + name: `a { + const a = __embyTest.numericId("mv-12") + const b = __embyTest.numericId("mv-12") + assert.equal(a, b) + assert.equal(a, "12") + assert.notEqual( + __embyTest.numericId("sr-abc"), + __embyTest.numericId("sr-abd"), + ) +}) + +// ---- 封面权限:origId 对未映射 ID 原样返回,不能借此读全库 ---- + +test("Emby 封面:访客根目录之外 / hidden 的条目一律 404", async () => { + await seedMediaEnv([ + mediaItem({ + id: 1, + media_type: "video", + folder_path: "/public", + file_name: "ok.mkv", + cover: "https://example.com/ok.jpg", + }), + mediaItem({ + id: 42, + media_type: "video", + folder_path: "/private", + file_name: "secret.mkv", + cover: "https://example.com/secret.jpg", + }), + mediaItem({ + id: 43, + media_type: "video", + folder_path: "/public", + file_name: "hidden.mkv", + cover: "https://example.com/hidden.jpg", + hidden: true, + }), + ]) + + const app = buildApp() + const ok = await app.request("/@emby/Items/mv-1/Images/Primary", {}, {}) + assert.equal(ok.status, 302) + assert.equal(ok.headers.get("location"), "https://example.com/ok.jpg") + + // 未映射的 mv-42 / 纯数字 42 都不能命中 /private 下的条目 + for (const id of ["mv-42", "42"]) { + const res = await app.request(`/@emby/Items/${id}/Images/Primary`, {}, {}) + assert.equal(res.status, 404, `${id} 不应返回访客根目录外的封面`) + } + + const hidden = await app.request("/@emby/Items/mv-43/Images/Primary", {}, {}) + assert.equal(hidden.status, 404) +}) + +test("Emby 封面:媒体类型关闭后不再返回封面", async () => { + await seedMediaEnv( + [ + mediaItem({ + id: 1, + media_type: "video", + folder_path: "/public", + file_name: "ok.mkv", + cover: "https://example.com/ok.jpg", + }), + ], + [], + false, + ) + + const res = await buildApp().request( + "/@emby/Items/mv-1/Images/Primary", + {}, + {}, + ) + assert.equal(res.status, 404) +}) + +test("Emby 封面:剧集封面同样经过 meta 校验", async () => { + const show = mediaItem({ + id: 10, + media_type: "video", + folder_path: "/public/Show", + file_name: "Show", + scraped_name: "Show", + video_type: "tv", + is_folder: true, + cover: "https://example.com/show.jpg", + }) + const key = __embyTest.seriesKeyOf(show) + const path = `/@emby/Items/sr-${key}/Images/Primary` + const app = buildApp() + + // 无 meta 限制时可访问(正例,确保下面的 404 不是路径/ID 写错造成) + await seedMediaEnv([show]) + const allowed = await app.request(path, {}, {}) + assert.equal(allowed.status, 302) + + // /public/Show 仅对用户 9999 开放:聚合出来的 series.cover 不能再直接返回 + await seedMediaEnv([show], [ + { + id: 1, + path: "/public/Show", + password: "", + p_sub: false, + read_users: [9999], + read_users_sub: true, + write_users: [], + write_users_sub: false, + write: false, + w_sub: false, + hide: "", + h_sub: false, + readme: "", + r_sub: false, + header: "", + header_sub: false, + }, + ]) + const denied = await app.request(path, {}, {}) + assert.equal(denied.status, 404) +}) diff --git a/src/backend/server/media.ts b/src/backend/server/media.ts new file mode 100644 index 00000000..599edac4 --- /dev/null +++ b/src/backend/server/media.ts @@ -0,0 +1,1594 @@ +/** + * 媒体库 HTTP API(对齐 Go 版 server/handles/media*.go)。 + * + * 挂载: + * /api/fs/media/* 登录用户(访客受 base_path/meta 限制) + * /api/admin/media/* 管理员 + * /api/public/media* 公开(状态 / 封面代理 / 缩略图) + * /api/auth/me/appkey/* 当前用户自助签发应用密钥 + */ + +import { Hono } from "hono" + +import { + adminAuthMiddleware, + getUserFromContext, + getJwtSecret, +} from "./middlewares" +import { getNearestMeta, canAccess } from "../pkg/meta" +import { isAdmin, isWithinUserRoot, UserRole } from "../pkg/permission" +import { encodeDownloadPath } from "../pkg/path" +import { getSignPolicy, signDownloadPath } from "../pkg/sign" +import { isSafeUrl } from "../pkg/http" +import { encrypt, decrypt } from "../pkg/crypto" +import { getItem, putItem, readFileRange } from "../internal/op/storage" +import { resolvePath } from "../internal/model/db" +import { canUseProxyEndpoint, normalizeExtList } from "../internal/driver/proxy" +import { + baseNameOf, + extOf, + MEDIA_TYPES, + mediaEnabled, + mediaProtocolEnabled, + PLAYED_RATIO, + PLAYED_RESET_SECONDS, + settingBool, + ticksToSeconds, + type EpisodeInfo, + type MediaAppKey, + type MediaItem, + type MediaType, + type MediaUserData, +} from "../internal/media/model" +import { + allocateIds, + ensureUserData, + findItem, + findScanPath, + getConfig, + getMediaDb, + getSettingsMap, + getUserData, + isTypeEnabled, + listFavorites, + listResume, + listScanPaths, + nextId, + queryItems, + readThumb, + repairItemScanPath, + saveMediaDb, +} from "../internal/media/store" +import { getScanProgress, isScanRunning, scanMedia } from "../internal/media/scan" +import { getScrapeStats, isScrapeRunning, scrapeMedia } from "../internal/media/scrape" +import { generateStrmForType, renderStrmContent } from "../internal/media/strm" +import { + persistCover, + resolveCoverUrl, + thumbOptionsOf, +} from "../internal/media/thumb" +import { sniffImageMime } from "../internal/media/tag" +import { + authFailed, + authLockedFor, + authSucceeded, +} from "../internal/media/ratelimit" + +const ok = (c: any, data: any = null) => + c.json({ code: 200, message: "success", data }) +const fail = (c: any, message: string, status = 400) => + c.json({ code: status, message, data: null }, status as any) + +const originOf = (c: any) => new URL(c.req.url).origin + +const sha256Hex = async (value: string) => { + const digest = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(value), + ) + return Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") +} + +const randomToken = (prefix: string) => { + const bytes = new Uint8Array(24) + crypto.getRandomValues(bytes) + return ( + prefix + + Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + ) +} + +const pathPrefixOf = (user: any) => { + const base = String(user?.base_path || "/").replace(/\/+$/, "") + return base === "" ? "/" : base +} + +/** 对外输出:cover 的 thumb: 标记转成可访问 URL */ +export const serializeItem = (item: MediaItem, origin: string) => ({ + ...item, + cover: resolveCoverUrl(item.cover || "", origin), +}) + +export async function canAccessItem(env: any, user: any, item: MediaItem) { + if (!user || item.hidden) return false + const db = await getMediaDb(env) + if (!isTypeEnabled(db, item.media_type)) return false + if (!isWithinUserRoot(user, item.folder_path)) return false + // 目录合并条目要检查到具体文件路径,避免仅凭父目录判断绕过 meta + const target = `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + try { + const meta = await getNearestMeta(target, env) + return canAccess(user, meta, target, "") + } catch { + return true + } +} + +async function accessibleItems(env: any, user: any, items: MediaItem[]) { + const out: MediaItem[] = [] + for (const item of items) { + if (await canAccessItem(env, user, item)) out.push(item) + } + return out +} + +/** 条目对应的实际文件路径(剧集取第 season 季第 ep 集) */ +export const itemPath = (item: MediaItem, ep?: number, season?: number) => { + const folder = item.folder_path.replace(/\/+$/, "") || "" + let path = `${folder}/${item.file_name}`.replace(/\/+/g, "/") + if (!item.episodes) return path + try { + const eps: EpisodeInfo[] = JSON.parse(item.episodes) + if (!Array.isArray(eps) || eps.length === 0) return path + const wantSeason = Number(season) || 0 + const found = + ep && ep > 0 + ? eps.find( + (e) => + (e.index === ep || e.index === Number(ep)) && + (!wantSeason || + (Number(e.season) || Number(item.season) || 1) === wantSeason), + ) || eps.find((e) => e.index === ep || e.index === Number(ep)) + : eps[0] + if (found?.file_name) { + path = `${folder}/${item.file_name}/${found.file_name}`.replace(/\/+/g, "/") + } + } catch { + /* ignore */ + } + return path +} + +const mapUserData = (row: MediaUserData) => ({ + item_id: row.item_id, + episode_index: row.episode_index, + season: row.season || 1, + played: row.played, + play_count: row.play_count, + is_favorite: row.is_favorite, + position_ticks: row.position_ticks, + duration_ticks: row.duration_ticks, + rating: row.rating || 0, + last_played_at: row.last_played_at || null, + updated_at: row.updated_at, +}) + +/** 进度写入语义(对齐 Go SaveMediaProgress) */ +export function applyProgress( + row: MediaUserData, + positionSeconds: number, + durationSeconds: number, + playedFlag: boolean, +) { + const now = new Date().toISOString() + const pos = Math.max(0, Number(positionSeconds) || 0) + const dur = Math.max(0, Number(durationSeconds) || 0) + row.position_ticks = Math.round( + (dur > 0 ? Math.min(pos, dur) : pos) * 10_000_000, + ) + row.duration_ticks = Math.round(dur * 10_000_000) + let finished = !!playedFlag + if (dur > 0 && pos / dur >= PLAYED_RATIO) finished = true + if (finished && !row.played) { + row.played = true + row.play_count += 1 + } + // 重新播放:进度很小且此前已看 → 清除已看标记 + if (!finished && pos < PLAYED_RESET_SECONDS && row.played) { + row.played = false + } + row.last_played_at = now + row.updated_at = now +} + +// ==================== 用户侧路由 ==================== + +export const mediaRouter = new Hono() + +mediaRouter.use("*", async (c, next) => { + const settings = await getSettingsMap(c.env) + if (!mediaEnabled(settings)) return c.notFound() + await next() +}) + +mediaRouter.get("/list", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const type = (c.req.query("media_type") || "") as MediaType + if (!type || !MEDIA_TYPES.includes(type)) return fail(c, "invalid media_type") + const db = await getMediaDb(c.env) + if (!isTypeEnabled(db, type)) return ok(c, { content: [], total: 0 }) + const q = { + media_type: type, + scan_path_id: Number(c.req.query("scan_path_id") || 0) || undefined, + folder_path: c.req.query("folder_path") || undefined, + keyword: c.req.query("keyword") || undefined, + type_tag: c.req.query("type_tag") || undefined, + content_tag: c.req.query("content_tag") || undefined, + path_prefix: pathPrefixOf(user), + hidden: false, + order_by: c.req.query("order_by") || "name", + order_dir: c.req.query("order_dir") || "asc", + page: Number(c.req.query("page") || 1), + page_size: Math.min(Number(c.req.query("page_size") || 40), 200), + } + const { content, total } = queryItems(db, q) + const filtered = await accessibleItems(c.env, user, content) + const origin = originOf(c) + return ok(c, { + content: filtered.map((i) => serializeItem(i, origin)), + total, + }) +}) + +mediaRouter.get("/item/:id", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const item = findItem(db, Number(c.req.param("id"))) + if (!item || !(await canAccessItem(c.env, user, item))) { + return fail(c, "资源不存在", 404) + } + return ok(c, serializeItem(item, originOf(c))) +}) + +mediaRouter.get("/albums", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + if (!isTypeEnabled(db, "music")) return ok(c, { content: [], total: 0 }) + const keyword = (c.req.query("keyword") || "").toLowerCase() + const scanPathId = Number(c.req.query("scan_path_id") || 0) + const items = db.items.filter( + (i) => + i.media_type === "music" && + (!scanPathId || i.scan_path_id === scanPathId) && + (!keyword || + i.file_name.toLowerCase().includes(keyword) || + (i.scraped_name || "").toLowerCase().includes(keyword) || + (i.album_name || "").toLowerCase().includes(keyword)), + ) + const map = new Map() + // 鉴权缓存按「实际校验路径」去重:合并专辑的每首歌 file_name 不同, + // 但 canAccessItem 校验的是 directory/,同一专辑命中同一键, + // 因此逐曲过滤不会比原来「取首曲」多花 meta 查询。 + const accessCache = new Map() + const allowed = async (item: MediaItem) => { + const key = `${item.folder_path}/${item.file_name}` + if (!accessCache.has(key)) { + accessCache.set(key, await canAccessItem(c.env, user, item)) + } + return accessCache.get(key)! + } + for (const item of items) { + const album = item.album_name || "未知专辑" + const artist = item.album_artist || "未知艺术家" + const key = `${album}\u0000${artist}` + let row = map.get(key) + // 每一首都要通过鉴权:只看首曲会让同一专辑里被限制的曲目 + // 仍然贡献时长/数量甚至封面(PR #148 评审 #12)。 + if (!(await allowed(item))) continue + if (!row) { + row = { + album_name: album, + album_artist: artist, + cover: resolveCoverUrl(item.cover || "", originOf(c)), + release_date: item.release_date, + genre: item.genre, + track_count: 0, + duration: 0, + } + map.set(key, row) + } + row.track_count += 1 + row.duration += item.duration || 0 + if (!row.cover && item.cover) { + row.cover = resolveCoverUrl(item.cover, originOf(c)) + } + } + const page = Math.max(1, Number(c.req.query("page") || 1)) + const pageSize = Math.min(Number(c.req.query("page_size") || 40), 200) + const all = Array.from(map.values()).sort((a, b) => + String(a.album_name).localeCompare(String(b.album_name), "zh-CN"), + ) + return ok(c, { + content: all.slice((page - 1) * pageSize, page * pageSize), + total: all.length, + }) +}) + +mediaRouter.get("/album", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const album = c.req.query("album_name") || "" + const artist = c.req.query("album_artist") || "" + const list = await accessibleItems( + c.env, + user, + db.items.filter( + (i) => + i.media_type === "music" && + i.album_name === album && + (!artist || i.album_artist === artist), + ), + ) + list.sort( + (a, b) => + a.track_number - b.track_number || a.file_name.localeCompare(b.file_name), + ) + const origin = originOf(c) + return ok(c, list.map((i) => serializeItem(i, origin))) +}) + +mediaRouter.get("/folders", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const type = (c.req.query("media_type") || "") as MediaType + const db = await getMediaDb(c.env) + if (type && !isTypeEnabled(db, type)) return ok(c, []) + const set = new Set() + for (const item of db.items) { + if (type && item.media_type !== type) continue + if (item.hidden) continue + if (!isWithinUserRoot(user, item.folder_path)) continue + set.add(item.folder_path) + } + // 只对去重后的目录做 meta 校验(逐条校验在上万条时会超时) + const out: string[] = [] + for (const folder of set) { + try { + const meta = await getNearestMeta(folder, c.env) + if (canAccess(user, meta, folder, "")) out.push(folder) + } catch { + out.push(folder) + } + } + return ok(c, out.sort()) +}) + +mediaRouter.get("/scan_paths", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const type = c.req.query("media_type") as MediaType | undefined + const db = await getMediaDb(c.env) + const paths = listScanPaths(db, type).filter((p) => + isWithinUserRoot(user, p.path), + ) + return ok(c, paths) +}) + +mediaRouter.get("/userdata", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const ids = (c.req.query("ids") || "") + .split(",") + .map((s) => Number(s)) + .filter((n) => Number.isFinite(n) && n > 0) + .slice(0, 500) + const rows = db.user_data + .filter((d) => d.user_id === user.id && ids.includes(d.item_id)) + .map(mapUserData) + return ok(c, rows) +}) + +mediaRouter.get("/progress", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const itemId = Number(c.req.query("item_id") || 0) + if (!itemId) return ok(c, null) + const item = findItem(db, itemId) + if (!item || !(await canAccessItem(c.env, user, item))) return ok(c, null) + const row = getUserData( + db, + user.id, + itemId, + Number(c.req.query("episode_index") || 0), + Number(c.req.query("season") || 0), + ) + return ok(c, row ? mapUserData(row) : null) +}) + +mediaRouter.post("/progress", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const body = await c.req.json().catch(() => ({})) + const db = await getMediaDb(c.env) + const item = findItem(db, Number(body.item_id)) + if (!item || !(await canAccessItem(c.env, user, item))) { + return fail(c, "资源不存在", 404) + } + const row = ensureUserData( + db, + user.id, + item.id, + Number(body.episode_index) || 0, + Number(body.season) || 0, + ) + applyProgress( + row, + Number(body.position) || 0, + Number(body.duration) || 0, + !!body.played, + ) + await saveMediaDb(c.env, db) + return ok(c, mapUserData(row)) +}) + +mediaRouter.post("/favorite", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const body = await c.req.json().catch(() => ({})) + const db = await getMediaDb(c.env) + const item = findItem(db, Number(body.item_id)) + if (!item || !(await canAccessItem(c.env, user, item))) { + return fail(c, "资源不存在", 404) + } + const row = ensureUserData( + db, + user.id, + item.id, + Number(body.episode_index) || 0, + Number(body.season) || 0, + ) + row.is_favorite = !!body.favorite + row.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return ok(c, { favorite: row.is_favorite }) +}) + +mediaRouter.get("/favorites", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const type = c.req.query("media_type") as MediaType | undefined + const db = await getMediaDb(c.env) + // 对齐 Go:只返回条目级收藏(episode_index === 0) + const ids = listFavorites(db, user.id) + .filter((d) => d.episode_index === 0) + .map((d) => d.item_id) + const page = Math.max(1, Number(c.req.query("page") || 1)) + const size = Math.min(Number(c.req.query("page_size") || 20), 100) + const filtered = await accessibleItems( + c.env, + user, + db.items.filter( + (i) => ids.includes(i.id) && (!type || i.media_type === type), + ), + ) + filtered.sort((a, b) => a.id - b.id) + const origin = originOf(c) + return ok(c, { + content: filtered + .slice((page - 1) * size, page * size) + .map((i) => serializeItem(i, origin)), + total: filtered.length, + }) +}) + +mediaRouter.get("/resume", async (c) => { + const user = await getUserFromContext(c) + if (!user?.id) return fail(c, "Guest user is disabled, login please", 401) + const type = c.req.query("media_type") as MediaType | undefined + const limit = Math.min(Number(c.req.query("limit") || 20), 100) + const db = await getMediaDb(c.env) + const rows = listResume(db, user.id, 200) + const out: any[] = [] + for (const row of rows) { + const item = findItem(db, row.item_id) + if (!item || (type && item.media_type !== type)) continue + if (!(await canAccessItem(c.env, user, item))) continue + out.push({ + ...serializeItem(item, originOf(c)), + position_ticks: row.position_ticks, + episode_index: row.episode_index, + }) + if (out.length >= limit) break + } + return ok(c, out) +}) + +mediaRouter.get("/stream_url", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const id = Number(c.req.query("id") || 0) + const rawPath = c.req.query("path") || "" + const episode = Number(c.req.query("episode") || 0) + let path = rawPath + let name = baseNameOf(rawPath) + if (id) { + const item = findItem(db, id) + if (!item || !(await canAccessItem(c.env, user, item))) { + return fail(c, "资源不存在", 404) + } + path = itemPath(item, episode) + name = item.file_name + } + if (!path || !isWithinUserRoot(user, path)) return fail(c, "invalid path") + if (!(await pathReadable(c.env, user, path))) { + return fail(c, "资源不存在", 404) + } + const policy = await getSignPolicy(c) + const expires = policy.expiresIn + const origin = originOf(c) + const downloadPath = encodeDownloadPath(path) + // /d 用普通签名;/p 用 scope=media 的签名 —— raw 路由据此跳过 + // web_proxy=false 的代理限制。浏览器内播放/阅读必须走 /p: + // 只有同源代理才能保证 Content-Type、Range,以及 MediaBunny 的音频回退。 + const directSign = await signDownloadPath(c, path, expires) + const mediaSign = await signDownloadPath(c, path, expires, "media") + const url = `${origin}/d${downloadPath}?sign=${encodeURIComponent(directSign)}` + const proxied = `${origin}/p${downloadPath}?sign=${encodeURIComponent(mediaSign)}&scope=media` + return ok(c, { + url, + path, + name: name || baseNameOf(path), + proxied, + }) +}) + +mediaRouter.get("/strm", async (c) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "Guest user is disabled, login please", 401) + const db = await getMediaDb(c.env) + const item = findItem(db, Number(c.req.query("id") || 0)) + if (!item || !(await canAccessItem(c.env, user, item))) { + return fail(c, "资源不存在", 404) + } + const settings = await getSettingsMap(c.env) + const ep = Number(c.req.query("episode") || 0) + const season = Number(c.req.query("season") || 0) || Number(item.season) || 1 + const path = itemPath(item, ep, season) + // 合并条目只校验了目录,itemPath 选中的子文件可能另有 password/read_users, + // 必须对最终播放路径再校验一次(与 /stream_url 一致)。 + if (!(await pathReadable(c.env, user, path))) { + return fail(c, "资源不存在", 404) + } + // 按用户签发的链接必须遵守 link_expiration,不能给永久签名 + const policy = await getSignPolicy(c) + const content = await renderStrmContent( + c.env, + settings.media_strm_template || "{site_url}/d{path}?sign={sign}", + originOf(c), + path, + policy.expiresIn, + ) + const title = + item.video_type === "tv" && ep > 0 + ? `${item.scraped_name || item.file_name} S${String(season).padStart(2, "0")}E${String(ep).padStart(2, "0")}.strm` + : `${item.scraped_name || baseNameOf(item.file_name)}.strm` + c.header("Content-Type", "text/plain; charset=utf-8") + c.header( + "Content-Disposition", + `attachment; filename*=UTF-8''${encodeURIComponent(title)}`, + ) + return c.body(content) +}) + +/** 单文件路径的 meta 可读性(/stream_url?path= 分支) */ +async function pathReadable(env: any, user: any, path: string) { + try { + const meta = await getNearestMeta(path, env) + return canAccess(user, meta, path, "") + } catch { + return true + } +} + +// ==================== 管理端路由 ==================== + +export const mediaAdminRouter = new Hono() + +// 与 /api/admin 其余路由一致:只接受 Authorization 头。 +// getUserFromContext 还会从 ?token= 取 JWT,管理端把令牌放进 URL 会泄漏到 +// 日志/历史/Referer(PR #148 评审 #15)。 +mediaAdminRouter.use("*", adminAuthMiddleware) + +mediaAdminRouter.get("/config/list", async (c) => { + const db = await getMediaDb(c.env) + return ok(c, db.configs) +}) + +mediaAdminRouter.post("/config/save", async (c) => { + const body = await c.req.json().catch(() => ({})) + const type = body.media_type as MediaType + if (!MEDIA_TYPES.includes(type)) return fail(c, "invalid media_type") + const db = await getMediaDb(c.env) + const cfg = getConfig(db, type) + for (const key of [ + "enabled", + "thumbnail_mode", + "thumbnail_path", + "store_thumbnail", + "scrape_concurrency", + ] as const) { + if (body[key] !== undefined) (cfg as any)[key] = body[key] + } + if (body.scraper_config !== undefined) { + cfg.scraper_config = + typeof body.scraper_config === "string" + ? body.scraper_config + : JSON.stringify(body.scraper_config || {}) + } + cfg.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.get("/scan_paths", async (c) => { + const db = await getMediaDb(c.env) + return ok( + c, + listScanPaths(db, c.req.query("media_type") as MediaType | undefined), + ) +}) + +mediaAdminRouter.post("/scan_paths/create", async (c) => { + const body = await c.req.json().catch(() => ({})) + const type = body.media_type as MediaType + if (!MEDIA_TYPES.includes(type) || !String(body.path || "").startsWith("/")) { + return fail(c, "invalid scan path") + } + const db = await getMediaDb(c.env) + const now = new Date().toISOString() + const sp = { + id: nextId(db), + media_type: type, + name: body.name || body.path, + path: body.path, + type_tag: body.type_tag || "", + content_tags: body.content_tags || "", + created_at: now, + updated_at: now, + path_merge: !!body.path_merge, + enable_scrape: body.enable_scrape === undefined ? true : !!body.enable_scrape, + last_scan_at: null, + } + db.scan_paths.push(sp) + await saveMediaDb(c.env, db) + return ok(c, sp) +}) + +mediaAdminRouter.post("/scan_paths/update", async (c) => { + const body = await c.req.json().catch(() => ({})) + const db = await getMediaDb(c.env) + const sp = findScanPath(db, Number(body.id)) + if (!sp) return fail(c, "not found", 404) + for (const key of [ + "name", + "path", + "type_tag", + "content_tags", + "path_merge", + "enable_scrape", + ] as const) { + if (body[key] !== undefined) (sp as any)[key] = body[key] + } + sp.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return ok(c) +}) + +/** 解析管理端必填 id:缺失/非法一律 400,绝不能当成 0 去匹配孤儿数据 */ +const requireId = (raw: string | undefined): number | null => { + if (raw === undefined || raw === "") return null + const n = Number(raw) + return Number.isInteger(n) && n > 0 ? n : null +} + +mediaAdminRouter.post("/scan_paths/delete", async (c) => { + const id = requireId(c.req.query("id")) + if (id === null) return fail(c, "invalid id") + const db = await getMediaDb(c.env) + db.scan_paths = db.scan_paths.filter((p) => p.id !== id) + db.items = db.items.filter((i) => i.scan_path_id !== id) + repairItemScanPath(db) + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.post("/scan_paths/clear", async (c) => { + const id = requireId(c.req.query("id")) + if (id === null) return fail(c, "invalid id") + const db = await getMediaDb(c.env) + db.items = db.items.filter((i) => i.scan_path_id !== id) + repairItemScanPath(db) + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.get("/items", async (c) => { + const db = await getMediaDb(c.env) + const result = queryItems(db, { + media_type: (c.req.query("media_type") || undefined) as + | MediaType + | undefined, + scan_path_id: Number(c.req.query("scan_path_id") || 0) || undefined, + keyword: c.req.query("keyword") || undefined, + order_by: c.req.query("order_by") || "name", + order_dir: c.req.query("order_dir") || "asc", + page: Number(c.req.query("page") || 1), + page_size: Math.min(Number(c.req.query("page_size") || 20), 500), + }) + const origin = originOf(c) + return ok(c, { + ...result, + content: result.content.map((i) => serializeItem(i, origin)), + }) +}) + +/** + * 后台专辑聚合列表。 + * + * 前端管理页(MediaAlbumTable)从 /admin/media/albums 取数据,但后端一直 + * 只有用户侧的 /fs/media/albums,管理页请求落到 SPA 兜底拿到 HTML, + * 于是「扫描完了、管理端却是空的」。管理员视角不做逐条权限过滤。 + */ +mediaAdminRouter.get("/albums", async (c) => { + const db = await getMediaDb(c.env) + const scanPathId = Number(c.req.query("scan_path_id") || 0) + const keyword = (c.req.query("keyword") || "").toLowerCase() + const page = Math.max(1, Number(c.req.query("page") || 1)) + const pageSize = Math.min(Number(c.req.query("page_size") || 40), 200) + const items = db.items.filter( + (i) => + i.media_type === "music" && + (!scanPathId || i.scan_path_id === scanPathId) && + (!keyword || + i.file_name.toLowerCase().includes(keyword) || + (i.album_name || "").toLowerCase().includes(keyword) || + (i.scraped_name || "").toLowerCase().includes(keyword) || + (i.album_artist || "").toLowerCase().includes(keyword)), + ) + const map = new Map() + for (const item of items) { + const album = item.album_name || "未知专辑" + const artist = item.album_artist || "未知艺术家" + const key = `${album}\u0000${artist}` + let row = map.get(key) + if (!row) { + row = { + album_name: album, + album_artist: artist, + cover: resolveCoverUrl(item.cover || "", originOf(c)), + release_date: item.release_date, + genre: item.genre, + track_count: 0, + duration: 0, + } + map.set(key, row) + } + row.track_count += 1 + row.duration += item.duration || 0 + if (!row.cover && item.cover) { + row.cover = resolveCoverUrl(item.cover, originOf(c)) + } + } + const all = Array.from(map.values()).sort((a, b) => + String(a.album_name).localeCompare(String(b.album_name), "zh-CN"), + ) + return ok(c, { + content: all.slice((page - 1) * pageSize, page * pageSize), + total: all.length, + }) +}) + +/** 某专辑下的真实条目(含 id,供管理端逐条编辑) */ +mediaAdminRouter.get("/album/items", async (c) => { + const db = await getMediaDb(c.env) + const album = c.req.query("album_name") || "" + const artist = c.req.query("album_artist") || "" + const scanPathId = Number(c.req.query("scan_path_id") || 0) + const list = db.items.filter( + (i) => + i.media_type === "music" && + i.album_name === album && + (!artist || i.album_artist === artist) && + (!scanPathId || i.scan_path_id === scanPathId), + ) + return ok(c, list) +}) + +/** 按专辑批量更新(封面 / 简介 / 年份 / 类型 / 专辑名 / 艺术家) */ +mediaAdminRouter.post("/album/update", async (c) => { + const body = await c.req.json().catch(() => ({})) + const album = String(body.album_name || "") + const artist = String(body.album_artist || "") + if (!album) return fail(c, "album_name is required") + const scanPathId = Number(body.scan_path_id || 0) + const db = await getMediaDb(c.env) + const now = new Date().toISOString() + let affected = 0 + for (const item of db.items) { + if (item.media_type !== "music") continue + if (item.album_name !== album) continue + if (artist && item.album_artist !== artist) continue + if (scanPathId && item.scan_path_id !== scanPathId) continue + if (body.new_album_name !== undefined) { + item.album_name = String(body.new_album_name) + } + if (body.new_album_artist !== undefined) { + item.album_artist = String(body.new_album_artist) + } + if (body.cover !== undefined) item.cover = String(body.cover) + if (body.release_date !== undefined) item.release_date = String(body.release_date) + if (body.genre !== undefined) item.genre = String(body.genre) + if (body.plot !== undefined) item.plot = String(body.plot) + if (body.description !== undefined) { + item.description = String(body.description) + } + item.updated_at = now + affected += 1 + } + if (affected > 0) await saveMediaDb(c.env, db) + return ok(c, { affected }) +}) + +/** 上传/丢弃自定义封面(multipart:id + file,或 id + discard) */ +mediaAdminRouter.post("/items/cover", async (c) => { + let form: FormData + try { + form = await c.req.formData() + } catch { + return fail(c, "invalid form data") + } + const id = Number(form.get("id") || 0) + const db = await getMediaDb(c.env) + const item = findItem(db, id) + if (!item) return fail(c, "not found", 404) + + // 回收暂存封面:仅当条目仍引用它时才清空,避免误删已保存的封面 + const discard = String(form.get("discard") || "") + if (discard) { + if (item.cover === discard) { + item.cover = "" + item.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + } + return ok(c, null) + } + + const file = form.get("file") + if (!(file instanceof File)) return fail(c, "file is required") + if (file.size <= 0 || file.size > 8 * 1024 * 1024) { + return fail(c, "cover must be between 1 byte and 8 MB") + } + const data = new Uint8Array(await file.arrayBuffer()) + // 只信魔数:声明为 image/* 的 SVG/HTML 一律拒绝 + if (!sniffImageMime(data)) return fail(c, "unsupported image format") + + const cfg = getConfig(db, item.media_type) + const stored = await persistCover(c.env, data, thumbOptionsOf(cfg)) + // deferred:先返回暂存地址,等用户点保存再写进条目 + if (String(form.get("deferred") || "") === "true") { + return ok(c, { cover: stored.cover }) + } + item.cover = stored.cover + item.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return ok(c, { cover: stored.cover }) +}) + +mediaAdminRouter.post("/items/update", async (c) => { + const body = await c.req.json().catch(() => ({})) + const db = await getMediaDb(c.env) + const item = findItem(db, Number(body.id)) + if (!item) return fail(c, "not found", 404) + const editable = [ + "scraped_name", + "cover", + "authors", + "plot", + "description", + "genre", + "release_date", + "rating", + "publisher", + "isbn", + "external_id", + "hidden", + "album_name", + "album_artist", + "track_number", + "lyrics", + "video_type", + "season", + "episode", + "reviews", + "mime_type", + ] as const + for (const key of editable) { + if (body[key] !== undefined) (item as any)[key] = body[key] + } + item.updated_at = new Date().toISOString() + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.post("/items/delete", async (c) => { + const id = Number(c.req.query("id") || 0) + const db = await getMediaDb(c.env) + db.items = db.items.filter((i) => i.id !== id) + repairItemScanPath(db) + await saveMediaDb(c.env, db) + return ok(c) +}) + +const runBackground = (c: any, task: Promise) => { + try { + c.executionCtx.waitUntil(task) + } catch { + return task + } + return Promise.resolve() +} + +mediaAdminRouter.post("/scan/start", async (c) => { + const body = await c.req.json().catch(() => ({})) + const type = body.media_type as MediaType + if (!MEDIA_TYPES.includes(type)) return fail(c, "invalid media_type") + if (isScanRunning(type)) return fail(c, "scan already running", 409) + const id = Number(body.scan_path_id || 0) + const task = scanMedia(c.env, type, id).catch((e) => + console.error("[media] scan failed:", e?.message || e), + ) + await runBackground(c, task) + return ok(c) +}) + +mediaAdminRouter.get("/scan/progress", async (c) => { + const type = (c.req.query("media_type") || "video") as MediaType + return ok(c, getScanProgress(type, Number(c.req.query("scan_path_id") || 0))) +}) + +mediaAdminRouter.post("/scrape/start", async (c) => { + const body = await c.req.json().catch(() => ({})) + const type = body.media_type as MediaType + if (!MEDIA_TYPES.includes(type)) return fail(c, "invalid media_type") + if (isScrapeRunning(type)) return fail(c, "scrape already running", 409) + const settings = await getSettingsMap(c.env) + const task = scrapeMedia(c.env, settings, type, Number(body.item_id || 0)).catch( + (e) => console.error("[media] scrape failed:", e?.message || e), + ) + await runBackground(c, task) + return ok(c) +}) + +mediaAdminRouter.get("/scrape/stats", async (c) => { + const type = (c.req.query("media_type") || "video") as MediaType + const db = await getMediaDb(c.env) + const list = db.items.filter((i) => i.media_type === type) + return ok(c, { + ...getScrapeStats(type), + total: list.length, + scraped: list.filter((i) => !!i.scraped_at).length, + }) +}) + +mediaAdminRouter.post("/clear", async (c) => { + const type = c.req.query("media_type") as MediaType + const db = await getMediaDb(c.env) + if (type) db.items = db.items.filter((i) => i.media_type !== type) + else db.items = [] + repairItemScanPath(db) + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.post("/clear_scrape", async (c) => { + const type = c.req.query("media_type") as MediaType | undefined + const db = await getMediaDb(c.env) + let affected = 0 + for (const item of db.items) { + if (type && item.media_type !== type) continue + item.cover = "" + item.thumb = "" + item.thumb_mime = "" + item.scraped_name = "" + item.authors = "" + item.plot = "" + item.description = "" + item.genre = "" + item.release_date = "" + item.rating = 0 + item.publisher = "" + item.isbn = "" + item.external_id = "" + item.scraper_source = "" + item.lyrics = "" + item.reviews = "" + item.scraped_at = null + affected += 1 + } + await saveMediaDb(c.env, db) + return ok(c, { affected }) +}) + +/** + * 删除失效条目:逐条确认文件/目录在存储中是否还存在。 + * + * Worker 没有遍历整库的时间预算,因此限制单次检查条数(默认 500,最大 2000)。 + * 对齐 Go 的 f80b7cdb:只删除「确认不存在」的记录;查询异常时保留。 + */ +mediaAdminRouter.post("/delete_invalid", async (c) => { + const type = c.req.query("media_type") as MediaType | undefined + // 每条都要向存储换链 + 探测,Worker 单请求时间有限,默认只查 200 条 + const limit = Math.min(Number(c.req.query("limit") || 200) || 200, 1000) + // 游标分页:固定拿前 N 条会让后面的坏记录永远检查不到 + const cursor = Math.max(0, Math.floor(Number(c.req.query("cursor") || 0)) || 0) + const db = await getMediaDb(c.env) + const pool = db.items.filter((i) => !type || i.media_type === type) + const candidates = pool.slice(cursor, cursor + limit) + const gone = new Set() + for (const item of candidates) { + const path = item.is_folder + ? `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + : `${item.folder_path.replace(/\/+$/, "")}/${item.file_name}` + try { + await getItem(path, { env: c.env }) + } catch (e: any) { + // 目录条目:getItem 对目录可能失败,退化为 list 一次 + if (item.is_folder) { + try { + const { listItems } = await import("../internal/op/storage") + await listItems(path, { env: c.env }) + continue + } catch (listErr: any) { + // 只有明确的「不存在」才删;鉴权过期/超时/上游故障一律保留 + const lmsg = String(listErr?.message || "") + if (/not\s*found|不存在|404/i.test(lmsg)) gone.add(item.id) + continue + } + } + const msg = String(e?.message || "") + // 明确 404 / not found 才删除;网络/鉴权错误保留(对齐 Go) + if (/not\s*found|不存在|404/i.test(msg)) gone.add(item.id) + } + } + const before = db.items.length + if (gone.size > 0) { + db.items = db.items.filter((i) => !gone.has(i.id)) + repairItemScanPath(db) + await saveMediaDb(c.env, db) + } + const nextCursor = cursor + candidates.length + return ok(c, { + checked: candidates.length, + deleted: before - db.items.length, + remaining: db.items.length, + next_cursor: candidates.length > 0 && nextCursor < pool.length ? nextCursor : null, + }) +}) + +/** 修复孤儿数据(导入/删除后使用) */ +mediaAdminRouter.post("/repair", async (c) => { + const db = await getMediaDb(c.env) + const result = repairItemScanPath(db) + await saveMediaDb(c.env, db) + return ok(c, result) +}) + +mediaAdminRouter.get("/export", async (c) => { + const type = c.req.query("media_type") as MediaType | undefined + const scanPathId = Number(c.req.query("scan_path_id") || 0) + const db = await getMediaDb(c.env) + let items = db.items + if (scanPathId) items = items.filter((i) => i.scan_path_id === scanPathId) + if (type) items = items.filter((i) => i.media_type === type) + const scope = scanPathId ? "scan_path" : type ? "media_type" : "all" + const data = { + version: 2, + export_at: new Date().toISOString(), + scope, + media_type: type || "", + scan_path_id: scanPathId || 0, + scan_paths: scanPathId + ? db.scan_paths.filter((p) => p.id === scanPathId) + : listScanPaths(db, type), + items, + } + const name = + scope === "scan_path" + ? `media-scanpath-${scanPathId}.json` + : scope === "media_type" + ? `media-${type}.json` + : "media-export.json" + c.header("Content-Type", "application/json") + c.header("Content-Disposition", `attachment; filename=${name}`) + return c.body(JSON.stringify(data, null, 2)) +}) + +mediaAdminRouter.post("/import", async (c) => { + let body: any = null + try { + body = await c.req.json() + } catch { + try { + const form = await c.req.formData() + const file = form.get("file") as File | null + if (file) body = JSON.parse(await file.text()) + } catch { + body = null + } + } + if (!body || !Array.isArray(body.items)) return fail(c, "invalid import file") + + const db = await getMediaDb(c.env) + const redirectScanPathId = Number(c.req.query("scan_path_id") || 0) + const now = new Date().toISOString() + let spCreated = 0 + let spUpdated = 0 + let created = 0 + let updated = 0 + + // 1) 扫描路径:按 (media_type, path) 去重,建立旧 id → 新 id 映射 + const idMap = new Map() + const rawPaths: any[] = Array.isArray(body.scan_paths) ? body.scan_paths : [] + for (const raw of rawPaths) { + const mediaType = (raw?.media_type || body.media_type || "video") as MediaType + if (!MEDIA_TYPES.includes(mediaType)) continue + const path = String(raw?.path || "") + if (!path.startsWith("/")) continue + const exists = db.scan_paths.find( + (p) => p.media_type === mediaType && p.path === path, + ) + if (exists) { + exists.name = raw.name || exists.name + exists.type_tag = raw.type_tag ?? exists.type_tag + exists.content_tags = raw.content_tags ?? exists.content_tags + if (raw.path_merge !== undefined) exists.path_merge = !!raw.path_merge + if (raw.enable_scrape !== undefined) { + exists.enable_scrape = !!raw.enable_scrape + } + exists.updated_at = now + if (raw.id !== undefined) idMap.set(Number(raw.id), exists.id) + spUpdated += 1 + } else { + const sp = { + id: nextId(db), + media_type: mediaType, + name: raw.name || path, + path, + type_tag: raw.type_tag || "", + content_tags: raw.content_tags || "", + created_at: now, + updated_at: now, + path_merge: !!raw.path_merge, + enable_scrape: raw.enable_scrape === undefined ? true : !!raw.enable_scrape, + last_scan_at: null, + } + db.scan_paths.push(sp) + if (raw.id !== undefined) idMap.set(Number(raw.id), sp.id) + spCreated += 1 + } + } + + // 2) 条目 + for (const raw of body.items as MediaItem[]) { + if (!raw || !raw.folder_path || !raw.file_name) continue + let scanPathId = Number(raw.scan_path_id) || 0 + if (redirectScanPathId) scanPathId = redirectScanPathId + else if (idMap.has(scanPathId)) scanPathId = idMap.get(scanPathId)! + const exists = db.items.find( + (i) => + i.folder_path === raw.folder_path && + i.file_name === raw.file_name && + i.media_type === raw.media_type && + (i.album_name || "") === (raw.album_name || ""), + ) + if (exists) { + Object.assign(exists, raw, { + id: exists.id, + scan_path_id: scanPathId || exists.scan_path_id, + created_at: exists.created_at, + updated_at: now, + }) + updated += 1 + } else { + const [id] = allocateIds(db, 1) + db.items.push({ + ...raw, + id, + scan_path_id: scanPathId, + created_at: raw.created_at || now, + updated_at: now, + }) + created += 1 + } + } + + repairItemScanPath(db) + // 不要在这里 invalidate:saveMediaDb 会立刻把 db 重新装回缓存, + // 唯一效果是丢掉分片快照,导致本次保存重写全部分片、且被清空的分片 + // 不会写入 [],被移动的条目下次加载时复活(PR #148 评审 #17)。 + await saveMediaDb(c.env, db) + return ok(c, { + scan_paths_created: spCreated, + scan_paths_updated: spUpdated, + items_created: created, + items_updated: updated, + }) +}) + +// ==================== 应用密钥(管理员 + 用户自助) ==================== + +const appKeyPlainOf = async (c: any, row: MediaAppKey): Promise => { + if (row.enc) { + try { + return await decrypt(row.enc, await getJwtSecret(c)) + } catch { + return "" + } + } + return row.plain || "" +} + +const listKeysDto = (db: any, userId?: number) => + db.app_keys + .filter((k: MediaAppKey) => !userId || k.user_id === userId) + .map((k: MediaAppKey) => ({ + id: k.id, + name: k.name, + prefix: k.prefix, + user_id: k.user_id, + last_used_at: k.last_used_at, + created_at: k.created_at, + })) + +/** 每个用户最多持有的应用密钥数:每次创建都会全量重写媒体库文档,必须有上限 */ +const MAX_APP_KEYS_PER_USER = 20 + +/** + * 创建应用密钥前的准入检查。 + * + * getUserFromContext 在没有 Authorization 头时会回退成 guest,所以只判断 + * 「user 存在」等于允许匿名创建密钥——既会写脏数据,也会触发整份文档重写。 + */ +const denyKeyCreate = async (c: any, user: any) => { + if (!user) return fail(c, "未登录", 401) + if (String(user.username || "") === "guest") { + return fail(c, "访客不能创建应用密钥", 403) + } + const db = await getMediaDb(c.env) + const ownerId = user.id || 1 + const owned = db.app_keys.filter((k) => k.user_id === ownerId).length + if (owned >= MAX_APP_KEYS_PER_USER) { + return fail(c, `应用密钥数量已达上限(${MAX_APP_KEYS_PER_USER})`, 400) + } + return null +} + +const createKey = async (c: any, user: any, name: string) => { + const db = await getMediaDb(c.env) + const plain = randomToken("olmp_") + const row: MediaAppKey = { + id: nextId(db), + user_id: user?.id || 1, + name: name || "Media Key", + prefix: plain.slice(0, 10), + secret: await sha256Hex(plain), + enc: await encrypt(plain, await getJwtSecret(c)), + last_used_at: null, + created_at: new Date().toISOString(), + } + db.app_keys.push(row) + await saveMediaDb(c.env, db) + return { + id: row.id, + name: row.name, + prefix: row.prefix, + key: plain, + } +} + +mediaAdminRouter.get("/appkey/list", async (c) => { + const db = await getMediaDb(c.env) + return ok(c, listKeysDto(db)) +}) + +mediaAdminRouter.post("/appkey/create", async (c) => { + const body = await c.req.json().catch(() => ({})) + const user = await getUserFromContext(c) + const denied = await denyKeyCreate(c, user) + if (denied) return denied + return ok(c, await createKey(c, user, body.name)) +}) + +mediaAdminRouter.post("/appkey/delete", async (c) => { + const id = Number(c.req.query("id") || 0) + const db = await getMediaDb(c.env) + db.app_keys = db.app_keys.filter((k) => k.id !== id) + await saveMediaDb(c.env, db) + return ok(c) +}) + +/** 普通用户自助管理自己的密钥(对齐 Go 的 /api/me/appkey/*) */ +export const meAppkeyRouter = new Hono() + +meAppkeyRouter.use("*", async (c, next) => { + const user = await getUserFromContext(c) + if (!user) return fail(c, "未登录", 401) + await next() +}) + +meAppkeyRouter.get("/list", async (c) => { + const user = await getUserFromContext(c) + const db = await getMediaDb(c.env) + return ok(c, listKeysDto(db, user?.id)) +}) + +meAppkeyRouter.post("/create", async (c) => { + const body = await c.req.json().catch(() => ({})) + const user = await getUserFromContext(c) + const denied = await denyKeyCreate(c, user) + if (denied) return denied + return ok(c, await createKey(c, user, body.name)) +}) + +meAppkeyRouter.post("/delete", async (c) => { + const user = await getUserFromContext(c) + const id = Number(c.req.query("id") || 0) + const db = await getMediaDb(c.env) + const row = db.app_keys.find((k) => k.id === id) + if (!row) return fail(c, "not found", 404) + if (row.user_id !== user?.id && !isAdmin(user)) return fail(c, "forbidden", 403) + db.app_keys = db.app_keys.filter((k) => k.id !== id) + await saveMediaDb(c.env, db) + return ok(c) +}) + +mediaAdminRouter.post("/strm/generate", async (c) => { + const body = await c.req.json().catch(() => ({})) + const type = (body.media_type || "") as MediaType | "" + if (type && !MEDIA_TYPES.includes(type)) return fail(c, "invalid media_type") + const scanPathId = Number(body.scan_path_id || 0) + try { + const result = await generateStrmForType( + c.env, + type, + scanPathId, + originOf(c), + ) + return ok(c, { written: result.written, failed: result.failed, path: result.path }) + } catch (e: any) { + return fail(c, e?.message || "strm generate failed", 500) + } +}) + +// ==================== 公开路由 ==================== + +export const mediaPublicRouter = new Hono() + +mediaPublicRouter.get("/media_status", async (c) => { + const settings = await getSettingsMap(c.env) + const db = await getMediaDb(c.env) + const origin = originOf(c) + return ok(c, { + enabled: mediaEnabled(settings), + version: String(settings.version || "worker"), + types: MEDIA_TYPES.filter((t) => getConfig(db, t).enabled), + emby_enabled: mediaProtocolEnabled(settings, "media_emby_enabled"), + subsonic_enabled: mediaProtocolEnabled(settings, "media_subsonic_enabled"), + emby_base_url: `${origin}/@emby`, + subsonic_base_url: `${origin}/@rest`, + proxy_cover: settingBool(settings, "media_proxy_cover", false), + strm_path: settings.media_strm_path || "", + strm_same_dir: settingBool(settings, "media_strm_same_dir", true), + strm_on_scan: settingBool(settings, "media_strm_on_scan", false), + thumb_path: settings.media_thumbnail_path || "/imgs", + cover_gateway: settingBool(settings, "media_cover_gateway", false), + }) +}) + +/** 允许代理的封面来源(与 Go 一致 + Google Books / OpenLibrary) */ +const COVER_HOSTS = [ + "image.tmdb.org", + "tmdb.org", + "doubanio.com", + "douban.com", + "discogs.com", + "openlibrary.org", + "googleusercontent.com", + "books.google.com", + "books.googleusercontent.com", +] + +/** 公共图片网关(仅 media_cover_gateway 开启时使用,原始 URL 仍受 COVER_HOSTS 约束) */ +const COVER_GATEWAY_HOSTS = ["wsrv.nl"] + +const coverHostAllowed = (host: string) => { + const h = host.toLowerCase().replace(/\.$/, "") + if (!h) return false + return COVER_HOSTS.some((x) => h === x || h.endsWith(`.${x}`)) +} + +/** 逐跳校验的封面拉取:禁止域名跳到内网 / 非图片内容 */ +async function fetchCoverSafely( + rawUrl: string, + maxRedirects = 4, +): Promise<{ data: Uint8Array; mime: string } | null> { + let current = rawUrl + for (let i = 0; i < maxRedirects; i++) { + let u: URL + try { + u = new URL(current) + } catch { + return null + } + if (!/^https?:$/.test(u.protocol)) return null + if (!coverHostAllowed(u.hostname)) return null + if (!isSafeUrl(current, COVER_HOSTS)) return null + + // 豆瓣图床靠 Referer 防盗链:空 Referer 直接 403,带站点 Referer 才放行 + const referer = /(?:^|\.)douban(?:io)?\.com$/i.test(u.hostname) + ? "https://www.douban.com/" + : "" + const res = await fetch(current, { + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122 Safari/537.36", + Accept: "image/*", + ...(referer ? { Referer: referer } : {}), + }, + redirect: "manual", + signal: AbortSignal.timeout(20_000), + }).catch(() => null) + if (!res) return null + if (res.status >= 300 && res.status < 400) { + const loc = res.headers.get("location") + if (!loc) return null + current = new URL(loc, current).toString() + continue + } + if (!res.ok) return null + const data = new Uint8Array(await res.arrayBuffer()) + if (data.length === 0 || data.length > 8 * 1024 * 1024) return null + const mime = sniffImageMime(data) + if (!mime) return null + return { data, mime } + } + return null +} + +/** + * 公共图片网关的封面拉取。 + * + * 网关域名(wsrv.nl)不在 COVER_HOSTS 里,直接复用 fetchCoverSafely 会被 + * 域名白名单拒绝,导致 media_cover_gateway 打开后仍然是 502。这里给网关 + * 单独的白名单,并且要求原始地址本身先通过可信封面源校验——否则网关会 + * 变成任意 URL 的反向代理。 + */ +async function fetchCoverFromGateway( + rawUrl: string, +): Promise<{ data: Uint8Array; mime: string } | null> { + let u: URL + try { + u = new URL(rawUrl) + } catch { + return null + } + if (!/^https?:$/.test(u.protocol)) return null + if (!COVER_GATEWAY_HOSTS.includes(u.hostname.toLowerCase())) return null + + const res = await fetch(rawUrl, { + headers: { "User-Agent": "OpenList-Media/1.0", Accept: "image/*" }, + redirect: "manual", + signal: AbortSignal.timeout(20_000), + }).catch(() => null) + if (!res || !res.ok) return null + const data = new Uint8Array(await res.arrayBuffer()) + if (data.length === 0 || data.length > 8 * 1024 * 1024) return null + const mime = sniffImageMime(data) + return mime ? { data, mime } : null +} + +mediaPublicRouter.get("/media/proxy", async (c) => { + const settings = await getSettingsMap(c.env) + if (!mediaEnabled(settings)) return c.body(null, 404) + const raw = c.req.query("url") || "" + if (!raw) return c.body(null, 400) + + let found = await fetchCoverSafely(raw) + // 直连失败时可选回退公共图片网关(默认关闭,涉及第三方)。 + // 原始地址必须先通过可信封面源校验,网关域名单独白名单, + // 否则 media_cover_gateway 打开后这个分支永远 502(wsrv.nl 不在 COVER_HOSTS)。 + if (!found && settingBool(settings, "media_cover_gateway", false)) { + let originalTrusted = false + try { + originalTrusted = isSafeUrl(raw, COVER_HOSTS) + } catch { + originalTrusted = false + } + if (originalTrusted) { + const gateway = `https://wsrv.nl/?url=${encodeURIComponent(raw.replace(/^https?:\/\//, ""))}` + found = await fetchCoverFromGateway(gateway) + } + } + if (!found) return c.body(null, 502) + + c.header("Content-Type", found.mime) + c.header("Cache-Control", "public, max-age=604800") + c.header("X-Content-Type-Options", "nosniff") + c.header("Content-Disposition", 'inline; filename="cover"') + c.header("Content-Security-Policy", "default-src 'none'; sandbox") + return c.body(found.data as any) +}) + +/** + * 本地缩略图读取。 + * + * Worker 侧缩略图存在持久化存储里(`thumb:`),因此这里接受两种参数: + * - path=(本实现) + * - path=/imgs/(Go 语义,Worker 无本地盘 → 404) + */ +mediaPublicRouter.get("/media/thumb", async (c) => { + const settings = await getSettingsMap(c.env) + if (!mediaEnabled(settings)) return c.body(null, 404) + const raw = String(c.req.query("path") || "").trim() + if (!raw) return c.body(null, 400) + const hash = raw.startsWith("thumb:") + ? raw.slice("thumb:".length) + : raw.replace(/^\/+/, "") + if (!/^[a-f0-9]{16,64}$/i.test(hash)) return c.body(null, 404) + const thumb = await readThumb(c.env, hash) + if (!thumb) return c.body(null, 404) + // 存量数据的 mime 可能来自旧版本(会采信文件内声明的 MIME),按魔数复核一遍 + const mime = sniffImageMime(thumb.bytes) + if (!mime) return c.body(null, 404) + c.header("Content-Type", mime) + c.header("Cache-Control", "public, max-age=604800") + c.header("X-Content-Type-Options", "nosniff") + c.header("Content-Disposition", 'inline; filename="thumb"') + c.header("Content-Security-Policy", "default-src 'none'; sandbox") + return c.body(thumb.bytes as any) +}) + +// ==================== 供 Emby / Subsonic 复用 ==================== + +export const mediaHelpers = { + itemPath, + canAccessItem, + serializeItem, + applyProgress, + sha256Hex, + randomToken, + appKeyPlainOf, + accessibleItems, + resolveCoverUrl, + thumbOptionsOf, + authLockedFor, + authFailed, + authSucceeded, + sniffImageMime, + ticksToSeconds, + extOf, + UserRole, + readFileRange, + putItem, +} diff --git a/src/backend/server/raw.ts b/src/backend/server/raw.ts index a3d4fbbc..9a1cbb86 100644 --- a/src/backend/server/raw.ts +++ b/src/backend/server/raw.ts @@ -285,10 +285,15 @@ async function proxyUpstream( "Content-Range, Accept-Ranges, Content-Length, Content-Disposition", ) - // Content-Type: prefer upstream, fallback by extension + // Content-Type:上游给的是通用 octet-stream(139/OSS 等常见)时按扩展名 + // 覆盖,否则 PDF 在浏览器里不会被当成文档、视频也拿不到 video/*。 + const upstreamType = upstreamRes.headers.get("content-type") || "" + const fallbackType = contentTypeForPath(reqPath) c.header( "Content-Type", - upstreamRes.headers.get("content-type") || contentTypeForPath(reqPath), + !upstreamType || /^application\/octet-stream/i.test(upstreamType) + ? fallbackType + : upstreamType, ) // Forward range/length headers @@ -310,9 +315,16 @@ async function proxyUpstream( // 清洗 CR/LF 与控制字符,防止恶意上游注入额外响应头(Set-Cookie/Location)。 const contentDisposition = upstreamRes.headers.get("content-disposition") if (contentDisposition) { + // 媒体库(scope=media)用于在线播放/阅读:上游通常给 attachment, + // 会把 PDF 变成下载、把视频变成附件,这里改为 inline。 + const inlineForMedia = (c.req.query("scope") || "") === "media" c.header( "Content-Disposition", - sanitizeContentDisposition(contentDisposition), + sanitizeContentDisposition( + inlineForMedia + ? contentDisposition.replace(/^\s*attachment/i, "inline") + : contentDisposition, + ), ) } @@ -478,8 +490,20 @@ rawRouter.get("/*", async (c) => { // /p 是公开代理端点,对齐 Go handles.Proxy() 的 canProxy() 检查: // 未开启代理、且扩展名也不在 proxy_types / text_types 里 → 403 proxy not allowed。 // (Go 只对 /p 与归档 /ap 做此限制,/d、/sd 不做,故这里仅判 /p 端点。) + // 媒体库签发的 /p(scope=media):签名本身覆盖了 scope,等价于 + // 「该用户已获授权读取这个文件」,因此不再受存储 web_proxy 限制。 + // 不满足时仍按原策略判定(公开 /p 端点防滥用)。 + const mediaScoped = + (c.req.query("scope") || "") === "media" && + (await verifyDownloadSign( + c, + reqPath, + c.req.query("sign") || "", + "media", + )) if ( isProxyEndpoint && + !mediaScoped && !canUseProxyEndpoint({ storage: resolved.storage, driver: normDriver, diff --git a/src/backend/server/router.ts b/src/backend/server/router.ts index 3b25236c..567bd558 100644 --- a/src/backend/server/router.ts +++ b/src/backend/server/router.ts @@ -2,7 +2,10 @@ import { Hono } from "hono" import type { Context } from "hono" import { cors } from "hono/cors" import { getDb, getKvStatus } from "../internal/model/db" -import { isServerlessRuntime, NO_STORAGE_MESSAGE } from "../internal/model/store/backend" +import { + isServerlessRuntime, + NO_STORAGE_MESSAGE, +} from "../internal/model/store/backend" import { fsRouter } from "./fs" import { authRouter, @@ -21,6 +24,12 @@ import { taskRouter } from "./task" import { ssoRouter } from "./sso" import { webauthnRouter } from "./webauthn" import { updatePwdHandler } from "./user" +import { + mediaAdminRouter, + mediaPublicRouter, + mediaRouter, + meAppkeyRouter, +} from "./media" // --- 尽力而为的进程内限流 --- // 实现管理后台的 ip_limit(每 IP 每分钟请求数)与 traffic_limit(每 IP 每小时 @@ -230,9 +239,7 @@ export function setupRouter(app: Hono) { const env = (c as any).env || {} const allowedOriginsRaw = env.ALLOW_URLS || - (typeof process !== "undefined" - ? process.env?.ALLOW_URLS - : "") || + (typeof process !== "undefined" ? process.env?.ALLOW_URLS : "") || "" const allowedOrigins = allowedOriginsRaw .split(",") @@ -259,7 +266,28 @@ export function setupRouter(app: Hono) { // API core sub-routing (mounted at /api by the parent) app.route("/raw", rawRouter) + // 媒体路由必须放在 /fs 与 /admin 之前,避免被通配路由抢先匹配 + app.route("/fs/media", mediaRouter) + app.route("/admin/media", mediaAdminRouter) + app.route("/public", mediaPublicRouter) + // 普通用户自助签发媒体应用密钥(对齐 Go 的 /api/me/appkey/*) + app.route("/auth/me/appkey", meAppkeyRouter) app.route("/fs", fsRouter) + // Worker 不内置 ffmpeg,也不允许开启转码: + // 返回 200 + supported:false 让前端平滑降级到直链(不弹错误), + // 同时在响应里明确声明能力不存在,便于客户端区分「降级」与「失败」。 + app.post("/fs/transcode/play", async (c) => + c.json({ + code: 200, + message: "success", + data: { + transcode: false, + supported: false, + reason: + "Transcoding is not available in the Worker runtime (no ffmpeg)", + }, + }), + ) app.route("/auth", authRouter) app.route("/public", publicRouter) app.route("/admin", adminRouter) From 6fd5daaf54e471c29eaa51d44633844de699c14d Mon Sep 17 00:00:00 2001 From: wumingzhinu <78893617+wumingzhinu@users.noreply.github.com> Date: Mon, 9 Nov 2026 14:34:41 +0800 Subject: [PATCH 37/49] feat(config): add keep_vars to preserve dashboard env vars on redeploy (#176) Fixes part of #174: variables configured in the Cloudflare dashboard are wiped on every redeploy because wrangler treats the repo config as the single source of truth. keep_vars=true makes wrangler stop deleting dashboard-configured plain-text vars that are not declared in wrangler.jsonc. Secrets are unaffected either way. Refs: https://developers.cloudflare.com/workers/wrangler/configuration/ --- wrangler.jsonc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wrangler.jsonc b/wrangler.jsonc index e16090ec..7e495779 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -28,6 +28,10 @@ "compatibility_date": "2026-08-04", "compatibility_flags": ["nodejs_compat"], "workers_dev": false, + + // 见 issue #174:保留控制台配置的普通环境变量,部署更新后不再被清空。 + // Secrets 本就不受影响;此开关仅作用于 vars。 + "keep_vars": true, "preview_urls": false, // ⚠️ run_worker_first 只放行 HTML 入口,不能改成 true(全量放行): From ce82869646c39ef47e04d63c4dae3313f221001c Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:03:15 +0800 Subject: [PATCH 38/49] fix(store): stay within D1's 100 bound-parameter limit when pruning rows (#184) The default config ships 114 settings, so sqlFormat.save() built a single `DELETE ... WHERE key NOT IN (?, ...)` with 114 bound parameters. D1 (and Durable Object SQLite) cap a statement at 100 bound parameters, so /api/public/init/setup failed with "D1_ERROR: too many SQL variables at offset 350" after the first 100 statements had already been committed (issue #180). - sqlFormat.save(): when a table keeps more than 100 keys, read the existing primary keys, diff them in memory, and delete the stale ones with chunked `IN (...)` statements (NOT IN cannot be split without changing semantics). - d1 driver: reject any statement binding more than 100 parameters up front with the offending SQL, instead of the opaque SQLITE_ERROR. - store tests: mock driver enforces the 100-parameter cap and covers the >100-key save path plus stale-row cleanup. Co-authored-by: pikachuren --- src/backend/internal/model/store/driver/d1.ts | 17 +++- .../internal/model/store/format/sql.ts | 68 +++++++++++-- .../internal/model/store/store.test.ts | 97 +++++++++++++++++++ 3 files changed, 175 insertions(+), 7 deletions(-) diff --git a/src/backend/internal/model/store/driver/d1.ts b/src/backend/internal/model/store/driver/d1.ts index 98420f3a..99d14339 100644 --- a/src/backend/internal/model/store/driver/d1.ts +++ b/src/backend/internal/model/store/driver/d1.ts @@ -149,11 +149,26 @@ export const d1Driver: Driver = { // 语义边界(重要,不要误以为这里提供整体事务):**片内原子,跨片不原子**。 // D1 没有跨 batch 调用的事务,因此语句数 >100 时,后续片失败**不会**回滚 // 已提交的片。要让整体操作可恢复,依赖两点: - // ① 上层 format/sql 生成的是幂等 UPSERT + 末位 DELETE NOT IN,整体重跑可收敛; + // ① 上层 format/sql 生成的是幂等 UPSERT + 末位删除残留行(小表用 + // DELETE NOT IN,大表按差集分片 DELETE IN),整体重跑可收敛; // ② 这里对**单片的瞬时失败**做有限重试,避免一次网络抖动就留下半成品。 // 重试仍失败时,错误信息带上「已提交片数」,供调用方判断是否需要整体重跑。 const BATCH = 100 const MAX_RETRY = 2 + + // 单条语句的绑定参数上限同样是 100(D1 limits: Maximum bound parameters + // per query)。超限时 SQLite 只回 "too many SQL variables at offset N", + // 无法定位到具体语句(issue #180 的排查成本就来自这里),因此提前拦下。 + statements.forEach((s, idx) => { + if (s.params.length > BATCH) { + throw new Error( + `D1 statement #${idx + 1} binds ${s.params.length} parameters, over the ` + + `platform limit of ${BATCH} per query; split it into multiple statements. ` + + `SQL: ${s.sql.slice(0, 160)}`, + ) + } + }) + const stmts = statements.map((s) => db.prepare(s.sql).bind(...s.params)) let committed = 0 diff --git a/src/backend/internal/model/store/format/sql.ts b/src/backend/internal/model/store/format/sql.ts index 6498bfce..a64f12e9 100644 --- a/src/backend/internal/model/store/format/sql.ts +++ b/src/backend/internal/model/store/format/sql.ts @@ -18,6 +18,19 @@ import { const INIT_MARK = "openlist_config" +/** + * 单条 SQL 语句允许的绑定参数上限。 + * + * Cloudflare D1 的「Maximum bound parameters per query」为 100(DO 的 SQLite + * 同一限制),超过即报 `D1_ERROR: too many SQL variables`。 + * + * 批量 UPSERT 每行一条语句(参数数 = 列数,最多 22),不会触发;真正会超限的 + * 只有本文件底部「删除本端已移除的行」用的 `NOT IN (?...)`:它必须把该表全部 + * 保留键塞进同一条语句,而默认配置里 settings 就有 114 条,首次初始化直接 500 + * (issue #180)。 + */ +const MAX_BIND_PARAMS = 100 + function quote(name: string): string { return "`" + name + "`" } @@ -69,6 +82,30 @@ function cols(table: TableName): string { return TABLES[table].columns.map((c) => quote(c.name)).join(", ") } +/** + * 读取某表当前的主键集合(统一字符串化,避免数字/字符串主键比较失真)。 + * + * 只在大表删除已移除行时使用:`NOT IN` 的分片在语义上不可行(把保留键拆成 + * 多片后,每片都会把其余片的保留行当成"已移除"删掉),因此改为先取现有主键、 + * 在内存里求差集,再按 `IN` 分片删除。 + */ +async function listKeys( + driver: Driver, + table: TableName, + keyCol: string, + env?: any, +): Promise { + if (!driver.query) return [] + const rows = await driver.query( + `SELECT ${quote(keyCol)} AS k FROM ${qn(table, env)}`, + [], + env, + ) + return (rows || []) + .map((r: any) => (r?.k === undefined || r?.k === null ? "" : String(r.k))) + .filter((k: string) => k !== "") +} + export const sqlFormat: FormatAdapter = { name: "sql", @@ -126,13 +163,32 @@ export const sqlFormat: FormatAdapter = { } // 删除本端已移除的行(keys 为空则整表清空,与原语义一致) - const delSql = - keepKeys.length > 0 - ? `DELETE FROM ${qn(table, env)} WHERE ${quote(keyCol)} NOT IN (${keepKeys + if (keepKeys.length === 0) { + statements.push({ sql: `DELETE FROM ${qn(table, env)}`, params: [] }) + } else if (keepKeys.length <= MAX_BIND_PARAMS || !driver.query) { + statements.push({ + sql: `DELETE FROM ${qn(table, env)} WHERE ${quote(keyCol)} NOT IN (${keepKeys + .map(() => "?") + .join(", ")})`, + params: keepKeys, + }) + } else { + // 保留键超过单语句参数上限(默认配置的 settings 就有 114 条): + // 直接 NOT IN 会撞 D1 的 100 个绑定参数上限,必须换成差集 + 分片 IN。 + const keep = new Set(keepKeys) + const stale = (await listKeys(driver, table, keyCol, env)).filter( + (k) => !keep.has(k), + ) + for (let i = 0; i < stale.length; i += MAX_BIND_PARAMS) { + const group = stale.slice(i, i + MAX_BIND_PARAMS) + statements.push({ + sql: `DELETE FROM ${qn(table, env)} WHERE ${quote(keyCol)} IN (${group .map(() => "?") - .join(", ")})` - : `DELETE FROM ${qn(table, env)}` - statements.push({ sql: delSql, params: keepKeys }) + .join(", ")})`, + params: group, + }) + } + } } // 标记已初始化(方言兼容的 UPSERT) diff --git a/src/backend/internal/model/store/store.test.ts b/src/backend/internal/model/store/store.test.ts index 9ee8cb9f..3b03ed79 100644 --- a/src/backend/internal/model/store/store.test.ts +++ b/src/backend/internal/model/store/store.test.ts @@ -6,6 +6,7 @@ import { readPersistedSecret, writePersistedSecret } from "./json" import { mapFormat } from "./format/map" import { keyFormat } from "./format/key" import { sqlFormat } from "./format/sql" +import { d1Driver } from "./driver/d1" import { TABLE_NAMES, DDL_TABLE_NAMES, @@ -70,6 +71,12 @@ function createMockSqlDriver(name = "mock-sql"): Driver { const k = String(params[0]) return schemaInfo.has(k) ? [{ v: schemaInfo.get(k) }] : [] } + // SELECT `key` AS k FROM `table`(差集删除时读取现有主键) + const keySel = trimmed.match(/^SELECT `?(\w+)`? AS k FROM `?(\w+)`?/i) + if (keySel) { + const [, col, table] = keySel + return ensureTable(table).map((r: any) => ({ k: r[col] })) + } // SELECT * FROM `table` const m = trimmed.match(/^SELECT \* FROM `?(\w+)`?/i) if (m) return ensureTable(m[1]).map((r) => ({ ...r })) @@ -83,6 +90,14 @@ function createMockSqlDriver(name = "mock-sql"): Driver { ): Promise { for (const { sql, params } of statements) { const trimmed = sql.trim() + // 模拟 D1 的单条语句绑定参数上限(limits: 100 per query)。 + // 越过它 D1 只会报 "too many SQL variables at offset N", + // 这里直接让测试失败,防止再生成超限语句(issue #180)。 + if (params.length > 100) { + throw new Error( + `mock-sql: too many SQL variables (${params.length} > 100): ${trimmed.slice(0, 120)}`, + ) + } // DELETE FROM `table`(整表清空) const delAll = trimmed.match(/^DELETE FROM `?(\w+)`?$/i) if (delAll) { @@ -103,6 +118,19 @@ function createMockSqlDriver(name = "mock-sql"): Driver { ) continue } + // DELETE FROM `table` WHERE `key` IN (?, ?, ...)(大表差集分片删除) + const delIn = trimmed.match( + /^DELETE FROM `?(\w+)`? WHERE `?(\w+)`? IN \(([^)]*)\)/i, + ) + if (delIn) { + const [, table, keyCol] = delIn + const drop = new Set(params.map((p) => String(p))) + tables.set( + table, + ensureTable(table).filter((r: any) => !drop.has(String(r[keyCol]))), + ) + continue + } // schema_info 的 UPSERT 必须先于通用 INSERT 匹配,否则会被当作数据行。 // SQLite 方言:INSERT OR REPLACE INTO `schema_info` (`k`, `v`) VALUES (?, ?) if (/^INSERT OR REPLACE INTO `?schema_info`?/i.test(trimmed)) { @@ -302,6 +330,75 @@ test("sql format: MySQL dialect uses ON DUPLICATE KEY UPDATE (no INSERT OR REPLA assert.deepEqual(await sqlFormat.load(driver), SAMPLE_DB) }) +test("sql format: 大表不使用超限的 NOT IN,改用差集分片删除(issue #180)", async () => { + // 回归:默认配置有 114 条 settings,旧实现把它们塞进一条 + // `DELETE ... WHERE key NOT IN (?, ×114)`,撞上 D1 的 + // "Maximum bound parameters per query = 100",导致 + // /api/public/init/setup 直接 500(too many SQL variables at offset 350)。 + // mock 驱动会在任何语句参数 >100 时抛错,因此旧实现必然失败。 + const driver = createMockSqlDriver() + const settings = Array.from({ length: 150 }, (_, i) => ({ + key: `s${i}`, + value: String(i), + })) + + const big = { ...SAMPLE_DB, settings } + assert.equal(await sqlFormat.save(big, driver), true) + assert.deepEqual(await sqlFormat.load(driver), big) + + // 删掉 50 条后再次保存:被移除的行必须真的从表里消失(差集语义不变) + const shrunk = { ...SAMPLE_DB, settings: settings.slice(0, 100) } + assert.equal(await sqlFormat.save(shrunk, driver), true) + assert.deepEqual(await sqlFormat.load(driver), shrunk) +}) + +test("d1 driver: 单条语句绑定参数 >100 时提前报错(而不是 SQLITE_ERROR)", async () => { + // 回归辅助:D1 对超限只回 "too many SQL variables at offset N", + // 无法定位是哪条语句;驱动层应提前抛出可读错误。 + const db = { + prepare(_sql: string) { + const stmt: any = { + bind() { + return stmt + }, + async run() { + return { success: true } + }, + async first() { + return null + }, + async all() { + return { results: [] } + }, + } + return stmt + }, + async batch() { + return [] + }, + } + + // 2 个参数的语句是合法的,应正常提交 + await d1Driver.batch!( + [{ sql: "DELETE FROM `x` WHERE `k` NOT IN (?, ?)", params: [1, 2] }], + { DB: db }, + ) + + await assert.rejects( + () => + d1Driver.batch!( + [ + { + sql: `DELETE FROM \`x\` WHERE \`k\` NOT IN (${Array(101).fill("?").join(", ")})`, + params: Array(101).fill(1), + }, + ], + { DB: db }, + ), + /over the platform limit of 100 per query/, + ) +}) + /** * 构造一个最小可用的 D1 绑定桩(只实现 kv 表相关的 prepare 语义)。 * From 2fdfa9cff471e0fe699ad3fcbe95d61d50daf7d6 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:27:07 +0800 Subject: [PATCH 39/49] refactor(cache)!: remove persistent caches and singleflight dedup, drop all CACHE_* / SINGLEFLIGHT* variables (#185) * refactor(cache): remove persistent file-tree/link caches and all CACHE_* config * refactor(storage): remove singleflight dedup and all SINGLEFLIGHT_* variables --------- Co-authored-by: pikachuren --- .dev.vars.example | 35 +- .env.example | 35 +- README.md | 5 +- package.json | 30 +- readmes/README_en.md | 5 +- src/backend/durable-objects/OpenListDB.ts | 14 +- src/backend/internal/cache/cache.test.ts | 454 ---------- src/backend/internal/cache/config.ts | 267 ------ src/backend/internal/cache/filetree.ts | 176 ---- src/backend/internal/cache/index.ts | 127 --- src/backend/internal/cache/link.ts | 138 --- src/backend/internal/cache/store.ts | 443 ---------- .../internal/driver/storageopts.test.ts | 208 +---- src/backend/internal/driver/storageopts.ts | 264 +----- src/backend/internal/model/store/driver/d1.ts | 14 +- .../internal/model/store/driver/mysql.ts | 10 +- src/backend/internal/model/store/schema.ts | 83 -- src/backend/internal/op/storage.ts | 137 +-- src/backend/pkg/singleflight.test.ts | 542 ------------ src/backend/pkg/singleflight.ts | 783 ------------------ src/backend/server/admin.ts | 150 +--- src/backend/server/debug.ts | 4 - src/backend/server/fs.ts | 70 +- src/backend/server/public.ts | 25 - src/backend/server/raw.ts | 45 +- .../server/storage_request_context.test.ts | 156 ---- wrangler.jsonc | 64 +- 27 files changed, 88 insertions(+), 4196 deletions(-) delete mode 100644 src/backend/internal/cache/cache.test.ts delete mode 100644 src/backend/internal/cache/config.ts delete mode 100644 src/backend/internal/cache/filetree.ts delete mode 100644 src/backend/internal/cache/index.ts delete mode 100644 src/backend/internal/cache/link.ts delete mode 100644 src/backend/internal/cache/store.ts delete mode 100644 src/backend/pkg/singleflight.test.ts delete mode 100644 src/backend/pkg/singleflight.ts delete mode 100644 src/backend/server/storage_request_context.test.ts diff --git a/.dev.vars.example b/.dev.vars.example index 633cab33..10733370 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -96,34 +96,7 @@ CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= -# ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── -# 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 -# 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER -# (不会自动探测、不会自动回退)。 -# 完整说明见:https://doc.oplist.org/ecosystem/official_worker/guide_env -# -# 总开关,默认 true -# CACHE_ENABLED=true -# 缓存后端,默认 db;可选 db / kv / blob / cfkv / do / memory / none, -# 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 db,kv -# CACHE_DRIVER=db -# 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 -# TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) -# CACHE_FILE_TREE=true -# CACHE_DOWNLOAD_LINK=false -# 文件树缓存时长(分钟),0 = 跟随存储级 cache_expiration(默认 30) -# CACHE_TTL=0 -# 下载链接缓存时长(分钟),直链自带有效期,取值需保守(默认 5) -# CACHE_LINK_TTL=5 -# 不参与缓存的驱动(纯本地计算型) -# CACHE_EXCLUDE_DRIVERS=virtual,alias,url_tree,strm,chunk -# 缓存键前缀(与业务数据键隔离;仅允许 [A-Za-z0-9_],非法时回退默认值) -# CACHE_PREFIX=openlist_cache - -# singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 -# 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 -# auto(默认)- 有 SQL 库用表,否则退回进程内(memory) -# db - 强制用表协调;协调层出错时自动降级,不影响业务 -# memory - 仅进程内合并(等价 Go 的进程内 singleflight) -# off - 关闭去重,每次调用都直接执行 -SINGLEFLIGHT=auto +# ── 持久缓存 / 并发去重均已移除 ───────────────────────────────────────────── +# 目录列表与下载直链每次都直接回源驱动,既不做跨请求缓存,也不合并并发相同 +# 调用。相关环境变量(CACHE_* / SINGLEFLIGHT*)已全部删除,残留同名变量不会 +# 再被读取。 diff --git a/.env.example b/.env.example index ea96b104..79b6082e 100644 --- a/.env.example +++ b/.env.example @@ -82,34 +82,7 @@ CF_KV_UUID= # 具有 KV 读写权限的 API Token CF_API_KEY= -# ── 缓存(文件树缓存 / 下载链接缓存)──────────────────────────────────────── -# 默认【只向数据库启用】:缓存与业务数据同后端(DB_DRIVER 解析结果)。 -# 想要改用 / 额外启用 KV、Blob 等专用后端,必须显式设置 CACHE_DRIVER -# (不会自动探测、不会自动回退)。 -# 完整说明见:https://doc.oplist.org/ecosystem/official_worker/guide_env -# -# 总开关,默认 true -# CACHE_ENABLED=true -# 缓存后端,默认 db;可选 db / kv / blob / cfkv / do / memory / none, -# 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 db,kv -# CACHE_DRIVER=db -# 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 -# TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) -# CACHE_FILE_TREE=true -# CACHE_DOWNLOAD_LINK=false -# 文件树缓存时长(分钟),0 = 跟随存储级 cache_expiration(默认 30) -# CACHE_TTL=0 -# 下载链接缓存时长(分钟),直链自带有效期,取值需保守(默认 5) -# CACHE_LINK_TTL=5 -# 不参与缓存的驱动(纯本地计算型) -# CACHE_EXCLUDE_DRIVERS=virtual,alias,url_tree,strm,chunk -# 缓存键前缀(与业务数据键隔离;仅允许 [A-Za-z0-9_],非法时回退默认值) -# CACHE_PREFIX=openlist_cache - -# singleflight(并发相同调用去重)。一个部署会同时存在多个 isolate,进程内去重 -# 无法跨实例生效,因此存在 SQL 库(d1 / mysql / do)时用表 x_singleflight 做协调。 -# auto(默认)- 有 SQL 库用表,否则退回进程内(memory) -# db - 强制用表协调;协调层出错时自动降级,不影响业务 -# memory - 仅进程内合并(等价 Go 的进程内 singleflight) -# off - 关闭去重,每次调用都直接执行 -SINGLEFLIGHT=auto +# ── 持久缓存 / 并发去重均已移除 ───────────────────────────────────────────── +# 目录列表与下载直链每次都直接回源驱动,既不做跨请求缓存,也不合并并发相同 +# 调用。相关环境变量(CACHE_* / SINGLEFLIGHT*)已全部删除,残留同名变量不会 +# 再被读取。 diff --git a/README.md b/README.md index b466ac7b..433df57a 100644 --- a/README.md +++ b/README.md @@ -166,12 +166,13 @@ pnpm run deploy:worker | `DB_FORMAT` | `map` | 数据存储格式:`map`(整对象 JSON)/ `key`(分 key)/ `sql`(关系表,与 Go 后端一致) | | `DB_DRIVER` | `auto` | 存储驱动:`auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`;显式指定后不做回退 | | `DB_CIPHER` | `none` | 敏感字段落盘算法(默认不加密),如 `aes-256-gcm` | -| `CACHE_ENABLED` / `CACHE_DRIVER` | `true` / `db` | 文件树缓存与下载链接缓存,默认复用业务数据后端 | | `ALLOW_URLS` | — | CORS 允许白名单(逗号分隔,不设则仅同源) | | `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | 整体上传 / 分片上传大小上限(字节) | | `ASSET_URLS` | — | 前端静态资源 CDN 地址,支持 `$version` 占位符 | -完整环境变量(含缓存 `CACHE_*`、字段加密 `DB_CIPHER`、并发去重 `SINGLEFLIGHT`、`cfkv` / MySQL 连接配置等)与各平台部署步骤,请参阅文档站: +目录列表与下载直链每次都直接回源驱动,没有跨请求缓存(无 `CACHE_*` 变量),外部改动立即可见。 + +完整环境变量(含字段加密 `DB_CIPHER`、`cfkv` / MySQL 连接配置等)与各平台部署步骤,请参阅文档站: - [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) - [部署教程](https://doc.oplist.org/ecosystem/official_worker/guide) diff --git a/package.json b/package.json index 21f2a00b..9f9b2b8e 100644 --- a/package.json +++ b/package.json @@ -22,33 +22,6 @@ }, "DB_CIPHER": { "description": "At-rest cipher for sensitive fields (drive credentials, 2FA secrets, password hashes): `none` (default, no encryption), `aes-256-gcm` (HKDF-derived AES-256-GCM key — the `enc:v2:` envelope written by existing encrypted deployments, recommended), `aes-256-gcm-pbkdf2` (legacy `enc:v1:` envelope, PBKDF2 per field, slow), `aes-256-cbc-hmac` (`enc:v3:`), `chacha20-poly1305` (`enc:v4:`, RFC 8439, pure-JS implementation), `des-cbc-hmac` / `3des-cbc-hmac` (`enc:v5:`/`enc:v6:`, compatibility only — single DES is 56-bit and 3DES is deprecated, do not use for real data). Ciphertexts carry an `enc:vN:` prefix and are decrypted by prefix on read, so switching back to `none` (or changing algorithms) never makes existing data unreadable — it only changes how newly written data is stored (legacy ciphertext is migrated to plaintext on the next save). Key derivation is cached per isolate, and unchanged fields are not re-encrypted on save. This setting does NOT affect the shared secret: with `none`, a secret is still generated and persisted during setup when JWT_SECRET is not provided, because JWT signing needs it." - }, - "CACHE_ENABLED": { - "description": "Optional. Master switch for the file-tree cache and the download-link cache. Defaults to `true`. Set to `false` to disable both." - }, - "CACHE_DRIVER": { - "description": "Optional. Where the caches are stored. Defaults to `db`, meaning the same backend as DB_DRIVER (D1/KV/Blob/MySQL) — i.e. only the database is used unless you opt in explicitly. Comma-separated list to add dedicated backends: `kv`, `blob`, `cfkv`, `do`, `memory`, or `none` to disable. Example: `db,kv` (read prefers KV) or `kv` (KV only). Dedicated backends are never auto-detected or auto-substituted." - }, - "CACHE_FILE_TREE": { - "description": "Optional. Toggle the file-tree cache (directory listings). Defaults to `true`. Listings are cached per storage+path; only admin writes invalidate them." - }, - "CACHE_DOWNLOAD_LINK": { - "description": "Optional. Toggle the download-link cache (presigned/raw download URLs returned by drivers). Defaults to `false`: some drives mint links with a very short TTL, so cached reuse can hand out already-expired links. Enable explicitly (`true`) once you confirmed your drive's link lifetime is long enough." - }, - "CACHE_TTL": { - "description": "Optional. File-tree cache lifetime in minutes. Defaults to `0`, which means 'follow the per-storage cache_expiration' (30 minutes, overridable per path by custom_cache_policies). Set to 0 to never cache a storage whose cache_expiration is 0." - }, - "CACHE_LINK_TTL": { - "description": "Optional. Download-link cache lifetime in minutes. Defaults to `5`. Keep it conservative: raw URLs usually carry their own expiry, and a stale link is served to the browser as 403/404." - }, - "CACHE_EXCLUDE_DRIVERS": { - "description": "Optional. Comma-separated drivers excluded from caching. Defaults to `virtual,alias,url_tree,strm,chunk` (locally computed drivers where caching only adds staleness)." - }, - "CACHE_PREFIX": { - "description": "Optional. Key prefix used to isolate cache entries from business data. Defaults to `openlist_cache`. Only [A-Za-z0-9_] (max 64 chars) are allowed; invalid values fall back to the default." - }, - "SINGLEFLIGHT": { - "description": "Dedup of concurrent identical calls (singleflight). `auto` (default) uses the `x_singleflight` table when a SQL backend (d1/mysql/do) is available, otherwise falls back to in-process merging. `db` forces table coordination, `memory` keeps it per-isolate, `off` disables dedup." } } }, @@ -82,12 +55,11 @@ "test:server": "tsx --test \"src/backend/server/*.test.ts\"", "test:store": "tsx --test src/backend/internal/model/store/store.test.ts", "test:model": "tsx --test \"src/backend/internal/model/*.test.ts\"", - "test:cache": "tsx --test \"src/backend/internal/cache/*.test.ts\"", "test:pkg": "tsx --test \"src/backend/pkg/*.test.ts\"", "test:regress": "tsx scripts/_regress.mjs", "test:deploy": "node scripts/test-deploy.js", "env:check": "tsx scripts/env-check.mjs", - "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:cache && npm run test:pkg && npm run test:regress", + "test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:pkg && npm run test:regress", "sls:deploy": "serverless deploy", "sls:package": "serverless package", "sls:info": "serverless info", diff --git a/readmes/README_en.md b/readmes/README_en.md index bf35d179..e7f67e76 100644 --- a/readmes/README_en.md +++ b/readmes/README_en.md @@ -166,12 +166,13 @@ All other variables have sensible defaults: | `DB_FORMAT` | `map` | Storage format: `map` (whole JSON) / `key` (per-key) / `sql` (relational, Go-compatible) | | `DB_DRIVER` | `auto` | Storage driver: `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`; an explicit driver is never replaced automatically | | `DB_CIPHER` | `none` | At-rest cipher for sensitive fields (no encryption by default), e.g. `aes-256-gcm` | -| `CACHE_ENABLED` / `CACHE_DRIVER` | `true` / `db` | File-tree & download-link caches, reusing the data backend by default | | `ALLOW_URLS` | — | Comma-separated CORS allowlist (same-origin only when unset) | | `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | Max size (bytes) for a whole upload / a multipart chunk | | `ASSET_URLS` | — | Frontend asset CDN base URL, supports the `$version` placeholder | -For the full variable list (cache `CACHE_*`, field encryption `DB_CIPHER`, concurrency dedup `SINGLEFLIGHT`, `cfkv` / MySQL settings, …) and per-platform deployment steps, see the documentation site: +Directory listings and download links are always fetched from the drive — there is no cross-request cache (no `CACHE_*` variables), so external changes are visible immediately. + +For the full variable list (field encryption `DB_CIPHER`, `cfkv` / MySQL settings, …) and per-platform deployment steps, see the documentation site: - [Environment Variables](https://doc.oplist.org/ecosystem/official_worker/guide_env) - [Deployment](https://doc.oplist.org/ecosystem/official_worker/guide) diff --git a/src/backend/durable-objects/OpenListDB.ts b/src/backend/durable-objects/OpenListDB.ts index f81d3d0e..a6e2d614 100644 --- a/src/backend/durable-objects/OpenListDB.ts +++ b/src/backend/durable-objects/OpenListDB.ts @@ -13,11 +13,7 @@ * 通过 RPC 调用(stub.kvGet / sqlQuery 等),每个实例用 `idFromName` 定位到 * 固定实例,保证数据持久在同一 DO 实例。 */ -import { - buildSingleFlightDdl, - D1_SCHEMA, - KV_SCHEMA_SQLITE, -} from "../internal/model/store/schema" +import { D1_SCHEMA, KV_SCHEMA_SQLITE } from "../internal/model/store/schema" export class OpenListDB { private state: any @@ -32,14 +28,10 @@ export class OpenListDB { } /** - * 建表(幂等):KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表。 + * 建表(幂等):KV 表(map/key 格式)+ 列式表(sql 格式)。 */ private ensureSchema(): void { - for (const ddl of [ - ...KV_SCHEMA_SQLITE, - ...D1_SCHEMA, - ...buildSingleFlightDdl("sqlite"), - ]) { + for (const ddl of [...KV_SCHEMA_SQLITE, ...D1_SCHEMA]) { this.sql.exec(ddl) } } diff --git a/src/backend/internal/cache/cache.test.ts b/src/backend/internal/cache/cache.test.ts deleted file mode 100644 index 771689b4..00000000 --- a/src/backend/internal/cache/cache.test.ts +++ /dev/null @@ -1,454 +0,0 @@ -import assert from "node:assert/strict" -import { test } from "node:test" - -/** - * 缓存模块回归测试(文件树缓存 / 下载链接缓存)。 - * - * 锁定以下契约: - * 1. 默认只向数据库启用(CACHE_DRIVER 缺省 → `db`);KV/Blob 等专用后端 - * 必须显式配置,未配置时绝不启用; - * 2. 开关语义:CACHE_ENABLED / CACHE_FILE_TREE / CACHE_DOWNLOAD_LINK; - * 3. 键名对 KV 合法(EdgeOne 只接受 [A-Za-z0-9_]),且超长路径被确定性压缩; - * 4. 文件树缓存命中时返回副本(避免调用方就地修改污染缓存); - * 5. 过期条目视为未命中; - * 6. 下载链接缓存只缓存「确实拿到直链的文件」,目录 / 空直链不缓存; - * 7. clearCache 能按存储 id 过滤,避免 storages_1_ 误伤 storages_10_。 - * - * 测试通过 __setCacheDriversForTest 注入内存假驱动,完全不依赖真实存储后端。 - */ - -const mod = await import("./index") -const { - buildCacheKey, - cacheGetEnvelope, - cacheSetEnvelope, - clearCache, - encodeCachePathSegment, - expandToInvalidationPaths, - getCacheConfig, - getCachedFileTree, - getCachedLink, - parseCacheBackends, - parseCachePrefix, - setCachedFileTree, - setCachedLink, - storageIdFromCacheKey, - __resetCacheConfigForTest, - __resetCacheStoreForTest, - __setCacheDriversForTest, -} = mod as any - -/** 内存假驱动(满足 Driver 接口的键值部分)。 */ -function createFakeDriver(name = "fake") { - const store = new Map() - return { - name, - __store: store, - async isAvailable() { - return true - }, - async init() {}, - async get(key: string) { - return store.get(key) ?? null - }, - async put(key: string, value: string) { - store.set(key, value) - }, - async delete(key: string) { - store.delete(key) - }, - async list(prefix: string) { - return [...store.keys()].filter((k) => k.startsWith(prefix)) - }, - async health() { - return { connected: true } - }, - } -} - -function setup(driver: any = createFakeDriver()) { - __resetCacheConfigForTest() - __resetCacheStoreForTest() - __setCacheDriversForTest(async () => [driver]) - return driver -} - -const SAMPLE_ITEMS = [ - { name: "a.txt", size: 1, is_dir: false, modified: "2026-01-01", sign: "", type: 0 }, - { name: "sub", size: 0, is_dir: true, modified: "2026-01-01", sign: "", type: 1 }, -] - -// ───────────────────────── 配置 ───────────────────────── - -test("配置:默认只向数据库启用", () => { - __resetCacheConfigForTest() - const cfg = getCacheConfig({}) - assert.equal(cfg.enabled, true) - assert.deepEqual(cfg.backends, ["db"]) - assert.equal(cfg.fileTree, true) - // 下载链接缓存默认关闭:部分网盘直链 TTL 极短,复用易发出失效直链 - assert.equal(cfg.downloadLink, false) - // 默认 TTL:文件树跟随存储级 cache_expiration(0 = 不覆盖) - assert.equal(cfg.ttlMinutes, 0) - assert.equal(cfg.linkTtlMinutes, 5) -}) - -test("配置:CACHE_DRIVER 解析(db / kv / 多后端 / none / 未知值)", () => { - assert.deepEqual(parseCacheBackends(""), ["db"]) - assert.deepEqual(parseCacheBackends("kv"), ["kv"]) - assert.deepEqual(parseCacheBackends("db,kv"), ["db", "kv"]) - assert.deepEqual(parseCacheBackends("KV , BLOB"), ["kv", "blob"]) - assert.deepEqual(parseCacheBackends("none"), []) - // 未知值被忽略,且不会把列表清空(回退 db) - assert.deepEqual(parseCacheBackends("nope"), ["db"]) - assert.deepEqual(parseCacheBackends("nope,kv"), ["kv"]) -}) - -test("配置:开关与 TTL 读取", () => { - __resetCacheConfigForTest() - const cfg = getCacheConfig({ - CACHE_ENABLED: "false", - CACHE_FILE_TREE: "0", - CACHE_DOWNLOAD_LINK: "off", - CACHE_TTL: "12", - CACHE_LINK_TTL: "1", - CACHE_DRIVER: "db,kv", - }) - assert.equal(cfg.enabled, false) - assert.equal(cfg.fileTree, false) - assert.equal(cfg.downloadLink, false) - assert.equal(cfg.ttlMinutes, 12) - assert.equal(cfg.linkTtlMinutes, 1) - assert.deepEqual(cfg.backends, ["db", "kv"]) -}) - -test("配置:CACHE_DRIVER=none 时整体关闭", () => { - __resetCacheConfigForTest() - const cfg = getCacheConfig({ CACHE_DRIVER: "none" }) - assert.equal(cfg.enabled, false) - assert.deepEqual(cfg.backends, []) -}) - -test("配置:CACHE_PREFIX 字符集校验(非法回退默认值)", () => { - // 合法:字母数字下划线,最长 64 - assert.equal(parseCachePrefix(""), "openlist_cache") - assert.equal(parseCachePrefix(" my_cache_2 "), "my_cache_2") - assert.equal(parseCachePrefix("a".repeat(64)), "a".repeat(64)) - // 非法:空格 / 斜杠 / 冒号 / 连字符 / 超长 —— 一律回退默认值 - assert.equal(parseCachePrefix("my cache"), "openlist_cache") - assert.equal(parseCachePrefix("cache/v2"), "openlist_cache") - assert.equal(parseCachePrefix("cache:v2"), "openlist_cache") - assert.equal(parseCachePrefix("my-cache"), "openlist_cache") - assert.equal(parseCachePrefix("a".repeat(65)), "openlist_cache") -}) - -test("配置:默认排除纯本地计算型驱动", () => { - __resetCacheConfigForTest() - const cfg = getCacheConfig({}) - assert.ok(cfg.excludeDrivers.has("virtual")) - assert.ok(cfg.excludeDrivers.has("alias")) - assert.ok(cfg.excludeDrivers.has("urltree")) -}) - -// ───────────────────────── 键名编码 ───────────────────────── - -test("键名:KV 合法字符集 + 存储 id 可解析", () => { - const key = buildCacheKey("ft", 1, "/a b/c%d.txt", "openlist_cache") - assert.match(key, /^[A-Za-z0-9_]+$/, "键名必须只含 [A-Za-z0-9_]") - assert.equal(storageIdFromCacheKey(key, "ft", "openlist_cache"), "1") - // 同输入必得同键(失效时才能算出一致的键) - assert.equal(key, buildCacheKey("ft", 1, "/a b/c%d.txt", "openlist_cache")) -}) - -test("键名:storageId 解析不受前缀内下划线影响", () => { - const key = buildCacheKey("ln", 42, "/x", "my_cache_prefix") - assert.equal(storageIdFromCacheKey(key, "ln", "my_cache_prefix"), "42") - // kind 不同不会串味 - assert.equal(storageIdFromCacheKey(key, "ft", "my_cache_prefix"), null) -}) - -test("键名:超长路径被确定性压缩且仍在合法字符集内", () => { - const long = "/" + Array.from({ length: 60 }, (_, i) => `dir${i}`).join("/") - const a = encodeCachePathSegment(long) - const b = encodeCachePathSegment(long) - assert.equal(a, b) - assert.ok(a.length <= 180, "长路径应被压缩") - assert.match(a, /^[A-Za-z0-9_]+$/) - // 不同长路径不应碰撞 - assert.notEqual(a, encodeCachePathSegment(long + "x")) -}) - -test("键名:编码单射 —— 任意不同路径的键互不相等(键碰撞回归)", () => { - // 评审实测复现的碰撞对:转义前缀 x 未转义时,/a/b 与 /ax2fb 曾得到同一个键, - // 导致按 /ax2fb 的权限判定返回 /a/b 的内容(跨目录串列表 / 直链) - assert.notEqual( - encodeCachePathSegment("/a/b"), - encodeCachePathSegment("/ax2fb"), - "/a/b 与 /ax2fb 不得碰撞", - ) - assert.notEqual( - encodeCachePathSegment("/a/b.txt"), - encodeCachePathSegment("/ax2fb.txt"), - ) - - // 参数化:覆盖转义前缀字面量、转义序列字面量、分隔符、大小写、unicode、 - // 普通编码与长路径折叠分支的交叉 - const long = "/" + Array.from({ length: 60 }, (_, i) => `dir${i}`).join("/") - const longAlt = "/" + Array.from({ length: 59 }, (_, i) => `dir${i}`).concat("dir58x").join("/") - const paths = [ - "/", - "/a", - "/a/b", - "/ax2fb", - "/a/b.txt", - "/ax2fb.txt", - "/x/y", - "/xx2fy", - "/secret/report.pdf", - "/secretx2freport.pdf", - "x", - "/x2f", - "/x78", - "/xx", - "/X/x", // 大写 X 无歧义,原样保留 - "/照片/2024/img.jpg", - "/a b/c%d.txt", - long, - longAlt, // 与 long 折叠后前 100 字符相同,仅靠哈希区分 - long + "x", - ] - const keys = paths.map((p) => encodeCachePathSegment(p)) - - // 全部在 KV 合法字符集内 - for (const k of keys) { - assert.match(k, /^[A-Za-z0-9_]+$/, `键不在合法字符集内: ${k}`) - } - // 两两互不相等(O(n²),n=20) - for (let i = 0; i < paths.length; i++) { - for (let j = i + 1; j < paths.length; j++) { - assert.notEqual( - keys[i], - keys[j], - `缓存键碰撞: "${paths[i]}" vs "${paths[j]}"`, - ) - } - } -}) - -test("失效路径展开:包含父目录", () => { - assert.deepEqual(expandToInvalidationPaths("/a/b/c.txt").sort(), ["/a/b", "/a/b/c.txt"]) - assert.deepEqual(expandToInvalidationPaths("/x").sort(), ["/", "/x"]) -}) - -// ───────────────────────── 存储层 ───────────────────────── - -test("存储层:写入后可读出,过期条目视为未命中并被清理", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db" } - - await cacheSetEnvelope("k1", { hello: "world" }, 60_000, env) - const hit = await cacheGetEnvelope("k1", env) - assert.deepEqual(hit?.v, { hello: "world" }) - - // 手工写入一条已过期条目 - await driver.put("k2", JSON.stringify({ v: 1, ts: 1, exp: Date.now() - 1000 })) - assert.equal(await cacheGetEnvelope("k2", env), null) - // 惰性清理是 fire-and-forget,等一个 tick 再断言 - await new Promise((r) => setTimeout(r, 0)) - assert.equal(await driver.get("k2"), null, "过期条目应被惰性清理") -}) - -test("存储层:ttl<=0 时不写入", async () => { - const driver = setup() - await cacheSetEnvelope("k", 1, 0, {}) - assert.equal(await driver.get("k"), null) -}) - -// ───────────────────────── 文件树缓存 ───────────────────────── - -test("文件树缓存:命中时返回深拷贝副本", async () => { - setup() - const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } - const target = { storage: { id: 1, driver: "fake" }, cleanPath: "/a" } - - await setCachedFileTree(target, SAMPLE_ITEMS as any, env) - const hit = await getCachedFileTree(target, env) - assert.ok(hit) - assert.equal(hit!.length, 2) - assert.equal(hit![0].name, "a.txt") - - // 必须是副本:就地修改不能回写缓存 - assert.notEqual(hit, SAMPLE_ITEMS) - assert.notEqual(hit![0], SAMPLE_ITEMS[0]) - hit![0].name = "MUTATED" - const again = await getCachedFileTree(target, env) - assert.equal(again![0].name, "a.txt") -}) - -test("文件树缓存:空目录也会被缓存(避免反复击穿存储)", async () => { - setup() - const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } - const target = { storage: { id: 7, driver: "fake" }, cleanPath: "/empty" } - await setCachedFileTree(target, [], env) - const hit = await getCachedFileTree(target, env) - assert.ok(Array.isArray(hit)) - assert.equal(hit!.length, 0) -}) - -test("文件树缓存:cache_expiration=0(不缓存)时读写都被跳过", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db" } - const target = { - storage: { id: 2, driver: "fake", cache_expiration: 0 }, - cleanPath: "/nocache", - } - await setCachedFileTree(target, SAMPLE_ITEMS as any, env) - assert.equal(await getCachedFileTree(target, env), null) - assert.equal(driver.__store.size, 0, "不应产生任何缓存键") -}) - -test("文件树缓存:排除名单中的驱动不缓存", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db", CACHE_TTL: "30" } - const target = { storage: { id: 3, driver: "virtual" }, cleanPath: "/v" } - await setCachedFileTree(target, SAMPLE_ITEMS as any, env) - assert.equal(await getCachedFileTree(target, env), null) - assert.equal(driver.__store.size, 0) -}) - -test("文件树缓存:CACHE_FILE_TREE=false 时关闭", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db", CACHE_FILE_TREE: "false", CACHE_TTL: "30" } - const target = { storage: { id: 4, driver: "fake" }, cleanPath: "/a" } - await setCachedFileTree(target, SAMPLE_ITEMS as any, env) - assert.equal(await getCachedFileTree(target, env), null) - assert.equal(driver.__store.size, 0) -}) - -test("文件树缓存:custom_cache_policies 命中 0 分钟即不缓存", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db" } - const target = { - storage: { - id: 5, - driver: "fake", - cache_expiration: 30, - custom_cache_policies: JSON.stringify([ - { path: "/private/*", cache_expiration: 0 }, - ]), - }, - cleanPath: "/private/x", - } - await setCachedFileTree(target, SAMPLE_ITEMS as any, env) - assert.equal(await getCachedFileTree(target, env), null) - assert.equal(driver.__store.size, 0) -}) - -// ───────────────────────── 下载链接缓存 ───────────────────────── - -const FILE_ITEM = { - name: "movie.mp4", - size: 100, - is_dir: false, - modified: "2026-01-01", - sign: "", - type: 2, - raw_url: "https://cdn.example.com/movie.mp4?sig=abc", -} - -test("下载链接缓存:缓存拿到直链的文件,并返回副本", async () => { - setup() - // 链接缓存默认关闭(直链 TTL 风险),用例显式开启 - const env = { CACHE_DRIVER: "db", CACHE_DOWNLOAD_LINK: "true" } - const target = { storage: { id: 10, driver: "fake" }, cleanPath: "/movie.mp4" } - - await setCachedLink(target, FILE_ITEM as any, env) - const hit = await getCachedLink(target, env) - assert.ok(hit) - assert.equal(hit!.raw_url, FILE_ITEM.raw_url) - assert.notEqual(hit, FILE_ITEM) - - hit!.raw_url = "MUTATED" - const again = await getCachedLink(target, env) - assert.equal(again!.raw_url, FILE_ITEM.raw_url) -}) - -test("下载链接缓存:目录 / 空直链不缓存", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db" } - const dirTarget = { storage: { id: 11, driver: "fake" }, cleanPath: "/dir" } - const emptyTarget = { storage: { id: 11, driver: "fake" }, cleanPath: "/empty" } - - await setCachedLink(dirTarget, { ...FILE_ITEM, is_dir: true } as any, env) - await setCachedLink(emptyTarget, { ...FILE_ITEM, raw_url: "" } as any, env) - - assert.equal(await getCachedLink(dirTarget, env), null) - assert.equal(await getCachedLink(emptyTarget, env), null) - assert.equal(driver.__store.size, 0) -}) - -test("下载链接缓存:CACHE_LINK_TTL=0 时关闭", async () => { - const driver = setup() - const env = { CACHE_DRIVER: "db", CACHE_LINK_TTL: "0" } - const target = { storage: { id: 12, driver: "fake" }, cleanPath: "/movie.mp4" } - await setCachedLink(target, FILE_ITEM as any, env) - assert.equal(await getCachedLink(target, env), null) - assert.equal(driver.__store.size, 0) -}) - -// ───────────────────────── 清理 ───────────────────────── - -test("clearCache:按存储 id 过滤,不误伤相邻 id", async () => { - setup() - const env = { CACHE_DRIVER: "db", CACHE_TTL: "30", CACHE_DOWNLOAD_LINK: "true" } - await setCachedFileTree( - { storage: { id: 1, driver: "fake" }, cleanPath: "/a" }, - SAMPLE_ITEMS as any, - env, - ) - await setCachedFileTree( - { storage: { id: 10, driver: "fake" }, cleanPath: "/a" }, - SAMPLE_ITEMS as any, - env, - ) - await setCachedLink( - { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, - FILE_ITEM as any, - env, - ) - - const removed = await clearCache(env, { storageId: 1 }) - assert.equal(removed, 2, "只应清掉 storage 1 的两条缓存") - - // storage 10 的文件树缓存仍在 - const survivor = await getCachedFileTree( - { storage: { id: 10, driver: "fake" }, cleanPath: "/a" }, - env, - ) - assert.ok(survivor) - - const rest = await clearCache(env, {}) - assert.equal(rest, 1) -}) - -test("clearCache:按 kind 只清一类", async () => { - setup() - const env = { CACHE_DRIVER: "db", CACHE_TTL: "30", CACHE_DOWNLOAD_LINK: "true" } - await setCachedFileTree( - { storage: { id: 1, driver: "fake" }, cleanPath: "/a" }, - SAMPLE_ITEMS as any, - env, - ) - await setCachedLink( - { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, - FILE_ITEM as any, - env, - ) - assert.equal(await clearCache(env, { kind: "ft" }), 1) - assert.ok( - await getCachedLink( - { storage: { id: 1, driver: "fake" }, cleanPath: "/movie.mp4" }, - env, - ), - ) -}) diff --git a/src/backend/internal/cache/config.ts b/src/backend/internal/cache/config.ts deleted file mode 100644 index 39a77ce6..00000000 --- a/src/backend/internal/cache/config.ts +++ /dev/null @@ -1,267 +0,0 @@ -/** - * 缓存配置(文件树缓存 / 下载链接缓存)—— 完全由环境变量驱动。 - * - * 设计目标(对齐参考实现 OpenList.ts 的两级缓存): - * 1. **文件树缓存**:`/fs/list`、`/fs/get`、`/fs/dirs` 优先命中缓存, - * 避免每次浏览都去请求网盘 / 对象存储; - * 2. **下载链接缓存**:`/d`、`/p` 等下载链路把驱动返回的直链缓存起来, - * 避免重复签名 / 重复换取临时链接。 - * - * 默认行为(**只向数据库启用**): - * 缓存内容与业务数据走同一条存储链路(`getStorageBackend()` 解析出的 - * driver,即 DB_DRIVER 指向的 d1 / mysql / kv / cfkv / blob / do), - * 因此「什么都不配」就等于「用数据库做缓存」。 - * - * 想要改用 / 额外启用 KV、Blob 等专用后端时,**必须显式添加环境变量** - * CACHE_DRIVER=kv # 只用 KV 缓存 - * CACHE_DRIVER=db,kv # 数据库 + KV 双写(读优先命中 KV) - * CACHE_DRIVER=blob,cfkv # 多个专用后端 - * 未显式配置的专用后端绝不会被自动启用,避免「以为缓存在 KV、实际写进了 - * 数据库」这类不可见行为(与 DB_DRIVER 的「显式配置不回退」哲学一致)。 - * - * 支持的环境变量一览: - * - * | 变量 | 默认值 | 说明 | - * | :-- | :-- | :-- | - * | `CACHE_ENABLED` | `true` | 总开关,`false` 时两级缓存全部关闭 | - * | `CACHE_DRIVER` | `db` | 缓存后端列表,逗号分隔:`db` / `kv` / `blob` / `cfkv` / `do` / `memory` / `none` | - * | `CACHE_FILE_TREE` | `true` | 文件树缓存开关 | - * | `CACHE_DOWNLOAD_LINK` | `false` | 下载链接缓存开关。默认**关闭**:部分网盘的直链 TTL 很短(分钟级甚至更短),缓存复用容易把已失效的直链发给用户;确认所用网盘直链有效期足够长后再显式开启 | - * | `CACHE_TTL` | `0` | 文件树缓存时长(分钟);`0` = 跟随存储级 `cache_expiration`(默认 30) | - * | `CACHE_LINK_TTL` | `5` | 下载链接缓存时长(分钟);直链通常带有效期,不宜过长 | - * | `CACHE_EXCLUDE_DRIVERS` | `virtual,alias,url_tree,strm,chunk` | 不参与缓存的驱动(本地计算型,缓存只会带来陈旧) | - * | `CACHE_PREFIX` | `openlist_cache` | 缓存键前缀,用于与业务数据键隔离;仅允许 `[A-Za-z0-9_]`,非法时回退默认值 | - */ - -/** 可用的缓存后端名。`db` = 与业务数据同一个后端(默认)。 */ -export type CacheBackendName = - | "db" - | "kv" - | "blob" - | "cfkv" - | "do" - | "memory" - | "none" - -/** 合法后端名(`none` 用于显式关闭) */ -export const VALID_CACHE_BACKENDS: readonly CacheBackendName[] = [ - "db", - "kv", - "blob", - "cfkv", - "do", - "memory", - "none", -] as const - -/** - * 默认排除的驱动:这些驱动不产生远程 IO(纯本地计算/别名映射), - * 缓存不但没有收益,还会因为它们的输入(如 url_structure)变更而变陈旧。 - * 可通过 `CACHE_EXCLUDE_DRIVERS` 覆盖(传空字符串表示不排除任何驱动)。 - */ -const DEFAULT_EXCLUDE_DRIVERS = ["virtual", "alias", "url_tree", "strm", "chunk"] - -/** 存储级 cache_expiration 缺省值(与 driver/storageopts.ts 保持一致) */ -export const DEFAULT_FILE_TREE_TTL_MINUTES = 30 - -/** 下载直链的默认缓存时长(分钟):直链通常带签名有效期,取值需保守 */ -export const DEFAULT_LINK_TTL_MINUTES = 5 - -/** 已解析的缓存配置 */ -export interface CacheConfig { - /** 总开关 */ - enabled: boolean - /** 缓存后端列表(已去掉 `none`;为空表示关闭) */ - backends: CacheBackendName[] - /** 文件树缓存开关 */ - fileTree: boolean - /** 下载链接缓存开关 */ - downloadLink: boolean - /** 文件树缓存时长(分钟);`0` 表示跟随存储级配置 */ - ttlMinutes: number - /** 下载链接缓存时长(分钟) */ - linkTtlMinutes: number - /** 不参与缓存的驱动(小写、去非字母数字) */ - excludeDrivers: Set - /** 缓存键前缀 */ - prefix: string -} - -/** 读取环境变量(同时兼容注入的 env 对象与 process.env)。 */ -function readRaw(key: string, env?: any): string { - const e = env && typeof env === "object" ? env : {} - const fromEnv = e[key] - if (fromEnv !== undefined && fromEnv !== null && String(fromEnv) !== "") { - return String(fromEnv) - } - if (typeof process !== "undefined" && process.env) { - const fromProcess = (process.env as any)[key] - if (fromProcess !== undefined && fromProcess !== null) { - return String(fromProcess) - } - } - return "" -} - -/** 解析布尔型环境变量(无法识别时回退默认值)。 */ -function readBool(key: string, fallback: boolean, env?: any): boolean { - const raw = readRaw(key, env).trim().toLowerCase() - if (!raw) return fallback - if (["1", "true", "yes", "on", "enable", "enabled"].includes(raw)) return true - if (["0", "false", "no", "off", "disable", "disabled"].includes(raw)) return false - return fallback -} - -/** 解析非负整数型环境变量(无法识别时回退默认值)。 */ -function readInt(key: string, fallback: number, env?: any): number { - const raw = readRaw(key, env).trim() - if (!raw) return fallback - const n = parseInt(raw, 10) - if (!Number.isFinite(n) || n < 0) return fallback - return n -} - -/** 驱动名归一化(与 storage.ts 的 normDriver 规则一致)。 */ -export function normalizeDriverName(name: string): string { - return String(name || "") - .toLowerCase() - .replace(/[^a-z0-9]/g, "") -} - -/** - * 解析 `CACHE_DRIVER`。 - * - * 规则: - * - 缺省 / 空 → `["db"]`(默认只向数据库启用); - * - 出现 `none` → 返回空数组(显式关闭); - * - 无法识别的取值会被忽略并告警一次(不静默改写成别的后端)。 - */ -export function parseCacheBackends(raw: string): CacheBackendName[] { - const tokens = String(raw || "") - .split(",") - .map((s) => s.trim().toLowerCase()) - .filter(Boolean) - if (tokens.length === 0) return ["db"] - if (tokens.includes("none")) return [] - - const out: CacheBackendName[] = [] - const unknown: string[] = [] - for (const token of tokens) { - const norm = normalizeDriverName(token) - const match = VALID_CACHE_BACKENDS.find( - (b) => b !== "none" && normalizeDriverName(b) === norm, - ) - if (match) { - if (!out.includes(match)) out.push(match) - } else { - unknown.push(token) - } - } - if (unknown.length > 0) { - console.warn( - `[Cache] Unknown CACHE_DRIVER value(s): ${unknown.join(", ")}. ` + - `Valid values: ${VALID_CACHE_BACKENDS.join(", ")}. They were ignored.`, - ) - } - return out.length > 0 ? out : ["db"] -} - -/** 解析 `CACHE_EXCLUDE_DRIVERS`(默认排除纯本地计算型驱动)。 */ -export function parseExcludeDrivers(raw: string): Set { - const source = String(raw || "").trim() - const tokens = (source === "" ? DEFAULT_EXCLUDE_DRIVERS : source.split(",")) - .map((s) => normalizeDriverName(s)) - .filter(Boolean) - return new Set(tokens) -} - -/** 缓存键前缀默认值。 */ -const DEFAULT_CACHE_PREFIX = "openlist_cache" - -/** - * 解析 `CACHE_PREFIX`(缓存键前缀)。 - * - * 前缀会成为 KV 键的一部分,因此只允许 `[A-Za-z0-9_]`(EdgeOne KV 的键名 - * 约束):含其他字符时所有缓存写入都会失败。另外前缀与业务数据的 KV 键 - * (`_<主键>`)落在同一个键空间,若放纵任意字符(如空格、`/`、`:`), - * 既可能与表名段混淆、又可能让 `sqlFormat.save()` 的整表 DELETE 波及缓存行。 - * 非法取值一律回退默认值并告警。 - */ -export function parseCachePrefix(raw: string): string { - const value = String(raw || "").trim() - if (!value) return DEFAULT_CACHE_PREFIX - if (/^[A-Za-z0-9_]{1,64}$/.test(value)) return value - console.warn( - `[Cache] Invalid CACHE_PREFIX "${value}": only [A-Za-z0-9_] (max 64 chars) ` + - `are allowed. Falling back to "${DEFAULT_CACHE_PREFIX}".`, - ) - return DEFAULT_CACHE_PREFIX -} - -/** 计算用于「配置缓存」的指纹(配置一变即失效)。 */ -function configSignature(env?: any): string { - return [ - readRaw("CACHE_ENABLED", env), - readRaw("CACHE_DRIVER", env), - readRaw("CACHE_FILE_TREE", env), - readRaw("CACHE_DOWNLOAD_LINK", env), - readRaw("CACHE_TTL", env), - readRaw("CACHE_LINK_TTL", env), - readRaw("CACHE_EXCLUDE_DRIVERS", env), - readRaw("CACHE_PREFIX", env), - ].join("\u0001") -} - -let cachedConfig: { sig: string; config: CacheConfig } | null = null - -/** - * 读取缓存配置(按环境变量指纹做进程内记忆化)。 - * - * 纯同步、无 IO:缓存配置只来自环境变量,不依赖数据库, - * 因此可以在请求热路径上安全调用。 - */ -export function getCacheConfig(env?: any): CacheConfig { - const sig = configSignature(env) - if (cachedConfig && cachedConfig.sig === sig) return cachedConfig.config - - const backends = parseCacheBackends(readRaw("CACHE_DRIVER", env)) - const enabledRaw = readBool("CACHE_ENABLED", true, env) - const prefix = parseCachePrefix(readRaw("CACHE_PREFIX", env)) - - const config: CacheConfig = { - enabled: enabledRaw && backends.length > 0, - backends, - fileTree: readBool("CACHE_FILE_TREE", true, env), - // 默认关闭:部分网盘直链 TTL 极短,缓存复用容易把失效直链发给用户; - // 确认网盘直链有效期后可显式开启(CACHE_DOWNLOAD_LINK=true) - downloadLink: readBool("CACHE_DOWNLOAD_LINK", false, env), - ttlMinutes: readInt("CACHE_TTL", 0, env), - linkTtlMinutes: readInt("CACHE_LINK_TTL", DEFAULT_LINK_TTL_MINUTES, env), - excludeDrivers: parseExcludeDrivers(readRaw("CACHE_EXCLUDE_DRIVERS", env)), - prefix, - } - - cachedConfig = { sig, config } - return config -} - -/** 仅供测试:清除配置记忆化,避免用例间串味。 */ -export function __resetCacheConfigForTest(): void { - cachedConfig = null -} - -/** 供管理接口回显当前生效配置(脱敏,不含任何密钥)。 */ -export function describeCacheConfig(env?: any): Record { - const cfg = getCacheConfig(env) - return { - enabled: cfg.enabled, - driver: cfg.backends.join(","), - backends: cfg.backends, - file_tree: cfg.fileTree, - download_link: cfg.downloadLink, - ttl_minutes: cfg.ttlMinutes, - link_ttl_minutes: cfg.linkTtlMinutes, - exclude_drivers: [...cfg.excludeDrivers], - prefix: cfg.prefix, - } -} diff --git a/src/backend/internal/cache/filetree.ts b/src/backend/internal/cache/filetree.ts deleted file mode 100644 index 3cb68c1b..00000000 --- a/src/backend/internal/cache/filetree.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * 文件树缓存(File Tree Cache)。 - * - * 对齐参考实现 OpenList.ts 的 `files` / `file_cache` 表:浏览目录时优先从 - * 缓存读取,只有缓存未命中(或已过期)才去请求真实存储。 - * - * 缓存粒度与安全边界(重要): - * - 只缓存**驱动层**的 `driver.list()` 结果(原始 FileItem 列表), - * 不缓存任何与请求者身份相关的东西(权限、meta 密码、签名、hide 过滤 - * 都在 server/fs.ts 里按请求实时计算)。因此「管理员看过一次、普通用户 - * 随后访问」不会因为缓存而越权。 - * - 空目录同样会被缓存(值为 `[]`),避免反复击穿到存储。 - * - 写操作(mkdir/rename/remove/move/copy/put)后由调用方主动失效, - * 见 cache/index.ts 的 invalidatePaths()。 - */ -import type { FileItem } from "../driver/base" -import { resolveCacheExpiration } from "../driver/storageopts" -import { resolvePath } from "../model/db" -import { - DEFAULT_FILE_TREE_TTL_MINUTES, - getCacheConfig, - normalizeDriverName, -} from "./config" -import { - buildCacheKey, - cacheDelete, - cacheGetEnvelope, - cacheSetEnvelope, -} from "./store" - -/** 缓存目标:`resolvePath()` 的返回值中本模块关心的字段。 */ -export interface CacheTarget { - storage: any - cleanPath: string - physical?: string | null - isVirtual?: boolean -} - -/** - * 该目标是否可参与文件树缓存。 - * - * 排除条件: - * - 总开关 / 文件树开关关闭; - * - 虚拟路径(没有对应存储); - * - 驱动在 CACHE_EXCLUDE_DRIVERS 中(纯本地计算型,缓存只会变陈旧); - * - 计算出的 TTL <= 0(存储级 `cache_expiration=0` 或自定义策略命中 0, - * 语义即「不缓存」)。 - */ -export function isFileTreeCacheable( - target: CacheTarget | null | undefined, - env?: any, -): boolean { - const cfg = getCacheConfig(env) - if (!cfg.enabled || !cfg.fileTree) return false - if (!target?.storage || target.isVirtual) return false - const driver = normalizeDriverName(target.storage.driver) - if (cfg.excludeDrivers.has(driver)) return false - return resolveFileTreeTtlMinutes(target, env) > 0 -} - -/** - * 文件树缓存的 TTL(分钟)。 - * - * 优先级: - * 1. `CACHE_TTL`(显式全局覆盖,>0 时生效) - * 2. 存储级 `cache_expiration`(可被 `custom_cache_policies` 按路径覆盖) - * 3. 默认 30 分钟 - */ -export function resolveFileTreeTtlMinutes( - target: CacheTarget, - env?: any, -): number { - const cfg = getCacheConfig(env) - if (cfg.ttlMinutes > 0) return cfg.ttlMinutes - try { - const minutes = resolveCacheExpiration(target.storage, target.cleanPath) - return Number.isFinite(minutes) ? minutes : DEFAULT_FILE_TREE_TTL_MINUTES - } catch { - return DEFAULT_FILE_TREE_TTL_MINUTES - } -} - -function keyFor(target: CacheTarget, env?: any): string { - const cfg = getCacheConfig(env) - return buildCacheKey("ft", target.storage.id, target.cleanPath, cfg.prefix) -} - -/** - * 读取文件树缓存。未命中 / 已过期 / 任何异常都返回 null(由调用方回源)。 - */ -export async function getCachedFileTree( - target: CacheTarget | null | undefined, - env?: any, -): Promise { - if (!target || !isFileTreeCacheable(target, env)) return null - try { - const hit = await cacheGetEnvelope(keyFor(target, env), env) - if (!hit) return null - const items = hit.v - if (!Array.isArray(items)) return null - // 防御性拷贝:缓存对象可能被上层归一化逻辑就地修改(如补 type 字段), - // 直接返回同一引用会把修改带回缓存(多实例/多请求下表现为数据串味)。 - return items.map((item) => ({ ...item })) - } catch { - return null - } -} - -/** 写入文件树缓存(TTL <= 0 时跳过)。 */ -export async function setCachedFileTree( - target: CacheTarget | null | undefined, - items: FileItem[], - env?: any, -): Promise { - if (!target || !isFileTreeCacheable(target, env)) return - const ttlMinutes = resolveFileTreeTtlMinutes(target, env) - if (ttlMinutes <= 0) return - try { - await cacheSetEnvelope(keyFor(target, env), items, ttlMinutes * 60_000, env) - } catch { - // 缓存写入失败绝不影响业务 - } -} - -/** 删除单个路径的文件树缓存(不做路径解析,调用方已知 storage)。 */ -export async function deleteFileTreeCacheEntry( - storageId: any, - cleanPath: string, - env?: any, -): Promise { - const cfg = getCacheConfig(env) - await cacheDelete(buildCacheKey("ft", storageId, cleanPath, cfg.prefix), env) -} - -/** 把某个虚拟路径与其父目录一并加入待失效集合(父目录的列表必然变化)。 */ -export function expandToInvalidationPaths(virtualPath: string): string[] { - const norm = - "/" + - String(virtualPath || "") - .split("/") - .filter(Boolean) - .join("/") - const idx = norm.lastIndexOf("/") - const parent = idx <= 0 ? "/" : norm.slice(0, idx) - return parent === norm ? [norm] : [norm, parent] -} - -/** - * 失效若干虚拟路径对应的文件树缓存。 - * - * 每个路径会连同其父目录一起失效:例如 `/a/b/c.txt` 的增删改会让 `/a/b` - * 的目录列表变陈旧;若该路径本身是目录,它自己的列表也会失效。 - */ -export async function invalidateFileTree( - virtualPaths: string[], - env?: any, -): Promise { - const cfg = getCacheConfig(env) - if (!cfg.enabled || !cfg.fileTree) return - - const keys = new Set() - for (const p of virtualPaths) { - for (const candidate of expandToInvalidationPaths(p)) { - try { - const resolved = await resolvePath(candidate, env) - if (resolved.isVirtual || !resolved.storage) continue - keys.add( - buildCacheKey("ft", resolved.storage.id, resolved.cleanPath, cfg.prefix), - ) - } catch { - // 路径无法解析(存储被删除等):忽略,缓存会随 TTL 自然过期 - } - } - } - for (const key of keys) await cacheDelete(key, env) -} diff --git a/src/backend/internal/cache/index.ts b/src/backend/internal/cache/index.ts deleted file mode 100644 index b0cb59e2..00000000 --- a/src/backend/internal/cache/index.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * 缓存模块统一入口。 - * - * 两级缓存(对齐参考实现 OpenList.ts): - * - 文件树缓存:`filetree.ts`,浏览目录时命中缓存,避免每次请求网盘; - * - 下载链接缓存:`link.ts`,复用驱动换取的直链,避免重复签名/换链。 - * - * 后端选择:`store.ts`,默认只向数据库启用(`CACHE_DRIVER=db`), - * 显式配置 `CACHE_DRIVER=db,kv` / `kv` / `blob` 等才会启用专用后端。 - * - * 失效:写操作(mkdir/rename/remove/move/copy/put)后调用 `invalidatePaths()`, - * 同时失效这两级缓存;分片 / 直传上传的收尾路径(/fs/upload/complete、 - * /fs/multipart/complete、/fs/other、/fs/get_direct_upload_info)同样接入。 - */ -import { getCacheConfig, describeCacheConfig } from "./config" -import { clearCache, getCacheDrivers, isCacheActive } from "./store" -import { invalidateFileTree } from "./filetree" -import { invalidateLinks } from "./link" - -export { - getCacheConfig, - describeCacheConfig, - parseCacheBackends, - parseExcludeDrivers, - parseCachePrefix, - normalizeDriverName, - __resetCacheConfigForTest, - DEFAULT_FILE_TREE_TTL_MINUTES, - DEFAULT_LINK_TTL_MINUTES, - VALID_CACHE_BACKENDS, -} from "./config" -export type { CacheBackendName, CacheConfig } from "./config" - -export { - buildCacheKey, - cacheKindPrefix, - cacheDelete, - cacheGetEnvelope, - cacheListKeys, - cacheSetEnvelope, - clearCache, - encodeCachePathSegment, - getCacheDrivers, - isCacheActive, - storageIdFromCacheKey, - __resetCacheStoreForTest, - __setCacheDriversForTest, -} from "./store" -export type { CacheEnvelope, CacheKind } from "./store" - -export { - getCachedFileTree, - setCachedFileTree, - invalidateFileTree, - isFileTreeCacheable, - resolveFileTreeTtlMinutes, - deleteFileTreeCacheEntry, - expandToInvalidationPaths, -} from "./filetree" -export type { CacheTarget } from "./filetree" - -export { - getCachedLink, - setCachedLink, - invalidateLinks, - isLinkCacheable, - resolveLinkTtlMinutes, - deleteLinkCacheEntry, -} from "./link" - -/** - * 失效若干虚拟路径对应的**全部**缓存(文件树 + 下载链接)。 - * - * 这是写操作后统一调用的入口:调用方只需给出「被改动的路径」, - * 父目录的列表失效由 `invalidateFileTree()` 内部处理。 - */ -export async function invalidatePaths( - virtualPaths: string[], - env?: any, -): Promise { - const paths = (virtualPaths || []).filter(Boolean) - if (paths.length === 0) return - const cfg = getCacheConfig(env) - if (!cfg.enabled) return - try { - await Promise.all([ - invalidateFileTree(paths, env), - invalidateLinks(paths, env), - ]) - } catch (err: any) { - // 失效失败不影响写操作本身:缓存会随 TTL 自然过期 - console.warn(`[Cache] invalidate failed: ${err?.message || err}`) - } -} - -/** 清空某个存储的全部缓存(存储配置变更 / 删除 / 启停时调用)。 */ -export async function clearStorageCache( - storageId: any, - env?: any, -): Promise { - try { - return await clearCache(env, { storageId }) - } catch (err: any) { - console.warn(`[Cache] clearStorageCache failed: ${err?.message || err}`) - return 0 - } -} - -/** - * 缓存运行时状态(供管理接口 / 诊断展示)。 - */ -export async function getCacheRuntimeStatus(env?: any): Promise<{ - config: Record - active: boolean - drivers: string[] -}> { - const config = describeCacheConfig(env) - let drivers: string[] = [] - let active = false - try { - active = await isCacheActive(env) - drivers = (await getCacheDrivers(env)).map((d) => d.name) - } catch { - active = false - } - return { config, active, drivers } -} diff --git a/src/backend/internal/cache/link.ts b/src/backend/internal/cache/link.ts deleted file mode 100644 index f12efa7d..00000000 --- a/src/backend/internal/cache/link.ts +++ /dev/null @@ -1,138 +0,0 @@ -/** - * 下载链接缓存(Download Link Cache)。 - * - * 对齐参考实现 OpenList.ts 的 `file_links` 表:把驱动 `get()` 返回的直链 - * (`raw_url` + 必需的 `raw_url_headers`)缓存起来,重复下载 / 预览时无需 - * 再次向网盘换取临时链接(这一步通常最贵,且容易被限流)。 - * - * 安全边界: - * - 只缓存**驱动层**结果,签名(`?sign=`)与访问控制在 server 层按请求 - * 实时签发/校验,因此缓存不会绕过权限; - * - 只缓存「确实拿到了直链」的成功结果(`raw_url` 非空、非目录), - * 失败结果不做负缓存,避免把瞬时故障固化; - * - TTL 默认仅 5 分钟(`CACHE_LINK_TTL`)。直链通常自带有效期, - * 缓存过久会把「已失效的链接」交给浏览器(表现为 403/404)。 - */ -import type { FileItem } from "../driver/base" -import { resolvePath } from "../model/db" -import { - DEFAULT_LINK_TTL_MINUTES, - getCacheConfig, - normalizeDriverName, -} from "./config" -import { - buildCacheKey, - cacheDelete, - cacheGetEnvelope, - cacheSetEnvelope, -} from "./store" -import type { CacheTarget } from "./filetree" - -/** 下载链接缓存的 TTL(分钟)。 */ -export function resolveLinkTtlMinutes(env?: any): number { - return getCacheConfig(env).linkTtlMinutes -} - -/** - * 该目标是否可参与下载链接缓存。 - * - * 与文件树缓存共享「驱动排除名单」:纯本地计算型驱动(virtual/alias/…) - * 的 get() 本来就没有网络开销,缓存没有意义。 - */ -export function isLinkCacheable( - target: CacheTarget | null | undefined, - env?: any, -): boolean { - const cfg = getCacheConfig(env) - if (!cfg.enabled || !cfg.downloadLink) return false - if (!target?.storage || target.isVirtual) return false - const driver = normalizeDriverName(target.storage.driver) - if (cfg.excludeDrivers.has(driver)) return false - return cfg.linkTtlMinutes > 0 -} - -function keyFor(target: CacheTarget, env?: any): string { - const cfg = getCacheConfig(env) - return buildCacheKey("ln", target.storage.id, target.cleanPath, cfg.prefix) -} - -/** - * 读取下载链接缓存。未命中 / 已过期 / 异常均返回 null。 - * - * 返回的是 FileItem 的浅拷贝,避免调用方就地修改污染缓存内容。 - */ -export async function getCachedLink( - target: CacheTarget, - env?: any, -): Promise { - if (!isLinkCacheable(target, env)) return null - try { - const hit = await cacheGetEnvelope(keyFor(target, env), env) - if (!hit || !hit.v || typeof hit.v !== "object") return null - return { ...hit.v } - } catch { - return null - } -} - -/** - * 写入下载链接缓存。 - * - * 只在「拿到了可用直链」时写入:目录、`raw_url` 为空、或带有 - * `raw_url_error` 的结果都会被跳过。 - */ -export async function setCachedLink( - target: CacheTarget | null | undefined, - item: FileItem | null | undefined, - env?: any, -): Promise { - if (!target || !isLinkCacheable(target, env)) return - if (!item || item.is_dir) return - if (!item.raw_url) return - const ttlMinutes = resolveLinkTtlMinutes(env) - if (ttlMinutes <= 0) return - try { - await cacheSetEnvelope(keyFor(target, env), item, ttlMinutes * 60_000, env) - } catch { - // 缓存写入失败绝不影响业务 - } -} - -/** 删除单个路径的下载链接缓存(调用方已知 storage)。 */ -export async function deleteLinkCacheEntry( - storageId: any, - cleanPath: string, - env?: any, -): Promise { - const cfg = getCacheConfig(env) - await cacheDelete(buildCacheKey("ln", storageId, cleanPath, cfg.prefix), env) -} - -/** - * 失效若干虚拟路径对应的下载链接缓存。 - * - * 仅失效路径自身(下载链接按文件粒度缓存,不存在「父目录链接」)。 - * 路径可能是目录(如删除整个目录),此时其下所有文件的链接会随 TTL - * 自然过期——直链缓存 TTL 很短(默认 5 分钟),无需递归清理。 - */ -export async function invalidateLinks( - virtualPaths: string[], - env?: any, -): Promise { - const cfg = getCacheConfig(env) - if (!cfg.enabled || !cfg.downloadLink) return - - const keys = new Set() - for (const p of virtualPaths) { - try { - const resolved = await resolvePath(p, env) - if (resolved.isVirtual || !resolved.storage) continue - keys.add( - buildCacheKey("ln", resolved.storage.id, resolved.cleanPath, cfg.prefix), - ) - } catch { - // 路径无法解析:忽略 - } - } - for (const key of keys) await cacheDelete(key, env) -} diff --git a/src/backend/internal/cache/store.ts b/src/backend/internal/cache/store.ts deleted file mode 100644 index 17cb47ea..00000000 --- a/src/backend/internal/cache/store.ts +++ /dev/null @@ -1,443 +0,0 @@ -/** - * 缓存存储层:把「缓存键值」落到某个 Driver 上。 - * - * 与业务数据的关系: - * - `db` 后端复用 `getStorageBackend()` 解析出的 driver,也就是与业务数据 - * **同一个** d1 / mysql / kv / cfkv / blob / do 后端(默认行为); - * - `kv` / `blob` / `cfkv` / `do` / `memory` 是**专用**后端,只有用户在 - * `CACHE_DRIVER` 里显式写出才会启用(不会自动探测、不会自动回退); - * - 缓存写入**直接走 driver 的 get/put/delete**,不经过 `saveDb()`, - * 因此不会触发「整配置对象序列化 / 写前守卫 / 字段加密」等开销, - * 也不会污染业务数据(键名统一带 `openlist_cache` 前缀)。 - * - * 失败策略:缓存永远不能让业务请求失败。任何读/写异常都按「未命中 / 未写入」 - * 处理,并只告警一次,避免把日志刷满。 - */ -import type { Driver } from "../model/store/types" -import { getStorageBackend } from "../model/store/backend" -import { kvDriver } from "../model/store/driver/kv" -import { cfkvDriver } from "../model/store/driver/cfkv" -import { blobDriver } from "../model/store/driver/blob" -import { doDriver } from "../model/store/driver/do" -import { memoryDriver } from "../model/store/driver/memory" -import { encodeKeyPart } from "../model/store/keycodec" -import { getCacheConfig, type CacheBackendName } from "./config" - -/** 专用后端名 → 驱动实现(`db` 单独处理,见 resolveDrivers) */ -const DEDICATED_DRIVERS: Record = { - kv: kvDriver, - cfkv: cfkvDriver, - blob: blobDriver, - do: doDriver, - memory: memoryDriver, -} - -/** 缓存条目信封:值 + 过期时间戳 */ -export interface CacheEnvelope { - /** 载荷 */ - v: T - /** 写入时间(ms) */ - ts: number - /** 过期时间(ms,绝对时间) */ - exp: number -} - -/** - * 驱动解析结果缓存。 - * - * 为什么需要:`blobDriver.isAvailable()` 每次都会动态 import EdgeOne SDK, - * `kvDriver.isAvailable()` 可能发一次 HTTP 探测;而缓存配置在实例生命周期内 - * 基本不变。按 env 对象身份记忆化即可(`1 env = 1 请求`,条目随 GC 回收)。 - */ -const resolvedByEnv = new WeakMap>() - -/** 仅供测试:注入缓存驱动解析结果。 */ -let driverLoaderOverride: ((env: any) => Promise) | null = null - -export function __setCacheDriversForTest( - loader: ((env: any) => Promise) | null, -): void { - driverLoaderOverride = loader -} - -/** 仅供测试:重置模块级状态。 */ -export function __resetCacheStoreForTest(): void { - driverLoaderOverride = null - warnedDrivers.clear() -} - -/** 每个驱动只告警一次(键为 `driverName:reason`) */ -const warnedDrivers = new Map() - -function warnOnce(key: string, message: string): void { - if (warnedDrivers.has(key)) return - warnedDrivers.set(key, true) - console.warn(message) -} - -/** - * 解析实际可用的缓存驱动列表(顺序即读取优先级)。 - * - * 语义约定(与 DB_DRIVER 一致): - * - 用户显式配置了某个专用后端但它不可用 → **跳过并告警**,绝不悄悄换成 - * 别的后端;若最终一个都不剩,则缓存整体失效(请求照常走真实存储)。 - */ -async function resolveDrivers(env: any): Promise { - if (driverLoaderOverride) return driverLoaderOverride(env) - - const cfg = getCacheConfig(env) - if (!cfg.enabled) return [] - - const drivers: Driver[] = [] - for (const name of cfg.backends) { - if (name === "db") { - try { - const { driver } = await getStorageBackend(env) - drivers.push(driver) - } catch (err: any) { - warnOnce( - "db:resolve", - `[Cache] CACHE_DRIVER includes "db" but the storage backend could not be ` + - `resolved (${err?.message || err}); the cache falls back to no-op.`, - ) - } - continue - } - - const driver = DEDICATED_DRIVERS[name] - if (!driver) continue - try { - if (await driver.isAvailable(env)) { - drivers.push(driver) - } else { - warnOnce( - `${name}:unavailable`, - `[Cache] CACHE_DRIVER="${name}" is not available in this runtime ` + - `(missing binding/credentials). This cache backend is skipped; ` + - `no other backend is used in its place.`, - ) - } - } catch (err: any) { - warnOnce( - `${name}:error`, - `[Cache] CACHE_DRIVER="${name}" availability probe failed: ` + - `${err?.message || err}. This cache backend is skipped.`, - ) - } - } - return drivers -} - -/** 获取(并记忆化)当前 env 的缓存驱动列表。 */ -export async function getCacheDrivers(env?: any): Promise { - if (driverLoaderOverride) return driverLoaderOverride(env) - if (env && typeof env === "object") { - let pending = resolvedByEnv.get(env) - if (!pending) { - pending = resolveDrivers(env) - resolvedByEnv.set(env, pending) - } - return pending - } - return resolveDrivers(env) -} - -/** 缓存是否已实际启用(用于诊断接口与管理页展示)。 */ -export async function isCacheActive(env?: any): Promise { - const drivers = await getCacheDrivers(env) - return drivers.length > 0 -} - -/** 把 driver.get 的返回值(可能是 Response 形态)统一成字符串。 */ -async function coerceText(value: any): Promise { - if (value === null || value === undefined) return null - if (typeof value === "string") return value - if (typeof value?.text === "function") { - try { - return String(await value.text()) - } catch { - return null - } - } - return String(value) -} - -/** - * 读取缓存条目(自动跳过已过期条目)。 - * - * 多个后端时按配置顺序逐个尝试:任一命中即返回(先写 KV 后写 DB 的部署, - * 即使 DB 侧被清理过也仍能命中 KV)。 - */ -export async function cacheGetEnvelope( - key: string, - env?: any, -): Promise | null> { - let drivers: Driver[] - try { - drivers = await getCacheDrivers(env) - } catch { - return null - } - if (drivers.length === 0) return null - - for (const driver of drivers) { - try { - const raw = await coerceText(await driver.get(key, env)) - if (!raw) continue - const parsed = JSON.parse(raw) as CacheEnvelope - if (!parsed || typeof parsed !== "object") continue - if (typeof parsed.exp === "number" && parsed.exp <= Date.now()) { - // 惰性清理过期条目,避免缓存无限膨胀 - void cacheDelete(key, env) - continue - } - return parsed - } catch (err: any) { - warnOnce( - `${driver.name}:get`, - `[Cache] read failed on backend "${driver.name}": ${err?.message || err}`, - ) - } - } - return null -} - -/** 写入缓存条目(TTL 单位毫秒;ttl<=0 时不写入)。 */ -export async function cacheSetEnvelope( - key: string, - value: T, - ttlMs: number, - env?: any, -): Promise { - if (!Number.isFinite(ttlMs) || ttlMs <= 0) return - - let drivers: Driver[] - try { - drivers = await getCacheDrivers(env) - } catch { - return - } - if (drivers.length === 0) return - - const now = Date.now() - const envelope: CacheEnvelope = { v: value, ts: now, exp: now + ttlMs } - let raw: string - try { - raw = JSON.stringify(envelope) - } catch { - return - } - - // 双写所有后端:读路径按顺序命中,写路径尽量铺满(各自失败互不影响) - await Promise.all( - drivers.map(async (driver) => { - try { - await driver.put(key, raw, env) - } catch (err: any) { - warnOnce( - `${driver.name}:put`, - `[Cache] write failed on backend "${driver.name}": ${err?.message || err}`, - ) - } - }), - ) -} - -/** 删除缓存键(多后端全删)。 */ -export async function cacheDelete(key: string, env?: any): Promise { - let drivers: Driver[] - try { - drivers = await getCacheDrivers(env) - } catch { - return - } - if (drivers.length === 0) return - - await Promise.all( - drivers.map(async (driver) => { - try { - await driver.delete(key, env) - } catch (err: any) { - warnOnce( - `${driver.name}:delete`, - `[Cache] delete failed on backend "${driver.name}": ${err?.message || err}`, - ) - } - }), - ) -} - -/** - * 列出以 `prefix` 开头的缓存键(多后端取并集)。 - * - * 仅用于管理接口(清空 / 统计),不在请求热路径上。 - */ -export async function cacheListKeys( - prefix: string, - env?: any, -): Promise { - let drivers: Driver[] - try { - drivers = await getCacheDrivers(env) - } catch { - return [] - } - if (drivers.length === 0) return [] - - const out = new Set() - for (const driver of drivers) { - try { - for (const key of await driver.list(prefix, env)) out.add(key) - } catch (err: any) { - warnOnce( - `${driver.name}:list`, - `[Cache] list failed on backend "${driver.name}": ${err?.message || err}`, - ) - } - } - return [...out] -} - -/** 缓存类型标识:`ft` = 文件树(file tree),`ln` = 下载链接(link) */ -export type CacheKind = "ft" | "ln" - -/** 32 位 FNV-1a 哈希(同步,用于把超长路径压成短键)。 */ -function fnv1a32(input: string, seed: number): number { - let hash = seed >>> 0 - for (let i = 0; i < input.length; i++) { - hash ^= input.charCodeAt(i) - hash = Math.imul(hash, 0x01000193) >>> 0 - } - return hash >>> 0 -} - -/** - * 缓存专用的单射字符判断。 - * - * 与业务数据的 `encodeKeyPart` 唯一的区别:**小写 `x` 不再原样保留**。 - * 业务键名仅作存储地址、主键取自记录 JSON,`x` 的歧义无碍;而缓存靠 - * 「键 → 值」取回数据,键不单射 = 取回别的路径的内容(评审实测复现: - * `/a/b` 与 `/ax2fb` 键相同,可跨目录串列表 / 直链)。`x` 的 UTF-8 - * 字节恰为 0x78,转义成 `x78` 即可让任何 `x` 都只能来自转义序列。 - */ -function isCachePlain(ch: string): boolean { - if (ch === "x") return false - return ( - (ch >= "a" && ch <= "z") || - (ch >= "A" && ch <= "Z") || - (ch >= "0" && ch <= "9") || - ch === "_" - ) -} - -/** `isCachePlain` 的快路径正则([A-Za-z] 去掉小写 x + [0-9] + _)。 */ -const CACHE_PLAIN_RE = /^[A-Za-wyz0-9_]+$/ - -/** 单射编码:合法字符原样(`x` 除外),其余按 UTF-8 逐字节转义为 `xHH`。 */ -function encodePathInjective(s: string): string { - if (s === "") return "0" - if (CACHE_PLAIN_RE.test(s)) return s - - let out = "" - for (const ch of s) { - if (isCachePlain(ch)) { - out += ch - } else { - for (const b of new TextEncoder().encode(ch)) { - out += "x" + b.toString(16).padStart(2, "0") - } - } - } - return out -} - -/** - * 路径片段编码为 KV 合法键片段(**单射**)。 - * - * EdgeOne KV 只接受 `[A-Za-z0-9_]`。编码必须单射:任意两个不同虚拟路径 - * 不得得到同一个键,否则「键碰撞 = 跨目录 / 跨文件串数据」(权限、hide - * 规则、meta 密码按请求路径判定,而内容取自碰撞路径,可被利用绕过)。 - * - * - 短路径:直接单射编码(`x` → `x78` 消除转义前缀歧义)。 - * - 超长路径(编码后 > 180,接近 KV 键长上限):截取前 100 字符 + - * `xg` + 双 32 位 FNV-1a(合计 64 位)收尾。标记选 `xg` 是因为普通 - * 编码结果中 `x` 后面必跟两位十六进制,`xg` 不可能出现,因此长路径 - * 折叠键不会与任何短路径的普通编码键碰撞;两个超长路径之间依赖 - * 64 位哈希区分。编码是确定性的,失效时仍能算出同一个键。 - */ -export function encodeCachePathSegment(virtualPath: string): string { - const norm = - "/" + - String(virtualPath || "") - .split("/") - .filter(Boolean) - .join("/") - const encoded = encodePathInjective(norm) - if (encoded.length <= 180) return encoded - const a = fnv1a32(norm, 0x811c9dc5).toString(16).padStart(8, "0") - const b = fnv1a32(norm, 0x9e3779b9).toString(16).padStart(8, "0") - return encoded.slice(0, 100) + "xg" + a + b -} - -/** - * 构造缓存键:`___`。 - * - * 键名分四段,第 4 段(索引 3)是存储 id,便于「按存储清空」时按段过滤, - * 避免 `storages_1_` 前缀误伤 `storages_10_` 这类包含关系。 - */ -export function buildCacheKey( - kind: CacheKind, - storageId: any, - virtualPath: string, - prefix: string, -): string { - return `${prefix}_${kind}_${encodeKeyPart(String(storageId ?? ""))}_${encodeCachePathSegment( - virtualPath, - )}` -} - -/** 某个 kind 在全部存储下的键前缀。 */ -export function cacheKindPrefix(kind: CacheKind, prefix: string): string { - return `${prefix}_${kind}_` -} - -/** - * 从缓存键中解析出所属存储 id(用于按存储过滤)。 - * - * 段数由前缀决定(前缀本身可能含 `_`),因此按前缀的段数动态定位, - * 不做硬编码索引。 - */ -export function storageIdFromCacheKey( - key: string, - kind: CacheKind, - prefix: string, -): string | null { - const head = `${prefix}_${kind}_` - if (!key.startsWith(head)) return null - const rest = key.slice(head.length) - const sep = rest.indexOf("_") - return sep === -1 ? rest : rest.slice(0, sep) -} - -/** 清空全部缓存(或仅某一 kind / 某一存储),返回删除的键数量。 */ -export async function clearCache( - env: any, - opts: { kind?: CacheKind; storageId?: any } = {}, -): Promise { - const cfg = getCacheConfig(env) - const kinds: CacheKind[] = opts.kind ? [opts.kind] : ["ft", "ln"] - let removed = 0 - - for (const kind of kinds) { - const keys = await cacheListKeys(cacheKindPrefix(kind, cfg.prefix), env) - for (const key of keys) { - if (opts.storageId !== undefined && opts.storageId !== null) { - const id = storageIdFromCacheKey(key, kind, cfg.prefix) - if (id !== String(opts.storageId)) continue - } - await cacheDelete(key, env) - removed++ - } - } - return removed -} diff --git a/src/backend/internal/driver/storageopts.test.ts b/src/backend/internal/driver/storageopts.test.ts index 92639e67..6146dd21 100644 --- a/src/backend/internal/driver/storageopts.test.ts +++ b/src/backend/internal/driver/storageopts.test.ts @@ -1,13 +1,6 @@ import assert from "node:assert/strict" import { test } from "node:test" -import { - getProxyRange, - getEnableSign, - getDisableIndex, - parseCustomCachePolicies, - matchGlob, - resolveCacheExpiration, -} from "./storageopts" +import { getProxyRange, getEnableSign, getDisableIndex } from "./storageopts" import { resolveProxyDecision, getDownProxyUrl } from "./proxy" // ---- proxy_range ---- @@ -38,205 +31,6 @@ test("enable_sign / disable_index: 解析字符串与布尔两种形式", () => assert.equal(getDisableIndex({}), false) }) -// ---- custom_cache_policies ---- - -test("custom_cache_policies: 支持 JSON 数组形式", () => { - const policies = parseCustomCachePolicies({ - custom_cache_policies: - '[{"path":"/photos/*","cache_expiration":1440},{"path":"/tmp/**","cache_expiration":0}]', - }) - assert.equal(policies.length, 2) - assert.equal(policies[0].path, "/photos/*") - assert.equal(policies[0].cacheExpiration, 1440) - assert.equal(policies[1].path, "/tmp/**") - assert.equal(policies[1].cacheExpiration, 0) -}) - -test("custom_cache_policies: 支持 max_age 别名", () => { - const policies = parseCustomCachePolicies({ - custom_cache_policies: [{ path: "/a/*", max_age: 60 }], - }) - assert.equal(policies.length, 1) - assert.equal(policies[0].cacheExpiration, 60) -}) - -test("custom_cache_policies: 支持 key-value 映射形式", () => { - const policies = parseCustomCachePolicies({ - custom_cache_policies: { "/x/*": 5, "/y/*": 10 }, - }) - assert.equal(policies.length, 2) - const byPath = Object.fromEntries( - policies.map((p) => [p.path, p.cacheExpiration]), - ) - assert.equal(byPath["/x/*"], 5) - assert.equal(byPath["/y/*"], 10) -}) - -test("custom_cache_policies: 空值与非法输入返回空数组而不抛错", () => { - assert.deepEqual(parseCustomCachePolicies({}), []) - assert.deepEqual(parseCustomCachePolicies({ custom_cache_policies: "" }), []) - assert.deepEqual( - parseCustomCachePolicies({ custom_cache_policies: "not-json" }), - [], - ) -}) - -// ---- glob 匹配 ---- - -test("matchGlob: 单个 * 不跨目录分隔符", () => { - assert.equal(matchGlob("/photos/*", "/photos/a.jpg"), true) - assert.equal(matchGlob("/photos/*", "/photos/sub/a.jpg"), false) -}) - -test("matchGlob: ** 可跨目录分隔符", () => { - assert.equal(matchGlob("/photos/**", "/photos/sub/deep/a.jpg"), true) - assert.equal(matchGlob("/**", "/any/thing"), true) -}) - -test("matchGlob: 精确匹配与 ? 通配", () => { - assert.equal(matchGlob("/a/b.txt", "/a/b.txt"), true) - assert.equal(matchGlob("/a/?.txt", "/a/b.txt"), true) -}) - -// ---- glob DoS 防护 ---- - -test("matchGlob: 超长模式被拒绝(长度上限)", () => { - const huge = "/" + "a".repeat(5000) - assert.equal(matchGlob(huge, "/a"), false) -}) - -test("matchGlob: 大量 ** 段仍能快速返回(不再依赖段数截断)", () => { - // 200 个 ** 段在语义上确实应匹配任意多级路径; - // 防护改由 DP 的 O(段数×段数) 复杂度和长度上限提供, - // 这里断言结果正确且耗时可控。 - const pattern = "/" + "**/".repeat(200) + "z" - const t0 = performance.now() - const got = matchGlob(pattern, "/a/b/c/z") - const elapsed = performance.now() - t0 - assert.equal(got, true) - assert.ok(elapsed < 500, `耗时 ${elapsed.toFixed(1)}ms,疑似复杂度过高`) -}) - -test("matchGlob: 相邻通配符折叠后仍可正确匹配", () => { - // 折叠不应破坏语义:*** 等价于 ** - assert.equal(matchGlob("/a/**/b", "/a/x/y/b"), true) - assert.equal(matchGlob("/a/*/b", "/a/x/b"), true) - assert.equal(matchGlob("/a/*/b", "/a/x/y/b"), false) -}) - -test("matchGlob: 恶意回溯模式能在合理时间内返回(不挂起)", () => { - // 关键回归:多个相邻 .* 曾可能触发灾难性回溯。 - // 这里断言匹配失败时的耗时可控,防止未来退化。 - const pattern = "/" + "**/".repeat(20) + "z" - const target = "/" + "ab/".repeat(40) + "x" - const t0 = performance.now() - const result = matchGlob(pattern, target) - const elapsed = performance.now() - t0 - assert.equal(result, false) - assert.ok( - elapsed < 1000, - `glob 匹配耗时 ${elapsed.toFixed(1)}ms,疑似回溯失控`, - ) -}) - -test("matchGlob: 正常规则不受防护影响", () => { - assert.equal(matchGlob("/photos/*", "/photos/a.jpg"), true) - assert.equal(matchGlob("/photos/**", "/photos/a/b/c.jpg"), true) - assert.equal(matchGlob("/data/**/*.log", "/data/a/b/c.log"), true) -}) - -// ---- 双星 + 单星组合(双回溯点回归)---- - -test("matchGlob: 双星后跟单星可正确回退", () => { - // 关键回归:双星先吞掉 /a/b,遇到单星不匹配时须退回双星 - assert.equal(matchGlob("/data/**/*.log", "/data/a/b/c.log"), true) - assert.equal(matchGlob("/data/**/*.log", "/data/c.log"), true) - assert.equal(matchGlob("/x/**/*/y", "/x/a/b/c/y"), true) - // 扩展名不匹配时必须为 false - assert.equal(matchGlob("/data/**/*.log", "/data/a/b/c.txt"), false) -}) - -test("matchGlob: 单星不得跨目录,双星可以", () => { - assert.equal(matchGlob("/a/*/b", "/a/x/b"), true) - assert.equal(matchGlob("/a/*/b", "/a/x/y/b"), false) - assert.equal(matchGlob("/a/**/b", "/a/x/y/b"), true) - // 双星可匹配 0 段 - assert.equal(matchGlob("/a/**/b", "/a/b"), true) -}) - -test("matchGlob: 仅由星号组成的段才跨级,混杂字符的段不跨级", () => { - // `**b` 是段内通配(对齐 doublestar:只有整段为 ** 时才跨目录) - assert.equal(matchGlob("/a/**b", "/a/x/y/b"), false) - assert.equal(matchGlob("/a/**b", "/a/xyb"), true) - assert.equal(matchGlob("/a/*b", "/a/xyb"), true) - assert.equal(matchGlob("/a/*b", "/a/x/yb"), false) -}) - -test("matchGlob: 通配符在开头与结尾", () => { - assert.equal(matchGlob("*.log", "a.log"), true) - assert.equal(matchGlob("*.log", "a.txt"), false) - assert.equal(matchGlob("/a/*", "/a/b"), true) - assert.equal(matchGlob("/a/*", "/a/b/c"), false) - assert.equal(matchGlob("/a/**", "/a/b/c"), true) -}) - -test("matchGlob: 目标已耗尽时模式剩余必须全是星号", () => { - assert.equal(matchGlob("/a/b*", "/a/b"), true) - assert.equal(matchGlob("/a/b**", "/a/b"), true) - assert.equal(matchGlob("/a/bc", "/a/b"), false) - assert.equal(matchGlob("/a/b?d", "/a/b"), false) -}) - -test("matchGlob: 连续多个星号等价于双星", () => { - assert.equal(matchGlob("/a/***/b", "/a/x/y/b"), true) - assert.equal(matchGlob("/a/*****/b", "/a/x/y/b"), true) -}) - -test("matchGlob: 空模式与空目标", () => { - assert.equal(matchGlob("", "/a"), false) - assert.equal(matchGlob("/a", ""), false) -}) - -// ---- 缓存时长合成 ---- - -test("resolveCacheExpiration: 无自定义策略时使用存储基础值", () => { - assert.equal(resolveCacheExpiration({ cache_expiration: 15 }, "/a/b.txt"), 15) -}) - -test("resolveCacheExpiration: 未配置时回退默认 30", () => { - assert.equal(resolveCacheExpiration({}, "/a/b.txt"), 30) -}) - -test("resolveCacheExpiration: 命中规则时覆盖基础值,未命中用基础值", () => { - const storage = { - cache_expiration: 30, - custom_cache_policies: '[{"path":"/photos/*","cache_expiration":1440}]', - } - assert.equal(resolveCacheExpiration(storage, "/photos/a.jpg"), 1440) - assert.equal(resolveCacheExpiration(storage, "/docs/a.txt"), 30) -}) - -test("resolveCacheExpiration: 规则值为 0 表示不缓存", () => { - const storage = { - cache_expiration: 30, - custom_cache_policies: '[{"path":"/tmp/*","cache_expiration":0}]', - } - assert.equal(resolveCacheExpiration(storage, "/tmp/x.bin"), 0) -}) - -test("resolveCacheExpiration: 多条规则命中时最后一条优先", () => { - const storage = { - custom_cache_policies: - '[{"path":"/a/**","cache_expiration":10},{"path":"/a/b/*","cache_expiration":20}]', - } - assert.equal(resolveCacheExpiration(storage, "/a/b/c.txt"), 20) -}) - -test("resolveCacheExpiration: 非法或负数回退默认值", () => { - assert.equal(resolveCacheExpiration({ cache_expiration: -5 }, "/a"), 30) - assert.equal(resolveCacheExpiration({ cache_expiration: "abc" }, "/a"), 30) -}) - // ---- 与代理决策协同 ---- test("proxy 决策: 默认驱动走 302", () => { diff --git a/src/backend/internal/driver/storageopts.ts b/src/backend/internal/driver/storageopts.ts index b0ab8bac..804ecacf 100644 --- a/src/backend/internal/driver/storageopts.ts +++ b/src/backend/internal/driver/storageopts.ts @@ -1,31 +1,18 @@ /** - * 存储级行为选项:proxy_range / enable_sign / disable_index / cache_expiration - * / custom_cache_policies。 + * 存储级行为选项:proxy_range / enable_sign / disable_index。 * * 这些字段在 Go 版由 internal/model/storage.go 与 internal/op/driver.go 定义并 - * 在下载链路中实际生效;在 TSWorker 中此前只有数据库列与表单项、没有读取方, - * 本模块把它们统一解析出来,供下载/列表链路使用。 + * 在下载链路中实际生效;本模块把它们统一解析出来,供下载/列表链路使用。 * * Go 对应位置: * - model.Storage.ProxyRange → proxy_range(driver.Config.ProxyRangeOption) * - model.Storage.EnableSign → common.IsStorageSignEnabled * - model.Storage.DisableIndex → handles.FsList - * - model.Storage.CacheExpiration → 对象缓存时长 - * - model.Storage.CustomCachePolicies → 路径级缓存覆盖 + * + * 说明:存储级 cache_expiration / custom_cache_policies 已随持久缓存一并移除, + * 这两个字段即使残留在 storage.addition 里也不会再被任何代码读取。 */ -/** 单条自定义缓存策略(对齐 Go model.CustomCachePolicy) */ -export interface CustomCachePolicy { - /** 路径通配符,如 "/photos/*"、"*.log"、"/cache/**" */ - path: string - /** 命中后的缓存时长(分钟),0 表示不缓存 */ - cacheExpiration: number - /** 可选的 max_age(秒);缺省时由 cacheExpiration 推导 */ - maxAge?: number -} - -const DEFAULT_CACHE_EXPIRATION_MINUTES = 30 - /** storage.addition 可能是 JSON 字符串或已解析对象,统一解析 */ export function parseAdditionLoose(addition: any): Record { if (!addition) return {} @@ -46,16 +33,6 @@ function asBool(value: any): boolean { return value === true } -function asInt(value: any): number | null { - if (typeof value === "number" && Number.isFinite(value)) - return Math.trunc(value) - if (typeof value === "string" && value.trim() !== "") { - const n = parseInt(value, 10) - if (Number.isFinite(n)) return n - } - return null -} - /** * proxy_range(对齐 Go 的 Range 处理)。 * @@ -87,234 +64,3 @@ export function getDisableIndex(storage: any): boolean { if (!storage) return false return asBool(storage.disable_index ?? storage.disableIndex) } - -/** - * 存储的基础缓存时长(分钟)。等价于 Go model.Storage.CacheExpiration。 - * 未配置时回退 30 分钟。 - */ -export function getCacheExpiration(storage: any): number { - const raw = asInt(storage?.cache_expiration ?? storage?.cacheExpiration) - if (raw === null || raw < 0) return DEFAULT_CACHE_EXPIRATION_MINUTES - return raw -} - -/** - * 解析 custom_cache_policies(对齐 Go 的路径级缓存覆盖)。 - * - * 兼容多种书写形式: - * - JSON 字符串:'[{"path":"/a/*","cache_expiration":10}]' - * - 对象数组 - * - key-value 映射:{"/a/*": 10} - * 逗号分隔的字符串形式也做了容错。 - */ -export function parseCustomCachePolicies(storage: any): CustomCachePolicy[] { - let raw = storage?.custom_cache_policies ?? storage?.customCachePolicies - if (!raw) return [] - - if (typeof raw === "string") { - const trimmed = raw.trim() - if (!trimmed) return [] - try { - raw = JSON.parse(trimmed) - } catch { - // 容错:退化为 "路径:分钟,路径:分钟" 形式 - const policies: CustomCachePolicy[] = [] - for (const chunk of trimmed.split(",")) { - const idx = chunk.lastIndexOf(":") - if (idx <= 0) continue - const p = chunk.slice(0, idx).trim() - const v = asInt(chunk.slice(idx + 1).trim()) - if (p && v !== null) policies.push({ path: p, cacheExpiration: v }) - } - return policies - } - } - - const policies: CustomCachePolicy[] = [] - - if (Array.isArray(raw)) { - for (const entry of raw) { - if (!entry) continue - if (typeof entry === "string") { - const idx = entry.lastIndexOf(":") - if (idx <= 0) continue - const p = entry.slice(0, idx).trim() - const v = asInt(entry.slice(idx + 1).trim()) - if (p && v !== null) policies.push({ path: p, cacheExpiration: v }) - continue - } - if (typeof entry === "object") { - const p = entry.path ?? entry.Path - const v = - asInt(entry.cache_expiration ?? entry.cacheExpiration) ?? - asInt(entry.max_age ?? entry.maxAge) - if (typeof p === "string" && p && v !== null) { - const maxAge = asInt(entry.max_age ?? entry.maxAge) - policies.push({ - path: p, - cacheExpiration: v, - ...(maxAge !== null ? { maxAge } : {}), - }) - } - } - } - return policies - } - - if (typeof raw === "object") { - for (const [key, value] of Object.entries(raw)) { - const v = asInt(value) - if (key && v !== null) policies.push({ path: key, cacheExpiration: v }) - } - } - - return policies -} - -/** 模式与目标路径的最大长度,超出直接判为不匹配,避免超长输入放大开销 */ -const MAX_GLOB_PATTERN_LENGTH = 1024 -const MAX_GLOB_TARGET_LENGTH = 4096 - -/** - * glob 匹配(对齐 Go 里 doublestar.Match 的按段语义)。 - * 输入路径与模式均已规范化(以 / 开头)。 - * - * 实现说明(重要):本函数**不使用正则**。早期版本把 glob 翻译成正则, - * 多个通配符会变成相邻的贪婪通配,匹配失败时触发灾难性回溯 - * (catastrophic backtracking)——实测单次匹配耗时 508 秒, - * 构成一个可由存储配置规则直接触发的 DoS 面。 - * - * 现改为「按 / 切段 + 逐段动态规划」: - * - 完全由 2 个及以上星号构成的段(`**`、`***`):可跨段,匹配 0..n 段 - * - 其余段:在本段内用迭代式双指针匹配,不跨 `/` - * 复杂度 O(段数 × 目标段数),无递归、无指数回溯。 - * - * 通配符语义: - * - 双星段:匹配零个或多个路径段(可跨 `/`) - * - 单星:在段内匹配任意字符(含空),**不跨** `/` - * - 问号:在段内匹配任意单个字符,**不跨** `/` - * - * 长度上限作为额外兜底,防止超长规则/路径放大计算量。 - */ -export function matchGlob(pattern: string, target: string): boolean { - if (!pattern) return false - if (pattern === target) return true - if (pattern.length > MAX_GLOB_PATTERN_LENGTH) return false - if (target.length > MAX_GLOB_TARGET_LENGTH) return false - - // 按 / 切分为路径段后逐段匹配。 - // 这样处理能自然区分「单星只在本段内通配」与「双星可跨任意多段」, - // 避免在字符级实现里纠缠回溯点的组合问题。 - const pSegs = pattern.split("/") - const tSegs = target.split("/") - - const pLen = pSegs.length - const tLen = tSegs.length - - // dp[i][j] 表示 pSegs[i:] 能否匹配 tSegs[j:] - // 采用一维滚动数组,自后向前填表,复杂度 O(n·m)、无递归。 - let next = new Array(tLen + 1).fill(false) - // 空模式只能匹配空目标 - next[tLen] = true - - for (let i = pLen - 1; i >= 0; i--) { - const seg = pSegs[i] - const cur = new Array(tLen + 1).fill(false) - // 完全由星号组成且数量 >= 2 的段视为跨段通配(`**`、`***` 等价) - const isDoubleStar = /^\*{2,}$/.test(seg) - - if (isDoubleStar) { - // 双星可匹配 0 段或 1 段后继续由自身匹配 - cur[tLen] = next[tLen] - for (let j = tLen - 1; j >= 0; j--) { - cur[j] = next[j] || cur[j + 1] - } - } else { - // 普通段(可能含单星/问号)只匹配一段 - for (let j = tLen - 1; j >= 0; j--) { - cur[j] = matchSegment(seg, tSegs[j]) && next[j + 1] - } - } - next = cur - } - - return next[0] -} - -/** - * 单段匹配:仅支持单星(不跨 /)与问号,不含双星。 - * 用迭代式双指针 + 单回溯点实现,为 O(n·m) 且无指数回溯。 - */ -function matchSegment(pattern: string, target: string): boolean { - let pi = 0 - let ti = 0 - let starPi = -1 - let starTi = -1 - - const pLen = pattern.length - const tLen = target.length - - while (ti < tLen) { - const pc = pi < pLen ? pattern[pi] : undefined - if (pc === "*") { - starPi = pi - starTi = ti - pi++ - continue - } - if (pc === "?" || pc === target[ti]) { - pi++ - ti++ - continue - } - if (starPi !== -1) { - starTi++ - ti = starTi - pi = starPi + 1 - continue - } - return false - } - - while (pi < pLen && pattern[pi] === "*") pi++ - return pi === pLen -} - -/** - * 计算某个路径在特定存储下的缓存时长(分钟)。 - * - * 匹配优先级(对齐 Go:自定义策略优先于存储基础值): - * 1. custom_cache_policies 中命中的规则——**最后一条命中的规则优先**, - * 与 Go 的顺序覆盖语义一致 - * 2. 存储级 cache_expiration - * 3. 默认 30 - * 4. 命中规则时按规则值返回(minute=0 表示不缓存) - */ -export function resolveCacheExpiration(storage: any, filePath: string): number { - const normalized = - "/" + - String(filePath || "") - .split("/") - .filter(Boolean) - .join("/") - const base = getCacheExpiration(storage) - - const policies = parseCustomCachePolicies(storage) - if (policies.length === 0) return base - - let matched: CustomCachePolicy | null = null - for (const policy of policies) { - let pattern = policy.path.trim() - if (!pattern) continue - if (!pattern.startsWith("/") && !pattern.startsWith("*")) - pattern = "/" + pattern - if ( - matchGlob(pattern, normalized) || - matchGlob(pattern, normalized.slice(1)) - ) { - matched = policy - } - } - - return matched ? matched.cacheExpiration : base -} diff --git a/src/backend/internal/model/store/driver/d1.ts b/src/backend/internal/model/store/driver/d1.ts index 99d14339..9dcead3c 100644 --- a/src/backend/internal/model/store/driver/d1.ts +++ b/src/backend/internal/model/store/driver/d1.ts @@ -6,11 +6,7 @@ * - OPENLIST_DB (别名) */ import type { Driver } from "../types" -import { - buildDdl, - buildSingleFlightDdl, - KV_SCHEMA_SQLITE, -} from "../schema" +import { buildDdl, KV_SCHEMA_SQLITE } from "../schema" /** * 判断对象是否具备 D1 绑定接口形态。 @@ -50,12 +46,8 @@ const d1Inited = new WeakMap() async function ensureSchema(db: any, env?: any): Promise { if (d1Inited.get(db)) return - // KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表一并创建 - for (const ddl of [ - ...KV_SCHEMA_SQLITE, - ...buildDdl("sqlite", env), - ...buildSingleFlightDdl("sqlite", env), - ]) { + // KV 表(map/key 格式)+ 列式表(sql 格式)一并创建 + for (const ddl of [...KV_SCHEMA_SQLITE, ...buildDdl("sqlite", env)]) { await db.prepare(ddl).run() } d1Inited.set(db, true) diff --git a/src/backend/internal/model/store/driver/mysql.ts b/src/backend/internal/model/store/driver/mysql.ts index 6a93409f..73604457 100644 --- a/src/backend/internal/model/store/driver/mysql.ts +++ b/src/backend/internal/model/store/driver/mysql.ts @@ -8,7 +8,7 @@ * - MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASS, MYSQL_NAME */ import type { Driver } from "../types" -import { buildDdl, buildSingleFlightDdl, getTablePrefix, KV_SCHEMA_MYSQL } from "../schema" +import { buildDdl, getTablePrefix, KV_SCHEMA_MYSQL } from "../schema" function isNode(): boolean { return typeof process !== "undefined" && process.release?.name === "node" @@ -52,12 +52,8 @@ let _schemaInitedPrefix: string | null = null async function ensureSchema(pool: any, env?: any): Promise { const prefix = getTablePrefix(env) if (_schemaInitedPrefix === prefix) return - // KV 表(map/key 格式)+ 列式表(sql 格式)+ singleflight 协调表一并创建 - for (const ddl of [ - ...KV_SCHEMA_MYSQL, - ...buildDdl("mysql", env), - ...buildSingleFlightDdl("mysql", env), - ]) { + // KV 表(map/key 格式)+ 列式表(sql 格式)一并创建 + for (const ddl of [...KV_SCHEMA_MYSQL, ...buildDdl("mysql", env)]) { await pool.query(ddl) } _schemaInitedPrefix = prefix diff --git a/src/backend/internal/model/store/schema.ts b/src/backend/internal/model/store/schema.ts index d707b596..b5ce1a0d 100644 --- a/src/backend/internal/model/store/schema.ts +++ b/src/backend/internal/model/store/schema.ts @@ -487,86 +487,3 @@ export const KV_SCHEMA_SQLITE: string[] = [ export const KV_SCHEMA_MYSQL: string[] = [ "CREATE TABLE IF NOT EXISTS `kv` (`key` VARCHAR(512) PRIMARY KEY, `value` LONGTEXT NOT NULL)", ] - -/** - * singleflight 协调表的逻辑名(不含 `x_` 前缀)。 - * - * 与 Go 后端**无对应表**:Go 是单进程模型,`golang.org/x/sync/singleflight` - * 只需进程内 Map;TS 侧一个部署会同时跑多个 isolate/实例,进程内 Map 无法 - * 互相看见,因此需要一张共享表来做跨实例的单飞协调(见 pkg/singleflight.ts)。 - */ -export const SINGLEFLIGHT_TABLE = "singleflight" - -/** singleflight 表的完整 SQL 表名(含 `x_` 前缀)。 */ -export function singleflightTableName(env?: any): string { - return getTablePrefix(env) + SINGLEFLIGHT_TABLE -} - -/** - * 生成 singleflight 协调表的建表语句(SQLite / Cloudflare D1 / Durable Object、 - * MySQL 方言)。 - * - * 列语义: - * - `key` 去重键(调用方给定,如 `fs.list:1:/movies`) - * - `owner` 执行者实例标识;只有 owner 能写回结果(防止被接管后串写) - * - `state` running | done | error - * - `started_at` 开始时间(epoch ms,诊断用) - * - `expires_at` 过期时间(epoch ms):running 超时表示执行者疑似崩溃, - * 可被其他实例接管;done/error 则是结果交接窗口的截止时间 - * - `result` 成功结果的 JSON 序列化(仅 done 有值) - * - `error` 失败原因文本(仅 error 有值) - * - * 注意:该表存的是**运行时在途记录**,绝不参与 db.ts 的配置往返, - * 因此刻意不放进 TABLE_NAMES —— 否则 sqlFormat.save() 的整表 DELETE - * 会在每次保存配置时把正在执行中的单飞记录清空。 - * - * 与 KV_SCHEMA / buildDdl 并列,由各 SQL 驱动在 ensureSchema 时一并执行。 - * 非 SQL 驱动(KV / Blob / 内存)没有该表,此时 singleflight 自动退回内存级。 - * - * 表名与索引名都在调用时经 `getTablePrefix(env)` 生成(而非模块加载期的常量): - * 多部署共用一个物理库、靠前缀隔离时,第二个部署的表与索引才能各自正确创建, - * 索引名若不带前缀会被首个部署的 `CREATE INDEX IF NOT EXISTS` 静默跳过。 - */ -export function buildSingleFlightDdl( - dialect: "sqlite" | "mysql", - env?: any, -): string[] { - const table = quote(singleflightTableName(env)) - const createTable = - dialect === "mysql" - ? `CREATE TABLE IF NOT EXISTS ${table} (` + - [ - `${quote("key")} VARCHAR(512) PRIMARY KEY`, - `${quote("owner")} VARCHAR(128) NOT NULL`, - `${quote("state")} VARCHAR(16) NOT NULL`, - `${quote("started_at")} BIGINT NOT NULL`, - `${quote("expires_at")} BIGINT NOT NULL`, - `${quote("result")} LONGTEXT`, - `${quote("error")} LONGTEXT`, - ].join(", ") + - `)` - : `CREATE TABLE IF NOT EXISTS ${table} (` + - [ - `${quote("key")} TEXT PRIMARY KEY`, - `${quote("owner")} TEXT NOT NULL`, - `${quote("state")} TEXT NOT NULL`, - `${quote("started_at")} INTEGER NOT NULL`, - `${quote("expires_at")} INTEGER NOT NULL`, - `${quote("result")} TEXT`, - `${quote("error")} TEXT`, - ].join(", ") + - `)` - - if (dialect === "mysql") { - // MySQL 不支持 `CREATE INDEX IF NOT EXISTS`,而索引缺失只影响过期行的回收效率 - // (建表失败会中断整个 init),因此这里刻意不建索引。 - return [createTable] - } - - // 回收过期行(崩溃残留 / 交接窗口结束)按 expires_at 过滤,走索引避免全表扫。 - return [ - createTable, - `CREATE INDEX IF NOT EXISTS ${quote(getTablePrefix(env) + "idx_singleflight_expires")} ` + - `ON ${table} (${quote("expires_at")})`, - ] -} diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index ef4f143b..869e72de 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1,17 +1,8 @@ import { resolvePath, getDb, getSettings, saveDb } from "../model/db" import { isSafeUrl } from "../../pkg/http" import { encodeDownloadPath } from "../../pkg/path" -import { singleflight } from "../../pkg/singleflight" import { canUseProxyEndpoint, normalizeExtList } from "../driver/proxy" import { FileItem, StorageDriver, calcFileType } from "../driver/base" -import { - getCachedFileTree, - setCachedFileTree, - getCachedLink, - setCachedLink, - invalidatePaths, - type CacheTarget, -} from "../cache" import { Onedrive } from "../../drivers/onedrive/driver" import { OnedriveAPP } from "../../drivers/onedrive_app/driver" import { AliyundriveOpen } from "../../drivers/aliyundrive_open/driver" @@ -1383,51 +1374,17 @@ export async function flushPendingDriverState( } /** - * 由 `resolvePath()` 结果构造缓存目标;虚拟路径(无存储)返回 null, - * 表示不参与缓存。 - */ -function toCacheTarget(resolved: any): CacheTarget | null { - if (!resolved?.storage) return null - return { - storage: resolved.storage, - cleanPath: resolved.cleanPath, - physical: resolved.physical, - isVirtual: resolved.isVirtual, - } -} - -/** - * fs 读操作的 singleflight 去重键。 + * fs 读操作的入口。 * - * 组成:操作名 + 存储 id + 存储修订号 + 虚拟路径。 - * - 带存储 id/修订号:不同存储、以及存储配置被编辑后,不会复用旧结果; - * - 带虚拟路径(调用方已做过 base_path 处理):不会跨用户串味。 - * - * 该键同时用于进程内(L1)与数据库表(L2)两级去重,因此必须稳定可复现: - * 绝不能包含时间戳、随机数、用户对象等每次调用都不同的内容。 + * 每次调用都直接回源驱动:既没有结果缓存,也没有跨请求/跨实例的调用合并, + * 目录与元信息始终反映上游的最新状态。 */ -function fsSingleflightKey( - op: string, - resolved: any, - virtualPath: string, -): string { - const storageId = resolved?.storage?.id ?? "virtual" - const revision = resolved?.storage?.modified ?? "" - return `${op}:${storageId}:${revision}:${virtualPath}` -} - export async function listItems( virtualPath: string, requestContext?: StorageRequestContext, ): Promise<{ content: FileItem[]; provider: string; storage?: any }> { const resolved = await resolvePath(virtualPath, requestContext?.env) - // 并发相同的「同一存储 + 同一目录」只向上游发起一次 list。 - // 键必须在 resolvePath 之后才能确定(需要存储 id),因此去重发生在路径解析之后。 - return singleflight( - fsSingleflightKey("fs.list", resolved, virtualPath), - () => listItemsResolved(resolved, virtualPath, requestContext), - { env: requestContext?.env }, - ) + return listItemsResolved(resolved, virtualPath, requestContext) } async function listItemsResolved( @@ -1440,32 +1397,17 @@ async function listItemsResolved( if (resolved.storage) { driverName = resolved.storage.driver - const cacheTarget = toCacheTarget(resolved) try { - // 文件树缓存:命中则完全跳过驱动(对齐 OpenList.ts「浏览只读缓存」)。 - // 注意缓存的是驱动层原始结果,权限/meta/签名仍在 server 层按请求计算。 - const cached = await getCachedFileTree(cacheTarget!, requestContext?.env) - if (cached) { - items = cached - } else { - const driver = await getDriver(driverName, resolved.storage) - // Get raw items from driver - try { - items = await driver.list(virtualPath, resolved.physical!) - } finally { - await flushPendingDriverState( - driverName, - resolved.storage, - driver, - requestContext, - ) - } - // 回源成功后写入缓存(空目录同样缓存,避免反复击穿存储)。 - // 传副本:后续 mount 合并 / type 计算会就地修改 items。 - await setCachedFileTree( - cacheTarget!, - items.map((item) => ({ ...item })), - requestContext?.env, + const driver = await getDriver(driverName, resolved.storage) + // Get raw items from driver + try { + items = await driver.list(virtualPath, resolved.physical!) + } finally { + await flushPendingDriverState( + driverName, + resolved.storage, + driver, + requestContext, ) } if (resolved.storage.status !== "work") { @@ -1584,12 +1526,7 @@ export async function getItem( requestContext?: StorageRequestContext, ): Promise<{ item: FileItem; provider: string; rawUrl: string }> { const resolved = await resolvePath(virtualPath, requestContext?.env) - // 与 listItems 同理:并发相同的「同一存储 + 同一路径」只向上游取一次元信息。 - return singleflight( - fsSingleflightKey("fs.get", resolved, virtualPath), - () => getItemResolved(resolved, virtualPath, requestContext), - { env: requestContext?.env }, - ) + return getItemResolved(resolved, virtualPath, requestContext) } async function getItemResolved( @@ -1632,25 +1569,17 @@ async function getItemResolved( } const driverName = resolved.storage ? resolved.storage.driver : "Local" - const cacheTarget = toCacheTarget(resolved) - // 下载链接缓存:命中则直接复用上次换取到的直链与元数据, - // 跳过驱动 get()(网盘换链通常是最贵、最易被限流的一步)。 - let item: FileItem | null = cacheTarget - ? await getCachedLink(cacheTarget, requestContext?.env) - : null - if (!item) { - const driver = await getDriver(driverName, resolved.storage) - try { - item = await driver.get(virtualPath, resolved.physical!) - } finally { - await flushPendingDriverState( - driverName, - resolved.storage, - driver, - requestContext, - ) - } - await setCachedLink(cacheTarget, item, requestContext?.env) + const driver = await getDriver(driverName, resolved.storage) + let item: FileItem + try { + item = await driver.get(virtualPath, resolved.physical!) + } finally { + await flushPendingDriverState( + driverName, + resolved.storage, + driver, + requestContext, + ) } if (!item.type) { item.type = calcFileType(item.name, item.is_dir) @@ -1685,8 +1614,6 @@ export async function makeDirectory( requestContext, ) } - // 新建目录:父目录列表已变化(目录自身也为空列表) - await invalidatePaths([virtualPath], requestContext?.env) } export async function renameItem( @@ -1709,10 +1636,6 @@ export async function renameItem( requestContext, ) } - // 重命名:旧路径与新路径都要失效(父目录列表同样变化) - const parent = virtualPath.slice(0, virtualPath.lastIndexOf("/")) || "/" - const newVirtualPath = `${parent === "/" ? "" : parent}/${newName}` - await invalidatePaths([virtualPath, newVirtualPath], requestContext?.env) } export async function removeItems( @@ -1737,8 +1660,6 @@ export async function removeItems( requestContext, ) } - // 删除:被删路径与其父目录缓存失效 - await invalidatePaths([itemVirtual], requestContext?.env) } } @@ -1777,8 +1698,6 @@ export async function moveItems( requestContext, ) } - // 移动:源路径与目标路径(含各自父目录)缓存全部失效 - await invalidatePaths([srcVirtual, dstVirtual], requestContext?.env) } } @@ -1817,8 +1736,6 @@ export async function copyItems( requestContext, ) } - // 复制:目标路径(含父目录)缓存失效 - await invalidatePaths([dstVirtual], requestContext?.env) } } @@ -1843,8 +1760,6 @@ export async function putItem( requestContext, ) } - // 上传/覆盖:该文件的旧直链与父目录列表缓存失效 - await invalidatePaths([virtualPath], requestContext?.env) } /** diff --git a/src/backend/pkg/singleflight.test.ts b/src/backend/pkg/singleflight.test.ts deleted file mode 100644 index caecffc0..00000000 --- a/src/backend/pkg/singleflight.test.ts +++ /dev/null @@ -1,542 +0,0 @@ -import assert from "node:assert/strict" -import { test } from "node:test" -import { - singleflight, - getSingleFlightStats, - __resetSingleFlightForTest, - __singleFlightInsertIgnoreForTest, -} from "./singleflight" -import { - buildSingleFlightDdl, - singleflightTableName, -} from "../internal/model/store/schema" - -/** - * singleflight 单飞去重回归测试。 - * - * 背景:本仓库跑在 Workers 上,一个部署有多个 isolate,进程内 Map 无法跨实例 - * 合并重复调用,因此需要数据库表做二级协调。本文件锁定以下行为: - * - * 1. L1(进程内)必须把并发的同 key 调用合并为一次执行; - * 2. L2(数据库表)必须能让等待方读到执行者的结果,而不是各自再执行一次; - * 3. **绝不脏读**:到达时若该 key 上已有「已结束」的记录,必须重新执行, - * 不能把上一轮的结果当缓存返回(否则会出现「删除后刷新仍看到被删文件」); - * 4. 业务异常原样抛出且不重试;协调层异常则降级为直接执行(可用性优先); - * 5. off / memory / db 三种模式与 SQL 方言分支正确。 - */ - -const delay = (ms: number) => new Promise((r) => setTimeout(r, ms)) - -interface MockRow { - key: string - owner: string - state: string - started_at: number - expires_at: number - result: string | null - error: string | null -} - -/** - * 基于局部 Map 的简易 SQL 驱动。 - * - * 只实现 pkg/singleflight.ts 实际会发出的那几条语句(与 store.test.ts 的 - * mock 驱动思路一致),并保留 rows 便于用例直接构造「别的实例写入的行」。 - */ -function createMockSqlDriver(dialect: "sqlite" | "mysql" = "sqlite") { - const rows = new Map() - let pendingFailure: Error | null = null - - const maybeFail = () => { - if (pendingFailure) { - const err = pendingFailure - pendingFailure = null - throw err - } - } - - const driver = { - name: dialect === "mysql" ? "mysql" : "d1", - - async query(sql: string, params: any[]): Promise { - maybeFail() - const s = sql.trim() - if (/^SELECT `owner` FROM/i.test(s)) { - const r = rows.get(String(params[0])) - return r ? [{ owner: r.owner }] : [] - } - if (/^SELECT `state`/i.test(s)) { - const r = rows.get(String(params[0])) - return r - ? [ - { - state: r.state, - result: r.result, - error: r.error, - expires_at: r.expires_at, - }, - ] - : [] - } - throw new Error(`mock-sql unsupported query: ${sql}`) - }, - - async execute(sql: string, params: any[]): Promise { - maybeFail() - const s = sql.trim() - - // 回收:过期行 或 已结束(state <> running)的行 - if ( - /^DELETE FROM .* WHERE `key` = \? AND \(`expires_at` <= \? OR `state` <> \?\)/i.test( - s, - ) - ) { - const [key, now, running] = params - const r = rows.get(String(key)) - if (r && (r.expires_at <= Number(now) || r.state !== String(running))) { - rows.delete(String(key)) - } - return - } - - // 执行者主动释放 - if (/^DELETE FROM .* WHERE `key` = \? AND `owner` = \?/i.test(s)) { - const [key, owner] = params - const r = rows.get(String(key)) - if (r && r.owner === String(owner)) rows.delete(String(key)) - return - } - - // 抢占:INSERT [OR] IGNORE,主键冲突即忽略(模拟数据库的原子性) - const ins = s.match( - /^INSERT (?:OR )?IGNORE INTO .* \(([^)]+)\) VALUES \(([^)]+)\)/i, - ) - if (ins) { - const cols = ins[1].split(",").map((c) => c.trim().replace(/`/g, "")) - const key = String(params[0]) - if (rows.has(key)) return - const row: any = {} - cols.forEach((c, i) => { - row[c] = params[i] - }) - rows.set(key, row as MockRow) - return - } - - // 写回结果 - if ( - /^UPDATE .* SET `state` = \?, `result` = \?, `error` = \?, `expires_at` = \? WHERE `key` = \? AND `owner` = \?/i.test( - s, - ) - ) { - const [state, result, error, expires, key, owner] = params - const r = rows.get(String(key)) - if (r && r.owner === String(owner)) { - r.state = String(state) - r.result = result ?? null - r.error = error ?? null - r.expires_at = Number(expires) - } - return - } - - // 续租 - if ( - /^UPDATE .* SET `expires_at` = \? WHERE `key` = \? AND `owner` = \? AND `state` = \?/i.test( - s, - ) - ) { - const [expires, key, owner, state] = params - const r = rows.get(String(key)) - if (r && r.owner === String(owner) && r.state === String(state)) { - r.expires_at = Number(expires) - } - return - } - - throw new Error(`mock-sql unsupported execute: ${sql}`) - }, - - isAvailable: async () => true, - init: async () => {}, - get: async () => null, - put: async () => {}, - delete: async () => {}, - list: async () => [], - health: async () => ({ connected: true }), - - /** 测试钩子:直接读写表内容,用于构造「别的实例」的行。 */ - rows, - failNext(err: Error) { - pendingFailure = err - }, - } - - return driver -} - -// ── 建表语句 ──────────────────────────────────────────────────────────────── - -test("schema: singleflight 表名带 x_ 前缀,且 DDL 覆盖两种方言", () => { - assert.equal(singleflightTableName(), "x_singleflight") - - const sqlite = buildSingleFlightDdl("sqlite").join("\n") - assert.match(sqlite, /CREATE TABLE IF NOT EXISTS `x_singleflight`/) - assert.match(sqlite, /`expires_at` INTEGER NOT NULL/) - // 回收过期行需要按 expires_at 过滤,SQLite 侧建索引 - // 索引名与表名一样带 x_ 前缀:多部署共用一个物理库时第二个部署才能建出自己的索引 - assert.match(sqlite, /CREATE INDEX IF NOT EXISTS `x_idx_singleflight_expires`/) - assert.match(sqlite, /ON `x_singleflight` \(`expires_at`\)/) - - const mysql = buildSingleFlightDdl("mysql").join("\n") - assert.match(mysql, /CREATE TABLE IF NOT EXISTS `x_singleflight`/) - assert.match(mysql, /`key` VARCHAR\(512\) PRIMARY KEY/) - assert.match(mysql, /`expires_at` BIGINT NOT NULL/) - // MySQL 不支持 CREATE INDEX IF NOT EXISTS,因此不建索引 - assert.doesNotMatch(mysql, /CREATE INDEX/) -}) - -test("schema: singleflight 表不得进入配置往返(否则保存配置会清空在途记录)", async () => { - const schema = await import("../internal/model/store/schema") - assert.ok( - !(schema.TABLE_NAMES as readonly string[]).includes("singleflight"), - "singleflight 是运行时数据,不能放进 TABLE_NAMES", - ) - assert.ok( - !(schema.DDL_TABLE_NAMES as readonly string[]).includes("singleflight"), - "建表由驱动单独执行(buildSingleFlightDdl),不混入列式表 DDL", - ) -}) - -test("SQL 方言:mysql 用 INSERT IGNORE,d1/do(SQLite)用 INSERT OR IGNORE", () => { - assert.equal(__singleFlightInsertIgnoreForTest("mysql"), "INSERT IGNORE") - assert.equal(__singleFlightInsertIgnoreForTest("d1"), "INSERT OR IGNORE") - assert.equal(__singleFlightInsertIgnoreForTest("do"), "INSERT OR IGNORE") -}) - -// ── L1:进程内合并 ────────────────────────────────────────────────────────── - -test("memory: 并发相同 key 只执行一次,所有调用方拿到同一结果", async () => { - __resetSingleFlightForTest() - let executed = 0 - - const fn = async () => { - executed++ - await delay(30) - return { value: "shared" } - } - - const results = await Promise.all( - Array.from({ length: 10 }, () => - singleflight("mem-a", fn, { mode: "memory", env: {} }), - ), - ) - - assert.equal(executed, 1, "10 个并发调用只应触发 1 次执行") - for (const r of results) assert.deepEqual(r, { value: "shared" }) - - const stats = getSingleFlightStats() - assert.equal(stats.executed, 1) - assert.equal(stats.coalesced, 9, "其余 9 次应被 L1 合并") -}) - -test("memory: 执行结束后不缓存结果,下一次调用会重新执行", async () => { - __resetSingleFlightForTest() - let executed = 0 - const fn = async () => { - executed++ - return executed - } - - assert.equal(await singleflight("mem-b", fn, { mode: "memory", env: {} }), 1) - assert.equal(await singleflight("mem-b", fn, { mode: "memory", env: {} }), 2) - assert.equal(executed, 2, "singleflight 只合并并发,不做结果缓存") -}) - -test("memory: 失败传播给所有等待方,且不残留导致后续调用被误合并", async () => { - __resetSingleFlightForTest() - let executed = 0 - const failing = async () => { - executed++ - await delay(10) - throw new Error("upstream boom") - } - - const settled = await Promise.allSettled([ - singleflight("mem-c", failing, { mode: "memory", env: {} }), - singleflight("mem-c", failing, { mode: "memory", env: {} }), - ]) - assert.equal(executed, 1) - assert.ok(settled.every((s) => s.status === "rejected")) - assert.match(String((settled[0] as any).reason?.message), /upstream boom/) - - // 在途表必须已清理:下一次调用应重新执行而不是复用已失败的 Promise - assert.equal( - await singleflight("mem-c", async () => "ok", { mode: "memory", env: {} }), - "ok", - ) - assert.equal(getSingleFlightStats().active, 0) -}) - -test("off: 完全关闭去重,每次调用都直接执行", async () => { - __resetSingleFlightForTest() - let executed = 0 - const fn = async () => { - executed++ - await delay(20) - return executed - } - - await Promise.all( - Array.from({ length: 3 }, () => - singleflight("off-a", fn, { mode: "off", env: {} }), - ), - ) - assert.equal(executed, 3) - assert.equal(getSingleFlightStats().mode, "off") -}) - -// ── L2:数据库表协调 ──────────────────────────────────────────────────────── - -test("db: 到达时发现执行者正在运行 → 等待并共享其结果(不重复执行)", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - const now = Date.now() - - // 模拟「另一个 isolate 已在执行」:running 行由 other 持有 - driver.rows.set("db-share", { - key: "db-share", - owner: "other-instance", - state: "running", - started_at: now, - expires_at: now + 5000, - result: null, - error: null, - }) - // 30ms 后对方写回结果 - setTimeout(() => { - const row = driver.rows.get("db-share")! - row.state = "done" - row.result = JSON.stringify({ from: "leader" }) - row.expires_at = Date.now() + 1000 - }, 30) - - let executed = 0 - const out = await singleflight( - "db-share", - async () => { - executed++ - return { from: "self" } - }, - { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 3000 }, - ) - - assert.deepEqual(out, { from: "leader" }) - assert.equal(executed, 0, "等待方不应自己执行") - assert.equal(getSingleFlightStats().shared, 1) -}) - -test("db: 到达时记录已结束 → 必须重新执行,绝不返回旧结果(防脏读)", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - const now = Date.now() - - // 上一轮调用留下的交接记录:内容已经过期 - driver.rows.set("db-stale", { - key: "db-stale", - owner: "other-instance", - state: "done", - started_at: now - 100, - expires_at: now + 10_000, - result: JSON.stringify({ value: "stale" }), - error: null, - }) - - let executed = 0 - const out = await singleflight( - "db-stale", - async () => { - executed++ - return { value: "fresh" } - }, - { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 500 }, - ) - - assert.deepEqual(out, { value: "fresh" }, "新到达者必须拿到新鲜结果") - assert.equal(executed, 1) - assert.equal(getSingleFlightStats().shared, 0, "不应命中交接记录") -}) - -test("db: 抢到锁后写回结果,行状态为 done 且带交接窗口", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - - const out = await singleflight("db-lead", async () => ({ n: 7 }), { - mode: "db", - driver, - env: {}, - handoffMs: 1234, - }) - - assert.deepEqual(out, { n: 7 }) - const row = driver.rows.get("db-lead") - assert.ok(row, "执行者应留下交接记录供等待方读取") - assert.equal(row!.state, "done") - assert.equal(row!.result, JSON.stringify({ n: 7 })) - assert.equal(row!.owner.length > 0, true) - assert.ok( - row!.expires_at > Date.now() + 1000 && row!.expires_at <= Date.now() + 1400, - "expires_at 应约为 now + handoffMs", - ) - assert.equal(getSingleFlightStats().executed, 1) -}) - -test("db: handoffMs=0 时执行完立即释放,不留交接记录", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - - await singleflight("db-nohand", async () => "v", { - mode: "db", - driver, - env: {}, - handoffMs: 0, - }) - - assert.equal(driver.rows.size, 0, "交接窗口为 0 应立即删除行") -}) - -test("db: 业务异常原样抛出,且不会被降级重试(只执行一次)", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - let executed = 0 - - await assert.rejects( - singleflight( - "db-err", - async () => { - executed++ - throw new Error("driver exploded") - }, - { mode: "db", driver, env: {} }, - ), - /driver exploded/, - ) - - assert.equal(executed, 1, "业务异常不得触发降级重试") - assert.equal(getSingleFlightStats().fallback, 0) - // 失败也应写回,让等待方共享失败而不是各自重试上游 - assert.equal(driver.rows.get("db-err")!.state, "error") -}) - -test("db: 协调层异常(DB 不可用)→ 降级为直接执行,业务不受影响", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - driver.failNext(new Error("D1_ERROR: no such table: x_singleflight")) - - let executed = 0 - const out = await singleflight( - "db-degrade", - async () => { - executed++ - return "business-ok" - }, - { mode: "db", driver, env: {} }, - ) - - assert.equal(out, "business-ok") - assert.equal(executed, 1) - assert.equal(getSingleFlightStats().fallback, 1) -}) - -test("db: 等待超时后自行执行,不会无限挂起", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - const now = Date.now() - - // 执行者一直 running 且迟迟不写回 - driver.rows.set("db-timeout", { - key: "db-timeout", - owner: "other-instance", - state: "running", - started_at: now, - expires_at: now + 60_000, - result: null, - error: null, - }) - - let executed = 0 - const out = await singleflight( - "db-timeout", - async () => { - executed++ - return "self-result" - }, - { mode: "db", driver, env: {}, pollMs: 5, waitTimeoutMs: 40 }, - ) - - assert.equal(out, "self-result") - assert.equal(executed, 1, "等待超时后应自行执行,保证请求不被拖死") -}) - -// ── auto 模式 ─────────────────────────────────────────────────────────────── - -test("auto: 探测不到 SQL 驱动 → memory(不会因为无数据库而报错)", async () => { - __resetSingleFlightForTest() - let executed = 0 - const out = await singleflight( - "auto-mem", - async () => { - executed++ - return "ok" - }, - { mode: "auto", driver: null, env: {} }, - ) - - assert.equal(out, "ok") - assert.equal(executed, 1) - assert.equal(getSingleFlightStats().mode, "memory") -}) - -test("auto: 探测到 SQL 驱动 → db", async () => { - __resetSingleFlightForTest() - const driver = createMockSqlDriver() - const out = await singleflight("auto-db", async () => "ok", { - mode: "auto", - driver, - env: {}, - }) - - assert.equal(out, "ok") - assert.equal(getSingleFlightStats().mode, "db") - assert.equal(driver.rows.get("auto-db")!.state, "done") -}) - -test("环境变量 SINGLEFLIGHT 可强制模式,非法值回退 auto", async () => { - __resetSingleFlightForTest() - let executed = 0 - const fn = async () => { - executed++ - await delay(15) - return executed - } - - // off:3 次调用全部执行 - await Promise.all( - Array.from({ length: 3 }, () => - singleflight("env-off", fn, { env: { SINGLEFLIGHT: "off" } }), - ), - ) - assert.equal(executed, 3) - assert.equal(getSingleFlightStats().mode, "off") - - __resetSingleFlightForTest() - executed = 0 - // 非法值 → auto → 无 SQL 驱动 → memory(仍然合并) - await Promise.all( - Array.from({ length: 3 }, () => - singleflight("env-bad", fn, { env: { SINGLEFLIGHT: "nonsense" } }), - ), - ) - assert.equal(executed, 1) - assert.equal(getSingleFlightStats().mode, "memory") -}) diff --git a/src/backend/pkg/singleflight.ts b/src/backend/pkg/singleflight.ts deleted file mode 100644 index 29e191bc..00000000 --- a/src/backend/pkg/singleflight.ts +++ /dev/null @@ -1,783 +0,0 @@ -/** - * singleflight(单飞)—— 把「同一时刻的同一个调用」合并成一次执行。 - * - * ## 为什么 TS 侧需要它(Go 侧不需要) - * - * Go 后端是**单进程**模型,`golang.org/x/sync/singleflight` 用一个进程内 Map - * 就够了:同一时刻只有一个 goroutine 真正执行,其余等待者共享结果。 - * - * 本仓库跑在 Cloudflare Workers / EdgeOne Edge Functions 上,**一个部署会同时 - * 存在多个 isolate / 实例**,且请求会被随机分发到不同 isolate。进程内 Map 只能 - * 合并「落在同一个 isolate 且时间重叠」的调用,跨 isolate 的重复请求依然会各自 - * 打一次上游网盘 API —— 这正是限流、封号、CPU 超时的主要来源。 - * - * ## 两级去重 - * - * L1 进程内(永远开启):`Map`,零成本,同一 isolate 内直接复用 - * 同一个 Promise。任何模式下都生效。 - * L2 跨实例(仅 db 模式):用数据库表 `x_singleflight` 做协调 —— 抢到行的一方 - * 执行,其余实例轮询该行,执行完成后**直接读取结果**, - * 因此上游只被调用一次。 - * - * ## 模式(环境变量 SINGLEFLIGHT) - * - * auto(默认)有 SQL 库(d1 / mysql / do)→ db;否则 → memory - * db 强制走数据库表协调;协调层出错时自动降级为直接执行 - * memory 仅进程内合并(等价于 Go 的进程内 singleflight) - * off 关闭去重,每次调用都直接执行 - * - * ## 可用性优先 - * - * 单飞是**优化**而非**依赖**:任何协调层面的异常(表不存在、DB 超时、结果无法 - * 序列化、执行者崩溃、等待超时)都不会让业务失败,最坏情况退化为「各自执行一次」。 - * 但**业务函数自身的异常会原样抛出**,且不会被误判为协调失败而重试 —— 见 - * `FlightOutcome` 的三态设计。 - */ - -import type { Driver } from "../internal/model/store/types" -import { getStorageBackend } from "../internal/model/store/backend" -import { singleflightTableName } from "../internal/model/store/schema" - -/** 单飞模式。 */ -export type SingleFlightMode = "off" | "memory" | "db" - -/** 模式 + auto(由运行时探测决定实际模式)。 */ -export type SingleFlightModeInput = SingleFlightMode | "auto" - -/** 支持 SQL 的驱动子集(d1 / mysql / do 具备 query + execute)。 */ -type SqlDriverLike = Pick & - Partial> - -/** 单次调用的可选项(多数场景无需传,用默认值 + 环境变量即可)。 */ -export interface SingleFlightOptions { - /** 覆盖模式;默认取环境变量 SINGLEFLIGHT(缺省 auto)。 */ - mode?: SingleFlightModeInput - /** 环境上下文(用于读取环境变量、探测存储驱动)。 */ - env?: any - /** - * 显式指定用于协调的 SQL 驱动。 - * 一般不需要:留空时自动取当前存储后端(见 getStorageBackend)。 - */ - driver?: SqlDriverLike | null - /** - * 结果交接窗口(ms,默认 1000)。 - * - * 执行完成后,结果/错误在表中保留这么久,供**正在等待的**并发实例读取。 - * 它**不是**结果缓存:新到达的调用若看到已结束的记录,会回收该行并重新执行 - * (见 dbFlight 注释),因此不存在「窗口内读到旧数据」的脏读。 - * 设 0 表示执行完立即释放,此时等待方基本拿不到结果、会各自执行一次。 - */ - handoffMs?: number - /** - * 执行者持有锁的最长时间(ms,默认 30000)。 - * 期间执行者会按 1/3 周期续租;若执行者崩溃,锁到期后其他实例可接管。 - */ - lockTtlMs?: number - /** - * 等待方的轮询间隔(ms,默认 200)。 - * - * 刻意不放太小:等待方每 tick 都是一次对协调表的 SELECT,间隔过小会显著 - * 消耗 D1 的 subrequest 配额与 CPU(最坏情况 ≈ waitTimeoutMs / pollMs 次 - * SELECT × 最多 3 轮)。默认 200ms 下单个等待请求最多约 75 次 SELECT/轮, - * 目录列表 / 元信息这类上游调用通常远快于此,多等一两个 tick 无感。 - */ - pollMs?: number - /** - * 等待方最长等待时间(ms,默认 15000)。 - * 超时后不再等待,自行执行(保证请求不会因为别人慢而被拖死)。 - */ - waitTimeoutMs?: number -} - -/** 运行期统计,用于 /debug/info 与排障。 */ -export interface SingleFlightStats { - /** 最近一次生效的模式(off/memory/db)。 */ - mode: SingleFlightMode - /** 真正执行了业务函数的次数。 */ - executed: number - /** 被进程内合并掉的次数(L1 命中)。 */ - coalesced: number - /** 从数据库表读到他人结果的次数(L2 命中)。 */ - shared: number - /** DB 协调异常而降级的次数。 */ - fallback: number - /** 业务函数抛错的次数。 */ - failed: number - /** 当前在途键数量。 */ - active: number -} - -// ── 默认值与环境变量 ──────────────────────────────────────────────────────── - -const DEFAULT_HANDOFF_MS = 1000 -const DEFAULT_LOCK_TTL_MS = 30_000 -const DEFAULT_POLL_MS = 200 -const DEFAULT_WAIT_TIMEOUT_MS = 15_000 - -/** 读取配置:env 绑定优先,其次 process.env(Node / 本地开发)。 */ -function readEnvValue(env: any, name: string): string | undefined { - try { - const fromEnv = env && typeof env === "object" ? env[name] : undefined - if (fromEnv !== undefined && fromEnv !== null && String(fromEnv) !== "") { - return String(fromEnv) - } - } catch { - // env 可能是 Proxy 等异常对象,忽略 - } - try { - const proc = typeof process !== "undefined" ? (process as any) : undefined - const v = proc?.env?.[name] - if (v !== undefined && v !== null && String(v) !== "") return String(v) - } catch { - // 忽略 - } - return undefined -} - -function readEnvInt(env: any, name: string, fallback: number): number { - const raw = readEnvValue(env, name) - if (raw === undefined) return fallback - const n = parseInt(raw, 10) - return Number.isFinite(n) && n >= 0 ? n : fallback -} - -/** 解析 SINGLEFLIGHT 环境变量(非法值按 auto 处理并告警一次)。 */ -let warnedBadMode = false -function readModeInput(env: any): SingleFlightModeInput { - const raw = readEnvValue(env, "SINGLEFLIGHT")?.trim().toLowerCase() - if (!raw) return "auto" - if (raw === "auto") return "auto" - if (raw === "off" || raw === "false" || raw === "0" || raw === "none") { - return "off" - } - if (raw === "memory" || raw === "mem" || raw === "local" || raw === "proc") { - return "memory" - } - if (raw === "db" || raw === "database" || raw === "sql" || raw === "table") { - return "db" - } - if (!warnedBadMode) { - warnedBadMode = true - console.warn( - `[singleflight] Unknown SINGLEFLIGHT="${raw}", falling back to auto ` + - `(expected one of: auto, off, memory, db)`, - ) - } - return "auto" -} - -// ── 实例标识 ──────────────────────────────────────────────────────────────── - -/** - * 本实例(isolate / 进程)的随机标识。 - * - * 用途:只有写入该值的执行者才能回写结果,避免「锁被接管后,原执行者迟到写回」 - * 造成结果错乱。 - */ -const INSTANCE_ID = (() => { - try { - const c: any = typeof crypto !== "undefined" ? crypto : undefined - if (c && typeof c.randomUUID === "function") return c.randomUUID() - } catch { - // 忽略 - } - return `inst-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}` -})() - -// ── 统计 ──────────────────────────────────────────────────────────────────── - -const stats: SingleFlightStats = { - mode: "memory", - executed: 0, - coalesced: 0, - shared: 0, - fallback: 0, - failed: 0, - active: 0, -} - -/** 当前统计快照(含当前在途键数量)。 */ -export function getSingleFlightStats(): SingleFlightStats { - return { ...stats, active: inflight.size } -} - -// ── L1:进程内合并 ────────────────────────────────────────────────────────── - -/** key → 在途 Promise。同一 isolate 内所有模式共用。 */ -const inflight = new Map>() - -/** - * 进程内合并:命中则复用已有 Promise;未命中则执行 factory 并登记。 - * - * 清理用 `then(onOk, onErr)` 而非 `finally()`:后者会产生一个「派生 Promise」, - * 若业务失败而调用方只 await 了原 Promise,派生 Promise 的 rejection 无人处理, - * 在 Node 下会触发 unhandledRejection 告警。 - */ -function coalesce(key: string, factory: () => Promise): Promise { - const existing = inflight.get(key) - if (existing) { - stats.coalesced++ - return existing as Promise - } - - const pending = factory() - inflight.set(key, pending) - const release = () => { - // 仅清理自己登记的那一个,避免误删后来者的在途项 - if (inflight.get(key) === pending) inflight.delete(key) - } - pending.then(release, release) - return pending -} - -// ── L2:数据库表协调 ──────────────────────────────────────────────────────── - -/** - * 协调层的结果三态。 - * - * 关键点:**业务异常与协调异常必须分开**。 - * - 业务异常:原样抛给调用方,绝不重试(否则上游会被打两次); - * - 协调异常:降级为「自己直接执行一次」,保证业务可用。 - * 用三态返回值而非异常标记,可避免给用户错误对象打标记(可能被冻结)的坑。 - */ -type FlightOutcome = - | { kind: "value"; value: T } - /** 执行失败;error 为业务函数抛出的原始异常,或等待方收到的远端失败原因 */ - | { kind: "business-error"; error: unknown } - /** 抢锁 / 轮询 / 写回等协调环节出错 —— 调用方应降级为直接执行 */ - | { kind: "coordination-error"; error: unknown } - -/** 反引号包裹标识符(SQLite 与 MySQL 均支持)。 */ -function q(name: string): string { - return "`" + name + "`" -} - -function supportsSql(driver: any): driver is SqlDriverLike { - return ( - !!driver && - typeof driver.query === "function" && - typeof driver.execute === "function" - ) -} - -/** SQL 方言:mysql 驱动用 `INSERT IGNORE`,其余(d1 / do,均 SQLite)用 `INSERT OR IGNORE`。 */ -function insertIgnoreClause(driver: SqlDriverLike): string { - return String(driver.name || "").toLowerCase() === "mysql" - ? "INSERT IGNORE" - : "INSERT OR IGNORE" -} - -/** 解析用于协调的 SQL 驱动;不可用时返回 null(调用方降级为内存级)。 */ -async function resolveSqlDriver( - env: any, - explicit: SqlDriverLike | null | undefined, -): Promise { - if (explicit !== undefined) return explicit - try { - const { driver } = await getStorageBackend(env) - return supportsSql(driver) ? driver : null - } catch { - // 无可用存储后端(如 serverless 未绑定)→ 内存级 - return null - } -} - -interface ResolvedOptions { - mode: SingleFlightMode - /** 解析出的协调驱动(仅 db 模式有值)。 */ - driver: SqlDriverLike | null - handoffMs: number - lockTtlMs: number - pollMs: number - waitTimeoutMs: number -} - -async function resolveOptions( - options: SingleFlightOptions, -): Promise { - const env = options.env - const input = options.mode ?? readModeInput(env) - - // 只在需要时才探测存储驱动:off / memory 模式完全不需要碰存储层。 - const needsDriver = input === "auto" || input === "db" - const driver = needsDriver - ? await resolveSqlDriver(env, options.driver) - : null - - const mode: SingleFlightMode = - input === "auto" ? (driver ? "db" : "memory") : input - - return { - mode, - driver, - handoffMs: - options.handoffMs ?? - readEnvInt(env, "SINGLEFLIGHT_HANDOFF_MS", DEFAULT_HANDOFF_MS), - lockTtlMs: - options.lockTtlMs ?? - readEnvInt(env, "SINGLEFLIGHT_LOCK_TTL_MS", DEFAULT_LOCK_TTL_MS), - pollMs: - options.pollMs ?? - readEnvInt(env, "SINGLEFLIGHT_POLL_MS", DEFAULT_POLL_MS), - waitTimeoutMs: - options.waitTimeoutMs ?? - readEnvInt(env, "SINGLEFLIGHT_WAIT_MS", DEFAULT_WAIT_TIMEOUT_MS), - } -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -/** - * 抢占 key。 - * - * 三步走,全部依赖数据库自身的原子性(不依赖「读后写」,因此无需事务): - * 1. 回收可回收的行 —— 崩溃残留的过期 running 行,以及**已结束**的 - * done/error 行(它们只是留给等待方读取的交接记录,谁先来谁回收); - * 2. `INSERT OR IGNORE` —— 主键冲突即表示已有他人在执行,我们成为等待方; - * 3. 回读 owner 确认归属 —— 避免「插入被忽略但仍以为自己是执行者」。 - * - * 注意第 1 步的删除条件:**未过期的 running 行绝不能被删**,否则正在执行的 - * 调用会被并发接管,单飞形同虚设。 - */ -async function dbAcquire( - driver: SqlDriverLike, - table: string, - key: string, - owner: string, - opts: ResolvedOptions, - env: any, -): Promise { - const t = q(table) - const now = Date.now() - - await driver.execute!( - `DELETE FROM ${t} WHERE ${q("key")} = ? AND (` + - `${q("expires_at")} <= ? OR ${q("state")} <> ?)`, - [key, now, "running"], - env, - ) - - await driver.execute!( - `${insertIgnoreClause(driver)} INTO ${t} (` + - `${q("key")},${q("owner")},${q("state")},${q("started_at")},${q("expires_at")},` + - `${q("result")},${q("error")}) VALUES (?,?,?,?,?,NULL,NULL)`, - [key, owner, "running", now, now + opts.lockTtlMs], - env, - ) - - const rows = await driver.query!( - `SELECT ${q("owner")} FROM ${t} WHERE ${q("key")} = ?`, - [key], - env, - ) - return rows.length > 0 && String((rows[0] as any).owner) === owner -} - -/** - * 执行者续租:把 expires_at 往后推。 - * - * 为什么需要:目录很大时上游 list 可能超过 lockTtl,若不续租,等待方会误判 - * 「执行者已崩溃」而接管,导致上游被调用两次 —— 恰好是单飞要消除的行为。 - */ -function startHeartbeat( - driver: SqlDriverLike, - table: string, - key: string, - owner: string, - opts: ResolvedOptions, - env: any, -): any { - try { - const interval = Math.max(1000, Math.floor(opts.lockTtlMs / 3)) - const timer = setInterval(() => { - Promise.resolve( - driver.execute!( - `UPDATE ${q(table)} SET ${q("expires_at")} = ? ` + - `WHERE ${q("key")} = ? AND ${q("owner")} = ? AND ${q("state")} = ?`, - [Date.now() + opts.lockTtlMs, key, owner, "running"], - env, - ), - ).catch(() => { - // 续租失败不影响业务:最坏情况是被接管后多执行一次 - }) - }, interval) - // Node 下不要因为心跳定时器而阻止进程退出(Workers 无此方法,可选调用) - ;(timer as any)?.unref?.() - return timer - } catch { - // 运行时不支持定时器(极端环境):放弃续租,退化为「锁到期可被接管」 - return null - } -} - -/** 停止心跳;定时器不可用时为 no-op。 */ -function stopHeartbeat(timer: any): void { - if (timer === null || timer === undefined) return - try { - clearInterval(timer) - } catch { - // 忽略 - } -} - -/** - * 执行者写回结果。 - * - * `WHERE key = ? AND owner = ?` 是必需的:若锁已被接管(原执行者卡顿超过 - * lockTtl 被他人接管),迟到写回必须作废,否则会覆盖接管者的正确结果。 - * - * 写回失败只告警不抛出:协调是尽力而为,业务结果已拿到,不该因为写不进去而失败。 - */ -async function dbPublish( - driver: SqlDriverLike, - table: string, - key: string, - owner: string, - state: "done" | "error", - result: string | null, - error: string | null, - opts: ResolvedOptions, - env: any, -): Promise { - const t = q(table) - try { - if (opts.handoffMs <= 0) { - // 交接窗口为 0:直接释放,让等待方立刻可以重新抢锁 - await driver.execute!( - `DELETE FROM ${t} WHERE ${q("key")} = ? AND ${q("owner")} = ?`, - [key, owner], - env, - ) - return - } - await driver.execute!( - `UPDATE ${t} SET ${q("state")} = ?, ${q("result")} = ?, ${q("error")} = ?, ` + - `${q("expires_at")} = ? WHERE ${q("key")} = ? AND ${q("owner")} = ?`, - [state, result, error, Date.now() + opts.handoffMs, key, owner], - env, - ) - } catch (err) { - console.warn( - `[singleflight] Failed to publish result for "${key}" (ignored):`, - err instanceof Error ? err.message : err, - ) - } -} - -/** 等待方的等待结果。 */ -type WaitOutcome = - | { status: "ok"; value: any } - | { status: "error"; error: string } - /** 行消失 / 执行者疑似崩溃 / 等待超时 —— 交给调用方自行处理 */ - | { status: "give-up" } - -/** 表中某个 key 的当前行(null 表示不存在)。 */ -interface ProbeRow { - state: string - result: any - error: any - expires_at: number -} - -async function dbProbe( - driver: SqlDriverLike, - table: string, - key: string, - env: any, -): Promise { - const rows = await driver.query!( - `SELECT ${q("state")},${q("result")},${q("error")},${q("expires_at")} ` + - `FROM ${q(table)} WHERE ${q("key")} = ?`, - [key], - env, - ) - if (rows.length === 0) return null - const row: any = rows[0] - return { - state: String(row.state ?? ""), - result: row.result, - error: row.error, - expires_at: Number(row.expires_at), - } -} - -/** - * 轮询等待执行者写回结果。 - * - * 仅在「确认执行者处于 running 状态」后调用,因此这里读到 done/error 就是 - * 本次并发去重的目标结果。 - */ -async function dbWait( - driver: SqlDriverLike, - table: string, - key: string, - opts: ResolvedOptions, - env: any, -): Promise { - const deadline = Date.now() + opts.waitTimeoutMs - - for (;;) { - const now = Date.now() - if (now >= deadline) return { status: "give-up" } - await sleep(Math.max(1, Math.min(opts.pollMs, deadline - now))) - - const row = await dbProbe(driver, table, key, env) - // 行已被回收(交接记录被新到达者回收,或执行者主动释放)→ 不再等待 - if (!row) return { status: "give-up" } - - if (row.state === "done") { - if (row.result === null || row.result === undefined) { - return { status: "give-up" } - } - try { - return { status: "ok", value: JSON.parse(String(row.result)) } - } catch { - // 结果不可反序列化(理论上不该发生)→ 自行执行 - return { status: "give-up" } - } - } - if (row.state === "error") { - return { status: "error", error: String(row.error ?? "") } - } - // running 且已过期 → 执行者疑似崩溃 - if (row.expires_at <= Date.now()) return { status: "give-up" } - } -} - -/** 执行者路径:真正调用业务函数,并把结果/异常写回表。 */ -async function dbLead( - driver: SqlDriverLike, - table: string, - key: string, - owner: string, - fn: () => Promise, - opts: ResolvedOptions, - env: any, -): Promise> { - const heartbeat = startHeartbeat(driver, table, key, owner, opts, env) - - let value: T - try { - value = await fn() - } catch (err) { - stopHeartbeat(heartbeat) - await dbPublish( - driver, - table, - key, - owner, - "error", - null, - err instanceof Error ? err.message : String(err), - opts, - env, - ) - return { kind: "business-error", error: err } - } - stopHeartbeat(heartbeat) - - // 结果必须可 JSON 往返才能共享给其他实例;不可序列化时直接释放锁, - // 让等待方自行执行(本次调用方仍然拿到正确的返回值)。 - let payload: string | null = null - try { - payload = JSON.stringify(value === undefined ? null : value) - } catch { - payload = null - } - if (payload === null) { - await dbPublish( - driver, - table, - key, - owner, - "done", - null, - null, - { - ...opts, - handoffMs: 0, - }, - env, - ) - } else { - await dbPublish(driver, table, key, owner, "done", payload, null, opts, env) - } - return { kind: "value", value } -} - -/** - * 数据库表协调的完整流程。 - * - * ## 关键判定:什么情况下才接受「别人的结果」 - * - * 只有在**本调用到达时执行者正处于 running 状态**时,才等待并接受它的结果 —— - * 这代表两者确实并发重叠,是真正的「单飞去重」。 - * - * 反过来,如果到达时该 key 上已经是一行**已结束**的 done/error(上一轮调用留下的 - * 交接记录),本次调用**绝不接受**它,而是回收该行、自己成为新的执行者。 - * - * 这一点是「不引入脏读」的核心:交接记录因此只是给**正在等待的**并发者读取的, - * 不会变成一个有 TTL 的结果缓存。否则会出现「删除文件后 1 秒内刷新列表仍看到 - * 被删文件」这类问题(handoffMs 窗口内命中旧结果)。 - * - * @returns 三态结果,由调用方决定是否降级 - */ -async function dbFlight( - driver: SqlDriverLike, - key: string, - fn: () => Promise, - opts: ResolvedOptions, - env: any, -): Promise> { - const table = singleflightTableName(env) - const owner = INSTANCE_ID - - try { - // 最多三轮:足够覆盖「探测到 running → 等待」「等待落空 → 接管」 - // 「抢占失败(别人刚抢到)→ 再探测」这几种交错。 - for (let attempt = 0; attempt < 3; attempt++) { - const row = await dbProbe(driver, table, key, env) - - if (row && row.state === "running" && row.expires_at > Date.now()) { - // 到达时正在执行 → 并发去重,等待并接受其结果 - const wait = await dbWait(driver, table, key, opts, env) - if (wait.status === "ok") { - stats.shared++ - return { kind: "value", value: wait.value as T } - } - if (wait.status === "error") { - // 共享他人的失败:与 Go singleflight 语义一致,等待方同样收到错误, - // 而不是各自再打一次上游(那正是要消除的行为)。 - stats.shared++ - return { - kind: "business-error", - error: new Error(wait.error || "singleflight: shared call failed"), - } - } - // 执行者崩溃 / 等待超时 → 下一轮尝试接管 - continue - } - - // 无行,或行已结束(done/error 交接记录)→ 回收并抢占,成为新执行者 - if (await dbAcquire(driver, table, key, owner, opts, env)) { - stats.executed++ - const outcome = await dbLead(driver, table, key, owner, fn, opts, env) - if (outcome.kind === "business-error") stats.failed++ - return outcome - } - // 抢占失败:别人刚成为执行者 → 下一轮会探测到 running 并等待 - } - return { kind: "coordination-error", error: null } - } catch (err) { - return { kind: "coordination-error", error: err } - } -} - -// ── 公开 API ──────────────────────────────────────────────────────────────── - -/** - * 以 `key` 为粒度执行 `fn`,同一时刻相同 key 只执行一次。 - * - * @param key 去重键。**必须能唯一标识一次调用的语义**, - * 例如 `fs.list:12:/movies`(存储 id + 物理路径)。 - * 不同 env / 不同存储务必使用不同 key。 - * @param fn 业务函数。注意其返回值需要可 JSON 往返(db 模式下要共享给 - * 其他实例),因此不要返回函数、Symbol、循环引用等。 - * @param options 可选覆盖项,见 SingleFlightOptions。 - * - * @example - * const items = await singleflight(`fs.list:${storage.id}:${path}`, () => - * driver.list(path), - * ) - */ -export async function singleflight( - key: string, - fn: () => Promise, - options: SingleFlightOptions = {}, -): Promise { - const opts = await resolveOptions(options) - stats.mode = opts.mode - - if (opts.mode === "off") { - stats.executed++ - try { - return await fn() - } catch (err) { - stats.failed++ - throw err - } - } - - // L1:进程内合并。db 模式同样先走这一步 —— 同一 isolate 内没必要为同一个 - // key 反复往返数据库。 - return coalesce(key, async () => { - if (opts.mode === "memory") { - stats.executed++ - try { - return await fn() - } catch (err) { - stats.failed++ - throw err - } - } - - const driver = opts.driver - if (!driver) { - // 探测不到 SQL 驱动 → 降级为内存级(L1 仍然生效) - stats.fallback++ - stats.executed++ - try { - return await fn() - } catch (err) { - stats.failed++ - throw err - } - } - - const outcome = await dbFlight(driver, key, fn, opts, options.env) - switch (outcome.kind) { - case "value": - return outcome.value - case "business-error": - throw outcome.error - default: - // 协调失败 → 降级为直接执行,业务照常可用 - stats.fallback++ - stats.executed++ - try { - return await fn() - } catch (err) { - stats.failed++ - throw err - } - } - }) -} - -/** - * 仅测试用:清空进程内在途表与统计,避免用例间互相污染。 - */ -export function __resetSingleFlightForTest(): void { - inflight.clear() - stats.mode = "memory" - stats.executed = 0 - stats.coalesced = 0 - stats.shared = 0 - stats.fallback = 0 - stats.failed = 0 - warnedBadMode = false -} - -/** - * 仅测试用:暴露 SQL 方言分支(mysql 与 sqlite 的 INSERT 语法差异)。 - */ -export function __singleFlightInsertIgnoreForTest(driverName: string): string { - return insertIgnoreClause({ name: driverName }) -} - -/** 仅测试用:暴露实例标识,便于断言 owner 写入。 */ -export function __singleFlightInstanceIdForTest(): string { - return INSTANCE_ID -} diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index fe5e8d96..722c66d9 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -21,14 +21,6 @@ import { driverPreferProxy, normalizeDriverName, } from "../internal/driver/proxy" -import { - cacheKindPrefix, - cacheListKeys, - clearCache, - clearStorageCache, - getCacheRuntimeStatus, - type CacheKind, -} from "../internal/cache" export const adminRouter = new Hono() @@ -452,9 +444,6 @@ adminRouter.post("/storage/update", async (c) => { } db.storages[idx] = updatedStorage await saveDb(db, c.env) - // 存储配置已变更(挂载路径 / root_folder_path / addition / 缓存策略等), - // 既有缓存全部失效,避免继续返回旧挂载点或旧凭据下的结果。 - await clearStorageCache(updatedStorage.id, c.env) } return c.json({ code: 200, message: "success", data: null }) }) @@ -464,9 +453,6 @@ adminRouter.post("/storage/delete", async (c) => { const db = await getDb(c.env) db.storages = db.storages.filter((s: any) => s.id !== id) await saveDb(db, c.env) - // 存储已删除:其文件树 / 下载链接缓存必须一并清理,否则 id 复用时 - // 新存储会读到旧存储的缓存内容。 - await clearStorageCache(id, c.env) return c.json({ code: 200, message: "success", data: null }) }) @@ -478,7 +464,6 @@ adminRouter.post("/storage/enable", async (c) => { s.disabled = false s.modified = new Date().toISOString() await saveDb(db, c.env) - await clearStorageCache(id, c.env) // 异步初始化驱动(不阻塞响应):启用多个云盘时立即返回, // 初始化完成后更新状态;失败时把错误写入 status,下次访问或 // 重新加载时会再次尝试。 @@ -512,7 +497,6 @@ adminRouter.post("/storage/disable", async (c) => { if (s) { s.disabled = true await saveDb(db, c.env) - await clearStorageCache(id, c.env) } return c.json({ code: 200, message: "success", data: null }) }) @@ -595,28 +579,8 @@ const BASE_FIELDS = [ default: "", required: false, }, - { - name: "cache_expiration", - type: "number", - default: "30", - required: false, - help: "目录内容缓存时长(分钟)", - }, ] -/** - * 路径级缓存覆盖(对齐 Go model.Storage.CustomCachePolicies)。 - * 支持 JSON 数组:[{"path":"/photos/*","cache_expiration":1440}] - * 或映射形式:{"/photos/*": 1440} - */ -const CUSTOM_CACHE_POLICIES_FIELD = { - name: "custom_cache_policies", - type: "text", - default: "", - required: false, - help: '按路径覆盖缓存时长,JSON 格式,如 [{"path":"/photos/*","cache_expiration":1440}]', -} - /** 存储级签名开关(对齐 Go common.IsStorageSignEnabled) */ const ENABLE_SIGN_FIELD = { name: "enable_sign", @@ -735,7 +699,6 @@ const DISABLE_PROXY_SIGN_FIELD = { const COMMON_FIELDS = [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields(), @@ -827,7 +790,6 @@ const driverConfigs: Record = { // webdav_policy 默认 302_redirect(OneDrive 默认走直链)。 common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Onedrive"), @@ -930,7 +892,6 @@ const driverConfigs: Record = { // 同 Onedrive:默认 302_redirect common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("OnedriveAPP"), @@ -1008,7 +969,6 @@ const driverConfigs: Record = { default_mount_path: "/google-drive", common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("GoogleDrive"), @@ -1202,7 +1162,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/123/meta.go:只有 PreferProxy(无 OnlyProxy) common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("123Pan"), @@ -1288,7 +1247,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/baidu_netdisk/meta.go:只有 PreferProxy(无 OnlyProxy) common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("BaiduNetdisk"), @@ -1426,7 +1384,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/115_open/meta.go:OnlyProxy(无 302 选项) common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("115Open"), @@ -1929,7 +1886,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/189/meta.go:OnlyProxy common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("189Cloud"), @@ -2161,7 +2117,6 @@ const driverConfigs: Record = { // web_proxy 默认 true、webdav_policy 默认 native_proxy。 common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("WebDav"), @@ -2717,7 +2672,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/weiyun/meta.go:OnlyProxy common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("WeiYun"), @@ -2777,7 +2731,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/sftp/meta.go:OnlyProxy + NoLinkURL(无直链可 302) common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("SFTP"), @@ -2849,7 +2802,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/ftp/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("FTP"), @@ -3136,7 +3088,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/terabox/meta.go:OnlyProxy common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Terabox"), @@ -3243,7 +3194,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/alias/meta.go:ProxyRangeOption common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Alias"), @@ -3443,7 +3393,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/139/meta.go:ProxyRangeOption 且实例默认 ProxyRange = true common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("139Yun"), @@ -3515,7 +3464,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/mega/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Mega_nz"), @@ -3646,7 +3594,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/123_share/meta.go:OnlyProxy + PreferProxy common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("123PanShare"), @@ -3858,7 +3805,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/smb/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("SMB"), @@ -3934,7 +3880,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/crypt/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Crypt"), @@ -4014,7 +3959,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/virtual/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Virtual"), @@ -4069,7 +4013,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/alist_v3/meta.go:ProxyRangeOption common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("AListV3"), @@ -4170,7 +4113,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/strm/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("Strm"), @@ -4636,7 +4578,6 @@ const driverConfigs: Record = { // 对齐 Go drivers/proton_drive/meta.go:OnlyProxy + NoLinkURL common: [ ...BASE_FIELDS, - CUSTOM_CACHE_POLICIES_FIELD, ENABLE_SIGN_FIELD, DISABLE_INDEX_FIELD, ...buildProxyFields("ProtonDrive"), @@ -5115,102 +5056,24 @@ adminRouter.get("/kv/status", async (c) => { }) }) -// --- 缓存管理(文件树缓存 / 下载链接缓存)--- +// --- 存储刷新(兼容接口)--- // -// 默认只向数据库启用(CACHE_DRIVER=db);想要改用 / 额外启用 KV、Blob 等 -// 专用后端,需显式配置 CACHE_DRIVER(如 `kv`、`db,kv`、`blob`)。 -// 这里的接口只做「查看状态 / 清空」,不改变后端选择(后端只认环境变量)。 - -/** 缓存状态:生效配置 + 实际后端 + 当前条目数。 */ -adminRouter.get("/cache/status", async (c) => { - const runtime = await getCacheRuntimeStatus(c.env) - let fileTree = 0 - let downloadLink = 0 - try { - const [ftKeys, lnKeys] = await Promise.all([ - cacheListKeys(cacheKindPrefix("ft", runtime.config.prefix), c.env), - cacheListKeys(cacheKindPrefix("ln", runtime.config.prefix), c.env), - ]) - fileTree = ftKeys.length - downloadLink = lnKeys.length - } catch (err: any) { - return c.json({ - code: 200, - message: "success", - data: { - ...runtime, - entries: null, - entries_error: safeErrorMessage(err), - }, - }) - } - return c.json({ - code: 200, - message: "success", - data: { - ...runtime, - entries: { - file_tree: fileTree, - download_link: downloadLink, - total: fileTree + downloadLink, - }, - }, - }) -}) - -/** - * 清空缓存。 - * body: { type?: "file_tree" | "download_link" | "all", storage_id?: number } - * 省略 type 表示两级缓存全清;省略 storage_id 表示所有存储。 - */ -adminRouter.post("/cache/clear", async (c) => { - const body = await c.req.json().catch(() => ({})) - const rawType = String(body.type ?? c.req.query("type") ?? "") - .trim() - .toLowerCase() - const kind: CacheKind | undefined = - rawType === "file_tree" || rawType === "ft" || rawType === "tree" - ? "ft" - : rawType === "download_link" || rawType === "link" || rawType === "ln" - ? "ln" - : undefined - - const idRaw = body.storage_id ?? c.req.query("storage_id") - const hasId = idRaw !== undefined && idRaw !== null && String(idRaw) !== "" +// 持久缓存已整体移除:`/fs/list`、`/fs/get`、`/d`、`/p` 每次都直接回源, +// 目录与直链实时生效。为兼容旧客户端保留这两个接口,固定返回 removed=0。 - const removed = await clearCache(c.env, { - ...(kind ? { kind } : {}), - ...(hasId ? { storageId: idRaw } : {}), - }) - return c.json({ - code: 200, - message: "success", - data: { removed, type: kind ?? "all", storage_id: hasId ? idRaw : null }, - }) -}) - -/** - * 刷新文件树缓存(对齐参考实现 OpenList.ts 的 POST /api/admin/storage/refresh)。 - * body/query 可选 `id`:只刷新该存储。 - */ adminRouter.post("/storage/refresh", async (c) => { const body = await c.req.json().catch(() => ({})) const idRaw = body.id ?? c.req.query("id") const hasId = idRaw !== undefined && idRaw !== null && String(idRaw) !== "" - const removed = await clearCache(c.env, { - kind: "ft", - ...(hasId ? { storageId: idRaw } : {}), - }) return c.json({ code: 200, message: "success", - data: { removed, storage_id: hasId ? idRaw : null }, + data: { removed: 0, storage_id: hasId ? idRaw : null }, }) }) /** - * 刷新单个存储的文件树缓存 - * (对齐参考实现 OpenList.ts 的 POST /api/admin/storage/refresh_one?id=)。 + * 刷新单个存储(兼容接口,同上)。 */ adminRouter.post("/storage/refresh_one", async (c) => { const body = await c.req.json().catch(() => ({})) @@ -5218,11 +5081,10 @@ adminRouter.post("/storage/refresh_one", async (c) => { if (idRaw === undefined || idRaw === null || String(idRaw) === "") { return c.json({ code: 400, message: "id is required", data: null }, 400) } - const removed = await clearCache(c.env, { kind: "ft", storageId: idRaw }) return c.json({ code: 200, message: "success", - data: { removed, storage_id: idRaw }, + data: { removed: 0, storage_id: idRaw }, }) }) diff --git a/src/backend/server/debug.ts b/src/backend/server/debug.ts index 80e0f1c2..ee32a805 100644 --- a/src/backend/server/debug.ts +++ b/src/backend/server/debug.ts @@ -1,7 +1,6 @@ import { Hono } from "hono" import { getDb, getStoreStatus } from "../internal/model/db" import { checkAdminAuth } from "../pkg/utils" -import { getSingleFlightStats } from "../pkg/singleflight" export const debugRouter = new Hono() @@ -14,9 +13,6 @@ debugRouter.get("/info", async (c) => { timestamp: new Date().toISOString(), // 后端驱动信息非敏感,未登录也返回,便于确认 D1/KV/MySQL 是否生效 store: await getStoreStatus(c.env), - // singleflight 去重统计:用于确认跨实例协调是否真的生效 - // (coalesced/shared 持续为 0 说明没有并发重复调用,或协调未生效) - singleflight: getSingleFlightStats(), } if (isAdmin) { diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index 9474d68a..e989e48d 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -45,13 +45,9 @@ import { import { safeErrorMessage } from "../pkg/errs" import { encodeDownloadPath } from "../pkg/path" import { search } from "../internal/op/search" -import { - getDisableIndex, - resolveCacheExpiration, -} from "../internal/driver/storageopts" +import { getDisableIndex } from "../internal/driver/storageopts" import { parseZip, extractZipEntry, ZipArchive } from "../internal/archive/zip" import { assertSafeUrl, getTrustedHosts } from "../pkg/http" -import { invalidatePaths } from "../internal/cache" import { seedRouter, torrentLegacyRouter } from "./seed" import { r2CanSignDirectUpload } from "../drivers/r2/driver" @@ -88,12 +84,8 @@ fsRouter.route("/torrent", torrentLegacyRouter) * 请求级存储上下文(`env` + `waitUntil`)。 * * 关键约定:**`env` 必须始终传递**,与 executionCtx 是否可用无关。 - * - * 缓存(`CACHE_DRIVER=db`)与 singleflight(`SINGLEFLIGHT=db`)都依赖它来解析 - * 后端:拿不到 `env` 时 `getStorageBackend(undefined)` 会解析成**内存驱动**, - * 而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与数据库被静默拆到两个后端, - * 表现为「列表看似命中缓存(内容陈旧),但持久层里一条缓存键都没有、 - * `/cache/status` 计数恒为 0、`/cache/clear` 永远 removed=0」。 + * 拿不到 `env` 时 `getStorageBackend(undefined)` 会解析成**内存驱动**,而业务 + * 数据仍走全局 env ctx 的持久化后端 —— 读写会被静默拆到两个后端。 * * 真正依赖 executionCtx 的只有 `waitUntil`;缺失时降级为同步等待 * (见 `scheduleStoragePersistence`),业务语义不变。 @@ -249,16 +241,6 @@ fsRouter.post("/dirs", async (c) => { if (!body.force_root) { reqPath = getActualPath(user, reqPath) } - if (body.refresh) { - try { - await invalidatePaths([reqPath], c.env) - } catch (err) { - console.warn( - `[fs/dirs] refresh: invalidating cache for "${reqPath}" failed:`, - err, - ) - } - } try { // Share path support for completeness if (reqPath.startsWith("/@s")) { @@ -333,19 +315,6 @@ fsRouter.post("/list", async (c) => { const page = parseInt(body.page, 10) || 1 const perPage = parseInt(body.per_page, 10) || 0 - // 对齐 Go 版 handles.FsList:refresh=true 时失效该路径的文件树/链接 - // 缓存,允许用户在缓存 TTL 内手动刷新(此前 refresh 参数未实现, - // 缓存内的错误列表最长 30 分钟无法自救)。 - if (body.refresh) { - try { - await invalidatePaths([reqPath], c.env) - } catch (err) { - console.warn( - `[fs] refresh: invalidating cache for "${reqPath}" failed:`, - err, - ) - } - } const paginateItems = (items: T[]) => { const total = items.length if (perPage <= 0) { @@ -593,13 +562,8 @@ fsRouter.post("/list", async (c) => { })() const { content: pagedContent, total } = paginateStorageItems(normalized) - // cache_expiration:由 custom_cache_policies 按路径覆盖后得出(分钟)。 - // 对齐 Go 的路径级缓存策略;前端/上层可据此决定该目录的列表缓存时长。 - const cacheExpiration = storage - ? resolveCacheExpiration(storage, reqPath) - : 0 // Virtual directories must not be cached; a cached Virtual result - // would persist after the real storage comes online, causing the - // 'always shows root / Virtual provider' symptom for subsequent requests. + // cache_expiration 保留字段以兼容 Go 客户端:持久缓存已移除,列表始终实时, + // 因此固定返回 0(Go 语义即「不缓存」)。 return c.json({ code: 200, message: "success", @@ -613,7 +577,7 @@ fsRouter.post("/list", async (c) => { provider, direct_upload_tools: directUploadTools, page_size: effectivePerPage > 0 ? effectivePerPage : undefined, - cache_expiration: cacheExpiration, + cache_expiration: 0, }, }) } catch (err: any) { @@ -1144,14 +1108,6 @@ fsRouter.post("/other", async (c) => { const driver = await getDriver(resolved.storage.driver, resolved.storage) if (typeof (driver as any).other === "function") { const data = await (driver as any).other(method, resolved.relative, body) - // other() 可能触发落盘(如 S3 直传 URL 的签发流程):按已知路径失效缓存。 - // 直传的实际写入发生在客户端,无法精确感知完成时机,这里失效目标目录 - // 及(若给定)文件路径,将陈旧窗口压到最小;失效失败不影响响应。 - const targets = [reqPath] - if (typeof body?.file_name === "string" && body.file_name) { - targets.push(`${reqPath === "/" ? "" : reqPath}/${body.file_name}`) - } - await invalidatePaths(targets, c.env) return c.json({ code: 200, message: "success", data }) } return c.json( @@ -1451,9 +1407,6 @@ fsRouter.post("/get_direct_upload_info", async (c) => { cleanName, file_size, ) - // 直传 URL 签发后客户端会向该路径写入新文件:预失效目录与目标路径 - // (本分支已把校验后的目录/文件名归一化为 dirPath / dstPath) - await invalidatePaths([dirPath, dstPath], c.env) return c.json({ code: 200, message: "success", data: info }) } // 回退到 other("direct_upload") / other("get_direct_upload_info") @@ -1465,8 +1418,6 @@ fsRouter.post("/get_direct_upload_info", async (c) => { file_size, }) if (info) { - // 同上:直传 URL 签发后预失效目录与目标路径 - await invalidatePaths([dirPath, dstPath], c.env) return c.json({ code: 200, message: "success", data: info }) } } catch { @@ -1701,15 +1652,6 @@ fsRouter.post("/multipart/complete", async (c) => { session.state = "completed" putSession(session) deleteSession(session.upload_id) - // 分片收尾:失效上传文件及其目录的缓存(对齐 op 层 put 的失效范围) - { - const { dir, name } = splitUploadPath(session.path) - const actualDir = getActualPath(user, dir) - await invalidatePaths( - [actualDir, `${actualDir === "/" ? "" : actualDir}/${name}`], - c.env, - ) - } return c.json({ code: 200, message: "success", data: mpSnapshot(session) }) } catch (e: any) { session.state = "failed_permanent" diff --git a/src/backend/server/public.ts b/src/backend/server/public.ts index b20fe929..0b41523f 100644 --- a/src/backend/server/public.ts +++ b/src/backend/server/public.ts @@ -17,11 +17,6 @@ import { readFormat, } from "../internal/model/store/backend" import { setUserPassword } from "../pkg/password" -import { - describeCacheConfig, - getCacheDrivers, - isCacheActive, -} from "../internal/cache" // 脱敏 / 截断 / 摘要 / 建议组装:与全局 503 拦截(index.ts)共用同一套规则 import { reasonLines, @@ -284,19 +279,6 @@ publicRouter.get("/env_check", async (c) => { // 但不再因此阻断向导。 const ready = storageAvailable - // ── 缓存状态(文件树 / 下载链接)── - // 默认只向数据库启用(CACHE_DRIVER=db);配置了 KV/Blob 等专用后端时 - // 这里会如实回显实际生效的后端列表,便于排查「以为缓存在 KV」的错觉。 - const cacheConfig = describeCacheConfig(env) - let cacheActive = false - let cacheDrivers: string[] = [] - try { - cacheActive = await isCacheActive(env) - cacheDrivers = (await getCacheDrivers(env)).map((d) => d.name) - } catch { - cacheActive = false - } - return c.json({ code: 200, message: "success", @@ -305,13 +287,6 @@ publicRouter.get("/env_check", async (c) => { serverless, platform: storage?.platform ?? null, }, - cache: { - ...cacheConfig, - /** 是否已有可用缓存后端(false 时缓存自动降级为不缓存) */ - active: cacheActive, - /** 实际解析出的后端驱动名(`db` 会展开成 d1/kv/blob/... ) */ - resolved_backends: cacheDrivers, - }, config: { // 配置值(用户显式设置,或默认值) db_format: formatCfg, diff --git a/src/backend/server/raw.ts b/src/backend/server/raw.ts index 9a1cbb86..e9a1c75b 100644 --- a/src/backend/server/raw.ts +++ b/src/backend/server/raw.ts @@ -3,11 +3,6 @@ import { getSettings, resolvePath } from "../internal/model/db" import { parseRangeHeader } from "../internal/stream/stream" import { flushPendingDriverState, getDriver } from "../internal/op/storage" import { resolveShare } from "../internal/op/share" -import { - getCachedLink, - setCachedLink, - type CacheTarget, -} from "../internal/cache" import { needDownloadSign, verifyDownloadSign, @@ -525,31 +520,21 @@ rawRouter.get("/*", async (c) => { // 管理员配置的受信存储 endpoint host(可能是内网自建 S3/WebDAV/MinIO), // 加上全局环境变量 SSRF_ALLOWED_HOSTS,合并为 SSRF 白名单,避免被误拦截。 const trustedHosts = getTrustedHosts(resolved.storage.addition, c.env) - // 下载链接缓存:与 /fs/get 共用同一份缓存(键 = storage + 虚拟路径)。 - // 命中时直接复用直链,跳过驱动换链(网盘侧通常最贵、最易被限流)。 - const cacheTarget: CacheTarget = { - storage: resolved.storage, - cleanPath: resolved.cleanPath, - physical: resolved.physical, - isVirtual: resolved.isVirtual, - } - let fileItem = await getCachedLink(cacheTarget, c.env) - // 仅在缓存未命中时才会实例化驱动(命中时 driver 保持 null, - // 见下方 createReadStream 分支的空值保护)。 - let driver: any = null - if (!fileItem) { - driver = await getDriver(resolved.storage.driver, resolved.storage) - try { - fileItem = await driver.get(reqPath, resolved.physical) - } finally { - await flushPendingDriverState( - resolved.storage.driver, - resolved.storage, - driver, - getStorageRequestContext(c), - ) - } - await setCachedLink(cacheTarget, fileItem, c.env) + // 每次请求都向驱动换取直链:不做任何跨请求缓存,保证链接与文件状态实时。 + const driver: any = await getDriver( + resolved.storage.driver, + resolved.storage, + ) + let fileItem: any + try { + fileItem = await driver.get(reqPath, resolved.physical) + } finally { + await flushPendingDriverState( + resolved.storage.driver, + resolved.storage, + driver, + getStorageRequestContext(c), + ) } if (fileItem && fileItem.raw_url) { diff --git a/src/backend/server/storage_request_context.test.ts b/src/backend/server/storage_request_context.test.ts deleted file mode 100644 index 648c7f87..00000000 --- a/src/backend/server/storage_request_context.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/** - * 回归测试:请求级存储上下文必须始终携带 `env`。 - * - * 背景(真实缺陷):`getStorageRequestContext()` 早期实现在缺少 - * `executionCtx.waitUntil` 时直接返回 `undefined`,把 `env` 一起丢了。 - * 于是缓存解析后端时拿到 `env=undefined` → `getStorageBackend(undefined)` - * 解析成**内存驱动**,而业务数据仍走全局 env ctx 的持久化后端 —— 缓存与 - * 数据库被静默拆到两个后端:列表看似命中缓存(内容陈旧),但持久层里 - * 一条缓存键都没有、`/cache/status` 计数恒为 0、`/cache/clear` 永远 removed=0。 - * - * 这里刻意**不传** `app.request()` 的第 4 个参数(executionCtx), - * 断言缓存仍然落到 `CACHE_DRIVER=db` 解析出的真实后端(KV)上。 - */ -import assert from "node:assert/strict" -import { test } from "node:test" -import { mkdirSync, writeFileSync, rmSync } from "node:fs" -import app from "../index" - -const JWT_SECRET = "jwt-secret-32-characters-long!!" -const ORIGIN = "http://localhost" -const SANDBOX = process.cwd() + "/.tmp-storage-ctx" -const ROOT = SANDBOX + "/root" - -/** 内存 KV 绑定(EdgeOne KV 语义:list -> { keys: [{ key }], complete }) */ -function makeKv() { - const store = new Map() - return { - store, - binding: { - async get(key: string) { - return store.has(key) ? store.get(key)! : null - }, - async put(key: string, value: string) { - store.set(key, value) - }, - async delete(key: string) { - store.delete(key) - }, - async list(opts?: { prefix?: string }) { - const prefix = opts?.prefix || "" - return { - keys: [...store.keys()] - .filter((k) => k.startsWith(prefix)) - .map((key) => ({ key })), - complete: true, - } - }, - }, - } -} - -test("缺少 executionCtx 时,缓存仍必须写入 CACHE_DRIVER=db 指向的真实后端", async () => { - const kv = makeKv() - const env: any = { - DB_DRIVER: "kv", - DB_FORMAT: "map", - KV: kv.binding, - JWT_SECRET, - ADMIN_PASS: "admin", - __requestOrigin: ORIGIN, - } - - rmSync(SANDBOX, { recursive: true, force: true }) - mkdirSync(ROOT + "/sub", { recursive: true }) - writeFileSync(ROOT + "/sub/c.txt", "gamma") - - try { - const post = (path: string, body: any, token?: string) => - // 注意:刻意省略第 4 个参数(executionCtx) - app.request( - path, - { - method: "POST", - headers: token - ? { "Content-Type": "application/json", Authorization: `Bearer ${token}` } - : { "Content-Type": "application/json" }, - body: JSON.stringify(body ?? {}), - }, - env, - ) - - const login: any = await (await post("/api/auth/login", { - username: "admin", - password: "admin", - })).json() - const token = login?.data?.token - assert.ok(token, `login failed: ${JSON.stringify(login)}`) - - const created: any = await (await post( - "/api/admin/storage/create", - { - mount_path: "/ctx", - driver: "Local", - addition: JSON.stringify({ root_folder_path: ROOT }), - order: 0, - }, - token, - )).json() - assert.equal(created?.code, 200, `create storage failed: ${JSON.stringify(created)}`) - - // 首次列目录 → 应回源并写入缓存 - const listed: any = await (await post( - "/api/fs/list", - { path: "/ctx/sub", page: 1, per_page: 0 }, - token, - )).json() - assert.equal(listed?.code, 200, `list failed: ${JSON.stringify(listed)}`) - assert.deepEqual( - (listed?.data?.content || []).map((i: any) => i.name), - ["c.txt"], - ) - - // 核心断言:缓存键必须出现在 KV 里。 - // 修复前这里恒为空 —— 缓存被静默写进了内存驱动。 - const cacheKeys = [...kv.store.keys()].filter((k) => k.startsWith("openlist_cache_ft_")) - assert.ok( - cacheKeys.length >= 1, - `expected a file-tree cache key in the configured KV backend, got keys: ` + - JSON.stringify([...kv.store.keys()]), - ) - - // 键名用缓存模块自己的编码函数推导,避免硬编码编码细节 - const { buildCacheKey } = await import("../internal/cache/store") - const storageId = 1 - const expectedKey = buildCacheKey("ft", storageId, "/ctx/sub", "openlist_cache") - assert.ok( - cacheKeys.includes(expectedKey), - `unexpected cache key: expected ${expectedKey}, got ${JSON.stringify(cacheKeys)}`, - ) - - // 缓存值应为带过期时间的信封 - const raw = kv.store.get(cacheKeys[0])! - const envelope = JSON.parse(raw) - assert.ok(Array.isArray(envelope?.v), `cache envelope payload is not an array: ${raw.slice(0, 120)}`) - assert.ok(typeof envelope?.exp === "number" && envelope.exp > Date.now(), "cache envelope missing future exp") - - // 管理接口应能统计到条目(同样验证 env 透传到了缓存层) - const status: any = await ( - await app.request("/api/admin/cache/status", { - method: "GET", - headers: { Authorization: `Bearer ${token}` }, - }, env) - ).json() - assert.equal(status?.code, 200) - assert.ok( - (status?.data?.entries?.file_tree ?? 0) >= 1, - `cache/status did not count the entry: ${JSON.stringify(status?.data?.entries)}`, - ) - assert.ok( - (status?.data?.drivers || []).includes("kv"), - `cache/status resolved unexpected drivers: ${JSON.stringify(status?.data?.drivers)}`, - ) - } finally { - rmSync(SANDBOX, { recursive: true, force: true }) - } -}) diff --git a/wrangler.jsonc b/wrangler.jsonc index 7e495779..fcfdc95d 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -86,65 +86,11 @@ // 自动生成并持久化共享密钥(JWT 签名需要)。 "DB_CIPHER": "none", - // ── 缓存(文件树缓存 / 下载链接缓存)───────────────────────────────── - // 参考 OpenList.ts 的两级持久缓存: - // 文件树缓存 - /api/fs/list|get|dirs 优先读缓存,未命中才请求网盘 - // 下载链接缓存 - /d、/p 复用驱动换取的直链,避免重复换链/签名 - // - // 默认【只向数据库启用】:缓存与业务数据走同一条存储链路(DB_DRIVER - // 解析出的 D1/KV/Blob/MySQL/DO)。想要改用或额外启用 KV、Blob 等专用 - // 后端,必须显式配置 CACHE_DRIVER(不会自动探测、不会自动回退)。 - // - // 总开关(默认 true) - "CACHE_ENABLED": "true", - // - // 缓存后端(默认 db = 与业务数据同后端): - // db - 数据库(DB_DRIVER 解析结果)★默认 - // kv - KV binding(名固定为 KV) - // blob - EdgeOne Blob SDK / ESA Blob - // cfkv - Cloudflare KV REST API(需 CF_* 三项) - // do - Durable Objects - // memory - 进程内存(仅本地调试) - // none - 关闭缓存 - // 支持逗号分隔多后端(读按顺序命中、写全部铺开),如 "db,kv" - "CACHE_DRIVER": "db", - // - // 分级开关:文件树缓存默认 true;下载链接缓存默认 false(部分网盘直链 - // TTL 很短,缓存复用可能把已失效的直链发给用户,确认后再显式开启) - "CACHE_FILE_TREE": "true", - "CACHE_DOWNLOAD_LINK": "false", - // - // 文件树缓存时长(分钟)。0 = 跟随存储级 cache_expiration(默认 30 分钟, - // 可被该存储的 custom_cache_policies 按路径覆盖;命中 0 表示不缓存) - "CACHE_TTL": "0", - // - // 下载链接缓存时长(分钟)。直链通常自带有效期,取值需保守(默认 5) - "CACHE_LINK_TTL": "5", - // - // 不参与缓存的驱动(纯本地计算型,缓存只会变陈旧) - "CACHE_EXCLUDE_DRIVERS": "virtual,alias,url_tree,strm,chunk", - // ── singleflight(并发相同调用去重)────────────────────────────────── - // - // 一个部署会同时存在多个 isolate,进程内去重无法跨实例生效,因此当存在 - // SQL 库(d1 / mysql / do)时用表 `x_singleflight` 做跨实例协调。 - // auto(默认)- 有 SQL 库用表,否则退回进程内(memory) - // db - 强制用表协调;协调层出错时自动降级,不影响业务 - // memory - 仅进程内合并(等价 Go 的进程内 singleflight) - // off - 关闭去重,每次调用都直接执行 - "SINGLEFLIGHT": "auto", - - // 执行完成后结果在表中保留多久(ms),供**正在等待的**并发实例读取。 - // 它不是结果缓存:新到达的调用会回收旧记录并重新执行,不会读到过期数据。 - // "SINGLEFLIGHT_HANDOFF_MS": "1000", - // - // 执行者持锁上限(ms);期间按 1/3 周期续租,执行者崩溃后到期可被接管。 - // "SINGLEFLIGHT_LOCK_TTL_MS": "30000", - // - // 等待方轮询间隔(ms) - // "SINGLEFLIGHT_POLL_MS": "200", - // - // 等待方最长等待(ms);超时后自行执行,避免被慢请求拖死 - // "SINGLEFLIGHT_WAIT_MS": "15000", + // ── 持久缓存 / 并发去重均已移除 ─────────────────────────────────────── + // 目录列表(/fs/list、/fs/get、/fs/dirs)与下载直链(/d、/p)每次都直接 + // 回源驱动:不做跨请求缓存,也不合并并发相同调用。外部改动、直链与并发 + // 请求的结果都以驱动当次返回为准。相关环境变量(CACHE_* / SINGLEFLIGHT*) + // 已全部删除,残留同名变量不会再被读取。 // ── 安全鉴权(推荐以 Secret 类型配置,见 .dev.vars.example)────────── // From d6ab379b7d3381b4ad198c79a640ba4646dfbbae Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:49:46 +0800 Subject: [PATCH 40/49] fix(init): simplify env check messages and refresh README (#188) * fix(init): simplify env check messages and make them bilingual * docs: add ESA deploy entry, repo badges and simplify README * chore(config): trim stale wrangler comments * https://github.com/Mcchen1008/NextList * docs: sync translated READMEs with the new layout * docs: remove PR description from the repository * docs: remove PR description documents --------- Co-authored-by: pikachuren --- README.md | 87 ++--- docs/pr-media-library-parity.md | 157 --------- docs/pr-storage-proxy-policy.md | 337 -------------------- readmes/README_ar.md | 75 +++-- readmes/README_de.md | 75 +++-- readmes/README_en.md | 87 ++--- readmes/README_es.md | 75 +++-- readmes/README_fr.md | 75 +++-- readmes/README_hi.md | 75 +++-- readmes/README_it.md | 75 +++-- readmes/README_ja.md | 75 +++-- readmes/README_ko.md | 75 +++-- readmes/README_pt.md | 75 +++-- readmes/README_ru.md | 75 +++-- readmes/README_zh-TW.md | 75 +++-- readmes/assets/esa-deploy.svg | 5 + src/backend/server/init_diagnostics.test.ts | 6 +- src/backend/server/public.ts | 177 +++++++--- src/backend/server/storage_policy.test.ts | 11 +- wrangler.jsonc | 82 +---- 20 files changed, 653 insertions(+), 1121 deletions(-) delete mode 100644 docs/pr-media-library-parity.md delete mode 100644 docs/pr-storage-proxy-policy.md create mode 100644 readmes/assets/esa-deploy.svg diff --git a/README.md b/README.md index 433df57a..b73ca213 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ License Build status -latest version discussions -Downloads +Stars +Forks + 📘 [使用文档](https://doc.oplist.org) · 🌏 [使用文档(中国大陆)](https://doc.oplist.org.cn) · ⚖️ [使用条款](https://doc.oplist.org/terms) · 🔒 [隐私政策](https://doc.oplist.org/privacy) @@ -35,17 +36,18 @@
-| EdgeOne Makers · 国际站 | EdgeOne Makers · 中国站 | Cloudflare Workers · 全球站 | -| :---: | :---: | :---: | -| [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · 国际站 | EdgeOne Makers · 中国站 | Cloudflare Workers · 全球站 | 阿里云 ESA · 中国站 | +| :---: | :---: | :---: | :---: | +| [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![使用阿里云 ESA 部署](readmes/assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] -> - 若 Cloudflare 提示`无法获取存储库内容`,则您需要先 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) 本项目,再通过连接到 Github 仓库功能部署 -> - 部署时请填写 `JWT_SECRET`(≥16 字符的随机串,可用 `openssl rand -hex 32` 生成):它用于 JWT 签名与数据加密;未配置时会在初始化时自动生成并持久化 -> - 数据存储默认自动选择平台自带的持久化后端(`DB_DRIVER=auto`),无需配置;其余变量见 [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) -> - 部署完成后首次访问站点,按**安装向导**在浏览器中设置管理员账号与密码即可完成初始化;如需跳过向导,可配置 `ADMIN_PASS` +> - 在 Cloudflare 平台部署,您需要先 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) 本项目,再连接到仓库部署 +> - 阿里云 ESA 请在控制台边缘计算和 AI → 函数和 Pages导入本仓库 +> - 部署时请填写 `JWT_SECRET`(≥16 随机串,用于 JWT 签名与加密) +> - 数据存储默认自动选择平台自带的持久化后端,其余变量见 [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) +> - 部署完成后首次访问站点,通过**安装向导**完成初始化并设置账号密码 ## 功能简介 @@ -58,7 +60,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea 内置 **78 个存储驱动**,开箱即用地挂载各类存储后端: -- **国内网盘**:阿里云盘(开放平台/分享)、夸克网盘(开放平台/UC TV 版)、百度网盘(相册)、115 网盘(开放平台/分享)、123 云盘(开放平台/分享)、天翼云盘(189/PC/TV)、中国移动云盘(139/和彩云)、沃家云盘、迅雷云盘、腾讯微云、蓝奏云、城通网盘、PikPak(分享)、豆包网盘、光亚盘、超星小组网盘、联想 NAS 分享、Teambition 网盘、WPS 网盘、阿里文档、HalalCloud、MediaTrack 等 +- **国内网盘**:阿里云盘(开放平台/分享)、夸克网盘(开放平台/UC TV 版)、百度网盘(相册)、115 网盘(开放平台/分享)、123 云盘(开放平台/分享)、天翼云盘(189/PC/TV)、中国移动云盘(139/和彩云)、沃家云盘、迅雷云盘、腾讯微云、蓝奏云、城通网盘、PikPak(分享)、豆包网盘、光鸭云盘、超星小组网盘、联想 NAS 分享、Teambition 网盘、WPS 网盘、阿里文档、HalalCloud、MediaTrack 等 - **国际网盘**:Google Drive(相册)、OneDrive(应用/分享链接)、Dropbox、MEGA、MediaFire、Proton Drive、Yandex Disk、Degoo、Bunny Storage、TeraBox 等 - **对象存储**:S3 兼容(AWS/OSS/COS/MinIO 等)、Cloudflare R2(Workers Binding 直连或 S3 API)、又拍云 USS、Azure Blob、WebDAV、FTP、SFTP、SMB、IPFS 等 - **代码托管**:GitHub、GitHub Releases、CNB Releases @@ -69,32 +71,23 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea ### 核心能力 -- **文件浏览**:统一的目录树浏览,支持图片、视频、音频、文档、代码、压缩包等格式在线预览。 -- **上传下载**:跨存储的上传、批量下载、流式传输与直链跳转。 -- **文件分享**:生成带有效期、密码与权限控制的分享链接,支持匿名访问与目录分享。 -- **全文搜索**:在已索引的存储中快速检索文件。 -- **离线任务**:后台任务队列,支持批量操作与异步处理。 -- **外部接口**:将聚合存储以 WebDAV 或 S3 兼容协议对外暴露,便于挂载到第三方工具。 -- **MCP 服务**:提供 Model Context Protocol 端点,可被 AI 助手等客户端集成调用。 -- **并发去重**:同一时刻相同的目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用同一结果,避免无谓的限流与 CPU 消耗。 - -### 权限管理 - -- **权限管理**:基于角色的访问控制(RBAC),支持用户分组、目录级读写权限与配额。 -- **认证方式**:内置账号密码,支持TOTP验证、WebAuthn/FIDO登录、SSO单点登录与 LDAP 目录认证。 -- **安全加固**:JWT 会话、CSRF 防护、点击劫持防护(X-Frame-Options)、内容安全策略(CSP)。 -- **健康检查**:提供 `/health` 存活探针与 `/healthz` 就绪探针,可用于监控与告警。 - -### 平台部署 - -- **运行平台**:Cloudflare Workers、腾讯云 EdgeOne Makers、阿里云 ESA、Vercel / AWS Lambda 及 Node.js 容器环境。 +- **文件管理**:统一目录树浏览,图片 / 视频 / 音频 / 文档 / 代码 / 压缩包在线预览;跨存储上传、批量下载、流式传输与直链跳转;带有效期、密码与权限控制的分享链接。 +- **检索与任务**:已索引存储的全文搜索,后台任务队列支持批量与异步处理。 +- **外部接口**:WebDAV、S3 兼容协议,以及可被 AI 助手集成的 MCP 端点。 +- **权限与认证**:RBAC(用户分组、目录级读写权限、配额),账号密码 + TOTP / WebAuthn / SSO / LDAP。 +- **安全与运维**:JWT 会话、CSRF、点击劫持(X-Frame-Options)与 CSP 防护,`/health`、`/healthz` 健康探针。 +- **并发去重**:相同目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用结果,减少限流与 CPU 消耗。 +- **运行平台**:Cloudflare Workers、腾讯云 EdgeOne Makers、阿里云 ESA、Vercel / AWS Lambda 与 Node.js 容器。 - **数据存储**:Cloudflare D1(SQLite)为主,也支持 KV / Blob / Durable Objects 与外部 MySQL / MariaDB。 -- **一键部署**:支持 EdgeOne、Cloudflare Workers 的一键部署按钮 + 安装向导。 +- **一键部署**:支持 EdgeOne、Cloudflare Workers、阿里云 ESA 部署按钮 + 安装向导。 --- ## 手动部署 +
+展开查看前置要求、本地开发与生产部署 + ### 前置要求 - Node.js 18+(推荐使用 pnpm) @@ -125,6 +118,8 @@ pnpm run deploy pnpm run deploy:worker ``` +
+ --- ## 技术架构 @@ -146,37 +141,21 @@ pnpm run deploy:worker --- -## 配置 - -部署完成后**首次访问站点会自动进入安装向导**,在浏览器中设置管理员账号与密码即可完成初始化;如需跳过向导,配置 `ADMIN_PASS` 即可自动初始化 admin。 - -绝大多数部署**不需要任何额外配置**:数据存储默认走 `DB_DRIVER=auto`,会自动选择平台可用的持久化后端(Cloudflare Workers → KV / D1 / DO,EdgeOne / ESA → Blob)。建议显式配置的只有一项: +## 部署配置 -| 变量 | 说明 | -| :--- | :--- | -| `JWT_SECRET` | JWT 令牌签名密钥(≥16 字符,推荐 `openssl rand -hex 32` 生成),同时用于定时任务鉴权。未配置时会在初始化时自动生成并持久化 | - -> `JWT_SECRET` 请以 **Secret**(而非普通变量)方式配置:普通变量可能在平台重新部署时被仓库内的配置覆盖,Secret 不会被覆盖。 - -其余变量均有合理默认值,需要时再调整: +部署完成后请设置如下变量: | 变量 | 默认值 | 说明 | | :--- | :--- | :--- | -| `ADMIN_PASS` | — | 跳过安装向导,以该密码自动初始化 admin | -| `DB_FORMAT` | `map` | 数据存储格式:`map`(整对象 JSON)/ `key`(分 key)/ `sql`(关系表,与 Go 后端一致) | -| `DB_DRIVER` | `auto` | 存储驱动:`auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`;显式指定后不做回退 | -| `DB_CIPHER` | `none` | 敏感字段落盘算法(默认不加密),如 `aes-256-gcm` | -| `ALLOW_URLS` | — | CORS 允许白名单(逗号分隔,不设则仅同源) | -| `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | 整体上传 / 分片上传大小上限(字节) | -| `ASSET_URLS` | — | 前端静态资源 CDN 地址,支持 `$version` 占位符 | +| `JWT_SECRET` | (无) | JWT 令牌签名密钥(≥16 字符) | +| `DB_FORMAT` | `map` | `map`(Json)/ `key`(分 key)/ `sql`(关系表) | +| `DB_DRIVER` | `auto` | 可选 `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | 敏感字段落盘算法,如 `aes-256-gcm` | + -目录列表与下载直链每次都直接回源驱动,没有跨请求缓存(无 `CACHE_*` 变量),外部改动立即可见。 -完整环境变量(含字段加密 `DB_CIPHER`、`cfkv` / MySQL 连接配置等)与各平台部署步骤,请参阅文档站: +完整环境变量,请参阅[配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) -- [配置变量](https://doc.oplist.org/ecosystem/official_worker/guide_env) -- [部署教程](https://doc.oplist.org/ecosystem/official_worker/guide) -- [常见问题](https://doc.oplist.org/ecosystem/official_worker/about) ## 帮助支持 diff --git a/docs/pr-media-library-parity.md b/docs/pr-media-library-parity.md deleted file mode 100644 index af211db5..00000000 --- a/docs/pr-media-library-parity.md +++ /dev/null @@ -1,157 +0,0 @@ - - -`feat(media): complete media library, Emby and Subsonic parity with Go backend` - -## Summary / 摘要 - -本 PR 把 Worker 版媒体库从「最小可跑闭环」补齐到与 Go 版 OpenList(`OpenList-Backends` 的 `dev-media`)功能对齐,覆盖扫描、刮削、媒体 API、Emby、Subsonic 五条链路,并同步了 Go 侧最近的协议与安全修复。 - -**问题背景**:此前的实现(`3638ad2`、`c2e4a6b`)只做到「能扫、能看、能播」: - -- 音乐不解析 ID3/Vorbis,`album_artist` 恒为空,专辑/曲序/封面/歌词全部缺失; -- 缩略图接口恒 404,图片类型完全不刮削; -- 刮削全串行、每次重刮全库,没有 `scraped_at` 判据; -- `delete_invalid` 是空壳(只删隐藏项),导入不恢复扫描路径; -- 媒体库数据存在单个 JSON 文档里,一次播放进度上报就要全量重写; -- Emby 缺 Season 层级、`Sessions/Playing*` 不落进度、`PlaybackInfo` 返回本地文件路径导致客户端播放失败; -- Subsonic 只有约 18 个方法且恒返回 JSON,播放列表恒空。 - -## 用户可感知的行为变化 - -### 媒体库 - -- **音乐库可用**:扫描时读取 ID3v2.2/2.3/2.4、ID3v1、FLAC Vorbis Comment、M4A `ilst`,填充标题/艺术家/专辑/曲序/年份/流派/内嵌封面;优先读取同目录 `.lrc`,回退内嵌歌词。 -- **扫描路径支持 `path_merge` 与 `enable_scrape`**:目录合并模式由每个扫描路径独立控制,不再对「视频目录 ≥2 个文件」硬编码聚合。 -- **剧集集号/季号按真实文件名解析**:支持 `SxxExx`、`EPxx`、`第N集`(含中文数字)、开头数字;`1080p` 之类不再被误判为集号。 -- **图片类型可刮削**:按文件头魔数校验真实格式(伪造扩展名的非图片文件不入库),并把原图缓存为缩略图。 -- **书籍封面**:新增 EPUB / CBZ / **PDF** 内嵌封面提取(PDF 支持 DCTDecode 直出与 FlateDecode → PNG 转码)。 -- **刮削并发**(默认 5,可配置)、只刮未刮削条目;TMDB 改为多候选 + 相似度打分 + 年份/类型加权,支持剧集单集信息。 -- **进度语义对齐 Go**:播放到 90% 自动标记已看;重新播放(<5s)清除已看;记录 `last_played_at`。 -- **收藏接口只返回条目级收藏**(排除选集收藏)。 -- **`delete_invalid` 真实探测文件是否存在**,只删除确认不存在的记录;导入/导出支持 `scan_path` 范围与扫描路径 id 重映射。 -- **STRM**:支持内容模板、覆盖开关、扫描后自动生成;输出 `Movie (Year)/`、`Series/Season 01/SxxExx`、`Artist/Album/` 目录结构。 -- **普通用户可自助签发媒体应用密钥**:`/api/auth/me/appkey/*`(此前仅管理员可用)。 - -### Emby - -- **`PlaybackInfo` 修复**:`Path` 与 `DirectStreamUrl` 改为带 `api_key` 的 HTTP 流地址(不再是本地文件路径),修复客户端 `Playback of this content is currently restricted` / 一直转圈。 -- **封面端点允许匿名访问**(客户端图片 URL 不带 token),支持 `thumb:` / `data:` / 远程 URL / VFS 四种来源。 -- **Series → Season → Episode 三层结构**,剧集按季归档。 -- **`Sessions/Playing`、`Playing/Progress`、`Playing/Stopped` 真正落播放进度**,停止时按 90% 标记已看。 -- 新增 FavoriteItems / PlayedItems / UserData 读写、`/Audio/:id/lyrics`(LRC 转 SRT 作为外挂字幕轨)。 -- 补齐 Views / Items / Counts / Latest / Resume / Root / Shows / Artists / Search Hints / Library / DisplayPreferences 等端点。 -- **Emby WebSocket**:Cloudflare Workers 下提供 `/@emby/embywebsocket`(`WebSocketPair` + `upgradeWebSocket`,30s 心跳);其它运行时返回 426。 - -### Subsonic - -- **默认输出 XML**,支持 `f=json` / `jsonp`,支持 POST 表单(formPost)。 -- 方法覆盖从约 18 个扩展到约 35 个:`getIndexes`、`getMusicDirectory`、`getRandomSongs`、`getSongsByGenre`、`search2/3`、`getLyrics`、`getLyricsBySongId`、`setRating`、`scrobble`、播放列表 CRUD、`savePlayQueue/getPlayQueue`、`getGenres`、`getUsers`、`tokeninfo`、`startscan` 等。 -- 播放列表、播放队列、评分按要求持久化。 -- `media_subsonic_proxy_stream`、`media_subsonic_allow_password_auth`(默认关闭,对齐 Go)生效。 -- 认证失败限流:IP + 用户名双维度,只有「带凭据但校验失败」才计数,锁定窗口不续期。 - -### 安全 - -- 封面代理改为**逐跳**重定向校验 + 域名白名单 + 图片魔数嗅探 + `nosniff` / CSP 响应头(此前只校验首跳并信任 `Content-Type`)。 -- 媒体类型开关纳入条目级权限校验(`/fs/media/item/:id` 不再能读取已关闭类型的条目)。 -- Emby / Subsonic 认证入口增加失败限流;应用密钥明文改为 AES-GCM 加密存储(旧 `plain` 字段仍兼容读取)。 - -### 转码 - -- **Worker 明确不支持转码**,设置被三层屏蔽:`/admin/setting/list` 过滤全部 `transcode_*`、`/admin/setting/save` 拒绝写入并强制落成 `false`、`getSettings()` 读取时强制关闭。 -- `/api/fs/transcode/play` 返回 `transcode:false, supported:false, reason`,前端平滑降级直链。 - -## 重要实现变化 - -| 模块 | 变更 | -|---|---| -| `internal/media/model.ts` | 对齐 Go 字段:`mime_type / reviews / lyrics / video_type / is_folder / scraped_at`、`scan_path.path_merge / enable_scrape / last_scan_at`、`config.scraper_config / last_*_at`、`userdata.rating / last_played_at`、`MediaPlaylist / MediaPlaylistItem / MediaPlayQueue` | -| `internal/media/store.ts` | 单 JSON 文档改为分片持久化:`items` 按 `scan_path_id`、`user_data` 按 `user_id`、缩略图按 hash 分键;写入前按分片 diff,只写变化部分;首次读取自动迁移旧数据 | -| `internal/media/tag.ts`(新增) | ID3v2/Vorbis/M4A/LRC/EXIF/ZIP/PDF 解析,图片魔数嗅探 | -| `internal/media/thumb.ts`(新增) | 封面落盘(`thumb:`)、base64 内联、URL 解析、类型级 `scraper_config` 覆盖 | -| `internal/media/strm.ts`(新增) | STRM 目标计算、模板渲染、批量生成、扫描后自动生成 | -| `internal/media/ratelimit.ts`(新增) | Emby/Subsonic 认证限流(内存 + 持久化锁定) | -| `internal/model/transcode-guard.ts`(新增) | `transcode_*` 设置识别、过滤、强制禁用 | -| `internal/op/storage.ts` | 新增 `readFileRange()`:Range 请求、上游忽略 Range 时流式跳过、32MB 上限 | -| `server/media.ts` | 重写:进度语义、收藏过滤、`delete_invalid` 真实探测、导入导出 scope、`/me/appkey`、STRM、封面代理加固 | -| `server/emby.ts` | 重写:Emby 完整端点 + Season 层级 + Sessions 进度 + WebSocket;Subsonic XML/JSON/JSONP + 方法补齐 | -| `server/admin.ts` | 设置列表/保存接入转码守卫 | -| `server/router.ts` | 注册 `/auth/me/appkey`;转码请求明确 `supported:false` | -| `internal/model/db.ts` | 新增 `media_subsonic_allow_password_auth`、`media_cover_gateway`;`getSettings()` 接入转码守卫 | - -## 兼容性 / 迁移 - -- **存储格式**:媒体库 meta 首次读取时自动把旧的单文档 `items` / `user_data` 拆分写入新分片键,无需人工迁移;旧键 `openlist_media_db_v1` 继续作为 meta 使用。 -- **前端接口**:现有字段名不变,`cover` 字段可能返回 `data:`、`/api/public/media/thumb?path=...` 或远程 URL,前端按普通图片 URL 处理即可。 -- **Emby / Subsonic**:ID 规则保持 `mv- / so- / ep- / sr- / sn- / al- / ar-`,并额外提供稳定的数字 ID 映射;旧客户端缓存的内嵌 ID 仍可解析。 -- **行为变更**:`media_subsonic_allow_password_auth` 默认 **false**(对齐 Go),使用 `u+p` 的客户端需要改用 `apiKey` 或 `u+t+s`(应用密钥)认证;管理员可在设置中显式开启。 -- **行为变更**:转码设置在 Worker 上不可见、不可写、不可启用。 - -## 已知限制(Worker 运行时) - -- 无 ffmpeg:转码按上述方式屏蔽,播放降级直链。 -- 无 sharp/canvas:缩略图为原图缓存(≤512KB),不做缩放。 -- Emby WebSocket 仅 Cloudflare Workers 可用;Node/EdgeOne 返回 426。 -- 扫描在请求内执行,受 CPU/时长约束:单路径最多 20000 条、深度 12。 -- PDF 封面只支持 `DCTDecode` 与 `FlateDecode`(含 ASCII85 组合),`JPXDecode` / `JBIG2Decode` 跳过。 - -## Testing / 测试 - -```bash -# 类型检查 -node node_modules/typescript/bin/tsc -p tsconfig.json --noEmit - -# 媒体库 + 协议单测 -node node_modules/tsx/dist/cli.mjs --test \ - src/backend/internal/media/media.test.ts \ - src/backend/server/emby_protocol.test.ts - -# 回归(路由注册 / 公开设置) -node node_modules/tsx/dist/cli.mjs --test src/backend/server/index_cdn.test.ts -node node_modules/tsx/dist/cli.mjs --test src/backend/server/public_settings.test.ts -``` - -结果: - -- `tsc --noEmit`:0 error -- 媒体 + 协议单测:**24 passed / 0 failed**,覆盖扩展名映射、查询过滤、多用户收藏隔离、集号/季号解析(含中文数字)、ID3v2、FLAC Vorbis、图片魔数、播放列表、PDF 封面(JPEG 直出 / Flate→PNG)、转码守卫、STRM 布局、Emby 公共端点、Subsonic XML/JSON/JSONP、XML 转义、数字 ID 稳定性 -- `index_cdn.test.ts`(15)、`public_settings.test.ts`(7):全部通过,无回归 - -手动验证建议: - -- 扫描一个含 MP3/FLAC 的目录,确认专辑/艺术家/曲序/封面/歌词 -- 扫描带 `SxxExx` 的剧集目录,确认 Emby 中 Season/Episode 层级 -- 用 Infuse / VidHub / Feishin / Symfonium 各连接一次,确认浏览、封面、播放、进度同步 -- 在管理端尝试保存 `transcode_enabled=true`,确认列表不显示、保存后值仍为 `false` - -## Checklist / 检查清单 - -- [x] I have read [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md). -- [x] I confirm this contribution follows the repository license, contribution policy, and code of conduct. -- [x] I have formatted the changed code with `prettier` where applicable. -- [ ] I have requested review from relevant maintainers or code owners where applicable. - -## AI Disclosure / AI 使用声明 - -- [x] This PR includes AI-assisted content. - -Tools used / 使用工具: - -- [x] Other (please specify) / 其他(请注明): CodeBuddy - -Usage scope / 使用范围: - -- [x] Code generation / 代码生成 -- [x] Refactoring / 重构 -- [x] Documentation / 文档 -- [x] Tests / 测试 -- [x] Review assistance / 审查辅助 - -- [x] I have reviewed and validated all AI-assisted content included in this PR. -- [x] I can reproduce all AI-assisted content included in this PR without any AI tools. diff --git a/docs/pr-storage-proxy-policy.md b/docs/pr-storage-proxy-policy.md deleted file mode 100644 index 3fb29353..00000000 --- a/docs/pr-storage-proxy-policy.md +++ /dev/null @@ -1,337 +0,0 @@ - - -`feat(storage): implement storage-level proxy policy and align drivers with Go behavior` - -## Summary / 摘要 - - - -本 PR 修复了存储级下载/代理配置在 TSWorker 后端**全部失效**的问题,并把四个此前只有数据库列与表单项、没有读取方的字段真正接入运行时,行为与 Go 版 OpenList 对齐。 - -**问题背景**:`web_proxy`、`webdav_policy`、`down_proxy_url`、`disable_proxy_sign`、`proxy_range`、`enable_sign`、`disable_index`、`custom_cache_policies` 这些字段在 TSWorker 中都能在后台填写、也能存入数据库,但后端**没有任何代码读取它们**。同时 `server/raw.ts` 用一段硬编码的驱动名单强制若干驱动走代理: - -```ts -const needsProxy = - isProxy || - normDriver === "webdav" || - normDriver === "sharepoint" || - normDriver === "onedrive" || - normDriver === "onedriveapp" || - normDriver === "weiyun" || - normDriver === "tencentweiyun" -``` - -因此 **OneDrive 无论后台怎么配置都无法使用 302 直链**,管理员在存储编辑页看到的所有代理选项实际上都是装饰。 - - - -### 用户可感知的行为变化 - -- **OneDrive / OneDriveAPP 默认改为 302 直链下载**(对齐 Go 默认值 `302_redirect`)。此前被硬编码强制代理。如需保留代理行为,请将该存储的 `webdav_policy` 设为 `native_proxy`。 -- `web_proxy`、`webdav_policy`、`down_proxy_url`、`disable_proxy_sign` 在后台的配置**从此真实生效**。 -- 存储编辑表单新增四个字段:`proxy_range`、`enable_sign`、`disable_index`、`custom_cache_policies`。 -- 存储表单按驱动能力差异化显示:Go 的 `MustProxy`(`OnlyProxy`/`NoLinkURL`)驱动不再出现 302 选项;`PreferProxy` 驱动的 `web_proxy` 默认勾选;`proxy_range` 仅对 Go 中声明 `ProxyRangeOption` 的驱动显示。 -- `/fs/list` 对开启 `disable_index` 的存储返回 `403 {"message":"Index is disabled for this storage"}`。 -- `/fs/list` 响应新增 `cache_expiration` 字段(路径级缓存策略计算后的分钟数)。 -- **代理下载超过平台载荷上限时不再返回平台错误页**:EdgeOne 云函数对单次请求/响应 body 的上限为 6 MiB,原生代理超限时改为「降级 302 直链」或返回可读的 413(详见下方「平台载荷上限保护」)。 - -### 重要实现变化 - -- 新增 `internal/driver/proxy.ts`:统一的下载模式决策与驱动代理能力注册表,对齐 Go 的 `Config.MustProxy()` / `Config.DefaultProxy()` / `ShouldProxy()`。 -- 新增 `internal/driver/storageopts.ts`:解析 `proxy_range` / `enable_sign` / `disable_index` / `cache_expiration` / `custom_cache_policies` 并提供 glob 匹配与缓存时长合成。 -- `server/raw.ts`:用 `resolveProxyDecision()` 替换硬编码驱动名单;抽出 `proxyUpstream()`;实现 `use_proxy_url`(含 `$path` 替换与运行时补签)。 -- `server/admin.ts`:新增 `buildProxyFields()` 复刻 Go `internal/op/driver.go` 的表单分支规则。 -- `pkg/sign.ts`:`isEncryptPath` 增加存储级签名分支,判定顺序为「存储 `enable_sign` → meta 密码」。 -- **修复 `getDownProxyUrl()`**:此前只读 `storage.addition`,而表单把 `down_proxy_url` 写在存储行**顶层**,导致 `use_proxy_url` 永远取不到值。 - -### 配置 / 存储 / API 变化 - -- 无数据库 schema 变更:所用列均已存在,本 PR 仅补上读取方。 -- `/fs/list` 响应为**向后兼容的增量变更**(新增 `cache_expiration`,且开启 `disable_index` 时会新增 403 分支)。 -- 新增 19 个单元测试。 - -- [ ] This PR has breaking changes. - / 此 PR 包含破坏性变更。 -- [x] This PR changes public API, config, storage format, or migration behavior. - / 此 PR 修改了公开 API、配置、存储格式或迁移行为。 -- [ ] This PR requires corresponding changes in related repositories. - / 此 PR 需要关联仓库同步修改。 - -> 关于兼容性:本 PR **不含破坏性变更**,但存在**行为变更**——OneDrive 系列的默认下载模式由「强制代理」变为「302 直链」。这与 Go 版一致,但会改变既有 OneDrive 存储的实际表现:存量存储没有 `webdav_policy` 值,将解析为 `302_redirect`。若某些挂载依赖代理(例如直链在受限租户下不可用),请显式设置 `webdav_policy = native_proxy`。该行为变更已在下方「Testing」中说明,建议在 release note 中提示用户。 - -Related repository PRs / 关联仓库 PR: - -- OpenList: N/A(本 PR 为对齐 Go 版既有行为,无需 Go 侧改动) -- OpenList-Docs: - -## Related Issues / 关联 Issue - - - -Relates to #51 - -> 说明:本 PR **不修复** #51 的根因。`enable_sign` 会走 `getJwtSecret`,而 `readPersistedSecret` 只探测 KV、没有 D1 分支,因此在 D1 多实例部署下仍会遇到签名密钥不稳定。`enable_sign=false` 可作为临时绕过手段,但密钥持久化需要单独修复。 - -## Testing / 测试 - - - -- [ ] `go test ./...`(本项目为 TypeScript,不适用;替代命令见下) -- [ ] Manual test / 手动测试: - -本项目使用的等价命令: - -```bash -npx tsc --noEmit -p tsconfig.json # 类型检查:exit 0,无错误 -node --import tsx --test "src/backend/**/*.test.ts" -``` - -测试结果(已合并 `origin/main`,即 main 上的 `9b9c243`):**276 个测试,271 通过**。 - -其中 5 个失败为**既有问题,与本 PR 无关**——在**纯净的 `origin/main` 上跑同一套命令得到完全相同的 5 个失败**(基线为 200 个测试 / 195 通过 / 5 失败): - -- `server/default_credentials.test.ts` — 默认凭据 / ADMIN_PASS 初始化与重置 - (`Initialization: a fresh deployment stays uninitialized without ADMIN_PASS`、 - `Initialization: an empty admin password stays empty (uninitialized), not a random one`、 - `Security(F-11): ADMIN_PASS still forces an explicit reset (to salted double-SHA256)`) -- `server/seed.test.ts` — `CAS codec matches casmeta base64 JSON field names` - -新增 `server/proxy_request.test.ts` 的平台载荷上限用例(21 个,该文件累计 36 个),覆盖: - -- 上限解析:EdgeOne 运行时默认 6 MiB(含 SCF / Blob 判据)、`RAW_PROXY_MAX_BYTES` 覆盖与置 0 关闭、非法值回退默认 -- 运行时判定:`__requestOrigin` 不构成 EdgeOne 判据(该值在所有平台都会注入,不能用来判平台) -- 超限判定:等于上限不算超限、Range 分片按分片大小、大小未知不拦截 -- 决策:未超限照常代理、超限且可直连时降级 302、超限且必须带私有鉴权头时返回可读 413 -- 串联 `resolveProxyDecision()`:`web_proxy=true` 的 OneDrive 大文件降级 302;带 `Authorization` 的 WebDAV 大文件返回可读 413 -- **Range + 签名 + 上限的交互**(响应评审意见):`upstreamBodySize()` 解析 Content-Length / Content-Range;412 兜底重试后上游回整份文件时按实际大小降级 302(签名 URL 无需重算);正常 206 分片不被误伤;必须带鉴权头时返回可读 413 -- **私有头判定**:`X-Emby-Token` / `X-Amz-Security-Token` / `X-Session-Id` 视为私有头;`User-Agent` / `Referer` / `Origin` / `Content-Type` 不算(与仓库内 33 处 `raw_url_headers` 的实际用法一致) - -新增 `internal/driver/storageopts.test.ts`(19 个用例),覆盖: - -- `proxy_range`:未配置时默认透传(对齐 Go 的透明代理)、显式 true/false 优先 -- `enable_sign` / `disable_index`:字符串与布尔两种存储形式 -- `custom_cache_policies`:JSON 数组、对象映射、`max_age` 别名、非法输入不抛错 -- glob 匹配:`*` 不跨目录分隔符、`**` 可跨、`?` 通配 -- 缓存时长合成:命中覆盖、未命中用基础值、值为 0 表示不缓存、多规则最后一条优先、非法/负数回退默认 -- 代理决策协同:默认驱动走 302、开启代理后 `proxy_range` 决定是否透传 Range、`down_proxy_url` 多写法解析 - -> **未做的验证**:未在真实 Cloudflare Workers / EdgeOne 环境跑端到端下载。`use_proxy_url` 的运行时补签路径、以及 Range 透传在中转场景下的实际表现建议在合并前手动回归。产物需重新执行 `node scripts/build-edge.mjs` 生成。 - -## Checklist / 检查清单 - -- [ ] I have read [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md). - / 我已阅读 [CONTRIBUTING](https://github.com/OpenListTeam/OpenList/blob/main/CONTRIBUTING.md)。 -- [ ] I confirm this contribution follows the repository license, contribution policy, and code of conduct. - / 我确认此贡献符合仓库许可证、贡献规范和行为准则。 -- [ ] I have formatted the changed code with `gofmt`, `go fmt`, or `prettier` where applicable. - / 我已按适用情况使用 `gofmt`、`go fmt` 或 `prettier` 格式化变更代码。 -- [ ] I have requested review from relevant maintainers or code owners where applicable. - / 我已在适用情况下请求相关维护者或代码所有者审查。 - -## AI Disclosure / AI 使用声明 - - - -- [x] This PR includes AI-assisted content. - / 此 PR 包含 AI 辅助内容。 - -Tools used / 使用工具: - -- [ ] ChatGPT -- [ ] Codex -- [ ] GitHub Copilot -- [ ] Claude -- [x] Gemini -- [x] Other (please specify) / 其他(请注明): CodeBuddy (DeepSeek-V4.1-Flash) - -Usage scope / 使用范围: - -- [x] Code generation / 代码生成 -- [x] Refactoring / 重构 -- [ ] Documentation / 文档 -- [x] Tests / 测试 -- [ ] Translation / 翻译 -- [x] Review assistance / 审查辅助 - -- [x] I have reviewed and validated all AI-assisted content included in this PR. - / 我已审核并验证此 PR 中的所有 AI 辅助内容。 -- [x] I have ensured that all AI-assisted commits include `Co-Authored-By` attribution. - / 我已确保所有 AI 辅助提交都包含 `Co-Authored-By` 归属信息。 -- [x] I can reproduce all AI-assisted content included in this PR without any AI tools. - / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。 - -> **已处理(响应评审 P0)**:本分支的全部提交都已在提交信息中带上 `Co-Authored-By: CodeBuddy `。此前缺少该 trailer 的三个提交通过 `git cherry-pick` + `git commit --amend --trailer` 重放补上(文件树与重写前完全一致,`git diff` 为空),因此分支历史被重写并 `--force-with-lease` 更新,提交哈希已变化(见下方「Commits / 提交」)。 - -## Implementation Notes / 实现说明 - -### 决策顺序(对齐 Go) - -`resolveProxyDecision()` 的判定顺序与 Go 的优先级一致: - -| 顺序 | 条件 | 结果 | 来源标记 | -|---|---|---|---| -| 1 | 驱动强制代理(`MustProxy`) | `native_proxy` | `force` | -| 2 | 存储 `web_proxy = true` | `native_proxy` | `web_proxy` | -| 3 | 请求命中 `/p`、`/sd` 等代理前缀 | `native_proxy` | `proxy_path` | -| 4 | 存储 `webdav_policy` 已配置 | 按配置值 | `storage_policy` | -| 5 | 驱动默认(`PreferProxy`,如 WebDav) | `native_proxy` | `driver_default` | -| 6 | 兜底 | `302_redirect` | `driver_default` | - -第 3 项保留了原有的 `isProxy` 路径判断作为显式入参,确保 `/p` 端点行为不发生回归。 - -### 驱动表单分支(对齐 Go `internal/op/driver.go`) - -- **15 个 `MustProxy`(`OnlyProxy`/`NoLinkURL`)驱动**(WeiYun、SFTP、FTP、SMB、Crypt、Virtual、Strm、Mega_nz、ProtonDrive、Chunk、GoogleDrive、GooglePhoto、QuarkOpen、QuarkUC、ChaoXing):策略选项为 `use_proxy_url,native_proxy`,默认 `native_proxy`,**不提供 302**。清单由 `internal/driver/proxy.ts` 单点提供,`admin.ts` 不再另抄一份(此前 123Pan / BaiduNetdisk / 115Open / 189Cloud / Terabox / 123PanShare 被误标为 `only_proxy`,而 Go 里它们只有 `PreferProxy` 或没有任何标记) -- **WebDav**:`web_proxy` 默认 `true`,策略默认 `native_proxy`(对应 Go `PreferProxy: true`) -- **Onedrive / OnedriveAPP**:默认 `302_redirect` -- **`proxy_range`**:仅对 Go 中声明 `ProxyRangeOption: true` 的 4 个驱动开放(139Yun、Alias、AListV3、OpenList)。语义为「是否透传客户端 Range」,**默认 `true`**:Go 的透明代理本就转发客户端请求头(`internal/net/serve.go` 的 `ProcessHeader`),默认关闭会让代理模式下的 seek / 断点续传静默退化;上游拒绝或忽略 Range 时由 `shouldRetryWithoutRange()` 兜底 - -### `use_proxy_url` 的签名处理 - -`down_proxy_url` 模板由管理员配置、不携带实例密钥,因此无法在模板中预置签名。本 PR 在运行时判定:当目标地址指向本站(相对路径或同 host)且未设置 `disable_proxy_sign` 时,自动补上 `sign` 查询参数,避免代理端点因缺少签名被拒。构造出的 URL 仍会经过 `assertSafeUrl` 做 SSRF 校验。 - -### 平台载荷上限保护(EdgeOne 云函数 6 MiB) - -`native_proxy` 会把整份文件当作云函数响应体回传,而 EdgeOne Makers 的 Cloud Functions 对「函数的请求/响应 body」有 **6 MiB** 硬上限,超限时平台在**网关层**直接返回 `413 CLOUD_FUNCTION_PAYLOAD_TOO_LARGE`(Powered by Tencent EdgeOne Makers 的错误页)。请求根本到不了本服务,应用侧的 CORS 头、Range 处理、错误提示一律不会执行。 - -这也是 issue 中「EdgeOne + OneDrive 下载报 413」的直接原因:OneDrive 此前被硬编码强制代理;上一提交只把**默认值**改成 302,任何**仍走代理**的路径(`web_proxy`、`/p`、`webdav_policy=native_proxy`、`PreferProxy`/`MustProxy` 驱动)都依旧会把整份文件塞进云函数响应体。 - -`server/proxy_request.ts` 新增上限决策 `decideProxyPayloadAction()`,在 `proxyUpstream()` 与两处服务端字节流分支(`driver.createReadStream`、本地文件回退)之前执行: - -| 情形 | 结果 | -|---|---| -| 未超限 / 平台不限制(非 EdgeOne 且未设置 `RAW_PROXY_MAX_BYTES`) | 正常原生代理 | -| 超限 + `raw_url` 可被浏览器直连(预授权直链) | 降级 302 直链,下载仍然可用 | -| 超限 + 无法降级(`driverMustProxy`,或直链需要 `Authorization`/`Cookie`) | 返回可读的 413,而不是平台错误页 | - -Range 只回传一个分片时按**分片大小**判断,视频拖动进度与断点续传不受影响;`proxy_range` 关闭时上游返回完整文件,因此按完整大小判断。 - -**二次校验(响应评审意见)**:前置判断用的是「客户端请求的分片大小」,但 `shouldRetryWithoutRange()` 会删掉 Range 重试、上游也可能忽略 Range 直接回 200——这两种情况下游回传的是**整份文件**,仅靠前置判断会漏放。因此 `proxyUpstream()` 在上游响应到达后再用 `upstreamBodySize()`(Content-Length,缺省回退 Content-Range 分段长度)复核一次实际大小,超限则取消上游 body 并降级 302 / 返回可读 413;206 分片与签名 URL 均不受影响(签名绑定 URL,重试无需重算)。 - -新增环境变量: - -| 变量 | 默认 | 说明 | -|---|---|---| -| `RAW_PROXY_MAX_BYTES` | EdgeOne 运行时 6 MiB;其他平台不限制 | 平台单次请求/响应上限(字节),设为 `0` 关闭限制(自托管恢复「永远代理」) | - -**运行时判定(`isEdgeOneRuntime()`)**:EdgeOne Node 云函数跑在腾讯 SCF 上,平台会注入 `TENCENTCLOUD_SCF_FUNCTIONNAME`(与本仓库 `internal/model/store/backend.ts` 的 `isServerlessRuntime()` 判据一致),因此以该变量 + `EDGEONE_BLOB` / 全局 `EdgeOne` 作为 EdgeOne 特征。**刻意不用 `__requestOrigin`**:它由本仓库 `src/backend/index.ts` 中间件在所有平台上注入,拿它判 EdgeOne 会把 Cloudflare / 自托管一起误判,反而让本可正常代理的大文件被降级成 302。Cloudflare Workers / 阿里云 ESA 不套用 6 MiB;若这些平台也有同类上限,用 `RAW_PROXY_MAX_BYTES` 显式声明即可。 - -### 已知限制 - -`custom_cache_policies` 目前**只在 `/fs/list` 响应中回传计算结果**,并未真正改变对象缓存的读写行为——TSWorker 的缓存层尚无「按路径取过期时长」的入口。要做到 Go 那样真正影响缓存,需要接入缓存层,属后续独立工作。 - -## Commits / 提交 - -- `8c0a88d` — `feat(proxy): align OneDrive and other drivers with Go 302/proxy policy` -- `7a9bcfc` — `feat(storage): implement proxy_range, enable_sign, disable_index and cache policies` -- `954ebff` — `fix(proxy): remove glob catastrophic backtracking and cover Range+sign interactions` -- `e5cbb9d` — `fix(proxy): fall back to 302 when the platform payload limit blocks native proxy` -- `cc896eb` — `docs(pr): document the platform payload-limit guard for the storage proxy policy PR` -- `cb4051f` — `fix(proxy): bound the upstream body size after a Range retry` -- `47d70d3` — `fix(proxy): detect EdgeOne runtimes by the SCF and Blob markers` -- `23a957e` — `refactor(proxy)!: align driver proxy capabilities with Go meta.go` -- `2e913e6` — `docs(pr): record the Go alignment pass` -- `8454f76` — `feat(proxy): honour proxy_types, text_types and proxy_ignore_headers` -- `38ed5de` — `docs(pr): document extension proxying and the /p gate` -- `6d38353` — `Merge remote-tracking branch 'origin/main' into feat/storage-proxy-policy` - (解决 `internal/model/db.ts` 的 `unsealDb` 并行解密与 `server/raw.ts` 的下载路径 - mount name 剥离两处冲突,均取 main 的实现 + 保留本 PR 的 `/p` 端点判定) -- `c2fad5a` — `chore(edgeone): refresh cloud-functions artifact` - (分支上的 `cloud-functions/[[default]].js` 早于本 PR 的改动,曾不含 payload 上限保护 - 与 `/p` 准入等代码;此处按仓库既有约定重建) -- (本条 docs 提交)— `docs(pr): sync the commit list and the test baseline after the main merge` - -(哈希随本 PR 的最新提交更新;完整 diff 规模以 GitHub PR 页面为准。) - -> **破坏性行为变更(第二处)**:`PreferProxy` 驱动的「未配置 `web_proxy`」仍默认代理;但 -> **显式设置 `web_proxy=false` 的 WebDav / 123Pan / BaiduNetdisk 存储会从「代理」变为「302 直链」** -> (与 Go 一致)。同理,此前被误标为强制代理的 115Open / 189Cloud / Terabox / 123PanShare -> 现在可以走直链;GoogleDrive / GooglePhoto / QuarkOpen / QuarkUC / ChaoXing 则改为强制代理。 - -## 评审意见处理 - -| 评审项 | 处理 | -|---|---| -| P0 提交缺少 `Co-Authored-By` 归属 | 已重写分支历史为全部 6 个提交补上 trailer(见上方「AI 使用声明」) | -| P1 `matchGlob()` 递归无深度限制可能 DoS | **已在 `954ebff` 修复**:`matchGlob()` 不再使用正则(旧实现把 glob 翻成正则,多个通配符产生相邻贪婪量词,实测单次匹配 508 秒 CPU),现为「按段切分 + 动态规划」,无递归、无指数回溯,并有模式/目标长度上限;已有 200 段 `**` 与恶意回溯模式的耗时回归测试。评审建议的 `MAX_GLOB_DEPTH = 10` 会误伤合法模式(如连续 200 个 `**/`),不是合适的修法 | -| P1 Range + 签名交互测试覆盖不足 | 已在 `954ebff` 抽出 `server/proxy_request.ts` 并补 17 个测试(Range 透传不改签名、412/200 兜底后无需重签、206 不重试等);本次 `cb4051f` 进一步补上「兜底重试后上游回整份文件」的二次大小校验与 4 个用例 | -| P2 驱动能力映射缺少文档注释 | **已在 `954ebff` 补上**:`internal/driver/proxy.ts` 顶部有驱动 ↔ Go `drivers/*/meta.go` 的完整映射表 | -| P2 未在真实环境端到端验证 | 仍需手动回归(见上方「未做的验证」) | -| **自审追加**:运行时判据写错,守卫在 EdgeOne 上不会生效 | **已在 `47d70d3` 修复**:原先只查 `EDGEONE` / `EO_REGION`,而 EdgeOne Node 云函数的平台特征是 `TENCENTCLOUD_SCF_FUNCTIONNAME`(见 `internal/model/store/backend.ts`),会导致 6 MiB 守卫在目标平台上静默失效;现补上 SCF / `EDGEONE_BLOB` 判据,并明确不采用 `__requestOrigin` | -| **自审追加**:私有头判定过于复杂 | 先在 `47d70d3` 改成「鉴权语义模式 + 白名单」,随后对比 Go 后**退回精确名单**(`Authorization` / `Cookie`):仓库内 33 处 `raw_url_headers` 只用这两种鉴权头,驱动若引入新头名应按 Go 的 `meta.go` 登记为 `MustProxy`,而不是在下载层猜头名 | -| **自审追加**:413 文案把平台写死 | **已在 `47d70d3` 修正**:改为中性表述,并提示限制也可能来自 `RAW_PROXY_MAX_BYTES` | - -## 与 Go 版逐项对齐(对比 `OpenList-Backends` 后修正) - -| 项 | Go 的事实 | 本 PR 的处理 | -|---|---|---| -| 强制代理清单 | `Config.MustProxy() = OnlyProxy \|\| NoLinkURL`,实际为 WeiYun、SFTP、FTP、SMB、Crypt、Virtual、Strm、Mega、ProtonDrive、Chunk、GoogleDrive、GooglePhoto、QuarkOpen、UC、ChaoXing(`local` 不在本 PR 的下载链路内;`bunny_storage` 是运行时条件) | 按此重写 `DRIVER_FORCE_PROXY`;删掉误标的 123Pan / BaiduNetdisk / 115Open / 189Cloud,补上缺失的 GoogleDrive / GooglePhoto / QuarkOpen / QuarkUC / ChaoXing / Chunk | -| 驱动能力的两份真相 | 能力声明只在各驱动 `meta.go`,`op/driver.go` 据此生成表单 | `admin.ts` 的 `buildProxyFields()` 改为按驱动名从 `internal/driver/proxy.ts` 派生(22 处调用点不再传 `only_proxy/prefer` 布尔值),并删除 `registerDriverProxyCapability()` 这类重复登记入口 | -| `PreferProxy` 的用途 | 只用于**表单默认值**(`op/driver.go`),运行时 `ShouldProxy` 只看 `MustProxy \|\| WebProxy` | `resolveProxyDecision()` 不再在运行时强制 `PreferProxy` 驱动代理,改为「`web_proxy` 未配置时回退到驱动默认值」(`effectiveWebProxy()`),显式 `web_proxy=false` 会被尊重 | -| `proxy_range` | 透明代理**默认转发客户端头(含 Range)**;`proxy_range` 只是额外启用驱动的 RangeReader 路径(`internal/net/serve.go`、`internal/stream/util.go`) | 默认改为**透传 Range**,`proxy_range=false` 才丢弃;保留 `shouldRetryWithoutRange()` 作为上游拒绝 Range 的兜底 | -| 未实现的 Go 特性 | `proxy_types` / `text_types` 扩展名代理、`/p` 的 403 限制、`proxy_ignore_headers` | **已实现**(见下方「扩展名代理与 /p 准入」),默认值对齐 Go 的 `internal/bootstrap/data/setting.go` | - -### 扩展名代理与 /p 准入(对齐 Go 的 proxy_types / text_types / canProxy) - -- **`proxy_types`(默认 `m3u8,url`)**:命中的扩展名一律由服务端代理——`.m3u8` / `.url` - 内部含相对引用,只有经本服务转发才能被正确解析。作用于 `/d`、`/sd` 与 `/p` - (即 Go `ShouldProxy` 的第三条)。 -- **`text_types`(默认值取 Go 与 TSWorker 既有默认的并集)**:`/p` 端点用它放行文本类 - 预览(README、歌词 `.lrc`、字幕 `.srt` / `.ass` / `.vtt`、弹幕等),即使存储没有开启 - `web_proxy`。历史默认值会由 `LEGACY_SETTING_MIGRATIONS` 自动迁移,避免升级后字幕/歌词 - 预览被 403。 -- **`/p` 准入检查(Go `canProxy`)**:命中 - `MustProxy || web_proxy || webdav_policy=use_proxy_url || proxy_types || text_types` - 才放行,否则 **403 `proxy not allowed`**。`/d`、`/sd` 不做该限制(Go 里它们走 - `ShouldProxy`)。WebDAV 协议端点(`/dav/*`)的 GET/HEAD 会按同一判据选择 - `/api/p` 或 `/api/d`,避免 WebDAV 客户端拿到 403。 -- **`proxy_ignore_headers`(默认 `authorization,referer`)**:作用于本服务转发给上游的 - **客户端头**(Range、兜底 User-Agent);驱动自己声明的头不受影响——与 Go 的 - `ProcessHeader` 一致(先过滤客户端头,再套驱动 override)。注意 TS 只转发这几个必要 - 的头,不会像 Go 那样把客户端全部请求头转发给上游(避免 Cookie/Authorization 泄漏给 - 第三方上游)。 -- **`bunny_storage` 的条件能力**:对齐 Go `drivers/bunny_storage/driver.go` 的 `Config()` - ——配置了 `storage_zone_name` 但未绑定 `cdn_base_url` 时视为 `MustProxy`(此时只有 - Storage API 可读,浏览器直连会 401);绑定 CDN 后按普通驱动处理。 -| 平台载荷上限守卫(本 PR 新增) | Go 无常驻进程内的大小限制(由 nginx 等外部配置负责) | 属 EdgeOne 等 Serverless 平台的适配层,非照搬 Go;已删掉其中无 Go 对应、也无实际需求的 `RAW_PROXY_OVERFLOW` 开关 | diff --git a/readmes/README_ar.md b/readmes/README_ar.md index b1a0de13..2adc0447 100644 --- a/readmes/README_ar.md +++ b/readmes/README_ar.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [التوثيق](https://doc.oplist.org) · 🌏 [التوثيق (الصين القارية)](https://doc.oplist.org.cn) · ⚖️ [شروط الاستخدام](https://doc.oplist.org/terms) · 🔒 [سياسة الخصوصية](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · دولي | EdgeOne Makers · الصين | Cloudflare Workers · عالمي | -| :---: | :---: | :---: | -| [![النشر على EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![النشر على EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · دولي | EdgeOne Makers · الصين | Cloudflare Workers · عالمي | Alibaba Cloud ESA · الصين | +| :---: | :---: | :---: | :---: | +| [![النشر على EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![النشر على EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![النشر على Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - إذا عرض Cloudflare رسالة `تعذّر جلب محتوى المستودع`، [قم بعمل fork](https://github.com/OpenListTeam/OpenList-Worker/fork) لهذا المشروع أولاً ثم انشر من خلال الاتصال بمستودع Github -> - عند النشر، املأ `JWT_SECRET` (سلسلة عشوائية من 16 حرفًا أو أكثر، مثل `openssl rand -hex 32`): تُستخدم لتوقيع JWT وتشفير البيانات؛ وإن لم تُضبط فسيتم توليدها وحفظها أثناء التهيئة +> - بالنسبة إلى Alibaba Cloud ESA، استورد هذا المستودع من **الحوسبة الطرفية والذكاء الاصطناعي ← الدوال وPages** في وحدة التحكم +> - اضبط `JWT_SECRET` أثناء النشر (≥16 حرفًا) لتوقيع JWT والتشفير > - يستخدم تخزين البيانات تلقائيًا الواجهة الخلفية الدائمة للمنصة (`DB_DRIVER=auto`) — لا حاجة لأي إعداد؛ ولمتغيرات أخرى راجع [متغيرات البيئة](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - بعد النشر، يظهر **معالج التثبيت** عند أول زيارة: عيّن حساب المسؤول وكلمة المرور في المتصفح لإكمال التهيئة (اضبط `ADMIN_PASS` لتخطي المعالج) @@ -69,32 +70,23 @@ OpenList-Worker هو المنفذ الرسمي TypeScript + Serverless لمشر ### القدرات الأساسية -- **تصفح الملفات**: تصفح موحّد لشجرة الأدلة، مع معاينة عبر الإنترنت للصور والفيديو والصوت والمستندات والأكواد والأرشيف وغيرها. -- **الرفع والتنزيل**: رفع عبر التخزين، وتنزيل جماعي، وبث، وإعادة توجيه برابط مباشر. -- **مشاركة الملفات**: إنشاء روابط مشاركة مع انتهاء صلاحية وكلمة مرور والتحكم في الصلاحيات، مع دعم الوصول المجهول ومشاركة الأدلة. -- **البحث بالنص الكامل**: بحث سريع عن الملفات في التخزين المفهرس. -- **المهام دون اتصال**: قائمة انتظار مهام في الخلفية تدعم العمليات الجماعية والمعالجة غير المتزامنة. -- **الواجهات الخارجية**: كشف التخزين المجمّع عبر بروتوكول WebDAV أو بروتوكول متوافق مع S3 للتركيب في أدوات خارجية. -- **خدمة MCP**: توفير نقطة نهاية Model Context Protocol يمكن دمجها واستدعاؤها من مساعدي الذكاء الاصطناعي والعملاء الآخرين. - -### إدارة الوصول - -- **الصلاحيات**: التحكم في الوصول القائم على الأدوار (RBAC)، مع دعم مجموعات المستخدمين وصلاحيات القراءة/الكتابة على مستوى الأدلة والحصص. -- **المصادقة**: كلمات مرور حسابات مدمجة مع التحقق عبر TOTP، وتسجيل الدخول WebAuthn/FIDO، والدخول الموحّد SSO، ومصادقة دليل LDAP. -- **تعزيز الأمان**: جلسات JWT، والحماية من CSRF، والحماية من النقر الخادع (X-Frame-Options)، وسياسة أمان المحتوى (CSP). -- **فحوصات السلامة**: توفير مسبار الحيوية `/health` ومسبار الجاهزية `/healthz` للمراقبة والتنبيه. - -### النشر على المنصات - -- **منصات التشغيل**: Cloudflare Workers وTencent Cloud EdgeOne Makers وVercel وServerless وبيئات حاويات Node.js. -- **تخزين البيانات**: Cloudflare D1 (SQLite) كأساس، مع دعم MySQL وMariaDB وPostgreSQL وSQL Server. -- **الذاكرة المؤقتة الدائمة**: Cloudflare KV / EdgeOne Blob (اختياري) لحفظ الإعدادات والتخزين المؤقت. -- **النشر بنقرة واحدة**: دعم أزرار النشر بنقرة واحدة + التهيئة على EdgeOne وCloudflare Workers ومنصات أخرى. +- **إدارة الملفات**: تصفح موحّد لشجرة الأدلة مع معاينة عبر الإنترنت للصور والفيديو والصوت والمستندات والأكواد والأرشيف؛ رفع عبر التخزين وتنزيل جماعي وبث وإعادة توجيه برابط مباشر؛ روابط مشاركة مع انتهاء صلاحية وكلمة مرور والتحكم في الصلاحيات. +- **البحث والمهام**: بحث بالنص الكامل في التخزين المفهرس؛ قائمة مهام في الخلفية للعمليات الجماعية والمعالجة غير المتزامنة. +- **الواجهات الخارجية**: WebDAV وبروتوكول متوافق مع S3 ونقطة نهاية MCP لمساعدي الذكاء الاصطناعي. +- **الوصول والمصادقة**: RBAC (مجموعات المستخدمين وصلاحيات القراءة/الكتابة على مستوى الأدلة والحصص)، وكلمات مرور الحسابات مع TOTP / WebAuthn / SSO / LDAP. +- **الأمان والتشغيل**: جلسات JWT والحماية من CSRF والنقر الخادع (X-Frame-Options) وCSP، ومسبارات `/health` و`/healthz`. +- **إزالة تكرار الطلبات المتزامنة**: طلبات قائمة الدليل / البيانات الوصفية المتطابقة تصل إلى القرص الأعلى مرة واحدة فقط؛ وتعيد الطلبات الأخرى استخدام النتيجة، مما يقلل حدود المعدل واستهلاك المعالج. +- **منصات التشغيل**: Cloudflare Workers وTencent Cloud EdgeOne Makers وAlibaba Cloud ESA وVercel / AWS Lambda وحاويات Node.js. +- **تخزين البيانات**: Cloudflare D1 (SQLite) كأساس، بالإضافة إلى KV / Blob / Durable Objects وMySQL / MariaDB الخارجيين. +- **النشر بنقرة واحدة**: أزرار نشر EdgeOne وCloudflare Workers وAlibaba Cloud ESA + معالج التثبيت. --- ## النشر اليدوي +
+عرض المتطلبات الأساسية والتطوير المحلي والنشر في الإنتاج + ### المتطلبات الأساسية - Node.js 18+ (يُوصى باستخدام pnpm) @@ -106,7 +98,7 @@ OpenList-Worker هو المنفذ الرسمي TypeScript + Serverless لمشر # 1. تثبيت التبعيات pnpm install -# 2. إعداد wrangler.toml (تعبئة JWT_SECRET وارتباطات KV/D1) +# 2. إعداد الارتباطات والمتغيرات في wrangler.jsonc حسب الحاجة (JWT_SECRET انظر .dev.vars.example) # 3. تشغيل خادم التطوير (يجلب الواجهة الأمامية الرسمية تلقائيًا ويشغّل Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### النشر في الإنتاج ```bash -# النشر بنقرة واحدة: التأكد من وجود namespace الخاص بـ KV → جلب الواجهة الأمامية الرسمية → النشر على Cloudflare Workers +# النشر بنقرة واحدة: بناء الواجهة الأمامية ← النشر على Cloudflare Workers (تُجهَّز ارتباطات KV تلقائيًا بواسطة wrangler) pnpm run deploy -# أو نشر Worker مباشرة (تخطي فحص KV وبناء الواجهة الأمامية) +# أو نشر Worker مباشرة (تخطي بناء الواجهة الأمامية) pnpm run deploy:worker ``` +
+ --- ## البنية التقنية ### الواجهة الخلفية -- **بيئة التشغيل**: Cloudflare Workers (Edge Computing) +- **بيئة التشغيل**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **إطار الويب**: Hono.js -- **قاعدة البيانات**: Cloudflare D1 (SQLite) / تدعم MySQL وMariaDB وPostgreSQL وSQL Server -- **الذاكرة المؤقتة**: Cloudflare KV (اختياري) +- **قاعدة البيانات**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects أو MySQL / MariaDB خارجي - **اللغة**: TypeScript - **أدوات البناء**: Wrangler وesbuild ### الواجهة الأمامية -- **الإطار**: React 19 + TypeScript -- **مكتبات الواجهة**: Ant Design / Material-UI +- **الإطار**: SolidJS + TypeScript (الرسمي [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **مكتبة الواجهة**: Hope UI - **أداة البناء**: Vite --- +## إعداد النشر + +بعد النشر، اضبط المتغيرات التالية: + +| المتغير | الافتراضي | الوصف | +| :--- | :--- | :--- | +| `JWT_SECRET` | (لا شيء) | مفتاح توقيع JWT (≥16 حرفًا) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (لكل مفتاح) / `sql` (علائقي) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | تشفير الحقول الحساسة عند التخزين، مثل `aes-256-gcm` | + +للقائمة الكاملة للمتغيرات، راجع [متغيرات الإعداد](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## الدعم diff --git a/readmes/README_de.md b/readmes/README_de.md index b8cebf03..be9d35db 100644 --- a/readmes/README_de.md +++ b/readmes/README_de.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Dokumentation](https://doc.oplist.org) · 🌏 [Dokumentation (chinesisches Festland)](https://doc.oplist.org.cn) · ⚖️ [Nutzungsbedingungen](https://doc.oplist.org/terms) · 🔒 [Datenschutzerklärung](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Klicken Sie auf die Schaltfläche unten, um dieses Projekt mit einem Klick auf d
-| EdgeOne Makers · International | EdgeOne Makers · China | Cloudflare Workers · Global | -| :---: | :---: | :---: | -| [![Auf EdgeOne bereitstellen](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Auf EdgeOne bereitstellen](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · International | EdgeOne Makers · China | Cloudflare Workers · Global | Alibaba Cloud ESA · China | +| :---: | :---: | :---: | :---: | +| [![Auf EdgeOne bereitstellen](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Auf EdgeOne bereitstellen](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Auf Alibaba Cloud ESA bereitstellen](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Wenn Cloudflare `Repository-Inhalt kann nicht abgerufen werden` anzeigt, [forken](https://github.com/OpenListTeam/OpenList-Worker/fork) Sie dieses Projekt zuerst und stellen Sie es dann über die Verbindung zum Github-Repository bereit -> - Tragen Sie bei der Bereitstellung `JWT_SECRET` ein (Zufallszeichenfolge mit 16+ Zeichen, z. B. `openssl rand -hex 32`): Sie signiert JWTs und verschlüsselt Daten; ohne Angabe wird sie bei der Initialisierung erzeugt und gespeichert +> - Importieren Sie für Alibaba Cloud ESA dieses Repository in der Konsole unter **Edge Computing und AI → Funktionen und Pages** +> - Setzen Sie bei der Bereitstellung `JWT_SECRET` (≥16 Zeichen) für JWT-Signatur und Verschlüsselung > - Der Datenspeicher verwendet automatisch das plattformeigene persistente Backend (`DB_DRIVER=auto`) — keine Konfiguration nötig; weitere Variablen siehe [Umgebungsvariablen](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - Beim ersten Besuch nach der Bereitstellung öffnet sich der **Installationsassistent**: Legen Sie Admin-Konto und Passwort im Browser fest, um die Initialisierung abzuschließen (mit `ADMIN_PASS` überspringen Sie den Assistenten) @@ -69,32 +70,23 @@ Zusätzlich zu den oben genannten echten Speichern werden auch virtuelle/funktio ### Kernfunktionen -- **Dateidurchsuchen**: einheitliches Durchsuchen des Verzeichnisbaums, mit Online-Vorschau von Bildern, Videos, Audio, Dokumenten, Code, Archiven und mehr. -- **Hochladen & Herunterladen**: speicherübergreifendes Hochladen, Stapel-Download, Streaming und Direktlink-Weiterleitung. -- **Dateifreigabe**: Erzeugen von Freigabe-Links mit Ablauf, Passwort und Berechtigungssteuerung, unterstützt anonymen Zugriff und Verzeichnisfreigabe. -- **Volltextsuche**: schnelles Suchen von Dateien in indizierten Speichern. -- **Offline-Aufgaben**: Hintergrund-Aufgabenwarteschlange für Stapeloperationen und asynchrone Verarbeitung. -- **Externe Schnittstellen**: aggregierten Speicher über WebDAV oder S3-kompatibles Protokoll bereitstellen, zum Mounten in Drittanbieter-Tools. -- **MCP-Dienst**: bietet einen Model-Context-Protocol-Endpunkt, der von KI-Assistenten und anderen Clients integriert und aufgerufen werden kann. - -### Zugriffsverwaltung - -- **Berechtigungen**: rollenbasierte Zugriffskontrolle (RBAC), unterstützt Benutzergruppen, Lese-/Schreibberechtigungen auf Verzeichnisebene und Kontingente. -- **Authentifizierung**: integrierte Kontokennwörter mit TOTP-Verifizierung, WebAuthn/FIDO-Anmeldung, SSO-Single-Sign-on und LDAP-Verzeichnisauthentifizierung. -- **Sicherheitshärtung**: JWT-Sitzungen, CSRF-Schutz, Clickjacking-Schutz (X-Frame-Options), Content Security Policy (CSP). -- **Gesundheitsprüfungen**: bietet eine `/health`-Liveness-Probe und eine `/healthz`-Readiness-Probe für Überwachung und Alarmierung. - -### Plattform-Bereitstellung - -- **Laufzeitplattformen**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless und Node.js-Containerumgebungen. -- **Datenspeicher**: Cloudflare D1 (SQLite) als primär, unterstützt auch MySQL, MariaDB, PostgreSQL, SQL Server. -- **Persistenter Cache**: Cloudflare KV / EdgeOne Blob (optional) für Konfigurationspersistenz und Caching. -- **One-Click-Bereitstellung**: unterstützt One-Click-Bereitstellungsbuttons + Initialisierung auf EdgeOne, Cloudflare Workers und anderen Plattformen. +- **Dateiverwaltung**: einheitliches Durchsuchen des Verzeichnisbaums mit Online-Vorschau für Bilder, Videos, Audio, Dokumente, Code und Archive; speicherübergreifendes Hochladen, Stapel-Download, Streaming und Direktlink-Weiterleitung; Freigabe-Links mit Ablauf, Passwort und Berechtigungssteuerung. +- **Suche & Aufgaben**: Volltextsuche in indizierten Speichern; Hintergrund-Aufgabenwarteschlange für Stapel- und asynchrone Verarbeitung. +- **Externe Schnittstellen**: WebDAV, S3-kompatibles Protokoll und ein MCP-Endpunkt für KI-Assistenten. +- **Zugriff & Authentifizierung**: RBAC (Benutzergruppen, Lese-/Schreibrechte auf Verzeichnisebene, Kontingente), Kontokennwörter plus TOTP / WebAuthn / SSO / LDAP. +- **Sicherheit & Betrieb**: JWT-Sitzungen, CSRF-, Clickjacking- (X-Frame-Options) und CSP-Schutz, `/health`- und `/healthz`-Probes. +- **Parallelitäts-Deduplizierung**: identische Verzeichnislisten- / Metadatenanfragen erreichen das Upstream-Laufwerk nur einmal; weitere Anfragen nutzen dasselbe Ergebnis und vermeiden Ratenlimits und CPU-Last. +- **Laufzeitplattformen**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda und Node.js-Container. +- **Datenspeicher**: Cloudflare D1 (SQLite) als primär, außerdem KV / Blob / Durable Objects und externes MySQL / MariaDB. +- **One-Click-Bereitstellung**: Bereitstellungsschaltflächen für EdgeOne, Cloudflare Workers und Alibaba Cloud ESA + Installationsassistent. --- ## Manuelle Bereitstellung +
+Voraussetzungen, lokale Entwicklung und Produktionsbereitstellung anzeigen + ### Voraussetzungen - Node.js 18+ (pnpm empfohlen) @@ -106,7 +98,7 @@ Zusätzlich zu den oben genannten echten Speichern werden auch virtuelle/funktio # 1. Abhängigkeiten installieren pnpm install -# 2. wrangler.toml konfigurieren (JWT_SECRET, KV/D1-Bindungen eintragen) +# 2. Bindungen und Variablen in wrangler.jsonc nach Bedarf konfigurieren (JWT_SECRET siehe .dev.vars.example) # 3. Entwicklungsserver starten (holt automatisch das offizielle Frontend und führt den Worker aus) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Produktionsbereitstellung ```bash -# One-Click-Bereitstellung: sicherstellen, dass der KV-Namespace existiert → offizielles Frontend holen → auf Cloudflare Workers bereitstellen +# One-Click-Bereitstellung: Frontend bauen → auf Cloudflare Workers bereitstellen (KV-Bindungen werden von wrangler automatisch bereitgestellt) pnpm run deploy -# Oder den Worker direkt bereitstellen (KV-Prüfung und Frontend-Build überspringen) +# Oder den Worker direkt bereitstellen (Frontend-Build überspringen) pnpm run deploy:worker ``` +
+ --- ## Tech-Stack ### Backend -- **Laufzeit**: Cloudflare Workers (Edge Computing) +- **Laufzeit**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Web-Framework**: Hono.js -- **Datenbank**: Cloudflare D1 (SQLite) / unterstützt MySQL, MariaDB, PostgreSQL, SQL Server -- **Cache**: Cloudflare KV (optional) +- **Datenbank**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects oder externes MySQL / MariaDB - **Sprache**: TypeScript - **Build-Tools**: Wrangler, esbuild ### Frontend -- **Framework**: React 19 + TypeScript -- **UI-Bibliotheken**: Ant Design / Material-UI +- **Framework**: SolidJS + TypeScript (offizielles [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI-Bibliothek**: Hope UI - **Build-Tool**: Vite --- +## Bereitstellungs-Konfiguration + +Setzen Sie nach der Bereitstellung die folgenden Variablen: + +| Variable | Standard | Beschreibung | +| :--- | :--- | :--- | +| `JWT_SECRET` | (keine) | Signaturschlüssel für JWT (≥16 Zeichen) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (pro Schlüssel) / `sql` (relational) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Verschlüsselung sensibler Felder, z. B. `aes-256-gcm` | + +Die vollständige Variablenliste finden Sie unter [Konfigurationsvariablen](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Support diff --git a/readmes/README_en.md b/readmes/README_en.md index e7f67e76..6ecb4ffc 100644 --- a/readmes/README_en.md +++ b/readmes/README_en.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Docs](https://doc.oplist.org) · 🌏 [Docs (China mainland)](https://doc.oplist.org.cn) · ⚖️ [Terms of use](https://doc.oplist.org/terms) · 🔒 [Privacy policy](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Click the button below to deploy this project to the corresponding platform with
-| EdgeOne Makers · International | EdgeOne Makers · China | Cloudflare Workers · Global | -| :---: | :---: | :---: | -| [![Deploy to EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · International | EdgeOne Makers · China | Cloudflare Workers · Global | Alibaba Cloud ESA · China | +| :---: | :---: | :---: | :---: | +| [![Deploy to EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Deploy to Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - If Cloudflare prompts `cannot fetch repository content`, [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) this project first, then deploy by connecting to the GitHub repository -> - Set `JWT_SECRET` during deployment (a random string of ≥16 chars, e.g. `openssl rand -hex 32`): it signs JWTs and encrypts data; if unset, it is generated and persisted during initialization +> - For Alibaba Cloud ESA, import this repository from **Edge Computing and AI → Functions and Pages** in the console +> - Set `JWT_SECRET` during deployment (≥16 random chars) for JWT signing and encryption > - Storage automatically uses the platform's built-in persistent backend (`DB_DRIVER=auto`) — no configuration needed; for other variables see [Environment Variables](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - After deployment, the first visit opens the **install wizard**: set the admin account and password in the browser to finish initialization; set `ADMIN_PASS` to skip the wizard @@ -69,32 +70,23 @@ In addition to the real storages above, virtual/functional drivers such as `Loca ### Core Capabilities -- **File browsing**: unified directory tree browsing, with online preview of images, videos, audio, documents, code, archives and more. -- **Upload & download**: cross-storage upload, batch download, streaming and direct-link redirect. -- **File sharing**: generate share links with expiry, password and permission control, supporting anonymous access and directory sharing. -- **Full-text search**: quickly search files in indexed storages. -- **Offline tasks**: background task queue supporting batch operations and async processing. -- **External interfaces**: expose aggregated storage via WebDAV or S3-compatible protocol for mounting into third-party tools. -- **MCP service**: provide a Model Context Protocol endpoint that can be integrated and called by AI assistants and other clients. - -### Access Control - -- **Permissions**: role-based access control (RBAC), supporting user groups, directory-level read/write permissions and quotas. -- **Authentication**: built-in account passwords with TOTP verification, WebAuthn/FIDO login, SSO single sign-on and LDAP directory authentication. -- **Security hardening**: JWT sessions, CSRF protection, clickjacking protection (X-Frame-Options), Content Security Policy (CSP). -- **Health checks**: provide `/health` liveness probe and `/healthz` readiness probe for monitoring and alerting. - -### Platform Deployment - -- **Runtime platforms**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless and Node.js container environments. -- **Data storage**: Cloudflare D1 (SQLite) as primary, also supporting MySQL, MariaDB, PostgreSQL, SQL Server. -- **Persistent cache**: Cloudflare KV / EdgeOne Blob (optional) for config persistence and caching. -- **One-click deploy**: support one-click deploy buttons + initialization on EdgeOne, Cloudflare Workers and other platforms. +- **File management**: unified directory tree browsing with online preview for images, videos, audio, documents, code and archives; cross-storage upload, batch download, streaming and direct-link redirect; share links with expiry, password and permission control. +- **Search & tasks**: full-text search across indexed storages; background task queue for batch and async processing. +- **External interfaces**: WebDAV, S3-compatible protocol, and an MCP endpoint for AI assistants. +- **Access & auth**: RBAC (user groups, directory-level read/write permissions, quotas), account passwords plus TOTP / WebAuthn / SSO / LDAP. +- **Security & ops**: JWT sessions, CSRF, clickjacking (X-Frame-Options) and CSP protection, `/health` and `/healthz` probes. +- **Concurrency deduplication**: identical directory listing / metadata requests hit the upstream drive only once; the rest reuse the result, reducing rate limits and CPU usage. +- **Runtime platforms**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda and Node.js containers. +- **Data storage**: Cloudflare D1 (SQLite) as primary, plus KV / Blob / Durable Objects and external MySQL / MariaDB. +- **One-click deploy**: EdgeOne, Cloudflare Workers and Alibaba Cloud ESA deploy buttons + install wizard. --- ## Manual Deployment +
+Expand prerequisites, local development and production deployment + ### Prerequisites - Node.js 18+ (pnpm recommended) @@ -106,7 +98,7 @@ In addition to the real storages above, virtual/functional drivers such as `Loca # 1. Install dependencies pnpm install -# 2. Configure wrangler.toml (fill in JWT_SECRET, KV/D1 bindings) +# 2. Configure bindings and variables in wrangler.jsonc as needed (see .dev.vars.example for JWT_SECRET) # 3. Start the dev server (auto-fetches the official frontend and runs the Worker) pnpm run dev:unified @@ -118,13 +110,15 @@ pnpm run dev:worker ### Production Deployment ```bash -# One-click deploy: ensure the KV namespace exists → fetch the official frontend → deploy to Cloudflare Workers +# One-click deploy: build the frontend → deploy to Cloudflare Workers (KV bindings are provisioned automatically by wrangler) pnpm run deploy -# Or deploy the Worker directly (skip KV check and frontend build) +# Or deploy the Worker directly (skip the frontend build) pnpm run deploy:worker ``` +
+ --- ## Tech Stack @@ -146,37 +140,18 @@ pnpm run deploy:worker --- -## Configuration +## Deployment Configuration -The **install wizard** appears automatically on the first visit — set the admin account and password in the browser to finish initialization. To skip the wizard, set `ADMIN_PASS` and the admin is initialized automatically. - -Most deployments need **no extra configuration**: storage defaults to `DB_DRIVER=auto`, which picks an available persistent backend (Cloudflare Workers → KV / D1 / DO, EdgeOne / ESA → Blob). The only variable worth setting explicitly is: - -| Variable | Description | -| :--- | :--- | -| `JWT_SECRET` | JWT signing key (≥16 chars, e.g. `openssl rand -hex 32`), also used for cron task auth. Auto-generated and persisted during initialization when unset | - -> Configure `JWT_SECRET` as a **Secret** (not a plain variable): plain variables can be overwritten by the repository config on redeploy, secrets are not. - -All other variables have sensible defaults: +After deployment, set the following variables: | Variable | Default | Description | | :--- | :--- | :--- | -| `ADMIN_PASS` | — | Skip the install wizard and auto-initialize the admin | -| `DB_FORMAT` | `map` | Storage format: `map` (whole JSON) / `key` (per-key) / `sql` (relational, Go-compatible) | -| `DB_DRIVER` | `auto` | Storage driver: `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql`; an explicit driver is never replaced automatically | -| `DB_CIPHER` | `none` | At-rest cipher for sensitive fields (no encryption by default), e.g. `aes-256-gcm` | -| `ALLOW_URLS` | — | Comma-separated CORS allowlist (same-origin only when unset) | -| `MAX_UPLOAD` / `MAX_UPPART` | 26214400 / 16777216 | Max size (bytes) for a whole upload / a multipart chunk | -| `ASSET_URLS` | — | Frontend asset CDN base URL, supports the `$version` placeholder | - -Directory listings and download links are always fetched from the drive — there is no cross-request cache (no `CACHE_*` variables), so external changes are visible immediately. - -For the full variable list (field encryption `DB_CIPHER`, `cfkv` / MySQL settings, …) and per-platform deployment steps, see the documentation site: +| `JWT_SECRET` | (none) | JWT signing key (≥16 chars) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (per key) / `sql` (relational) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | At-rest cipher for sensitive fields, e.g. `aes-256-gcm` | -- [Environment Variables](https://doc.oplist.org/ecosystem/official_worker/guide_env) -- [Deployment](https://doc.oplist.org/ecosystem/official_worker/guide) -- [FAQ](https://doc.oplist.org/ecosystem/official_worker/about) +For the full variable list, see [Configuration variables](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Support diff --git a/readmes/README_es.md b/readmes/README_es.md index 86cd1806..3dcf6f06 100644 --- a/readmes/README_es.md +++ b/readmes/README_es.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Documentación](https://doc.oplist.org) · 🌏 [Documentación (China continental)](https://doc.oplist.org.cn) · ⚖️ [Términos de uso](https://doc.oplist.org/terms) · 🔒 [Política de privacidad](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Haz clic en el botón de abajo para desplegar este proyecto en la plataforma cor
-| EdgeOne Makers · Internacional | EdgeOne Makers · China | Cloudflare Workers · Global | -| :---: | :---: | :---: | -| [![Desplegar en EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Desplegar en EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · Internacional | EdgeOne Makers · China | Cloudflare Workers · Global | Alibaba Cloud ESA · China | +| :---: | :---: | :---: | :---: | +| [![Desplegar en EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Desplegar en EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Desplegar en Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Si Cloudflare muestra `no se puede obtener el contenido del repositorio`, [bifurca](https://github.com/OpenListTeam/OpenList-Worker/fork) este proyecto primero y luego despliega conectándote al repositorio de Github -> - Durante el despliegue, rellena `JWT_SECRET` (cadena aleatoria de 16+ caracteres, por ejemplo `openssl rand -hex 32`): firma los JWT y cifra datos; si no se define, se genera y persiste durante la inicialización +> - Para Alibaba Cloud ESA, importa este repositorio desde **Computación en el borde e IA → Funciones y Pages** en la consola +> - Configura `JWT_SECRET` durante el despliegue (≥16 caracteres) para firmar JWT y cifrar datos > - El almacenamiento usa automáticamente el backend persistente de la plataforma (`DB_DRIVER=auto`) — sin configuración adicional; para el resto de variables consulta [Variables de entorno](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - Tras el despliegue, la primera visita abre el **asistente de instalación**: define la cuenta y la contraseña del administrador en el navegador para completar la inicialización (define `ADMIN_PASS` para omitirlo) @@ -69,32 +70,23 @@ Además de los almacenamientos reales anteriores, también se proporcionan contr ### Capacidades principales -- **Navegación de archivos**: navegación unificada del árbol de directorios, con vista previa en línea de imágenes, videos, audio, documentos, código, archivos y más. -- **Subida y descarga**: subida entre almacenamientos, descarga por lotes, transmisión y redirección de enlace directo. -- **Compartir archivos**: generar enlaces para compartir con caducidad, contraseña y control de permisos, admitiendo acceso anónimo y uso compartido de directorios. -- **Búsqueda de texto completo**: búsqueda rápida de archivos en almacenamientos indexados. -- **Tareas sin conexión**: cola de tareas en segundo plano que admite operaciones por lotes y procesamiento asíncrono. -- **Interfaces externas**: exponer el almacenamiento agregado mediante el protocolo WebDAV o compatible con S3 para montarlo en herramientas de terceros. -- **Servicio MCP**: proporcionar un endpoint de Model Context Protocol que puede integrarse e invocarse desde asistentes de IA y otros clientes. - -### Gestión de accesos - -- **Permisos**: control de acceso basado en roles (RBAC), que admite grupos de usuarios, permisos de lectura/escritura a nivel de directorio y cuotas. -- **Autenticación**: contraseñas de cuenta integradas con verificación TOTP, inicio de sesión WebAuthn/FIDO, inicio de sesión único SSO y autenticación de directorio LDAP. -- **Endurecimiento de seguridad**: sesiones JWT, protección CSRF, protección contra clickjacking (X-Frame-Options), Content Security Policy (CSP). -- **Comprobaciones de salud**: proporcionar una sonda de vida `/health` y una sonda de preparación `/healthz` para monitorización y alertas. - -### Despliegue en plataformas - -- **Plataformas de ejecución**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless y entornos de contenedores Node.js. -- **Almacenamiento de datos**: Cloudflare D1 (SQLite) como principal, también compatible con MySQL, MariaDB, PostgreSQL, SQL Server. -- **Caché persistente**: Cloudflare KV / EdgeOne Blob (opcional) para persistencia de configuración y caché. -- **Despliegue en un clic**: admite botones de despliegue en un clic + inicialización en EdgeOne, Cloudflare Workers y otras plataformas. +- **Gestión de archivos**: navegación unificada del árbol de directorios con vista previa en línea de imágenes, vídeos, audio, documentos, código y archivos; subida entre almacenamientos, descarga por lotes, streaming y redirección de enlace directo; enlaces compartidos con caducidad, contraseña y control de permisos. +- **Búsqueda y tareas**: búsqueda de texto completo en almacenamientos indexados; cola de tareas en segundo plano para operaciones por lotes y procesamiento asíncrono. +- **Interfaces externas**: WebDAV, protocolo compatible con S3 y endpoint MCP para asistentes de IA. +- **Acceso y autenticación**: RBAC (grupos de usuarios, permisos de lectura/escritura por directorio, cuotas), contraseñas de cuenta + TOTP / WebAuthn / SSO / LDAP. +- **Seguridad y operación**: sesiones JWT, protección CSRF, clickjacking (X-Frame-Options) y CSP, sondas `/health` y `/healthz`. +- **Deduplicación de solicitudes concurrentes**: las solicitudes idénticas de listado de directorios / metadatos llegan al disco upstream una sola vez; las demás reutilizan el resultado, reduciendo límites de tasa y consumo de CPU. +- **Plataformas de ejecución**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda y contenedores Node.js. +- **Almacenamiento de datos**: Cloudflare D1 (SQLite) como principal, además de KV / Blob / Durable Objects y MySQL / MariaDB externos. +- **Despliegue en un clic**: botones de despliegue de EdgeOne, Cloudflare Workers y Alibaba Cloud ESA + asistente de instalación. --- ## Despliegue manual +
+Ver requisitos, desarrollo local y despliegue en producción + ### Requisitos previos - Node.js 18+ (se recomienda pnpm) @@ -106,7 +98,7 @@ Además de los almacenamientos reales anteriores, también se proporcionan contr # 1. Instalar dependencias pnpm install -# 2. Configurar wrangler.toml (rellenar JWT_SECRET, enlaces KV/D1) +# 2. Configurar los enlaces y variables en wrangler.jsonc según sea necesario (JWT_SECRET ver .dev.vars.example) # 3. Iniciar el servidor de desarrollo (obtiene automáticamente el frontend oficial y ejecuta el Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Despliegue en producción ```bash -# Despliegue en un clic: asegurar que el namespace KV exista → obtener el frontend oficial → desplegar en Cloudflare Workers +# Despliegue en un clic: compilar el frontend → desplegar en Cloudflare Workers (los enlaces KV los aprovisiona wrangler automáticamente) pnpm run deploy -# O desplegar directamente el Worker (omitir la comprobación de KV y la compilación del frontend) +# O desplegar directamente el Worker (omitir la compilación del frontend) pnpm run deploy:worker ``` +
+ --- ## Stack tecnológico ### Backend -- **Entorno de ejecución**: Cloudflare Workers (Edge Computing) +- **Entorno de ejecución**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Framework web**: Hono.js -- **Base de datos**: Cloudflare D1 (SQLite) / compatible con MySQL, MariaDB, PostgreSQL, SQL Server -- **Caché**: Cloudflare KV (opcional) +- **Base de datos**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects o MySQL / MariaDB externo - **Lenguaje**: TypeScript - **Herramientas de build**: Wrangler, esbuild ### Frontend -- **Framework**: React 19 + TypeScript -- **Bibliotecas UI**: Ant Design / Material-UI +- **Framework**: SolidJS + TypeScript (oficial [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **Biblioteca UI**: Hope UI - **Herramienta de build**: Vite --- +## Configuración del despliegue + +Tras el despliegue, define las siguientes variables: + +| Variable | Valor predeterminado | Descripción | +| :--- | :--- | :--- | +| `JWT_SECRET` | (ninguno) | Clave de firma JWT (≥16 caracteres) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (por clave) / `sql` (relacional) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Cifrado en reposo de campos sensibles, por ejemplo `aes-256-gcm` | + +Para la lista completa de variables, consulta [Variables de configuración](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Soporte diff --git a/readmes/README_fr.md b/readmes/README_fr.md index b3766b45..b7c7a2a0 100644 --- a/readmes/README_fr.md +++ b/readmes/README_fr.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Documentation](https://doc.oplist.org) · 🌏 [Documentation (Chine continentale)](https://doc.oplist.org.cn) · ⚖️ [Conditions d'utilisation](https://doc.oplist.org/terms) · 🔒 [Politique de confidentialité](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Cliquez sur le bouton ci-dessous pour déployer ce projet sur la plateforme corr
-| EdgeOne Makers · International | EdgeOne Makers · Chine | Cloudflare Workers · Global | -| :---: | :---: | :---: | -| [![Déployer sur EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Déployer sur EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · International | EdgeOne Makers · Chine | Cloudflare Workers · Global | Alibaba Cloud ESA · Chine | +| :---: | :---: | :---: | :---: | +| [![Déployer sur EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Déployer sur EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Déployer sur Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Si Cloudflare affiche `impossible de récupérer le contenu du dépôt`, [forkez](https://github.com/OpenListTeam/OpenList-Worker/fork) d'abord ce projet puis déployez en vous connectant au dépôt Github -> - Lors du déploiement, renseignez `JWT_SECRET` (chaîne aléatoire de 16+ caractères, par ex. `openssl rand -hex 32`) : elle signe les JWT et chiffre les données ; si elle n'est pas définie, elle est générée et persistée pendant l'initialisation +> - Pour Alibaba Cloud ESA, importez ce dépôt depuis **Edge Computing et IA → Fonctions et Pages** dans la console +> - Renseignez `JWT_SECRET` lors du déploiement (≥16 caractères) pour la signature JWT et le chiffrement > - Le stockage utilise automatiquement le backend persistant de la plateforme (`DB_DRIVER=auto`) — aucune configuration requise ; pour les autres variables, voir [Variables d'environnement](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - Après le déploiement, la première visite ouvre l'**assistant d'installation** : définissez le compte et le mot de passe administrateur dans le navigateur pour terminer l'initialisation (définissez `ADMIN_PASS` pour l'ignorer) @@ -69,32 +70,23 @@ En plus des stockages réels ci-dessus, des pilotes virtuels/fonctionnels tels q ### Capacités principales -- **Navigation de fichiers** : navigation unifiée dans l'arborescence, avec aperçu en ligne d'images, vidéos, audio, documents, code, archives et plus. -- **Téléversement et téléchargement** : téléversement inter-stockage, téléchargement par lot, streaming et redirection de lien direct. -- **Partage de fichiers** : génération de liens de partage avec expiration, mot de passe et contrôle des permissions, prenant en charge l'accès anonyme et le partage de répertoires. -- **Recherche plein texte** : recherche rapide de fichiers dans les stockages indexés. -- **Tâches hors ligne** : file d'attente de tâches en arrière-plan prenant en charge les opérations par lot et le traitement asynchrone. -- **Interfaces externes** : exposer le stockage agrégé via le protocole WebDAV ou compatible S3 pour le montage dans des outils tiers. -- **Service MCP** : fournir un point de terminaison Model Context Protocol pouvant être intégré et appelé par des assistants IA et d'autres clients. - -### Gestion des accès - -- **Permissions** : contrôle d'accès basé sur les rôles (RBAC), prenant en charge les groupes d'utilisateurs, les permissions de lecture/écriture au niveau des répertoires et les quotas. -- **Authentification** : mots de passe de compte intégrés avec vérification TOTP, connexion WebAuthn/FIDO, authentification unique SSO et authentification d'annuaire LDAP. -- **Renforcement de la sécurité** : sessions JWT, protection CSRF, protection contre le détournement de clic (X-Frame-Options), Content Security Policy (CSP). -- **Contrôles de santé** : fournir une sonde de vivacité `/health` et une sonde de préparation `/healthz` pour la surveillance et l'alerte. - -### Déploiement de plateforme - -- **Plateformes d'exécution** : Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless et environnements de conteneurs Node.js. -- **Stockage de données** : Cloudflare D1 (SQLite) comme principal, prenant également en charge MySQL, MariaDB, PostgreSQL, SQL Server. -- **Cache persistant** : Cloudflare KV / EdgeOne Blob (optionnel) pour la persistance de la configuration et la mise en cache. -- **Déploiement en un clic** : prise en charge des boutons de déploiement en un clic + initialisation sur EdgeOne, Cloudflare Workers et d'autres plateformes. +- **Gestion des fichiers** : navigation unifiée dans l'arborescence avec aperçu en ligne des images, vidéos, audio, documents, code et archives ; téléversement inter-stockage, téléchargement par lot, streaming et redirection de lien direct ; liens de partage avec expiration, mot de passe et contrôle des permissions. +- **Recherche et tâches** : recherche plein texte dans les stockages indexés ; file de tâches en arrière-plan pour les opérations par lot et le traitement asynchrone. +- **Interfaces externes** : WebDAV, protocole compatible S3 et point de terminaison MCP pour les assistants IA. +- **Accès et authentification** : RBAC (groupes d'utilisateurs, permissions de lecture/écriture par répertoire, quotas), mots de passe de compte + TOTP / WebAuthn / SSO / LDAP. +- **Sécurité et exploitation** : sessions JWT, protections CSRF, clickjacking (X-Frame-Options) et CSP, sondes `/health` et `/healthz`. +- **Déduplication des requêtes concurrentes** : les requêtes identiques de liste de répertoire / métadonnées n'atteignent le disque upstream qu'une fois ; les autres réutilisent le résultat, réduisant les limitations de débit et la charge CPU. +- **Plateformes d'exécution** : Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda et conteneurs Node.js. +- **Stockage de données** : Cloudflare D1 (SQLite) comme principal, ainsi que KV / Blob / Durable Objects et MySQL / MariaDB externes. +- **Déploiement en un clic** : boutons de déploiement EdgeOne, Cloudflare Workers et Alibaba Cloud ESA + assistant d'installation. --- ## Déploiement manuel +
+Afficher les prérequis, le développement local et le déploiement en production + ### Prérequis - Node.js 18+ (pnpm recommandé) @@ -106,7 +98,7 @@ En plus des stockages réels ci-dessus, des pilotes virtuels/fonctionnels tels q # 1. Installer les dépendances pnpm install -# 2. Configurer wrangler.toml (renseigner JWT_SECRET, les liaisons KV/D1) +# 2. Configurer les liaisons et variables dans wrangler.jsonc selon les besoins (JWT_SECRET voir .dev.vars.example) # 3. Démarrer le serveur de développement (récupère automatiquement le frontend officiel et exécute le Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Déploiement en production ```bash -# Déploiement en un clic : vérifier que le namespace KV existe → récupérer le frontend officiel → déployer sur Cloudflare Workers +# Déploiement en un clic : compiler le frontend → déployer sur Cloudflare Workers (les liaisons KV sont provisionnées automatiquement par wrangler) pnpm run deploy -# Ou déployer directement le Worker (ignorer la vérification KV et la compilation du frontend) +# Ou déployer directement le Worker (ignorer la compilation du frontend) pnpm run deploy:worker ``` +
+ --- ## Stack technique ### Backend -- **Environnement d'exécution** : Cloudflare Workers (Edge Computing) +- **Environnement d'exécution** : Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Framework web** : Hono.js -- **Base de données** : Cloudflare D1 (SQLite) / prend en charge MySQL, MariaDB, PostgreSQL, SQL Server -- **Cache** : Cloudflare KV (optionnel) +- **Base de données** : Cloudflare D1 (SQLite) / KV / Blob / Durable Objects ou MySQL / MariaDB externe - **Langage** : TypeScript - **Outils de build** : Wrangler, esbuild ### Frontend -- **Framework** : React 19 + TypeScript -- **Bibliothèques UI** : Ant Design / Material-UI +- **Framework** : SolidJS + TypeScript (officiel [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **Bibliothèque UI** : Hope UI - **Outil de build** : Vite --- +## Configuration du déploiement + +Après le déploiement, définissez les variables suivantes : + +| Variable | Défaut | Description | +| :--- | :--- | :--- | +| `JWT_SECRET` | (aucun) | Clé de signature JWT (≥16 caractères) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (par clé) / `sql` (relationnel) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Chiffrement au repos des champs sensibles, par ex. `aes-256-gcm` | + +Pour la liste complète des variables, consultez [Variables de configuration](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Support diff --git a/readmes/README_hi.md b/readmes/README_hi.md index 4dc2d743..2508eb6c 100644 --- a/readmes/README_hi.md +++ b/readmes/README_hi.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [उपयोग दस्तावेज़](https://doc.oplist.org) · 🌏 [उपयोग दस्तावेज़(मुख्यभूमि चीन)](https://doc.oplist.org.cn) · ⚖️ [उपयोग की शर्तें](https://doc.oplist.org/terms) · 🔒 [गोपनीयता नीति](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · अंतर्राष्ट्रीय | EdgeOne Makers · चीन | Cloudflare Workers · वैश्विक | -| :---: | :---: | :---: | -| [![EdgeOne पर तैनात करें](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne पर तैनात करें](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · अंतर्राष्ट्रीय | EdgeOne Makers · चीन | Cloudflare Workers · वैश्विक | Alibaba Cloud ESA · चीन | +| :---: | :---: | :---: | :---: | +| [![EdgeOne पर तैनात करें](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne पर तैनात करें](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Alibaba Cloud ESA पर तैनात करें](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - यदि Cloudflare `रिपॉजिटरी सामग्री प्राप्त नहीं कर सकता` दिखाता है, तो पहले इस परियोजना को [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) करें, फिर Github रिपॉजिटरी से कनेक्ट करके तैनात करें -> - तैनाती के समय `JWT_SECRET` भरें (16+ वर्णों की यादृच्छिक स्ट्रिंग, उदा. `openssl rand -hex 32`): यह JWT पर हस्ताक्षर और डेटा एन्क्रिप्शन के लिए उपयोग होता है; न दिया जाए तो इनिशियलाइज़ेशन के दौरान स्वतः बनाकर सहेजा जाता है +> - Alibaba Cloud ESA के लिए, कंसोल में **एज कंप्यूटिंग और AI → फ़ंक्शन और Pages** से इस रिपॉजिटरी को आयात करें +> - तैनाती के समय `JWT_SECRET` सेट करें (≥16 वर्ण) — JWT हस्ताक्षर और एन्क्रिप्शन के लिए > - डेटा संग्रहण स्वतः प्लेटफ़ॉर्म के स्थायी बैकएंड का चयन करता है (`DB_DRIVER=auto`) — कोई कॉन्फ़िगरेशन आवश्यक नहीं; शेष चरों के लिए [पर्यावरण चर](https://doc.oplist.org/ecosystem/official_worker/guide_env) देखें > - तैनाती के बाद पहली विज़िट पर **इंस्टॉल विज़ार्ड** खुलता है: इनिशियलाइज़ेशन पूरा करने के लिए ब्राउज़र में एडमिन खाता और पासवर्ड सेट करें (`ADMIN_PASS` सेट करने पर विज़ार्ड छूट जाता है) @@ -69,32 +70,23 @@ OpenList-Worker आधिकारिक [OpenListTeam/OpenList](https://github. ### मुख्य क्षमताएँ -- **फ़ाइल ब्राउज़िंग**: एकीकृत निर्देशिका ट्री ब्राउज़िंग, जिसमें छवियों, वीडियो, ऑडियो, दस्तावेज़ों, कोड, संग्रहों आदि का ऑनलाइन पूर्वावलोकन शामिल है। -- **अपलोड और डाउनलोड**: क्रॉस-स्टोरेज अपलोड, बैच डाउनलोड, स्ट्रीमिंग और डायरेक्ट-लिंक रीडायरेक्ट। -- **फ़ाइल साझाकरण**: समाप्ति, पासवर्ड और अनुमति नियंत्रण के साथ साझाकरण लिंक बनाना, अनाम पहुंच और निर्देशिका साझाकरण का समर्थन करता है। -- **पूर्ण-पाठ खोज**: अनुक्रमित स्टोरेज में फ़ाइलों की त्वरित खोज। -- **ऑफ़लाइन कार्य**: बैकग्राउंड कार्य कतार, बैच संचालन और अतुल्यकालिक प्रसंस्करण का समर्थन करती है। -- **बाहरी इंटरफ़ेस**: एग्रीगेटेड स्टोरेज को WebDAV या S3-संगत प्रोटोकॉल के माध्यम से उजागर करना, ताकि तृतीय-पक्ष टूल में माउंट किया जा सके। -- **MCP सेवा**: Model Context Protocol एंडपॉइंट प्रदान करता है, जिसे AI सहायक और अन्य क्लाइंट एकीकृत और कॉल कर सकते हैं। - -### पहुंच प्रबंधन - -- **अनुमतियाँ**: भूमिका-आधारित पहुंच नियंत्रण(RBAC), उपयोगकर्ता समूहों, निर्देशिका-स्तरीय पढ़ने/लिखने की अनुमतियों और कोटा का समर्थन करता है। -- **प्रमाणीकरण**: अंतर्निहित खाता पासवर्ड, TOTP सत्यापन, WebAuthn/FIDO लॉगिन, SSO सिंगल साइन-ऑन और LDAP निर्देशिका प्रमाणीकरण का समर्थन करता है। -- **सुरक्षा सुदृढ़ीकरण**: JWT सत्र, CSRF सुरक्षा, क्लिकजैकिंग सुरक्षा(X-Frame-Options), कंटेंट सिक्योरिटी पॉलिसी(CSP). -- **स्वास्थ्य जाँच**: निगरानी और अलर्ट के लिए `/health` लाइवनेस प्रोब और `/healthz` रेडीनेस प्रोब प्रदान करता है। - -### प्लेटफ़ॉर्म तैनाती - -- **रनटाइम प्लेटफ़ॉर्म**: Cloudflare Workers、Tencent Cloud EdgeOne Makers、Vercel、Serverless और Node.js कंटेनर वातावरण। -- **डेटा स्टोरेज**: Cloudflare D1(SQLite)प्राथमिक, साथ ही MySQL、MariaDB、PostgreSQL、SQL Server का समर्थन करता है। -- **स्थायी कैश**: Cloudflare KV / EdgeOne Blob(वैकल्पिक), कॉन्फ़िगरेशन स्थायित्व और कैशिंग के लिए। -- **एक-क्लिक तैनाती**: EdgeOne、Cloudflare Workers और अन्य प्लेटफ़ॉर्म पर एक-क्लिक तैनाती बटन + प्रारंभिकरण का समर्थन करता है। +- **फ़ाइल प्रबंधन**:एकीकृत निर्देशिका ट्री ब्राउज़िंग, छवियों / वीडियो / ऑडियो / दस्तावेज़ों / कोड / संग्रहों का ऑनलाइन पूर्वावलोकन; क्रॉस-स्टोरेज अपलोड, बैच डाउनलोड, स्ट्रीमिंग और डायरेक्ट-लिंक रीडायरेक्ट; समाप्ति, पासवर्ड और अनुमति नियंत्रण के साथ साझाकरण लिंक। +- **खोज और कार्य**:अनुक्रमित स्टोरेज में पूर्ण-पाठ खोज; बैच और अतुल्यकालिक प्रसंस्करण के लिए बैकग्राउंड कार्य कतार। +- **बाहरी इंटरफ़ेस**:WebDAV、S3-संगत प्रोटोकॉल, और AI सहायकों के लिए MCP एंडपॉइंट। +- **पहुंच और प्रमाणीकरण**:RBAC(उपयोगकर्ता समूह, निर्देशिका-स्तरीय पढ़ने/लिखने की अनुमतियाँ, कोटा), खाता पासवर्ड + TOTP / WebAuthn / SSO / LDAP। +- **सुरक्षा और संचालन**:JWT सत्र, CSRF, क्लिकजैकिंग(X-Frame-Options)और CSP सुरक्षा, `/health` और `/healthz` प्रोब। +- **समवर्ती अनुरोध डिडुप्लिकेशन**:समान निर्देशिका सूची / मेटाडेटा अनुरोध अपस्ट्रीम ड्राइव पर केवल एक बार जाते हैं; बाकी वही परिणाम पुनः उपयोग करते हैं, जिससे रेट लिमिट और CPU खपत कम होती है। +- **रनटाइम प्लेटफ़ॉर्म**:Cloudflare Workers、Tencent Cloud EdgeOne Makers、Alibaba Cloud ESA、Vercel / AWS Lambda और Node.js कंटेनर। +- **डेटा स्टोरेज**:Cloudflare D1(SQLite)प्राथमिक, साथ ही KV / Blob / Durable Objects और बाहरी MySQL / MariaDB। +- **एक-क्लिक तैनाती**:EdgeOne、Cloudflare Workers और Alibaba Cloud ESA तैनाती बटन + इंस्टॉल विज़ार्ड। --- ## मैन्युअल तैनाती +
+पूर्वापेक्षाएँ, स्थानीय विकास और उत्पादन तैनाती देखें + ### पूर्वापेक्षाएँ - Node.js 18+(pnpm अनुशंसित) @@ -106,7 +98,7 @@ OpenList-Worker आधिकारिक [OpenListTeam/OpenList](https://github. # 1. निर्भरताएँ स्थापित करें pnpm install -# 2. wrangler.toml कॉन्फ़िगर करें(JWT_SECRET, KV/D1 बाइंडिंग भरें) +# 2. आवश्यकतानुसार wrangler.jsonc में बाइंडिंग और चर कॉन्फ़िगर करें(JWT_SECRET के लिए .dev.vars.example देखें) # 3. विकास सर्वर शुरू करें(आधिकारिक फ्रंटएंड को स्वचालित रूप से प्राप्त करता है और Worker चलाता है) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### उत्पादन तैनाती ```bash -# एक-क्लिक तैनाती: सुनिश्चित करें कि KV namespace मौजूद है → आधिकारिक फ्रंटएंड प्राप्त करें → Cloudflare Workers पर तैनात करें +# एक-क्लिक तैनाती: फ्रंटएंड बिल्ड करें → Cloudflare Workers पर तैनात करें(KV बाइंडिंग wrangler द्वारा स्वतः प्रावधानित) pnpm run deploy -# या Worker को सीधे तैनात करें(KV जाँच और फ्रंटएंड बिल्ड छोड़ें) +# या Worker को सीधे तैनात करें(फ्रंटएंड बिल्ड छोड़ें) pnpm run deploy:worker ``` +
+ --- ## तकनीकी संरचना ### बैकएंड -- **रनटाइम वातावरण**: Cloudflare Workers(Edge Computing) +- **रनटाइम वातावरण**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA(Edge Computing) - **वेब फ्रेमवर्क**: Hono.js -- **डेटाबेस**: Cloudflare D1(SQLite)/ MySQL、MariaDB、PostgreSQL、SQL Server का समर्थन करता है -- **कैश**: Cloudflare KV(वैकल्पिक) +- **डेटाबेस**: Cloudflare D1(SQLite)/ KV / Blob / Durable Objects या बाहरी MySQL / MariaDB - **भाषा**: TypeScript - **बिल्ड टूल**: Wrangler、esbuild ### फ्रंटएंड -- **फ्रेमवर्क**: React 19 + TypeScript -- **UI लाइब्रेरी**: Ant Design / Material-UI +- **फ्रेमवर्क**: SolidJS + TypeScript(आधिकारिक [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI लाइब्रेरी**: Hope UI - **बिल्ड टूल**: Vite --- +## तैनाती कॉन्फ़िगरेशन + +तैनाती के बाद निम्न चर सेट करें: + +| चर | डिफ़ॉल्ट | विवरण | +| :--- | :--- | :--- | +| `JWT_SECRET` | (कोई नहीं) | JWT हस्ताक्षर कुंजी (≥16 वर्ण) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (प्रति कुंजी) / `sql` (संबंधपरक) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | संवेदनशील फ़ील्ड का एट-रेस्ट एन्क्रिप्शन, जैसे `aes-256-gcm` | + +पूरी चर सूची के लिए [कॉन्फ़िगरेशन चर](https://doc.oplist.org/ecosystem/official_worker/guide_env) देखें। ## सहायता diff --git a/readmes/README_it.md b/readmes/README_it.md index a1585e50..c36c3e04 100644 --- a/readmes/README_it.md +++ b/readmes/README_it.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Documentazione](https://doc.oplist.org) · 🌏 [Documentazione (Cina continentale)](https://doc.oplist.org.cn) · ⚖️ [Termini d'uso](https://doc.oplist.org/terms) · 🔒 [Informativa sulla privacy](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Clicca il pulsante qui sotto per distribuire questo progetto sulla piattaforma c
-| EdgeOne Makers · Internazionale | EdgeOne Makers · Cina | Cloudflare Workers · Globale | -| :---: | :---: | :---: | -| [![Deploy su EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy su EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · Internazionale | EdgeOne Makers · Cina | Cloudflare Workers · Globale | Alibaba Cloud ESA · Cina | +| :---: | :---: | :---: | :---: | +| [![Deploy su EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy su EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Deploy su Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Se Cloudflare mostra `impossibile recuperare il contenuto del repository`, [fai un fork](https://github.com/OpenListTeam/OpenList-Worker/fork) di questo progetto e poi distribuisci collegandoti al repository Github -> - Durante il deploy compila `JWT_SECRET` (stringa casuale di 16+ caratteri, ad es. `openssl rand -hex 32`): firma i JWT e cifra i dati; se non impostata, viene generata e salvata durante l'inizializzazione +> - Per Alibaba Cloud ESA, importa questo repository da **Edge Computing e AI → Funzioni e Pages** nella console +> - Imposta `JWT_SECRET` durante il deploy (≥16 caratteri) per firma JWT e cifratura > - L'archiviazione usa automaticamente il backend persistente della piattaforma (`DB_DRIVER=auto`) — nessuna configurazione necessaria; per le altre variabili vedi [Variabili d'ambiente](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - Dopo il deploy, la prima visita apre la **procedura guidata di installazione**: imposta account e password admin nel browser per completare l'inizializzazione (imposta `ADMIN_PASS` per saltarla) @@ -69,32 +70,23 @@ Oltre agli storage reali sopra indicati, sono forniti anche driver virtuali/funz ### Capacità principali -- **Navigazione dei file**: navigazione unificata dell'albero delle directory, con anteprima online di immagini, video, audio, documenti, codice, archivi e altro. -- **Caricamento e download**: caricamento tra storage, download in batch, streaming e reindirizzamento con link diretto. -- **Condivisione file**: generazione di link di condivisione con scadenza, password e controllo dei permessi, con supporto per accesso anonimo e condivisione di directory. -- **Ricerca full-text**: ricerca rapida di file negli storage indicizzati. -- **Attività offline**: coda di attività in background con supporto per operazioni batch ed elaborazione asincrona. -- **Interfacce esterne**: esporre lo storage aggregato tramite protocollo WebDAV o compatibile con S3 per il montaggio in strumenti di terze parti. -- **Servizio MCP**: fornire un endpoint Model Context Protocol integrabile e richiamabile da assistenti IA e altri client. - -### Gestione degli accessi - -- **Permessi**: controllo degli accessi basato sui ruoli (RBAC), con supporto per gruppi di utenti, permessi di lettura/scrittura a livello di directory e quote. -- **Autenticazione**: password degli account integrate con verifica TOTP, accesso WebAuthn/FIDO, single sign-on SSO e autenticazione di directory LDAP. -- **Rafforzamento della sicurezza**: sessioni JWT, protezione CSRF, protezione dal clickjacking (X-Frame-Options), Content Security Policy (CSP). -- **Controlli di salute**: fornire una sonda di vitalità `/health` e una sonda di prontezza `/healthz` per monitoraggio e avvisi. - -### Deploy su piattaforme - -- **Piattaforme di esecuzione**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless e ambienti container Node.js. -- **Archiviazione dati**: Cloudflare D1 (SQLite) come principale, con supporto anche per MySQL, MariaDB, PostgreSQL, SQL Server. -- **Cache persistente**: Cloudflare KV / EdgeOne Blob (opzionale) per persistenza della configurazione e caching. -- **Deploy con un clic**: supporto per pulsanti di deploy con un clic + inizializzazione su EdgeOne, Cloudflare Workers e altre piattaforme. +- **Gestione file**: navigazione unificata dell'albero delle directory con anteprima online di immagini, video, audio, documenti, codice e archivi; caricamento tra storage, download in batch, streaming e reindirizzamento con link diretto; link di condivisione con scadenza, password e controllo dei permessi. +- **Ricerca e attività**: ricerca full-text negli storage indicizzati; coda di attività in background per operazioni batch ed elaborazione asincrona. +- **Interfacce esterne**: WebDAV, protocollo compatibile S3 ed endpoint MCP per assistenti IA. +- **Accesso e autenticazione**: RBAC (gruppi di utenti, permessi di lettura/scrittura a livello di directory, quote), password degli account + TOTP / WebAuthn / SSO / LDAP. +- **Sicurezza e operatività**: sessioni JWT, protezione CSRF, clickjacking (X-Frame-Options) e CSP, sonde `/health` e `/healthz`. +- **Deduplicazione delle richieste concorrenti**: richieste identiche di elenco directory / metadati raggiungono il disco upstream una sola volta; le altre riutilizzano il risultato, riducendo limiti di frequenza e consumo CPU. +- **Piattaforme di esecuzione**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda e container Node.js. +- **Archiviazione dati**: Cloudflare D1 (SQLite) come principale, oltre a KV / Blob / Durable Objects e MySQL / MariaDB esterni. +- **Deploy con un clic**: pulsanti di deploy per EdgeOne, Cloudflare Workers e Alibaba Cloud ESA + procedura guidata di installazione. --- ## Deploy manuale +
+Mostra prerequisiti, sviluppo locale e deploy in produzione + ### Prerequisiti - Node.js 18+ (pnpm consigliato) @@ -106,7 +98,7 @@ Oltre agli storage reali sopra indicati, sono forniti anche driver virtuali/funz # 1. Installa le dipendenze pnpm install -# 2. Configura wrangler.toml (inserisci JWT_SECRET, binding KV/D1) +# 2. Configura binding e variabili in wrangler.jsonc secondo necessità (JWT_SECRET vedi .dev.vars.example) # 3. Avvia il server di sviluppo (ottiene automaticamente il frontend ufficiale ed esegue il Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Deploy in produzione ```bash -# Deploy con un clic: assicurati che il namespace KV esista → ottieni il frontend ufficiale → distribuisci su Cloudflare Workers +# Deploy con un clic: compila il frontend → distribuisci su Cloudflare Workers (i binding KV vengono forniti automaticamente da wrangler) pnpm run deploy -# Oppure distribuisci direttamente il Worker (salta la verifica KV e la build del frontend) +# Oppure distribuisci direttamente il Worker (salta la build del frontend) pnpm run deploy:worker ``` +
+ --- ## Stack tecnologico ### Backend -- **Ambiente di esecuzione**: Cloudflare Workers (Edge Computing) +- **Ambiente di esecuzione**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Framework web**: Hono.js -- **Database**: Cloudflare D1 (SQLite) / supporta MySQL, MariaDB, PostgreSQL, SQL Server -- **Cache**: Cloudflare KV (opzionale) +- **Database**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects o MySQL / MariaDB esterno - **Linguaggio**: TypeScript - **Strumenti di build**: Wrangler, esbuild ### Frontend -- **Framework**: React 19 + TypeScript -- **Librerie UI**: Ant Design / Material-UI +- **Framework**: SolidJS + TypeScript (ufficiale [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **Libreria UI**: Hope UI - **Strumento di build**: Vite --- +## Configurazione del deploy + +Dopo il deploy, imposta le seguenti variabili: + +| Variabile | Predefinito | Descrizione | +| :--- | :--- | :--- | +| `JWT_SECRET` | (nessuno) | Chiave di firma JWT (≥16 caratteri) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (per chiave) / `sql` (relazionale) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Cifratura at-rest dei campi sensibili, ad es. `aes-256-gcm` | + +Per l'elenco completo delle variabili, consulta [Variabili di configurazione](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Supporto diff --git a/readmes/README_ja.md b/readmes/README_ja.md index 8c7095ae..1ac5f6fa 100644 --- a/readmes/README_ja.md +++ b/readmes/README_ja.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [使用ドキュメント](https://doc.oplist.org) · 🌏 [使用ドキュメント(中国本土)](https://doc.oplist.org.cn) · ⚖️ [利用規約](https://doc.oplist.org/terms) · 🔒 [プライバシーポリシー](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · 国際版 | EdgeOne Makers · 中国版 | Cloudflare Workers · グローバル | -| :---: | :---: | :---: | -| [![EdgeOne にデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne にデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · 国際版 | EdgeOne Makers · 中国版 | Cloudflare Workers · グローバル | Alibaba Cloud ESA · 中国版 | +| :---: | :---: | :---: | :---: | +| [![EdgeOne にデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne にデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Alibaba Cloud ESA にデプロイ](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Cloudflare が `ストレージリポジトリの内容を取得できません` と表示する場合、まず本プロジェクトを [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) し、Github リポジトリへの接続からデプロイしてください -> - デプロイ時に `JWT_SECRET`(16 文字以上のランダム文字列、例:`openssl rand -hex 32`)を設定してください。JWT 署名とデータ暗号化に使われ、未設定の場合は初期化時に自動生成・永続化されます +> - Alibaba Cloud ESA の場合は、コンソールの **エッジコンピューティングと AI → 関数と Pages** から本リポジトリをインポートしてください +> - デプロイ時に `JWT_SECRET`(16 文字以上)を設定してください。JWT 署名と暗号化に使用されます > - データ保存はプラットフォーム標準の永続化バックエンドを自動選択します(`DB_DRIVER=auto`)——通常は設定不要です。その他の変数は [環境変数](https://doc.oplist.org/ecosystem/official_worker/guide_env) を参照してください > - デプロイ後の初回アクセスでは**インストールウィザード**が開きます。ブラウザで管理者アカウントとパスワードを設定して初期化してください(`ADMIN_PASS` を設定するとウィザードをスキップできます) @@ -69,32 +70,23 @@ OpenList-Worker は公式 [OpenListTeam/OpenList](https://github.com/OpenListTea ### 主な機能 -- **ファイル閲覧**:統一されたディレクトリツリー閲覧。画像、動画、音声、ドキュメント、コード、アーカイブなどのオンラインプレビューに対応。 -- **アップロード・ダウンロード**:ストレージ間のアップロード、一括ダウンロード、ストリーミング、ダイレクトリンクへのリダイレクト。 -- **ファイル共有**:有効期限・パスワード・権限制御付きの共有リンクを生成。匿名アクセスとディレクトリ共有に対応。 -- **全文検索**:インデックス済みストレージ内のファイルを高速検索。 -- **オフラインタスク**:バックグラウンドタスクキューにより一括操作と非同期処理に対応。 -- **外部インターフェース**:集約ストレージを WebDAV または S3 互換プロトコルで公開し、サードパーティツールへのマウントが可能。 -- **MCP サービス**:Model Context Protocol エンドポイントを提供し、AI アシスタントなどのクライアントから統合・呼び出し可能。 - -### 権限管理 - -- **権限管理**:ロールベースのアクセス制御(RBAC)。ユーザーグループ、ディレクトリ単位の読み書き権限、クォータに対応。 -- **認証方式**:組み込みのアカウントパスワードに加え、TOTP 検証、WebAuthn/FIDO ログイン、SSO シングルサインオン、LDAP ディレクトリ認証に対応。 -- **セキュリティ強化**:JWT セッション、CSRF 対策、クリックジャッキング対策(X-Frame-Options)、コンテンツセキュリティポリシー(CSP)。 -- **ヘルスチェック**:`/health` 生存プローブと `/healthz` 準備プローブを提供し、監視・アラートに利用可能。 - -### プラットフォームデプロイ - -- **実行プラットフォーム**:Cloudflare Workers、Tencent Cloud EdgeOne Makers、Vercel、Serverless、Node.js コンテナ環境。 -- **データストレージ**:Cloudflare D1(SQLite)を主とし、MySQL、MariaDB、PostgreSQL、SQL Server にも対応。 -- **永続キャッシュ**:Cloudflare KV / EdgeOne Blob(オプション)。設定の永続化とキャッシュに使用。 -- **ワンクリックデプロイ**:EdgeOne、Cloudflare Workers などのプラットフォームでワンクリックデプロイボタン+初期化に対応。 +- **ファイル管理**:統一されたディレクトリツリー閲覧と、画像 / 動画 / 音声 / ドキュメント / コード / アーカイブのオンラインプレビュー;ストレージ間アップロード、一括ダウンロード、ストリーミング、ダイレクトリンクへのリダイレクト;有効期限・パスワード・権限制御付きの共有リンク。 +- **検索とタスク**:インデックス済みストレージの全文検索;一括操作と非同期処理のためのバックグラウンドタスクキュー。 +- **外部インターフェース**:WebDAV、S3 互換プロトコル、AI アシスタント向け MCP エンドポイント。 +- **アクセスと認証**:RBAC(ユーザーグループ、ディレクトリ単位の読み書き権限、クォータ)、アカウントパスワード + TOTP / WebAuthn / SSO / LDAP。 +- **セキュリティと運用**:JWT セッション、CSRF、クリックジャッキング(X-Frame-Options)、CSP 保護、`/health`・`/healthz` プローブ。 +- **同時リクエストの重複排除**:同一瞬間の同一ディレクトリ一覧 / メタデータ要求は上流ドライブへ一度だけ送信され、他の要求は同じ結果を再利用するため、レート制限と CPU 消費を抑えられます。 +- **実行プラットフォーム**:Cloudflare Workers、Tencent Cloud EdgeOne Makers、Alibaba Cloud ESA、Vercel / AWS Lambda、Node.js コンテナ。 +- **データストレージ**:Cloudflare D1(SQLite)を主とし、KV / Blob / Durable Objects と外部 MySQL / MariaDB にも対応。 +- **ワンクリックデプロイ**:EdgeOne、Cloudflare Workers、Alibaba Cloud ESA のデプロイボタン + インストールウィザード。 --- ## 手動デプロイ +
+前提条件・ローカル開発・本番デプロイを表示 + ### 前提条件 - Node.js 18+(pnpm 推奨) @@ -106,7 +98,7 @@ OpenList-Worker は公式 [OpenListTeam/OpenList](https://github.com/OpenListTea # 1. 依存関係をインストール pnpm install -# 2. wrangler.toml を設定(JWT_SECRET、KV/D1 バインディングを入力) +# 2. 必要に応じて wrangler.jsonc のバインディングと変数を設定(JWT_SECRET は .dev.vars.example を参照) # 3. 開発サーバーを起動(公式フロントエンドを自動取得して Worker を実行) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### 本番デプロイ ```bash -# ワンクリックデプロイ:KV namespace の存在確認 → 公式フロントエンド取得 → Cloudflare Workers へデプロイ +# ワンクリックデプロイ:フロントエンドをビルド → Cloudflare Workers へデプロイ(KV バインディングは wrangler が自動プロビジョニング) pnpm run deploy -# または Worker を直接デプロイ(KV チェックとフロントエンドビルドをスキップ) +# または Worker を直接デプロイ(フロントエンドのビルドをスキップ) pnpm run deploy:worker ``` +
+ --- ## 技術構成 ### バックエンド -- **実行環境**:Cloudflare Workers(Edge Computing) +- **実行環境**:Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA(Edge Computing) - **Web フレームワーク**:Hono.js -- **データベース**:Cloudflare D1(SQLite)/ MySQL、MariaDB、PostgreSQL、SQL Server に対応 -- **キャッシュ**:Cloudflare KV(オプション) +- **データベース**:Cloudflare D1(SQLite)/ KV / Blob / Durable Objects または外部 MySQL / MariaDB - **言語**:TypeScript - **ビルドツール**:Wrangler、esbuild ### フロントエンド -- **フレームワーク**:React 19 + TypeScript -- **UI ライブラリ**:Ant Design / Material-UI +- **フレームワーク**:SolidJS + TypeScript(公式 [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI ライブラリ**:Hope UI - **ビルドツール**:Vite --- +## デプロイ設定 + +デプロイ後に以下の変数を設定してください: + +| 変数 | デフォルト | 説明 | +| :--- | :--- | :--- | +| `JWT_SECRET` | (なし) | JWT 署名キー(≥16 文字) | +| `DB_FORMAT` | `map` | `map`(JSON)/ `key`(キー単位)/ `sql`(リレーショナル) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | 機密フィールドの保存時暗号化、例:`aes-256-gcm` | + +完全な変数一覧は [設定変数](https://doc.oplist.org/ecosystem/official_worker/guide_env) を参照してください。 ## サポート diff --git a/readmes/README_ko.md b/readmes/README_ko.md index 38b82e60..664edf57 100644 --- a/readmes/README_ko.md +++ b/readmes/README_ko.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [사용 문서](https://doc.oplist.org) · 🌏 [사용 문서(중국 본토)](https://doc.oplist.org.cn) · ⚖️ [이용 약관](https://doc.oplist.org/terms) · 🔒 [개인정보 처리방침](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · 국제 | EdgeOne Makers · 중국 | Cloudflare Workers · 글로벌 | -| :---: | :---: | :---: | -| [![EdgeOne에 배포](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne에 배포](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · 국제 | EdgeOne Makers · 중국 | Cloudflare Workers · 글로벌 | Alibaba Cloud ESA · 중국 | +| :---: | :---: | :---: | :---: | +| [![EdgeOne에 배포](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![EdgeOne에 배포](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Alibaba Cloud ESA에 배포](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Cloudflare에서 `저장소 콘텐츠를 가져올 수 없습니다`라고 표시되면, 먼저 이 프로젝트를 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork)한 다음 Github 저장소 연결 기능으로 배포하세요 -> - 배포 시 `JWT_SECRET`(16자 이상의 임의 문자열, 예: `openssl rand -hex 32`)을 입력하세요. JWT 서명과 데이터 암호화에 사용되며, 미설정 시 초기화 단계에서 자동 생성·영구 저장됩니다 +> - Alibaba Cloud ESA의 경우 콘솔의 **엣지 컴퓨팅 및 AI → 함수 및 Pages**에서 이 저장소를 가져오세요 +> - 배포 시 `JWT_SECRET`(≥16자)을 설정하세요. JWT 서명과 암호화에 사용됩니다 > - 데이터 저장은 플랫폼 내장 영구 저장소를 자동으로 선택합니다(`DB_DRIVER=auto`) — 대부분 설정이 필요 없으며, 나머지 변수는 [환경 변수](https://doc.oplist.org/ecosystem/official_worker/guide_env)를 참고하세요 > - 배포 후 첫 방문 시 **설치 마법사**가 열립니다. 브라우저에서 관리자 계정과 비밀번호를 설정하면 초기화가 완료됩니다(`ADMIN_PASS`를 설정하면 마법사를 건너뜁니다) @@ -69,32 +70,23 @@ OpenList-Worker는 공식 [OpenListTeam/OpenList](https://github.com/OpenListTea ### 핵심 기능 -- **파일 탐색**:통합된 디렉터리 트리 탐색. 이미지, 동영상, 오디오, 문서, 코드, 압축 파일 등의 온라인 미리보기 지원. -- **업로드·다운로드**:스토리지 간 업로드, 일괄 다운로드, 스트리밍, 다이렉트 링크 리다이렉트. -- **파일 공유**:유효기간·비밀번호·권한 제어가 포함된 공유 링크 생성. 익명 액세스 및 디렉터리 공유 지원. -- **전체 텍스트 검색**:인덱싱된 스토리지에서 파일을 빠르게 검색. -- **오프라인 작업**:백그라운드 작업 큐로 일괄 작업 및 비동기 처리 지원. -- **외부 인터페이스**:집계 스토리지를 WebDAV 또는 S3 호환 프로토콜로 노출하여 타사 도구에 마운트 가능. -- **MCP 서비스**:Model Context Protocol 엔드포인트를 제공하여 AI 어시스턴트 등의 클라이언트에서 통합·호출 가능. - -### 권한 관리 - -- **권한 관리**:역할 기반 접근 제어(RBAC). 사용자 그룹, 디렉터리 수준 읽기·쓰기 권한 및 할당량 지원. -- **인증 방식**:내장 계정 비밀번호에 더해 TOTP 검증, WebAuthn/FIDO 로그인, SSO 싱글 사인온, LDAP 디렉터리 인증 지원. -- **보안 강화**:JWT 세션, CSRF 방지, 클릭재킹 방지(X-Frame-Options), 콘텐츠 보안 정책(CSP). -- **헬스 체크**:`/health` 라이브니스 프로브와 `/healthz` 레디니스 프로브를 제공하여 모니터링·알림에 활용. - -### 플랫폼 배포 - -- **실행 플랫폼**:Cloudflare Workers、Tencent Cloud EdgeOne Makers、Vercel、Serverless、Node.js 컨테이너 환경. -- **데이터 스토리지**:Cloudflare D1(SQLite)을 주로 사용하며 MySQL、MariaDB、PostgreSQL、SQL Server도 지원. -- **영속 캐시**:Cloudflare KV / EdgeOne Blob(선택). 설정 영속화 및 캐시에 사용. -- **원클릭 배포**:EdgeOne、Cloudflare Workers 등 플랫폼에서 원클릭 배포 버튼 + 초기화 지원. +- **파일 관리**:통합 디렉터리 트리 탐색과 이미지 / 동영상 / 오디오 / 문서 / 코드 / 압축 파일 온라인 미리보기;스토리지 간 업로드, 일괄 다운로드, 스트리밍, 다이렉트 링크 리다이렉트;유효기간·비밀번호·권한 제어가 포함된 공유 링크. +- **검색 및 작업**:인덱싱된 스토리지의 전체 텍스트 검색;일괄 작업과 비동기 처리를 위한 백그라운드 작업 큐. +- **외부 인터페이스**:WebDAV、S3 호환 프로토콜, AI 어시스턴트용 MCP 엔드포인트. +- **접근 및 인증**:RBAC(사용자 그룹, 디렉터리 수준 읽기·쓰기 권한, 할당량), 계정 비밀번호 + TOTP / WebAuthn / SSO / LDAP. +- **보안 및 운영**:JWT 세션, CSRF, 클릭재킹(X-Frame-Options)및 CSP 보호, `/health`·`/healthz` 프로브. +- **동시 요청 중복 제거**:같은 순간의 동일한 디렉터리 목록 / 메타데이터 요청은 업스트림 드라이브에 한 번만 전달되고, 나머지는 같은 결과를 재사용하여 속도 제한과 CPU 소모를 줄입니다. +- **실행 플랫폼**:Cloudflare Workers、Tencent Cloud EdgeOne Makers、Alibaba Cloud ESA、Vercel / AWS Lambda 및 Node.js 컨테이너. +- **데이터 스토리지**:Cloudflare D1(SQLite)주 사용, KV / Blob / Durable Objects 및 외부 MySQL / MariaDB 지원. +- **원클릭 배포**:EdgeOne、Cloudflare Workers、Alibaba Cloud ESA 배포 버튼 + 설치 마법사. --- ## 수동 배포 +
+사전 요구 사항, 로컬 개발 및 프로덕션 배포 보기 + ### 사전 요구 사항 - Node.js 18+(pnpm 권장) @@ -106,7 +98,7 @@ OpenList-Worker는 공식 [OpenListTeam/OpenList](https://github.com/OpenListTea # 1. 의존성 설치 pnpm install -# 2. wrangler.toml 설정(JWT_SECRET, KV/D1 바인딩 입력) +# 2. 필요에 따라 wrangler.jsonc의 바인딩과 변수 설정(JWT_SECRET은 .dev.vars.example 참고) # 3. 개발 서버 시작(공식 프런트엔드를 자동으로 가져와 Worker 실행) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### 프로덕션 배포 ```bash -# 원클릭 배포:KV namespace 존재 확인 → 공식 프런트엔드 가져오기 → Cloudflare Workers에 배포 +# 원클릭 배포:프런트엔드 빌드 → Cloudflare Workers에 배포(KV 바인딩은 wrangler가 자동으로 프로비저닝) pnpm run deploy -# 또는 Worker 직접 배포(KV 확인 및 프런트엔드 빌드 건너뜀) +# 또는 Worker 직접 배포(프런트엔드 빌드 건너뜀) pnpm run deploy:worker ``` +
+ --- ## 기술 스택 ### 백엔드 -- **실행 환경**:Cloudflare Workers(Edge Computing) +- **실행 환경**:Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA(Edge Computing) - **웹 프레임워크**:Hono.js -- **데이터베이스**:Cloudflare D1(SQLite)/ MySQL、MariaDB、PostgreSQL、SQL Server 지원 -- **캐시**:Cloudflare KV(선택) +- **데이터베이스**:Cloudflare D1(SQLite)/ KV / Blob / Durable Objects 또는 외부 MySQL / MariaDB - **언어**:TypeScript - **빌드 도구**:Wrangler、esbuild ### 프런트엔드 -- **프레임워크**:React 19 + TypeScript -- **UI 라이브러리**:Ant Design / Material-UI +- **프레임워크**:SolidJS + TypeScript(공식 [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI 라이브러리**:Hope UI - **빌드 도구**:Vite --- +## 배포 설정 + +배포 후 다음 변수를 설정하세요: + +| 변수 | 기본값 | 설명 | +| :--- | :--- | :--- | +| `JWT_SECRET` | (없음) | JWT 서명 키 (≥16자) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (키별) / `sql` (관계형) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | 민감 필드 저장 시 암호화, 예: `aes-256-gcm` | + +전체 변수 목록은 [설정 변수](https://doc.oplist.org/ecosystem/official_worker/guide_env)를 참고하세요. ## 도움말 diff --git a/readmes/README_pt.md b/readmes/README_pt.md index d9bf6eab..fc815fb1 100644 --- a/readmes/README_pt.md +++ b/readmes/README_pt.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Documentação](https://doc.oplist.org) · 🌏 [Documentação (China continental)](https://doc.oplist.org.cn) · ⚖️ [Termos de uso](https://doc.oplist.org/terms) · 🔒 [Política de privacidade](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@ Clique no botão abaixo para implantar este projeto na plataforma correspondente
-| EdgeOne Makers · Internacional | EdgeOne Makers · China | Cloudflare Workers · Global | -| :---: | :---: | :---: | -| [![Implantar no EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Implantar no EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · Internacional | EdgeOne Makers · China | Cloudflare Workers · Global | Alibaba Cloud ESA · China | +| :---: | :---: | :---: | :---: | +| [![Implantar no EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Implantar no EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Implantar no Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Se o Cloudflare exibir `não é possível obter o conteúdo do repositório`, [bifurque](https://github.com/OpenListTeam/OpenList-Worker/fork) este projeto primeiro e depois implante conectando-se ao repositório do Github -> - Na implantação, informe `JWT_SECRET` (string aleatória com 16+ caracteres, por exemplo `openssl rand -hex 32`): ela assina JWTs e criptografa dados; se não definida, é gerada e persistida durante a inicialização +> - Para o Alibaba Cloud ESA, importe este repositório em **Computação de borda e IA → Funções e Pages** no console +> - Configure `JWT_SECRET` na implantação (≥16 caracteres) para assinatura JWT e criptografia > - O armazenamento de dados usa automaticamente o backend persistente da plataforma (`DB_DRIVER=auto`) — sem configuração necessária; para as demais variáveis, consulte [Variáveis de ambiente](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - Após a implantação, o primeiro acesso abre o **assistente de instalação**: defina a conta e a senha do administrador no navegador para concluir a inicialização (defina `ADMIN_PASS` para pular o assistente) @@ -69,32 +70,23 @@ Além dos armazenamentos reais acima, também são fornecidos drivers virtuais/f ### Principais recursos -- **Navegação de arquivos**: navegação unificada na árvore de diretórios, com visualização online de imagens, vídeos, áudio, documentos, código, arquivos e muito mais. -- **Upload e download**: upload entre armazenamentos, download em lote, streaming e redirecionamento de link direto. -- **Compartilhamento de arquivos**: gerar links de compartilhamento com expiração, senha e controle de permissão, suportando acesso anônimo e compartilhamento de diretórios. -- **Busca em texto completo**: pesquisa rápida de arquivos em armazenamentos indexados. -- **Tarefas offline**: fila de tarefas em segundo plano com suporte a operações em lote e processamento assíncrono. -- **Interfaces externas**: expor o armazenamento agregado via protocolo WebDAV ou compatível com S3 para montagem em ferramentas de terceiros. -- **Serviço MCP**: fornecer um endpoint de Model Context Protocol que pode ser integrado e chamado por assistentes de IA e outros clientes. - -### Gestão de acesso - -- **Permissões**: controle de acesso baseado em funções (RBAC), com suporte a grupos de usuários, permissões de leitura/gravação em nível de diretório e cotas. -- **Autenticação**: senhas de conta integradas com verificação TOTP, login WebAuthn/FIDO, login único SSO e autenticação de diretório LDAP. -- **Endurecimento de segurança**: sessões JWT, proteção CSRF, proteção contra clickjacking (X-Frame-Options), Content Security Policy (CSP). -- **Verificações de saúde**: fornecer uma sonda de vivacidade `/health` e uma sonda de prontidão `/healthz` para monitoramento e alertas. - -### Implantação em plataformas - -- **Plataformas de execução**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless e ambientes de contêiner Node.js. -- **Armazenamento de dados**: Cloudflare D1 (SQLite) como principal, também compatível com MySQL, MariaDB, PostgreSQL, SQL Server. -- **Cache persistente**: Cloudflare KV / EdgeOne Blob (opcional) para persistência de configuração e cache. -- **Implantação em um clique**: suporta botões de implantação em um clique + inicialização no EdgeOne, Cloudflare Workers e outras plataformas. +- **Gerenciamento de arquivos**: navegação unificada na árvore de diretórios com visualização online de imagens, vídeos, áudio, documentos, código e arquivos; upload entre armazenamentos, download em lote, streaming e redirecionamento de link direto; links de compartilhamento com expiração, senha e controle de permissão. +- **Busca e tarefas**: busca em texto completo em armazenamentos indexados; fila de tarefas em segundo plano para operações em lote e processamento assíncrono. +- **Interfaces externas**: WebDAV, protocolo compatível com S3 e endpoint MCP para assistentes de IA. +- **Acesso e autenticação**: RBAC (grupos de usuários, permissões de leitura/gravação por diretório, cotas), senhas de conta + TOTP / WebAuthn / SSO / LDAP. +- **Segurança e operação**: sessões JWT, proteção CSRF, clickjacking (X-Frame-Options) e CSP, sondas `/health` e `/healthz`. +- **Deduplicação de solicitações concorrentes**: solicitações idênticas de listagem de diretório / metadados consultam o disco upstream apenas uma vez; as demais reutilizam o resultado, reduzindo limites de taxa e consumo de CPU. +- **Plataformas de execução**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda e contêineres Node.js. +- **Armazenamento de dados**: Cloudflare D1 (SQLite) como principal, além de KV / Blob / Durable Objects e MySQL / MariaDB externos. +- **Implantação em um clique**: botões de implantação do EdgeOne, Cloudflare Workers e Alibaba Cloud ESA + assistente de instalação. --- ## Implantação manual +
+Ver pré-requisitos, desenvolvimento local e implantação em produção + ### Pré-requisitos - Node.js 18+ (pnpm recomendado) @@ -106,7 +98,7 @@ Além dos armazenamentos reais acima, também são fornecidos drivers virtuais/f # 1. Instalar dependências pnpm install -# 2. Configurar o wrangler.toml (preencher JWT_SECRET, ligações KV/D1) +# 2. Configurar as ligações e variáveis no wrangler.jsonc conforme necessário (JWT_SECRET veja .dev.vars.example) # 3. Iniciar o servidor de desenvolvimento (obtém automaticamente o frontend oficial e executa o Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Implantação em produção ```bash -# Implantação em um clique: garantir que o namespace KV exista → obter o frontend oficial → implantar no Cloudflare Workers +# Implantação em um clique: compilar o frontend → implantar no Cloudflare Workers (as ligações KV são provisionadas automaticamente pelo wrangler) pnpm run deploy -# Ou implantar diretamente o Worker (pular a verificação de KV e a compilação do frontend) +# Ou implantar diretamente o Worker (pular a compilação do frontend) pnpm run deploy:worker ``` +
+ --- ## Stack tecnológico ### Backend -- **Ambiente de execução**: Cloudflare Workers (Edge Computing) +- **Ambiente de execução**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Framework web**: Hono.js -- **Banco de dados**: Cloudflare D1 (SQLite) / compatível com MySQL, MariaDB, PostgreSQL, SQL Server -- **Cache**: Cloudflare KV (opcional) +- **Banco de dados**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects ou MySQL / MariaDB externo - **Linguagem**: TypeScript - **Ferramentas de build**: Wrangler, esbuild ### Frontend -- **Framework**: React 19 + TypeScript -- **Bibliotecas UI**: Ant Design / Material-UI +- **Framework**: SolidJS + TypeScript (oficial [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **Biblioteca UI**: Hope UI - **Ferramenta de build**: Vite --- +## Configuração da implantação + +Após a implantação, defina as seguintes variáveis: + +| Variável | Padrão | Descrição | +| :--- | :--- | :--- | +| `JWT_SECRET` | (nenhum) | Chave de assinatura JWT (≥16 caracteres) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (por chave) / `sql` (relacional) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Criptografia em repouso de campos sensíveis, por exemplo `aes-256-gcm` | + +Para a lista completa de variáveis, consulte [Variáveis de configuração](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Suporte diff --git a/readmes/README_ru.md b/readmes/README_ru.md index 8355c6a2..a834da4b 100644 --- a/readmes/README_ru.md +++ b/readmes/README_ru.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [Документация](https://doc.oplist.org) · 🌏 [Документация (континентальный Китай)](https://doc.oplist.org.cn) · ⚖️ [Условия использования](https://doc.oplist.org/terms) · 🔒 [Политика конфиденциальности](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · Международный | EdgeOne Makers · Китай | Cloudflare Workers · Глобальный | -| :---: | :---: | :---: | -| [![Развернуть на EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Развернуть на EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · Международный | EdgeOne Makers · Китай | Cloudflare Workers · Глобальный | Alibaba Cloud ESA · Китай | +| :---: | :---: | :---: | :---: | +| [![Развернуть на EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Развернуть на EdgeOne](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![Развернуть на Alibaba Cloud ESA](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - Если Cloudflare отображает `не удаётся получить содержимое репозитория`, сначала [сделайте fork](https://github.com/OpenListTeam/OpenList-Worker/fork) этого проекта, а затем разверните, подключившись к репозиторию Github -> - При развёртывании укажите `JWT_SECRET` (случайная строка от 16 символов, например `openssl rand -hex 32`): она подписывает JWT и шифрует данные; если не задана, будет сгенерирована и сохранена при инициализации +> - Для Alibaba Cloud ESA импортируйте этот репозиторий в консоли через **Периферийные вычисления и ИИ → Функции и Pages** +> - Задайте `JWT_SECRET` при развёртывании (≥16 символов) для подписи JWT и шифрования > - Хранилище данных автоматически использует встроенный постоянный бэкенд платформы (`DB_DRIVER=auto`) — настройка не требуется; остальные переменные см. в [Переменные окружения](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - При первом посещении после развёртывания открывается **мастер установки**: задайте учётную запись и пароль администратора в браузере, чтобы завершить инициализацию (установите `ADMIN_PASS`, чтобы пропустить мастер) @@ -69,32 +70,23 @@ OpenList-Worker — это официальный порт проекта [OpenL ### Основные возможности -- **Просмотр файлов**: единый просмотр дерева каталогов с онлайн-предпросмотром изображений, видео, аудио, документов, кода, архивов и др. -- **Загрузка и скачивание**: загрузка между хранилищами, пакетное скачивание, потоковая передача и перенаправление по прямой ссылке. -- **Обмен файлами**: создание ссылок для обмена со сроком действия, паролем и контролем прав, с поддержкой анонимного доступа и обмена каталогами. -- **Полнотекстовый поиск**: быстрый поиск файлов в проиндексированных хранилищах. -- **Офлайн-задачи**: фоновая очередь задач с поддержкой пакетных операций и асинхронной обработки. -- **Внешние интерфейсы**: предоставление агрегированного хранилища по протоколу WebDAV или S3-совместимому протоколу для монтирования в сторонние инструменты. -- **Сервис MCP**: предоставление конечной точки Model Context Protocol, которую можно интегрировать и вызывать из ИИ-ассистентов и других клиентов. - -### Управление доступом - -- **Права**: управление доступом на основе ролей (RBAC), с поддержкой групп пользователей, прав чтения/записи на уровне каталогов и квот. -- **Аутентификация**: встроенные пароли учётных записей с проверкой TOTP, вход WebAuthn/FIDO, единый вход SSO и аутентификация в каталоге LDAP. -- **Усиление безопасности**: сессии JWT, защита от CSRF, защита от кликджекинга (X-Frame-Options), Content Security Policy (CSP). -- **Проверки работоспособности**: предоставление пробы живости `/health` и пробы готовности `/healthz` для мониторинга и оповещений. - -### Развёртывание на платформах - -- **Платформы выполнения**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Vercel, Serverless и контейнерные среды Node.js. -- **Хранение данных**: Cloudflare D1 (SQLite) как основное, также поддерживаются MySQL, MariaDB, PostgreSQL, SQL Server. -- **Постоянный кэш**: Cloudflare KV / EdgeOne Blob (необязательно) для сохранения конфигурации и кэширования. -- **Развёртывание в один клик**: поддержка кнопок развёртывания в один клик + инициализация на EdgeOne, Cloudflare Workers и других платформах. +- **Управление файлами**: единый просмотр дерева каталогов с онлайн-предпросмотром изображений, видео, аудио, документов, кода и архивов; загрузка между хранилищами, пакетное скачивание, потоковая передача и перенаправление по прямой ссылке; ссылки для обмена со сроком действия, паролем и контролем прав. +- **Поиск и задачи**: полнотекстовый поиск в проиндексированных хранилищах; фоновая очередь задач для пакетных операций и асинхронной обработки. +- **Внешние интерфейсы**: WebDAV, S3-совместимый протокол и конечная точка MCP для ИИ-ассистентов. +- **Доступ и аутентификация**: RBAC (группы пользователей, права чтения/записи на уровне каталогов, квоты), пароли учётных записей + TOTP / WebAuthn / SSO / LDAP. +- **Безопасность и эксплуатация**: сессии JWT, защита CSRF, кликджекинг (X-Frame-Options) и CSP, пробы `/health` и `/healthz`. +- **Дедупликация одновременных запросов**: одинаковые запросы списка каталога / метаданных обращаются к вышестоящему диску только один раз; остальные используют тот же результат, снижая ограничения частоты и нагрузку на CPU. +- **Платформы выполнения**: Cloudflare Workers, Tencent Cloud EdgeOne Makers, Alibaba Cloud ESA, Vercel / AWS Lambda и контейнеры Node.js. +- **Хранение данных**: Cloudflare D1 (SQLite) как основное, а также KV / Blob / Durable Objects и внешние MySQL / MariaDB. +- **Развёртывание в один клик**: кнопки развёртывания EdgeOne, Cloudflare Workers и Alibaba Cloud ESA + мастер установки. --- ## Ручное развёртывание +
+Показать требования, локальную разработку и развёртывание в продакшн + ### Предварительные требования - Node.js 18+ (рекомендуется pnpm) @@ -106,7 +98,7 @@ OpenList-Worker — это официальный порт проекта [OpenL # 1. Установить зависимости pnpm install -# 2. Настроить wrangler.toml (указать JWT_SECRET, привязки KV/D1) +# 2. При необходимости настроить привязки и переменные в wrangler.jsonc (JWT_SECRET см. .dev.vars.example) # 3. Запустить сервер разработки (автоматически получает официальный фронтенд и запускает Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### Развёртывание в продакшн ```bash -# Развёртывание в один клик: убедиться, что namespace KV существует → получить официальный фронтенд → развернуть на Cloudflare Workers +# Развёртывание в один клик: собрать фронтенд → развернуть на Cloudflare Workers (привязки KV создаются wrangler автоматически) pnpm run deploy -# Или развернуть Worker напрямую (пропустить проверку KV и сборку фронтенда) +# Или развернуть Worker напрямую (пропустить сборку фронтенда) pnpm run deploy:worker ``` +
+ --- ## Технологический стек ### Бэкенд -- **Среда выполнения**: Cloudflare Workers (Edge Computing) +- **Среда выполнения**: Cloudflare Workers / EdgeOne Functions / Alibaba Cloud ESA (Edge Computing) - **Веб-фреймворк**: Hono.js -- **База данных**: Cloudflare D1 (SQLite) / поддержка MySQL, MariaDB, PostgreSQL, SQL Server -- **Кэш**: Cloudflare KV (необязательно) +- **База данных**: Cloudflare D1 (SQLite) / KV / Blob / Durable Objects или внешние MySQL / MariaDB - **Язык**: TypeScript - **Инструменты сборки**: Wrangler, esbuild ### Фронтенд -- **Фреймворк**: React 19 + TypeScript -- **UI-библиотеки**: Ant Design / Material-UI +- **Фреймворк**: SolidJS + TypeScript (официальный [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI-библиотека**: Hope UI - **Инструмент сборки**: Vite --- +## Конфигурация развёртывания + +После развёртывания задайте следующие переменные: + +| Переменная | По умолчанию | Описание | +| :--- | :--- | :--- | +| `JWT_SECRET` | (нет) | Ключ подписи JWT (≥16 символов) | +| `DB_FORMAT` | `map` | `map` (JSON) / `key` (по ключам) / `sql` (реляционный) | +| `DB_DRIVER` | `auto` | `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | Шифрование чувствительных полей на диске, например `aes-256-gcm` | + +Полный список переменных см. в разделе [Переменные конфигурации](https://doc.oplist.org/ecosystem/official_worker/guide_env). ## Поддержка diff --git a/readmes/README_zh-TW.md b/readmes/README_zh-TW.md index cb170b9d..c1af2b45 100644 --- a/readmes/README_zh-TW.md +++ b/readmes/README_zh-TW.md @@ -7,9 +7,9 @@ License Build status -latest version discussions -Downloads +Stars +Forks 📘 [使用文件](https://doc.oplist.org) · 🌏 [使用文件(中國大陸)](https://doc.oplist.org.cn) · ⚖️ [使用條款](https://doc.oplist.org/terms) · 🔒 [隱私政策](https://doc.oplist.org/privacy) @@ -35,15 +35,16 @@
-| EdgeOne Makers · 國際站 | EdgeOne Makers · 中國站 | Cloudflare Workers · 全球站 | -| :---: | :---: | :---: | -| [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | +| EdgeOne Makers · 國際站 | EdgeOne Makers · 中國站 | Cloudflare Workers · 全球站 | 阿里雲 ESA · 中國站 | +| :---: | :---: | :---: | :---: | +| [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![使用 EdgeOne 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://console.cloud.tencent.com/edgeone/pages/new?project-name=openlist-tsworker&repository-url=https://github.com/OpenListTeam/OpenList-Worker&install-command=pnpm%20install%20--no-frozen-lockfile&build-command=pnpm%20run%20build&output-directory=dist&env=JWT_SECRET) | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/OpenListTeam/OpenList-Worker) | [![使用阿里雲 ESA 部署](assets/esa-deploy.svg)](https://esa.console.aliyun.com/edge/function/list) |
> [!IMPORTANT] > - 若 Cloudflare 提示`無法取得儲存庫內容`,則您需要先 [Fork](https://github.com/OpenListTeam/OpenList-Worker/fork) 本專案,再透過連接到 Github 倉庫功能部署 -> - 部署時請填寫 `JWT_SECRET`(≥16 字元的隨機字串,可用 `openssl rand -hex 32` 產生):用於 JWT 簽章與資料加密;未設定時會在初始化時自動產生並持久化 +> - 阿里雲 ESA 請在控制台「邊緣運算和 AI → 函數和 Pages」中匯入本儲存庫 +> - 部署時請填寫 `JWT_SECRET`(≥16 字元),用於 JWT 簽章與加密 > - 資料儲存預設自動選擇平台內建的持久化後端(`DB_DRIVER=auto`),無需設定;其餘變數見 [設定變數](https://doc.oplist.org/ecosystem/official_worker/guide_env) > - 部署完成後首次造訪站點,依**安裝精靈**在瀏覽器中設定管理員帳號與密碼即可完成初始化;如需跳過精靈,可設定 `ADMIN_PASS` @@ -69,32 +70,23 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea ### 核心能力 -- **檔案瀏覽**:統一的目錄樹瀏覽,支援圖片、影片、音訊、文件、程式碼、壓縮檔等格式線上預覽。 -- **上傳下載**:跨儲存的上傳、批次下載、串流傳輸與直鏈跳轉。 -- **檔案分享**:生成帶有效期、密碼與權限控制的分享連結,支援匿名存取與目錄分享。 -- **全文搜尋**:在已索引的儲存中快速檢索檔案。 -- **離線任務**:後台任務佇列,支援批次操作與非同步處理。 -- **外部介面**:將聚合儲存以 WebDAV 或 S3 相容協定對外暴露,便於掛載到第三方工具。 -- **MCP 服務**:提供 Model Context Protocol 端點,可被 AI 助手等客戶端整合呼叫。 - -### 權限管理 - -- **權限管理**:基於角色的存取控制(RBAC),支援使用者分組、目錄級讀寫權限與配額。 -- **認證方式**:內建帳號密碼,支援 TOTP 驗證、WebAuthn/FIDO 登入、SSO 單點登入與 LDAP 目錄認證。 -- **安全加固**:JWT 會話、CSRF 防護、點擊劫持防護(X-Frame-Options)、內容安全策略(CSP)。 -- **健康檢查**:提供 `/health` 存活探針與 `/healthz` 就緒探針,可用於監控與告警。 - -### 平台部署 - -- **運行平台**:Cloudflare Workers、騰訊雲 EdgeOne Makers、Vercel、Serverless 及 Node.js 容器環境。 -- **資料儲存**:Cloudflare D1(SQLite)為主,同時支援 MySQL、MariaDB、PostgreSQL、SQL Server。 -- **持久快取**:Cloudflare KV / EdgeOne Blob(可選),用於配置持久化與快取。 -- **一鍵部署**:支援 EdgeOne、Cloudflare Workers 等平台的一鍵部署按鈕+初始化。 +- **檔案管理**:統一目錄樹瀏覽,圖片 / 影片 / 音訊 / 文件 / 程式碼 / 壓縮檔線上預覽;跨儲存上傳、批次下載、串流傳輸與直鏈跳轉;帶有效期、密碼與權限控制的分享連結。 +- **檢索與任務**:已索引儲存的全文搜尋;後台任務佇列支援批次與非同步處理。 +- **外部介面**:WebDAV、S3 相容協定,以及可供 AI 助手整合的 MCP 端點。 +- **權限與認證**:RBAC(使用者分組、目錄級讀寫權限、配額),帳號密碼 + TOTP / WebAuthn / SSO / LDAP。 +- **安全與維運**:JWT 會話、CSRF、點擊劫持(X-Frame-Options)與 CSP 防護,`/health`、`/healthz` 健康探針。 +- **並行去重**:相同目錄列表 / 中繼資料請求只向上游網盤發起一次,其餘請求重用結果,減少限流與 CPU 消耗。 +- **執行平台**:Cloudflare Workers、騰訊雲 EdgeOne Makers、阿里雲 ESA、Vercel / AWS Lambda 與 Node.js 容器。 +- **資料儲存**:Cloudflare D1(SQLite)為主,也支援 KV / Blob / Durable Objects 與外部 MySQL / MariaDB。 +- **一鍵部署**:支援 EdgeOne、Cloudflare Workers、阿里雲 ESA 部署按鈕 + 安裝精靈。 --- ## 手動部署 +
+展開查看前置要求、本地開發與生產部署 + ### 前置要求 - Node.js 18+(推薦使用 pnpm) @@ -106,7 +98,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea # 1. 安裝依賴 pnpm install -# 2. 配置 wrangler.toml(填寫 JWT_SECRET、KV/D1 綁定) +# 2. 按需設定 wrangler.jsonc 中的綁定與變數(JWT_SECRET 見 .dev.vars.example) # 3. 啟動開發伺服器(自動拉取官方前端並運行 Worker) pnpm run dev:unified @@ -118,34 +110,47 @@ pnpm run dev:worker ### 生產部署 ```bash -# 一鍵部署:確保 KV namespace 存在 → 拉取官方前端 → 部署到 Cloudflare Workers +# 一鍵部署:建置前端產物 → 部署到 Cloudflare Workers(KV 綁定由 wrangler 自動預配) pnpm run deploy -# 或直接部署 Worker(跳過 KV 檢查與前端建置) +# 或直接部署 Worker(跳過前端建置) pnpm run deploy:worker ``` +
+ --- ## 技術架構 ### 後端 -- **運行環境**:Cloudflare Workers(Edge Computing) +- **運行環境**:Cloudflare Workers / EdgeOne 雲函數 / 阿里雲 ESA(Edge Computing) - **Web 框架**:Hono.js -- **資料庫**:Cloudflare D1(SQLite)/ 支援 MySQL、MariaDB、PostgreSQL、SQL Server -- **快取**:Cloudflare KV(可選) +- **資料庫**:Cloudflare D1(SQLite)/ KV / Blob / Durable Objects,或外部 MySQL / MariaDB - **語言**:TypeScript - **建置工具**:Wrangler、esbuild ### 前端 -- **框架**:React 19 + TypeScript -- **UI 庫**:Ant Design / Material-UI +- **框架**:SolidJS + TypeScript(官方 [OpenList-Frontend](https://github.com/OpenListTeam/OpenList-Frontend)) +- **UI 庫**:Hope UI - **建置工具**:Vite --- +## 部署設定 + +部署完成後請設定如下變數: + +| 變數 | 預設值 | 說明 | +| :--- | :--- | :--- | +| `JWT_SECRET` | (無) | JWT 令牌簽章金鑰(≥16 字元) | +| `DB_FORMAT` | `map` | `map`(JSON)/ `key`(分 key)/ `sql`(關聯表) | +| `DB_DRIVER` | `auto` | 可選 `auto` / `blob` / `cfkv` / `kv` / `d1` / `do` / `mysql` | +| `DB_CIPHER` | `none` | 敏感欄位落盤演算法,如 `aes-256-gcm` | + +完整環境變數,請參閱[設定變數](https://doc.oplist.org/ecosystem/official_worker/guide_env) ## 幫助支援 diff --git a/readmes/assets/esa-deploy.svg b/readmes/assets/esa-deploy.svg new file mode 100644 index 00000000..0bf0ffa0 --- /dev/null +++ b/readmes/assets/esa-deploy.svg @@ -0,0 +1,5 @@ + + + + Deploy to Alibaba Cloud ESA + diff --git a/src/backend/server/init_diagnostics.test.ts b/src/backend/server/init_diagnostics.test.ts index 7dbdd0c7..ab0b2b99 100644 --- a/src/backend/server/init_diagnostics.test.ts +++ b/src/backend/server/init_diagnostics.test.ts @@ -159,8 +159,8 @@ test("驱动不可用:错误必须写明该驱动需要什么(d1 示例)", assert.equal(data.storage.error_code, "DRIVER_UNAVAILABLE") assert.equal( data.storage.summary, - `DB_DRIVER is set to "d1", but that driver is not available in this runtime.`, - "summary 是给界面的一行短句", + `存储驱动不可用:DB_DRIVER=d1 / Storage driver unavailable: DB_DRIVER=d1`, + "summary 是给界面的一行中英双语短句", ) // 接口只透传前 3 行,因此这 3 行必须是「原因 + 不回退 + 怎么做」 assert.match( @@ -302,7 +302,7 @@ test("显式配置的驱动不可用:绝不回退(即使 auto 能选到别 await buildApp().request("/api/public/init_status", { method: "GET" }, env) ).json()).data assert.equal(st.initialized, false) - assert.match(String(st.storage_error), /not available in this runtime/) + assert.match(String(st.storage_error), /Storage driver unavailable|存储驱动不可用/) assert.match(String(st.storage_suggestion), /DB_DRIVER=blob/) }) diff --git a/src/backend/server/public.ts b/src/backend/server/public.ts index 0b41523f..10132e5c 100644 --- a/src/backend/server/public.ts +++ b/src/backend/server/public.ts @@ -17,12 +17,8 @@ import { readFormat, } from "../internal/model/store/backend" import { setUserPassword } from "../pkg/password" -// 脱敏 / 截断 / 摘要 / 建议组装:与全局 503 拦截(index.ts)共用同一套规则 -import { - reasonLines, - redact, - storageErrorSummary, -} from "./storage-error" +// 脱敏 / 截断:与全局 503 拦截(index.ts)共用同一套规则 +import { reasonLines, redact } from "./storage-error" export const publicRouter = new Hono() @@ -41,27 +37,86 @@ function resolvedOrNull(value: any): string | null { } /** - * 「没有可用存储后端」时的统一建议(单一来源)。 - * - * ## 为什么不再提 `set DB_DRIVER=auto` - * - * 旧文案是「Bind a storage backend (D1 / KV / Blob) **or set DB_DRIVER=auto**」。 - * 后半句在**绝大多数真实场景下是循环建议**:用户看到这条 issue 时,`DB_DRIVER` - * 往往**本来就是 auto**(CF / EdgeOne 部署的默认形态)。让他「去设置 auto」等于 - * 让他改一个已经正确的值,改完照旧报错,只会加深「这软件坏了」的印象。 + * 初始化向导展示的「一行原因 + 一行建议」:中英双语、尽量短。 * - * 准确的表述是:auto **已经把所有候选驱动探测过一遍且都不可用**,因此缺的是 - * **平台侧的绑定**,不是配置值。这里只说这件事。 + * 安装页只读取 issue.summary / issue.suggestion(以及 init_status 的 + * storage_error / storage_suggestion),且不会按语言切换,所以这里直接返回 + * 「中文 / English」。详细排查信息仍保留在 message / reason 里,只给日志和 + * 诊断工具使用 —— 界面不再被多行长文淹没。 * - * 若驱动解析器给出了更精确的 hint(如 `noStorageHint` 的逐平台文案),调用方 - * 会优先使用它,本函数只是兜底。 + * 建议里仍保留「auto 会选中哪个驱动」这类可抄写答案(见 DRIVER_UNAVAILABLE), + * 因为精简不等于少给下一步动作。 */ -function bindBackendSuggestion(): string { - return ( - "No storage binding was detected (auto probes every driver). " + - "Bind one and redeploy — Cloudflare: D1 or KV namespace; " + - "EdgeOne: Blob; ESA: ESA_BLOB." - ) +function initIssueText( + code: string | null | undefined, + opts: { + driver?: string | null + format?: string | null + detail?: string | null + } = {}, +): { summary: string; suggestion: string } { + const driver = String(opts.driver || "").trim() + const format = String(opts.format || "").trim() + const label = driver && driver !== "auto" ? driver : "DB_DRIVER" + + switch (code) { + case "NO_STORAGE": + return { + summary: "没有可用的存储后端 / No storage backend available", + suggestion: + "绑定持久化后端后重新部署:Cloudflare 用 D1 或 KV,EdgeOne / ESA 用 Blob / " + + "Bind a persistent backend and redeploy: D1 or KV on Cloudflare, Blob on EdgeOne / ESA", + } + case "INVALID_COMBINATION": + return { + summary: + `存储组合无效:DB_FORMAT=${format || "?"} 与 DB_DRIVER=${driver || "?"} 不兼容 / ` + + `Invalid storage combination: DB_FORMAT=${format || "?"} cannot be used with DB_DRIVER=${driver || "?"}`, + suggestion: + "改为 DB_FORMAT=map(或 key),或换用 d1 / do / mysql / " + + "Set DB_FORMAT=map (or key), or switch to d1 / do / mysql", + } + case "DRIVER_UNAVAILABLE": { + const picked = String(opts.detail || "").match( + /Auto-detection would pick: DB_DRIVER=([\w-]+)/, + )?.[1] + return { + summary: `存储驱动不可用:DB_DRIVER=${label} / Storage driver unavailable: DB_DRIVER=${label}`, + suggestion: picked + ? `改为 DB_DRIVER=${picked}(或 auto)/ Set DB_DRIVER=${picked} (or auto)` + : "检查该驱动的绑定与凭据,或设置 DB_DRIVER=auto / " + + "Check the binding and credentials, or set DB_DRIVER=auto", + } + } + case "PROXY_CONFIG": + return { + summary: "KV 代理配置错误 / KV proxy configuration error", + suggestion: + "检查 EO_KV_URLS,或改用 DB_DRIVER=auto / " + + "Check EO_KV_URLS, or use DB_DRIVER=auto", + } + case "UNKNOWN_DRIVER": + return { + summary: `未知的存储驱动:DB_DRIVER=${label} / Unknown DB_DRIVER: ${label}`, + suggestion: + "使用 auto / blob / kv / cfkv / d1 / do / mysql / " + + "Use auto / blob / kv / cfkv / d1 / do / mysql", + } + case "STORAGE_READ_FAILED": + return { + summary: "存储读取失败,已中止初始化 / Storage is not readable; initialization was aborted", + suggestion: + "检查存储绑定与凭据,修复后重新部署 / " + + "Check the storage binding and credentials, then redeploy", + } + default: + return { + summary: "存储配置有误 / Storage configuration error", + suggestion: + "检查 DB_DRIVER、DB_FORMAT 与平台绑定 / " + + "Check DB_DRIVER, DB_FORMAT and the platform bindings", + } + } } /** @@ -150,11 +205,16 @@ publicRouter.get("/env_check", async (c) => { if (hasConfigError) { const detail = await getStoreConfigErrorDetail(env, { silent: true }) + const text = initIssueText(detail.code, { + driver: driverCfg, + format: formatCfg, + detail: detail.message, + }) storageDetail = { code: detail.code, - summary: storageErrorSummary(detail.message), + summary: text.summary, message: detail.message, - suggestion: detail.suggestion, + suggestion: text.suggestion, } } @@ -162,13 +222,14 @@ publicRouter.get("/env_check", async (c) => { // 一个可选后端都没有:这类错误本身就是「没有可用的存储」,用一条通用提示 // 说清即可,不必再叠一条内容相同的配置错误(两条 issue 说同一件事只会让 // 用户以为出了两个问题)。 + const text = initIssueText("NO_STORAGE") issues.push({ code: "STORAGE_UNAVAILABLE", level: "error", - summary: "No storage backend available.", + summary: text.summary, message: "No storage backend available.", docUrl: DOC_STORAGE, - suggestion: storageDetail.suggestion || bindBackendSuggestion(), + suggestion: text.suggestion, }) } else if (hasConfigError) { // 配置错误:界面只给「一行短原因 + 一行怎么改」;完整排查说明留在 message / @@ -186,8 +247,8 @@ publicRouter.get("/env_check", async (c) => { summary: storageDetail.summary || (isInvalidCombination - ? "Unsupported storage combination." - : "Storage driver is not configured correctly."), + ? "存储组合无效 / Unsupported storage combination" + : "存储配置有误 / Storage configuration error"), message: isInvalidCombination ? `Unsupported storage combination: ${reason}` : `Storage driver is not configured correctly: ${reason}`, @@ -195,26 +256,28 @@ publicRouter.get("/env_check", async (c) => { suggestion: storageDetail.suggestion, }) } else if (isMemory) { + const text = initIssueText("NO_STORAGE") issues.push({ code: "STORAGE_MEMORY_ONLY", level: serverless ? "error" : "warning", summary: serverless - ? "In-memory storage only; data will be lost immediately." - : "In-memory storage only; data will be lost on restart (fine for local dev).", + ? "仅内存存储,实例销毁后数据丢失 / In-memory storage only; data is lost when the instance stops" + : "仅内存存储,重启后数据丢失 / In-memory storage only; data is lost on restart (fine for local dev)", message: serverless ? "In-memory storage only; data will be lost immediately." : "In-memory storage only; data will be lost on restart (fine for local dev).", docUrl: DOC_STORAGE, - suggestion: bindBackendSuggestion(), + suggestion: text.suggestion, }) } else if (!hasDriver) { + const text = initIssueText("NO_STORAGE") issues.push({ code: "STORAGE_UNAVAILABLE", level: "error", - summary: "No storage backend available.", + summary: text.summary, message: "No storage backend available.", docUrl: DOC_STORAGE, - suggestion: bindBackendSuggestion(), + suggestion: text.suggestion, }) } @@ -226,7 +289,8 @@ publicRouter.get("/env_check", async (c) => { issues.push({ code: "STORAGE_UNHEALTHY", level: "error", - summary: unreachable, + // 原因只给「哪个驱动连不上」,原始错误留在 message 里(可能含内部主机名)。 + summary: `存储驱动连接失败:${resolvedDriver} / Storage driver is configured but unreachable: ${resolvedDriver}`, message: unreachable, docUrl: DOC_DRIVER, // 注意:这里**不**再建议「set DB_DRIVER=auto」。 @@ -234,8 +298,8 @@ publicRouter.get("/env_check", async (c) => { // (如 KV 代理 401)。此时改回 auto 没有任何帮助 —— auto 会解析出同一个 // 驱动、撞上同一个错误,属于把用户支去绕圈。真正要做的是修凭据/绑定。 suggestion: - `Check the credentials/bindings for "${resolvedDriver}" ` + - "(it resolved, but its health check failed).", + `检查该驱动的凭据与绑定 / ` + + `Check the credentials and bindings for "${resolvedDriver}" (it resolved, but its health check failed).`, }) } @@ -249,16 +313,17 @@ publicRouter.get("/env_check", async (c) => { // 密钥支持**自动生成 + 持久化**(见 ensureEncryptionSecret),所以文案 // 不说「必须手动配置」,而是同时给出「也可留空让 setup 自动生成」这条路, // 避免用户以为这是个必填项而无谓地卡在向导里。 - summary: "JWT_SECRET is not set.", + summary: + "未配置 JWT_SECRET(可留空,初始化时自动生成)/ " + + "JWT_SECRET is not set (optional; generated at setup)", message: "JWT_SECRET is not set. If storage is available, a secret is generated " + "and persisted automatically during setup; setting it explicitly is " + "recommended so that all instances and cold starts share one key.", docUrl: DOC_STORAGE, suggestion: - "Set JWT_SECRET (32+ chars recommended, `openssl rand -hex 32`) in your " + - "deployment variables — or leave it empty and let setup generate one " + - "(requires working storage).", + "建议设置 32+ 位随机串:openssl rand -hex 32 / " + + "Recommended: set a 32+ char secret via openssl rand -hex 32", }) } @@ -600,15 +665,20 @@ publicRouter.get("/init_status", async (c) => { // 安装向导只能看到本接口,因此这里必须给出可展示的原因,否则用户只会拿到 // 一个 500 或一句「未初始化」,无从判断是绑定缺失、组合写错还是密钥缺失。 // 两类原因都返回(已脱敏): - // storage_error 一行短原因(界面直接展示,故取首行摘要) - // storage_suggestion 一句话修复建议(「改什么」) - // db_load_error 上一次从持久化后端读取失败的原因(运行期故障) + // storage_error 一行中英双语短原因(界面直接展示) + // storage_suggestion 一句话中英双语修复建议(「改什么」) + // db_load_error 上一次从持久化后端读取失败的原因(运行期故障,完整版) let storageError: string | null = null let storageSuggestion: string | null = null if (!storageReady) { const detail = await getStoreConfigErrorDetail(c.env, { silent: true }) - storageError = storageErrorSummary(detail.message) - storageSuggestion = detail.suggestion + const text = initIssueText(detail.code || "STORAGE_READ_FAILED", { + driver: readDriver(c.env), + format: readFormat(c.env), + detail: detail.message, + }) + storageError = text.summary + storageSuggestion = text.suggestion } const dbLoadError = getDbLoadError() @@ -668,6 +738,11 @@ publicRouter.post("/init/setup", async (c) => { // data,让安装向导能直接展示,而不是只给用户一个通用 500。 const detail = await getStoreConfigErrorDetail(c.env, { silent: true }) const code = detail.code || "STORAGE_READ_FAILED" + const text = initIssueText(code, { + driver: readDriver(c.env), + format: readFormat(c.env), + detail: detail.message || loadError, + }) return c.json( { code: 500, @@ -675,12 +750,12 @@ publicRouter.post("/init/setup", async (c) => { "database is not readable; refusing to initialize to avoid overwriting existing config", data: { code, - // 界面展示 summary(完整一句);reason 保留给工具/日志阅读 - summary: storageErrorSummary(loadError), + // 界面展示 summary(一行中英双语);reason 保留给工具/日志阅读 + summary: text.summary, reason: redact(loadError, reasonLines(code)), - // 一句话修复建议:有配置原因时优先用它(如「改成 DB_DRIVER=d1」), + // 一句话中英双语修复建议(如「改成 DB_DRIVER=d1」), // 否则向导只能展示一段原因,用户仍不知道下一步做什么。 - suggestion: detail.suggestion, + suggestion: text.suggestion, }, }, 500, diff --git a/src/backend/server/storage_policy.test.ts b/src/backend/server/storage_policy.test.ts index c8d392a4..68150273 100644 --- a/src/backend/server/storage_policy.test.ts +++ b/src/backend/server/storage_policy.test.ts @@ -230,7 +230,16 @@ test("未配置 JWT_SECRET:ready 仍应为 true(存储可用即可开始安 "warning", "缺密钥不是 error:存储可用时 setup 会自动生成并持久化", ) - assert.match(String(issue.suggestion), /let setup generate one/) + assert.match( + String(issue.summary), + /generated at setup|初始化时自动生成/, + "summary 要说明可以留空、由初始化自动生成,避免用户误以为是必填项", + ) + assert.match( + String(issue.suggestion), + /openssl rand -hex 32/, + "建议里给出可直接执行的生成命令", + ) }) test("未配置 JWT_SECRET:初始化成功后必须自动生成并持久化密钥", async () => { diff --git a/wrangler.jsonc b/wrangler.jsonc index fcfdc95d..7d76bef2 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -2,22 +2,6 @@ // OpenList-TSWorker · Cloudflare Workers 配置 // 文档:https://developers.cloudflare.com/workers/wrangler/configuration/ // -// ⚠️ 本文件只声明 KV 一个存储绑定(见文件末 kv_namespaces),D1 / DO 等 -// 其它存储请按需在控制台添加。 -// -// 为什么声明 KV 且【省略 id 字段】(不能写 "id": ""): -// wrangler 会自动预配(automatic provisioning):首次部署创建 -// "项目名-绑定名" 的命名空间(此处即 openlist-tsworkers-kv)并绑定; -// 二次部署通过服务端 inherit 复用已有绑定,不会重复创建。 -// -// 注意:CI / 一键部署属非交互环境,wrangler 不会把生成的 id 回写进本文件。 -// 这是预期行为 —— 复用靠的是服务端 inherit 绑定,而非本地 id。 -// -// ⚠️ 不要用 `wrangler kv namespace create` 手动建名为 "KV" 的命名空间: -// wrangler 的预配链路无法按 title 复用已有命名空间(KVHandler 未实现 -// isConnectedToExistingResource),它会另建 openlist-tsworkers-kv, -// 手动建的那个不会被任何绑定使用,成为孤儿资源。参见 Issue #34。 -// // Secret(JWT_SECRET / ADMIN_PASS / ALLOW_URLS / MYSQL_URLS / EO_KV_URLS / CF_* 等)请在 // .dev.vars.example 或 .env.example 中声明 —— 部署按钮据此生成输入项, // 用户填入的值会成为该 Worker 的 Secret,后续更新不会被覆盖。 @@ -28,18 +12,8 @@ "compatibility_date": "2026-08-04", "compatibility_flags": ["nodejs_compat"], "workers_dev": false, - - // 见 issue #174:保留控制台配置的普通环境变量,部署更新后不再被清空。 - // Secrets 本就不受影响;此开关仅作用于 vars。 - "keep_vars": true, "preview_urls": false, - - // ⚠️ run_worker_first 只放行 HTML 入口,不能改成 true(全量放行): - // 默认「资源优先」时 / 与 /index.html 由静态层直接返回,Worker 根本不会执行, - // ASSET_URLS 的 CDN 注入(fetch CDN index.html → 注入 window.OPENLIST_CONFIG.cdn) - // 永远不生效,前端仍从源站加载全部静态资源。 - // 仅放行 HTML 入口后,带 hash 的 JS/CSS/图片仍由静态层直出、零 Worker 调用, - // 只有页面导航请求才多付一次 Worker 调用。 + "keep_vars": true, "assets": { "directory": "./dist", "binding": "ASSETS", @@ -73,25 +47,10 @@ // 敏感字段落盘算法(默认 none:不加密;与 DB_DRIVER/DB_FORMAT 正交): // none(默认) - 不加密,敏感字段明文落盘 // aes-256-gcm - HKDF 派生 AES-256-GCM 密钥(enc:v2:,推荐) - // aes-256-gcm-pbkdf2 - AES-256-GCM + PBKDF2-SHA256(10 万次) 逐字段派生 - // (历史 enc:v1: envelope,每字段约 27ms,慢) // aes-256-cbc-hmac - AES-256-CBC + HMAC-SHA256(enc:v3:) // chacha20-poly1305 - ChaCha20-Poly1305(enc:v4:,RFC 8439,纯 JS) - // des-cbc-hmac - 单 DES + HMAC-SHA256(enc:v5:,仅兼容,不安全) - // 3des-cbc-hmac - 3DES + HMAC-SHA256(enc:v6:,仅兼容,已弃用) - // 说明:加密对象为 storages[].addition / 敏感 settings / users[].password / - // users[].otp_secret。密文带 enc:vN: 前缀,读取时按前缀自动解密, - // 因此把该项改回 none 不会导致既有数据不可读(下次保存自动转为明文)。 - // 注意:该项与 JWT 密钥无关 —— DB_CIPHER=none 时,未配置 JWT_SECRET 仍会 - // 自动生成并持久化共享密钥(JWT 签名需要)。 "DB_CIPHER": "none", - // ── 持久缓存 / 并发去重均已移除 ─────────────────────────────────────── - // 目录列表(/fs/list、/fs/get、/fs/dirs)与下载直链(/d、/p)每次都直接 - // 回源驱动:不做跨请求缓存,也不合并并发相同调用。外部改动、直链与并发 - // 请求的结果都以驱动当次返回为准。相关环境变量(CACHE_* / SINGLEFLIGHT*) - // 已全部删除,残留同名变量不会再被读取。 - // ── 安全鉴权(推荐以 Secret 类型配置,见 .dev.vars.example)────────── // // JWT 签名密钥(>=16 字符,兼作可选的字段加密密钥与定时任务鉴权) @@ -112,13 +71,8 @@ // "MAX_UPPART": "16777216", // // 前端静态资源 CDN 地址,需包含完整 dist(含 index.html),支持 $version 占位符 - // 优先「本地 index.html + CDN 资产」(不要求 CDN 能返回 HTML,npmmirror 也能用); - // 本地哈希不在 CDN 上时改用 CDN 的 index.html 保证同源;都不可用则回退源站 - // ⚠️ 资源由浏览器直接加载,CDN 必须对 .js/.css 返回 Access-Control-Allow-Origin // "ASSET_URLS": "https://registry.npmmirror.com/@openlist-frontend/openlist-frontend/$version/files/dist", - // "ASSET_URLS": "https://cdn.jsdelivr.net/npm/@openlist-frontend/openlist-frontend@$version/dist", - // "ASSET_URLS": "https://unpkg.com/@openlist-frontend/openlist-frontend@$version/dist", - // + // 允许作为种子数据来源的主机白名单,逗号/空格分隔 // "ALLOW_SEED": "", @@ -134,9 +88,6 @@ // "CF_API_KEY": "", // ── EdgeOne Node 云函数 KV 代理(DB_DRIVER=kv 时可选)────────────────── - // - // EdgeOne 的 KV 绑定只注入 Edge Functions,不会注入 Node 云函数。 - // 运行在 EdgeOne Node 云函数时,必须经同部署的 Edge Functions 代理访问 KV。 // 此处填部署 origin,如 https://your-project.edgeone.app // 留空则自动使用当前请求 origin(同一部署自调用);跨域或本地调试才需显式设置。 // "EO_KV_URLS": "", @@ -159,23 +110,14 @@ // =========================================================================== // // ▶ 方案 A:KV(默认启用,免费额度大、最通用) - // ⚠️ 必须【完全省略 id 字段】,不能写成 "id": "": - // wrangler 配置校验会拒绝空字符串 id("should have a string id field")。 - // 省略 id 后:首次部署由 wrangler 自动创建 openlist-tsworkers-kv 并绑定; - // 二次部署通过 inherit 复用已有绑定,不会重复创建。 - // 若想改用账户里已有的 KV,把该命名空间的 id 填进来即可。 - // // "kv_namespaces": [{ "binding": "KV" }], // - // ▶ 方案 B:D1(关系型,可与 Go 后端共享库;DB_FORMAT 建议改为 "sql") - // 控制台:Workers & Pages → D1 → 创建数据库 → 绑定到 Worker,变量名填 DB - // 省略 database_id 时,wrangler >= 4.45.0 会自动创建同名库并回写 - // + // ▶ 方案 B:D1(关系型,可与 Go 后端共享库) // "d1_databases": [ // { "binding": "DB", "database_name": "openlist", "database_id": "" } // ], // - // ▶ 方案 C:Durable Objects(内置 SQLite,强一致;DB_FORMAT 建议改为 "sql") + // ▶ 方案 C:Durable Objects(内置 SQLite,强一致) // 注意:DO 需要 migrations,且删除成本高,适合单租户部署。 // // "durable_objects": { @@ -185,21 +127,7 @@ // { "tag": "v1", "new_sqlite_classes": ["OpenListDB"] } // ], // - // ▶ 方案 D:Blob / MySQL - // Blob 由 EdgeOne / ESA 自动注入,无需配置; - // MySQL 通过 Secret 配置 MYSQL_URLS(或 MYSQL_HOST 等分项)。 - // - // ▶ 方案 E:R2 对象存储(作为「存储驱动」,与上面的 DB 绑定无关) - // 后台「存储 → 新增 → R2」里把 bucket_binding 填成与下面 binding 相同的名字, - // 即可走 Workers 原生绑定读写(无需 API Token,推荐)。 - // 不配 binding 时该驱动自动回退 S3 兼容 API:在存储配置里填 - // account_id + access_key_id / secret_access_key 即可(R2 控制台 → - // Manage R2 API Tokens)。两者共用同一套目录语义。 - // 绑定名可自定义,未在表单填写时按 R2 → R2_BUCKET → CF_R2 → OPENLIST_R2 顺序探测。 - // - // 注意:binding 模式下载走 Worker 流式转发(R2 绑定不提供预签名 URL); - // 想让浏览器直连,可在存储配置里填 custom_host(r2.dev 公开域名或自定义域名)。 - // + // ▶ 方案 D:R2 对象存储(作为「存储驱动」,与上面的 DB 绑定无关) // "r2_buckets": [ // { "binding": "R2", "bucket_name": "your-bucket" } // ], From fd1c4e019a3cd5de12c917b2b5e0389f45d8b3e3 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:32:48 +0800 Subject: [PATCH 41/49] =?UTF-8?q?fix(edgeone):=20=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E4=BD=93=E8=AF=BB=E4=B8=8D=E5=88=B0=E6=97=B6=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=20503=20=E8=80=8C=E4=B8=8D=E6=98=AF=20400=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=9C=A8=E5=85=A5=E5=8F=A3=E9=A2=84=E8=AF=BB=20JSON=20body=20(?= =?UTF-8?q?#170)=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EdgeOne Node 云函数在平台侧提前消费请求体,实测 30%~45% 的 JSON POST 在 c.req.text() 处失败(GET 不受影响;同代码部署 Cloudflare Workers 0 失败, 见 #112 的对照实验)。#139 引入的 jsonBodyGuard 把这种平台故障一律判成 400,前端不会重试,于是偶发丢包变成用户可见的硬报错(issue #170)。 - router: 严格区分三类结果。「读不到 body」(bodyUsed / 读取抛异常 / Content-Length 与实际长度不符)返回 503 + Retry-After 与 X-OpenList-Retry: body:这是服务端暂时不可用,且 handler 尚未执行, 客户端重试不会产生重复副作用;真正的空 body / 非法 JSON 仍是 400, /api/fs、/api/share 的严格语义不变(仍是 400,不退化成 #112 的静默根目录)。 - router: 补结构化 warn 日志(reason / declared / hasBody / bodyUsedBeforeRead), 这是把结论交给腾讯云之前唯一还缺的现场证据。读取前必须先取快照, 否则读过之后 bodyUsed 恒为 true,日志会失真。 - router: 严格路径判定抽成 isStrictBodyPath,改为精确匹配 + 尾斜杠, 不再用 startsWith("/api/fs") 误伤 /api/fsx。 - api/_makers: 在 onRequest 最前面(任何 await 之前)预读 <=1MB 的 /api JSON body 并重建 Request,把与平台 drain 的竞态窗口压到接近 0; 读不到时原样下交,由 guard 统一返回 503 并记日志。 - 新增 json_body_guard.test.ts:9 个用例覆盖 503 / 400 / 放行三条分支。 未采用 PR #158 的「非严格路径 next() 放行」:Hono 的 bodyCache 里留下的是 同一个 rejected Promise,放行只是把错误推进 handler,且会让 /api/admin/* 这类写接口在空 body 下按默认值执行。 验证:npm run test:server 229 通过;tsc --noEmit 通过;build:edge 通过。 Co-authored-by: pikachuren --- api/_makers.ts | 60 ++++++- src/backend/server/json_body_guard.test.ts | 170 ++++++++++++++++++++ src/backend/server/router.ts | 177 +++++++++++++++------ 3 files changed, 356 insertions(+), 51 deletions(-) create mode 100644 src/backend/server/json_body_guard.test.ts diff --git a/api/_makers.ts b/api/_makers.ts index a98f0e48..93214b8c 100644 --- a/api/_makers.ts +++ b/api/_makers.ts @@ -6,8 +6,64 @@ import INDEX_HTML from "../dist/index.html" // 注入后由函数直接返回 SPA 壳(配合根级 middleware.js 的边缘改写双保险)。 setSpaFallbackHtml(INDEX_HTML) -export function onRequest(context: any) { - return app.fetch(context.request, context.env, context) +/** + * 预读上限:超过该字节数的 JSON body 不预读,避免把大请求整体驻留内存。 + * OpenList 的 JSON 接口都是小请求(< 几十 KB),1 MB 足够覆盖。 + */ +const PREFETCH_MAX_BYTES = 1 << 20 + +/** + * EdgeOne Node 云函数会把请求体的底层流在**平台侧**提前消费(issue #170: + * 实测 30%~45% 的 POST 读不到 body,GET 不受影响,同代码在 Cloudflare + * Workers 上 0 失败)。平台是在函数执行期间异步 drain 的,所以读得越晚 + * 越容易撞上——而本项目在 Hono 读取 body 之前还有存储配置检查等 await。 + * + * 这里在 onRequest 入口、**任何 await 之前**就把小的 JSON body 读掉, + * 并用读到的文本重建 Request,把竞态窗口压到接近 0: + * - 读到 → 重建 Request(后续 c.req.text() 拿到的是确定的字符串); + * - 读不到 → 原样交给下游 jsonBodyGuard,由它返回 503 + 结构化日志。 + * 条件收得很窄(仅 POST/PUT/PATCH 的 /api JSON 小请求),不影响上传 + * (multipart 走云盘存储,不经过这里)与流式响应。 + */ +async function prefetchJsonBody(request: Request): Promise { + if (!(request instanceof Request)) return request + + const method = request.method.toUpperCase() + if (method === "GET" || method === "HEAD" || method === "OPTIONS") { + return request + } + + const contentType = (request.headers.get("content-type") || "").toLowerCase() + if (!contentType.includes("application/json")) return request + + let pathname = "" + try { + pathname = new URL(request.url).pathname + } catch { + return request + } + if (!pathname.startsWith("/api")) return request + + const declared = Number(request.headers.get("content-length") ?? -1) + if (!(declared > 0) || declared > PREFETCH_MAX_BYTES) return request + + try { + const text = await request.text() + return new Request(request.url, { + method, + headers: request.headers, + body: text, + }) + } catch { + // 读失败:流可能已被平台 drain。保留原 Request 让 jsonBodyGuard + // 走 503 分支并留下诊断日志(它会再读一次并捕获真实原因)。 + return request + } +} + +export async function onRequest(context: any) { + const request = await prefetchJsonBody(context.request) + return app.fetch(request, context.env, context) } export default onRequest diff --git a/src/backend/server/json_body_guard.test.ts b/src/backend/server/json_body_guard.test.ts new file mode 100644 index 00000000..e434a51f --- /dev/null +++ b/src/backend/server/json_body_guard.test.ts @@ -0,0 +1,170 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Hono } from "hono" +import { setupRouter } from "./router" + +/** + * jsonBodyGuard 回归测试(issue #170)。 + * + * EdgeOne Node 云函数会把请求体的底层流在平台侧提前消费,实测 30%~45% 的 + * JSON POST 读不到 body。这里钉死三条长期必须成立的语义: + * + * 1. 「读不到 body」是**服务端暂时不可用**(503 + 可重试信号),不是 + * 客户端请求非法(400)——否则前端不重试,40% 的丢包直接变成 40% 的 + * 用户可见报错(issue #170 的原症状); + * 2. 无论读到还是读不到 body,都**不能**放行到 handler 去"再读一次"—— + * Hono 的 bodyCache 里留下的是同一个 rejected Promise,二次读取只会 + * 把错误变成 500 或静默的根目录(issue #112); + * 3. 真正由客户端造成的「空 body / 非法 JSON」仍然必须是 400,且严格 + * 路径的判定不能靠 `startsWith("/api/fs")` 这类前缀误伤 `/api/fsx`。 + */ + +const RETRY_MESSAGE = "request body unavailable, please retry" + +function buildApp() { + const api = new Hono() + setupRouter(api) + const app = new Hono() + app.route("/api", api) + return app +} + +/** 测试用 env:本文件只关心守卫分支,业务中间件拿不到配置即可。 */ +const env = {} as any + +function jsonPost( + path: string, + body: BodyInit | null, + headers: Record = {}, +) { + return new Request(`http://localhost${path}`, { + method: "POST", + headers: { "content-type": "application/json", ...headers }, + body, + }) +} + +async function envelope(res: Response): Promise<{ message?: string }> { + try { + return (await res.json()) as { message?: string } + } catch { + return {} + } +} + +test("平台已提前消费请求体(bodyUsed)→ 503 + 可重试信号,而不是 400", async () => { + const request = jsonPost( + "/api/auth/login", + JSON.stringify({ username: "u", password: "p" }), + ) + // 模拟 EdgeOne 平台在函数执行前把流读掉 + await request.text() + assert.equal(request.bodyUsed, true) + + const res = await buildApp().request(request, undefined, env) + + assert.equal(res.status, 503) + assert.equal(res.headers.get("x-openlist-retry"), "body") + assert.equal(res.headers.get("retry-after"), "1") + assert.equal((await envelope(res)).message, RETRY_MESSAGE) +}) + +test("读取请求体抛异常(terminated)→ 503,不放行到 handler", async () => { + const broken = new ReadableStream({ + pull(controller) { + controller.error(new Error("terminated")) + }, + }) + const request = new Request("http://localhost/api/fs/list", { + method: "POST", + headers: { "content-type": "application/json" }, + body: broken, + // Node 的 undici 对流式 body 要求显式声明半双工 + duplex: "half", + } as any) + + const res = await buildApp().request(request, undefined, env) + + assert.equal(res.status, 503) + assert.equal((await envelope(res)).message, RETRY_MESSAGE) +}) + +test("Content-Length 与实际读到长度不符(截断)→ 503", async () => { + // 声明 100 字节,实际只有 12 字节:平台提前 EOF 的特征 + const request = jsonPost("/api/fs/list", JSON.stringify({ path: "/" }), { + "content-length": "100", + }) + + const res = await buildApp().request(request, undefined, env) + + assert.equal(res.status, 503) + assert.equal((await envelope(res)).message, RETRY_MESSAGE) +}) + +test("读到完整 body 后,非法 JSON 仍然是 400(客户端问题)", async () => { + const res = await buildApp().request( + jsonPost("/api/auth/login", "not-a-json"), + undefined, + env, + ) + + assert.equal(res.status, 400) + assert.equal((await envelope(res)).message, "invalid JSON request body") +}) + +test("严格路径(/api/fs/*)空 body 仍然是 400", async () => { + const res = await buildApp().request( + jsonPost("/api/fs/list", null), + undefined, + env, + ) + + assert.equal(res.status, 400) + assert.equal((await envelope(res)).message, "request body required") +}) + +test("严格路径判定不再前缀误伤 /api/fsx/*", async () => { + const res = await buildApp().request( + jsonPost("/api/fsx/list", null), + undefined, + env, + ) + + assert.notEqual(res.status, 503) + assert.notEqual((await envelope(res)).message, "request body required") +}) + +test("非严格路径空 body 仍放行(兼容无 body 调用)", async () => { + const res = await buildApp().request( + jsonPost("/api/auth/login", null), + undefined, + env, + ) + + assert.notEqual(res.status, 503) + assert.notEqual((await envelope(res)).message, "request body required") +}) + +test("正常 JSON body 不会被守卫拦截", async () => { + const res = await buildApp().request( + jsonPost( + "/api/auth/login", + JSON.stringify({ username: "u", password: "p" }), + ), + undefined, + env, + ) + + assert.notEqual(res.status, 503) + assert.notEqual((await envelope(res)).message, RETRY_MESSAGE) +}) + +test("GET 请求不读 body(平台丢包只影响写请求)", async () => { + const res = await buildApp().request( + "http://localhost/api/public/settings", + undefined, + env, + ) + + assert.notEqual(res.status, 503) +}) diff --git a/src/backend/server/router.ts b/src/backend/server/router.ts index 567bd558..cd69e7d0 100644 --- a/src/backend/server/router.ts +++ b/src/backend/server/router.ts @@ -126,19 +126,85 @@ async function rateLimitMiddleware(c: any, next: () => Promise) { } } -// JSON 请求体校验中间件 (添加日期: 2026-10-20) -// 背景:EdgeOne Node 运行时偶发丢失/截断请求体,各 handler 的 -// `c.req.json().catch(() => ({}))` 会把解析失败静默吞掉,随后 +// JSON 请求体校验中间件 (添加日期: 2026-10-20;2026-11-09 按 issue #170 修订) +// +// 背景:EdgeOne Node 云函数运行时会把请求体在**平台层**提前消费掉, +// 表现为对 POST 请求 `c.req.text()` 时好时坏:有的直接抛异常,有的读到 +// 空串(实测失败率 30%~45%,与传输编码无关,GET 不受影响,同代码在 +// Cloudflare Workers 上 0 失败)。各 handler 的 +// `c.req.json().catch(() => ({}))` 会把它静默吞掉,随后 // `body.path || "/"` 降级为根目录,导致"访问子目录返回根目录内容" -// (issue #112)。此处把空 body / 非法 JSON 显式转为 400,让前端 -// 立刻看到错误并可重试,而不是收到错误的成功响应。 -// - /api/fs/* 与 /api/share/*(POST/PUT/PATCH/DELETE): -// 空 body 或非法 JSON 一律 400(对齐 Go 版 OpenList 必填 path); -// - 其余 /api/*:空 body 放行(兼容无 body 调用),非空但非法 → 400; -// - 跳过:非 JSON content-type(multipart 上传等)、/api/task/* -// (EdgeOne Schedules 定时调度)与非 /api 路径。 +// (issue #112)。因此这里在 handler 之前预读校验。 +// +// 三类结果的语义必须严格区分: +// - **读不到 body**(平台故障:bodyUsed / 读取抛异常 / Content-Length +// 与实际长度不符)→ **503 + Retry-After**。这是服务端暂时不可用,不是 +// 客户端的请求写错了;而且此时 handler 尚未执行,客户端重试不会产生 +// 重复副作用。此前一律回 400,既误导用户又让前端不重试,把 40% 的 +// 丢包放大成 40% 的硬报错(issue #170 的直接症状)。 +// - **读到了但为空 / 非法 JSON** → 400(真正的客户端问题); +// /api/fs/* 与 /api/share/* 空 body 仍是 400(对齐 Go 版必填 path)。 +// - 其余 /api/* 空 body 放行(兼容无 body 调用)。 +// 跳过:非 JSON content-type(multipart 上传等)、/api/task/* +// (EdgeOne Schedules 定时调度,payload 由平台注入)与非 /api 路径。 +// // Hono 会缓存已读取的 body 文本(bodyCache.text),此处预读后 -// handler 里的 c.req.json() 复用缓存,不会二次消费请求流。 +// handler 里的 c.req.json() 复用缓存,不会二次消费请求流;反过来, +// 读取失败时缓存里留下的是 rejected Promise,所以**绝不能**放行到 +// handler 去"再试一次"——只能返回 503 让客户端重发新请求。 +const BODY_RETRY_MESSAGE = "request body unavailable, please retry" + +/** 空 body 会被当成"根目录/空对象"执行业务的路径,需要严格校验。 */ +function isStrictBodyPath(path: string): boolean { + return ( + path === "/api/fs" || + path.startsWith("/api/fs/") || + path === "/api/share" || + path.startsWith("/api/share/") + ) +} + +type BodyReadResult = + | { ok: true; text: string } + | { ok: false; reason: string; declared: number } + +/** + * 预读 JSON body,并把"读不到"(平台故障)与"读到了"彻底分开。 + * 只有读到确定的文本才交给后续的合法性校验,避免把平台故障误判成 400。 + */ +async function readJsonBody(c: Context): Promise { + const declared = Number(c.req.header("content-length") ?? -1) + + // 平台/上游已经把流读掉了:再读只会抛 "Body is unusable"。 + if (c.req.raw.bodyUsed) { + return { ok: false, reason: "body-used", declared } + } + + let text: string + try { + text = await c.req.text() + } catch (err) { + const message = (err as Error)?.message ?? String(err) + return { ok: false, reason: `read-failed:${message}`, declared } + } + + // Content-Length 声明了字节数却读到更少 = 流被提前 EOF(截断)。 + // 有 content-encoding 时长度不可比(读到的可能是解压后文本),跳过该判定。 + // UTF-8 字节数恒 >= JS 字符串长度,故 declared <= text.length 时不可能被截断, + // 免掉常见 ASCII 请求上的一次全量编码。 + const encoding = ( + c.req.header("content-encoding") || "identity" + ).toLowerCase() + if (declared > 0 && encoding === "identity" && declared > text.length) { + const actual = text ? new TextEncoder().encode(text).byteLength : 0 + if (actual < declared) { + return { ok: false, reason: `truncated:${actual}/${declared}`, declared } + } + } + + return { ok: true, text } +} + async function jsonBodyGuard(c: Context, next: () => Promise) { const path = c.req.path const method = c.req.method.toUpperCase() @@ -147,51 +213,64 @@ async function jsonBodyGuard(c: Context, next: () => Promise) { .toLowerCase() .includes("application/json") - if (isMutable && isJson && path.startsWith("/api")) { - // 显式跳过的路径:/api/task/*(EdgeOne Schedules 定时调度通道)。 - // 其 payload 由平台注入、鉴权走 matchCronSecret,语义独立于业务 - // 请求且历史上 body 可缺省;body 丢弃时也无需让调度通道可自愈, - // 保持宽松。其余端点一律校验。 - if (path.startsWith("/api/task/")) { - return next() - } + if (!isMutable || !isJson || !path.startsWith("/api")) { + return next() + } - let text: string - try { - text = await c.req.text() - } catch { - return c.json( - { code: 400, message: "failed to read request body", data: null }, - 400, - ) - } + // 显式跳过的路径:/api/task/*(EdgeOne Schedules 定时调度通道)。 + // 其 payload 由平台注入、鉴权走 matchCronSecret,语义独立于业务 + // 请求且历史上 body 可缺省;body 丢弃时也无需让调度通道可自愈, + // 保持宽松。其余端点一律校验。 + if (path.startsWith("/api/task/")) { + return next() + } - if (text.trim() === "") { - const strict = path.startsWith("/api/fs") || path.startsWith("/api/share") - if (strict) { - return c.json( - { code: 400, message: "request body required", data: null }, - 400, - ) - } - // 空 body 放行(保持向后兼容),但管理端空 body 属于可疑行为 - // (如 cache/clear 空 body 会清空全部缓存),显式记日志便于定位。 - if (path.startsWith("/api/admin")) { - console.warn( - `[jsonBodyGuard] empty JSON body passed to ${method} ${path}`, - ) - } - return next() - } + // 诊断快照必须在读取之前取:读过之后 bodyUsed 恒为 true,日志会失真。 + const hadBody = !!c.req.raw.body + const wasUsedBeforeRead = c.req.raw.bodyUsed - try { - JSON.parse(text) - } catch { + const body = await readJsonBody(c) + if (!body.ok) { + // 平台丢包/截断:留一条可上报腾讯云的结构化证据(body 是否存在、 + // 声明长度与实际长度是否一致、读取前是否已被消费),否则永远只能"推测"。 + console.warn( + `[jsonBodyGuard] body unavailable (${body.reason}) ${method} ${path} ` + + `declared=${body.declared} hasBody=${hadBody} bodyUsedBeforeRead=${wasUsedBeforeRead}`, + ) + return c.json( + { code: 503, message: BODY_RETRY_MESSAGE, data: null }, + 503, + // Retry-After 让网关/客户端知道这是暂时性故障;自定义头给前端 + // 一个明确的"可以安全重试"信号(handler 未执行,无副作用)。 + { "Retry-After": "1", "X-OpenList-Retry": "body" }, + ) + } + + const text = body.text + if (text.trim() === "") { + if (isStrictBodyPath(path)) { return c.json( - { code: 400, message: "invalid JSON request body", data: null }, + { code: 400, message: "request body required", data: null }, 400, ) } + // 空 body 放行(保持向后兼容),但管理端空 body 属于可疑行为 + // (如 cache/clear 空 body 会清空全部缓存),显式记日志便于定位。 + if (path.startsWith("/api/admin")) { + console.warn( + `[jsonBodyGuard] empty JSON body passed to ${method} ${path}`, + ) + } + return next() + } + + try { + JSON.parse(text) + } catch { + return c.json( + { code: 400, message: "invalid JSON request body", data: null }, + 400, + ) } return next() From 83451aeb5a44d4b8edb60833afcede911a2abafa Mon Sep 17 00:00:00 2001 From: Pikachu Bot Date: Mon, 9 Nov 2026 17:33:22 +0800 Subject: [PATCH 42/49] refactor(driver): unify put() content type to Uint8Array (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 #171 评审的 P2:驱动里的 put() 仍普遍声明 content: Buffer。TS 方法参数 是双变的,编译器不会报错,但运行时实参早已是 Uint8Array —— 继续使用 Buffer 专有方法的实现会在 ESA(无全局 Buffer)上静默产出错误数据或直接抛错。 - 75 处 content: Buffer / Buffer | Uint8Array 统一为 Uint8Array - url_tree: content.toString("utf8") → TextDecoder - github putBlob: Buffer.from(content).toString("base64") → 纯 JS bytesToBase64 - server/fs.ts 解压写回:去掉多余的 Buffer.from(content) - 189pc calcMD5/calcSHA1 参数放宽为 Uint8Array - StorageDriver.put 注释写明实现方不得使用 Buffer 专有方法 验证:tsc --noEmit 0 error;drivers 197/197、server 206/206、pkg+model 89/89。 Co-authored-by: pikachuren --- src/backend/drivers/115_share/driver.ts | 2 +- src/backend/drivers/115open/driver.ts | 4 ++-- src/backend/drivers/123_share/driver.ts | 2 +- src/backend/drivers/123pan/driver.ts | 4 ++-- src/backend/drivers/123pan/util.ts | 2 +- src/backend/drivers/139/driver.ts | 2 +- src/backend/drivers/189/driver.ts | 4 ++-- src/backend/drivers/189_tv/driver.ts | 2 +- src/backend/drivers/189pc/driver.ts | 2 +- src/backend/drivers/189pc/util.ts | 7 +++++-- src/backend/drivers/alias/driver.ts | 2 +- src/backend/drivers/alidoc/driver.ts | 2 +- src/backend/drivers/alist_v3/driver.ts | 2 +- src/backend/drivers/aliyundrive_open/driver.ts | 2 +- src/backend/drivers/aliyundrive_open/util.ts | 2 +- src/backend/drivers/aliyundrive_share/driver.ts | 2 +- src/backend/drivers/autoindex/driver.ts | 2 +- src/backend/drivers/azure_blob/driver.ts | 2 +- src/backend/drivers/baidu_netdisk/driver.ts | 2 +- src/backend/drivers/baidu_photo/driver.ts | 2 +- src/backend/drivers/bunny_storage/driver.ts | 2 +- src/backend/drivers/chaoxing/driver.ts | 2 +- src/backend/drivers/chunk/driver.ts | 2 +- src/backend/drivers/cloudflare_imgbed/driver.ts | 2 +- src/backend/drivers/cloudreve_v4/driver.ts | 2 +- src/backend/drivers/crypt/driver.ts | 2 +- src/backend/drivers/ctfile/driver.ts | 2 +- src/backend/drivers/dropbox/driver.ts | 2 +- src/backend/drivers/emby/driver.ts | 2 +- src/backend/drivers/ftp/driver.ts | 2 +- src/backend/drivers/ftp/ftp-client.ts | 2 +- src/backend/drivers/github/driver.ts | 2 +- src/backend/drivers/github/util.ts | 2 +- src/backend/drivers/google_drive/driver.ts | 2 +- src/backend/drivers/google_drive/util.ts | 2 +- src/backend/drivers/guangyapan/driver.ts | 2 +- src/backend/drivers/ipfs_api/driver.ts | 2 +- src/backend/drivers/ipfs_api/util.ts | 2 +- src/backend/drivers/kodbox/driver.ts | 2 +- src/backend/drivers/kodbox/util.ts | 2 +- src/backend/drivers/local.ts | 2 +- src/backend/drivers/mediafire/driver.ts | 2 +- src/backend/drivers/mediatrack/driver.ts | 2 +- src/backend/drivers/mega/driver.ts | 2 +- src/backend/drivers/misskey/driver.ts | 2 +- src/backend/drivers/mopan/driver.ts | 2 +- src/backend/drivers/onedrive/driver.ts | 2 +- src/backend/drivers/onedrive_app/driver.ts | 2 +- src/backend/drivers/onedrive_sharelink/driver.ts | 2 +- src/backend/drivers/openlist/driver.ts | 2 +- src/backend/drivers/pikpak/driver.ts | 2 +- src/backend/drivers/pikpak_share/driver.ts | 2 +- src/backend/drivers/proton_drive/driver.ts | 2 +- src/backend/drivers/quark/driver.ts | 2 +- src/backend/drivers/r2/driver.ts | 2 +- src/backend/drivers/s3/driver.ts | 2 +- src/backend/drivers/seafile/driver.ts | 2 +- src/backend/drivers/sftp/driver.ts | 2 +- src/backend/drivers/sftp/util.ts | 2 +- src/backend/drivers/smb/driver.ts | 2 +- src/backend/drivers/teldrive/driver.ts | 2 +- src/backend/drivers/terabox/driver.ts | 2 +- src/backend/drivers/thunder/driver.ts | 2 +- src/backend/drivers/uss/driver.ts | 2 +- src/backend/drivers/webdav/driver.ts | 2 +- src/backend/drivers/weiyun/driver.ts | 4 ++-- src/backend/drivers/weiyun/util.ts | 2 +- src/backend/drivers/wopan/driver.ts | 2 +- src/backend/drivers/wps/driver.ts | 2 +- src/backend/drivers/yandex/driver.ts | 2 +- src/backend/server/fs.ts | 1 + 71 files changed, 79 insertions(+), 75 deletions(-) diff --git a/src/backend/drivers/115_share/driver.ts b/src/backend/drivers/115_share/driver.ts index ed63fb38..94db3aa6 100644 --- a/src/backend/drivers/115_share/driver.ts +++ b/src/backend/drivers/115_share/driver.ts @@ -208,7 +208,7 @@ export class Pan115ShareDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { throw new Error("115 Share is read-only") } diff --git a/src/backend/drivers/115open/driver.ts b/src/backend/drivers/115open/driver.ts index 9d5ba2d4..6042190e 100644 --- a/src/backend/drivers/115open/driver.ts +++ b/src/backend/drivers/115open/driver.ts @@ -473,7 +473,7 @@ export class Pan115Driver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { if (content.length < 1) { throw new Error("115 网盘不允许上传空文件") @@ -545,7 +545,7 @@ export class Pan115Driver implements StorageDriver { SecurityToken: string }, initResp: { bucket: string; object: string; callback: any }, - content: Buffer, + content: Uint8Array, ): Promise { const endpoint = token.endpoint.startsWith("http") ? token.endpoint diff --git a/src/backend/drivers/123_share/driver.ts b/src/backend/drivers/123_share/driver.ts index 9264f19f..e66de581 100644 --- a/src/backend/drivers/123_share/driver.ts +++ b/src/backend/drivers/123_share/driver.ts @@ -185,7 +185,7 @@ export class Pan123ShareDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { throw new Error("123Pan Share is read-only") } diff --git a/src/backend/drivers/123pan/driver.ts b/src/backend/drivers/123pan/driver.ts index 1024c8df..3184cb91 100644 --- a/src/backend/drivers/123pan/driver.ts +++ b/src/backend/drivers/123pan/driver.ts @@ -391,7 +391,7 @@ export class Pan123Driver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { this.budget.used = 0 const segs = String(physicalPath || "") @@ -467,7 +467,7 @@ export class Pan123Driver implements StorageDriver { async uploadPart( session: string, partNumber: number, - content: Buffer, + content: Uint8Array, ): Promise { this.budget.used = 0 const s = decodeSession(session) diff --git a/src/backend/drivers/123pan/util.ts b/src/backend/drivers/123pan/util.ts index dd39a018..8e4d700d 100644 --- a/src/backend/drivers/123pan/util.ts +++ b/src/backend/drivers/123pan/util.ts @@ -650,7 +650,7 @@ export class Pan123Client { public async uploadFile( parentId: string, fileName: string, - content: Buffer, + content: Uint8Array, ): Promise { let etag = "" try { diff --git a/src/backend/drivers/139/driver.ts b/src/backend/drivers/139/driver.ts index cf38306f..92418937 100644 --- a/src/backend/drivers/139/driver.ts +++ b/src/backend/drivers/139/driver.ts @@ -279,7 +279,7 @@ export class Yun139Driver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { console.warn(`[139] put for ${physicalPath}`) } diff --git a/src/backend/drivers/189/driver.ts b/src/backend/drivers/189/driver.ts index 49a2c37b..01b142a0 100644 --- a/src/backend/drivers/189/driver.ts +++ b/src/backend/drivers/189/driver.ts @@ -369,7 +369,7 @@ export class Cloud189Driver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = String(physicalPath || "") .split("/") @@ -469,7 +469,7 @@ export class Cloud189Driver implements StorageDriver { async uploadPart( sessionToken: string, partNumber: number, - content: Buffer, + content: Uint8Array, ): Promise<{ partMd5: string }> { const session = decodeUploadSession(sessionToken) if ( diff --git a/src/backend/drivers/189_tv/driver.ts b/src/backend/drivers/189_tv/driver.ts index 3e00811f..248b7b02 100644 --- a/src/backend/drivers/189_tv/driver.ts +++ b/src/backend/drivers/189_tv/driver.ts @@ -207,7 +207,7 @@ export class Driver189TV implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts.pop() || "file" diff --git a/src/backend/drivers/189pc/driver.ts b/src/backend/drivers/189pc/driver.ts index 688a780a..43ce69ee 100644 --- a/src/backend/drivers/189pc/driver.ts +++ b/src/backend/drivers/189pc/driver.ts @@ -319,7 +319,7 @@ export class Cloud189PCDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const pathParts = physicalPath.split("/").filter(Boolean) const fileName = pathParts.pop() || "upload" diff --git a/src/backend/drivers/189pc/util.ts b/src/backend/drivers/189pc/util.ts index f650f3e2..8736f070 100644 --- a/src/backend/drivers/189pc/util.ts +++ b/src/backend/drivers/189pc/util.ts @@ -161,10 +161,13 @@ export class Cloud189PCClient { } } -export function calcMD5(buffer: Buffer): string { +// 入参统一为 Uint8Array:StorageDriver.put 的内容契约不再假设全局 Buffer +// (ESA 边缘运行时没有 Buffer)。这两个函数本身仍依赖 node:crypto, +// 只在天翼云驱动可用的 Node 运行时执行。 +export function calcMD5(buffer: Uint8Array): string { return createHash("md5").update(buffer).digest("hex").toUpperCase() } -export function calcSHA1(buffer: Buffer): string { +export function calcSHA1(buffer: Uint8Array): string { return createHash("sha1").update(buffer).digest("hex").toUpperCase() } diff --git a/src/backend/drivers/alias/driver.ts b/src/backend/drivers/alias/driver.ts index 1aa35068..e938be78 100644 --- a/src/backend/drivers/alias/driver.ts +++ b/src/backend/drivers/alias/driver.ts @@ -298,7 +298,7 @@ export class AliasDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const targets = this.getTargetsForPath(physicalPath) if (targets.length === 0) { diff --git a/src/backend/drivers/alidoc/driver.ts b/src/backend/drivers/alidoc/driver.ts index c83b2df9..773daf9c 100644 --- a/src/backend/drivers/alidoc/driver.ts +++ b/src/backend/drivers/alidoc/driver.ts @@ -225,7 +225,7 @@ export class AliDocDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { throw new Error("AliDoc does not support upload") } diff --git a/src/backend/drivers/alist_v3/driver.ts b/src/backend/drivers/alist_v3/driver.ts index ac4a3c8b..11fa1ced 100644 --- a/src/backend/drivers/alist_v3/driver.ts +++ b/src/backend/drivers/alist_v3/driver.ts @@ -193,7 +193,7 @@ export class AListV3Driver implements StorageDriver { }) } - async put(_v: string, physicalPath: string, content: Buffer): Promise { + async put(_v: string, physicalPath: string, content: Uint8Array): Promise { const res = await fetch(`${this.baseUrl}/api/fs/put`, { method: "PUT", headers: { diff --git a/src/backend/drivers/aliyundrive_open/driver.ts b/src/backend/drivers/aliyundrive_open/driver.ts index 319cb75a..af84ce0e 100644 --- a/src/backend/drivers/aliyundrive_open/driver.ts +++ b/src/backend/drivers/aliyundrive_open/driver.ts @@ -189,7 +189,7 @@ export class AliyundriveOpen implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts.pop() || "upload" diff --git a/src/backend/drivers/aliyundrive_open/util.ts b/src/backend/drivers/aliyundrive_open/util.ts index 98e90516..e3c365fe 100644 --- a/src/backend/drivers/aliyundrive_open/util.ts +++ b/src/backend/drivers/aliyundrive_open/util.ts @@ -371,7 +371,7 @@ export class AliyunOpenClient { public async putFile( parentFileId: string, filename: string, - content: Buffer, + content: Uint8Array, ): Promise { const size = content.length const createResp = await this.openApiRequest("/openFile/create", { diff --git a/src/backend/drivers/aliyundrive_share/driver.ts b/src/backend/drivers/aliyundrive_share/driver.ts index 9861a500..f23cc19a 100644 --- a/src/backend/drivers/aliyundrive_share/driver.ts +++ b/src/backend/drivers/aliyundrive_share/driver.ts @@ -193,7 +193,7 @@ export class AliyundriveShareDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { throw new Error("Aliyundrive Share is read-only") } diff --git a/src/backend/drivers/autoindex/driver.ts b/src/backend/drivers/autoindex/driver.ts index c05185d5..0f9bf0c7 100644 --- a/src/backend/drivers/autoindex/driver.ts +++ b/src/backend/drivers/autoindex/driver.ts @@ -177,7 +177,7 @@ export class AutoIndexDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { throw new Error("AutoIndex is read-only") } diff --git a/src/backend/drivers/azure_blob/driver.ts b/src/backend/drivers/azure_blob/driver.ts index 405a3938..dcfd70ae 100644 --- a/src/backend/drivers/azure_blob/driver.ts +++ b/src/backend/drivers/azure_blob/driver.ts @@ -323,7 +323,7 @@ export class AzureBlobDriver implements StorageDriver { await this.copyBlob(srcKey, dstKey) } - async put(_v: string, physicalPath: string, content: Buffer): Promise { + async put(_v: string, physicalPath: string, content: Uint8Array): Promise { const key = this.getKey(physicalPath) const res = await this.request("PUT", key, "", { body: new Uint8Array(content), diff --git a/src/backend/drivers/baidu_netdisk/driver.ts b/src/backend/drivers/baidu_netdisk/driver.ts index d561a883..b4f8a430 100644 --- a/src/backend/drivers/baidu_netdisk/driver.ts +++ b/src/backend/drivers/baidu_netdisk/driver.ts @@ -266,7 +266,7 @@ export class BaiduDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { // 百度网盘不允许上传空文件 if (content.length < 1) { diff --git a/src/backend/drivers/baidu_photo/driver.ts b/src/backend/drivers/baidu_photo/driver.ts index a675e889..6240a695 100644 --- a/src/backend/drivers/baidu_photo/driver.ts +++ b/src/backend/drivers/baidu_photo/driver.ts @@ -321,7 +321,7 @@ export class DriverBaiduPhoto implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts.pop() || "file" diff --git a/src/backend/drivers/bunny_storage/driver.ts b/src/backend/drivers/bunny_storage/driver.ts index 3c581de8..fb6fa3fb 100644 --- a/src/backend/drivers/bunny_storage/driver.ts +++ b/src/backend/drivers/bunny_storage/driver.ts @@ -200,7 +200,7 @@ export class BunnyStorageDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = cleanObjectPath(physicalPath) const url = storageURL(this.addition, clean, false) diff --git a/src/backend/drivers/chaoxing/driver.ts b/src/backend/drivers/chaoxing/driver.ts index 39f8551f..39f2822e 100644 --- a/src/backend/drivers/chaoxing/driver.ts +++ b/src/backend/drivers/chaoxing/driver.ts @@ -160,7 +160,7 @@ export class DriverChaoXing implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts.pop() || "file" diff --git a/src/backend/drivers/chunk/driver.ts b/src/backend/drivers/chunk/driver.ts index fc2a2cbf..c9d7554b 100644 --- a/src/backend/drivers/chunk/driver.ts +++ b/src/backend/drivers/chunk/driver.ts @@ -279,7 +279,7 @@ export class ChunkDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { this.ensureReady() const size = content.length diff --git a/src/backend/drivers/cloudflare_imgbed/driver.ts b/src/backend/drivers/cloudflare_imgbed/driver.ts index ddc78b0c..fd20e894 100644 --- a/src/backend/drivers/cloudflare_imgbed/driver.ts +++ b/src/backend/drivers/cloudflare_imgbed/driver.ts @@ -187,7 +187,7 @@ export class CFImgBedDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const parentPath = clean.split("/").slice(0, -1).join("/") || "/" diff --git a/src/backend/drivers/cloudreve_v4/driver.ts b/src/backend/drivers/cloudreve_v4/driver.ts index edf8c800..f0bd689a 100644 --- a/src/backend/drivers/cloudreve_v4/driver.ts +++ b/src/backend/drivers/cloudreve_v4/driver.ts @@ -135,7 +135,7 @@ export class DriverCloudreve implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error("[Cloudreve] Direct put not supported in stateless environment") } diff --git a/src/backend/drivers/crypt/driver.ts b/src/backend/drivers/crypt/driver.ts index cff46245..a4793a38 100644 --- a/src/backend/drivers/crypt/driver.ts +++ b/src/backend/drivers/crypt/driver.ts @@ -199,7 +199,7 @@ export class CryptDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { this.ensureReady() const remotePhysical = this.joinRemote(physicalPath) + this.suffix diff --git a/src/backend/drivers/ctfile/driver.ts b/src/backend/drivers/ctfile/driver.ts index 42c91efb..cf0481ef 100644 --- a/src/backend/drivers/ctfile/driver.ts +++ b/src/backend/drivers/ctfile/driver.ts @@ -304,7 +304,7 @@ export class CtFileDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const segs = splitPath(physicalPath) if (segs.length === 0) throw new Error("[CtFile] 路径无效") diff --git a/src/backend/drivers/dropbox/driver.ts b/src/backend/drivers/dropbox/driver.ts index 4aefb63f..daa94383 100644 --- a/src/backend/drivers/dropbox/driver.ts +++ b/src/backend/drivers/dropbox/driver.ts @@ -164,7 +164,7 @@ export class DropboxDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) await this.client.request("/2/files/upload", { diff --git a/src/backend/drivers/emby/driver.ts b/src/backend/drivers/emby/driver.ts index 4bfd53a0..838b2a01 100644 --- a/src/backend/drivers/emby/driver.ts +++ b/src/backend/drivers/emby/driver.ts @@ -219,7 +219,7 @@ export class EmbyDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { throw new Error("Emby is read-only") } diff --git a/src/backend/drivers/ftp/driver.ts b/src/backend/drivers/ftp/driver.ts index 71e078f6..499acb31 100644 --- a/src/backend/drivers/ftp/driver.ts +++ b/src/backend/drivers/ftp/driver.ts @@ -173,7 +173,7 @@ export class FTPDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const target = cleanPosixPath(physicalPath) await this.client.mkdirAll(posixDirname(target)) diff --git a/src/backend/drivers/ftp/ftp-client.ts b/src/backend/drivers/ftp/ftp-client.ts index dc1a4e47..955cdcfd 100644 --- a/src/backend/drivers/ftp/ftp-client.ts +++ b/src/backend/drivers/ftp/ftp-client.ts @@ -508,7 +508,7 @@ export class FTPClient { await this.removeDir(remotePath) } - async upload(remotePath: string, content: Buffer): Promise { + async upload(remotePath: string, content: Uint8Array): Promise { await this.connect() const { dataSocket } = await this.openDataConnection() diff --git a/src/backend/drivers/github/driver.ts b/src/backend/drivers/github/driver.ts index 9643c056..f486b19a 100644 --- a/src/backend/drivers/github/driver.ts +++ b/src/backend/drivers/github/driver.ts @@ -268,7 +268,7 @@ export class GithubDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { if (!this.isOnBranch) { throw new Error("cannot write to non-branch reference") diff --git a/src/backend/drivers/github/util.ts b/src/backend/drivers/github/util.ts index 9fd90b3d..169cf353 100644 --- a/src/backend/drivers/github/util.ts +++ b/src/backend/drivers/github/util.ts @@ -280,7 +280,7 @@ export class GithubApiClient { return res.sha } - async putBlob(content: Buffer | Uint8Array): Promise { + async putBlob(content: Uint8Array): Promise { // 不用 Buffer.from(content).toString("base64"):ESA 等边缘运行时没有全局 // Buffer,上传路径会直接抛 "Buffer is not defined"(Issue #169 的同类问题)。 const base64Content = bytesToBase64(content) diff --git a/src/backend/drivers/google_drive/driver.ts b/src/backend/drivers/google_drive/driver.ts index ba6bbcbc..0a849811 100644 --- a/src/backend/drivers/google_drive/driver.ts +++ b/src/backend/drivers/google_drive/driver.ts @@ -142,7 +142,7 @@ export class GoogleDrive implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const { parentId, name } = await this.client.resolveParentAndName(physicalPath) diff --git a/src/backend/drivers/google_drive/util.ts b/src/backend/drivers/google_drive/util.ts index 230c4d20..78cafa53 100644 --- a/src/backend/drivers/google_drive/util.ts +++ b/src/backend/drivers/google_drive/util.ts @@ -309,7 +309,7 @@ export class GoogleDriveClient { public async putFile( parentId: string, filename: string, - content: Buffer, + content: Uint8Array, mimeType = "application/octet-stream", ): Promise { const chunkSize = (this.addition.chunk_size || 5) * 1024 * 1024 diff --git a/src/backend/drivers/guangyapan/driver.ts b/src/backend/drivers/guangyapan/driver.ts index 97ea2e1d..646ac6cf 100644 --- a/src/backend/drivers/guangyapan/driver.ts +++ b/src/backend/drivers/guangyapan/driver.ts @@ -337,7 +337,7 @@ export class GuangYaPanDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const parentPath = clean.split("/").slice(0, -1).join("/") || "/" diff --git a/src/backend/drivers/ipfs_api/driver.ts b/src/backend/drivers/ipfs_api/driver.ts index e32c4348..0b381003 100644 --- a/src/backend/drivers/ipfs_api/driver.ts +++ b/src/backend/drivers/ipfs_api/driver.ts @@ -165,7 +165,7 @@ export class DriverIpfs implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { if (!this.isMfs()) throw new Error("[IPFS] only write in mfs mode") const full = this.fullPath(physicalPath) diff --git a/src/backend/drivers/ipfs_api/util.ts b/src/backend/drivers/ipfs_api/util.ts index d8703178..f13644b1 100644 --- a/src/backend/drivers/ipfs_api/util.ts +++ b/src/backend/drivers/ipfs_api/util.ts @@ -53,7 +53,7 @@ export class ClientIpfs { await this.post("files/rm", [path, "recursive=true"]) } - async add(content: Buffer, fileName: string): Promise { + async add(content: Uint8Array, fileName: string): Promise { const form = new FormData() form.append("file", new Blob([content as unknown as BlobPart]), fileName) const resp = await fetch(`${this.endpoint}/api/v0/add`, { method: "POST", body: form }) diff --git a/src/backend/drivers/kodbox/driver.ts b/src/backend/drivers/kodbox/driver.ts index 7e52ba1b..b2ca403d 100644 --- a/src/backend/drivers/kodbox/driver.ts +++ b/src/backend/drivers/kodbox/driver.ts @@ -160,7 +160,7 @@ export class DriverKodbox implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const full = this.fullPath(physicalPath) const parts = full.split("/").filter(Boolean) diff --git a/src/backend/drivers/kodbox/util.ts b/src/backend/drivers/kodbox/util.ts index c09f26ce..ea517fa4 100644 --- a/src/backend/drivers/kodbox/util.ts +++ b/src/backend/drivers/kodbox/util.ts @@ -96,7 +96,7 @@ export class ClientKodbox { }) } - async put(dirPath: string, fileName: string, content: Buffer): Promise { + async put(dirPath: string, fileName: string, content: Uint8Array): Promise { const form = new FormData() form.append("accessToken", this.authorization) form.append("path", dirPath) diff --git a/src/backend/drivers/local.ts b/src/backend/drivers/local.ts index 2bffdb9b..ce1d0fed 100644 --- a/src/backend/drivers/local.ts +++ b/src/backend/drivers/local.ts @@ -141,7 +141,7 @@ export class LocalDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { await initNodeModules() if (!fs || !path) diff --git a/src/backend/drivers/mediafire/driver.ts b/src/backend/drivers/mediafire/driver.ts index fef74423..4f632558 100644 --- a/src/backend/drivers/mediafire/driver.ts +++ b/src/backend/drivers/mediafire/driver.ts @@ -139,7 +139,7 @@ export class DriverMediafire implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error("[MediaFire] Direct put not supported in stateless environment") } diff --git a/src/backend/drivers/mediatrack/driver.ts b/src/backend/drivers/mediatrack/driver.ts index c1188086..77727f9a 100644 --- a/src/backend/drivers/mediatrack/driver.ts +++ b/src/backend/drivers/mediatrack/driver.ts @@ -298,7 +298,7 @@ export class MediatrackDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const parentPath = clean.split("/").slice(0, -1).join("/") diff --git a/src/backend/drivers/mega/driver.ts b/src/backend/drivers/mega/driver.ts index 3e232450..e54e7cc7 100644 --- a/src/backend/drivers/mega/driver.ts +++ b/src/backend/drivers/mega/driver.ts @@ -196,7 +196,7 @@ export class MegaDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { console.warn(`[Mega] put for ${physicalPath}`) } diff --git a/src/backend/drivers/misskey/driver.ts b/src/backend/drivers/misskey/driver.ts index 4f322e5f..dcb3ca96 100644 --- a/src/backend/drivers/misskey/driver.ts +++ b/src/backend/drivers/misskey/driver.ts @@ -173,7 +173,7 @@ export class DriverMisskey implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error("[Misskey] Direct put not supported in stateless environment") } diff --git a/src/backend/drivers/mopan/driver.ts b/src/backend/drivers/mopan/driver.ts index 8b2b280d..81ed0694 100644 --- a/src/backend/drivers/mopan/driver.ts +++ b/src/backend/drivers/mopan/driver.ts @@ -375,7 +375,7 @@ export class MoPanDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const pathParts = physicalPath.split("/").filter(Boolean) const fileName = pathParts.pop() || "未命名文件" diff --git a/src/backend/drivers/onedrive/driver.ts b/src/backend/drivers/onedrive/driver.ts index 62898ba6..a7a16cd6 100644 --- a/src/backend/drivers/onedrive/driver.ts +++ b/src/backend/drivers/onedrive/driver.ts @@ -285,7 +285,7 @@ export class Onedrive implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { if (content.length <= 4 * 1024 * 1024) { // upSmall diff --git a/src/backend/drivers/onedrive_app/driver.ts b/src/backend/drivers/onedrive_app/driver.ts index db27e8d9..8bae9f72 100644 --- a/src/backend/drivers/onedrive_app/driver.ts +++ b/src/backend/drivers/onedrive_app/driver.ts @@ -224,7 +224,7 @@ export class OnedriveAPP implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { if (content.length <= 4 * 1024 * 1024) { const url = getMetaUrl(this, false, physicalPath, "content") diff --git a/src/backend/drivers/onedrive_sharelink/driver.ts b/src/backend/drivers/onedrive_sharelink/driver.ts index 5f583055..47cba471 100644 --- a/src/backend/drivers/onedrive_sharelink/driver.ts +++ b/src/backend/drivers/onedrive_sharelink/driver.ts @@ -138,7 +138,7 @@ export class OnedriveSharelinkDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { throw new Error("OneDrive Sharelink is read-only") } diff --git a/src/backend/drivers/openlist/driver.ts b/src/backend/drivers/openlist/driver.ts index 8128f5e1..60cabb2e 100644 --- a/src/backend/drivers/openlist/driver.ts +++ b/src/backend/drivers/openlist/driver.ts @@ -117,7 +117,7 @@ export class DriverOpenlist implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error("[OpenList] Direct put not supported in stateless environment") } diff --git a/src/backend/drivers/pikpak/driver.ts b/src/backend/drivers/pikpak/driver.ts index 9b16453f..7ba3716a 100644 --- a/src/backend/drivers/pikpak/driver.ts +++ b/src/backend/drivers/pikpak/driver.ts @@ -406,7 +406,7 @@ export class PikPakDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const parentPath = clean.split("/").slice(0, -1).join("/") diff --git a/src/backend/drivers/pikpak_share/driver.ts b/src/backend/drivers/pikpak_share/driver.ts index 6069ca8c..61927871 100644 --- a/src/backend/drivers/pikpak_share/driver.ts +++ b/src/backend/drivers/pikpak_share/driver.ts @@ -194,7 +194,7 @@ export class PikPakShareDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { throw new Error("PikPak Share is read-only") } diff --git a/src/backend/drivers/proton_drive/driver.ts b/src/backend/drivers/proton_drive/driver.ts index 14701148..a0b05693 100644 --- a/src/backend/drivers/proton_drive/driver.ts +++ b/src/backend/drivers/proton_drive/driver.ts @@ -508,7 +508,7 @@ export class ProtonDriveDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { // TODO(未实现):上传需走 Proton 的两阶段协议: // 1) POST /drive/volumes/{vol}/links/{parent}/draft 创建草稿,得到 revisionID diff --git a/src/backend/drivers/quark/driver.ts b/src/backend/drivers/quark/driver.ts index c5c6c4fc..7d3a0d1c 100644 --- a/src/backend/drivers/quark/driver.ts +++ b/src/backend/drivers/quark/driver.ts @@ -164,7 +164,7 @@ export class QuarkDriver implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error( "[Quark/UC] Direct put not supported in stateless environment", diff --git a/src/backend/drivers/r2/driver.ts b/src/backend/drivers/r2/driver.ts index 93f3cde8..066b13e0 100644 --- a/src/backend/drivers/r2/driver.ts +++ b/src/backend/drivers/r2/driver.ts @@ -609,7 +609,7 @@ export class R2Driver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { if (this.resolveMode() === "api") { this.requireApiConfig() diff --git a/src/backend/drivers/s3/driver.ts b/src/backend/drivers/s3/driver.ts index b6afeb40..6d4979cf 100644 --- a/src/backend/drivers/s3/driver.ts +++ b/src/backend/drivers/s3/driver.ts @@ -330,7 +330,7 @@ export class S3Driver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { await this.checkDogeToken() const remotePath = this.getRemotePath(physicalPath) diff --git a/src/backend/drivers/seafile/driver.ts b/src/backend/drivers/seafile/driver.ts index d17e73e4..9d3aafa6 100644 --- a/src/backend/drivers/seafile/driver.ts +++ b/src/backend/drivers/seafile/driver.ts @@ -341,7 +341,7 @@ export class SeafileDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const { repoId, innerPath } = await this.resolveRepoAndPath(physicalPath) const parentPath = innerPath.split("/").slice(0, -1).join("/") || "/" diff --git a/src/backend/drivers/sftp/driver.ts b/src/backend/drivers/sftp/driver.ts index acb840f8..369a8b29 100644 --- a/src/backend/drivers/sftp/driver.ts +++ b/src/backend/drivers/sftp/driver.ts @@ -222,7 +222,7 @@ export class SFTPDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const target = cleanPosixPath(physicalPath) await this.client.mkdirAll(posixDirname(target)) diff --git a/src/backend/drivers/sftp/util.ts b/src/backend/drivers/sftp/util.ts index b084eaa7..f91fe1bd 100644 --- a/src/backend/drivers/sftp/util.ts +++ b/src/backend/drivers/sftp/util.ts @@ -294,7 +294,7 @@ export class SFTPClientWrapper { } } - async writeFile(remotePath: string, content: Buffer): Promise { + async writeFile(remotePath: string, content: Uint8Array): Promise { const sftp = await this.getSFTP() return new Promise((resolve, reject) => { const ws = sftp.createWriteStream(remotePath) diff --git a/src/backend/drivers/smb/driver.ts b/src/backend/drivers/smb/driver.ts index e3b47962..1fd2c4a1 100644 --- a/src/backend/drivers/smb/driver.ts +++ b/src/backend/drivers/smb/driver.ts @@ -93,7 +93,7 @@ export class SMBDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { // SMB put } diff --git a/src/backend/drivers/teldrive/driver.ts b/src/backend/drivers/teldrive/driver.ts index 1fbaeec2..4440993b 100644 --- a/src/backend/drivers/teldrive/driver.ts +++ b/src/backend/drivers/teldrive/driver.ts @@ -124,7 +124,7 @@ export class DriverTeldrive implements StorageDriver { async put( _virtualPath: string, _physicalPath: string, - _content: Buffer, + _content: Uint8Array, ): Promise { throw new Error("[TelDrive] Direct put not supported in stateless environment") } diff --git a/src/backend/drivers/terabox/driver.ts b/src/backend/drivers/terabox/driver.ts index e6a4fe3c..253484cc 100644 --- a/src/backend/drivers/terabox/driver.ts +++ b/src/backend/drivers/terabox/driver.ts @@ -249,7 +249,7 @@ export class TeraboxDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const parentPath = clean.split("/").slice(0, -1).join("/") || "/" diff --git a/src/backend/drivers/thunder/driver.ts b/src/backend/drivers/thunder/driver.ts index ea0d0194..df08e362 100644 --- a/src/backend/drivers/thunder/driver.ts +++ b/src/backend/drivers/thunder/driver.ts @@ -305,7 +305,7 @@ export class ThunderDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const fileName = parts.pop() || "file" diff --git a/src/backend/drivers/uss/driver.ts b/src/backend/drivers/uss/driver.ts index 914ecf04..d434262b 100644 --- a/src/backend/drivers/uss/driver.ts +++ b/src/backend/drivers/uss/driver.ts @@ -196,7 +196,7 @@ export class UssDriver implements StorageDriver { await this.moveOrCopy(this.getKey(srcPhys), this.getKey(dstPhys), "copy") } - async put(_v: string, physicalPath: string, content: Buffer): Promise { + async put(_v: string, physicalPath: string, content: Uint8Array): Promise { const key = this.getKey(physicalPath) const res = await fetch(`${this.apiBase}/${key}`, { method: "PUT", diff --git a/src/backend/drivers/webdav/driver.ts b/src/backend/drivers/webdav/driver.ts index f4d98438..87d5a5a0 100644 --- a/src/backend/drivers/webdav/driver.ts +++ b/src/backend/drivers/webdav/driver.ts @@ -144,7 +144,7 @@ export class WebdavDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const remotePath = this.getRemotePath(physicalPath) await this.client.put(remotePath, content) diff --git a/src/backend/drivers/weiyun/driver.ts b/src/backend/drivers/weiyun/driver.ts index 6095618c..80482978 100644 --- a/src/backend/drivers/weiyun/driver.ts +++ b/src/backend/drivers/weiyun/driver.ts @@ -547,7 +547,7 @@ export class WeiyunDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = String(physicalPath || "") .split("/") @@ -681,7 +681,7 @@ export class WeiyunDriver implements StorageDriver { async uploadPart( sessionToken: string, partNumber: number, - content: Buffer, + content: Uint8Array, ): Promise<{ partNumber: number }> { const session = decodeUploadSession(sessionToken) if ( diff --git a/src/backend/drivers/weiyun/util.ts b/src/backend/drivers/weiyun/util.ts index 6b423444..084160e6 100644 --- a/src/backend/drivers/weiyun/util.ts +++ b/src/backend/drivers/weiyun/util.ts @@ -552,7 +552,7 @@ export class WeiyunClient { dirKey: string, fileName: string, fileSize: number, - content: Uint8Array | Buffer, + content: Uint8Array, channelCount = 4, fileExistOption = 1, ): Promise { diff --git a/src/backend/drivers/wopan/driver.ts b/src/backend/drivers/wopan/driver.ts index d3df28e1..21b77f23 100644 --- a/src/backend/drivers/wopan/driver.ts +++ b/src/backend/drivers/wopan/driver.ts @@ -286,7 +286,7 @@ export class WoPanDriver implements StorageDriver { async put( _virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const parts = physicalPath.split("/").filter(Boolean) const name = parts.pop() || "upload" diff --git a/src/backend/drivers/wps/driver.ts b/src/backend/drivers/wps/driver.ts index 0ad78367..93d6f98b 100644 --- a/src/backend/drivers/wps/driver.ts +++ b/src/backend/drivers/wps/driver.ts @@ -296,7 +296,7 @@ export class WpsDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer | Uint8Array, + content: Uint8Array, ): Promise { // WPS direct upload requires chunked upload protocol const clean = this.cleanPath(physicalPath) diff --git a/src/backend/drivers/yandex/driver.ts b/src/backend/drivers/yandex/driver.ts index 74685710..dcf7afb2 100644 --- a/src/backend/drivers/yandex/driver.ts +++ b/src/backend/drivers/yandex/driver.ts @@ -185,7 +185,7 @@ export class YandexDriver implements StorageDriver { async put( virtualPath: string, physicalPath: string, - content: Buffer, + content: Uint8Array, ): Promise { const clean = this.cleanPath(physicalPath) const uploadUrl = await this.client.getUploadLink(clean) diff --git a/src/backend/server/fs.ts b/src/backend/server/fs.ts index e989e48d..be7c751e 100644 --- a/src/backend/server/fs.ts +++ b/src/backend/server/fs.ts @@ -1973,6 +1973,7 @@ fsRouter.post("/archive/decompress", async (c) => { continue } const content = await extractZipEntry(bytes, entry) + // content 已经是 Uint8Array;再包一层 Buffer 会在 ESA 上直接抛错 await putItem(targetPath, content, getStorageRequestContext(c)) count++ } From d75a5abaa2fdb057e90ca937312fa173b5e8aa35 Mon Sep 17 00:00:00 2001 From: nike64542-byte Date: Mon, 9 Nov 2026 17:35:50 +0800 Subject: [PATCH 43/49] fix(guangyapan): sync SMS login state via driver callback (#142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - storage.ts: guangyapan onStateChanged 回调把驱动改动同步回 in-flight 的 updatedStorage(以本次提交的 addition 为基底合并),并补上 deferredTokenPersistence 判断。避免 /admin/storage/update 用旧快照覆盖 verification_id/captcha_token/send_code,同时不把用户本次编辑的其它 配置项回退成旧值。 - 新增 handler 级回归测试:提交改过的 page_size 同时驱动回写登录态, 断言两者都保留。 - 不再包含与本修复无关的 build-edge createRequire banner(另行拆分)。 --- src/backend/internal/op/storage.ts | 11 ++ .../server/storage_update_guangyapan.test.ts | 119 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/backend/server/storage_update_guangyapan.test.ts diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 869e72de..550f1871 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1212,6 +1212,17 @@ async function createDriver( // captcha_token/send_code 等),空串用于显式清除一次性字段 Object.assign(stAddition, changes) st.addition = JSON.stringify(stAddition) + // 把驱动回写同步给 in-flight 的 updatedStorage:/admin/storage/update 会用 + // updatedStorage 覆盖 db,若不同步,驱动刚写入的登录态(verification_id 等) + // 会被提交前的旧快照覆盖。这里以本次提交的 addition 为基底叠加驱动改动, + // 既保留登录态,也不会把用户本次编辑的其它配置项回退成旧值。 + if (deferredTokenPersistence.has(storageConfig)) { + storageConfig.addition = JSON.stringify({ + ...parseAddition(storageConfig), + ...changes, + }) + return + } await saveDb(db) } catch (e) { console.warn("[GuangYaPan] failed to persist changes:", e) diff --git a/src/backend/server/storage_update_guangyapan.test.ts b/src/backend/server/storage_update_guangyapan.test.ts new file mode 100644 index 00000000..cb05e7df --- /dev/null +++ b/src/backend/server/storage_update_guangyapan.test.ts @@ -0,0 +1,119 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Hono } from "hono" +import { adminRouter } from "./admin" +import { getDb, saveDb } from "../internal/model/db" + +/** + * PR #142 回归:/admin/storage/update 走驱动初始化时,驱动通过 onStateChanged + * 回写的一次性登录态(verification_id / captcha_token / send_code)必须落库, + * 同时不能把用户本次提交的其它 addition 配置项回退成旧值。 + * + * 修复前 handler 会在 getDriver 之后重新读库再 `{...提交, ...读库}` 合并, + * 由于读到的库值是「旧 addition + 驱动改动」,提交里所有已存在的键都会被旧值覆盖 + * (例如提交 page_size=500,旧值 200,最终写入 200)。 + */ + +const authHeaders = { Authorization: "Bearer admin-token" } + +const DEVICE_ID = "0123456789abcdef0123456789abcdef" + +async function setupEnv(storage: any): Promise { + const env: any = {} + await saveDb( + { + settings: [{ key: "token", value: "admin-token" }], + users: [], + storages: [storage], + shares: [], + metas: [], + }, + env, + ) + return env +} + +function makeApp(): Hono { + const app = new Hono() + app.route("/api/admin", adminRouter) + return app +} + +function jsonResponse(body: any): Response { + return new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }) +} + +test("storage/update 同时保留提交的 addition 与驱动回写的登录态", async () => { + const env = await setupEnv({ + id: 1, + driver: "GuangYaPan", + mount_path: "/guangyapan-test", + addition: JSON.stringify({ + client_id: "cid", + device_id: DEVICE_ID, + phone_number: "13800138000", + send_code: false, + page_size: 200, + }), + disabled: false, + modified: "2026-01-01T00:00:00.000Z", + }) + const app = makeApp() + + const originalFetch = globalThis.fetch + globalThis.fetch = (async (input: any, init?: any) => { + const url = new URL(String(input)) + const key = `${init?.method || "GET"} ${url.pathname}` + if (key === "POST /v1/shield/captcha/init") { + return jsonResponse({ captcha_token: "ct1" }) + } + if (key === "POST /v1/auth/verification") { + return jsonResponse({ verification_id: "vid1" }) + } + throw new Error(`unexpected fetch: ${key}`) + }) as typeof fetch + + try { + const res = await app.request( + "/api/admin/storage/update", + { + method: "POST", + headers: { ...authHeaders, "Content-Type": "application/json" }, + body: JSON.stringify({ + id: 1, + driver: "GuangYaPan", + mount_path: "/guangyapan-test", + // 用户本次把 page_size 从 200 改成 500,并触发发送短信验证码 + addition: JSON.stringify({ + client_id: "cid", + device_id: DEVICE_ID, + phone_number: "13800138000", + send_code: true, + page_size: 500, + }), + }), + }, + env, + ) + assert.equal(res.status, 200) + const body: any = await res.json() + assert.equal(body.code, 200) + } finally { + globalThis.fetch = originalFetch + } + + const db: any = await getDb(env) + const saved = (db.storages || []).find((s: any) => s.id === 1) + assert.ok(saved, "storage 仍应存在") + const addition = JSON.parse(saved.addition) + + // 用户提交的配置项不能被旧值回退 + assert.equal(addition.page_size, 500) + // 驱动初始化时回写的登录态必须落库 + assert.equal(addition.captcha_token, "ct1") + assert.equal(addition.verification_id, "vid1") + assert.equal(addition.send_code, false) +}) From 2e0f89beb88e3a6c9423f3a90d47b0719b7aab00 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:37:05 +0800 Subject: [PATCH 44/49] fix(deploy): expose safer-buffer to Wrangler bundler (#160) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deploy): expose safer-buffer to Wrangler bundler Wrangler fails to resolve safer-buffer from iconv-lite in the Worker bundle even though pnpm-lock.yaml contains it transitively. Declare safer-buffer as a direct production dependency so the package is available from the project root to Wrangler/esbuild under pnpm's hoisted node_modules layout. Update the lockfile importer; the existing locked 2.1.2 package is reused. Validation: pnpm install --lockfile-only --offline --no-frozen-lockfile completed; git diff --check passed. Full Cloudflare deployment was not run because it requires installing dependencies and Cloudflare credentials. * fix(deploy): pin safer-buffer to 2.1.2 issue #151 的 8 处报错全部来自 node_modules/iconv-lite 的 require(''safer-buffer''),即根级 node_modules 里找不到该包;在 node-linker=hoisted 下显式声明可保证它被装到根,因此保留该依赖声明。按评审意见把 ^2.1.2 收敛为精确 2.1.2(仅为打包可见性引入,不应跟随上游自动升级),并同步 lockfile importer 段;pnpm install --lockfile-only --offline --frozen-lockfile 校验通过。 * fix(deploy): alias iconv-lite to a stub so Wrangler stops resolving safer-buffer 反证成立:.npmrc 是 node-linker=hoisted + shamefully-hoist,safer-buffer 作为 iconv-lite 的传递依赖本就被铺到顶层 node_modules;把它提为直接依赖是 no-op, 已移除该声明。 真正根因是两条构建链路不一致:scripts/build-edge.mjs 早已把 iconv-lite external / 空模块化(ESA、EdgeOne、dist-server 产物),而 `wrangler deploy` 直接以 src/backend/worker.ts 为入口打包,仍会解析 FTP 驱动的 require("iconv-lite") 及其传递依赖 safer-buffer(issue #151)。 修复:wrangler.jsonc 增加 alias,把 iconv-lite 指向新增的 drivers/ftp/iconv-stub.ts。Workers 没有 TCP socket,FTP 转码路径不会执行, 空桩只在打包期提供可解析的模块,真被调用时抛出明确错误。 验证:wrangler deploy --dry-run 产物由 4378.99 KiB 降到 3834.32 KiB, worker.js 内联空桩文案且不再出现 safer-buffer。 --------- Co-authored-by: pikachuren --- src/backend/drivers/ftp/iconv-stub.ts | 21 +++++++++++++++++++++ wrangler.jsonc | 8 ++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/backend/drivers/ftp/iconv-stub.ts diff --git a/src/backend/drivers/ftp/iconv-stub.ts b/src/backend/drivers/ftp/iconv-stub.ts new file mode 100644 index 00000000..f36c1d56 --- /dev/null +++ b/src/backend/drivers/ftp/iconv-stub.ts @@ -0,0 +1,21 @@ +/** + * Worker 打包时的 iconv-lite 空桩(见 wrangler.jsonc 的 alias)。 + * + * 背景(issue #151):`wrangler deploy` 直接以 src/backend/worker.ts 为入口打包, + * 不经过 scripts/build-edge.mjs 的 emptyNodeDriverPlugin —— 后者在 ESA / EdgeOne / + * dist-server 产物里早已把 iconv-lite 替换成空模块。于是 FTP 驱动的 + * `require("iconv-lite")` 在 Wrangler 这条链路上仍会被解析,进而连带解析 + * iconv-lite 的传递依赖 safer-buffer,在依赖布局稍有差异的环境里直接构建失败。 + * + * Workers 运行时没有 TCP socket,FTP 驱动不可用,这里的编码转码路径永远不会 + * 被真正执行;保留同名导出只是为了在打包期能解析,真被调用时给出明确错误。 + */ +const unavailable = (): never => { + throw new Error("iconv-lite is not available in this runtime") +} + +export const decode = unavailable +export const encode = unavailable +export const encodingExists = () => false + +export default { decode, encode, encodingExists } diff --git a/wrangler.jsonc b/wrangler.jsonc index 7d76bef2..ec22634f 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -9,6 +9,14 @@ { "name": "openlist-tsworkers", "main": "src/backend/worker.ts", + + // iconv-lite 只有 FTP 驱动在用,而 Workers 没有 TCP socket、FTP 驱动不可用。 + // build-edge.mjs 早已把 iconv-lite 当作 external / 空模块,但 `wrangler deploy` + // 不经过那条链路,仍会去解析它以及它的传递依赖 safer-buffer(issue #151)。 + // 这里指向空桩,让 Wrangler 的打包与其它产物的行为一致。 + "alias": { + "iconv-lite": "./src/backend/drivers/ftp/iconv-stub.ts" + }, "compatibility_date": "2026-08-04", "compatibility_flags": ["nodejs_compat"], "workers_dev": false, From 38cfdc4096bbbc57cd455153ff56ec6f11674842 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:14:49 +0800 Subject: [PATCH 45/49] fix(139): keep valid tokens when refresh metadata is absent (#164) * fix(139): keep valid tokens when refresh metadata is absent Issue #122 reports that existing 139 authorizations became unusable as soon as they entered the 15-day refresh window, even when they were still accepted by the service. The PC refreshToken endpoint requires user_domain_id, but legacy storage configurations may not contain it. Skip only the automatic refresh when user_domain_id is absent; retain the current authorization and allow the normal API request to determine whether the token is still valid. When user_domain_id is present, preserve the existing near-expiry refresh flow and persistence callback. Expose the optional field in the 139 storage form with a description. Validation: 139 driver tests 13/13 passed; mutation check restoring the previous throw makes the new regression test fail; git diff --check passed. * fix(139): refresh tokens without user_domain_id via legacy endpoint The PC refreshToken API requires user_domain_id, so configs that only contain Authorization were left with no automatic refresh and emitted a near-expiry warning on every storage init. Fall back to the legacy authTokenRefresh.do endpoint already used by the Go backend when user_domain_id is empty. It only needs the current token and account, accepts both and candidates, and only writes back a token with a later expiry. Refresh failures are silent and keep the current authorization usable until the service rejects it. user_domain_id stays optional and the PC path is unchanged when set. Tests: 139 driver 14/14, drivers 199/199, server 206/206, tsc clean. * feat(139): port email login to auto-fill user_domain_id Align the Worker driver with the Go backend's credential flow: - Port step1 mail password login, step2 artifact exchange and step3 encrypted third-party login (AES-CBC/ECB, RSA PKCS1 v1.5). - Support fast login with existing mail_cookies and SMS risk control (sms_code), including cookie persistence between attempts. - Persist authorization, user_domain_id, mail_cookies and sms_code after a successful login, matching Go's MustSaveDriverStorage behavior. - Keep manual Authorization + user_domain_id authoritative: when Authorization is present no login runs; a login only overwrites the manual value when it is actually triggered. - Re-login after a refresh failure when credentials are available, matching Go's loginAfterAuthorizationFailure. - Expose authorization (now optional), username, password, sms_code and mail_cookies in the 139 storage form. Tests: driver 25/25, all drivers 210/210, server 206/206, tsc clean. * fix(139): match Go's password-only refresh fallback Go's loginAfterAuthorizationFailure never retries cookie fast login after a refresh failure; mail_cookies are only reused as device context for the password login request. Restrict the refresh-failure re-login to username + password and add a skipFastLogin option so the fallback performs step1 -> step2 -> step3 instead of step2 -> step3. Tests: 139 driver 25/25, tsc clean. --------- Co-authored-by: pikachuren --- src/backend/drivers/139/driver.test.ts | 152 +++++ src/backend/drivers/139/driver.ts | 8 +- src/backend/drivers/139/login.test.ts | 475 +++++++++++++ src/backend/drivers/139/login.ts | 893 +++++++++++++++++++++++++ src/backend/drivers/139/types.ts | 1 + src/backend/drivers/139/util.ts | 226 ++++++- src/backend/internal/op/storage.ts | 73 +- src/backend/server/admin.ts | 38 +- 8 files changed, 1828 insertions(+), 38 deletions(-) create mode 100644 src/backend/drivers/139/login.test.ts create mode 100644 src/backend/drivers/139/login.ts diff --git a/src/backend/drivers/139/driver.test.ts b/src/backend/drivers/139/driver.test.ts index 15f13672..b2fa8fad 100644 --- a/src/backend/drivers/139/driver.test.ts +++ b/src/backend/drivers/139/driver.test.ts @@ -174,6 +174,158 @@ test("Yun139 refreshes a near-expiry token with no Cookie header", async () => { } }) +test("Yun139 keeps a valid near-expiry token and stays quiet when user_domain_id is not configured", async () => { + const expiresAt = Date.now() + 5 * 24 * 60 * 60 * 1000 + const token = `legacy-token|1|RCS|${expiresAt}|opaque` + const authorization = Buffer.from(`pc:13800138000:${token}`).toString( + "base64", + ) + const originalFetch = globalThis.fetch + const originalWarn = console.warn + const originalDebug = console.debug + const calls: string[] = [] + const warnings: string[] = [] + console.warn = (...args: any[]) => { + warnings.push(args.map((arg) => String(arg)).join(" ")) + } + console.debug = () => {} + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if ( + url === "https://aas.caiyun.feixin.10086.cn/tellin/authTokenRefresh.do" + ) { + return new Response( + "1unavailable", + { status: 200, headers: { "content-type": "application/xml" } }, + ) + } + assert.equal( + url, + "https://user-njs.yun.139.com/user/route/qryRoutePolicy", + "missing user_domain_id must not block the normal route policy request", + ) + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { + routePolicyList: [ + { + modName: "personal", + httpsUrl: "https://personal-kd-njs.yun.139.com/hcy", + }, + ], + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + }) as typeof fetch + + try { + const client = new Yun139ApiClient({ authorization, type: "personal_new" }) + await client.init() + + assert.deepEqual(calls, [ + "https://aas.caiyun.feixin.10086.cn/tellin/authTokenRefresh.do", + "https://user-njs.yun.139.com/user/route/qryRoutePolicy", + ]) + assert.deepEqual(warnings, [], "near-expiry warning must be suppressed") + assert.equal( + Buffer.from(client.getAuthString(), "base64").toString("utf8"), + `pc:13800138000:${token}`, + ) + } finally { + globalThis.fetch = originalFetch + console.warn = originalWarn + console.debug = originalDebug + } +}) + +test("Yun139 refreshes a near-expiry token via the legacy endpoint when user_domain_id is absent", async () => { + const account = "13800138000" + const oldToken = `legacy-old|1|RCS|${Date.now() + 5 * 24 * 60 * 60 * 1000}|opaque` + const newToken = `legacy-new|1|RCS|${Date.now() + 30 * 24 * 60 * 60 * 1000}|opaque` + const addition: Yun139Addition = { + authorization: Buffer.from(`pc:${account}:${oldToken}`).toString("base64"), + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: Array<{ url: string; init?: RequestInit }> = [] + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { + const url = String(input) + calls.push({ url, init }) + + if ( + url === "https://aas.caiyun.feixin.10086.cn/tellin/authTokenRefresh.do" + ) { + // `` 可能仍是原 token,只有过期时间更晚的候选才能写回。 + return new Response( + `0${oldToken}` + + `${newToken}` + + "2592000", + { status: 200, headers: { "content-type": "application/xml" } }, + ) + } + + if (url === "https://user-njs.yun.139.com/user/route/qryRoutePolicy") { + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { + routePolicyList: [ + { + modName: "personal", + httpsUrl: "https://personal-kd-njs.yun.139.com/hcy", + }, + ], + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) + } + + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const refreshedAuthorizations: string[] = [] + const client = new Yun139ApiClient(addition, async (authorization) => { + refreshedAuthorizations.push(authorization) + }) + await client.init() + + assert.deepEqual( + calls.map((call) => call.url), + [ + "https://aas.caiyun.feixin.10086.cn/tellin/authTokenRefresh.do", + "https://user-njs.yun.139.com/user/route/qryRoutePolicy", + ], + ) + const refreshCall = calls[0] + assert.equal(refreshCall.init?.method, "POST") + const headers = refreshCall.init?.headers as Record + assert.equal(headers["Content-Type"], "application/xml;charset=UTF-8") + assert.equal(headers.Referer, "https://yun.139.com/") + const body = String(refreshCall.init?.body) + assert.ok(body.includes(`${oldToken}`)) + assert.ok(body.includes(`${account}`)) + assert.ok(body.includes("656")) + + assert.equal( + Buffer.from(addition.authorization, "base64").toString("utf8"), + `pc:${account}:${newToken}`, + ) + assert.deepEqual(refreshedAuthorizations, [addition.authorization]) + } finally { + globalThis.fetch = originalFetch + } +}) + test("Yun139Driver lists subdir contents; file path must not degrade to parent (issue #112)", async () => { const addition: Yun139Addition = { authorization: Buffer.from( diff --git a/src/backend/drivers/139/driver.ts b/src/backend/drivers/139/driver.ts index 92418937..86f3010b 100644 --- a/src/backend/drivers/139/driver.ts +++ b/src/backend/drivers/139/driver.ts @@ -5,6 +5,7 @@ import { } from "../../internal/driver/base" import { sortFileItems } from "../../internal/driver/sort" import { Yun139Addition } from "./types" +import { Yun139LoginResult } from "./login" import { Yun139ApiClient } from "./util" export class Yun139Driver implements StorageDriver { @@ -14,9 +15,14 @@ export class Yun139Driver implements StorageDriver { constructor( addition: Yun139Addition, onAuthorizationRefresh?: (authorization: string) => Promise, + onLoginStateChange?: (state: Yun139LoginResult) => Promise, ) { this.addition = addition - this.client = new Yun139ApiClient(addition, onAuthorizationRefresh) + this.client = new Yun139ApiClient( + addition, + onAuthorizationRefresh, + onLoginStateChange, + ) } async init(): Promise { diff --git a/src/backend/drivers/139/login.test.ts b/src/backend/drivers/139/login.test.ts new file mode 100644 index 00000000..40f8c8aa --- /dev/null +++ b/src/backend/drivers/139/login.test.ts @@ -0,0 +1,475 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { constants, generateKeyPairSync, privateDecrypt } from "node:crypto" +import CryptoJS from "crypto-js" +import { Yun139Addition } from "./types" +import { Yun139ApiClient } from "./util" +import { + aesCbcDecrypt, + aesCbcEncrypt, + aesEcbDecrypt, + aesEcbEncrypt, + loginWithCredentials, + rsaEncryptPKCS1v15, + sortedJsonStringify, + Yun139SmsRequiredError, +} from "./login" + +const KEY_HEX_1 = "73634235495062495331515373756c734e7253306c673d3d" +const KEY_HEX_2 = "7150714477323633586746674c337538" +const MAIL_PASSWORD_URL = "https://mail.10086.cn/Login/Login.ashx" +const SSO_LOGIN_URL = "https://user-njs.yun.139.com/user/thirdlogin" +const ROUTE_POLICY_URL = + "https://user-njs.yun.139.com/user/route/qryRoutePolicy" + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") +} + +function encryptCbcBase64(plaintext: string, keyHex: string): string { + const iv = CryptoJS.lib.WordArray.random(16) + const encrypted = CryptoJS.AES.encrypt( + CryptoJS.enc.Utf8.parse(plaintext), + CryptoJS.enc.Hex.parse(keyHex), + { + iv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }, + ) + return CryptoJS.enc.Base64.stringify(iv.clone().concat(encrypted.ciphertext)) +} + +function encryptedStep3Response(payload: any): string { + const finalJson = JSON.stringify(payload) + const inner = bytesToHex(aesEcbEncrypt(finalJson, KEY_HEX_2)) + return encryptCbcBase64(JSON.stringify({ data: inner }), KEY_HEX_1) +} + +function routePolicyResponse(): Response { + return new Response( + JSON.stringify({ + success: true, + code: "0000", + data: { + routePolicyList: [ + { + modName: "personal", + httpsUrl: "https://personal-kd-njs.yun.139.com/hcy", + }, + ], + }, + }), + { status: 200, headers: { "content-type": "application/json" } }, + ) +} + +test("Yun139 sorted JSON matches Go sortedJsonStringify", () => { + assert.equal( + sortedJsonStringify({ + z: true, + a: { d: "x", c: [2, 1], e: null }, + n: 3, + }), + '{"a":{"c":[2,1],"d":"x","e":null},"n":3,"z":true}', + ) +}) + +test("Yun139 AES CBC/ECB roundtrip", () => { + const { iv, ciphertext } = aesCbcEncrypt("hello 139", KEY_HEX_1) + assert.equal(aesCbcDecrypt(ciphertext, KEY_HEX_1, iv), "hello 139") + const ecb = aesEcbEncrypt("outer", KEY_HEX_2) + assert.equal(aesEcbDecrypt(ecb, KEY_HEX_2), "outer") +}) + +test("Yun139 RSA PKCS1 v1.5 encryption decrypts with the matching private key", () => { + const { publicKey, privateKey } = generateKeyPairSync("rsa", { + modulusLength: 1024, + }) + const der = publicKey.export({ type: "spki", format: "der" }) as Buffer + const ciphertext = rsaEncryptPKCS1v15( + Buffer.from("13800138000"), + der.toString("base64"), + ) + const decrypted = privateDecrypt( + { key: privateKey, padding: constants.RSA_PKCS1_PADDING }, + Buffer.from(ciphertext), + ) + assert.equal(decrypted.toString("utf8"), "13800138000") +}) + +test("Yun139 password login obtains account, user_domain_id and authorization", async () => { + const addition: Yun139Addition = { + authorization: "", + username: "13800138000", + password: "secret", + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if (url === MAIL_PASSWORD_URL) { + return new Response(null, { + status: 302, + headers: { + Location: "https://mail.10086.cn/default.html?sid=test-sid", + "Set-Cookie": "RMKEY=test-rmkey; Path=/; HttpOnly", + }, + }) + } + if (url.startsWith("https://smsrebuild1.mail.10086.cn/setting/s?")) { + return new Response( + JSON.stringify({ var: { artifact: "test-artifact" } }), + { + status: 200, + }, + ) + } + if (url === SSO_LOGIN_URL) { + return new Response( + encryptedStep3Response({ + authToken: "test-token", + account: "13800138000", + userDomainId: "domain-123", + }), + { status: 200 }, + ) + } + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const result = await loginWithCredentials(addition) + assert.equal(result.account, "13800138000") + assert.equal(result.userDomainId, "domain-123") + assert.equal( + result.authorization, + Buffer.from("pc:13800138000:test-token").toString("base64"), + ) + assert.ok(result.mailCookies.includes("RMKEY=test-rmkey")) + assert.equal(calls.length, 3) + assert.equal(calls[0], MAIL_PASSWORD_URL) + assert.ok(calls[1].startsWith("https://smsrebuild1.mail.10086.cn/")) + assert.equal(calls[2], SSO_LOGIN_URL) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 login surfaces SMS verification and preserves refreshed cookies", async () => { + const addition: Yun139Addition = { + authorization: "", + username: "13800138000", + password: "secret", + type: "personal_new", + } + const originalFetch = globalThis.fetch + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + if (url === MAIL_PASSWORD_URL) { + return new Response(null, { + status: 302, + headers: { + Location: "https://mail.10086.cn/default.html?ec=PML401010062", + }, + }) + } + if (url.startsWith("https://mail.10086.cn/s?")) { + return new Response(JSON.stringify({ code: "S_OK" }), { + status: 200, + headers: { "Set-Cookie": "Os_SSo_Sid=test-sid; Path=/" }, + }) + } + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + await assert.rejects( + () => loginWithCredentials(addition), + (error: unknown) => { + assert.ok(error instanceof Yun139SmsRequiredError) + assert.ok(error.mailCookies.includes("Os_SSo_Sid=test-sid")) + assert.match(error.message, /fill sms_code/) + return true + }, + ) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 login verifies the SMS code and continues the password flow", async () => { + const addition: Yun139Addition = { + authorization: "", + username: "13800138000", + password: "secret", + sms_code: "123456", + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if (url === MAIL_PASSWORD_URL) { + return new Response(null, { + status: 302, + headers: { + Location: "https://mail.10086.cn/default.html?ec=MW0016", + }, + }) + } + if (url.startsWith("https://mail.10086.cn/s?")) { + return new Response( + JSON.stringify({ + code: "S_OK", + var: { + loginSuccessUrl: "https://mail.10086.cn/default.html?sid=sms-sid", + }, + }), + { status: 200, headers: { "Set-Cookie": "RMKEY=test-rmkey; Path=/" } }, + ) + } + if (url.startsWith("https://smsrebuild1.mail.10086.cn/setting/s?")) { + return new Response( + JSON.stringify({ var: { artifact: "test-artifact" } }), + { status: 200 }, + ) + } + if (url === SSO_LOGIN_URL) { + return new Response( + encryptedStep3Response({ + authToken: "test-token", + account: "13800138000", + userDomainId: "domain-sms", + }), + { status: 200 }, + ) + } + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const result = await loginWithCredentials(addition) + assert.equal(result.userDomainId, "domain-sms") + assert.equal(calls.length, 4) + assert.ok(calls[1].includes("inlogin.action")) + assert.equal(calls[3], SSO_LOGIN_URL) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 fast login with existing mail cookies obtains user_domain_id", async () => { + const addition: Yun139Addition = { + authorization: "", + mail_cookies: "Os_SSo_Sid=test-sid; RMKEY=test-rmkey", + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if (url.startsWith("https://smsrebuild1.mail.10086.cn/setting/s?")) { + return new Response( + JSON.stringify({ var: { artifact: "test-artifact" } }), + { + status: 200, + }, + ) + } + if (url === SSO_LOGIN_URL) { + return new Response( + encryptedStep3Response({ + authToken: "test-token", + account: "13800138000", + userDomainId: "domain-fast", + }), + { status: 200 }, + ) + } + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const result = await loginWithCredentials(addition) + assert.equal(result.userDomainId, "domain-fast") + assert.equal(result.mailCookies, addition.mail_cookies) + assert.equal(calls.length, 2) + assert.equal(calls[1], SSO_LOGIN_URL) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 re-logins when the PC refresh fails and credentials are available", async () => { + const oldToken = `old-token|1|RCS|${Date.now() + 5 * 24 * 60 * 60 * 1000}|opaque` + const addition: Yun139Addition = { + authorization: Buffer.from(`pc:13800138000:${oldToken}`).toString("base64"), + user_domain_id: "old-domain", + username: "13800138000", + password: "secret", + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if (url === "https://user-njs.yun.139.com/user/auth/refreshToken") { + return new Response("refresh rejected", { status: 500 }) + } + if (url === MAIL_PASSWORD_URL) { + return new Response(null, { + status: 302, + headers: { + Location: "https://mail.10086.cn/default.html?sid=test-sid", + "Set-Cookie": "RMKEY=test-rmkey; Path=/", + }, + }) + } + if (url.startsWith("https://smsrebuild1.mail.10086.cn/setting/s?")) { + return new Response( + JSON.stringify({ var: { artifact: "test-artifact" } }), + { status: 200 }, + ) + } + if (url === SSO_LOGIN_URL) { + return new Response( + encryptedStep3Response({ + authToken: "test-token", + account: "13800138000", + userDomainId: "domain-reborn", + }), + { status: 200 }, + ) + } + if (url === ROUTE_POLICY_URL) return routePolicyResponse() + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const states: Array<{ authorization: string; userDomainId: string }> = [] + const client = new Yun139ApiClient(addition, undefined, async (state) => { + states.push({ + authorization: state.authorization, + userDomainId: state.userDomainId, + }) + }) + await client.init() + + assert.equal(addition.user_domain_id, "domain-reborn") + assert.equal( + addition.authorization, + Buffer.from("pc:13800138000:test-token").toString("base64"), + ) + assert.equal(states.length, 1) + assert.equal(states[0].userDomainId, "domain-reborn") + assert.equal( + calls[0], + "https://user-njs.yun.139.com/user/auth/refreshToken", + ) + assert.equal(calls.filter((url) => url === ROUTE_POLICY_URL).length, 1) + } finally { + globalThis.fetch = originalFetch + } +}) + +test("Yun139 keeps a manually configured user_domain_id when Authorization is present", async () => { + let fetchCalls = 0 + const result = await loginWithCredentials( + { + authorization: "existing-authorization", + user_domain_id: "manual-domain", + type: "personal_new", + }, + (async () => { + fetchCalls++ + return new Response("should not be called", { status: 500 }) + }) as typeof fetch, + ) + assert.equal(result.authorization, "existing-authorization") + assert.equal(result.userDomainId, "manual-domain") + assert.equal(fetchCalls, 0) +}) + +test("Yun139 rejects empty credentials with a clear error", async () => { + await assert.rejects( + () => + new Yun139ApiClient({ + authorization: "", + type: "personal_new", + }).init(), + /authorization is empty and credentials are not provided/, + ) +}) + +test("Yun139 init logs in and persists authorization plus user_domain_id", async () => { + const addition: Yun139Addition = { + authorization: "", + username: "13800138000", + password: "secret", + type: "personal_new", + } + const originalFetch = globalThis.fetch + const calls: string[] = [] + globalThis.fetch = (async (input: string | URL | Request) => { + const url = String(input) + calls.push(url) + if (url === MAIL_PASSWORD_URL) { + return new Response(null, { + status: 302, + headers: { + Location: "https://mail.10086.cn/default.html?sid=test-sid", + "Set-Cookie": "RMKEY=test-rmkey; Path=/", + }, + }) + } + if (url.startsWith("https://smsrebuild1.mail.10086.cn/setting/s?")) { + return new Response( + JSON.stringify({ var: { artifact: "test-artifact" } }), + { status: 200 }, + ) + } + if (url === SSO_LOGIN_URL) { + return new Response( + encryptedStep3Response({ + authToken: "test-token", + account: "13800138000", + userDomainId: "domain-123", + }), + { status: 200 }, + ) + } + if (url === ROUTE_POLICY_URL) return routePolicyResponse() + return new Response("unexpected URL", { status: 404 }) + }) as typeof fetch + + try { + const states: Array<{ authorization: string; userDomainId: string }> = [] + const client = new Yun139ApiClient(addition, undefined, async (state) => { + states.push({ + authorization: state.authorization, + userDomainId: state.userDomainId, + }) + }) + await client.init() + + assert.equal(addition.user_domain_id, "domain-123") + assert.equal( + addition.authorization, + Buffer.from("pc:13800138000:test-token").toString("base64"), + ) + assert.equal(states.length, 1) + assert.equal(states[0].userDomainId, "domain-123") + assert.equal(states[0].authorization, addition.authorization) + assert.equal(calls.filter((url) => url === ROUTE_POLICY_URL).length, 1) + } finally { + globalThis.fetch = originalFetch + } +}) diff --git a/src/backend/drivers/139/login.ts b/src/backend/drivers/139/login.ts new file mode 100644 index 00000000..c031e1b6 --- /dev/null +++ b/src/backend/drivers/139/login.ts @@ -0,0 +1,893 @@ +import CryptoJS from "crypto-js" +import { Yun139Addition } from "./types" + +const KEY_HEX_1 = "73634235495062495331515373756c734e7253306c673d3d" +const KEY_HEX_2 = "7150714477323633586746674c337538" +const MAIL_PUBLIC_KEY = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnsOHTFtwW5rq/8gGhPlM5Z3RPdeN/d+FYIHb5JmcfBOCozXuT8c+0anvxtkjzghixwNlnmBuhN8OYfS789YuH/qReQbHC7OdlisLildNWPHRNUYcPa0W3lXSG3+81CXK7FDXPvXo5ubw2GqVbIsccMarI1dyfXdi4ITiCXvmM9wYBdUs9yXtoorhlpyYUI2GV8HNsQjWK9P5QZHT3ox5Qy+mjRmvv6RUFJLPOMkOS/pGZ0DwC1ypFZBxstW0/ftVupdOmGWvW7J2/e3dq3A/UvIkC4YUY/diL1wighJx1G9MiRROISjNMvNyUDSTqPJy516+l3sgHEbc067QIJx2NQIDAQAB" +const MAIL_PASSWORD_URL = "https://mail.10086.cn/Login/Login.ashx" +const MAIL_SMS_URL = "https://mail.10086.cn/s" +const SSO_LOGIN_URL = "https://user-njs.yun.139.com/user/thirdlogin" + +export interface Yun139LoginResult { + authorization: string + account: string + userDomainId: string + mailCookies: string + smsCode: string +} + +/** 邮箱登录触发了短信风控,需要用户填写 sms_code 后重新保存。 */ +export class Yun139SmsRequiredError extends Error { + constructor( + message: string, + public readonly mailCookies: string, + ) { + super(message) + this.name = "Yun139SmsRequiredError" + } +} + +type MailLoginResponse = { + code: string + summary: string + loginSuccessUrl: string +} + +export function sha1Hex(value: string): string { + return CryptoJS.SHA1(value).toString(CryptoJS.enc.Hex) +} + +export function sortedJsonStringify(value: any): string { + if (value === null || value === undefined) return "null" + if (typeof value === "string") { + try { + return sortedJsonStringify(JSON.parse(value)) + } catch { + return JSON.stringify(value) + } + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value) + } + if (Array.isArray(value)) { + return `[${value.map((item) => sortedJsonStringify(item)).join(",")}]` + } + if (typeof value === "object") { + const keys = Object.keys(value).sort() + return `{${keys + .map((key) => `${JSON.stringify(key)}:${sortedJsonStringify(value[key])}`) + .join(",")}}` + } + return JSON.stringify(value) +} + +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") +} + +function hexToBytes(hex: string): Uint8Array { + if (hex.length % 2 !== 0) throw new Error("invalid hex string") + const bytes = new Uint8Array(hex.length / 2) + for (let i = 0; i < bytes.length; i++) { + const value = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16) + if (Number.isNaN(value)) throw new Error("invalid hex string") + bytes[i] = value + } + return bytes +} + +function bytesToWordArray(bytes: Uint8Array): CryptoJS.lib.WordArray { + return CryptoJS.enc.Hex.parse(bytesToHex(bytes)) +} + +function bytesToBase64(bytes: Uint8Array): string { + return CryptoJS.enc.Base64.stringify(bytesToWordArray(bytes)) +} + +function base64ToBytes(value: string): Uint8Array { + const hex = CryptoJS.enc.Base64.parse(value).toString(CryptoJS.enc.Hex) + return hexToBytes(hex) +} + +function concatBytes(...parts: Uint8Array[]): Uint8Array { + const total = parts.reduce((sum, part) => sum + part.length, 0) + const result = new Uint8Array(total) + let offset = 0 + for (const part of parts) { + result.set(part, offset) + offset += part.length + } + return result +} + +function randomBytes(length: number): Uint8Array { + const bytes = new Uint8Array(length) + crypto.getRandomValues(bytes) + return bytes +} + +export function aesCbcEncrypt( + plaintext: string, + keyHex: string, +): { iv: Uint8Array; ciphertext: Uint8Array } { + const key = CryptoJS.enc.Hex.parse(keyHex) + const iv = randomBytes(16) + const encrypted = CryptoJS.AES.encrypt( + CryptoJS.enc.Utf8.parse(plaintext), + key, + { + iv: bytesToWordArray(iv), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }, + ) + return { + iv, + ciphertext: hexToBytes(encrypted.ciphertext.toString(CryptoJS.enc.Hex)), + } +} + +export function aesCbcDecrypt( + ciphertext: Uint8Array, + keyHex: string, + iv: Uint8Array, +): string { + const decrypted = CryptoJS.AES.decrypt( + { ciphertext: bytesToWordArray(ciphertext) } as any, + CryptoJS.enc.Hex.parse(keyHex), + { + iv: bytesToWordArray(iv), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }, + ) + return decrypted.toString(CryptoJS.enc.Utf8) +} + +export function aesEcbDecrypt(ciphertext: Uint8Array, keyHex: string): string { + const decrypted = CryptoJS.AES.decrypt( + { ciphertext: bytesToWordArray(ciphertext) } as any, + CryptoJS.enc.Hex.parse(keyHex), + { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }, + ) + return decrypted.toString(CryptoJS.enc.Utf8) +} + +export function aesEcbEncrypt(plaintext: string, keyHex: string): Uint8Array { + const encrypted = CryptoJS.AES.encrypt( + CryptoJS.enc.Utf8.parse(plaintext), + CryptoJS.enc.Hex.parse(keyHex), + { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }, + ) + return hexToBytes(encrypted.ciphertext.toString(CryptoJS.enc.Hex)) +} + +type DerElement = { tag: number; start: number; end: number } + +function readDer(bytes: Uint8Array, offset: number): DerElement { + if (offset + 2 > bytes.length) throw new Error("invalid DER") + const tag = bytes[offset] + let length = bytes[offset + 1] + offset += 2 + if (length & 0x80) { + const count = length & 0x7f + if (count === 0 || offset + count > bytes.length) { + throw new Error("invalid DER length") + } + length = 0 + for (let i = 0; i < count; i++) { + length = (length << 8) | bytes[offset + i] + } + offset += count + } + if (offset + length > bytes.length) throw new Error("invalid DER payload") + return { tag, start: offset, end: offset + length } +} + +function bytesToBigInt(bytes: Uint8Array): bigint { + let hex = bytesToHex(bytes) + hex = hex.replace(/^0+/, "") + return hex ? BigInt(`0x${hex}`) : 0n +} + +function modPow(base: bigint, exponent: bigint, modulus: bigint): bigint { + let result = 1n + let b = base % modulus + let e = exponent + while (e > 0n) { + if (e & 1n) result = (result * b) % modulus + b = (b * b) % modulus + e >>= 1n + } + return result +} + +export function parseRsaPublicKey(der: Uint8Array): { + modulus: bigint + exponent: bigint +} { + const outer = readDer(der, 0) + if (outer.tag !== 0x30) throw new Error("invalid RSA public key") + const algorithm = readDer(der, outer.start) + const bitString = readDer(der, algorithm.end) + if (bitString.tag !== 0x03) + throw new Error("invalid RSA public key bit string") + const inner = readDer(der, bitString.start + 1) // skip unused-bits byte + const modulusElement = readDer(der, inner.start) + const exponentElement = readDer(der, modulusElement.end) + if (modulusElement.tag !== 0x02 || exponentElement.tag !== 0x02) { + throw new Error("invalid RSA public key integers") + } + return { + modulus: bytesToBigInt(der.slice(modulusElement.start, modulusElement.end)), + exponent: bytesToBigInt( + der.slice(exponentElement.start, exponentElement.end), + ), + } +} + +/** RSAES-PKCS1-v1_5,对应 Go 的 rsa.EncryptPKCS1v15。 */ +export function rsaEncryptPKCS1v15( + message: Uint8Array, + publicKeyDerBase64: string, +): Uint8Array { + const der = base64ToBytes(publicKeyDerBase64) + const { modulus, exponent } = parseRsaPublicKey(der) + const keyLength = Math.ceil(modulus.toString(16).length / 2) + if (message.length > keyLength - 11) { + throw new Error("RSA message too long") + } + + const padding = randomBytes(keyLength - message.length - 3) + for (let i = 0; i < padding.length; i++) { + while (padding[i] === 0) padding[i] = randomBytes(1)[0] + } + + const encoded = new Uint8Array(keyLength) + encoded[0] = 0 + encoded[1] = 2 + encoded.set(padding, 2) + encoded[2 + padding.length] = 0 + encoded.set(message, 3 + padding.length) + + const ciphertext = modPow(bytesToBigInt(encoded), exponent, modulus).toString( + 16, + ) + return hexToBytes(ciphertext.padStart(keyLength * 2, "0")) +} + +export function parseCookies( + raw: string, +): Array<{ name: string; value: string }> { + const cookies: Array<{ name: string; value: string }> = [] + for (const part of (raw || "").split(";")) { + const item = part.trim() + if (!item) continue + const index = item.indexOf("=") + if (index <= 0) continue + const name = item.slice(0, index).trim() + const value = item.slice(index + 1).trim() + if (!name) continue + const existing = cookies.find((cookie) => cookie.name === name) + if (existing) existing.value = value + else cookies.push({ name, value }) + } + return cookies +} + +export function cookiesToString( + cookies: Array<{ name: string; value: string }>, +): string { + return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join(";") +} + +export function setCookie(raw: string, name: string, value: string): string { + const cookies = parseCookies(raw) + const existing = cookies.find((cookie) => cookie.name === name) + if (existing) existing.value = value + else cookies.push({ name, value }) + return cookiesToString(cookies) +} + +export function getCookie(raw: string, name: string): string { + return parseCookies(raw).find((cookie) => cookie.name === name)?.value || "" +} + +export function hasCookiePair(raw: string): boolean { + return parseCookies(raw).some( + (cookie) => cookie.name !== "" && cookie.value !== "", + ) +} + +export function extractFastLoginCookies(mailCookies: string): { + sid: string + rmkey: string +} { + const cookies = parseCookies(mailCookies) + return { + sid: + cookies.find((cookie) => cookie.name === "Os_SSo_Sid")?.value || + cookies.find((cookie) => cookie.name === "sid")?.value || + "", + rmkey: cookies.find((cookie) => cookie.name === "RMKEY")?.value || "", + } +} + +function getSetCookieHeaders(response: Response): string[] { + const headers = response.headers as Headers & { + getAll?: (name: string) => string[] + getSetCookie?: () => string[] + } + // Cloudflare Workers exposes Set-Cookie through the non-standard getAll(). + if (typeof headers.getAll === "function") { + try { + return headers.getAll("Set-Cookie") + } catch { + // fall through to the standard accessors + } + } + if (typeof headers.getSetCookie === "function") { + return headers.getSetCookie() + } + const raw = response.headers.get("set-cookie") + if (!raw) return [] + // Fallback for runtimes without getSetCookie: the cookies used here do not + // contain commas in their values, so split before the next `name=`. + return raw.split(/,(?=\s*[^;,=\s]+=)/) +} + +export function mergeResponseCookies( + mailCookies: string, + response: Response, +): string { + let merged = mailCookies + for (const header of getSetCookieHeaders(response)) { + const pair = header.split(";")[0].trim() + const index = pair.indexOf("=") + if (index <= 0) continue + const name = pair.slice(0, index).trim() + const value = pair.slice(index + 1).trim() + if (!name) continue + merged = setCookie(merged, name, value) + } + return merged +} + +function escapeXml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") +} + +function mailXmlField(name: string, value: string): string { + return `${escapeXml(value)}` +} + +function parseMailLoginResponse(text: string): MailLoginResponse { + try { + const json = JSON.parse(text) + const response = { + code: String(json?.code || ""), + summary: String(json?.summary || ""), + loginSuccessUrl: String(json?.var?.loginSuccessUrl || ""), + } + if (response.code || response.summary || response.loginSuccessUrl) { + return response + } + } catch { + // Fall back to the regex extraction used by the Go driver. + } + const pick = (name: string): string => + new RegExp(`['"]?${name}['"]?\\s*:\\s*['"]([^'"]+)`).exec(text)?.[1] || "" + return { + code: pick("code"), + summary: pick("summary"), + loginSuccessUrl: pick("loginSuccessUrl"), + } +} + +function smsSceneForRisk(riskCode: string): number | null { + switch (riskCode) { + case "PML401010062": + return 2 + case "MW0016": + return 4 + case "S025": + case "S035": + return 1 + default: + return null + } +} + +function mailRiskCode(location: string): string { + try { + return new URL(location, MAIL_PASSWORD_URL).searchParams.get("ec") || "" + } catch { + return "" + } +} + +function mailHeaders(cookie: string): Record { + const headers: Record = { + "Content-Type": "application/xml; charset=utf-8", + "Accept-Encoding": "gzip", + "User-Agent": "okhttp/4.12.0", + } + if (cookie) headers.Cookie = cookie + return headers +} + +function base64Of(value: string): string { + return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(value)) +} + +async function sendSmsCode( + username: string, + riskCode: string, + mailCookies: string, + fetchImpl: typeof fetch, +): Promise { + const scene = smsSceneForRisk(riskCode) + if (scene === null) { + throw new Error( + `139 Mail risk control does not support SMS verification: ${riskCode}`, + ) + } + const loginName = bytesToBase64( + rsaEncryptPKCS1v15(new TextEncoder().encode(username), MAIL_PUBLIC_KEY), + ) + const body = [ + "", + mailXmlField("loginName", loginName), + mailXmlField("fv", "4"), + mailXmlField("clientId", "1003"), + mailXmlField("eMode", "1"), + mailXmlField("loginFailureUrl", ""), + mailXmlField("loginSuccessUrl", ""), + mailXmlField("verifyCode", ""), + mailXmlField("version", "1.0"), + mailXmlField("scene", String(scene)), + "", + ].join("") + const url = `${MAIL_SMS_URL}?func=${encodeURIComponent("login:sendSmsCodeByScene")}&cguid=${Date.now()}` + const response = await fetchImpl(url, { + method: "POST", + headers: mailHeaders(mailCookies), + body, + }) + const merged = mergeResponseCookies(mailCookies, response) + const result = parseMailLoginResponse(await response.text()) + if (result.code === "S_OK") return merged + switch (result.code) { + case "PML401010021": + case "PML401010022": + throw new Error( + `139 Mail requires picture verification before SMS can be sent: ${result.code}`, + ) + case "PML404010001": + throw new Error( + "139 Mail SMS verification code was requested too frequently", + ) + case "PML401010002": + throw new Error("139 Mail rejected the SMS verification parameters") + default: + throw new Error( + `send 139 Mail SMS verification code failed: code=${result.code} summary=${result.summary}`, + ) + } +} + +async function verifySmsCode( + username: string, + riskCode: string, + smsCode: string, + mailCookies: string, + fetchImpl: typeof fetch, +): Promise<{ sid: string; mailCookies: string }> { + if (smsSceneForRisk(riskCode) === null) { + throw new Error( + `139 Mail risk control does not support SMS verification: ${riskCode}`, + ) + } + const loginName = bytesToBase64( + rsaEncryptPKCS1v15(new TextEncoder().encode(username), MAIL_PUBLIC_KEY), + ) + const fields = [ + "", + mailXmlField("clientId", "1003"), + mailXmlField("version", "4"), + mailXmlField("loginType", "0"), + mailXmlField("authType", "2"), + mailXmlField("loginName", loginName), + mailXmlField("eMode", "1"), + mailXmlField("loginPassword", sha1Hex(`fetion.com.cn:${smsCode}`)), + mailXmlField("createAutoLoginSecretKey", "1"), + mailXmlField("verifyCode", ""), + mailXmlField("verifyAgentId", ""), + mailXmlField("reqFrom", "3"), + mailXmlField("needWCookie", "1"), + ] + if (riskCode === "MW0016") fields.push(mailXmlField("pwdType", "1")) + fields.push("") + + const url = `${MAIL_SMS_URL}?func=${encodeURIComponent("/login/inlogin.action")}&cguid=${Date.now()}` + const response = await fetchImpl(url, { + method: "POST", + headers: mailHeaders(mailCookies), + body: fields.join(""), + }) + const merged = mergeResponseCookies(mailCookies, response) + const result = parseMailLoginResponse(await response.text()) + if (result.code !== "S_OK") { + throw new Error( + `verify 139 Mail SMS code failed: code=${result.code} summary=${result.summary}`, + ) + } + + let sid = "" + if (result.loginSuccessUrl) { + try { + sid = + new URL(result.loginSuccessUrl, MAIL_PASSWORD_URL).searchParams.get( + "sid", + ) || "" + } catch { + sid = "" + } + } + if (!sid) { + sid = getCookie(merged, "Os_SSo_Sid") || getCookie(merged, "sid") || "" + } + if (!sid) { + throw new Error( + "139 Mail SMS verification succeeded but did not return sid", + ) + } + return { sid, mailCookies: merged } +} + +async function step1PasswordLogin( + addition: Yun139Addition, + fetchImpl: typeof fetch, +): Promise<{ sid: string; mailCookies: string }> { + const username = (addition.username || "").trim() + const password = addition.password || "" + const cguid = String(Date.now()) + const referer = + `https://mail.10086.cn/default.html?&s=1&v=0&u=${base64Of(username)}` + + `&m=1&ec=S001&resource=indexLogin&clientid=1003&auto=on&cguid=${cguid}&mtime=45` + + const form = new URLSearchParams({ + UserName: username, + passOld: "", + auto: "on", + Password: sha1Hex(`fetion.com.cn:${password}`), + webIndexPagePwdLogin: "1", + pwdType: "1", + clientId: "1003", + authType: "2", + }) + + let mailCookies = (addition.mail_cookies || "").trim() + const headers: Record = { + 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,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6,en-GB;q=0.5", + "cache-control": "max-age=0", + "content-type": "application/x-www-form-urlencoded", + dnt: "1", + origin: "https://mail.10086.cn", + priority: "u=0, i", + referer, + "sec-ch-ua": + '"Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "document", + "sec-fetch-mode": "navigate", + "sec-fetch-site": "same-origin", + "sec-fetch-user": "?1", + "upgrade-insecure-requests": "1", + "user-agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0", + } + if (mailCookies) headers.Cookie = mailCookies + + const response = await fetchImpl(MAIL_PASSWORD_URL, { + method: "POST", + headers, + body: form.toString(), + redirect: "manual", + }) + mailCookies = mergeResponseCookies(mailCookies, response) + + const location = response.headers.get("location") || "" + const riskCode = mailRiskCode(location) + if (riskCode) { + if (smsSceneForRisk(riskCode) === null) { + throw new Error(`139 Mail risk control triggered: ${riskCode}`) + } + if (!(addition.sms_code || "").trim()) { + mailCookies = await sendSmsCode( + username, + riskCode, + mailCookies, + fetchImpl, + ) + throw new Yun139SmsRequiredError( + "139 Mail SMS verification code sent; fill sms_code and save the storage again", + mailCookies, + ) + } + const verified = await verifySmsCode( + username, + riskCode, + (addition.sms_code || "").trim(), + mailCookies, + fetchImpl, + ) + return { sid: verified.sid, mailCookies: verified.mailCookies } + } + + let sid = "" + if (location) { + try { + sid = new URL(location, MAIL_PASSWORD_URL).searchParams.get("sid") || "" + } catch { + sid = "" + } + } + if (!sid) { + sid = + getCookie(mailCookies, "Os_SSo_Sid") || + getCookie(mailCookies, "sid") || + "" + } + if (!sid) { + throw new Error("failed to extract sid from login response") + } + return { sid, mailCookies } +} + +async function step2GetArtifact( + sid: string, + mailCookies: string, + fetchImpl: typeof fetch, +): Promise { + const rmkey = getCookie(mailCookies, "RMKEY") + if (!rmkey) throw new Error("RMKEY not found in MailCookies") + const url = + "https://smsrebuild1.mail.10086.cn/setting/s?" + + `func=${encodeURIComponent("umc:getArtifact")}` + + `&sid=${encodeURIComponent(sid)}&cguid=${Date.now()}` + const response = await fetchImpl(url, { + method: "POST", + headers: mailHeaders(`RMKEY=${rmkey}`), + }) + const text = await response.text() + let artifact = "" + try { + artifact = String(JSON.parse(text)?.var?.artifact || "") + } catch { + artifact = "" + } + if (!artifact) + throw new Error("failed to extract dycpwd from artifact exchange response") + return artifact +} + +async function encryptedRequest( + url: string, + body: any, + headers: Record, + keyHex: string, + fetchImpl: typeof fetch, +): Promise { + const { iv, ciphertext } = aesCbcEncrypt(sortedJsonStringify(body), keyHex) + const payload = bytesToBase64(concatBytes(iv, ciphertext)) + const response = await fetchImpl(url, { + method: "POST", + headers, + body: payload, + }) + if (response.status !== 200) { + throw new Error( + `139 encrypted request failed (${response.status}): ${await response.text()}`, + ) + } + const text = (await response.text()).trim() + let jsonText: string + if (text.startsWith("{")) { + jsonText = text + } else { + const decoded = base64ToBytes(text) + if (decoded.length < 16) { + throw new Error( + `139 encrypted response is too short to contain an IV (${decoded.length})`, + ) + } + jsonText = aesCbcDecrypt(decoded.slice(16), keyHex, decoded.slice(0, 16)) + } + try { + return JSON.parse(jsonText) + } catch { + throw new Error("139 encrypted response is not valid JSON") + } +} + +async function step3ThirdPartyLogin( + username: string, + dycpwd: string, + fetchImpl: typeof fetch, +): Promise<{ authorization: string; account: string; userDomainId: string }> { + const body = { + clientkey_decrypt: "l3TryM&Q+X7@dzwk)qP", + clienttype: "886", + cpid: "507", + dycpwd, + extInfo: { ifOpenAccount: "0" }, + loginMode: "0", + msisdn: username, + pintype: "13", + secinfo: sha1Hex(`fetion.com.cn:${dycpwd}`).toUpperCase(), + version: "20250901", + } + const headers: Record = { + "hcy-cool-flag": "1", + "x-huawei-channelSrc": "10246600", + "x-sdk-channelSrc": "", + "x-MM-Source": "0", + "x-UserAgent": "android|23116PN5BC|android15|1.2.6|||1440x3200|10246600", + "x-DeviceInfo": + "4|127.0.0.1|5|1.2.6|Xiaomi|23116PN5BC||02-00-00-00-00-00|android 15|1440x3200|android|||", + "Content-Type": "text/plain;charset=UTF-8", + "Accept-Encoding": "gzip", + "User-Agent": "okhttp/3.12.2", + } + + const firstLayer = await encryptedRequest( + SSO_LOGIN_URL, + body, + headers, + KEY_HEX_1, + fetchImpl, + ) + const hexInner = String(firstLayer?.data || "") + if (!hexInner) { + throw new Error("missing data field in first layer decryption result") + } + const final = JSON.parse(aesEcbDecrypt(hexToBytes(hexInner), KEY_HEX_2)) + const authToken = String(final?.authToken || "") + const account = String(final?.account || "") + const userDomainId = String(final?.userDomainId || "") + if (!authToken || !account || !userDomainId) { + throw new Error( + "failed to extract authToken, account or userDomainId from final decryption result", + ) + } + return { + authorization: base64Of(`pc:${account}:${authToken}`), + account, + userDomainId, + } +} + +async function loginWithPassword( + addition: Yun139Addition, + fetchImpl: typeof fetch, +): Promise { + const username = (addition.username || "").trim() + const password = addition.password || "" + if (!username || !password) { + throw new Error("username or password is empty") + } + const step1 = await step1PasswordLogin(addition, fetchImpl) + const artifact = await step2GetArtifact( + step1.sid, + step1.mailCookies, + fetchImpl, + ) + const step3 = await step3ThirdPartyLogin(username, artifact, fetchImpl) + return { + authorization: step3.authorization, + account: step3.account, + userDomainId: step3.userDomainId, + mailCookies: step1.mailCookies, + smsCode: "", + } +} + +async function tryFastLoginWithCookies( + addition: Yun139Addition, + fetchImpl: typeof fetch, +): Promise { + const mailCookies = (addition.mail_cookies || "").trim() + const { sid, rmkey } = extractFastLoginCookies(mailCookies) + if (!sid || !rmkey) return null + try { + const artifact = await step2GetArtifact(sid, mailCookies, fetchImpl) + const step3 = await step3ThirdPartyLogin( + (addition.username || "").trim(), + artifact, + fetchImpl, + ) + return { + authorization: step3.authorization, + account: step3.account, + userDomainId: step3.userDomainId, + mailCookies, + smsCode: "", + } + } catch { + return null + } +} + +/** + * 对齐 Go 主项目的凭证初始化: + * - Authorization 非空:直接使用; + * - MailCookies 含 SID/RMKEY:先尝试快速登录; + * - Username + Password:完整密码登录(必要时走短信风控)。 + */ +export async function loginWithCredentials( + addition: Yun139Addition, + fetchImpl: typeof fetch = fetch, + options: { ignoreAuthorization?: boolean; skipFastLogin?: boolean } = {}, +): Promise { + const authorization = (addition.authorization || "").trim() + const username = (addition.username || "").trim() + const password = addition.password || "" + const mailCookies = (addition.mail_cookies || "").trim() + + if (authorization && !options.ignoreAuthorization) { + if (/^basic\s/i.test(authorization)) { + throw new Error("authorization should not include Basic prefix") + } + return { + authorization, + account: "", + userDomainId: (addition.user_domain_id || "").trim(), + mailCookies, + smsCode: (addition.sms_code || "").trim(), + } + } + + if (mailCookies && !hasCookiePair(mailCookies)) { + throw new Error( + "MailCookies format is invalid, please check your configuration", + ) + } + const hasUsername = username !== "" + const hasPassword = password.trim() !== "" + if (hasUsername !== hasPassword) { + throw new Error("username and password must be provided together") + } + + if (mailCookies && !options.skipFastLogin) { + const fast = await tryFastLoginWithCookies(addition, fetchImpl) + if (fast) return fast + } + if (hasUsername) { + return loginWithPassword(addition, fetchImpl) + } + if (mailCookies) { + throw new Error( + "fast login with cookies failed, and cannot fallback to password login (missing username/password)", + ) + } + throw new Error("authorization is empty and credentials are not provided") +} diff --git a/src/backend/drivers/139/types.ts b/src/backend/drivers/139/types.ts index dceeba76..10f8d19d 100644 --- a/src/backend/drivers/139/types.ts +++ b/src/backend/drivers/139/types.ts @@ -12,6 +12,7 @@ export interface Yun139Addition { authorization: string username?: string password?: string + sms_code?: string mail_cookies?: string root_folder_id?: string type?: "personal_new" | "family" | "group" | "personal" | "share" diff --git a/src/backend/drivers/139/util.ts b/src/backend/drivers/139/util.ts index edc6cca4..cb5f4466 100644 --- a/src/backend/drivers/139/util.ts +++ b/src/backend/drivers/139/util.ts @@ -1,4 +1,10 @@ import CryptoJS from "crypto-js" +import { DOMParser } from "@xmldom/xmldom" +import { + loginWithCredentials, + Yun139LoginResult, + Yun139SmsRequiredError, +} from "./login" import { Yun139Addition, QueryRoutePolicyResp, @@ -58,6 +64,26 @@ export function formatTime(d: Date): string { // host 就是硬失败,绝不允许静默回退到这个占位域名。 const YUN139_DEFAULT_HOST = "https://yun.139.com" +/** + * 旧版 token 刷新接口,主项目 Go 驱动仍在用:只需要当前 token 与账号, + * 不要求 user_domain_id。用于替代 PC refreshToken 接口不可用时的硬失败。 + */ +const YUN139_LEGACY_REFRESH_URL = + "https://aas.caiyun.feixin.10086.cn/tellin/authTokenRefresh.do" + +/** 旧接口失败或刚刷新后,至少隔这么久再试,避免每次文件操作都请求一次。 */ +const YUN139_LEGACY_REFRESH_COOLDOWN_MS = 30 * 60 * 1000 + +/** 读取旧版刷新接口 XML 响应中第一个同名标签的文本。 */ +function readXmlTag(xml: string, tag: string): string { + try { + const doc = new DOMParser().parseFromString(xml, "text/xml") + return (doc.getElementsByTagName(tag)[0]?.textContent || "").trim() + } catch { + return "" + } +} + /** 139 目录 id 的"根"写法:这些父 id 不做内容归属校验(根列表出现什么都不奇怪)。 */ const ROOT_PARENT_IDS = new Set(["/", "0", "root", "cloud139_root"]) @@ -110,13 +136,17 @@ export class Yun139ApiClient { public groupHost = YUN139_DEFAULT_HOST public account = "" private onAuthorizationRefresh?: (authorization: string) => Promise + private onLoginStateChange?: (state: Yun139LoginResult) => Promise + private legacyRefreshNextAttemptAt = 0 constructor( addition: Yun139Addition, onAuthorizationRefresh?: (authorization: string) => Promise, + onLoginStateChange?: (state: Yun139LoginResult) => Promise, ) { this.addition = addition this.onAuthorizationRefresh = onAuthorizationRefresh + this.onLoginStateChange = onLoginStateChange this.extractAccount() } @@ -401,20 +431,106 @@ export class Yun139ApiClient { if (pieces.length < 3) return const token = pieces.slice(2).join(":") - const tokenFields = token.split("|") - const expiresAt = Number(tokenFields[3]) - if (!Number.isFinite(expiresAt) || expiresAt <= 0) return + const expiresAt = this.tokenExpiresAt(token) + if (expiresAt <= 0) return const refreshWindowMs = 15 * 24 * 60 * 60 * 1000 if (expiresAt - Date.now() > refreshWindowMs) return const userDomainId = (this.addition.user_domain_id || "").trim() - if (!userDomainId) { - throw new Error( - "139 Cloud token is near expiry; configure user_domain_id to use the PC refreshToken API", + if (userDomainId) { + try { + await this.refreshWithPcApi(auth, pieces, userDomainId) + return + } catch (e) { + if (await this.tryReLogin()) return + throw e + } + } + + // user_domain_id 只是 PC refreshToken 接口的必填项,不是自动续期的前提。 + // 留空时改走主项目仍在使用的旧版刷新接口(只需要当前 token)。旧接口 + // 失败也静默保留现有 token:不再因为“即将过期”打断初始化或刷告警。 + if (Date.now() < this.legacyRefreshNextAttemptAt) return + try { + const newToken = await this.refreshWithLegacyApi(token, expiresAt) + await this.applyRefreshedAuthorization(pieces, newToken) + this.legacyRefreshNextAttemptAt = + Date.now() + YUN139_LEGACY_REFRESH_COOLDOWN_MS + } catch (e) { + this.legacyRefreshNextAttemptAt = + Date.now() + YUN139_LEGACY_REFRESH_COOLDOWN_MS + if (await this.tryReLogin()) return + console.debug( + "[139] automatic token refresh skipped:", + e instanceof Error ? e.message : String(e), + ) + } + } + + /** + * 对齐 Go 的 loginAfterAuthorizationFailure:刷新失败时,如果配置里还有 + * 邮箱 Cookie 或用户名/密码,就重新登录一次并保存新的 authorization 与 + * user_domain_id;没有可用凭证时返回 false,由调用方保持原有降级行为。 + */ + private async tryReLogin(): Promise { + const hasUsername = (this.addition.username || "").trim() !== "" + const hasPassword = (this.addition.password || "") !== "" + // 对齐 Go 的 loginAfterAuthorizationFailure:刷新失败时只走密码登录回退, + // 不重新尝试 Cookie 快速登录;mail_cookies 仅作为密码登录的设备上下文。 + if (!hasUsername || !hasPassword) return false + try { + const result = await loginWithCredentials(this.addition, fetch, { + ignoreAuthorization: true, + skipFastLogin: true, + }) + this.addition.authorization = result.authorization + this.addition.user_domain_id = result.userDomainId + this.addition.mail_cookies = result.mailCookies + this.addition.sms_code = "" + this.account = result.account || this.account + await this.onLoginStateChange?.(result) + return true + } catch (e) { + if (e instanceof Yun139SmsRequiredError) { + this.addition.mail_cookies = e.mailCookies + await this.onLoginStateChange?.({ + authorization: "", + account: this.account, + userDomainId: (this.addition.user_domain_id || "").trim(), + mailCookies: e.mailCookies, + smsCode: (this.addition.sms_code || "").trim(), + }) + } + console.debug( + "[139] re-login after refresh failure skipped:", + e instanceof Error ? e.message : String(e), ) + return false } + } + /** 从 `|...||...` 中读取毫秒时间戳,非法时返回 0。 */ + private tokenExpiresAt(token: string): number { + const expiresAt = Number(token.split("|")[3]) + return Number.isFinite(expiresAt) && expiresAt > 0 ? expiresAt : 0 + } + + private async applyRefreshedAuthorization( + pieces: string[], + newToken: string, + ): Promise { + this.addition.authorization = CryptoJS.enc.Base64.stringify( + CryptoJS.enc.Utf8.parse(`${pieces[0]}:${this.account}:${newToken}`), + ) + await this.onAuthorizationRefresh?.(this.addition.authorization) + } + + private async refreshWithPcApi( + auth: string, + pieces: string[], + userDomainId: string, + ): Promise { const authHeader = `Basic ${auth}` // Match the minimal header set from the successful PowerShell request. @@ -461,24 +577,106 @@ export class Yun139ApiClient { `139 Cloud PC token refresh failed: code=${result.code || "unknown"}, message=${result.message || "empty token"}`, ) } - const newExpiresAt = Number(newToken.split("|")[3]) - if (!Number.isFinite(newExpiresAt) || newExpiresAt <= Date.now()) { + if (this.tokenExpiresAt(newToken) <= Date.now()) { throw new Error( "139 Cloud PC refresh returned a token without a valid future expiry", ) } - this.addition.authorization = CryptoJS.enc.Base64.stringify( - CryptoJS.enc.Utf8.parse(`${pieces[0]}:${this.account}:${newToken}`), + await this.applyRefreshedAuthorization(pieces, newToken) + } + + /** + * 旧版刷新接口(对齐主项目 Go 驱动):使用 XML 请求,不需要 user_domain_id。 + * 响应里 `` 与 `` 都可能是新 token,只接受过期时间比 + * 旧 token 更晚的值,避免把原 token 或非 pipe 格式的字段写回 Authorization。 + */ + private async refreshWithLegacyApi( + token: string, + currentExpiresAt: number, + ): Promise { + const body = + `${token}${this.account}` + + "656" + + const response = await fetch(YUN139_LEGACY_REFRESH_URL, { + method: "POST", + headers: { + "Content-Type": "application/xml;charset=UTF-8", + Referer: "https://yun.139.com/", + }, + body, + }) + + const text = await response.text() + if (!response.ok) { + throw new Error( + `139 Cloud legacy token refresh failed (${response.status}): ${text}`, + ) + } + + const code = readXmlTag(text, "return") + if (code !== "0") { + throw new Error( + `139 Cloud legacy token refresh failed: return=${code || "unknown"}, ` + + `desc=${readXmlTag(text, "desc") || "empty"}`, + ) + } + + for (const candidate of [ + readXmlTag(text, "token"), + readXmlTag(text, "accessToken"), + ]) { + if ( + this.tokenExpiresAt(candidate) > Math.max(currentExpiresAt, Date.now()) + ) { + return candidate + } + } + + throw new Error( + "139 Cloud legacy refresh returned no token with a valid future expiry", ) - await this.onAuthorizationRefresh?.(this.addition.authorization) } - async init(): Promise { - if (!this.addition.authorization) { - throw new Error("139 Cloud Authorization is required") + /** + * 对齐主项目:Authorization 为空时用邮箱 Cookie / 用户名密码登录,并在登录 + * 成功后保存 Authorization、user_domain_id 与更新后的 MailCookies。 + */ + private async ensureAuthorization(): Promise { + const authorization = (this.addition.authorization || "").trim() + if (authorization) { + if (/^basic\s/i.test(authorization)) { + throw new Error("authorization should not include Basic prefix") + } + return } + try { + const result = await loginWithCredentials(this.addition) + this.addition.authorization = result.authorization + this.addition.user_domain_id = result.userDomainId + this.addition.mail_cookies = result.mailCookies + this.addition.sms_code = "" + this.account = result.account || this.account + await this.onLoginStateChange?.(result) + } catch (e) { + if (e instanceof Yun139SmsRequiredError) { + this.addition.mail_cookies = e.mailCookies + await this.onLoginStateChange?.({ + authorization: "", + account: this.account, + userDomainId: (this.addition.user_domain_id || "").trim(), + mailCookies: e.mailCookies, + smsCode: (this.addition.sms_code || "").trim(), + }) + } + throw e + } + } + + async init(): Promise { + await this.ensureAuthorization() await this.refreshAuthorizationIfNeeded() let routeError: unknown = null diff --git a/src/backend/internal/op/storage.ts b/src/backend/internal/op/storage.ts index 550f1871..3078e928 100644 --- a/src/backend/internal/op/storage.ts +++ b/src/backend/internal/op/storage.ts @@ -1008,28 +1008,57 @@ async function createDriver( normDriver.includes("139") ) { const addition = parseAddition(storageConfig) - driver = new Yun139Driver(addition, async (authorization) => { - try { - const db = await getDb() - const st = (db.storages || []).find( - (s: any) => s.id === storageConfig?.id, - ) - if (!st) return - const stAddition = - typeof st.addition === "string" - ? JSON.parse(st.addition || "{}") - : st.addition || {} - stAddition.authorization = authorization - st.addition = JSON.stringify(stAddition) - // Keep the in-flight admin update object in sync so it cannot overwrite - // the newly refreshed authorization with the previous value. - storageConfig.addition = st.addition - if (deferredTokenPersistence.has(storageConfig)) return - await saveDb(db) - } catch (e) { - console.warn("[139] failed to persist refreshed authorization:", e) - } - }) + driver = new Yun139Driver( + addition, + async (authorization) => { + try { + const db = await getDb() + const st = (db.storages || []).find( + (s: any) => s.id === storageConfig?.id, + ) + if (!st) return + const stAddition = + typeof st.addition === "string" + ? JSON.parse(st.addition || "{}") + : st.addition || {} + stAddition.authorization = authorization + st.addition = JSON.stringify(stAddition) + // Keep the in-flight admin update object in sync so it cannot overwrite + // the newly refreshed authorization with the previous value. + storageConfig.addition = st.addition + if (deferredTokenPersistence.has(storageConfig)) return + await saveDb(db) + } catch (e) { + console.warn("[139] failed to persist refreshed authorization:", e) + } + }, + async (state) => { + try { + const db = await getDb() + const st = (db.storages || []).find( + (s: any) => s.id === storageConfig?.id, + ) + if (!st) return + const stAddition = + typeof st.addition === "string" + ? JSON.parse(st.addition || "{}") + : st.addition || {} + if (state.authorization) + stAddition.authorization = state.authorization + if (state.userDomainId) { + stAddition.user_domain_id = state.userDomainId + } + stAddition.mail_cookies = state.mailCookies + stAddition.sms_code = state.smsCode + st.addition = JSON.stringify(stAddition) + storageConfig.addition = st.addition + if (deferredTokenPersistence.has(storageConfig)) return + await saveDb(db) + } catch (e) { + console.warn("[139] failed to persist login state:", e) + } + }, + ) await driver.init?.() } else if ( normDriver === "mega" || diff --git a/src/backend/server/admin.ts b/src/backend/server/admin.ts index 722c66d9..243dc72e 100644 --- a/src/backend/server/admin.ts +++ b/src/backend/server/admin.ts @@ -3405,7 +3405,43 @@ const driverConfigs: Record = { name: "authorization", type: "text", default: "", - required: true, + required: false, + help: "Authorization can be used alone. If empty, use username + password; mail_cookies is optional and will be established/updated automatically. Existing mail_cookies can also be used alone for fast login.", + }, + { + name: "username", + type: "string", + default: "", + required: false, + help: "Use together with password when Authorization is empty. mail_cookies may be left empty on the first login.", + }, + { + name: "password", + type: "string", + default: "", + required: false, + help: "Use together with username when Authorization is empty. mail_cookies may be left empty on the first login.", + }, + { + name: "sms_code", + type: "string", + default: "", + required: false, + help: "Fill this only after the driver reports that a 139 Mail SMS verification code was sent, then save the storage again.", + }, + { + name: "mail_cookies", + type: "text", + default: "", + required: false, + help: "Optional cookies from mail.10086.cn. Leave empty for a first username/password login; cookies created or updated by password/SMS login are persisted and reused as device context. Existing cookies may also be used alone for fast login.", + }, + { + name: "user_domain_id", + type: "string", + default: "", + required: false, + help: "Optional. ud_id cookie value; filled automatically after password/fast login. Used by the PC refreshToken API, otherwise the legacy refresh endpoint is used.", }, { name: "root_folder_id", From 6e11cfbb2bc9640f90df9740d2a25f000e5e0eb5 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:24:18 +0800 Subject: [PATCH 46/49] fix(139): surface SMS verification during refresh fallback (#190) When a refresh failure triggers password login and 139 Mail asks for an SMS code, propagate Yun139SmsRequiredError instead of swallowing it and reporting only the original refresh error. The refreshed mail_cookies are still persisted first, so the next save with sms_code can continue. Tests: 139 driver 25/25, tsc clean. Co-authored-by: pikachuren --- src/backend/drivers/139/util.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/backend/drivers/139/util.ts b/src/backend/drivers/139/util.ts index cb5f4466..bf3dd69a 100644 --- a/src/backend/drivers/139/util.ts +++ b/src/backend/drivers/139/util.ts @@ -501,6 +501,7 @@ export class Yun139ApiClient { mailCookies: e.mailCookies, smsCode: (this.addition.sms_code || "").trim(), }) + throw e } console.debug( "[139] re-login after refresh failure skipped:", From 705f603151b5d5cd6ae3f3aa17686bb1578abd72 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:26:52 +0800 Subject: [PATCH 47/49] fix(auth): honor disabled guest access (#162) * fix(auth): honor disabled guest access Issue #133 reports that unauthenticated visitors can browse mounted content despite expecting sign-in. The public settings endpoint already exposes allow_guest=false, but getUserFromContext previously fell back to any active guest account without enforcing that setting. When no Authorization header or query token is present, return null before guest fallback if allow_guest is false. Preserve the current default guest behavior when the setting is absent. Add a regression test proving default fallback remains enabled and that disabling it makes anonymous /api/fs/list return 401. Document that guest access is an OpenList application setting, not a Cloudflare environment variable. Fixes #133 Validation: git diff --cached --check passed. Targeted runtime test is pending dependency installation in an isolated verification copy. * fix(auth): unify allow_guest check and cover Emby anonymous entry - Add pkg/permission.isGuestAllowed(): tolerates boolean false / "False" / padded values, and is shared by /api/public/settings and every auth path so display and enforcement cannot drift apart - getUserFromContext uses isGuestAllowed (bare === "false" failed open) - Emby anonymous cover entry (guestUserOf) now honors allow_guest too - Regression tests: boolean/case/padded settings values and Emby anonymous cover returning 401; both verified by mutation Co-Authored-By: CodeBuddy * feat(auth)!: require sign-in on fresh installs - defaultDb: disable the guest account by default (issue #133) - defaultDb: make allow_guest a real site setting (value "true" on insert so existing deployments are not silently switched to "must sign in") - init/setup and ADMIN_PASS first boot write allow_guest=false for new installs - docs: two-step opt-in for anonymous read-only browsing - tests: fresh install (both init paths) is blocked and persisted; an existing deployment without the setting keeps its previous behavior Co-Authored-By: CodeBuddy * refactor(auth)!: drop allow_guest to match the Go backend Go has no allow_guest setting: anonymous access is decided only by whether the guest account exists and is disabled (server/middlewares/auth.go Auth(false)). The TS-only switch added earlier was therefore a divergence, not a fix. - remove the default setting row and the init-time writes - remove isGuestAllowed and the checks in getUserFromContext / Emby guestUserOf - stop echoing allow_guest from /api/public/settings (the frontend never read it) - keep the actual #133 fix: fresh installs create a disabled guest, so anonymous browsing requires an explicit opt-in by enabling the guest user - tests assert parity: no allow_guest in defaults or public settings, fresh install returns 401, an existing deployment is left untouched BREAKING CHANGE: /api/public/settings no longer returns allow_guest. Co-Authored-By: CodeBuddy --------- Co-authored-by: pikachuren Co-authored-by: CodeBuddy --- README.md | 1 + readmes/README_en.md | 1 + src/backend/internal/model/db.ts | 5 +- src/backend/server/auth.ts | 5 +- src/backend/server/emby_protocol.test.ts | 7 + src/backend/server/guest_auth.test.ts | 11 +- src/backend/server/guest_defaults.test.ts | 157 +++++++++++++++++++++ src/backend/server/middlewares.ts | 5 +- src/backend/server/public.ts | 15 +- src/backend/server/public_settings.test.ts | 4 +- 10 files changed, 190 insertions(+), 21 deletions(-) create mode 100644 src/backend/server/guest_defaults.test.ts diff --git a/README.md b/README.md index b73ca213..bca3fac3 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ OpenList-Worker 是官方 [OpenListTeam/OpenList](https://github.com/OpenListTea - **文件管理**:统一目录树浏览,图片 / 视频 / 音频 / 文档 / 代码 / 压缩包在线预览;跨存储上传、批量下载、流式传输与直链跳转;带有效期、密码与权限控制的分享链接。 - **检索与任务**:已索引存储的全文搜索,后台任务队列支持批量与异步处理。 - **外部接口**:WebDAV、S3 兼容协议,以及可被 AI 助手集成的 MCP 端点。 +- **访客访问**:新装默认要求登录(`guest` 账号默认禁用)。需要匿名只读时,在「用户管理」里启用 `guest` 即可;存量部署升级后保持原状。是否允许匿名只取决于 guest 账号本身是否启用(与 Go 版一致),没有额外的站点开关,也不是 Cloudflare 控制台的环境变量。 - **权限与认证**:RBAC(用户分组、目录级读写权限、配额),账号密码 + TOTP / WebAuthn / SSO / LDAP。 - **安全与运维**:JWT 会话、CSRF、点击劫持(X-Frame-Options)与 CSP 防护,`/health`、`/healthz` 健康探针。 - **并发去重**:相同目录列表 / 元信息请求只向上游网盘发起一次,其余请求复用结果,减少限流与 CPU 消耗。 diff --git a/readmes/README_en.md b/readmes/README_en.md index 6ecb4ffc..f013e124 100644 --- a/readmes/README_en.md +++ b/readmes/README_en.md @@ -73,6 +73,7 @@ In addition to the real storages above, virtual/functional drivers such as `Loca - **File management**: unified directory tree browsing with online preview for images, videos, audio, documents, code and archives; cross-storage upload, batch download, streaming and direct-link redirect; share links with expiry, password and permission control. - **Search & tasks**: full-text search across indexed storages; background task queue for batch and async processing. - **External interfaces**: WebDAV, S3-compatible protocol, and an MCP endpoint for AI assistants. +- **Guest access**: fresh installs require sign-in by default (the `guest` account is disabled). To allow anonymous read-only browsing, enable the `guest` user; existing deployments keep their current behavior. Anonymous access is controlled solely by the guest account state (same as the Go backend) — there is no extra site setting, and it is not a Cloudflare environment variable. - **Access & auth**: RBAC (user groups, directory-level read/write permissions, quotas), account passwords plus TOTP / WebAuthn / SSO / LDAP. - **Security & ops**: JWT sessions, CSRF, clickjacking (X-Frame-Options) and CSP protection, `/health` and `/healthz` probes. - **Concurrency deduplication**: identical directory listing / metadata requests hit the upstream drive only once; the rest reuse the result, reducing rate limits and CPU usage. diff --git a/src/backend/internal/model/db.ts b/src/backend/internal/model/db.ts index 38894380..e07a9c36 100644 --- a/src/backend/internal/model/db.ts +++ b/src/backend/internal/model/db.ts @@ -1029,7 +1029,10 @@ export const defaultDb = { role: 1, permission: 0, base_path: "/", - disabled: false, + // 新装默认不启用访客:旧行为是 guest 默认启用、匿名请求直接按 guest 浏览, + // 与「打开网页就要登录」的预期相反(issue #133)。存量部署的用户表存在 + // 持久化存储里,不受这里影响;需要匿名只读时在用户管理中显式启用。 + disabled: true, sso_id: "", allow_ldap: false, pwd_update_at: new Date().toISOString(), diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index d2753809..775bded7 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -332,7 +332,8 @@ export async function getOrInitUsers(envCtx: any) { role: 1, permission: 0, base_path: "/", - disabled: false, + // 与 defaultDb.users 保持一致:新装默认不启用访客(issue #133)。 + disabled: true, sso_id: "", allow_ldap: false, pwd_update_at: new Date().toISOString(), @@ -362,6 +363,8 @@ export async function getOrInitUsers(envCtx: any) { // 未初始化:仅创建 guest,admin 由 Web 安装向导(POST /api/public/init/setup)创建 db.users = [guest] } + // 新装默认要求登录(issue #133):由上面的「guest 默认禁用」实现, + // 与 Go 版一致 —— Go 没有 allow_guest 设置项,只有 guest 账号的启用状态。 // 只有配置了 ADMIN_PASS(或已存在真实管理员)时,这份库才不是空壳, // 才应该落盘。未配置时应等待 init/setup 完成初始化,而不是抢先写入一个 // 未初始化的占位库(会被 saveDb 的写前守卫拦截)。 diff --git a/src/backend/server/emby_protocol.test.ts b/src/backend/server/emby_protocol.test.ts index ff205046..402bdfb0 100644 --- a/src/backend/server/emby_protocol.test.ts +++ b/src/backend/server/emby_protocol.test.ts @@ -18,6 +18,7 @@ function buildApp() { const original = new Map() let originalGuestBasePath: string | null = null +let originalGuestDisabled: boolean | null = null let originalMetas: any[] | null = null async function setSetting(key: string, value: string) { @@ -43,6 +44,7 @@ after(async () => { } const guest = (db.users || []).find((u: any) => u.username === "guest") if (guest && originalGuestBasePath !== null) guest.base_path = originalGuestBasePath + if (guest && originalGuestDisabled !== null) guest.disabled = originalGuestDisabled if (originalMetas !== null) db.metas = originalMetas await saveDb(db, undefined, { force: true } as any) }) @@ -101,6 +103,10 @@ async function seedMediaEnv(items: MediaItem[], metas: any[] = [], enabled = tru originalGuestBasePath = String(guest.base_path || "/") } guest.base_path = "/public" + // 新装默认禁用访客(issue #133),本文件验证的是「访客开启时」的封面语义, + // 因此显式启用;是否允许匿名只取决于 guest 账号状态(与 Go 一致)。 + if (originalGuestDisabled === null) originalGuestDisabled = !!guest.disabled + guest.disabled = false } if (originalMetas === null) originalMetas = JSON.parse(JSON.stringify(userDb.metas || [])) userDb.metas = metas @@ -333,3 +339,4 @@ test("Emby 封面:剧集封面同样经过 meta 校验", async () => { const denied = await app.request(path, {}, {}) assert.equal(denied.status, 404) }) + diff --git a/src/backend/server/guest_auth.test.ts b/src/backend/server/guest_auth.test.ts index 12256f09..ddb982ca 100644 --- a/src/backend/server/guest_auth.test.ts +++ b/src/backend/server/guest_auth.test.ts @@ -111,7 +111,7 @@ test("Security: /api/me returns 401 when unauthenticated and guest is deleted or assert.equal(json.code, 401) }) -test("Security: /api/public/settings returns allow_guest 'false' when guest is deleted or disabled", async () => { +test("Security: /api/public/settings does not expose allow_guest (parity with Go)", async () => { const env: any = {} await saveDb( { @@ -136,15 +136,13 @@ test("Security: /api/public/settings returns allow_guest 'false' when guest is d const app = new Hono() app.route("/api/public", publicRouter) - const res = await app.request("/api/public/settings", { - method: "GET", - }) + const res = await app.request("/api/public/settings", { method: "GET" }) assert.equal(res.status, 200) const json: any = await res.json() assert.equal( json.data.allow_guest, - "false", - "allow_guest must be false when guest user is deleted", + undefined, + "Go 版公开设置里没有 allow_guest,这里也不应输出", ) }) @@ -255,3 +253,4 @@ test("/raw/* returns 401 without sign when sign_all is enabled (parity with Go n const res = await app.request("/raw/some/file.txt", { method: "GET" }) assert.equal(res.status, 401, "sign_all enabled must require a valid sign") }) + diff --git a/src/backend/server/guest_defaults.test.ts b/src/backend/server/guest_defaults.test.ts new file mode 100644 index 00000000..6e3dd104 --- /dev/null +++ b/src/backend/server/guest_defaults.test.ts @@ -0,0 +1,157 @@ +import assert from "node:assert/strict" +import { test } from "node:test" +import { Hono } from "hono" +import { + __resetDbCacheForTest, + defaultDb, + getDb, + saveDb, +} from "../internal/model/db" +import { memoryDriver } from "../internal/model/store/driver/memory" +import { getOrInitUsers } from "./auth" +import { getUserFromContext } from "./middlewares" +import { fsRouter } from "./fs" +import { publicRouter } from "./public" + +const ADMIN_HASH = "a".repeat(64) + +async function freshEnv() { + for (const key of await memoryDriver.list("openlist_")) { + await memoryDriver.delete(key) + } + __resetDbCacheForTest() + return {} as any +} + +function anonContext(env: any) { + return { + req: { header: () => undefined, query: () => undefined }, + env, + } as any +} + +function buildApp() { + const app = new Hono() + app.route("/api/fs", fsRouter) + app.route("/api/public", publicRouter) + return app +} + +async function anonList(env: any) { + const res = await buildApp().request( + "/api/fs/list", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: "/" }), + }, + env, + ) + return res.status +} + +test("默认库:guest 默认禁用,且不引入 Go 没有的 allow_guest 设置项", () => { + const guest = defaultDb.users.find((u: any) => u.username === "guest") + assert.equal(guest?.disabled, true, "新装默认不应启用 guest") + + const row = defaultDb.settings.find((s: any) => s.key === "allow_guest") + assert.equal(row, undefined, "Go 版没有 allow_guest,默认设置里也不应有") +}) + +test("新装(ADMIN_PASS 首启):guest 禁用,匿名 401", async () => { + const env = await freshEnv() + env.ADMIN_PASS = "admin123" + + const { db } = await getOrInitUsers(env) + const guest = (db.users || []).find((u: any) => u.username === "guest") + assert.equal(guest?.disabled, true) + + // 必须真的落盘:清掉模块缓存后重新读取,不能只是内存里的默认值 + __resetDbCacheForTest() + const reloaded = await getDb(env) + assert.equal( + (reloaded.users || []).find((u: any) => u.username === "guest")?.disabled, + true, + "新装的 guest 禁用状态必须持久化", + ) + + assert.equal(await anonList(env), 401) + assert.equal(await getUserFromContext(anonContext(env)), null) + + const settings = await buildApp().request( + "/api/public/settings", + { method: "GET" }, + env, + ) + const body: any = await settings.json() + assert.equal(body.data.allow_guest, undefined, "公开设置不应输出 allow_guest") +}) + +test("存量库升级(guest 启用):不会被静默改成必须登录", async () => { + const env = await freshEnv() + await saveDb( + { + settings: [], + users: [ + { + id: 1, + username: "admin", + password: ADMIN_HASH, + role: 2, + permission: 0, + base_path: "/", + disabled: false, + }, + { + id: 2, + username: "guest", + password: "", + role: 1, + permission: 0, + base_path: "/", + disabled: false, + }, + ], + storages: [], + shares: [], + metas: [], + } as any, + env, + ) + __resetDbCacheForTest() + + const db = await getDb(env) + assert.equal( + (db.users || []).find((u: any) => u.username === "guest")?.disabled, + false, + "存量库的 guest 状态不应被升级改动", + ) + assert.equal((await getUserFromContext(anonContext(env)))?.username, "guest") +}) + +test("新装(安装向导 init/setup):完成后 guest 仍禁用,匿名 401", async () => { + const env = await freshEnv() + + const res = await buildApp().request( + "/api/public/init/setup", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: "admin", + password: "admin123", + site_title: "Test", + }), + }, + env, + ) + assert.equal(res.status, 200) + + __resetDbCacheForTest() + const db = await getDb(env) + assert.equal( + (db.users || []).find((u: any) => u.username === "guest")?.disabled, + true, + ) + assert.equal(await anonList(env), 401) +}) diff --git a/src/backend/server/middlewares.ts b/src/backend/server/middlewares.ts index 0c7d7d45..b61d3b4b 100644 --- a/src/backend/server/middlewares.ts +++ b/src/backend/server/middlewares.ts @@ -494,8 +494,9 @@ export async function getUserFromContext(c: Context): Promise<{ } } - // 无 Authorization header:尝试使用 guest 用户作为后备 - // 注意:guest 不可用时返回 null 是合理的,调用方应该正确处理 + // 无 Authorization header:尝试使用 guest 用户作为后备。 + // 与 Go 版 server/middlewares/auth.go 的 Auth(false) 一致:是否允许匿名访问 + // 只取决于 guest 账号本身是否存在/被禁用,没有额外的站点开关。 if (!authHeader) { try { const db = await getDb(c.env) diff --git a/src/backend/server/public.ts b/src/backend/server/public.ts index 10132e5c..a91995b0 100644 --- a/src/backend/server/public.ts +++ b/src/backend/server/public.ts @@ -490,7 +490,6 @@ publicRouter.get("/settings", async (c) => { check_update: "false", // --- Auth --- - allow_guest: "true", webauthn_login_enabled: "false", sso_login_enabled: "false", sso_compatibility_mode: "false", @@ -548,14 +547,9 @@ publicRouter.get("/settings", async (c) => { } }) - // 动态检查是否存在且启用了 guest 账号 - const guest = (db.users || []).find((u: any) => u.username === "guest") - const isGuestActive = Boolean(guest && !guest.disabled) - if (!isGuestActive || settingsObj.allow_guest === "false") { - settingsObj.allow_guest = "false" - } else { - settingsObj.allow_guest = "true" - } + // 注意:不再输出 allow_guest。Go 版没有这个键,匿名访问是否放行完全由 + // guest 账号是否存在/被禁用决定(server/middlewares/auth.go 的 Auth(false))。 + // 前端也不读取该字段,删掉它是为了和 Go 的公开设置保持一致。 return c.json({ code: 200, @@ -821,6 +815,9 @@ publicRouter.post("/init/setup", async (c) => { } } + // 注意:新装默认要求登录由「guest 账号默认禁用」实现(defaultDb.users), + // 与 Go 保持同一套语义,不再写 allow_guest 设置项。 + // 初始化是唯一允许「在一次成功读取(确认存储为空)之后写入空壳」的场景, // 因此显式 force;此时 db 已包含新建的管理员用户,本身也不再是空壳。 await saveDb(db, c.env, { force: true }) diff --git a/src/backend/server/public_settings.test.ts b/src/backend/server/public_settings.test.ts index 3b28aa63..1d3f2fa6 100644 --- a/src/backend/server/public_settings.test.ts +++ b/src/backend/server/public_settings.test.ts @@ -59,8 +59,8 @@ test("Security(C-1): public settings still returns display options (no regressio assert.equal(json.data.webauthn_login_enabled, "false") assert.equal(json.data.sign_all, "true") assert.equal(json.data.site_title, "My Site") - // NOTE: allow_guest is deliberately not asserted here — it is derived from - // whether a guest account exists (see guest_auth.test.ts), not a plain echo. + // NOTE: allow_guest is not part of the public response — the Go backend has no + // such key; anonymous access is controlled by the guest account state only. }) test("Security(F-14): unknown keys fail closed — they are not echoed", async () => { From 85bac8c05c02d631e93d4048527a6ae56cb20f47 Mon Sep 17 00:00:00 2001 From: Pikachu Ren <40362270+PIKACHUIM@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:03:05 +0800 Subject: [PATCH 48/49] fix(webdav): sign the download redirect to stop 401 on playback (#187) Issue #175: mounting OpenList over WebDAV lets clients list directories and see files, but any attempt to play a video returns `401 sign verify failed` for the raw URL behind the redirect. WebDAV GET/HEAD redirects to the raw endpoint returned by getItem() (`/api/p/` or `/api/d/`) without appending a signature. Those endpoints deliberately have no auth middleware, so access is decided solely by needDownloadSign(): when sign_all, link_expiration, storage-level enable_sign or a password-protected meta is in effect, the unsigned link is rejected. This is the same root cause as #66, which only fixed /fs/get. Add a shared appendDownloadSignToRawUrl() helper (idempotent, same semantics as /fs/get) and use it for the WebDAV GET/HEAD redirect as well as the S3 GetObject redirect, which had the identical gap. Sign over the scoped actualPath so the signature matches what the raw endpoint verifies. Validation: webdav_get_sign 5/5 (2 of them fail on the pre-fix tree), test:server 225/225, test:drivers 197/197, tsc --noEmit clean. Co-authored-by: pikachuren Co-authored-by: DeepSeek --- docs/pr-issue-175-webdav-download-sign.md | 113 +++++++++++++ src/backend/pkg/sign.ts | 26 +++ src/backend/server/s3.ts | 9 +- src/backend/server/webdav.ts | 9 +- src/backend/server/webdav_get_sign.test.ts | 184 +++++++++++++++++++++ 5 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 docs/pr-issue-175-webdav-download-sign.md create mode 100644 src/backend/server/webdav_get_sign.test.ts diff --git a/docs/pr-issue-175-webdav-download-sign.md b/docs/pr-issue-175-webdav-download-sign.md new file mode 100644 index 00000000..1e3529d9 --- /dev/null +++ b/docs/pr-issue-175-webdav-download-sign.md @@ -0,0 +1,113 @@ + + +`fix(webdav): sign the download redirect so playback stops returning 401` + +## Summary / 摘要 + +本 PR 修复「WebDAV 挂载后能列目录、能看文件,一点播放就 401」的问题(Issue #175)。 + +**根因**:WebDAV 的 `GET` / `HEAD` 只把 `getItem()` 返回的 raw 链接(`/api/p/` 或 `/api/d/`)302 出去,**从不补签名**: + +```ts +return c.redirect(rawUrl || `/api/d${encodeDownloadPath(actualPath)}`, 302) +``` + +而 `/d`、`/p` 设计上是**没有 Auth 中间件的公开下载端点**(播放器、`