修复固件哈希兼容性并完善部署配置
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
WEB_TOOLS_PORT=8080
|
||||
WEB_TOOLS_PORT=9000
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
Before changing this project, read `README.zh-CN.md` in full. Its architecture constraints, extension procedure, validation checklist, and release steps are authoritative.
|
||||
|
||||
## Project identity
|
||||
|
||||
- This is a self-hosted Vue 3 + TypeScript browser-local tools application with a zero-dependency Node.js static/configuration server.
|
||||
- `package.json` is the authoritative application version. The default production port is `9000`.
|
||||
- `shared/tool-catalog.json` is the authoritative tool catalog; `shared/default-navigation.json` defines first-run ordering.
|
||||
- `README.zh-CN.md` is the complete product, architecture, deployment, extension, and AI handoff document. `README.md` is only the universal entry point.
|
||||
- Windows Server uses `web-tools.ps1`; containers use `Dockerfile` and `docker-compose.yml`.
|
||||
|
||||
## First actions in a new session
|
||||
|
||||
1. Read `README.zh-CN.md`, then inspect `git status --short --branch` and preserve all existing worktree changes.
|
||||
2. Read the closest existing tool, its `src/lib/` logic, and related tests before adding or changing behavior.
|
||||
3. Keep default production port references synchronized in `server/index.mjs`, `web-tools.ps1`, `Dockerfile`, `docker-compose.yml`, `.env.example`, and deployment documentation.
|
||||
4. Run `npm run check`; for deployment changes also validate PowerShell syntax and Docker Compose configuration when those tools are available.
|
||||
|
||||
Key rules:
|
||||
|
||||
- Preserve the anonymous, no-login, no-database design. Tool calculations are isolated in each browser tab; only navigation order is shared globally.
|
||||
|
||||
+9
-1
@@ -14,10 +14,18 @@
|
||||
### Fixed
|
||||
|
||||
- Intel HEX 解析器增加全部非空记录上限、显式 uint32 地址空间检查和无符号扩展地址运算,拒绝越过 `0xFFFFFFFF` 的数据。
|
||||
- 固件 Hash 在普通 HTTP 局域网访问时增加本地 SHA-256/SHA-512 回退,不再依赖 `SubtleCrypto` 必须可用。
|
||||
- 生产服务在监听端口前检查 `dist/index.html`,缺少构建产物时立即给出构建命令,不再启动后按请求重复返回 500。
|
||||
|
||||
### Documentation
|
||||
|
||||
- 补充 `dist/index.html` 缺失、Node 进程未重启、浏览器页面未刷新和前台 PowerShell 退出等启动故障排查说明。
|
||||
- 将 Node、Docker Compose、容器健康检查、Windows 脚本和示例配置的生产默认端口统一为 `9000`。
|
||||
- 增加通用 `README.md` 入口并强化 `AGENTS.md` 新会话接手流程,便于其他 AI 在不依赖历史对话的情况下继续维护。
|
||||
|
||||
### Verification
|
||||
|
||||
- 68 条 Vitest 测试通过,新增标准 SHA 向量、Block 头大小端、地址绑定、最高 uint32 地址、地址溢出和控制记录边界覆盖。
|
||||
- 74 条 Vitest 测试通过,新增标准 SHA 向量、Block 头大小端、地址绑定、最高 uint32 地址、地址溢出、控制记录边界、无 Web Crypto 回退、生产构建预检和部署配置一致性覆盖。
|
||||
- Vue/TypeScript 类型检查和生产构建通过。
|
||||
|
||||
## 0.0.5 - 2026-08-07
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ FROM node:22-alpine AS runtime
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
HOST=0.0.0.0 \
|
||||
PORT=8080 \
|
||||
PORT=9000 \
|
||||
DATA_DIR=/data
|
||||
|
||||
WORKDIR /app
|
||||
@@ -24,9 +24,9 @@ COPY --chown=node:node server ./server
|
||||
COPY --chown=node:node shared ./shared
|
||||
|
||||
USER node
|
||||
EXPOSE 8080
|
||||
EXPOSE 9000
|
||||
VOLUME ["/data"]
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "fetch('http://127.0.0.1:8080/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
|
||||
CMD node -e "fetch('http://127.0.0.1:9000/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
|
||||
|
||||
CMD ["node", "server/index.mjs"]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Web Tools
|
||||
|
||||
Self-hosted browser-local utilities built with Vue 3, TypeScript, and a small Node.js server. Tool inputs and results stay in each browser tab; the server only persists the shared navigation order.
|
||||
|
||||
Default production address: `http://localhost:9000`
|
||||
|
||||
## Documentation
|
||||
|
||||
- Complete Chinese documentation: [README.zh-CN.md](README.zh-CN.md)
|
||||
- AI/coding-agent instructions: [AGENTS.md](AGENTS.md)
|
||||
- Release history: [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
Any AI or coding agent starting in a new conversation must read `AGENTS.md` and `README.zh-CN.md` before editing the project, then inspect the current Git worktree instead of assuming it is clean.
|
||||
|
||||
## Quick start
|
||||
|
||||
Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
Windows Server after the first build:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 start
|
||||
```
|
||||
|
||||
See `README.zh-CN.md` for first installation, updates, Windows startup, security boundaries, tool extension rules, tests, and packaging.
|
||||
+43
-9
@@ -86,12 +86,18 @@ CHANGELOG.md 版本变更、修复和验证记录
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
默认访问地址:`http://服务器地址:8080`
|
||||
默认访问地址:`http://服务器地址:9000`
|
||||
|
||||
自定义端口:
|
||||
自定义宿主机端口时,在项目根目录创建 `.env`(可从 `.env.example` 复制),例如改为 `9100`:
|
||||
|
||||
```dotenv
|
||||
WEB_TOOLS_PORT=9100
|
||||
```
|
||||
|
||||
然后正常启动:
|
||||
|
||||
```bash
|
||||
WEB_TOOLS_PORT=9000 docker compose up -d --build
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
检查状态:
|
||||
@@ -109,7 +115,7 @@ docker compose down
|
||||
|
||||
导航配置保存在 Docker 卷 `web-tools-data` 中的 `/data/navigation.json`。全新部署且从未保存过排序时,应用直接使用内置默认配置,该文件可能尚不存在;首次保存后会创建。删除或重建容器不会丢失已有配置,执行 `docker compose down -v` 才会删除该卷。
|
||||
|
||||
健康检查地址:`http://服务器地址:8080/healthz`
|
||||
健康检查地址:`http://服务器地址:9000/healthz`
|
||||
|
||||
## Windows Server 部署(不使用 Docker)
|
||||
|
||||
@@ -134,10 +140,19 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 start
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 start `
|
||||
-ListenAddress 127.0.0.1 `
|
||||
-Port 8080 `
|
||||
-Port 9100 `
|
||||
-DataDir C:\ProgramData\WebTools\data
|
||||
```
|
||||
|
||||
脚本不会自动修改 Windows 防火墙。需要从局域网访问默认端口时,可由管理员仅对专用网络配置入站规则:
|
||||
|
||||
```powershell
|
||||
New-NetFirewallRule -DisplayName 'Web Tools 9000' -Direction Inbound -Action Allow `
|
||||
-Protocol TCP -LocalPort 9000 -Profile Private
|
||||
```
|
||||
|
||||
只允许本机或由反向代理访问时,使用 `-ListenAddress 127.0.0.1`,无需开放该端口。
|
||||
|
||||
从 Git `main` 分支更新时,先在运行窗口按 `Ctrl+C` 停止服务,再执行:
|
||||
|
||||
```powershell
|
||||
@@ -148,12 +163,19 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\web-tools.ps1 update
|
||||
|
||||
脚本不会读取、覆盖或删除 `C:\ProgramData\WebTools\data`。PowerShell 窗口关闭后前台服务会停止;如使用 Windows 计划任务,应让任务执行 `web-tools.ps1 start`,更新时停止任务、运行 `web-tools.ps1 update -NoStart`,然后重新启动任务。
|
||||
|
||||
## 启动故障排查
|
||||
|
||||
- `ENOENT ... dist\\index.html`:当前目录只有源码或尚未生成生产前端。先执行 `npm.cmd ci --include=dev` 和 `npm.cmd run build`,再启动服务;直接执行 `node server\\index.mjs` 不会替代构建步骤。
|
||||
- 页面能打开但左侧没有新工具:仅重新构建 `dist` 不会更新已经运行的 Node 进程。停止旧进程后重新启动,并在浏览器执行一次强制刷新(`Ctrl+F5`)。
|
||||
- 新工具仍未出现:确认服务输出的 `Navigation config` 路径与实际数据目录一致。旧的 `navigation.json` 会保留用户排序,只在服务重启后首次读取 API 时补入新工具。
|
||||
- 关闭 PowerShell 窗口后服务消失:前台启动是预期行为。需要长期运行时使用 Windows 计划任务或服务管理器,让任务执行 `web-tools.ps1 start`,不要依赖打开的交互窗口。
|
||||
|
||||
## 工具管理
|
||||
|
||||
管理入口不显示在页面中,直接访问:
|
||||
|
||||
```text
|
||||
http://服务器地址:8080/setting
|
||||
http://服务器地址:9000/setting
|
||||
```
|
||||
|
||||
可以调整分类顺序、分类内工具顺序及工具所属分类。保存时采用同目录临时文件和原子替换。
|
||||
@@ -192,7 +214,15 @@ docker compose up -d --no-build
|
||||
npm ci
|
||||
```
|
||||
|
||||
终端一运行配置服务:
|
||||
终端一运行开发配置服务。这里的 `8081` 仅用于 Vite 开发代理,不是生产默认端口。PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:PORT = '8081'
|
||||
$env:DATA_DIR = (Join-Path (Get-Location) 'data')
|
||||
node .\server\index.mjs
|
||||
```
|
||||
|
||||
Bash:
|
||||
|
||||
```bash
|
||||
PORT=8081 DATA_DIR=./data node server/index.mjs
|
||||
@@ -211,7 +241,9 @@ npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
生产服务默认监听 `0.0.0.0:8080`。
|
||||
生产服务默认监听 `0.0.0.0:9000`。
|
||||
|
||||
新增工具或修改 `shared/tool-catalog.json`、`shared/default-navigation.json` 后,不能只执行 `npm run build`。Node 服务会在启动时加载工具目录;必须先停止旧进程,再重新执行 `npm start`(Windows Server 使用 `web-tools.ps1 start`,Docker 使用 `docker compose up -d --build`)。服务重启后,已打开的浏览器页面还需要刷新,才会重新读取导航 API;已有 `data/navigation.json` 不会被覆盖,只会自动补入缺少的新工具。
|
||||
|
||||
完整工程检查:
|
||||
|
||||
@@ -230,6 +262,8 @@ npm audit --audit-level=low
|
||||
|
||||
固件 Hash 与 CRC 共用经过校验的 Intel HEX 解析器,但使用独立纯逻辑、Worker 和 Vue 页面。解析器处理扩展段/线性地址,将数据按 32 位绝对地址排序,合并连续记录,去重内容一致的重叠记录,并拒绝冲突或越过 `0xFFFFFFFF` 的数据。记录文本、记录校验和、EOF 和启动地址不参与 Hash。
|
||||
|
||||
摘要计算优先使用浏览器原生 Web Crypto;通过普通 HTTP 局域网地址访问而无法使用 `SubtleCrypto` 时,自动回退到项目内已有的 `node-forge` SHA-256/SHA-512 实现。两条路径都在 Web Worker 中本地处理,不会把固件数据发送到服务端。
|
||||
|
||||
“有效数据”模式的总输入为所有 block 数据按地址升序直接拼接,不填充地址空洞。“Block 校验头”模式在每个 block 前加入 8 个二进制字节:
|
||||
|
||||
```text
|
||||
@@ -311,7 +345,7 @@ npm audit --audit-level=low
|
||||
|
||||
- 新工具的默认路径 `/tools/<id>` 可以直接打开和刷新。
|
||||
- 左侧导航、搜索和 `/setting` 正常显示新工具。
|
||||
- 已保存的旧 `navigation.json` 启动后会自动补入新工具。
|
||||
- 已保存的旧 `navigation.json` 在服务重启并首次读取后会自动补入新工具;仅重新构建 `dist` 不会刷新已经运行的 Node 进程。
|
||||
- 桌面端和约 `390 x 844` 的移动端竖屏布局正常。
|
||||
- 浏览器控制台无错误,工具内容没有发往服务端。
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ services:
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
ports:
|
||||
- "${WEB_TOOLS_PORT:-8080}:8080"
|
||||
- "${WEB_TOOLS_PORT:-9000}:9000"
|
||||
volumes:
|
||||
- web-tools-data:/data
|
||||
read_only: true
|
||||
|
||||
+18
-2
@@ -16,6 +16,7 @@ const serverFile = fileURLToPath(import.meta.url)
|
||||
const serverDir = path.dirname(serverFile)
|
||||
const projectRoot = path.resolve(serverDir, '..')
|
||||
export const MAX_JSON_BODY_SIZE = 64 * 1024
|
||||
export const DEFAULT_PORT = 9000
|
||||
|
||||
const mimeTypes = new Map([
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
@@ -76,6 +77,19 @@ function isMissingFile(error) {
|
||||
return error?.code === 'ENOENT' || error?.code === 'ENOTDIR'
|
||||
}
|
||||
|
||||
export async function assertProductionBuild(staticRoot = path.join(projectRoot, 'dist')) {
|
||||
const indexPath = path.join(path.resolve(staticRoot), 'index.html')
|
||||
let details
|
||||
try {
|
||||
details = await stat(indexPath)
|
||||
} catch (error) {
|
||||
if (!isMissingFile(error)) throw error
|
||||
}
|
||||
if (!details?.isFile()) {
|
||||
throw new Error(`Production build not found: ${indexPath}. Run "npm.cmd ci --include=dev" and "npm.cmd run build" before starting the server.`)
|
||||
}
|
||||
}
|
||||
|
||||
function acceptsHtmlNavigation(request, pathname) {
|
||||
if (path.extname(pathname)) return false
|
||||
return String(request.headers.accept || '').split(',').some((value) => value.trim().toLowerCase().startsWith('text/html'))
|
||||
@@ -186,8 +200,10 @@ export async function createWebToolsServer(options = {}) {
|
||||
|
||||
export async function startWebToolsServer() {
|
||||
const host = process.env.HOST || '0.0.0.0'
|
||||
const port = Number(process.env.PORT || 8080)
|
||||
const server = await createWebToolsServer()
|
||||
const port = Number(process.env.PORT || DEFAULT_PORT)
|
||||
const staticRoot = path.join(projectRoot, 'dist')
|
||||
await assertProductionBuild(staticRoot)
|
||||
const server = await createWebToolsServer({ staticRoot })
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Web Tools listening on http://${host}:${port}`)
|
||||
console.log(`Navigation config: ${path.resolve(process.env.DATA_DIR || path.join(projectRoot, 'data'), 'navigation.json')}`)
|
||||
|
||||
+28
-6
@@ -24,7 +24,9 @@ export interface FirmwareHashReport {
|
||||
headerBytes: number
|
||||
}
|
||||
|
||||
export type DigestBytes = (algorithm: 'SHA-256' | 'SHA-512', data: Uint8Array) => Promise<string>
|
||||
export type DigestAlgorithm = 'SHA-256' | 'SHA-512'
|
||||
export type DigestBytes = (algorithm: DigestAlgorithm, data: Uint8Array) => Promise<string>
|
||||
type SubtleDigestProvider = Pick<SubtleCrypto, 'digest'>
|
||||
|
||||
function validateUint32(value: number, label: string): void {
|
||||
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_UINT32_ADDRESS) {
|
||||
@@ -123,13 +125,33 @@ export function buildFirmwareHashInput(
|
||||
return input
|
||||
}
|
||||
|
||||
async function webCryptoDigest(algorithm: 'SHA-256' | 'SHA-512', data: Uint8Array): Promise<string> {
|
||||
if (!globalThis.crypto?.subtle) throw new Error('当前浏览器不支持 Web Crypto Hash')
|
||||
async function forgeDigest(algorithm: DigestAlgorithm, data: Uint8Array): Promise<string> {
|
||||
const { default: forge } = await import('node-forge')
|
||||
const digest = algorithm === 'SHA-256' ? forge.md.sha256.create() : forge.md.sha512.create()
|
||||
const chunkSize = 0x8000
|
||||
for (let offset = 0; offset < data.length; offset += chunkSize) {
|
||||
digest.update(String.fromCharCode(...data.subarray(offset, Math.min(offset + chunkSize, data.length))))
|
||||
}
|
||||
return digest.digest().toHex().toUpperCase()
|
||||
}
|
||||
|
||||
export async function digestFirmwareBytes(
|
||||
algorithm: DigestAlgorithm,
|
||||
data: Uint8Array,
|
||||
subtle: SubtleDigestProvider | undefined = globalThis.crypto?.subtle,
|
||||
): Promise<string> {
|
||||
const input = data.buffer instanceof ArrayBuffer && data.byteOffset === 0 && data.byteLength === data.buffer.byteLength
|
||||
? data.buffer
|
||||
: data.slice().buffer
|
||||
const digest = await globalThis.crypto.subtle.digest(algorithm, input)
|
||||
return formatBytes(new Uint8Array(digest), { format: 'continuous-hex' })
|
||||
if (subtle) {
|
||||
try {
|
||||
const digest = await subtle.digest(algorithm, input)
|
||||
return formatBytes(new Uint8Array(digest), { format: 'continuous-hex' })
|
||||
} catch {
|
||||
// Some browsers expose crypto but disable SubtleCrypto on plain HTTP LAN origins.
|
||||
}
|
||||
}
|
||||
return forgeDigest(algorithm, data)
|
||||
}
|
||||
|
||||
async function calculateDigestPair(data: Uint8Array, digestBytes: DigestBytes): Promise<FirmwareHashDigest> {
|
||||
@@ -142,7 +164,7 @@ export async function calculateFirmwareHashReport(
|
||||
blocks: DataBlock[],
|
||||
mode: FirmwareHashMode,
|
||||
byteOrder: FirmwareHashByteOrder = 'big-endian',
|
||||
digestBytes: DigestBytes = webCryptoDigest,
|
||||
digestBytes: DigestBytes = digestFirmwareBytes,
|
||||
): Promise<FirmwareHashReport> {
|
||||
const sorted = validateAndSortBlocks(blocks)
|
||||
const combinedInput = buildFirmwareHashInput(sorted, mode, byteOrder)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DEFAULT_PORT } from '../server/index.mjs'
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
async function readProjectFile(relativePath) {
|
||||
return readFile(path.join(projectRoot, relativePath), 'utf8')
|
||||
}
|
||||
|
||||
describe('deployment configuration', () => {
|
||||
it('keeps the default production port synchronized', async () => {
|
||||
const [dockerfile, compose, environment, script, readme] = await Promise.all([
|
||||
readProjectFile('Dockerfile'),
|
||||
readProjectFile('docker-compose.yml'),
|
||||
readProjectFile('.env.example'),
|
||||
readProjectFile('web-tools.ps1'),
|
||||
readProjectFile('README.zh-CN.md'),
|
||||
])
|
||||
|
||||
expect(DEFAULT_PORT).toBe(9000)
|
||||
expect(dockerfile).toContain('PORT=9000')
|
||||
expect(dockerfile).toContain('EXPOSE 9000')
|
||||
expect(dockerfile).toContain('http://127.0.0.1:9000/healthz')
|
||||
expect(compose).toContain('${WEB_TOOLS_PORT:-9000}:9000')
|
||||
expect(environment.trim()).toBe('WEB_TOOLS_PORT=9000')
|
||||
expect(script).toMatch(/\[int\]\$Port = 9000/)
|
||||
expect(readme).toContain('默认访问地址:`http://服务器地址:9000`')
|
||||
expect(readme).toContain('生产服务默认监听 `0.0.0.0:9000`')
|
||||
})
|
||||
|
||||
it('preserves the Windows and container production safeguards', async () => {
|
||||
const [dockerfile, compose, dockerignore, script] = await Promise.all([
|
||||
readProjectFile('Dockerfile'),
|
||||
readProjectFile('docker-compose.yml'),
|
||||
readProjectFile('.dockerignore'),
|
||||
readProjectFile('web-tools.ps1'),
|
||||
])
|
||||
|
||||
expect(dockerfile).toContain('USER node')
|
||||
expect(compose).toContain('read_only: true')
|
||||
expect(compose).toContain('no-new-privileges:true')
|
||||
expect(compose).toContain('cap_drop:')
|
||||
expect(dockerignore).toContain('node_modules')
|
||||
expect(dockerignore).toContain('data')
|
||||
expect(script).toContain('#Requires -Version 5.1')
|
||||
expect(script).toContain("@('pull', '--ff-only', $Remote, $Branch)")
|
||||
expect(script).toContain("@('ci', '--include=dev')")
|
||||
})
|
||||
|
||||
it('provides standard entry points for a new AI session', async () => {
|
||||
const [entryReadme, agentGuide, completeReadme] = await Promise.all([
|
||||
readProjectFile('README.md'),
|
||||
readProjectFile('AGENTS.md'),
|
||||
readProjectFile('README.zh-CN.md'),
|
||||
])
|
||||
|
||||
expect(entryReadme).toContain('[README.zh-CN.md](README.zh-CN.md)')
|
||||
expect(entryReadme).toContain('[AGENTS.md](AGENTS.md)')
|
||||
expect(agentGuide).toContain('## First actions in a new session')
|
||||
expect(agentGuide).toContain('git status --short --branch')
|
||||
expect(completeReadme).toContain('## AI 接手清单')
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildFirmwareHashInput,
|
||||
calculateFirmwareHashReport,
|
||||
digestFirmwareBytes,
|
||||
encodeFirmwareBlockHeader,
|
||||
type FirmwareHashByteOrder,
|
||||
type FirmwareHashMode,
|
||||
@@ -131,3 +132,11 @@ describe('firmware SHA report', () => {
|
||||
expect(relocatedWithHeaders.combined).not.toEqual(withHeaders.combined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('firmware digest provider', () => {
|
||||
it('falls back to the local implementation when SubtleCrypto is unavailable', async () => {
|
||||
const data = textToBytes('plain HTTP LAN fallback')
|
||||
expect(await digestFirmwareBytes('SHA-256', data, undefined)).toBe(hash('sha256', data))
|
||||
expect(await digestFirmwareBytes('SHA-512', data, undefined)).toBe(hash('sha512', data))
|
||||
})
|
||||
})
|
||||
|
||||
+48
-1
@@ -2,7 +2,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createWebToolsServer, MAX_JSON_BODY_SIZE } from '../server/index.mjs'
|
||||
import { assertProductionBuild, createWebToolsServer, MAX_JSON_BODY_SIZE } from '../server/index.mjs'
|
||||
|
||||
const temporaryDirectories = []
|
||||
const servers = []
|
||||
@@ -47,6 +47,12 @@ async function createFixtureServer() {
|
||||
}
|
||||
|
||||
describe('HTTP server', () => {
|
||||
it('fails before listening when the production build is missing', async () => {
|
||||
const directory = await mkdtemp(path.join(tmpdir(), 'web-tools-missing-build-'))
|
||||
temporaryDirectories.push(directory)
|
||||
await expect(assertProductionBuild(path.join(directory, 'dist'))).rejects.toThrow('npm.cmd run build')
|
||||
})
|
||||
|
||||
it('serves direct SPA navigation but returns 404 for missing assets', async () => {
|
||||
const { baseUrl } = await createFixtureServer()
|
||||
const page = await fetch(`${baseUrl}/tools/base64`, { headers: { Accept: 'text/html' } })
|
||||
@@ -88,6 +94,47 @@ describe('HTTP server', () => {
|
||||
expect(JSON.parse(await readFile(path.join(dataDir, 'navigation.json'), 'utf8'))).toEqual(updated)
|
||||
})
|
||||
|
||||
it('normalizes an existing navigation file when a later release adds a tool', async () => {
|
||||
const { baseUrl, dataDir } = await createFixtureServer()
|
||||
const legacy = { version: 1, categories: [{ id: 'encoding', tools: ['hex'] }, { id: 'crypto', tools: ['cmac'] }] }
|
||||
await mkdir(dataDir, { recursive: true })
|
||||
await writeFile(path.join(dataDir, 'navigation.json'), JSON.stringify(legacy))
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/navigation`)
|
||||
expect(await response.json()).toEqual(legacy)
|
||||
|
||||
const expandedCatalog = { ...catalog, tools: [...catalog.tools, { id: 'base64', category: 'encoding' }] }
|
||||
const expandedDefaults = { ...defaults, categories: [{ id: 'encoding', tools: ['hex', 'base64'] }, ...defaults.categories.slice(1)] }
|
||||
const secondServer = await createWebToolsServer({
|
||||
staticRoot: path.join(dataDir, 'missing-dist'),
|
||||
dataDir,
|
||||
catalog: expandedCatalog,
|
||||
defaults: expandedDefaults,
|
||||
logger: { error: () => undefined },
|
||||
})
|
||||
servers.push(secondServer)
|
||||
await new Promise((resolve, reject) => {
|
||||
secondServer.once('error', reject)
|
||||
secondServer.listen(0, '127.0.0.1', resolve)
|
||||
})
|
||||
const address = secondServer.address()
|
||||
const upgraded = await fetch(`http://127.0.0.1:${address.port}/api/navigation`)
|
||||
expect(await upgraded.json()).toEqual({
|
||||
version: 1,
|
||||
categories: [
|
||||
{ id: 'encoding', tools: ['hex', 'base64'] },
|
||||
{ id: 'crypto', tools: ['cmac'] },
|
||||
],
|
||||
})
|
||||
expect(JSON.parse(await readFile(path.join(dataDir, 'navigation.json'), 'utf8'))).toEqual({
|
||||
version: 1,
|
||||
categories: [
|
||||
{ id: 'encoding', tools: ['hex', 'base64'] },
|
||||
{ id: 'crypto', tools: ['cmac'] },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects invalid, oversized and cross-site navigation writes', async () => {
|
||||
const { baseUrl } = await createFixtureServer()
|
||||
const invalid = await fetch(`${baseUrl}/api/navigation`, {
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ function Assert-ProductionBuild {
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $distIndex -PathType Leaf)) {
|
||||
throw "Production build was not found: $distIndex. Run '.\web-tools.ps1 update' first."
|
||||
throw "Production build was not found: $distIndex. Run '.\web-tools.ps1 update -NoStart' or 'npm.cmd run build' first."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user