From b85c990921fb1df0be866fc52f96a465c1092ab8 Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Sun, 30 Aug 2026 19:29:07 -0700 Subject: [PATCH 1/4] Add R2 asset directory browser --- .gitignore | 1 + eslint.config.mjs | 15 + package-lock.json | 1487 ++++++++++++++++- package.json | 14 +- vitest.config.mjs | 5 +- workers/assets-directory-browser/README.md | 83 + workers/assets-directory-browser/src/index.js | 361 ++++ .../src/index.test.mjs | 286 ++++ .../src/local-env.d.ts | 3 + .../src/template.d.ts | 4 + .../src/template.html | 302 ++++ .../assets-directory-browser/tsconfig.json | 19 + .../vitest.config.mjs | 15 + .../worker-configuration.d.ts | 12 + .../assets-directory-browser/wrangler.jsonc | 31 + 15 files changed, 2634 insertions(+), 4 deletions(-) create mode 100644 workers/assets-directory-browser/README.md create mode 100644 workers/assets-directory-browser/src/index.js create mode 100644 workers/assets-directory-browser/src/index.test.mjs create mode 100644 workers/assets-directory-browser/src/local-env.d.ts create mode 100644 workers/assets-directory-browser/src/template.d.ts create mode 100644 workers/assets-directory-browser/src/template.html create mode 100644 workers/assets-directory-browser/tsconfig.json create mode 100644 workers/assets-directory-browser/vitest.config.mjs create mode 100644 workers/assets-directory-browser/worker-configuration.d.ts create mode 100644 workers/assets-directory-browser/wrangler.jsonc diff --git a/.gitignore b/.gitignore index 2351d9e..86c9632 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ dist dist-ssr coverage *.local +.wrangler .env docker-compose.override.yml diff --git a/eslint.config.mjs b/eslint.config.mjs index 5dac9cf..7410385 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -53,9 +53,24 @@ export default [ }, }, + // Cloudflare types are generated from the Worker configuration. + { + files: ['workers/**/*.js'], + rules: { + 'jsdoc/no-undefined-types': ['warn', { definedTypes: [ + 'Env', + 'ExportedHandler', + 'HeadersInit', + 'R2Bucket', + 'R2Objects', + ] }], + }, + }, + // Ignored files { ignores: [ + '**/.wrangler/**', 'dist/**', 'src/assets/i18n/index.mjs', ], diff --git a/package-lock.json b/package-lock.json index 7e34fc0..0a29017 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "jsonpath-plus": "^10.4.0", "lodash": "^4.17.21", "masto": "^8.0.0", + "mustache": "^4.2.0", "nxapi": "1.6.1-next.254", "p-filter": "^4.1.0", "p-limit": "^7.3.0", @@ -36,10 +37,13 @@ "vue-router": "^5.0.2" }, "devDependencies": { + "@cloudflare/vitest-plugin": "^1.1.2", + "@cloudflare/workers-types": "^5.20260830.1", "@dword-design/eslint-plugin-import-alias": "^8.1.3", "@eslint/js": "^10.0.1", "@intlify/unplugin-vue-i18n": "^11.2.5", "@tailwindcss/vite": "^4.3.3", + "@types/mustache": "^4.2.6", "@vitejs/plugin-vue": "^6.0.8", "eslint": "^10.9.1", "eslint-plugin-jsdoc": "^63.3.3", @@ -49,7 +53,8 @@ "typescript": "^6.0.3", "vite": "^8.2.2", "vitest": "^4.1.11", - "vue-eslint-parser": "^10.4.0" + "vue-eslint-parser": "^10.4.0", + "wrangler": "^4.127.1" } }, "node_modules/@atproto/api": { @@ -921,6 +926,668 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/vitest-plugin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-plugin/-/vitest-plugin-1.1.2.tgz", + "integrity": "sha512-TLkPo1JTKk0LyFwQlNWY0MnZ0RW7FsjianjSdrKiPi/NbL7DUnIEe8O8leJAeBboiRyn23g/NG5jLeyZvWlmVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cjs-module-lexer": "1.2.3", + "esbuild": "0.28.1", + "miniflare": "5.20260828.0-alpha", + "wrangler": "4.127.1", + "zod": "4.4.3" + }, + "peerDependencies": { + "@vitest/runner": "^4.1.0", + "@vitest/snapshot": "^4.1.0", + "vitest": "^4.1.0" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/@cloudflare/vitest-plugin/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260828.1.tgz", + "integrity": "sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260828.1.tgz", + "integrity": "sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260828.1.tgz", + "integrity": "sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260828.1.tgz", + "integrity": "sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260828.1.tgz", + "integrity": "sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "5.20260831.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260831.1.tgz", + "integrity": "sha512-yXg4pwfYjhsDH9rYc3qZ3K+z62DCSvO/aj7GiZo6AyDeWGZpyFRpPMYcQ6LF/zfaf1x0Ngw2gSqL8JjuUtMGlA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dword-design/defaults": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@dword-design/defaults/-/defaults-1.1.9.tgz", @@ -3357,6 +4024,48 @@ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "license": "MIT" }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/dumper/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, "node_modules/@puppeteer/browsers": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.1.tgz", @@ -3961,6 +4670,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@smithy/core": { "version": "3.33.3", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.3.tgz", @@ -4042,6 +4764,13 @@ "node": ">=18.0.0" } }, + "node_modules/@speed-highlight/core": { + "version": "1.2.24", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz", + "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4272,6 +5001,72 @@ "node": ">=14.0.0" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { + "version": "2.8.1", + "dev": true, + "inBundle": true, + "license": "0BSD", + "optional": true + }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", @@ -4454,6 +5249,13 @@ "integrity": "sha512-jJ41Q4z6ZVO260MNDdHfW7+7a5iMiX8Mr6ZJHcmgrvhZha6dz5704o/lF2kKl6URjH6ivEL97w9xS/MgpJEphg==", "license": "MIT" }, + "node_modules/@types/mustache": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.6.tgz", + "integrity": "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.4.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", @@ -5112,6 +5914,13 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.6", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", @@ -5797,6 +6606,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -7196,6 +8015,16 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7686,6 +8515,56 @@ "node": ">= 0.6" } }, + "node_modules/miniflare": { + "version": "5.20260828.0-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260828.0-alpha.tgz", + "integrity": "sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.29.0", + "workerd": "1.20260828.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/miniflare/node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/miniflare/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -7816,6 +8695,15 @@ "integrity": "sha512-5vQEh3y+DG/lMPM0mCGPDnyV8chYg/g7rl6v3Gd8WMF9S429ox3Xk8qrk174kWhG767KQMqqxLD1WnGd77hiew==", "license": "MIT" }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -9572,6 +10460,16 @@ "optional": true, "peer": true }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, "node_modules/unicode-segmenter": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.5.tgz", @@ -10396,6 +11294,554 @@ "node": ">=0.10.0" } }, + "node_modules/workerd": { + "version": "1.20260828.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260828.1.tgz", + "integrity": "sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260828.1", + "@cloudflare/workerd-darwin-arm64": "1.20260828.1", + "@cloudflare/workerd-linux-64": "1.20260828.1", + "@cloudflare/workerd-linux-arm64": "1.20260828.1", + "@cloudflare/workerd-windows-64": "1.20260828.1" + } + }, + "node_modules/wrangler": { + "version": "4.127.1", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.127.1.tgz", + "integrity": "sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "5.20260828.0-alpha", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260828.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260828.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/wrangler/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10538,6 +11984,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/youch/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 8abfc55..3bc0983 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,15 @@ "scripts": { "dev": "vite", "build": "vite build", + "assets-browser:dev": "wrangler dev --local --var LOCAL_DEVELOPMENT:true --persist-to .wrangler/state --cwd workers/assets-directory-browser", + "assets-browser:test": "vitest run --config workers/assets-directory-browser/vitest.config.mjs", + "assets-browser:types": "wrangler types --include-runtime false --cwd workers/assets-directory-browser", + "assets-browser:typecheck": "tsc --project workers/assets-directory-browser/tsconfig.json", + "assets-browser:dry-run": "wrangler deploy --dry-run --cwd workers/assets-directory-browser", "preview": "vite preview --port 5050", "lint": "eslint .", "lint-fix": "npm run lint -- --fix", - "test": "vitest run", + "test": "vitest run && npm run assets-browser:test", "test:watch": "vitest", "cron": "node app/index.mjs cron", "start": "npm run sync:download && npm run splatnet:quick && npm run social && npm run cron", @@ -47,6 +52,7 @@ "jsonpath-plus": "^10.4.0", "lodash": "^4.17.21", "masto": "^8.0.0", + "mustache": "^4.2.0", "nxapi": "1.6.1-next.254", "p-filter": "^4.1.0", "p-limit": "^7.3.0", @@ -63,10 +69,13 @@ "vue-router": "^5.0.2" }, "devDependencies": { + "@cloudflare/vitest-plugin": "^1.1.2", + "@cloudflare/workers-types": "^5.20260830.1", "@dword-design/eslint-plugin-import-alias": "^8.1.3", "@eslint/js": "^10.0.1", "@intlify/unplugin-vue-i18n": "^11.2.5", "@tailwindcss/vite": "^4.3.3", + "@types/mustache": "^4.2.6", "@vitejs/plugin-vue": "^6.0.8", "eslint": "^10.9.1", "eslint-plugin-jsdoc": "^63.3.3", @@ -76,7 +85,8 @@ "typescript": "^6.0.3", "vite": "^8.2.2", "vitest": "^4.1.11", - "vue-eslint-parser": "^10.4.0" + "vue-eslint-parser": "^10.4.0", + "wrangler": "^4.127.1" }, "overrides": { "sharp": "$sharp", diff --git a/vitest.config.mjs b/vitest.config.mjs index 55ef3ce..7c3fb8c 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -8,6 +8,9 @@ export default defineConfig({ }, }, test: { - include: ['app/**/*.test.mjs', 'src/**/*.test.{js,mjs}'], + include: [ + 'app/**/*.test.mjs', + 'src/**/*.test.{js,mjs}', + ], }, }); diff --git a/workers/assets-directory-browser/README.md b/workers/assets-directory-browser/README.md new file mode 100644 index 0000000..591afe5 --- /dev/null +++ b/workers/assets-directory-browser/README.md @@ -0,0 +1,83 @@ +# Assets directory browser + +This Worker renders read-only directory listings for the public R2 bucket without +running on ordinary object requests. + +## Request routing + +The browser-visible directory URLs end in `/`, for example: + +- `https://assets.splatoon3.ink/` +- `https://assets.splatoon3.ink/data/` +- `https://assets.splatoon3.ink/splatnet/` + +A Cloudflare URL Rewrite Rule rewrites only those requests to an internal prefix. +The rewrite is not visible to the client. + +Filter expression: + +```text +(http.host eq "assets.splatoon3.ink" and ends_with(http.request.uri.path, "/")) +``` + +Dynamic path rewrite: + +```text +concat("/__directory", http.request.uri.path) +``` + +The Wrangler configuration routes only `assets.splatoon3.ink/__directory/*` to +the Worker. Every JSON file, calendar, image, and screenshot object continues to +go directly to the R2 custom domain. + +## JSON listings + +Each directory has a machine-readable representation at the same public URL: + +```text +https://assets.splatoon3.ink/splatnet/images/?format=json +``` + +The response contains immediate child `directories`, immediate child `files`, +and an absolute `next` URL when another page is available. Listings are not +recursive. Clients can walk the bucket by following directory URLs and must +follow `next` verbatim rather than interpreting its opaque R2 cursor. + +File records include their canonical URL, byte size, ETag, and R2 upload time. +The HTML listing displays that same upload time in its **Last modified** column. +JSON listing and error responses allow anonymous cross-origin reads with +`Access-Control-Allow-Origin: *`. + +## Local preview + +Start the Worker with its explicit local-development flag: + +```sh +npm run assets-browser:dev +``` + +Then open `http://localhost:8787/`. Local mode accepts the natural directory +paths and serves linked objects from Wrangler's simulated R2 bucket. It never +connects to the production bucket. + +## Validate without deploying + +```sh +npm run assets-browser:test +npm run assets-browser:types +npm run assets-browser:typecheck +npm run assets-browser:dry-run +``` + +## Production rollout + +Do not create the URL Rewrite Rule until the Worker route has been deployed and +verified directly at an internal URL such as: + +```text +https://assets.splatoon3.ink/__directory/data/ +``` + +After that direct check succeeds, create the URL Rewrite Rule and verify the +browser-visible `/`, `/data/`, and `/splatnet/` directory URLs. To roll back, +disable the URL Rewrite Rule first, then remove the Worker route. diff --git a/workers/assets-directory-browser/src/index.js b/workers/assets-directory-browser/src/index.js new file mode 100644 index 0000000..a35f282 --- /dev/null +++ b/workers/assets-directory-browser/src/index.js @@ -0,0 +1,361 @@ +import Mustache from 'mustache'; +import template from './template.html'; + +const internalPrefix = '/__directory'; +const pageSize = 1000; + +Mustache.parse(template); + +/** @param {string} key */ +function publicPath(key) { + return `/${key.split('/').map(encodeURIComponent).join('/')}`; +} + +/** @param {string} prefix */ +function parentPath(prefix) { + let parts = prefix.split('/').filter(Boolean); + parts.pop(); + return parts.length ? publicPath(`${parts.join('/')}/`) : '/'; +} + +/** + * @param {string} key + * @param {string} prefix + */ +function displayName(key, prefix) { + return key.slice(prefix.length); +} + +/** + * @typedef {object} Breadcrumb + * @property {boolean} [current] Whether this is the current directory. + * @property {string} [href] Link destination. + * @property {boolean} [link] Whether this breadcrumb is a link. + * @property {string} [name] Visible directory name. + * @property {boolean} [separator] Whether this is a path separator. + */ + +/** + * @param {string} prefix + * @returns {Breadcrumb[]} Breadcrumb presentation model. + */ +function breadcrumbs(prefix) { + let parts = prefix.split('/').filter(Boolean); + if (!parts.length) { + return [{ current: true, name: 'assets' }]; + } + + /** @type {Breadcrumb[]} */ + let result = [{ href: '/', link: true, name: 'assets' }]; + for (let index = 0; index < parts.length; index++) { + result.push({ separator: true }); + if (index === parts.length - 1) { + result.push({ current: true, name: parts[index] }); + } else { + let path = `${parts.slice(0, index + 1).join('/')}/`; + result.push({ href: publicPath(path), link: true, name: parts[index] }); + } + } + return result; +} + +/** @param {number} bytes */ +function formatSize(bytes) { + if (bytes < 1024) { + return `${bytes} B`; + } + + let units = ['KB', 'MB', 'GB', 'TB']; + let value = bytes; + let unit = 'B'; + for (let candidate of units) { + value /= 1024; + unit = candidate; + if (value < 1024) { + break; + } + } + + return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; +} + +/** @param {Date} date */ +function formatDate(date) { + return `${date.toISOString().slice(0, 19).replace('T', ' ')} UTC`; +} + +/** + * @param {string} origin + * @param {string} prefix + * @param {R2Objects} listing + * @param {string | undefined} cursor + */ +function directoryPage(origin, prefix, listing, cursor) { + let pathname = publicPath(prefix); + + return { + directories: listing.delimitedPrefixes.map(directoryPrefix => ({ + name: displayName(directoryPrefix, prefix), + pathname: publicPath(directoryPrefix), + prefix: directoryPrefix, + url: new URL(publicPath(directoryPrefix), origin).href, + })), + files: listing.objects.map(object => ({ + etag: object.etag, + key: object.key, + name: displayName(object.key, prefix), + pathname: publicPath(object.key), + size: object.size, + uploaded: object.uploaded, + url: new URL(publicPath(object.key), origin).href, + })), + nextCursor: listing.truncated ? listing.cursor : undefined, + pathname, + prefix, + requestedCursor: cursor, + url: new URL(pathname, origin).href, + }; +} + +/** + * @param {Request} request + * @param {R2Bucket} bucket + * @param {string} key + */ +async function localObjectResponse(request, bucket, key) { + let object = await bucket.get(key); + if (!object) { + return errorResponse(404, 'Object not found'); + } + + let headers = new Headers; + object.writeHttpMetadata(headers); + headers.set('Cache-Control', 'no-store'); + headers.set('ETag', object.httpEtag); + + return new Response(request.method === 'HEAD' ? null : object.body, { + headers, + }); +} + +/** + * @param {ReturnType} page + * @param {string} styleNonce + */ +function renderListing(page, styleNonce) { + let rows = [ + ...(page.prefix ? [{ + href: parentPath(page.prefix), + isParent: true, + name: 'Parent directory', + }] : []), + ...page.directories.map(directory => ({ + href: directory.pathname, + isFolder: true, + name: directory.name, + })), + ...page.files.map(file => ({ + date: formatDate(file.uploaded), + datetime: file.uploaded.toISOString(), + href: file.pathname, + isFile: true, + name: file.name, + size: formatSize(file.size), + })), + ]; + let folderCount = page.directories.length; + let fileCount = page.files.length; + + return Mustache.render(template, { + breadcrumbs: breadcrumbs(page.prefix), + canonicalUrl: page.url, + hasRows: rows.length > 0, + jsonCursor: page.requestedCursor ? encodeURIComponent(page.requestedCursor) : false, + jsonPath: page.pathname, + next: page.nextCursor ? { + href: `${page.pathname}?cursor=${encodeURIComponent(page.nextCursor)}`, + } : false, + rows, + styleNonce, + summary: `${folderCount} ${folderCount === 1 ? 'folder' : 'folders'} · ${fileCount} ${fileCount === 1 ? 'file' : 'files'}`, + title: `Asset browser — ${page.pathname}`, + }); +} + +/** + * @param {Request} request + * @param {ReturnType} page + */ +function jsonListingResponse(request, page) { + let next = page.nextCursor + ? new URL(`${page.pathname}?format=json&cursor=${encodeURIComponent(page.nextCursor)}`, page.url).href + : null; + return jsonResponse(request, { + schemaVersion: 1, + directory: { + prefix: page.prefix, + url: page.url, + }, + directories: page.directories.map(directory => ({ + name: directory.name, + prefix: directory.prefix, + url: directory.url, + })), + files: page.files.map(file => ({ + name: file.name, + key: file.key, + url: file.url, + size: file.size, + uploaded: file.uploaded.toISOString(), + etag: file.etag, + })), + next, + }); +} + +/** + * @param {Request} request + * @param {unknown} value + * @param {number} status + * @param {HeadersInit} headers + */ +function jsonResponse(request, value, status = 200, headers = {}) { + let body = JSON.stringify(value); + + return new Response(request.method === 'HEAD' ? null : body, { + status, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Cache-Control': 'no-store', + 'Content-Type': 'application/json; charset=utf-8', + 'X-Content-Type-Options': 'nosniff', + ...headers, + }, + }); +} + +/** + * @param {number} status + * @param {string} message + * @param {HeadersInit} headers + */ +function errorResponse(status, message, headers = {}) { + return new Response(message, { + status, + headers: { + 'Cache-Control': 'no-store', + 'Content-Type': 'text/plain; charset=utf-8', + ...headers, + }, + }); +} + +/** + * @param {Request} request + * @param {number} status + * @param {string} code + * @param {string} message + * @param {HeadersInit} headers + */ +function listingErrorResponse(request, status, code, message, headers = {}) { + if (new URL(request.url).searchParams.get('format') !== 'json') { + return errorResponse(status, message, headers); + } + + return jsonErrorResponse(request, status, code, message, headers); +} + +/** + * @param {Request} request + * @param {number} status + * @param {string} code + * @param {string} message + * @param {HeadersInit} headers + */ +function jsonErrorResponse(request, status, code, message, headers = {}) { + return jsonResponse(request, { + schemaVersion: 1, + error: { code, message }, + }, status, headers); +} + +/** @type {ExportedHandler} */ +export default { + async fetch(request, env) { + let url = new URL(request.url); + let localDevelopment = env.LOCAL_DEVELOPMENT === 'true'; + + if (!['GET', 'HEAD'].includes(request.method)) { + return listingErrorResponse(request, 405, 'method_not_allowed', 'Method not allowed', { Allow: 'GET, HEAD' }); + } + + let encodedPath; + if (url.pathname.startsWith(`${internalPrefix}/`)) { + encodedPath = url.pathname.slice(internalPrefix.length + 1); + } else if (localDevelopment && url.pathname.endsWith('/')) { + encodedPath = url.pathname.slice(1); + } else if (localDevelopment) { + let key; + try { + key = decodeURIComponent(url.pathname.slice(1)); + } catch { + return listingErrorResponse(request, 400, 'invalid_path', 'Invalid path'); + } + return localObjectResponse(request, env.ASSETS, key); + } else { + return listingErrorResponse(request, 404, 'not_found', 'Not found'); + } + + let prefix; + try { + prefix = decodeURIComponent(encodedPath); + } catch { + return listingErrorResponse(request, 400, 'invalid_path', 'Invalid path'); + } + + let format = url.searchParams.get('format'); + if (format && format !== 'json') { + return jsonErrorResponse(request, 400, 'unsupported_format', 'Unsupported format'); + } + + let cursor = url.searchParams.get('cursor') || undefined; + /** @type {R2Objects} */ + let listing; + try { + listing = await env.ASSETS.list({ + cursor, + delimiter: '/', + limit: pageSize, + prefix, + }); + } catch (error) { + console.error(JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + message: 'R2 directory listing failed', + prefix, + })); + return listingErrorResponse(request, 500, 'listing_unavailable', 'Directory listing unavailable'); + } + if (prefix && !listing.objects.length && !listing.delimitedPrefixes.length) { + return listingErrorResponse(request, 404, 'directory_not_found', 'Directory not found'); + } + + let page = directoryPage(url.origin, prefix, listing, cursor); + if (url.searchParams.get('format') === 'json') { + return jsonListingResponse(request, page); + } + + let styleNonce = crypto.randomUUID().replaceAll('-', ''); + let html = renderListing(page, styleNonce); + + return new Response(request.method === 'HEAD' ? null : html, { + headers: { + 'Cache-Control': 'no-store', + 'Content-Security-Policy': `default-src 'none'; style-src 'nonce-${styleNonce}'; base-uri 'none'; frame-ancestors 'none'`, + 'Content-Type': 'text/html; charset=utf-8', + 'X-Content-Type-Options': 'nosniff', + 'X-Robots-Tag': 'noindex', + }, + }); + }, +}; diff --git a/workers/assets-directory-browser/src/index.test.mjs b/workers/assets-directory-browser/src/index.test.mjs new file mode 100644 index 0000000..5e373a4 --- /dev/null +++ b/workers/assets-directory-browser/src/index.test.mjs @@ -0,0 +1,286 @@ +import { describe, expect, it, vi } from 'vitest'; +import { env } from 'cloudflare:workers'; +import worker from './index.js'; + +class FakeBucket +{ + calls = []; + + constructor(result = {}) { + this.result = { + cursor: 'next page', + delimitedPrefixes: ['data/locale/'], + objects: [{ + key: 'data/schedules.json', + size: 54861, + uploaded: new Date('2026-08-30T17:30:26Z'), + }], + truncated: true, + ...result, + }; + } + + async list(options) { + this.calls.push(options); + return this.result; + } +} + +describe('assets directory browser', () => { + it('uses natural directory paths only when local development is enabled', async () => { + await env.ASSETS.put('local/example.json', '{}'); + let localEnv = { + ASSETS: env.ASSETS, + LOCAL_DEVELOPMENT: 'true', + }; + + let localResponse = await worker.fetch( + new Request('http://localhost:8787/local/'), + localEnv, + ); + let productionResponse = await worker.fetch( + new Request('https://assets.splatoon3.ink/local/'), + env, + ); + + expect(localResponse.status).toBe(200); + expect(await localResponse.text()).toContain('href="/local/example.json"'); + expect(productionResponse.status).toBe(404); + }); + + it('serves linked objects from local R2 only in local development', async () => { + await env.ASSETS.put('local/example.json', '{"local":true}', { + httpMetadata: { contentType: 'application/json' }, + }); + let localEnv = { + ASSETS: env.ASSETS, + LOCAL_DEVELOPMENT: 'true', + }; + + let response = await worker.fetch( + new Request('http://localhost:8787/local/example.json'), + localEnv, + ); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + expect(await response.text()).toBe('{"local":true}'); + }); + + it('lists objects from the configured R2 binding using canonical public links', async () => { + await env.ASSETS.put('data/schedules.json', 'schedule data'); + await env.ASSETS.put('data/locale/en-US.json', 'locale data'); + + let request = new Request( + 'https://assets.splatoon3.ink/__directory/data/', + ); + + let response = await worker.fetch(request, env); + let html = await response.text(); + + expect(response.status).toBe(200); + expect(response.headers.get('cache-control')).toBe('no-store'); + expect(response.headers.get('content-type')).toBe('text/html; charset=utf-8'); + expect(html).not.toContain('

Asset browser

'); + expect(html).not.toContain('Public Splatoon 3 data and media'); + expect(html).toContain('data'); + expect(html).toContain('href="/"'); + expect(html).toContain('href="/data/locale/"'); + expect(html).toContain('href="/data/schedules.json"'); + expect(html).not.toContain('__directory'); + }); + + it('returns a public JSON representation of a directory page', async () => { + let bucket = new FakeBucket({ + cursor: 'next page', + delimitedPrefixes: ['splatnet/images/badges/'], + objects: [{ + etag: 'opaque-etag', + key: 'splatnet/images/example.png', + size: 48123, + uploaded: new Date('2026-08-30T01:02:03Z'), + }], + truncated: true, + }); + let response = await worker.fetch(new Request( + 'https://assets.splatoon3.ink/__directory/splatnet/images/?format=json&cursor=current%20page', + ), { ASSETS: bucket }); + + expect(response.status).toBe(200); + expect(response.headers.get('access-control-allow-origin')).toBe('*'); + expect(response.headers.get('cache-control')).toBe('no-store'); + expect(response.headers.get('content-type')).toBe('application/json; charset=utf-8'); + expect(await response.json()).toEqual({ + schemaVersion: 1, + directory: { + prefix: 'splatnet/images/', + url: 'https://assets.splatoon3.ink/splatnet/images/', + }, + directories: [{ + name: 'badges/', + prefix: 'splatnet/images/badges/', + url: 'https://assets.splatoon3.ink/splatnet/images/badges/', + }], + files: [{ + name: 'example.png', + key: 'splatnet/images/example.png', + url: 'https://assets.splatoon3.ink/splatnet/images/example.png', + size: 48123, + uploaded: '2026-08-30T01:02:03.000Z', + etag: 'opaque-etag', + }], + next: 'https://assets.splatoon3.ink/splatnet/images/?format=json&cursor=next%20page', + }); + expect(bucket.calls).toEqual([{ + cursor: 'current page', + delimiter: '/', + limit: 1000, + prefix: 'splatnet/images/', + }]); + }); + + it('renders responsive light and dark themes under a nonce-based CSP', async () => { + let response = await worker.fetch( + new Request('https://assets.splatoon3.ink/__directory/'), + env, + ); + let html = await response.text(); + let policy = response.headers.get('content-security-policy'); + let nonce = policy?.match(/style-src 'nonce-([^']+)'/)?.[1]; + + expect(nonce).toBeTruthy(); + expect(html).toContain(` + + +
+
+
+ +
+ {{summary}} + JSON +
+
+
+ + + + {{#rows}} + + + + + + {{/rows}} + {{^hasRows}}{{/hasRows}} + +
NameSizeLast modified
{{#isParent}}{{/isParent}}{{#isFolder}}{{/isFolder}}{{#isFile}}{{/isFile}}{{name}}{{#isFolder}}Folder{{/isFolder}}{{#isFile}}{{size}}{{/isFile}}{{#isFile}}{{/isFile}}
This directory is empty.
+
+ +
+
+ + diff --git a/workers/assets-directory-browser/tsconfig.json b/workers/assets-directory-browser/tsconfig.json new file mode 100644 index 0000000..a6aa974 --- /dev/null +++ b/workers/assets-directory-browser/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "strict": true, + "target": "ES2022", + "types": ["@cloudflare/workers-types"] + }, + "include": [ + "src/index.js", + "src/local-env.d.ts", + "src/template.d.ts", + "worker-configuration.d.ts" + ] +} diff --git a/workers/assets-directory-browser/vitest.config.mjs b/workers/assets-directory-browser/vitest.config.mjs new file mode 100644 index 0000000..97a73a7 --- /dev/null +++ b/workers/assets-directory-browser/vitest.config.mjs @@ -0,0 +1,15 @@ +import { cloudflareTest } from '@cloudflare/vitest-plugin'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { + configPath: './workers/assets-directory-browser/wrangler.jsonc', + }, + }), + ], + test: { + include: ['workers/assets-directory-browser/src/**/*.test.mjs'], + }, +}); diff --git a/workers/assets-directory-browser/worker-configuration.d.ts b/workers/assets-directory-browser/worker-configuration.d.ts new file mode 100644 index 0000000..e89c258 --- /dev/null +++ b/workers/assets-directory-browser/worker-configuration.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: ebbe4016149875d3bf7884f67316fd2a) +interface __BaseEnv_Env { + ASSETS: R2Bucket; +} +declare namespace Cloudflare { + interface GlobalProps { + mainModule: typeof import("./src/index"); + } + interface Env extends __BaseEnv_Env {} +} +interface Env extends __BaseEnv_Env {} diff --git a/workers/assets-directory-browser/wrangler.jsonc b/workers/assets-directory-browser/wrangler.jsonc new file mode 100644 index 0000000..484d4b1 --- /dev/null +++ b/workers/assets-directory-browser/wrangler.jsonc @@ -0,0 +1,31 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "splatoon3-assets-directory-browser", + "main": "src/index.js", + "compatibility_date": "2026-08-30", + "compatibility_flags": ["nodejs_compat"], + "workers_dev": false, + "preview_urls": false, + "routes": [ + { + "pattern": "assets.splatoon3.ink/__directory/*", + "zone_name": "splatoon3.ink" + } + ], + "r2_buckets": [ + { + "binding": "ASSETS", + "bucket_name": "splatoon3-ink-assets" + } + ], + "observability": { + "enabled": true, + "logs": { + "head_sampling_rate": 1 + }, + "traces": { + "enabled": true, + "head_sampling_rate": 0.01 + } + } +} From 8d157963f0dd2e28e99fef9c49be88361cdd7f52 Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Sun, 30 Aug 2026 19:56:22 -0700 Subject: [PATCH 2/4] Remove Worker TypeScript tooling --- eslint.config.mjs | 2 +- package-lock.json | 13 +++---------- package.json | 4 ---- workers/assets-directory-browser/README.md | 2 -- .../src/local-env.d.ts | 3 --- .../src/template.d.ts | 4 ---- .../assets-directory-browser/tsconfig.json | 19 ------------------- .../worker-configuration.d.ts | 12 ------------ 8 files changed, 4 insertions(+), 55 deletions(-) delete mode 100644 workers/assets-directory-browser/src/local-env.d.ts delete mode 100644 workers/assets-directory-browser/src/template.d.ts delete mode 100644 workers/assets-directory-browser/tsconfig.json delete mode 100644 workers/assets-directory-browser/worker-configuration.d.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 7410385..b853371 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -53,7 +53,7 @@ export default [ }, }, - // Cloudflare types are generated from the Worker configuration. + // Cloudflare runtime types referenced by Worker JSDoc. { files: ['workers/**/*.js'], rules: { diff --git a/package-lock.json b/package-lock.json index 0a29017..3a0a036 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,12 +38,10 @@ }, "devDependencies": { "@cloudflare/vitest-plugin": "^1.1.2", - "@cloudflare/workers-types": "^5.20260830.1", "@dword-design/eslint-plugin-import-alias": "^8.1.3", "@eslint/js": "^10.0.1", "@intlify/unplugin-vue-i18n": "^11.2.5", "@tailwindcss/vite": "^4.3.3", - "@types/mustache": "^4.2.6", "@vitejs/plugin-vue": "^6.0.8", "eslint": "^10.9.1", "eslint-plugin-jsdoc": "^63.3.3", @@ -1562,7 +1560,9 @@ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260831.1.tgz", "integrity": "sha512-yXg4pwfYjhsDH9rYc3qZ3K+z62DCSvO/aj7GiZo6AyDeWGZpyFRpPMYcQ6LF/zfaf1x0Ngw2gSqL8JjuUtMGlA==", "dev": true, - "license": "MIT OR Apache-2.0" + "license": "MIT OR Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", @@ -5249,13 +5249,6 @@ "integrity": "sha512-jJ41Q4z6ZVO260MNDdHfW7+7a5iMiX8Mr6ZJHcmgrvhZha6dz5704o/lF2kKl6URjH6ivEL97w9xS/MgpJEphg==", "license": "MIT" }, - "node_modules/@types/mustache": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.6.tgz", - "integrity": "sha512-t+8/QWTAhOFlrF1IVZqKnMRJi84EgkIK5Kh0p2JV4OLywUvCwJPFxbJAl7XAow7DVIHsF+xW9f1MVzg0L6Szjw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "26.4.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", diff --git a/package.json b/package.json index 3bc0983..1799357 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,6 @@ "build": "vite build", "assets-browser:dev": "wrangler dev --local --var LOCAL_DEVELOPMENT:true --persist-to .wrangler/state --cwd workers/assets-directory-browser", "assets-browser:test": "vitest run --config workers/assets-directory-browser/vitest.config.mjs", - "assets-browser:types": "wrangler types --include-runtime false --cwd workers/assets-directory-browser", - "assets-browser:typecheck": "tsc --project workers/assets-directory-browser/tsconfig.json", "assets-browser:dry-run": "wrangler deploy --dry-run --cwd workers/assets-directory-browser", "preview": "vite preview --port 5050", "lint": "eslint .", @@ -70,12 +68,10 @@ }, "devDependencies": { "@cloudflare/vitest-plugin": "^1.1.2", - "@cloudflare/workers-types": "^5.20260830.1", "@dword-design/eslint-plugin-import-alias": "^8.1.3", "@eslint/js": "^10.0.1", "@intlify/unplugin-vue-i18n": "^11.2.5", "@tailwindcss/vite": "^4.3.3", - "@types/mustache": "^4.2.6", "@vitejs/plugin-vue": "^6.0.8", "eslint": "^10.9.1", "eslint-plugin-jsdoc": "^63.3.3", diff --git a/workers/assets-directory-browser/README.md b/workers/assets-directory-browser/README.md index 591afe5..6792753 100644 --- a/workers/assets-directory-browser/README.md +++ b/workers/assets-directory-browser/README.md @@ -64,8 +64,6 @@ connects to the production bucket. ```sh npm run assets-browser:test -npm run assets-browser:types -npm run assets-browser:typecheck npm run assets-browser:dry-run ``` diff --git a/workers/assets-directory-browser/src/local-env.d.ts b/workers/assets-directory-browser/src/local-env.d.ts deleted file mode 100644 index 46952ee..0000000 --- a/workers/assets-directory-browser/src/local-env.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface Env { - LOCAL_DEVELOPMENT?: string; -} diff --git a/workers/assets-directory-browser/src/template.d.ts b/workers/assets-directory-browser/src/template.d.ts deleted file mode 100644 index 15d69e1..0000000 --- a/workers/assets-directory-browser/src/template.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.html' { - const template: string; - export default template; -} diff --git a/workers/assets-directory-browser/tsconfig.json b/workers/assets-directory-browser/tsconfig.json deleted file mode 100644 index a6aa974..0000000 --- a/workers/assets-directory-browser/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "lib": ["ES2022"], - "module": "ESNext", - "moduleResolution": "Bundler", - "noEmit": true, - "strict": true, - "target": "ES2022", - "types": ["@cloudflare/workers-types"] - }, - "include": [ - "src/index.js", - "src/local-env.d.ts", - "src/template.d.ts", - "worker-configuration.d.ts" - ] -} diff --git a/workers/assets-directory-browser/worker-configuration.d.ts b/workers/assets-directory-browser/worker-configuration.d.ts deleted file mode 100644 index e89c258..0000000 --- a/workers/assets-directory-browser/worker-configuration.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable */ -// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: ebbe4016149875d3bf7884f67316fd2a) -interface __BaseEnv_Env { - ASSETS: R2Bucket; -} -declare namespace Cloudflare { - interface GlobalProps { - mainModule: typeof import("./src/index"); - } - interface Env extends __BaseEnv_Env {} -} -interface Env extends __BaseEnv_Env {} From 57e46c3121a0fd9cb683eebe5d8d2d735499b5fc Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Sun, 30 Aug 2026 20:04:06 -0700 Subject: [PATCH 3/4] Automate asset browser deployment --- .github/workflows/build-frontend.yml | 1 + package.json | 3 ++- workers/assets-directory-browser/README.md | 8 +++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-frontend.yml b/.github/workflows/build-frontend.yml index b1493e0..c5c2432 100644 --- a/.github/workflows/build-frontend.yml +++ b/.github/workflows/build-frontend.yml @@ -25,3 +25,4 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build + - run: npm run assets-browser:deploy:dry-run diff --git a/package.json b/package.json index 1799357..7b59f55 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "build": "vite build", "assets-browser:dev": "wrangler dev --local --var LOCAL_DEVELOPMENT:true --persist-to .wrangler/state --cwd workers/assets-directory-browser", "assets-browser:test": "vitest run --config workers/assets-directory-browser/vitest.config.mjs", - "assets-browser:dry-run": "wrangler deploy --dry-run --cwd workers/assets-directory-browser", + "assets-browser:deploy": "wrangler deploy --cwd workers/assets-directory-browser", + "assets-browser:deploy:dry-run": "wrangler deploy --dry-run --cwd workers/assets-directory-browser", "preview": "vite preview --port 5050", "lint": "eslint .", "lint-fix": "npm run lint -- --fix", diff --git a/workers/assets-directory-browser/README.md b/workers/assets-directory-browser/README.md index 6792753..80f3fd0 100644 --- a/workers/assets-directory-browser/README.md +++ b/workers/assets-directory-browser/README.md @@ -64,9 +64,15 @@ connects to the production bucket. ```sh npm run assets-browser:test -npm run assets-browser:dry-run +npm run assets-browser:deploy:dry-run ``` +`npm run assets-browser:deploy` performs the production deployment. Production +deployments are handled by Cloudflare Workers Builds, configured from the +repository root with `main` as the production branch, +`npm run assets-browser:test` as the build command, and +`npm run assets-browser:deploy` as the deploy command. + ## Production rollout Do not create the URL Rewrite Rule until the Worker route has been deployed and From 00957e3c9c3ca46f5a36c7dbbd932460f0686979 Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Sun, 30 Aug 2026 20:35:02 -0700 Subject: [PATCH 4/4] Make asset browser rows clickable --- .../src/template.html | 106 ++++++++++++------ 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/workers/assets-directory-browser/src/template.html b/workers/assets-directory-browser/src/template.html index c3ed250..4aca772 100644 --- a/workers/assets-directory-browser/src/template.html +++ b/workers/assets-directory-browser/src/template.html @@ -112,50 +112,78 @@ .json-link:hover { text-decoration: underline; } - .table-wrap { overflow-x: auto; } + .listing-wrap { overflow-x: auto; } - table { + .listing { + min-width: 520px; width: 100%; - border-collapse: collapse; font-size: 0.9rem; } - th { + .listing-items { + margin: 0; + padding: 0; + list-style: none; + } + + .listing-header, .listing-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 210px 110px; + } + + .listing-header > span { padding: 12px 20px; color: var(--muted); font-size: 0.7rem; font-weight: 750; letter-spacing: 0.08em; - text-align: left; text-transform: uppercase; } - th:nth-child(2), td:nth-child(2) { width: 110px; } - th:nth-child(3), td:nth-child(3) { width: 210px; } - - td { - height: 56px; - padding: 8px 20px; + .listing-row { + min-height: 56px; border-top: 1px solid var(--border); color: var(--muted); + text-decoration: none; + transition: background-color 120ms ease; + } + + .listing-row:hover { background: var(--row-hover); } + .listing-row:focus-visible { + border-radius: 4px; + outline: 2px solid var(--accent); + outline-offset: -3px; + } + + .listing-cell { + display: flex; + min-width: 0; + align-items: center; + padding: 8px 20px; white-space: nowrap; } - tr { transition: background-color 120ms ease; } - tbody tr:hover { background: var(--row-hover); } + .visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } .entry { - display: inline-flex; max-width: min(560px, 58vw); - align-items: center; gap: 12px; color: var(--text); font-weight: 620; - text-decoration: none; } - .entry span:last-child { overflow: hidden; text-overflow: ellipsis; } - .entry:hover span:last-child { color: var(--accent); text-decoration: underline; } + .entry-name { overflow: hidden; text-overflow: ellipsis; } + .listing-row:hover .entry-name { color: var(--accent); text-decoration: underline; } .entry-icon { position: relative; @@ -221,7 +249,13 @@ font-weight: 700; } - .empty-state { height: 120px; text-align: center; } + .empty-state { + display: grid; + height: 120px; + place-items: center; + border-top: 1px solid var(--border); + text-align: center; + } .browser-footer { display: flex; @@ -264,8 +298,8 @@ .shell { width: min(100% - 24px, 960px); padding: 24px 0; } .browser-header { align-items: flex-start; flex-direction: column; gap: 6px; } .browser-meta { width: 100%; justify-content: space-between; } - th, td { padding-right: 14px; padding-left: 14px; } - th:nth-child(2), td:nth-child(2) { width: 92px; } + .listing-header, .listing-row { grid-template-columns: minmax(0, 1fr) 190px 92px; } + .listing-header > span, .listing-cell { padding-right: 14px; padding-left: 14px; } .entry { max-width: calc(100vw - 150px); } } @@ -280,20 +314,22 @@ JSON -
- - - - {{#rows}} - - - - - - {{/rows}} - {{^hasRows}}{{/hasRows}} - -
NameSizeLast modified
{{#isParent}}{{/isParent}}{{#isFolder}}{{/isFolder}}{{#isFile}}{{/isFile}}{{name}}{{#isFolder}}Folder{{/isFolder}}{{#isFile}}{{size}}{{/isFile}}{{#isFile}}{{/isFile}}
This directory is empty.
+