feat: initial cfdav project with webdav+r2+d1 and pages admin docs
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
.dist
|
||||||
|
.wrangler
|
||||||
|
.env
|
||||||
150
README.md
Normal file
150
README.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# cfdav
|
||||||
|
|
||||||
|
A minimal WebDAV → Cloudflare R2 service built on Cloudflare Workers + Hono, with D1 metadata and a small admin UI (Cloudflare Pages).
|
||||||
|
|
||||||
|
## ✅ Project Purpose
|
||||||
|
- Provide a **lightweight WebDAV gateway** backed by Cloudflare R2
|
||||||
|
- Support **multi-user isolation** (each user sees only their own file tree)
|
||||||
|
- Offer a **simple admin UI** to manage users (add/delete)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Features
|
||||||
|
- WebDAV endpoints: OPTIONS, PROPFIND, GET, HEAD, PUT, MKCOL, DELETE, MOVE, COPY, LOCK, UNLOCK, PROPPATCH
|
||||||
|
- Basic Auth (multi-user in D1; bootstrap admin supported)
|
||||||
|
- Metadata stored in D1 (SQLite)
|
||||||
|
- File content stored in R2 (binding: FILES)
|
||||||
|
- Windows WebDAV compatibility headers (DAV + MS-Author-Via)
|
||||||
|
- Admin UI (Cloudflare Pages) for user management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Deployment Flow (Production)
|
||||||
|
|
||||||
|
### 1) Install
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Configure `wrangler.toml`
|
||||||
|
Edit `wrangler.toml` and fill real IDs:
|
||||||
|
```toml
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "cfdav-db"
|
||||||
|
database_id = "YOUR_D1_DATABASE_ID"
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = "FILES"
|
||||||
|
bucket_name = "YOUR_R2_BUCKET"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
BASIC_USER = "YOUR_BOOTSTRAP_EMAIL"
|
||||||
|
BASIC_PASS = "YOUR_BOOTSTRAP_PASSWORD"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) Create D1 + apply migrations
|
||||||
|
```bash
|
||||||
|
wrangler d1 create cfdav-db
|
||||||
|
wrangler d1 migrations apply cfdav-db --remote
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4) Create R2 bucket
|
||||||
|
```bash
|
||||||
|
wrangler r2 bucket create <your-bucket-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5) Deploy Worker
|
||||||
|
```bash
|
||||||
|
wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Admin UI (Cloudflare Pages)
|
||||||
|
|
||||||
|
### 1) Create Pages Project
|
||||||
|
```bash
|
||||||
|
wrangler pages project create cfdav-admin --production-branch main
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Deploy static UI
|
||||||
|
```bash
|
||||||
|
cd web
|
||||||
|
wrangler pages deploy . --project-name cfdav-admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3) Login
|
||||||
|
- API Base: `https://<your-worker-domain>`
|
||||||
|
- Email/Password: your admin account
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ User Management
|
||||||
|
|
||||||
|
### Bootstrap admin
|
||||||
|
If `users` table is empty, first login with:
|
||||||
|
- `BASIC_USER`
|
||||||
|
- `BASIC_PASS`
|
||||||
|
|
||||||
|
This account will be auto-created as **admin**.
|
||||||
|
|
||||||
|
### Create user via UI
|
||||||
|
Open the Pages admin UI and create users from the form.
|
||||||
|
|
||||||
|
### Create user via CLI (optional)
|
||||||
|
```bash
|
||||||
|
node tools/hash.mjs <password>
|
||||||
|
wrangler d1 execute cfdav-db --remote --command \
|
||||||
|
"INSERT INTO users (id,email,password_hash,is_admin,created_at) VALUES ('<uuid>','user@example.com','<HASH>',0,'<ISO>')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ WebDAV Usage
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
```
|
||||||
|
https://<your-worker-domain>/dav/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example (curl)
|
||||||
|
```bash
|
||||||
|
# upload
|
||||||
|
curl -u user@example.com:password -T ./file.txt https://<your-domain>/dav/file.txt
|
||||||
|
|
||||||
|
# list
|
||||||
|
curl -u user@example.com:password -X PROPFIND -H 'Depth: 1' https://<your-domain>/dav/
|
||||||
|
|
||||||
|
# download
|
||||||
|
curl -u user@example.com:password https://<your-domain>/dav/file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows Mount (Explorer)
|
||||||
|
- 右键“此电脑” → “映射网络驱动器” → 地址:
|
||||||
|
`https://<your-domain>/dav/`
|
||||||
|
- 账号:邮箱
|
||||||
|
- 密码:对应密码
|
||||||
|
|
||||||
|
### macOS Finder
|
||||||
|
- Finder → “前往” → “连接服务器”
|
||||||
|
- 输入:`https://<your-domain>/dav/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Required Config Parameters
|
||||||
|
- **Cloudflare API Token** (Workers + D1 + R2 + Pages权限)
|
||||||
|
- **CLOUDFLARE_ACCOUNT_ID**
|
||||||
|
- `D1 database_id`
|
||||||
|
- `R2 bucket_name`
|
||||||
|
- `BASIC_USER` / `BASIC_PASS`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- WebDAV endpoint: `/dav`
|
||||||
|
- Admin API: `/api/admin/users`
|
||||||
|
- R2 binding: `FILES`
|
||||||
|
- D1 binding: `DB`
|
||||||
|
|
||||||
24
README.windows.md
Normal file
24
README.windows.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Windows WebDAV Mount Guide (cfdav)
|
||||||
|
|
||||||
|
## 1) 先确认站点
|
||||||
|
- WebDAV 地址:`https://<your-domain>/dav/`
|
||||||
|
- Basic Auth:邮箱 + 密码
|
||||||
|
|
||||||
|
## 2) Windows 资源管理器挂载
|
||||||
|
1. 打开“此电脑”
|
||||||
|
2. 顶部菜单 → “映射网络驱动器”
|
||||||
|
3. 地址输入:
|
||||||
|
`https://<your-domain>/dav/`
|
||||||
|
4. 使用其他凭据 → 输入账号/密码
|
||||||
|
|
||||||
|
## 3) 常见问题
|
||||||
|
- **提示“文件夹无效”**:
|
||||||
|
- 你的服务必须在 401 响应上携带 `DAV` 头
|
||||||
|
- 必须支持 `LOCK/UNLOCK`
|
||||||
|
- **无法列目录**:
|
||||||
|
- 确认 `PROPFIND` 返回 207,并根节点 `<href>` 与请求路径完全一致
|
||||||
|
|
||||||
|
## 4) 测试命令
|
||||||
|
```bash
|
||||||
|
curl -u user@example.com:password -X PROPFIND -H 'Depth: 1' https://<your-domain>/dav/
|
||||||
|
```
|
||||||
20
migrations/0001_init.sql
Normal file
20
migrations/0001_init.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- initial schema (files)
|
||||||
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
parent_id TEXT,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_folder INTEGER NOT NULL DEFAULT 0,
|
||||||
|
size INTEGER NOT NULL DEFAULT 0,
|
||||||
|
mime_type TEXT,
|
||||||
|
r2_key TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL,
|
||||||
|
deleted_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_path ON files(path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_parent ON files(parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_deleted ON files(deleted_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_owner ON files(owner_id);
|
||||||
9
migrations/0002_users.sql
Normal file
9
migrations/0002_users.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
-- users table for multi-user auth
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
2
migrations/0003_admin.sql
Normal file
2
migrations/0003_admin.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- add admin flag to users
|
||||||
|
ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;
|
||||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "cfdav",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wrangler dev",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"db:migrate": "wrangler d1 migrations apply cfdav-db",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.6.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20260311.0",
|
||||||
|
"wrangler": "^3.78.6",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
985
pnpm-lock.yaml
generated
Normal file
985
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,985 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
hono:
|
||||||
|
specifier: ^4.6.3
|
||||||
|
version: 4.12.8
|
||||||
|
devDependencies:
|
||||||
|
'@cloudflare/workers-types':
|
||||||
|
specifier: ^4.20260311.0
|
||||||
|
version: 4.20260317.1
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.6.3
|
||||||
|
version: 5.9.3
|
||||||
|
wrangler:
|
||||||
|
specifier: ^3.78.6
|
||||||
|
version: 3.114.17(@cloudflare/workers-types@4.20260317.1)
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
|
resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==}
|
||||||
|
engines: {node: '>=16.13'}
|
||||||
|
|
||||||
|
'@cloudflare/unenv-preset@2.0.2':
|
||||||
|
resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==}
|
||||||
|
peerDependencies:
|
||||||
|
unenv: 2.0.0-rc.14
|
||||||
|
workerd: ^1.20250124.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
workerd:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workerd-darwin-64@1.20250718.0':
|
||||||
|
resolution: {integrity: sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@cloudflare/workerd-darwin-arm64@1.20250718.0':
|
||||||
|
resolution: {integrity: sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@cloudflare/workerd-linux-64@1.20250718.0':
|
||||||
|
resolution: {integrity: sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@cloudflare/workerd-linux-arm64@1.20250718.0':
|
||||||
|
resolution: {integrity: sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@cloudflare/workerd-windows-64@1.20250718.0':
|
||||||
|
resolution: {integrity: sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260317.1':
|
||||||
|
resolution: {integrity: sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==}
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.9.1':
|
||||||
|
resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==}
|
||||||
|
|
||||||
|
'@esbuild-plugins/node-globals-polyfill@0.2.3':
|
||||||
|
resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
|
||||||
|
peerDependencies:
|
||||||
|
esbuild: '*'
|
||||||
|
|
||||||
|
'@esbuild-plugins/node-modules-polyfill@0.2.2':
|
||||||
|
resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==}
|
||||||
|
peerDependencies:
|
||||||
|
esbuild: '*'
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.17.19':
|
||||||
|
resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.17.19':
|
||||||
|
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.17.19':
|
||||||
|
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.17.19':
|
||||||
|
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.17.19':
|
||||||
|
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.17.19':
|
||||||
|
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.17.19':
|
||||||
|
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@fastify/busboy@2.1.1':
|
||||||
|
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||||
|
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||||
|
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||||
|
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.33.5':
|
||||||
|
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.33.5':
|
||||||
|
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.33.5':
|
||||||
|
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.33.5':
|
||||||
|
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.33.5':
|
||||||
|
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
acorn-walk@8.3.2:
|
||||||
|
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
acorn@8.14.0:
|
||||||
|
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
as-table@1.0.55:
|
||||||
|
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||||
|
|
||||||
|
blake3-wasm@2.1.5:
|
||||||
|
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||||
|
engines: {node: '>=7.0.0'}
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||||
|
|
||||||
|
color@4.2.3:
|
||||||
|
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||||
|
engines: {node: '>=12.5.0'}
|
||||||
|
|
||||||
|
cookie@0.7.2:
|
||||||
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
data-uri-to-buffer@2.0.2:
|
||||||
|
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
||||||
|
|
||||||
|
defu@6.1.4:
|
||||||
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
esbuild@0.17.19:
|
||||||
|
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
escape-string-regexp@4.0.0:
|
||||||
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
estree-walker@0.6.1:
|
||||||
|
resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
|
||||||
|
|
||||||
|
exit-hook@2.2.1:
|
||||||
|
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
exsolve@1.0.8:
|
||||||
|
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
get-source@2.0.12:
|
||||||
|
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
|
||||||
|
|
||||||
|
glob-to-regexp@0.4.1:
|
||||||
|
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||||
|
|
||||||
|
hono@4.12.8:
|
||||||
|
resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
|
||||||
|
engines: {node: '>=16.9.0'}
|
||||||
|
|
||||||
|
is-arrayish@0.3.4:
|
||||||
|
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
|
||||||
|
|
||||||
|
magic-string@0.25.9:
|
||||||
|
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||||
|
|
||||||
|
mime@3.0.0:
|
||||||
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
miniflare@3.20250718.3:
|
||||||
|
resolution: {integrity: sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ==}
|
||||||
|
engines: {node: '>=16.13'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
mustache@4.2.0:
|
||||||
|
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
ohash@2.0.11:
|
||||||
|
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||||
|
|
||||||
|
path-to-regexp@6.3.0:
|
||||||
|
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
|
printable-characters@1.0.42:
|
||||||
|
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||||
|
|
||||||
|
rollup-plugin-inject@3.0.2:
|
||||||
|
resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
|
||||||
|
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
|
||||||
|
|
||||||
|
rollup-plugin-node-polyfills@0.2.1:
|
||||||
|
resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==}
|
||||||
|
|
||||||
|
rollup-pluginutils@2.8.2:
|
||||||
|
resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
|
||||||
|
|
||||||
|
semver@7.7.4:
|
||||||
|
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
sharp@0.33.5:
|
||||||
|
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
|
simple-swizzle@0.2.4:
|
||||||
|
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||||
|
|
||||||
|
source-map@0.6.1:
|
||||||
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
sourcemap-codec@1.4.8:
|
||||||
|
resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==}
|
||||||
|
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||||
|
|
||||||
|
stacktracey@2.2.0:
|
||||||
|
resolution: {integrity: sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg==}
|
||||||
|
|
||||||
|
stoppable@1.1.0:
|
||||||
|
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
|
||||||
|
engines: {node: '>=4', npm: '>=6'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
ufo@1.6.3:
|
||||||
|
resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==}
|
||||||
|
|
||||||
|
undici@5.29.0:
|
||||||
|
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
|
||||||
|
engines: {node: '>=14.0'}
|
||||||
|
|
||||||
|
unenv@2.0.0-rc.14:
|
||||||
|
resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==}
|
||||||
|
|
||||||
|
workerd@1.20250718.0:
|
||||||
|
resolution: {integrity: sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
wrangler@3.114.17:
|
||||||
|
resolution: {integrity: sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA==}
|
||||||
|
engines: {node: '>=16.17.0'}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@cloudflare/workers-types': ^4.20250408.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@cloudflare/workers-types':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
ws@8.18.0:
|
||||||
|
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||||
|
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
|
||||||
|
|
||||||
|
youch@3.3.4:
|
||||||
|
resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
|
||||||
|
|
||||||
|
zod@3.22.3:
|
||||||
|
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@cloudflare/kv-asset-handler@0.3.4':
|
||||||
|
dependencies:
|
||||||
|
mime: 3.0.0
|
||||||
|
|
||||||
|
'@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250718.0)':
|
||||||
|
dependencies:
|
||||||
|
unenv: 2.0.0-rc.14
|
||||||
|
optionalDependencies:
|
||||||
|
workerd: 1.20250718.0
|
||||||
|
|
||||||
|
'@cloudflare/workerd-darwin-64@1.20250718.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workerd-darwin-arm64@1.20250718.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workerd-linux-64@1.20250718.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workerd-linux-arm64@1.20250718.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workerd-windows-64@1.20250718.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@cloudflare/workers-types@4.20260317.1': {}
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.9.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)':
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.17.19
|
||||||
|
|
||||||
|
'@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)':
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.17.19
|
||||||
|
escape-string-regexp: 4.0.0
|
||||||
|
rollup-plugin-node-polyfills: 0.2.1
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.17.19':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@fastify/busboy@2.1.1': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.33.5':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.9.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.33.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.33.5':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
acorn-walk@8.3.2: {}
|
||||||
|
|
||||||
|
acorn@8.14.0: {}
|
||||||
|
|
||||||
|
as-table@1.0.55:
|
||||||
|
dependencies:
|
||||||
|
printable-characters: 1.0.42
|
||||||
|
|
||||||
|
blake3-wasm@2.1.5: {}
|
||||||
|
|
||||||
|
color-convert@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
color-name@1.1.4:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
color-string@1.9.1:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.4
|
||||||
|
simple-swizzle: 0.2.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
color@4.2.3:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 2.0.1
|
||||||
|
color-string: 1.9.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
data-uri-to-buffer@2.0.2: {}
|
||||||
|
|
||||||
|
defu@6.1.4: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
esbuild@0.17.19:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/android-arm': 0.17.19
|
||||||
|
'@esbuild/android-arm64': 0.17.19
|
||||||
|
'@esbuild/android-x64': 0.17.19
|
||||||
|
'@esbuild/darwin-arm64': 0.17.19
|
||||||
|
'@esbuild/darwin-x64': 0.17.19
|
||||||
|
'@esbuild/freebsd-arm64': 0.17.19
|
||||||
|
'@esbuild/freebsd-x64': 0.17.19
|
||||||
|
'@esbuild/linux-arm': 0.17.19
|
||||||
|
'@esbuild/linux-arm64': 0.17.19
|
||||||
|
'@esbuild/linux-ia32': 0.17.19
|
||||||
|
'@esbuild/linux-loong64': 0.17.19
|
||||||
|
'@esbuild/linux-mips64el': 0.17.19
|
||||||
|
'@esbuild/linux-ppc64': 0.17.19
|
||||||
|
'@esbuild/linux-riscv64': 0.17.19
|
||||||
|
'@esbuild/linux-s390x': 0.17.19
|
||||||
|
'@esbuild/linux-x64': 0.17.19
|
||||||
|
'@esbuild/netbsd-x64': 0.17.19
|
||||||
|
'@esbuild/openbsd-x64': 0.17.19
|
||||||
|
'@esbuild/sunos-x64': 0.17.19
|
||||||
|
'@esbuild/win32-arm64': 0.17.19
|
||||||
|
'@esbuild/win32-ia32': 0.17.19
|
||||||
|
'@esbuild/win32-x64': 0.17.19
|
||||||
|
|
||||||
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
|
estree-walker@0.6.1: {}
|
||||||
|
|
||||||
|
exit-hook@2.2.1: {}
|
||||||
|
|
||||||
|
exsolve@1.0.8: {}
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
get-source@2.0.12:
|
||||||
|
dependencies:
|
||||||
|
data-uri-to-buffer: 2.0.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
|
||||||
|
glob-to-regexp@0.4.1: {}
|
||||||
|
|
||||||
|
hono@4.12.8: {}
|
||||||
|
|
||||||
|
is-arrayish@0.3.4:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
magic-string@0.25.9:
|
||||||
|
dependencies:
|
||||||
|
sourcemap-codec: 1.4.8
|
||||||
|
|
||||||
|
mime@3.0.0: {}
|
||||||
|
|
||||||
|
miniflare@3.20250718.3:
|
||||||
|
dependencies:
|
||||||
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
|
acorn: 8.14.0
|
||||||
|
acorn-walk: 8.3.2
|
||||||
|
exit-hook: 2.2.1
|
||||||
|
glob-to-regexp: 0.4.1
|
||||||
|
stoppable: 1.1.0
|
||||||
|
undici: 5.29.0
|
||||||
|
workerd: 1.20250718.0
|
||||||
|
ws: 8.18.0
|
||||||
|
youch: 3.3.4
|
||||||
|
zod: 3.22.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
mustache@4.2.0: {}
|
||||||
|
|
||||||
|
ohash@2.0.11: {}
|
||||||
|
|
||||||
|
path-to-regexp@6.3.0: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
|
printable-characters@1.0.42: {}
|
||||||
|
|
||||||
|
rollup-plugin-inject@3.0.2:
|
||||||
|
dependencies:
|
||||||
|
estree-walker: 0.6.1
|
||||||
|
magic-string: 0.25.9
|
||||||
|
rollup-pluginutils: 2.8.2
|
||||||
|
|
||||||
|
rollup-plugin-node-polyfills@0.2.1:
|
||||||
|
dependencies:
|
||||||
|
rollup-plugin-inject: 3.0.2
|
||||||
|
|
||||||
|
rollup-pluginutils@2.8.2:
|
||||||
|
dependencies:
|
||||||
|
estree-walker: 0.6.1
|
||||||
|
|
||||||
|
semver@7.7.4:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
sharp@0.33.5:
|
||||||
|
dependencies:
|
||||||
|
color: 4.2.3
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
semver: 7.7.4
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.33.5
|
||||||
|
'@img/sharp-darwin-x64': 0.33.5
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||||
|
'@img/sharp-linux-arm': 0.33.5
|
||||||
|
'@img/sharp-linux-arm64': 0.33.5
|
||||||
|
'@img/sharp-linux-s390x': 0.33.5
|
||||||
|
'@img/sharp-linux-x64': 0.33.5
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.33.5
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.33.5
|
||||||
|
'@img/sharp-wasm32': 0.33.5
|
||||||
|
'@img/sharp-win32-ia32': 0.33.5
|
||||||
|
'@img/sharp-win32-x64': 0.33.5
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
simple-swizzle@0.2.4:
|
||||||
|
dependencies:
|
||||||
|
is-arrayish: 0.3.4
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
source-map@0.6.1: {}
|
||||||
|
|
||||||
|
sourcemap-codec@1.4.8: {}
|
||||||
|
|
||||||
|
stacktracey@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
as-table: 1.0.55
|
||||||
|
get-source: 2.0.12
|
||||||
|
|
||||||
|
stoppable@1.1.0: {}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
ufo@1.6.3: {}
|
||||||
|
|
||||||
|
undici@5.29.0:
|
||||||
|
dependencies:
|
||||||
|
'@fastify/busboy': 2.1.1
|
||||||
|
|
||||||
|
unenv@2.0.0-rc.14:
|
||||||
|
dependencies:
|
||||||
|
defu: 6.1.4
|
||||||
|
exsolve: 1.0.8
|
||||||
|
ohash: 2.0.11
|
||||||
|
pathe: 2.0.3
|
||||||
|
ufo: 1.6.3
|
||||||
|
|
||||||
|
workerd@1.20250718.0:
|
||||||
|
optionalDependencies:
|
||||||
|
'@cloudflare/workerd-darwin-64': 1.20250718.0
|
||||||
|
'@cloudflare/workerd-darwin-arm64': 1.20250718.0
|
||||||
|
'@cloudflare/workerd-linux-64': 1.20250718.0
|
||||||
|
'@cloudflare/workerd-linux-arm64': 1.20250718.0
|
||||||
|
'@cloudflare/workerd-windows-64': 1.20250718.0
|
||||||
|
|
||||||
|
wrangler@3.114.17(@cloudflare/workers-types@4.20260317.1):
|
||||||
|
dependencies:
|
||||||
|
'@cloudflare/kv-asset-handler': 0.3.4
|
||||||
|
'@cloudflare/unenv-preset': 2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250718.0)
|
||||||
|
'@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
|
||||||
|
'@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19)
|
||||||
|
blake3-wasm: 2.1.5
|
||||||
|
esbuild: 0.17.19
|
||||||
|
miniflare: 3.20250718.3
|
||||||
|
path-to-regexp: 6.3.0
|
||||||
|
unenv: 2.0.0-rc.14
|
||||||
|
workerd: 1.20250718.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@cloudflare/workers-types': 4.20260317.1
|
||||||
|
fsevents: 2.3.3
|
||||||
|
sharp: 0.33.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
ws@8.18.0: {}
|
||||||
|
|
||||||
|
youch@3.3.4:
|
||||||
|
dependencies:
|
||||||
|
cookie: 0.7.2
|
||||||
|
mustache: 4.2.0
|
||||||
|
stacktracey: 2.2.0
|
||||||
|
|
||||||
|
zod@3.22.3: {}
|
||||||
651
src/index.ts
Normal file
651
src/index.ts
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
import { Hono } from 'hono';
|
||||||
|
import { cors } from 'hono/cors';
|
||||||
|
|
||||||
|
type Env = {
|
||||||
|
DB: D1Database;
|
||||||
|
FILES: R2Bucket;
|
||||||
|
BASIC_USER?: string;
|
||||||
|
BASIC_PASS?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Variables = {
|
||||||
|
userId: string;
|
||||||
|
userEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FileRow = {
|
||||||
|
id: string;
|
||||||
|
owner_id: string;
|
||||||
|
parent_id: string | null;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
is_folder: number;
|
||||||
|
size: number;
|
||||||
|
mime_type: string | null;
|
||||||
|
r2_key: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserRow = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
password_hash: string;
|
||||||
|
is_admin: number;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||||
|
|
||||||
|
const DAV_BASE_HEADERS = {
|
||||||
|
DAV: '1, 2',
|
||||||
|
'MS-Author-Via': 'DAV',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PBKDF2_ITERATIONS = 100_000;
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
'*',
|
||||||
|
cors({
|
||||||
|
origin: '*',
|
||||||
|
allowMethods: [
|
||||||
|
'GET',
|
||||||
|
'POST',
|
||||||
|
'PUT',
|
||||||
|
'PATCH',
|
||||||
|
'DELETE',
|
||||||
|
'OPTIONS',
|
||||||
|
'PROPFIND',
|
||||||
|
'PROPPATCH',
|
||||||
|
'MKCOL',
|
||||||
|
'COPY',
|
||||||
|
'MOVE',
|
||||||
|
'HEAD',
|
||||||
|
'LOCK',
|
||||||
|
'UNLOCK',
|
||||||
|
],
|
||||||
|
allowHeaders: ['Content-Type', 'Authorization', 'Depth', 'Destination', 'X-Requested-With', 'If', 'Lock-Token'],
|
||||||
|
exposeHeaders: ['Content-Length', 'Content-Range', 'ETag', 'DAV', 'Lock-Token'],
|
||||||
|
maxAge: 86400,
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.get('/', (c) => c.json({ name: 'cfdav', status: 'ok' }));
|
||||||
|
|
||||||
|
function nowIso() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePath(path: string) {
|
||||||
|
if (!path.startsWith('/')) return '/' + path;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToBytes(hex: string): Uint8Array {
|
||||||
|
const out = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < out.length; i++) {
|
||||||
|
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pbkdf2Hash(password: string, iterations = PBKDF2_ITERATIONS): Promise<string> {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
const baseKey = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
|
||||||
|
const bits = await crypto.subtle.deriveBits(
|
||||||
|
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
|
||||||
|
baseKey,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
const hash = new Uint8Array(bits);
|
||||||
|
return `pbkdf2:${iterations}:${bytesToHex(salt)}:${bytesToHex(hash)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyPassword(password: string, stored: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const [scheme, iterStr, saltHex, hashHex] = stored.split(':');
|
||||||
|
if (scheme !== 'pbkdf2') return false;
|
||||||
|
const iterations = Number(iterStr);
|
||||||
|
const salt = hexToBytes(saltHex);
|
||||||
|
const expected = hexToBytes(hashHex);
|
||||||
|
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const baseKey = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
|
||||||
|
const bits = await crypto.subtle.deriveBits(
|
||||||
|
{ name: 'PBKDF2', salt, iterations, hash: 'SHA-256' },
|
||||||
|
baseKey,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
const actual = new Uint8Array(bits);
|
||||||
|
if (actual.length !== expected.length) return false;
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < actual.length; i++) diff |= actual[i] ^ expected[i];
|
||||||
|
return diff === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBasicAuthHeader(authHeader: string | undefined): { email: string; password: string } | null {
|
||||||
|
if (!authHeader || !authHeader.startsWith('Basic ')) return null;
|
||||||
|
try {
|
||||||
|
const credentials = atob(authHeader.slice(6));
|
||||||
|
const idx = credentials.indexOf(':');
|
||||||
|
if (idx === -1) return null;
|
||||||
|
return {
|
||||||
|
email: credentials.slice(0, idx),
|
||||||
|
password: credentials.slice(idx + 1),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureBootstrapUser(db: D1Database, env: Env): Promise<void> {
|
||||||
|
const countRes = await db.prepare('SELECT COUNT(*) as c FROM users').first<{ c: number }>();
|
||||||
|
const userCount = Number(countRes?.c || 0);
|
||||||
|
if (userCount > 0) return;
|
||||||
|
|
||||||
|
const email = env.BASIC_USER || 'demo@example.com';
|
||||||
|
const pass = env.BASIC_PASS || 'demo123';
|
||||||
|
const hash = await pbkdf2Hash(pass);
|
||||||
|
await db
|
||||||
|
.prepare('INSERT INTO users (id, email, password_hash, is_admin, created_at) VALUES (?, ?, ?, 1, ?)')
|
||||||
|
.bind(crypto.randomUUID(), email, hash, nowIso())
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unauthorized(): Promise<Response> {
|
||||||
|
return new Response('Unauthorized', {
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
'WWW-Authenticate': 'Basic realm="cfdav"',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function basicAuth(c: any, next: any) {
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
await ensureBootstrapUser(db, c.env as Env);
|
||||||
|
|
||||||
|
const parsed = parseBasicAuthHeader(c.req.header('Authorization'));
|
||||||
|
if (!parsed) return unauthorized();
|
||||||
|
|
||||||
|
const user = await db.prepare('SELECT * FROM users WHERE email = ? LIMIT 1').bind(parsed.email).first<UserRow>();
|
||||||
|
if (!user) return unauthorized();
|
||||||
|
|
||||||
|
const ok = await verifyPassword(parsed.password, user.password_hash);
|
||||||
|
if (!ok) return unauthorized();
|
||||||
|
|
||||||
|
c.set('userId', user.id);
|
||||||
|
c.set('userEmail', user.email);
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findFileByPath(db: D1Database, ownerId: string, path: string): Promise<FileRow | undefined> {
|
||||||
|
const p = normalizePath(path);
|
||||||
|
const row = await db
|
||||||
|
.prepare('SELECT * FROM files WHERE owner_id = ? AND path = ? AND deleted_at IS NULL LIMIT 1')
|
||||||
|
.bind(ownerId, p)
|
||||||
|
.first<FileRow>();
|
||||||
|
if (row) return row;
|
||||||
|
|
||||||
|
if (p.endsWith('/')) {
|
||||||
|
return db
|
||||||
|
.prepare('SELECT * FROM files WHERE owner_id = ? AND path = ? AND deleted_at IS NULL LIMIT 1')
|
||||||
|
.bind(ownerId, p.slice(0, -1))
|
||||||
|
.first<FileRow>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return db
|
||||||
|
.prepare('SELECT * FROM files WHERE owner_id = ? AND path = ? AND deleted_at IS NULL LIMIT 1')
|
||||||
|
.bind(ownerId, p + '/')
|
||||||
|
.first<FileRow>();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listChildren(db: D1Database, ownerId: string, parentId: string | null): Promise<FileRow[]> {
|
||||||
|
if (parentId === null) {
|
||||||
|
const { results } = await db
|
||||||
|
.prepare('SELECT * FROM files WHERE owner_id = ? AND parent_id IS NULL AND deleted_at IS NULL ORDER BY is_folder DESC, name ASC')
|
||||||
|
.bind(ownerId)
|
||||||
|
.all<FileRow>();
|
||||||
|
return (results || []) as FileRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { results } = await db
|
||||||
|
.prepare('SELECT * FROM files WHERE owner_id = ? AND parent_id = ? AND deleted_at IS NULL ORDER BY is_folder DESC, name ASC')
|
||||||
|
.bind(ownerId, parentId)
|
||||||
|
.all<FileRow>();
|
||||||
|
return (results || []) as FileRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPropfindXML(items: FileRow[], rawPath: string, includeRoot: boolean): string {
|
||||||
|
const responses: string[] = [];
|
||||||
|
|
||||||
|
if (includeRoot) {
|
||||||
|
responses.push(`
|
||||||
|
<response>
|
||||||
|
<href>${escapeXml(rawPath)}</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<displayname></displayname>
|
||||||
|
<resourcetype><collection/></resourcetype>
|
||||||
|
<getlastmodified>${new Date().toUTCString()}</getlastmodified>
|
||||||
|
<creationdate>${new Date().toISOString()}</creationdate>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of items) {
|
||||||
|
let logicalPath = file.path;
|
||||||
|
if (!logicalPath.startsWith('/')) logicalPath = '/' + logicalPath;
|
||||||
|
if (file.is_folder && !logicalPath.endsWith('/')) logicalPath += '/';
|
||||||
|
const href = '/dav' + logicalPath;
|
||||||
|
|
||||||
|
responses.push(`
|
||||||
|
<response>
|
||||||
|
<href>${escapeXml(href)}</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(file.name)}</displayname>
|
||||||
|
<getcontentlength>${file.size}</getcontentlength>
|
||||||
|
<getlastmodified>${new Date(file.updated_at).toUTCString()}</getlastmodified>
|
||||||
|
<creationdate>${file.created_at}</creationdate>
|
||||||
|
<resourcetype>${file.is_folder ? '<collection/>' : ''}</resourcetype>
|
||||||
|
<getcontenttype>${file.mime_type || 'application/octet-stream'}</getcontenttype>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>\n<multistatus xmlns="DAV:">${responses.join('')}\n</multistatus>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dav = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||||
|
|
||||||
|
dav.options('/*', () => {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
Allow: 'OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, PROPFIND, PROPPATCH, MOVE, COPY, LOCK, UNLOCK',
|
||||||
|
'Content-Length': '0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dav.use('*', basicAuth);
|
||||||
|
|
||||||
|
dav.all('/*', async (c) => {
|
||||||
|
const method = c.req.method.toUpperCase();
|
||||||
|
const rawPath = new URL(c.req.url).pathname;
|
||||||
|
const path = rawPath.replace(/^\/dav/, '') || '/';
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case 'PROPFIND':
|
||||||
|
return handlePropfind(c, path, rawPath);
|
||||||
|
case 'GET':
|
||||||
|
case 'HEAD':
|
||||||
|
return handleGet(c, path, method === 'HEAD');
|
||||||
|
case 'PUT':
|
||||||
|
return handlePut(c, path);
|
||||||
|
case 'MKCOL':
|
||||||
|
return handleMkcol(c, path);
|
||||||
|
case 'DELETE':
|
||||||
|
return handleDelete(c, path);
|
||||||
|
case 'MOVE':
|
||||||
|
return handleMove(c, path);
|
||||||
|
case 'COPY':
|
||||||
|
return handleCopy(c, path);
|
||||||
|
case 'LOCK':
|
||||||
|
return handleLock(rawPath);
|
||||||
|
case 'UNLOCK':
|
||||||
|
return new Response(null, { status: 204, headers: DAV_BASE_HEADERS });
|
||||||
|
case 'PROPPATCH':
|
||||||
|
return handleProppatch(rawPath);
|
||||||
|
default:
|
||||||
|
return new Response('Method Not Allowed', { status: 405, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handlePropfind(c: any, path: string, rawPath: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const depth = c.req.header('Depth') || '1';
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const isRoot = path === '/' || path === '';
|
||||||
|
|
||||||
|
const xmlHeaders = {
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (depth === '0') {
|
||||||
|
if (isRoot) {
|
||||||
|
return new Response(buildPropfindXML([], rawPath, true), { status: 207, headers: xmlHeaders });
|
||||||
|
}
|
||||||
|
const current = await findFileByPath(db, ownerId, path);
|
||||||
|
return new Response(buildPropfindXML(current ? [current] : [], rawPath, false), { status: 207, headers: xmlHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
const items = await listChildren(db, ownerId, null);
|
||||||
|
return new Response(buildPropfindXML(items, rawPath, true), { status: 207, headers: xmlHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = await findFileByPath(db, ownerId, path);
|
||||||
|
if (!parent) return new Response(buildPropfindXML([], rawPath, false), { status: 207, headers: xmlHeaders });
|
||||||
|
|
||||||
|
const items = await listChildren(db, ownerId, parent.id);
|
||||||
|
return new Response(buildPropfindXML(items, rawPath, true), { status: 207, headers: xmlHeaders });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGet(c: any, path: string, headOnly: boolean) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
|
||||||
|
if (path === '/' || path === '') {
|
||||||
|
return new Response(headOnly ? null : 'Root Collection', {
|
||||||
|
status: 200,
|
||||||
|
headers: { ...DAV_BASE_HEADERS, 'Content-Type': 'text/html', 'Content-Length': '14' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = await findFileByPath(db, ownerId, path);
|
||||||
|
if (!file) return new Response('Not Found', { status: 404, headers: DAV_BASE_HEADERS });
|
||||||
|
if (file.is_folder) return new Response('Is a collection', { status: 400, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
const obj = await c.env.FILES.get(file.r2_key);
|
||||||
|
if (!obj) return new Response('Not Found', { status: 404, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
return new Response(headOnly ? null : obj.body, {
|
||||||
|
headers: {
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
'Content-Type': file.mime_type || 'application/octet-stream',
|
||||||
|
'Content-Length': String(file.size),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePut(c: any, path: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const body = await c.req.arrayBuffer();
|
||||||
|
const fileName = path.split('/').pop() || 'untitled';
|
||||||
|
const parentPath = path.lastIndexOf('/') > 0 ? path.slice(0, path.lastIndexOf('/')) : '/';
|
||||||
|
|
||||||
|
let parentId: string | null = null;
|
||||||
|
if (parentPath !== '/') {
|
||||||
|
const parent = await findFileByPath(db, ownerId, parentPath);
|
||||||
|
if (!parent) {
|
||||||
|
const parts = parentPath.split('/').filter(Boolean);
|
||||||
|
let currentPath = '';
|
||||||
|
let currentParentId: string | null = null;
|
||||||
|
for (const part of parts) {
|
||||||
|
currentPath = currentPath ? `${currentPath}/${part}` : `/${part}`;
|
||||||
|
const folder = await findFileByPath(db, ownerId, currentPath);
|
||||||
|
if (!folder) {
|
||||||
|
const folderId = crypto.randomUUID();
|
||||||
|
const now = nowIso();
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
'INSERT INTO files (id, owner_id, parent_id, path, name, is_folder, size, mime_type, r2_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, 0, NULL, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.bind(folderId, ownerId, currentParentId, currentPath, part, `folders/${folderId}`, now, now)
|
||||||
|
.run();
|
||||||
|
currentParentId = folderId;
|
||||||
|
} else {
|
||||||
|
currentParentId = folder.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentId = currentParentId;
|
||||||
|
} else {
|
||||||
|
parentId = parent.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await findFileByPath(db, ownerId, path);
|
||||||
|
const fileId = existing?.id || crypto.randomUUID();
|
||||||
|
const mimeType = c.req.header('Content-Type') || 'application/octet-stream';
|
||||||
|
const now = nowIso();
|
||||||
|
const r2Key = `files/${ownerId}/${fileId}/${fileName}`;
|
||||||
|
|
||||||
|
await c.env.FILES.put(r2Key, body, { httpMetadata: { contentType: mimeType } });
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await db
|
||||||
|
.prepare('UPDATE files SET size = ?, mime_type = ?, updated_at = ?, r2_key = ? WHERE id = ? AND owner_id = ?')
|
||||||
|
.bind(body.byteLength, mimeType, now, r2Key, fileId, ownerId)
|
||||||
|
.run();
|
||||||
|
return new Response(null, { status: 204, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
'INSERT INTO files (id, owner_id, parent_id, path, name, is_folder, size, mime_type, r2_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.bind(fileId, ownerId, parentId, path, fileName, body.byteLength, mimeType, r2Key, now, now)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return new Response(null, { status: 201, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMkcol(c: any, path: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const folderName = path.split('/').pop() || 'untitled';
|
||||||
|
const parentPath = path.lastIndexOf('/') > 0 ? path.slice(0, path.lastIndexOf('/')) : '/';
|
||||||
|
|
||||||
|
let parentId: string | null = null;
|
||||||
|
if (parentPath !== '/') {
|
||||||
|
const parent = await findFileByPath(db, ownerId, parentPath);
|
||||||
|
if (!parent) return new Response('Conflict: parent not found', { status: 409, headers: DAV_BASE_HEADERS });
|
||||||
|
parentId = parent.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = path.endsWith('/') ? path : path + '/';
|
||||||
|
const existing = await findFileByPath(db, ownerId, normalizedPath);
|
||||||
|
if (existing) return new Response('Method Not Allowed: already exists', { status: 405, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
const folderId = crypto.randomUUID();
|
||||||
|
const now = nowIso();
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
'INSERT INTO files (id, owner_id, parent_id, path, name, is_folder, size, mime_type, r2_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 1, 0, NULL, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.bind(folderId, ownerId, parentId, normalizedPath, folderName, `folders/${folderId}`, now, now)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return new Response(null, { status: 201, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(c: any, path: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const file = await findFileByPath(db, ownerId, path);
|
||||||
|
if (!file) return new Response('Not Found', { status: 404, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
if (!file.is_folder) await c.env.FILES.delete(file.r2_key);
|
||||||
|
await db.prepare('UPDATE files SET deleted_at = ? WHERE id = ? AND owner_id = ?').bind(nowIso(), file.id, ownerId).run();
|
||||||
|
|
||||||
|
return new Response(null, { status: 204, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMove(c: any, path: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const destination = c.req.header('Destination');
|
||||||
|
if (!destination) return new Response('Destination header required', { status: 400, headers: DAV_BASE_HEADERS });
|
||||||
|
const destPath = new URL(destination).pathname.replace(/^\/dav/, '') || '/';
|
||||||
|
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const file = await findFileByPath(db, ownerId, path);
|
||||||
|
if (!file) return new Response('Not Found', { status: 404, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
const newName = destPath.split('/').pop() || file.name;
|
||||||
|
const destParentPath = destPath.lastIndexOf('/') > 0 ? destPath.slice(0, destPath.lastIndexOf('/')) : '/';
|
||||||
|
let destParentId: string | null = null;
|
||||||
|
if (destParentPath !== '/') {
|
||||||
|
const destParent = await findFileByPath(db, ownerId, destParentPath);
|
||||||
|
destParentId = destParent?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.prepare('UPDATE files SET name = ?, path = ?, parent_id = ?, updated_at = ? WHERE id = ? AND owner_id = ?')
|
||||||
|
.bind(newName, destPath, destParentId, nowIso(), file.id, ownerId)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
return new Response(null, { status: 201, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCopy(c: any, path: string) {
|
||||||
|
const ownerId = c.get('userId') as string;
|
||||||
|
const destination = c.req.header('Destination');
|
||||||
|
if (!destination) return new Response('Destination header required', { status: 400, headers: DAV_BASE_HEADERS });
|
||||||
|
const destPath = new URL(destination).pathname.replace(/^\/dav/, '') || '/';
|
||||||
|
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const file = await findFileByPath(db, ownerId, path);
|
||||||
|
if (!file) return new Response('Not Found', { status: 404, headers: DAV_BASE_HEADERS });
|
||||||
|
|
||||||
|
const newName = destPath.split('/').pop() || file.name;
|
||||||
|
const newId = crypto.randomUUID();
|
||||||
|
const now = nowIso();
|
||||||
|
|
||||||
|
if (!file.is_folder) {
|
||||||
|
const src = await c.env.FILES.get(file.r2_key);
|
||||||
|
if (src) {
|
||||||
|
const newKey = `files/${ownerId}/${newId}/${newName}`;
|
||||||
|
await c.env.FILES.put(newKey, src.body, {
|
||||||
|
httpMetadata: { contentType: file.mime_type || 'application/octet-stream' },
|
||||||
|
});
|
||||||
|
await db
|
||||||
|
.prepare(
|
||||||
|
'INSERT INTO files (id, owner_id, parent_id, path, name, is_folder, size, mime_type, r2_key, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)'
|
||||||
|
)
|
||||||
|
.bind(newId, ownerId, file.parent_id, destPath, newName, file.size, file.mime_type, newKey, now, now)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 201, headers: DAV_BASE_HEADERS });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLock(rawPath: string) {
|
||||||
|
const token = `urn:uuid:${crypto.randomUUID()}`;
|
||||||
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<prop xmlns="DAV:">
|
||||||
|
<lockdiscovery>
|
||||||
|
<activelock>
|
||||||
|
<locktype><write/></locktype>
|
||||||
|
<lockscope><exclusive/></lockscope>
|
||||||
|
<depth>0</depth>
|
||||||
|
<owner/>
|
||||||
|
<timeout>Second-3600</timeout>
|
||||||
|
<locktoken><href>${escapeXml(token)}</href></locktoken>
|
||||||
|
<lockroot><href>${escapeXml(rawPath)}</href></lockroot>
|
||||||
|
</activelock>
|
||||||
|
</lockdiscovery>
|
||||||
|
</prop>`;
|
||||||
|
|
||||||
|
return new Response(xml, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
'Lock-Token': `<${token}>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProppatch(rawPath: string) {
|
||||||
|
const xml = `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<multistatus xmlns="DAV:">
|
||||||
|
<response>
|
||||||
|
<href>${escapeXml(rawPath)}</href>
|
||||||
|
<propstat>
|
||||||
|
<prop/>
|
||||||
|
<status>HTTP/1.1 403 Forbidden</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
|
</multistatus>`;
|
||||||
|
|
||||||
|
return new Response(xml, {
|
||||||
|
status: 207,
|
||||||
|
headers: {
|
||||||
|
...DAV_BASE_HEADERS,
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ───────────────────────────────────────────────────────────
|
||||||
|
// Admin API
|
||||||
|
// ───────────────────────────────────────────────────────────
|
||||||
|
const api = new Hono<{ Bindings: Env; Variables: Variables }>();
|
||||||
|
|
||||||
|
api.use('*', basicAuth);
|
||||||
|
|
||||||
|
api.use('*', async (c, next) => {
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const user = await db.prepare('SELECT * FROM users WHERE id = ? LIMIT 1').bind(c.get('userId')).first<UserRow>();
|
||||||
|
if (!user || user.is_admin !== 1) {
|
||||||
|
return c.json({ success: false, error: 'forbidden' }, 403);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
|
api.get('/admin/users', async (c) => {
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const { results } = await db.prepare('SELECT id,email,is_admin,created_at FROM users ORDER BY created_at DESC').all();
|
||||||
|
return c.json({ success: true, data: results || [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
api.post('/admin/users', async (c) => {
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
const body = await c.req.json();
|
||||||
|
const email = String(body.email || '').trim();
|
||||||
|
const password = String(body.password || '');
|
||||||
|
const isAdmin = body.isAdmin ? 1 : 0;
|
||||||
|
if (!email || !password) return c.json({ success: false, error: 'email/password required' }, 400);
|
||||||
|
|
||||||
|
const existing = await db.prepare('SELECT id FROM users WHERE email = ? LIMIT 1').bind(email).first();
|
||||||
|
if (existing) return c.json({ success: false, error: 'user exists' }, 409);
|
||||||
|
|
||||||
|
const hash = await pbkdf2Hash(password);
|
||||||
|
await db
|
||||||
|
.prepare('INSERT INTO users (id,email,password_hash,is_admin,created_at) VALUES (?, ?, ?, ?, ?)')
|
||||||
|
.bind(crypto.randomUUID(), email, hash, isAdmin, nowIso())
|
||||||
|
.run();
|
||||||
|
return c.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
api.delete('/admin/users/:id', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const db = c.env.DB as D1Database;
|
||||||
|
await db.prepare('DELETE FROM users WHERE id = ?').bind(id).run();
|
||||||
|
return c.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.route('/api', api);
|
||||||
|
app.route('/dav', dav);
|
||||||
|
|
||||||
|
export default app;
|
||||||
30
tools/hash.mjs
Normal file
30
tools/hash.mjs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { webcrypto } from 'node:crypto';
|
||||||
|
|
||||||
|
const password = process.argv[2];
|
||||||
|
if (!password) {
|
||||||
|
console.error('Usage: node tools/hash.mjs <password>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PBKDF2_ITERATIONS = 100_000;
|
||||||
|
const SALT_BYTES = 16;
|
||||||
|
|
||||||
|
async function hashPassword(pw) {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const salt = webcrypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
||||||
|
const baseKey = await webcrypto.subtle.importKey('raw', enc.encode(pw), 'PBKDF2', false, ['deriveBits']);
|
||||||
|
const bits = await webcrypto.subtle.deriveBits(
|
||||||
|
{ name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
|
||||||
|
baseKey,
|
||||||
|
256
|
||||||
|
);
|
||||||
|
const hashArr = new Uint8Array(bits);
|
||||||
|
const saltHex = [...salt].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
const hashHex = [...hashArr].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
return `pbkdf2:${PBKDF2_ITERATIONS}:${saltHex}:${hashHex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hashPassword(password).then((h) => {
|
||||||
|
console.log(h);
|
||||||
|
});
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "WebWorker"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["@cloudflare/workers-types"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
117
web/app.js
Normal file
117
web/app.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
function log(msg) {
|
||||||
|
const el = $('log');
|
||||||
|
el.textContent = `[${new Date().toISOString()}] ${msg}\n` + el.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthHeader() {
|
||||||
|
const email = $('email').value.trim();
|
||||||
|
const pass = $('password').value;
|
||||||
|
const token = btoa(`${email}:${pass}`);
|
||||||
|
return `Basic ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiBase() {
|
||||||
|
const base = $('apiBase').value.trim();
|
||||||
|
return base ? base.replace(/\/$/, '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiFetch(path, options = {}) {
|
||||||
|
const url = apiBase() + path;
|
||||||
|
const headers = options.headers || {};
|
||||||
|
headers['Authorization'] = getAuthHeader();
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
const res = await fetch(url, { ...options, headers });
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers() {
|
||||||
|
const data = await apiFetch('/api/admin/users');
|
||||||
|
const list = data.data || [];
|
||||||
|
const tbody = $('userList');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
list.forEach((u) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${u.email}</td>
|
||||||
|
<td>${u.is_admin ? 'yes' : 'no'}</td>
|
||||||
|
<td>${u.created_at}</td>
|
||||||
|
<td><button data-id="${u.id}">Delete</button></td>
|
||||||
|
`;
|
||||||
|
tr.querySelector('button').addEventListener('click', () => deleteUser(u.id));
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
log('Loaded users');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser() {
|
||||||
|
const email = $('newEmail').value.trim();
|
||||||
|
const password = $('newPassword').value;
|
||||||
|
const isAdmin = $('newIsAdmin').checked;
|
||||||
|
if (!email || !password) {
|
||||||
|
log('Email and password required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await apiFetch('/api/admin/users', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, password, isAdmin })
|
||||||
|
});
|
||||||
|
$('newEmail').value = '';
|
||||||
|
$('newPassword').value = '';
|
||||||
|
$('newIsAdmin').checked = false;
|
||||||
|
log('User created');
|
||||||
|
await loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(id) {
|
||||||
|
if (!confirm('Delete this user?')) return;
|
||||||
|
await apiFetch(`/api/admin/users/${id}`, { method: 'DELETE' });
|
||||||
|
log('User deleted');
|
||||||
|
await loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
localStorage.setItem('cfdav_api_base', $('apiBase').value.trim());
|
||||||
|
localStorage.setItem('cfdav_email', $('email').value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
$('apiBase').value = localStorage.getItem('cfdav_api_base') || '';
|
||||||
|
$('email').value = localStorage.getItem('cfdav_email') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoggedIn(state) {
|
||||||
|
$('loginCard').classList.toggle('hidden', state);
|
||||||
|
$('app').classList.toggle('hidden', !state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
try {
|
||||||
|
saveSettings();
|
||||||
|
await loadUsers();
|
||||||
|
setLoggedIn(true);
|
||||||
|
log('Login success');
|
||||||
|
} catch (e) {
|
||||||
|
setLoggedIn(false);
|
||||||
|
log(`Login failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
$('password').value = '';
|
||||||
|
setLoggedIn(false);
|
||||||
|
log('Logged out');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('loginBtn').addEventListener('click', login);
|
||||||
|
$('refreshBtn').addEventListener('click', () => loadUsers().catch((e) => log(e.message)));
|
||||||
|
$('createBtn').addEventListener('click', () => createUser().catch((e) => log(e.message)));
|
||||||
|
$('logoutBtn').addEventListener('click', logout);
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
setLoggedIn(false);
|
||||||
77
web/index.html
Normal file
77
web/index.html
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>cfdav Admin</title>
|
||||||
|
<link rel="stylesheet" href="./style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<h1>cfdav Admin</h1>
|
||||||
|
|
||||||
|
<section id="loginCard" class="card">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>API Base URL</label>
|
||||||
|
<input id="apiBase" placeholder="https://cfdav.fnos.workers.dev" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Email</label>
|
||||||
|
<input id="email" placeholder="admin@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Password</label>
|
||||||
|
<input id="password" type="password" placeholder="******" />
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn">Login</button>
|
||||||
|
<p class="hint">提示:API Base 留空则默认同域(/api/admin)。</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="app" class="hidden">
|
||||||
|
<section class="card">
|
||||||
|
<h2>Users</h2>
|
||||||
|
<div class="toolbar">
|
||||||
|
<button id="refreshBtn">Refresh</button>
|
||||||
|
<button id="logoutBtn" class="ghost">Logout</button>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Admin</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="userList"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Create User</h2>
|
||||||
|
<div class="row">
|
||||||
|
<label>Email</label>
|
||||||
|
<input id="newEmail" placeholder="user@example.com" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Password</label>
|
||||||
|
<input id="newPassword" type="password" placeholder="password" />
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Admin</label>
|
||||||
|
<input id="newIsAdmin" type="checkbox" />
|
||||||
|
</div>
|
||||||
|
<button id="createBtn">Create</button>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Log</h2>
|
||||||
|
<pre id="log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1
web/style.css
Normal file
1
web/style.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*{box-sizing:border-box;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}body{margin:0;background:#0b0f14;color:#e6edf3}.container{max-width:960px;margin:40px auto;padding:0 16px}.card{background:#111827;border:1px solid #1f2937;border-radius:12px;padding:16px;margin-bottom:16px}h1,h2{margin:0 0 12px}label{display:block;margin-bottom:6px;color:#9ca3af}.row{margin-bottom:12px}input{width:100%;padding:8px;border-radius:8px;border:1px solid #374151;background:#0f172a;color:#e6edf3}button{padding:8px 14px;border:0;border-radius:8px;background:#2563eb;color:#fff;cursor:pointer}button:hover{background:#1d4ed8}.ghost{background:#374151}.ghost:hover{background:#4b5563}.toolbar{margin-bottom:8px;display:flex;gap:8px;align-items:center}table{width:100%;border-collapse:collapse}th,td{border-bottom:1px solid #1f2937;padding:8px;text-align:left}.hint{color:#9ca3af;font-size:12px}pre{background:#0f172a;border:1px solid #1f2937;border-radius:8px;padding:10px;white-space:pre-wrap}.hidden{display:none}
|
||||||
21
wrangler.toml
Normal file
21
wrangler.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name = "cfdav"
|
||||||
|
main = "src/index.ts"
|
||||||
|
compatibility_date = "2024-01-01"
|
||||||
|
compatibility_flags = ["nodejs_compat"]
|
||||||
|
|
||||||
|
[[d1_databases]]
|
||||||
|
binding = "DB"
|
||||||
|
database_name = "cfdav-db"
|
||||||
|
database_id = "YOUR_D1_DATABASE_ID"
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = "FILES"
|
||||||
|
bucket_name = "YOUR_R2_BUCKET"
|
||||||
|
|
||||||
|
[vars]
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
BASIC_USER = "YOUR_BOOTSTRAP_EMAIL"
|
||||||
|
BASIC_PASS = "YOUR_BOOTSTRAP_PASSWORD"
|
||||||
|
|
||||||
|
[triggers]
|
||||||
|
crons = []
|
||||||
Reference in New Issue
Block a user