From 56179e6a7595d4d11e0be910afd7909551ac24d7 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Mon, 23 Mar 2026 04:08:27 +0800 Subject: [PATCH] feat: import smsreceiver workerized code with full README --- README.md | 220 +++++ pages/public/app.css | 95 ++ pages/public/app.js | 319 +++++++ pages/public/index.html | 13 + worker/migrate_sqlite_to_d1.py | 56 ++ worker/package-lock.json | 1531 ++++++++++++++++++++++++++++++++ worker/package.json | 17 + worker/schema.sql | 31 + worker/src/index.ts | 47 + worker/src/middlewares/auth.ts | 10 + worker/src/routes/auth.ts | 33 + worker/src/routes/logs.ts | 29 + worker/src/routes/messages.ts | 70 ++ worker/src/routes/receive.ts | 99 +++ worker/src/routes/stats.ts | 24 + worker/src/utils/common.ts | 9 + worker/src/utils/db.ts | 40 + worker/src/utils/session.ts | 39 + worker/src/utils/sign.ts | 14 + worker/tsconfig.json | 10 + worker/wrangler.toml | 19 + 21 files changed, 2725 insertions(+) create mode 100644 README.md create mode 100644 pages/public/app.css create mode 100644 pages/public/app.js create mode 100644 pages/public/index.html create mode 100644 worker/migrate_sqlite_to_d1.py create mode 100644 worker/package-lock.json create mode 100644 worker/package.json create mode 100644 worker/schema.sql create mode 100644 worker/src/index.ts create mode 100644 worker/src/middlewares/auth.ts create mode 100644 worker/src/routes/auth.ts create mode 100644 worker/src/routes/logs.ts create mode 100644 worker/src/routes/messages.ts create mode 100644 worker/src/routes/receive.ts create mode 100644 worker/src/routes/stats.ts create mode 100644 worker/src/utils/common.ts create mode 100644 worker/src/utils/db.ts create mode 100644 worker/src/utils/session.ts create mode 100644 worker/src/utils/sign.ts create mode 100644 worker/tsconfig.json create mode 100644 worker/wrangler.toml diff --git a/README.md b/README.md new file mode 100644 index 0000000..77fa5f1 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# SmsReceiver Worker(前后端分离版) + +> 将 SmsReceiver-go 迁移到 Cloudflare Worker + Pages + D1 的 **Worker 化**实现。 +> 目标是把「短信接收 API + 管理界面」拆分为独立的 API 服务与静态前端,提升部署弹性、跨域能力与运维简洁度。 + +## 开发目的 + +1. **Worker 化**:摆脱传统服务器进程部署,使用 Cloudflare Worker 作为 API 运行环境。 +2. **前后端分离**:前端使用 Pages 静态托管,API 单独在 Worker 上运行,天然解耦。 +3. **低运维成本**:无需自建数据库与运行时,D1 提供托管 SQLite 体验。 +4. **部署可重复**:通过 wrangler + 配置文件完成快速部署与迁移。 +5. **兼容原协议**:保留原 SmsReceiver-go 的字段、签名逻辑与接入方式。 + +## 架构概览 + +``` +[TranspondSms / 客户端] + | + | HTTP POST (token/sign) + v + Cloudflare Worker (Hono) + | + | D1 (SQLite) + v + 数据持久化 + 管理 API + + Cloudflare Pages (静态 UI) + | + | API_BASE 指向 Worker + v + 管理后台 +``` + +## 项目结构 + +``` +. +├── worker/ # Cloudflare Worker API (Hono + D1) +│ ├── src/ # API 代码 +│ ├── schema.sql # D1 表结构 +│ ├── migrate_sqlite_to_d1.py # 旧 sqlite 数据迁移脚本 +│ ├── wrangler.toml # Worker 配置 +│ └── package.json +└── pages/public/ # Cloudflare Pages 静态管理 UI + ├── index.html + ├── app.js + └── app.css +``` + +--- + +# 关键设计说明(详细解读) + +## 1. 前后端分离特性 + +- **API** 与 **UI** 完全解耦。 +- UI 仅为静态资源(HTML/JS/CSS),部署到 Pages; +- API 只处理数据与鉴权,部署到 Worker; +- UI 通过 `API_BASE` 指向后端,无需在同域运行。 + +**优势**: +- UI 部署独立、回滚简单。 +- API 可单独扩容/升级。 +- 适配多域名、CDN 就近访问。 + +## 2. 数据层:D1 (SQLite) + +- 使用 Cloudflare D1 替代本地 SQLite。 +- 表结构与原 Go 版本保持一致:`sms_messages`、`receive_logs`。 +- 使用 SQL 语句创建表,便于版本迁移与重建。 + +## 3. 兼容原接收协议 + +API 保持字段一致: + +- `from` / `content` / `timestamp` / `sign` / `device` / `sim` / `token` + +签名逻辑保持与 Go 版一致: + +``` +stringToSign = `${timestamp}\n${secret}` +sign = HMAC-SHA256(stringToSign) -> Base64 -> URL encode +``` + +## 4. 认证与登录 + +- 使用 `ADMIN_USER` + `ADMIN_PASS_HASH` 登录。 +- 登录密码使用 HMAC-SHA256 生成 hash(与 SESSION_SECRET 绑定)。 +- Cookie 采用 `SameSite=None; Secure`,解决跨域登录。 + +## 5. 查询能力 + +- 支持 `from`、时间区间筛选(start_ts / end_ts)。 +- UI 默认显示当前月,并提供“本月 / 上月 / 全部”。 +- 移动端适配:表格横向滚动,筛选区可滚动。 + +--- + +# 部署指南 + +## 1) 部署 Worker API + +```bash +cd worker +npm install +``` + +编辑 `wrangler.toml`: +- `database_id` +- vars: `ADMIN_USER` / `ADMIN_PASS_HASH` / `SESSION_SECRET` / `HMAC_SECRET` + +初始化 D1 表结构: +```bash +npx wrangler d1 execute smsreceiver --file=schema.sql +``` + +本地调试: +```bash +npx wrangler dev +``` + +发布: +```bash +npx wrangler deploy +``` + +--- + +## 2) 部署 Pages UI + +将 `pages/public/` 作为静态站点部署到 Cloudflare Pages。 + +修改 `pages/public/app.js`: +```js +const API_BASE = "https://你的-worker域名" +``` + +部署完成后即可访问管理后台。 + +--- + +## 3) 密码 hash 生成 + +当前登录逻辑:`HMAC_SHA256(password, SESSION_SECRET)` + +生成方式: +```bash +node -e "const c=require('crypto');console.log(c.createHmac('sha256','YOUR_SESSION_SECRET').update('YOUR_PASSWORD').digest('hex'))" +``` + +将结果写入 `ADMIN_PASS_HASH`。 + +--- + +## 4) 从旧 SQLite 迁移到 D1 + +脚本:`worker/migrate_sqlite_to_d1.py` + +需要环境变量: +- `CF_ACCOUNT_ID` +- `CF_API_TOKEN` +- `D1_DATABASE_ID` +- `SQLITE_PATH`(默认 `sms_receiver_go.db`) + +运行: +```bash +cd worker +python3 migrate_sqlite_to_d1.py +``` + +--- + +# 使用指南 + +## 1. 客户端上报短信 + +`POST /api/v1/receive` + +字段: +- `from`:发件人 +- `content`:短信正文 +- `timestamp`:Unix 时间戳 +- `token`:API Token +- `sign`:签名 +- `device` / `sim`:设备与卡槽信息(可选) + +## 2. 登录管理后台 + +- 访问 Pages URL +- 输入 `ADMIN_USER` / 密码 +- 可查看短信列表、详情、日志、筛选记录 + +## 3. 管理 API 端点示例 + +- `GET /api/v1/messages` +- `GET /api/v1/messages/:id` +- `GET /api/v1/logs` + +--- + +# 本地开发流程(推荐) + +1. 在 `worker/` 中使用 `wrangler dev` 启动本地 API。 +2. 在 `pages/public/` 中修改前端并直接用静态服务器预览。 +3. API_BASE 指向本地 API 或临时 Worker 域名。 +4. 测试完成后分别部署 Worker 与 Pages。 + +--- + +# 注意事项 + +- UI 与 API 部署后跨域必须使用 `SameSite=None; Secure` Cookie。 +- D1 为 SQLite 兼容,但限制与云端事务特性不同于本地。 +- API Token 与 HMAC 密钥要妥善保管。 + +--- + +# License + +内部项目使用,按需扩展。 diff --git a/pages/public/app.css b/pages/public/app.css new file mode 100644 index 0000000..be4abdf --- /dev/null +++ b/pages/public/app.css @@ -0,0 +1,95 @@ +:root { + --bg: #e8e8e8; + --card: #fefefe; + --ink: #1b1b1b; + --primary: #6c7cff; + --primary-dark: #4f5de1; + --danger: #e34a4a; + --border: #1b1b1b; + --shadow: 4px 4px 0 #1b1b1b; +} + +* { box-sizing: border-box; } +body { + margin: 0; + font-family: "Pixel", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", sans-serif; + background: var(--bg); + color: var(--ink); +} + +main { max-width: 1200px; margin: 24px auto; padding: 0 16px; } + +.pixel-card { + background: var(--card); + border: 2px solid var(--border); + box-shadow: var(--shadow); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.header { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; } +.title { font-size: 20px; font-weight: 700; } + +.nav { display: flex; gap: 8px; flex-wrap: wrap; } + +.btn { + display: inline-flex; align-items: center; justify-content: center; + padding: 6px 12px; border: 2px solid var(--border); + background: #fff; cursor: pointer; text-decoration: none; color: var(--ink); + box-shadow: 2px 2px 0 var(--border); border-radius: 6px; font-size: 14px; +} +.btn.active, .btn.primary { background: var(--primary); color: #fff; } +.btn.danger { background: var(--danger); color: #fff; } +.btn.small { padding: 4px 8px; font-size: 12px; } + +.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; } +.stat-card { text-align: center; } +.stat-card .label { font-size: 12px; color: #555; } +.stat-card .value { font-size: 28px; font-weight: 700; } + +.filter .tags { display: flex; flex-wrap: wrap; gap: 6px; } +.tag { + padding: 4px 10px; border: 2px solid var(--border); border-radius: 6px; + background: #fff; cursor: pointer; box-shadow: 2px 2px 0 var(--border); +} +.tag.active { background: var(--primary); color: #fff; } + +.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } +input { + padding: 8px 10px; border: 2px solid var(--border); border-radius: 6px; + box-shadow: 2px 2px 0 var(--border); outline: none; font-size: 14px; +} + +.table-wrap { overflow-x: auto; } +.table-wrap table { width: 100%; border-collapse: collapse; min-width: 720px; } +.table-wrap th, .table-wrap td { padding: 10px; border-bottom: 2px dashed #ccc; text-align: left; } +.table-wrap th { background: #f3f3f3; } +.ellipsis { max-width: 420px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.badge { padding: 2px 8px; border: 2px solid var(--border); border-radius: 6px; box-shadow: 2px 2px 0 var(--border); } +.badge.ok { background: #9be38b; } +.badge.err { background: #f08c8c; } + +.pager { display: flex; gap: 10px; align-items: center; justify-content: center; margin: 16px 0; flex-wrap: wrap; } + +.detail h3 { margin-top: 0; } +.detail-row { display: grid; grid-template-columns: 140px 1fr; gap: 8px; padding: 6px 0; border-bottom: 1px dashed #ddd; } +.detail-row .k { color: #666; } + +.login-wrap { min-height: 80vh; display: flex; align-items: center; justify-content: center; padding: 0 12px; } +.login-card { max-width: 420px; width: 100%; } +.form-group { margin-bottom: 12px; } +.form-group label { display: block; margin-bottom: 6px; } + +@media (max-width: 768px) { + main { padding: 0 10px; } + .header { padding: 12px; } + .title { width: 100%; text-align: center; } + .nav { width: 100%; justify-content: center; } + .stats { grid-template-columns: 1fr; } + .toolbar { flex-direction: column; align-items: stretch; } + .filter .tags { max-height: 120px; overflow-y: auto; } + .detail-row { grid-template-columns: 1fr; } + .table-wrap table { min-width: 600px; } +} diff --git a/pages/public/app.js b/pages/public/app.js new file mode 100644 index 0000000..884362d --- /dev/null +++ b/pages/public/app.js @@ -0,0 +1,319 @@ +const API_BASE = window.API_BASE || 'https://sms-api.ouai.nyc.mn' + +const app = document.getElementById('app') + +const state = { + view: 'login', + messages: [], + logs: [], + page: 1, + total: 0, + logsPage: 1, + logsTotal: 0, + currentMessage: null, + fromNumbers: [], + selectedFrom: '', + search: '', + stats: { total: 0, today: 0, failed: 0 }, + startTs: 0, + endTs: 0, +} + +function monthRange(offset = 0) { + const now = new Date() + const y = now.getFullYear() + const m = now.getMonth() + offset + const start = new Date(y, m, 1, 0, 0, 0) + const end = new Date(y, m + 1, 0, 23, 59, 59) + return [start.getTime(), end.getTime()] +} + +function initDefaultRange() { + const [s, e] = monthRange(0) + state.startTs = s + state.endTs = e +} + +async function api(path, options = {}) { + const res = await fetch(`${API_BASE}${path}`, { + credentials: 'include', + headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }, + ...options, + }) + return res.json() +} + +function setView(view) { + state.view = view + render() +} + +function nav(active) { + return ` +
+
📱 短信转发接收端
+ +
+ ` +} + +function loginView() { + return ` +
+ +
+ ` +} + +function statsCards() { + const s = state.stats + return ` +
+
总短信
${s.total}
+
今日
${s.today}
+
异常
${s.failed}
+
+ ` +} + +function fromFilter() { + const tags = state.fromNumbers.map(n => ` + ${n} + `).join('') + return ` +
+
发送方筛选
+
+ 全部 + ${tags} +
+
+ ` +} + +function timeFilter() { + return ` +
+ 时间筛选 + + + +
+ ` +} + +function toolbar() { + return ` +
+ + + +
+ ` +} + +function listView() { + const rows = state.messages.map(m => ` + + ${m.id} + ${m.from_number} + ${m.content} + ${new Date(m.timestamp).toLocaleString()} + 详情 + + `).join('') + + return ` + ${nav('list')} + ${statsCards()} + ${fromFilter()} + ${timeFilter()} + ${toolbar()} +
+ + + ${rows || ''} +
ID号码内容时间
+
+
+ + 第 ${state.page} 页 + +
+ ` +} + +function detailView() { + const m = state.currentMessage + if (!m) return '' + return ` + ${nav('list')} +
+

📱 短信详情

+
ID
${m.id}
+
发送方号码
${m.from_number}
+
短信内容
${m.content}
+
原始时间戳
${m.timestamp}
+
入库时间
${new Date(m.created_at || m.timestamp).toLocaleString()}
+
签名验证
${m.sign_verified ? '已验证' : '未验证'}
+
设备信息
${m.device_info || '-'}
+
SIM 卡信息
${m.sim_info || '-'}
+
IP 地址
${m.ip_address || '-'}
+ +
+ ` +} + +function logsView() { + const rows = state.logs.map(l => ` + + ${l.id} + ${l.from_number} + ${l.status} + ${l.error_message || ''} + ${new Date(l.timestamp).toLocaleString()} + + `).join('') + + return ` + ${nav('logs')} +
+ + + ${rows || ''} +
ID号码状态错误时间
+
+
+ + 第 ${state.logsPage} 页 + +
+ ` +} + +function statsView() { + return ` + ${nav('stats')} + ${statsCards()} +
+

统计概览

+
总短信
${state.stats.total}
+
今日
${state.stats.today}
+
异常
${state.stats.failed}
+
+ ` +} + +async function loadStats() { + const data = await api('/api/stats') + if (data.success) state.stats = data.data +} + +async function loadMessages() { + const data = await api(`/api/messages?page=${state.page}&limit=20&from=${encodeURIComponent(state.selectedFrom)}&q=${encodeURIComponent(state.search)}&start_ts=${state.startTs}&end_ts=${state.endTs}`) + if (!data.success) return + state.messages = data.data + state.total = data.total + state.fromNumbers = data.from_numbers || [] +} + +async function loadLogs() { + const data = await api(`/api/logs?page=${state.logsPage}&limit=20`) + if (!data.success) return + state.logs = data.data + state.logsTotal = data.total +} + +async function render() { + if (state.view === 'login') { + app.innerHTML = loginView() + document.getElementById('loginBtn').onclick = async () => { + const username = document.getElementById('username').value + const password = document.getElementById('password').value + const res = await api('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }) + if (res.success) { + initDefaultRange() + state.view = 'list' + await loadStats() + await loadMessages() + render() + } else alert(res.error || '登录失败') + } + return + } + + if (state.view === 'list') { + await loadStats() + await loadMessages() + app.innerHTML = listView() + bindCommon() + document.querySelectorAll('.tag').forEach(tag => { + tag.onclick = () => { state.selectedFrom = tag.dataset.from || ''; state.page = 1; render(); } + }) + document.getElementById('searchBtn').onclick = () => { state.search = document.getElementById('search').value; state.page = 1; render(); } + document.getElementById('refreshBtn').onclick = () => render() + document.getElementById('prevPage').onclick = () => { if (state.page>1) { state.page--; render(); } } + document.getElementById('nextPage').onclick = () => { state.page++; render(); } + document.getElementById('thisMonthBtn').onclick = () => { const [s,e] = monthRange(0); state.startTs=s; state.endTs=e; state.page=1; render(); } + document.getElementById('lastMonthBtn').onclick = () => { const [s,e] = monthRange(-1); state.startTs=s; state.endTs=e; state.page=1; render(); } + document.getElementById('allBtn').onclick = () => { state.startTs=0; state.endTs=0; state.page=1; render(); } + document.querySelectorAll('[data-action="detail"]').forEach(btn => { + btn.onclick = async () => { + const id = btn.dataset.id + const data = await api(`/api/messages/${id}`) + if (data.success) { state.currentMessage = data.data; state.view = 'detail'; render(); } + } + }) + return + } + + if (state.view === 'detail') { + app.innerHTML = detailView() + bindCommon() + document.getElementById('backBtn').onclick = () => { state.view = 'list'; render(); } + return + } + + if (state.view === 'logs') { + await loadLogs() + app.innerHTML = logsView() + bindCommon() + document.getElementById('prevLogs').onclick = () => { if (state.logsPage>1) { state.logsPage--; render(); } } + document.getElementById('nextLogs').onclick = () => { state.logsPage++; render(); } + return + } + + if (state.view === 'stats') { + await loadStats() + app.innerHTML = statsView() + bindCommon() + return + } +} + +function bindCommon() { + document.querySelectorAll('[data-view]').forEach(a => { + a.onclick = () => setView(a.dataset.view) + }) + const logout = document.getElementById('logoutBtn') + if (logout) logout.onclick = async () => { await api('/api/auth/logout', { method: 'POST' }); state.view='login'; render(); } +} + +initDefaultRange() +render() diff --git a/pages/public/index.html b/pages/public/index.html new file mode 100644 index 0000000..ed31130 --- /dev/null +++ b/pages/public/index.html @@ -0,0 +1,13 @@ + + + + + + SmsReceiver + + + +
+ + + diff --git a/worker/migrate_sqlite_to_d1.py b/worker/migrate_sqlite_to_d1.py new file mode 100644 index 0000000..763caa6 --- /dev/null +++ b/worker/migrate_sqlite_to_d1.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +import os +import json +import sqlite3 +import time +import requests + +D1_DATABASE_ID = os.getenv('D1_DATABASE_ID', '') +CF_API_TOKEN = os.getenv('CF_API_TOKEN', '') +CF_ACCOUNT_ID = os.getenv('CF_ACCOUNT_ID', '') +SQLITE_PATH = os.getenv('SQLITE_PATH', 'sms_receiver_go.db') +BATCH_SIZE = int(os.getenv('BATCH_SIZE', '50')) + +if not D1_DATABASE_ID or not CF_API_TOKEN or not CF_ACCOUNT_ID: + raise SystemExit('Missing env: D1_DATABASE_ID / CF_API_TOKEN / CF_ACCOUNT_ID') + +api = f"https://api.cloudflare.com/client/v4/accounts/{CF_ACCOUNT_ID}/d1/database/{D1_DATABASE_ID}/query" +headers = {"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"} + +conn = sqlite3.connect(SQLITE_PATH) +conn.row_factory = sqlite3.Row + +TABLES = ['sms_messages', 'receive_logs'] + + +def post_query(sql, params=None): + payload = {"sql": sql} + if params is not None: + payload["params"] = params + r = requests.post(api, headers=headers, data=json.dumps(payload)) + r.raise_for_status() + + +def migrate_table(table): + cur = conn.cursor() + cur.execute(f"SELECT * FROM {table}") + rows = cur.fetchall() + print(f"{table}: {len(rows)} rows") + + batch = 0 + for row in rows: + cols = row.keys() + placeholders = ','.join(['?'] * len(cols)) + sql = f"INSERT INTO {table} ({','.join(cols)}) VALUES ({placeholders})" + params = [row[c] for c in cols] + post_query(sql, params) + batch += 1 + if batch >= BATCH_SIZE: + time.sleep(0.2) + batch = 0 + + +for table in TABLES: + migrate_table(table) + +print('done') diff --git a/worker/package-lock.json b/worker/package-lock.json new file mode 100644 index 0000000..fc1b66c --- /dev/null +++ b/worker/package-lock.json @@ -0,0 +1,1531 @@ +{ + "name": "smsreceiver-worker-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "smsreceiver-worker-api", + "version": "0.1.0", + "dependencies": { + "hono": "^4.6.10" + }, + "devDependencies": { + "typescript": "^5.6.3", + "wrangler": "^4.7.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", + "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz", + "integrity": "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz", + "integrity": "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz", + "integrity": "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz", + "integrity": "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz", + "integrity": "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz", + "integrity": "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "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/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "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/@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/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/@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/@speed-highlight/core": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", + "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", + "dev": true, + "license": "CC0-1.0" + }, + "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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "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/miniflare": { + "version": "4.20260317.1", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.1.tgz", + "integrity": "sha512-A3csI1HXEIfqe3oscgpoRMHdYlkReQKPH/g5JE53vFSjoM6YIAOGAzyDNeYffwd9oQkPWDj9xER8+vpxei8klA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "^0.34.5", + "undici": "7.24.4", + "workerd": "1.20260317.1", + "ws": "8.18.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "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/workerd": { + "version": "1.20260317.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz", + "integrity": "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260317.1", + "@cloudflare/workerd-darwin-arm64": "1.20260317.1", + "@cloudflare/workerd-linux-64": "1.20260317.1", + "@cloudflare/workerd-linux-arm64": "1.20260317.1", + "@cloudflare/workerd-windows-64": "1.20260317.1" + } + }, + "node_modules/wrangler": { + "version": "4.76.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.76.0.tgz", + "integrity": "sha512-Wan+CU5a0tu4HIxGOrzjNbkmxCT27HUmzrMj6kc7aoAnjSLv50Ggcn2Ant7wNQrD6xW3g31phKupZJgTZ8wZfQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.2", + "@cloudflare/unenv-preset": "2.16.0", + "blake3-wasm": "2.1.5", + "esbuild": "0.27.3", + "miniflare": "4.20260317.1", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260317.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260317.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "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/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" + } + } + } +} diff --git a/worker/package.json b/worker/package.json new file mode 100644 index 0000000..c8642d3 --- /dev/null +++ b/worker/package.json @@ -0,0 +1,17 @@ +{ + "name": "smsreceiver-worker-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "dependencies": { + "hono": "^4.6.10" + }, + "devDependencies": { + "typescript": "^5.6.3", + "wrangler": "^4.7.0" + } +} diff --git a/worker/schema.sql b/worker/schema.sql new file mode 100644 index 0000000..fa65d5f --- /dev/null +++ b/worker/schema.sql @@ -0,0 +1,31 @@ +CREATE TABLE sms_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_number TEXT NOT NULL, + content TEXT NOT NULL, + timestamp INTEGER NOT NULL, + device_info TEXT, + sim_info TEXT, + sign_verified INTEGER, + ip_address TEXT, + created_at TEXT +); + +CREATE TABLE receive_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_number TEXT NOT NULL, + content TEXT NOT NULL, + timestamp INTEGER NOT NULL, + sign TEXT, + sign_valid INTEGER, + ip_address TEXT, + status TEXT NOT NULL, + error_message TEXT, + created_at TEXT +); + +CREATE INDEX idx_messages_from ON sms_messages(from_number); +CREATE INDEX idx_messages_timestamp ON sms_messages(timestamp); +CREATE INDEX idx_messages_created ON sms_messages(created_at); + +CREATE INDEX idx_logs_created ON receive_logs(created_at); +CREATE INDEX idx_logs_status ON receive_logs(status); diff --git a/worker/src/index.ts b/worker/src/index.ts new file mode 100644 index 0000000..6475c8c --- /dev/null +++ b/worker/src/index.ts @@ -0,0 +1,47 @@ +import { Hono } from 'hono' +import { authRoutes } from './routes/auth' +import { messagesRoutes } from './routes/messages' +import { logsRoutes } from './routes/logs' +import { receiveRoutes } from './routes/receive' +import { statsRoutes } from './routes/stats' + +export type Env = { + DB: D1Database + ADMIN_USER: string + ADMIN_PASS_HASH: string + SESSION_SECRET: string + API_TOKEN: string + HMAC_SECRET: string + SIGN_VERIFY: string + SIGN_MAX_AGE_MS: string + CORS_ORIGIN: string +} + +const app = new Hono<{ Bindings: Env }>() + +app.use('*', async (c, next) => { + const reqOrigin = c.req.header('Origin') + const allowOrigin = reqOrigin || c.env.CORS_ORIGIN || '*' + + c.header('Access-Control-Allow-Origin', allowOrigin) + c.header('Vary', 'Origin') + c.header('Access-Control-Allow-Credentials', 'true') + c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization') + c.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS') + + if (c.req.method === 'OPTIONS') { + return c.body(null, 204) + } + + await next() +}) + +app.route('/api/auth', authRoutes) +app.route('/api/messages', messagesRoutes) +app.route('/api/logs', logsRoutes) +app.route('/api/receive', receiveRoutes) +app.route('/api/stats', statsRoutes) + +app.get('/api/health', (c) => c.json({ ok: true })) + +export default app diff --git a/worker/src/middlewares/auth.ts b/worker/src/middlewares/auth.ts new file mode 100644 index 0000000..64dc9c7 --- /dev/null +++ b/worker/src/middlewares/auth.ts @@ -0,0 +1,10 @@ +import { createMiddleware } from 'hono/factory' +import type { Env } from '../index' +import { verifySessionCookie } from '../utils/session' + +export const requireAuth = createMiddleware<{ Bindings: Env }>(async (c, next) => { + const cookie = c.req.header('Cookie') || '' + const ok = verifySessionCookie(cookie, c.env.SESSION_SECRET) + if (!ok) return c.json({ success: false, error: '未授权' }, 401) + await next() +}) diff --git a/worker/src/routes/auth.ts b/worker/src/routes/auth.ts new file mode 100644 index 0000000..15c286f --- /dev/null +++ b/worker/src/routes/auth.ts @@ -0,0 +1,33 @@ +import { Hono } from 'hono' +import type { Env } from '../index' +import { createSessionCookie, clearSessionCookie, hashPassword } from '../utils/session' + +export const authRoutes = new Hono<{ Bindings: Env }>() + +authRoutes.post('/login', async (c) => { + const body = await c.req.json().catch(() => ({})) as any + const username = body.username || '' + const password = body.password || '' + + if (!username || !password) { + return c.json({ success: false, error: '缺少用户名或密码' }, 400) + } + + if (username !== c.env.ADMIN_USER) { + return c.json({ success: false, error: '用户名或密码错误' }, 401) + } + + const passHash = hashPassword(password, c.env.SESSION_SECRET) + if (passHash !== c.env.ADMIN_PASS_HASH) { + return c.json({ success: false, error: '用户名或密码错误' }, 401) + } + + const cookie = createSessionCookie(username, c.env.SESSION_SECRET) + c.header('Set-Cookie', cookie) + return c.json({ success: true }) +}) + +authRoutes.post('/logout', async (c) => { + c.header('Set-Cookie', clearSessionCookie()) + return c.json({ success: true }) +}) diff --git a/worker/src/routes/logs.ts b/worker/src/routes/logs.ts new file mode 100644 index 0000000..4d9f7e9 --- /dev/null +++ b/worker/src/routes/logs.ts @@ -0,0 +1,29 @@ +import { Hono } from 'hono' +import type { Env } from '../index' +import { requireAuth } from '../middlewares/auth' + +export const logsRoutes = new Hono<{ Bindings: Env }>() + +logsRoutes.get('/', requireAuth, async (c) => { + const page = Math.max(1, Number(c.req.query('page') || 1)) + const limit = Math.min(100, Math.max(1, Number(c.req.query('limit') || 20))) + const status = (c.req.query('status') || '').trim() + const offset = (page - 1) * limit + + let where = '' + const params: any[] = [] + if (status) { + where = 'WHERE status = ?' + params.push(status) + } + + const totalStmt = c.env.DB.prepare(`SELECT COUNT(*) as cnt FROM receive_logs ${where}`) + const total = (await totalStmt.bind(...params).first() as any)?.cnt || 0 + + const stmt = c.env.DB.prepare( + `SELECT * FROM receive_logs ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?` + ) + const rows = await stmt.bind(...params, limit, offset).all() + + return c.json({ success: true, data: rows.results || [], total, page, limit }) +}) diff --git a/worker/src/routes/messages.ts b/worker/src/routes/messages.ts new file mode 100644 index 0000000..622959e --- /dev/null +++ b/worker/src/routes/messages.ts @@ -0,0 +1,70 @@ +import { Hono } from 'hono' +import type { Env } from '../index' +import { requireAuth } from '../middlewares/auth' + +export const messagesRoutes = new Hono<{ Bindings: Env }>() + +messagesRoutes.get('/', requireAuth, async (c) => { + const page = Math.max(1, Number(c.req.query('page') || 1)) + const limit = Math.min(100, Math.max(1, Number(c.req.query('limit') || 20))) + const q = (c.req.query('q') || '').trim() + const from = (c.req.query('from') || '').trim() + const startTs = Number(c.req.query('start_ts') || 0) + const endTs = Number(c.req.query('end_ts') || 0) + const offset = (page - 1) * limit + + const whereParts: string[] = [] + const params: any[] = [] + + if (from) { + whereParts.push('from_number = ?') + params.push(from) + } + + if (q) { + whereParts.push('(from_number LIKE ? OR content LIKE ?)') + params.push(`%${q}%`, `%${q}%`) + } + + if (startTs > 0) { + whereParts.push('timestamp >= ?') + params.push(startTs) + } + + if (endTs > 0) { + whereParts.push('timestamp <= ?') + params.push(endTs) + } + + const where = whereParts.length ? `WHERE ${whereParts.join(' AND ')}` : '' + + const totalStmt = c.env.DB.prepare(`SELECT COUNT(*) as cnt FROM sms_messages ${where}`) + const total = (await totalStmt.bind(...params).first() as any)?.cnt || 0 + + const stmt = c.env.DB.prepare( + `SELECT * FROM sms_messages ${where} ORDER BY timestamp DESC LIMIT ? OFFSET ?` + ) + const rows = await stmt.bind(...params, limit, offset).all() + + const fromRows = await c.env.DB.prepare( + `SELECT DISTINCT from_number FROM sms_messages ORDER BY from_number` + ).all() + + return c.json({ + success: true, + data: rows.results || [], + total, + page, + limit, + from_numbers: (fromRows.results || []).map((r: any) => r.from_number), + }) +}) + +messagesRoutes.get('/:id', requireAuth, async (c) => { + const id = c.req.param('id') + const stmt = c.env.DB.prepare(`SELECT * FROM sms_messages WHERE id = ?`) + const row = await stmt.bind(id).first() + if (!row) return c.json({ success: false, error: '消息不存在' }, 404) + return c.json({ success: true, data: row }) +}) + diff --git a/worker/src/routes/receive.ts b/worker/src/routes/receive.ts new file mode 100644 index 0000000..b7bcd6b --- /dev/null +++ b/worker/src/routes/receive.ts @@ -0,0 +1,99 @@ +import { Hono } from 'hono' +import type { Env } from '../index' +import { getClientIP, nowMs } from '../utils/common' +import { verifySign } from '../utils/sign' +import { insertMessageAndLog } from '../utils/db' + +export const receiveRoutes = new Hono<{ Bindings: Env }>() + +receiveRoutes.post('/', async (c) => { + const contentType = c.req.header('content-type') || '' + + let form: Record = {} + if (contentType.includes('application/json')) { + const body = (await c.req.json().catch(() => ({}))) as any + form = { ...body } + } else { + const body = await c.req.parseBody() + for (const [k, v] of Object.entries(body)) { + form[k] = Array.isArray(v) ? String(v[0]) : String(v ?? '') + } + } + + const from = form.from || '' + const msg = form.content || '' + if (!from || !msg) { + return c.json({ success: false, error: `缺少必填参数 (from: '${from}', content: '${msg}')` }, 400) + } + + const timestamp = form.timestamp ? Number(form.timestamp) : nowMs() + const sign = form.sign || '' + const device = form.device || '' + const sim = form.sim || '' + const token = c.req.query('token') || form.token || '' + const ip = getClientIP(c.req.raw.headers) + + let signValid: number | null = 1 + let status = 'success' + let errorMessage: string | null = null + + if (c.env.SIGN_VERIFY === 'true' && token) { + if (c.env.API_TOKEN && token !== c.env.API_TOKEN) { + signValid = 0 + status = 'error' + errorMessage = '无效的 token' + } else if (!c.env.HMAC_SECRET) { + // 对齐旧版:token 存在但 secret 为空时,跳过签名验证 + signValid = 1 + } else { + const maxAge = Number(c.env.SIGN_MAX_AGE_MS || 300000) + const now = nowMs() + const diff = now - timestamp + if (diff > maxAge) { + signValid = 0 + status = 'error' + errorMessage = `时间戳过期(差异: ${(diff / 1000).toFixed(1)} 秒)` + } else { + const valid = verifySign(timestamp, sign, c.env.HMAC_SECRET) + signValid = valid ? 1 : 0 + if (!valid) { + status = 'error' + errorMessage = '签名不匹配' + } + } + } + } + + const nowIso = new Date().toISOString() + const messageRow = { + from_number: from, + content: msg, + timestamp, + device_info: device || null, + sim_info: sim || null, + sign_verified: signValid, + ip_address: ip, + created_at: nowIso, + } + + const logRow = { + from_number: from, + content: msg, + timestamp, + sign: sign || null, + sign_valid: signValid, + ip_address: ip, + status, + error_message: errorMessage, + created_at: nowIso, + } + + try { + await insertMessageAndLog(c.env, messageRow, logRow) + } catch { + return c.json({ success: false, error: '保存消息失败' }, 500) + } + + return c.json({ success: true, message: '短信已接收' }) +}) + diff --git a/worker/src/routes/stats.ts b/worker/src/routes/stats.ts new file mode 100644 index 0000000..db434bb --- /dev/null +++ b/worker/src/routes/stats.ts @@ -0,0 +1,24 @@ +import { Hono } from 'hono' +import type { Env } from '../index' +import { requireAuth } from '../middlewares/auth' + +export const statsRoutes = new Hono<{ Bindings: Env }>() + +statsRoutes.get('/', requireAuth, async (c) => { + const totalRow = await c.env.DB.prepare(`SELECT COUNT(*) as cnt FROM sms_messages`).first<{ cnt: number }>() + const todayRow = await c.env.DB.prepare(` + SELECT COUNT(*) as cnt + FROM sms_messages + WHERE datetime(timestamp/1000, 'unixepoch') >= date('now') + `).first<{ cnt: number }>() + const failRow = await c.env.DB.prepare(`SELECT COUNT(*) as cnt FROM receive_logs WHERE status != 'success'`).first<{ cnt: number }>() + + return c.json({ + success: true, + data: { + total: totalRow?.cnt || 0, + today: todayRow?.cnt || 0, + failed: failRow?.cnt || 0, + }, + }) +}) diff --git a/worker/src/utils/common.ts b/worker/src/utils/common.ts new file mode 100644 index 0000000..5e8f516 --- /dev/null +++ b/worker/src/utils/common.ts @@ -0,0 +1,9 @@ +export function getClientIP(headers: Headers) { + const forwarded = headers.get('X-Forwarded-For') + if (forwarded) return forwarded.split(',')[0].trim() + return headers.get('CF-Connecting-IP') || '' +} + +export function nowMs() { + return Date.now() +} diff --git a/worker/src/utils/db.ts b/worker/src/utils/db.ts new file mode 100644 index 0000000..dafb8b1 --- /dev/null +++ b/worker/src/utils/db.ts @@ -0,0 +1,40 @@ +import type { Env } from '../index' + +export async function insertMessageAndLog(env: Env, msg: any, log: any) { + const stmtMsg = env.DB.prepare( + `INSERT INTO sms_messages (from_number, content, timestamp, device_info, sim_info, sign_verified, ip_address, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + const stmtLog = env.DB.prepare( + `INSERT INTO receive_logs (from_number, content, timestamp, sign, sign_valid, ip_address, status, error_message, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) + + const now = new Date().toISOString() + const batch = env.DB.batch([ + stmtMsg.bind( + msg.from_number, + msg.content, + msg.timestamp, + msg.device_info, + msg.sim_info, + msg.sign_verified, + msg.ip_address, + msg.created_at || now + ), + stmtLog.bind( + log.from_number, + log.content, + log.timestamp, + log.sign, + log.sign_valid, + log.ip_address, + log.status, + log.error_message, + log.created_at || now + ), + ]) + + const results = await batch + return results +} diff --git a/worker/src/utils/session.ts b/worker/src/utils/session.ts new file mode 100644 index 0000000..66db479 --- /dev/null +++ b/worker/src/utils/session.ts @@ -0,0 +1,39 @@ +import { randomBytes, createHmac, timingSafeEqual } from 'node:crypto' + +const COOKIE_NAME = 'sms_session' + +export function hashPassword(password: string, secret: string) { + const h = createHmac('sha256', secret) + h.update(password) + return h.digest('hex') +} + +export function createSessionCookie(username: string, secret: string) { + const nonce = randomBytes(16).toString('hex') + const payload = `${username}.${Date.now()}.${nonce}` + const sig = createHmac('sha256', secret).update(payload).digest('hex') + return `${COOKIE_NAME}=${payload}.${sig}; HttpOnly; Path=/; SameSite=None; Secure` +} + +export function clearSessionCookie() { + return `${COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0; SameSite=None; Secure` +} + +export function verifySessionCookie(cookieHeader: string, secret: string) { + const cookie = parseCookie(cookieHeader, COOKIE_NAME) + if (!cookie) return false + const parts = cookie.split('.') + if (parts.length < 4) return false + const sig = parts.pop() as string + const payload = parts.join('.') + const expected = createHmac('sha256', secret).update(payload).digest('hex') + return timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) +} + +function parseCookie(header: string, key: string) { + const items = header.split(';').map((v) => v.trim()) + for (const item of items) { + if (item.startsWith(key + '=')) return item.slice(key.length + 1) + } + return '' +} diff --git a/worker/src/utils/sign.ts b/worker/src/utils/sign.ts new file mode 100644 index 0000000..f7ad7c2 --- /dev/null +++ b/worker/src/utils/sign.ts @@ -0,0 +1,14 @@ +import { createHmac } from 'node:crypto' + +export function generateSign(timestamp: number, secret: string) { + const stringToSign = `${timestamp}\n${secret}` + const hmac = createHmac('sha256', secret) + hmac.update(stringToSign) + const base64 = hmac.digest('base64') + return encodeURIComponent(base64) +} + +export function verifySign(timestamp: number, sign: string, secret: string) { + const expected = generateSign(timestamp, secret) + return expected === sign +} diff --git a/worker/tsconfig.json b/worker/tsconfig.json new file mode 100644 index 0000000..842140e --- /dev/null +++ b/worker/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "types": ["@cloudflare/workers-types"] + } +} diff --git a/worker/wrangler.toml b/worker/wrangler.toml new file mode 100644 index 0000000..2e569e0 --- /dev/null +++ b/worker/wrangler.toml @@ -0,0 +1,19 @@ +name = "smsreceiver-worker-api" +main = "src/index.ts" +compatibility_date = "2026-03-22" +compatibility_flags = ["nodejs_compat"] + +[vars] +ADMIN_USER = "admin" +ADMIN_PASS_HASH = "b8e450efa9c66b2b50ec8a4d7eb51213d7fa3395e107575881ad999ee92744b5" +SESSION_SECRET = "1e81b5f9e5a695eba01e996b14871db8899b08e111cf8252df8aa4c91d1c7144" +API_TOKEN = "default_token" +HMAC_SECRET = "" +SIGN_VERIFY = "true" +SIGN_MAX_AGE_MS = "3600000" +CORS_ORIGIN = "*" + +[[d1_databases]] +binding = "DB" +database_name = "smsreceiver" +database_id = "50bd73ba-218c-4eb6-948a-9014a8892575"