aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md114
-rwxr-xr-xexamples.sh356
-rw-r--r--package-lock.json2105
-rw-r--r--package.json11
-rw-r--r--src/activity/index.ts146
-rw-r--r--src/storage/index.ts79
-rw-r--r--src/types/index.ts2
-rw-r--r--test/integrations.test.ts128
-rw-r--r--test/storage.test.ts106
-rw-r--r--test/token.test.ts72
-rw-r--r--test/types.test.ts73
-rw-r--r--vitest.config.ts10
12 files changed, 3016 insertions, 186 deletions
diff --git a/README.md b/README.md
index 064fa71..a59bcc1 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ A simple API service for receiving and storing webhook requests with dynamic rou
**⚠️ Recommendation**: Put `/admin/*` behind an OAuth proxy (e.g., OAuth2 Proxy, Pomerium) for authentication.
#### Register a Route
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -40,6 +41,7 @@ Content-Type: application/json
```
With hCaptcha protection:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -53,6 +55,7 @@ Content-Type: application/json
```
With ntfy notifications:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -70,6 +73,7 @@ Content-Type: application/json
```
With CSRF token protection:
+
```bash
POST /admin/routes
Content-Type: application/json
@@ -83,6 +87,7 @@ Content-Type: application/json
```
#### List Routes
+
```bash
GET /admin/routes
```
@@ -90,17 +95,19 @@ GET /admin/routes
### Public Webhook Endpoints
#### Get CSRF Token (for routes with requireToken: true)
+
```bash
GET /hook/{routeName}/token
```
Response:
+
```json
{
- "ok": {
- "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...",
- "expiresAt": 1702835467890
- }
+ "ok": {
+ "token": "eyJ0aW1lc3RhbXAiOjE3MDI4MzQ1Njc4OTB5...",
+ "expiresAt": 1702835467890
+ }
}
```
@@ -109,6 +116,7 @@ Response:
#### Send Webhook
Send webhooks to registered routes:
+
```bash
POST /hook/{routeName}
Content-Type: application/json
@@ -119,6 +127,7 @@ Content-Type: application/json
```
For hCaptcha-protected routes, include the token:
+
```bash
POST /hook/{routeName}
H-Captcha-Response: <token>
@@ -130,6 +139,7 @@ Content-Type: application/json
```
For form submissions with redirect (returns 303 redirect instead of JSON):
+
```bash
POST /hook/{routeName}
Content-Type: application/x-www-form-urlencoded
@@ -138,16 +148,18 @@ name=John&email=john@example.com&_redirect=https://example.com/thank-you
```
Or in HTML:
+
```html
<form action="/hook/my-form" method="POST">
- <input type="text" name="name" required>
- <input type="email" name="email" required>
- <input type="hidden" name="_redirect" value="https://example.com/thank-you">
- <button type="submit">Submit</button>
+ <input type="text" name="name" required />
+ <input type="email" name="email" required />
+ <input type="hidden" name="_redirect" value="https://example.com/thank-you" />
+ <button type="submit">Submit</button>
</form>
```
For token-protected routes, include the token:
+
```bash
POST /hook/{routeName}
X-CSRF-Token: <token>
@@ -159,6 +171,7 @@ Content-Type: application/json
```
Or in form data:
+
```bash
POST /hook/{routeName}
Content-Type: application/x-www-form-urlencoded
@@ -167,6 +180,7 @@ name=John&email=john@example.com&_token=<token>
```
#### Health Check
+
```bash
GET /health
```
@@ -177,7 +191,7 @@ GET /health
- `form` - Parse as URL-encoded form data (supports `_redirect` field)
- `text` - Store as plain text
- `raw` - Store raw body
-- `multipart` - Multipart form data (not yet implemented, will support `_redirect` field)
+- `multipart` - Multipart form data (supports file uploads and `_redirect`)
## CSRF Token Protection
@@ -186,18 +200,18 @@ Posthook supports stateless CSRF token protection using HMAC-signed tokens. When
### How it works:
1. **Token Generation**: Tokens are HMAC-signed with a secret and contain:
- - Route name (prevents token reuse across routes)
- - Timestamp (30-second TTL)
+ - Route name (prevents token reuse across routes)
+ - Timestamp (30-second TTL)
2. **Token Delivery**: Tokens can be provided via:
- - `_token` field in form data (extracted and not stored)
- - `X-CSRF-Token` request header
+ - `_token` field in form data (extracted and not stored)
+ - `X-CSRF-Token` request header
3. **Validation**: Server validates:
- - HMAC signature
- - Route name matches
- - Token not expired (30 seconds)
- - Timestamp not from future
+ - HMAC signature
+ - Route name matches
+ - Token not expired (30 seconds)
+ - Timestamp not from future
### Token Secret Configuration:
@@ -218,23 +232,27 @@ npm start # Warns and generates random secret
With a 30-second TTL, you should generate the token server-side when rendering the form, not via client-side JavaScript.
**Example with server-side rendering**:
+
```html
<!-- Your server fetches the token before rendering -->
<form action="/hook/contact-form" method="POST">
- <input type="hidden" name="_token" value="<%= token %>">
- <input type="text" name="name" required>
- <input type="email" name="email" required>
- <input type="hidden" name="_redirect" value="/thanks">
- <button type="submit">Submit</button>
+ <input type="hidden" name="_token" value="<%= token %>" />
+ <input type="text" name="name" required />
+ <input type="email" name="email" required />
+ <input type="hidden" name="_redirect" value="/thanks" />
+ <button type="submit">Submit</button>
</form>
```
**Your server** (Node.js example):
+
```javascript
app.get('/contact', async (req, res) => {
- const response = await fetch('http://localhost:9000/hook/contact-form/token');
- const { ok: { token } } = await response.json();
- res.render('contact', { token });
+ const response = await fetch('http://localhost:9000/hook/contact-form/token');
+ const {
+ ok: { token },
+ } = await response.json();
+ res.render('contact', { token });
});
```
@@ -243,11 +261,13 @@ app.get('/contact', async (req, res) => {
When using `contentType: "form"` or `contentType: "multipart"`, you can include a `_redirect` field in your form data. If the request is successfully stored, the server will respond with a `303 See Other` redirect to the specified URL instead of a JSON response.
The `_redirect` field is:
+
- Extracted from the form data and not stored in the request body
- Only honored on successful requests (validation failures, errors, etc. will still return JSON error responses)
- Useful for traditional HTML form submissions where you want to redirect users after submission
Example form flow:
+
1. User submits form with `_redirect` field (and `_token` if required)
2. Posthook validates token (if required) and stores the request
3. Posthook sends ntfy notification (if configured)
@@ -264,6 +284,7 @@ When you register a route with ntfy enabled, posthook will send a notification t
- Route name
Example notification:
+
```
Title: Webhook received: my-webhook
Message: Method: POST
@@ -273,26 +294,43 @@ UUID: 123e4567-e89b-12d3-a456-426614174000
## Storage Format
-Requests are stored per-topic in `data/{routeName}/{timestamp}_{uuid}.json`:
+Requests are stored per-topic in a per-request directory: `data/{routeName}/{timestamp}_{uuid}/`:
+
+- `request.json` - full stored request (headers + parsed body + file metadata)
+- `body.json` - parsed body only (convenience)
+- `files/` - uploaded files (multipart only)
+
+Example `request.json`:
```json
{
- "timestamp": 1702834567890,
- "uuid": "123e4567-e89b-12d3-a456-426614174000",
- "routeName": "my-webhook",
- "method": "POST",
- "headers": {
- "content-type": "application/json"
- },
- "body": {
- "your": "data"
- }
+ "timestamp": 1702834567890,
+ "uuid": "123e4567-e89b-12d3-a456-426614174000",
+ "routeName": "my-webhook",
+ "method": "POST",
+ "headers": {
+ "content-type": "application/json"
+ },
+ "body": {
+ "your": "data"
+ },
+ "files": [
+ {
+ "fieldName": "attachment",
+ "originalFilename": "invoice.pdf",
+ "filename": "0_invoice.pdf",
+ "contentType": "application/pdf",
+ "size": 12345,
+ "path": "files/0_invoice.pdf"
+ }
+ ]
}
```
## Usage
### Development
+
```bash
npm install
npm run build
@@ -301,6 +339,7 @@ npm start -- --port 9000 --host 0.0.0.0 --data-dir ./data
```
### Docker
+
```bash
docker build -t posthook .
docker run -p 9000:9000 \
@@ -330,16 +369,19 @@ docker run -p 4180:4180 \
```
Then configure your reverse proxy:
+
- `/admin/*` → OAuth2 Proxy on port 4180
- `/` → Posthook on port 9000
## Configuration
### Command Line Arguments:
+
- `--port` - Server port (default: 9000)
- `--host` - Server host (default: 0.0.0.0)
- `--data-dir` - Data storage directory (default: ./data)
- `--token-secret` - HMAC secret for token signing (optional, generates random if not provided)
### Environment Variables:
+
- `POSTHOOK_TOKEN_SECRET` - HMAC secret for token signing (recommended for production)
diff --git a/examples.sh b/examples.sh
index 69c1eb8..4efbc75 100755
--- a/examples.sh
+++ b/examples.sh
@@ -1,162 +1,304 @@
#!/bin/bash
# Example usage of posthook API
+#
+# If your Posthook instance is behind oauth2-proxy, provide the cookie value:
+# OAUTH2_PROXY_COOKIE="<cookie value>" ./examples.sh
+# ./examples.sh --cookie "<cookie value>"
+#
+# Optionally override the base URL:
+# BASE_URL="http://localhost:9000" ./examples.sh
+# ./examples.sh --base-url "http://localhost:9000"
+
+set -euo pipefail
+
+usage() {
+ cat <<'EOF'
+Usage: ./examples.sh [--base-url <url>] [--cookie <value>]
+
+Options:
+ --base-url <url> Posthook base URL (or set BASE_URL)
+ --cookie <value> Value for the _oauth2_proxy cookie (or set OAUTH2_PROXY_COOKIE)
+ -h, --help Show this help
+EOF
+}
+
+require_cmd() {
+ if ! command -v "$1" >/dev/null 2>&1; then
+ echo "Missing required command: $1" >&2
+ exit 1
+ fi
+}
BASE_URL="${BASE_URL:-http://localhost:9000}"
+OAUTH2_PROXY_COOKIE="${OAUTH2_PROXY_COOKIE:-}"
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --base-url)
+ BASE_URL="$2"
+ shift 2
+ ;;
+ --cookie|--oauth2-proxy-cookie)
+ OAUTH2_PROXY_COOKIE="$2"
+ shift 2
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ *)
+ echo "Unknown argument: $1" >&2
+ usage >&2
+ exit 1
+ ;;
+ esac
+done
+
+require_cmd curl
+require_cmd jq
+
+CURL_AUTH_ARGS=()
+if [[ -n "$OAUTH2_PROXY_COOKIE" ]]; then
+ if [[ "$OAUTH2_PROXY_COOKIE" == _oauth2_proxy=* ]]; then
+ CURL_AUTH_ARGS=(-b "$OAUTH2_PROXY_COOKIE")
+ else
+ CURL_AUTH_ARGS=(-b "_oauth2_proxy=$OAUTH2_PROXY_COOKIE")
+ fi
+fi
+
+curl_with_status() {
+ local method="$1"
+ local url="$2"
+ local content_type="${3:-}"
+ local data="${4:-}"
+
+ local curl_args=("${CURL_AUTH_ARGS[@]}" -sS)
+
+ if [[ -n "$content_type" ]]; then
+ curl_args+=(-H "Content-Type: $content_type")
+ fi
+
+ # Avoid curl's "Unnecessary use of -X" warning when POST already inferred.
+ if [[ "$method" != "POST" || -z "$data" ]]; then
+ curl_args+=(-X "$method")
+ fi
+
+ if [[ -n "$data" ]]; then
+ curl_args+=(-d "$data")
+ fi
+
+ curl "${curl_args[@]}" "$url" -w $'\n%{http_code}'
+}
+
+expect_json() {
+ local label="$1"
+ local method="$2"
+ local url="$3"
+ local content_type="${4:-}"
+ local data="${5:-}"
+ local expected_status="$6"
+ local jq_check="$7"
+
+ local out body status
+ out="$(curl_with_status "$method" "$url" "$content_type" "$data")"
+ status="${out##*$'\n'}"
+ body="${out%$'\n'*}"
+
+ if [[ "$status" != "$expected_status" ]]; then
+ echo "$label: unexpected HTTP $status (expected $expected_status)" >&2
+ echo "$body" >&2
+ exit 1
+ fi
+
+ if ! echo "$body" | jq -e "$jq_check" >/dev/null; then
+ echo "$label: response did not match expected JSON" >&2
+ echo "$body" >&2
+ exit 1
+ fi
+
+ echo "$body"
+}
+
+expect_redirect() {
+ local label="$1"
+ local url="$2"
+ local content_type="$3"
+ local data="$4"
+ local expected_status="$5"
+ local expected_location="$6"
+
+ local out header_text status location
+ out="$(curl "${CURL_AUTH_ARGS[@]}" -sS -o /dev/null -D - -H "Content-Type: $content_type" -d "$data" "$url" -w $'\n%{http_code}')"
+ status="${out##*$'\n'}"
+ header_text="${out%$'\n'*}"
+
+ if [[ "$status" != "$expected_status" ]]; then
+ echo "$label: unexpected HTTP $status (expected $expected_status)" >&2
+ echo "$header_text" >&2
+ exit 1
+ fi
+
+ location=""
+ while IFS= read -r line; do
+ line="${line%$'\r'}"
+ case "$line" in
+ [Ll]ocation:*)
+ location="${line#*: }"
+ ;;
+ esac
+ done <<< "$header_text"
+
+ if [[ "$location" != "$expected_location" ]]; then
+ echo "$label: unexpected Location: $location" >&2
+ echo "$header_text" >&2
+ exit 1
+ fi
+
+ echo "HTTP $status"
+ echo "Location: $location"
+}
echo "=== Posthook Examples ==="
echo
# 1. Register a simple JSON webhook
echo "1. Registering a simple JSON webhook..."
-curl -X POST "$BASE_URL/admin/routes" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "simple-webhook",
- "contentType": "json",
- "hcaptchaProtected": false
- }'
-echo
+ROUTE_SIMPLE_JSON="$(cat <<'JSON'
+{
+ "name": "simple-webhook",
+ "contentType": "json",
+ "hcaptchaProtected": false
+}
+JSON
+)"
+expect_json "register simple-webhook" POST "$BASE_URL/admin/routes" "application/json" "$ROUTE_SIMPLE_JSON" 200 '.ok.success == true'
echo
-# 2. Register a form webhook
echo "2. Registering a form webhook..."
-curl -X POST "$BASE_URL/admin/routes" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "form-webhook",
- "contentType": "form",
- "hcaptchaProtected": false
- }'
-echo
+ROUTE_FORM_JSON="$(cat <<'JSON'
+{
+ "name": "form-webhook",
+ "contentType": "form",
+ "hcaptchaProtected": false
+}
+JSON
+)"
+expect_json "register form-webhook" POST "$BASE_URL/admin/routes" "application/json" "$ROUTE_FORM_JSON" 200 '.ok.success == true'
echo
-# 3. Register an hCaptcha-protected webhook
echo "3. Registering an hCaptcha-protected webhook..."
-curl -X POST "$BASE_URL/admin/routes" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "protected-webhook",
- "contentType": "json",
- "hcaptchaProtected": true,
- "hcaptchaSecret": "0x0000000000000000000000000000000000000000"
- }'
-echo
+ROUTE_HCAPTCHA_JSON="$(cat <<'JSON'
+{
+ "name": "protected-webhook",
+ "contentType": "json",
+ "hcaptchaProtected": true,
+ "hcaptchaSecret": "0x0000000000000000000000000000000000000000"
+}
+JSON
+)"
+expect_json "register protected-webhook" POST "$BASE_URL/admin/routes" "application/json" "$ROUTE_HCAPTCHA_JSON" 200 '.ok.success == true'
echo
-# 3b. Register a webhook with ntfy notifications
echo "3b. Registering a webhook with ntfy notifications..."
-curl -X POST "$BASE_URL/admin/routes" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "ntfy-webhook",
- "contentType": "json",
- "hcaptchaProtected": false,
- "ntfy": {
- "enabled": true,
- "server": "https://ntfy.sh",
- "topic": "posthook-demo-alerts"
- }
- }'
-echo
-echo
-
-# 3c. Register a CSRF token-protected form
-echo "3c. Registering a CSRF token-protected form..."
-curl -X POST "$BASE_URL/admin/routes" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "secure-form",
- "contentType": "form",
- "hcaptchaProtected": false,
- "requireToken": true
- }'
+ROUTE_NTFY_JSON="$(cat <<'JSON'
+{
+ "name": "ntfy-webhook",
+ "contentType": "json",
+ "hcaptchaProtected": false,
+ "ntfy": {
+ "enabled": true,
+ "server": "https://ntfy.sh",
+ "topic": "posthook-demo-alerts"
+ }
+}
+JSON
+)"
+expect_json "register ntfy-webhook" POST "$BASE_URL/admin/routes" "application/json" "$ROUTE_NTFY_JSON" 200 '.ok.success == true'
echo
+
+echo "3c. Registering a CSRF token-protected form..."
+ROUTE_SECURE_FORM_JSON="$(cat <<'JSON'
+{
+ "name": "secure-form",
+ "contentType": "form",
+ "hcaptchaProtected": false,
+ "requireToken": true
+}
+JSON
+)"
+expect_json "register secure-form" POST "$BASE_URL/admin/routes" "application/json" "$ROUTE_SECURE_FORM_JSON" 200 '.ok.success == true'
echo
-# 4. List all routes
echo "4. Listing all routes..."
-curl -X GET "$BASE_URL/admin/routes"
-echo
+expect_json "list routes" GET "$BASE_URL/admin/routes" "" "" 200 \
+ '(.ok.routes | type == "array")
+ and ((.ok.routes | map(.name) | index("simple-webhook")) != null)
+ and ((.ok.routes | map(.name) | index("form-webhook")) != null)
+ and ((.ok.routes | map(.name) | index("protected-webhook")) != null)
+ and ((.ok.routes | map(.name) | index("ntfy-webhook")) != null)
+ and ((.ok.routes | map(.name) | index("secure-form")) != null)'
echo
-# 5. Send a test webhook to simple-webhook
echo "5. Sending test data to simple-webhook..."
-curl -X POST "$BASE_URL/hook/simple-webhook" \
- -H "Content-Type: application/json" \
- -d '{
- "event": "test",
- "data": {
- "foo": "bar",
- "timestamp": 1234567890
- }
- }'
-echo
+SIMPLE_WEBHOOK_BODY="$(cat <<'JSON'
+{
+ "event": "test",
+ "data": {
+ "foo": "bar",
+ "timestamp": 1234567890
+ }
+}
+JSON
+)"
+expect_json "post simple-webhook" POST "$BASE_URL/hook/simple-webhook" "application/json" "$SIMPLE_WEBHOOK_BODY" 200 '.ok.success == true and (.ok.stored | type == "string") and (.ok.stored | endswith("/request.json"))'
echo
-# 6. Send a form webhook
echo "6. Sending form data to form-webhook..."
-curl -X POST "$BASE_URL/hook/form-webhook" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "name=John&email=john@example.com&message=Hello+World"
-echo
+expect_json "post form-webhook" POST "$BASE_URL/hook/form-webhook" "application/x-www-form-urlencoded" \
+ "name=John&email=john@example.com&message=Hello+World" 200 '.ok.success == true and (.ok.stored | type == "string") and (.ok.stored | endswith("/request.json"))'
echo
-# 6b. Send a webhook to ntfy-enabled route (will trigger notification)
echo "6b. Sending webhook to ntfy-webhook (check https://ntfy.sh/posthook-demo-alerts)..."
-curl -X POST "$BASE_URL/hook/ntfy-webhook" \
- -H "Content-Type: application/json" \
- -d '{
- "event": "test-notification",
- "message": "This should trigger an ntfy alert"
- }'
-echo
+NTFY_WEBHOOK_BODY="$(cat <<'JSON'
+{
+ "event": "test-notification",
+ "message": "This should trigger an ntfy alert"
+}
+JSON
+)"
+expect_json "post ntfy-webhook" POST "$BASE_URL/hook/ntfy-webhook" "application/json" "$NTFY_WEBHOOK_BODY" 200 '.ok.success == true and (.ok.stored | type == "string") and (.ok.stored | endswith("/request.json"))'
echo
-# 6c. Send a form with redirect (should return 303 redirect)
echo "6c. Sending form with _redirect (should return 303 redirect)..."
-curl -v -X POST "$BASE_URL/hook/form-webhook" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "name=Jane&email=jane@example.com&message=Testing+redirect&_redirect=https://example.com/thank-you"
-echo
+expect_redirect "redirect form-webhook" "$BASE_URL/hook/form-webhook" "application/x-www-form-urlencoded" \
+ "name=Jane&email=jane@example.com&message=Testing+redirect&_redirect=https://example.com/thank-you" 303 \
+ "https://example.com/thank-you"
echo
-# 6d. Get CSRF token for secure-form
echo "6d. Getting CSRF token for secure-form..."
-TOKEN_RESPONSE=$(curl -s -X GET "$BASE_URL/hook/secure-form/token")
-TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.ok.token')
+TOKEN_RESPONSE="$(expect_json "get secure-form token" GET "$BASE_URL/hook/secure-form/token" "" "" 200 '.ok.token | type == "string" and length > 0')"
+TOKEN="$(echo "$TOKEN_RESPONSE" | jq -r '.ok.token')"
echo "Token: $TOKEN"
echo
-echo
-# 6e. Send form with CSRF token
echo "6e. Sending form with CSRF token..."
-curl -X POST "$BASE_URL/hook/secure-form" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "name=Secure&email=secure@example.com&message=With+token&_token=$TOKEN"
-echo
+expect_json "post secure-form with token" POST "$BASE_URL/hook/secure-form" "application/x-www-form-urlencoded" \
+ "name=Secure&email=secure@example.com&message=With+token&_token=$TOKEN" 200 '.ok.success == true and (.ok.stored | type == "string") and (.ok.stored | endswith("/request.json"))'
echo
-# 6f. Try sending without token (should fail with 400)
echo "6f. Trying to send without token (should fail)..."
-curl -X POST "$BASE_URL/hook/secure-form" \
- -H "Content-Type: application/x-www-form-urlencoded" \
- -d "name=Insecure&email=insecure@example.com&message=No+token"
-echo
+expect_json "post secure-form without token" POST "$BASE_URL/hook/secure-form" "application/x-www-form-urlencoded" \
+ "name=Insecure&email=insecure@example.com&message=No+token" 400 '.error.message == "Missing CSRF token" and .error.status == 400'
echo
-# 7. Test 404 on non-existent route
echo "7. Testing 404 on non-existent route..."
-curl -X POST "$BASE_URL/hook/does-not-exist" \
- -H "Content-Type: application/json" \
- -d '{"test": true}'
-echo
+expect_json "post missing route" POST "$BASE_URL/hook/does-not-exist" "application/json" '{"test": true}' 404 \
+ '.error.message == "Route not found" and .error.status == 404'
echo
-# 8. Health check
echo "8. Health check..."
-curl -X GET "$BASE_URL/health"
-echo
+expect_json "health" GET "$BASE_URL/health" "" "" 200 '.ok == "ok"'
echo
echo "=== Examples complete ==="
diff --git a/package-lock.json b/package-lock.json
index d771e35..f5fa199 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"name": "@emprespresso/posthook",
"version": "0.1.0",
"dependencies": {
- "@emprespresso/pengueno": "^0.0.17",
+ "@emprespresso/pengueno": "^0.0.18",
"@hono/node-server": "^1.14.0",
"hono": "^4.8.9"
},
@@ -16,21 +16,97 @@
"@types/node": "^24.0.3",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
+ "@vitest/coverage-v8": "^3.2.4",
"eslint": "^8.34.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.0",
"prettier": "^3.5.3",
- "typescript": "^5.8.3"
+ "typescript": "^5.8.3",
+ "vitest": "^3.2.4"
},
"engines": {
"node": ">=22.16.0",
"npm": ">=10.0.0"
}
},
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@emprespresso/pengueno": {
- "version": "0.0.17",
- "resolved": "https://registry.npmjs.org/@emprespresso/pengueno/-/pengueno-0.0.17.tgz",
- "integrity": "sha512-pyfXLYGoLlQlMQAyYptRLLiRrnQmCju4I1/sv1KtsJv06oPvNqzHqUzJJGHrXg/90krg7GRcxLKQ0TnQ2Bskjw==",
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/@emprespresso/pengueno/-/pengueno-0.0.18.tgz",
+ "integrity": "sha512-CP/GQpdpBGRNKG4AeoomtnmS4FzRiEwj9Qi6T9rIlkBiD+78gkPBk0M4XnQto0LJHRRq5hB86FjGYHI0SCHRhw==",
"license": "MIT",
"engines": {
"node": ">=22.16.0",
@@ -41,6 +117,448 @@
"hono": "^4.8.0"
}
},
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -212,6 +730,102 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "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.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -250,6 +864,17 @@
"node": ">= 8"
}
},
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@pkgr/core": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
@@ -263,6 +888,339 @@
"url": "https://opencollective.com/pkgr"
}
},
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/node": {
"version": "24.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
@@ -513,6 +1471,155 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/@vitest/coverage-v8": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz",
+ "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@bcoe/v8-coverage": "^1.0.2",
+ "ast-v8-to-istanbul": "^0.3.3",
+ "debug": "^4.4.1",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.1.7",
+ "magic-string": "^0.30.17",
+ "magicast": "^0.3.5",
+ "std-env": "^3.9.0",
+ "test-exclude": "^7.0.1",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "3.2.4",
+ "vitest": "3.2.4"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -586,6 +1693,28 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz",
+ "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^9.0.1"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -603,6 +1732,16 @@
"balanced-match": "^1.0.0"
}
},
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -613,6 +1752,23 @@
"node": ">=6"
}
},
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -630,6 +1786,16 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/check-error": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+ "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -690,6 +1856,16 @@
}
}
},
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -710,6 +1886,69 @@
"node": ">=6.0.0"
}
},
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -945,6 +2184,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -955,6 +2204,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1063,6 +2322,23 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -1070,6 +2346,21 @@
"dev": true,
"license": "ISC"
},
+ "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/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1171,6 +2462,13 @@
"node": ">=16.9.0"
}
},
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -1237,6 +2535,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1267,6 +2575,83 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -1348,6 +2733,58 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
+ "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.4",
+ "@babel/types": "^7.25.4",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -1364,6 +2801,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1371,6 +2818,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -1438,6 +2904,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -1481,6 +2954,47 @@
"node": ">=8"
}
},
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
@@ -1494,6 +3008,35 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -1602,6 +3145,48 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -1662,6 +3247,120 @@
"node": ">=8"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -1675,6 +3374,20 @@
"node": ">=8"
}
},
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -1688,6 +3401,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -1717,6 +3443,42 @@
"url": "https://opencollective.com/synckit"
}
},
+ "node_modules/test-exclude": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+ "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^9.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -1724,6 +3486,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -1741,6 +3517,36 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -1811,6 +3617,177 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/vite": {
+ "version": "7.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
+ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -1827,6 +3804,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -1837,6 +3831,107 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index 7232519..7265fe2 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,13 @@
"format:check": "prettier --check .",
"type-check": "tsc --noEmit",
"clean": "rm -rf dist node_modules",
- "start": "node dist/index.js"
+ "start": "node dist/index.js",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage"
},
"dependencies": {
- "@emprespresso/pengueno": "^0.0.17",
+ "@emprespresso/pengueno": "^0.0.18",
"@hono/node-server": "^1.14.0",
"hono": "^4.8.9"
},
@@ -23,11 +26,13 @@
"@types/node": "^24.0.3",
"@typescript-eslint/eslint-plugin": "^8.34.1",
"@typescript-eslint/parser": "^8.34.1",
+ "@vitest/coverage-v8": "^3.2.4",
"eslint": "^8.34.1",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-prettier": "^5.5.0",
"prettier": "^3.5.3",
- "typescript": "^5.8.3"
+ "typescript": "^5.8.3",
+ "vitest": "^3.2.4"
},
"engines": {
"node": ">=22.16.0",
diff --git a/src/activity/index.ts b/src/activity/index.ts
index 20d123c..c3ee821 100644
--- a/src/activity/index.ts
+++ b/src/activity/index.ts
@@ -110,25 +110,58 @@ export class WebhookActivityImpl implements IWebhookActivity {
private async parseBody(
req: PenguenoRequest,
contentType: ContentType,
- ): Promise<IEither<PenguenoError, { body: unknown; redirect: string | undefined; token: string | undefined }>> {
+ ): Promise<
+ IEither<
+ PenguenoError,
+ {
+ body: unknown;
+ redirect: string | undefined;
+ token: string | undefined;
+ uploads:
+ | Array<{
+ fieldName: string;
+ filename: string;
+ contentType: string;
+ size: number;
+ data: Uint8Array;
+ }>
+ | undefined;
+ }
+ >
+ > {
try {
- const rawBody = await req.req.text();
-
- type ParsedBody = { body: unknown; redirect: string | undefined; token: string | undefined };
+ type ParsedBody = {
+ body: unknown;
+ redirect: string | undefined;
+ token: string | undefined;
+ uploads:
+ | Array<{
+ fieldName: string;
+ filename: string;
+ contentType: string;
+ size: number;
+ data: Uint8Array;
+ }>
+ | undefined;
+ };
switch (contentType) {
- case ContentType.JSON:
+ case ContentType.JSON: {
+ const rawBody = await req.req.text();
try {
return Either.right(<ParsedBody>{
body: JSON.parse(rawBody),
redirect: undefined,
token: undefined,
+ uploads: undefined,
});
} catch {
return Either.left(new PenguenoError('Invalid JSON', 400));
}
+ }
- case ContentType.FORM:
+ case ContentType.FORM: {
+ const rawBody = await req.req.text();
try {
const formData = new URLSearchParams(rawBody);
const obj: Record<string, string> = {};
@@ -144,22 +177,95 @@ export class WebhookActivityImpl implements IWebhookActivity {
obj[key] = value;
}
}
- return Either.right(<ParsedBody>{ body: obj, redirect, token });
+ return Either.right(<ParsedBody>{ body: obj, redirect, token, uploads: undefined });
} catch {
return Either.left(new PenguenoError('Invalid form data', 400));
}
+ }
+
+ case ContentType.MULTIPART: {
+ try {
+ const formData = await req.req.formData();
+ const obj: Record<string, string | string[]> = {};
+ const uploads: ParsedBody['uploads'] = [];
+ let redirect: string | undefined;
+ let token: string | undefined;
- case ContentType.TEXT:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ for (const [key, value] of formData.entries()) {
+ if (typeof value === 'string') {
+ if (key === '_redirect') {
+ redirect = value;
+ } else if (key === '_token') {
+ token = value;
+ } else {
+ const existing = obj[key];
+ if (existing === undefined) {
+ obj[key] = value;
+ } else if (Array.isArray(existing)) {
+ existing.push(value);
+ } else {
+ obj[key] = [existing, value];
+ }
+ }
+ continue;
+ }
- case ContentType.RAW:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ // Avoid DOM typings; treat as a File-like object.
+ const maybeFile = value as unknown as {
+ name?: unknown;
+ type?: unknown;
+ size?: unknown;
+ arrayBuffer?: unknown;
+ };
+
+ const filename = typeof maybeFile.name === 'string' ? maybeFile.name : 'upload.bin';
+ const contentType =
+ typeof maybeFile.type === 'string' ? maybeFile.type : 'application/octet-stream';
+ const size = typeof maybeFile.size === 'number' ? maybeFile.size : 0;
+
+ if (typeof maybeFile.arrayBuffer !== 'function') {
+ return Either.left(new PenguenoError('Invalid multipart file upload', 400));
+ }
- case ContentType.MULTIPART:
- return Either.left(new PenguenoError('Multipart not yet implemented', 501));
+ const buf = new Uint8Array(await (maybeFile.arrayBuffer as () => Promise<ArrayBuffer>)());
+ uploads.push({ fieldName: key, filename, contentType, size, data: buf });
+ }
- default:
- return Either.right(<ParsedBody>{ body: rawBody, redirect: undefined, token: undefined });
+ return Either.right(<ParsedBody>{ body: obj, redirect, token, uploads });
+ } catch {
+ return Either.left(new PenguenoError('Invalid multipart form data', 400));
+ }
+ }
+
+ case ContentType.TEXT: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
+
+ case ContentType.RAW: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
+
+ default: {
+ const rawBody = await req.req.text();
+ return Either.right(<ParsedBody>{
+ body: rawBody,
+ redirect: undefined,
+ token: undefined,
+ uploads: undefined,
+ });
+ }
}
} catch (err) {
return Either.left(new PenguenoError(err instanceof Error ? err.message : String(err), 500));
@@ -227,7 +333,7 @@ export class WebhookActivityImpl implements IWebhookActivity {
return tReq.move(Either.left<PenguenoError, WebhookResult>(bodyResult.left().get()));
}
- const { body, redirect, token: bodyToken } = bodyResult.right().get();
+ const { body, redirect, token: bodyToken, uploads } = bodyResult.right().get();
// Validate token if required
if (route.requireToken) {
@@ -252,7 +358,7 @@ export class WebhookActivityImpl implements IWebhookActivity {
}
// Store the request
- const storeResult = await this.storage.storeRequest(routeName, req.method, headers, body);
+ const storeResult = await this.storage.storeRequest(routeName, req.method, headers, body, uploads);
if (storeResult.left().present()) {
return tReq.move(
Either.left<PenguenoError, WebhookResult>(
@@ -274,11 +380,13 @@ export class WebhookActivityImpl implements IWebhookActivity {
}
}
- const filename = `${storedRequest.timestamp}_${storedRequest.uuid}.json`;
+ const baseName = `${storedRequest.timestamp}_${storedRequest.uuid}`;
+ const storedPath = `${baseName}/request.json`;
+
return tReq.move(
Either.right<PenguenoError, WebhookResult>({
success: true,
- stored: filename,
+ stored: storedPath,
redirect,
}),
);
diff --git a/src/storage/index.ts b/src/storage/index.ts
index 2c8ffb2..631fc2e 100644
--- a/src/storage/index.ts
+++ b/src/storage/index.ts
@@ -1,9 +1,23 @@
import { randomUUID } from 'crypto';
import { mkdir, writeFile, readFile } from 'fs/promises';
-import { join } from 'path';
+import { basename, join } from 'path';
import { isSafeRouteName, type RouteConfig, type StoredRequest } from '../types/index.js';
import { Either, type IEither } from '@emprespresso/pengueno';
+type IncomingUpload = {
+ fieldName: string;
+ filename: string;
+ contentType: string;
+ size: number;
+ data: Uint8Array;
+};
+
+function sanitizeFilename(filename: string): string {
+ const base = basename(filename);
+ const safe = base.replace(/[^a-zA-Z0-9._-]/g, '_');
+ return safe.length > 0 ? safe.slice(0, 200) : 'upload.bin';
+}
+
export class Storage {
private routes: Map<string, RouteConfig> = new Map();
@@ -83,7 +97,7 @@ export class Storage {
method: string,
headers: Record<string, string>,
body: unknown,
- files?: StoredRequest['files'],
+ uploads?: IncomingUpload[],
): Promise<IEither<Error, StoredRequest>> {
if (!isSafeRouteName(routeName)) {
return Either.left(new Error('Invalid route name'));
@@ -91,21 +105,54 @@ export class Storage {
const timestamp = Date.now();
const uuid = randomUUID();
- const filename = `${timestamp}_${uuid}.json`;
-
- const stored: StoredRequest = {
- timestamp,
- uuid,
- routeName,
- method,
- headers,
- body,
- files,
- };
-
- const filepath = join(this.dataDir, routeName, filename);
+ const baseName = `${timestamp}_${uuid}`;
+ const routeDir = join(this.dataDir, routeName);
+
try {
- await writeFile(filepath, JSON.stringify(stored, null, 2));
+ await mkdir(routeDir, { recursive: true });
+
+ const requestDir = join(routeDir, baseName);
+ await mkdir(requestDir, { recursive: true });
+
+ const files: StoredRequest['files'] = uploads?.length
+ ? await (async () => {
+ const filesDir = join(requestDir, 'files');
+ await mkdir(filesDir, { recursive: true });
+
+ const storedFiles: NonNullable<StoredRequest['files']> = [];
+ for (let i = 0; i < uploads.length; i++) {
+ const upload = uploads[i];
+ const safeOriginal = sanitizeFilename(upload.filename);
+ const savedName = `${i}_${safeOriginal}`;
+ const diskPath = join(filesDir, savedName);
+ await writeFile(diskPath, Buffer.from(upload.data));
+
+ storedFiles.push({
+ fieldName: upload.fieldName,
+ originalFilename: upload.filename,
+ filename: savedName,
+ contentType: upload.contentType,
+ size: upload.size,
+ path: join('files', savedName),
+ });
+ }
+
+ return storedFiles;
+ })()
+ : undefined;
+
+ const stored: StoredRequest = {
+ timestamp,
+ uuid,
+ routeName,
+ method,
+ headers,
+ body,
+ files,
+ };
+
+ await writeFile(join(requestDir, 'request.json'), JSON.stringify(stored, null, 2));
+ await writeFile(join(requestDir, 'body.json'), JSON.stringify(body, null, 2));
return Either.right(stored);
} catch (err) {
return Either.left(err instanceof Error ? err : new Error(String(err)));
diff --git a/src/types/index.ts b/src/types/index.ts
index fbfc70d..5e0b2d4 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -29,6 +29,8 @@ export interface StoredRequest {
headers: Record<string, string>;
body: unknown;
files?: Array<{
+ fieldName: string;
+ originalFilename: string;
filename: string;
contentType: string;
size: number;
diff --git a/test/integrations.test.ts b/test/integrations.test.ts
new file mode 100644
index 0000000..310945a
--- /dev/null
+++ b/test/integrations.test.ts
@@ -0,0 +1,128 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { verifyHCaptcha } from '../src/integrations/hcaptcha.js';
+import { sendNtfyNotification } from '../src/integrations/ntfy.js';
+import type { NtfyConfig, StoredRequest } from '../src/types/index.js';
+
+describe('verifyHCaptcha', () => {
+ beforeEach(() => {
+ vi.stubGlobal('fetch', vi.fn());
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('returns success boolean when response is ok', async () => {
+ const fetchMock = vi.mocked(fetch);
+ fetchMock.mockResolvedValue({
+ ok: true,
+ statusText: 'OK',
+ json: async () => ({ success: true }),
+ } as Response);
+
+ const result = await verifyHCaptcha('token', 'secret');
+ expect(result.left().present()).toBe(false);
+ expect(result.right().get()).toBe(true);
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ const [url, init] = fetchMock.mock.calls[0]!;
+ expect(url).toBe('https://hcaptcha.com/siteverify');
+ expect(init?.method).toBe('POST');
+ expect(init?.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });
+ expect(init?.body).toBeInstanceOf(URLSearchParams);
+ expect((init?.body as URLSearchParams).get('secret')).toBe('secret');
+ expect((init?.body as URLSearchParams).get('response')).toBe('token');
+ });
+
+ it('returns error when response is not ok', async () => {
+ const fetchMock = vi.mocked(fetch);
+ fetchMock.mockResolvedValue({
+ ok: false,
+ statusText: 'Bad Request',
+ } as Response);
+
+ const result = await verifyHCaptcha('token', 'secret');
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('hCaptcha verification failed: Bad Request');
+ });
+});
+
+describe('sendNtfyNotification', () => {
+ beforeEach(() => {
+ vi.stubGlobal('fetch', vi.fn());
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('is a no-op when not enabled or misconfigured', async () => {
+ const fetchMock = vi.mocked(fetch);
+
+ const config: NtfyConfig = { enabled: false };
+ const request: StoredRequest = {
+ timestamp: 1,
+ uuid: 'uuid',
+ routeName: 'route1',
+ method: 'POST',
+ headers: {},
+ body: {},
+ };
+
+ const result = await sendNtfyNotification(config, request);
+ expect(result.left().present()).toBe(false);
+ expect(fetchMock).not.toHaveBeenCalled();
+ });
+
+ it('posts a notification to the configured server/topic', async () => {
+ const fetchMock = vi.mocked(fetch);
+ fetchMock.mockResolvedValue({ ok: true, statusText: 'OK' } as Response);
+
+ const config: NtfyConfig = { enabled: true, server: 'https://ntfy.example.com', topic: 'topic1' };
+ const request: StoredRequest = {
+ timestamp: Date.parse('2020-01-01T00:00:00.000Z'),
+ uuid: 'uuid',
+ routeName: 'route1',
+ method: 'POST',
+ headers: {},
+ body: {},
+ };
+
+ const result = await sendNtfyNotification(config, request);
+ expect(result.left().present()).toBe(false);
+
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ const [url, init] = fetchMock.mock.calls[0]!;
+ expect(url).toBe('https://ntfy.example.com/topic1');
+ expect(init?.method).toBe('POST');
+ expect(init?.headers).toEqual({
+ Title: 'Webhook received: route1',
+ Tags: 'webhook,posthook',
+ Priority: '3',
+ });
+
+ expect(init?.body).toContain('Method: POST');
+ expect(init?.body).toContain('Timestamp: 2020-01-01T00:00:00.000Z');
+ expect(init?.body).toContain('UUID: uuid');
+ });
+
+ it('returns an error when ntfy responds with non-2xx', async () => {
+ const fetchMock = vi.mocked(fetch);
+ fetchMock.mockResolvedValue({ ok: false, statusText: 'Unauthorized' } as Response);
+
+ const config: NtfyConfig = { enabled: true, server: 'https://ntfy.example.com', topic: 'topic1' };
+ const request: StoredRequest = {
+ timestamp: 1,
+ uuid: 'uuid',
+ routeName: 'route1',
+ method: 'POST',
+ headers: {},
+ body: {},
+ };
+
+ const result = await sendNtfyNotification(config, request);
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('ntfy notification failed: Unauthorized');
+ });
+});
diff --git a/test/storage.test.ts b/test/storage.test.ts
new file mode 100644
index 0000000..7b64aa1
--- /dev/null
+++ b/test/storage.test.ts
@@ -0,0 +1,106 @@
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+
+import { mkdtemp, readFile, rm } from 'fs/promises';
+import { tmpdir } from 'os';
+import { join } from 'path';
+
+import { Storage } from '../src/storage/index.js';
+import { ContentType, type RouteConfig } from '../src/types/index.js';
+
+describe('Storage', () => {
+ let dataDir: string;
+
+ beforeEach(async () => {
+ dataDir = await mkdtemp(join(tmpdir(), 'posthook-test-'));
+ });
+
+ afterEach(async () => {
+ await rm(dataDir, { recursive: true, force: true });
+ });
+
+ it('persists routes to routes.json and loads them on init', async () => {
+ const route: RouteConfig = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ };
+
+ const storage1 = new Storage(dataDir);
+ expect((await storage1.init()).left().present()).toBe(false);
+ expect((await storage1.registerRoute(route)).left().present()).toBe(false);
+
+ const storage2 = new Storage(dataDir);
+ expect((await storage2.init()).left().present()).toBe(false);
+ expect(storage2.getRoute('route1')).toEqual(route);
+ expect(storage2.listRoutes()).toEqual([route]);
+ });
+
+ it('rejects unsafe route names', async () => {
+ const storage = new Storage(dataDir);
+ expect((await storage.init()).left().present()).toBe(false);
+
+ const result = await storage.registerRoute({
+ name: '../bad',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ });
+
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('Invalid route name');
+ });
+
+ it('stores a request and sanitizes uploaded filenames', async () => {
+ const route: RouteConfig = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ };
+
+ const storage = new Storage(dataDir);
+ expect((await storage.init()).left().present()).toBe(false);
+ expect((await storage.registerRoute(route)).left().present()).toBe(false);
+
+ const upload = {
+ fieldName: 'file',
+ filename: '../evil.txt',
+ contentType: 'text/plain',
+ size: 3,
+ data: new Uint8Array([1, 2, 3]),
+ };
+
+ const storeResult = await storage.storeRequest(
+ 'route1',
+ 'POST',
+ { 'content-type': 'application/json' },
+ { hello: 'world' },
+ [upload],
+ );
+
+ expect(storeResult.left().present()).toBe(false);
+
+ const stored = storeResult.right().get();
+ expect(stored.routeName).toBe('route1');
+ expect(stored.files?.length).toBe(1);
+
+ const storedFile = stored.files![0];
+ expect(storedFile.filename).toMatch(/^0_/);
+ expect(storedFile.filename).not.toContain('..');
+ expect(storedFile.filename).not.toContain('/');
+ expect(storedFile.path).toBe(`files/${storedFile.filename}`);
+
+ const requestDir = join(dataDir, 'route1', `${stored.timestamp}_${stored.uuid}`);
+
+ const requestJson = JSON.parse(await readFile(join(requestDir, 'request.json'), 'utf-8')) as {
+ routeName: string;
+ files?: Array<{ filename: string }>;
+ };
+ expect(requestJson.routeName).toBe('route1');
+ expect(requestJson.files?.[0].filename).toBe(storedFile.filename);
+
+ const bodyJson = JSON.parse(await readFile(join(requestDir, 'body.json'), 'utf-8'));
+ expect(bodyJson).toEqual({ hello: 'world' });
+
+ const savedBytes = await readFile(join(requestDir, storedFile.path));
+ expect(savedBytes.length).toBe(3);
+ });
+});
diff --git a/test/token.test.ts b/test/token.test.ts
new file mode 100644
index 0000000..060b591
--- /dev/null
+++ b/test/token.test.ts
@@ -0,0 +1,72 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { TokenSigner } from '../src/token/index.js';
+
+describe('TokenSigner', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2020-01-01T00:00:00.000Z'));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('generates and validates a token for the expected route', () => {
+ const signer = new TokenSigner('test-secret', 30);
+
+ const now = Date.now();
+ const token = signer.generate('route1');
+
+ const result = signer.validate(token, 'route1');
+ expect(result.left().present()).toBe(false);
+ expect(result.right().get()).toEqual({ routeName: 'route1', timestamp: now });
+ });
+
+ it('rejects token when route does not match', () => {
+ const signer = new TokenSigner('test-secret', 30);
+ const token = signer.generate('route1');
+
+ const result = signer.validate(token, 'route2');
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('Token route mismatch');
+ });
+
+ it('rejects expired tokens', () => {
+ const signer = new TokenSigner('test-secret', 1);
+ const token = signer.generate('route1');
+
+ vi.advanceTimersByTime(2000);
+
+ const result = signer.validate(token, 'route1');
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('Token expired');
+ });
+
+ it('rejects tokens with a bad signature', () => {
+ const signer = new TokenSigner('test-secret', 30);
+ const token = signer.generate('route1');
+
+ const decoded = Buffer.from(token, 'base64url').toString('utf-8');
+ const dotIndex = decoded.lastIndexOf('.');
+ const payload = decoded.substring(0, dotIndex);
+ const signature = decoded.substring(dotIndex + 1);
+
+ const parsed = JSON.parse(payload) as { routeName: string; timestamp: number };
+ const tamperedPayload = JSON.stringify({ ...parsed, routeName: 'route2' });
+ const tamperedToken = Buffer.from(`${tamperedPayload}.${signature}`).toString('base64url');
+
+ const result = signer.validate(tamperedToken, 'route2');
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('Invalid token signature');
+ });
+
+ it('rejects invalid token format', () => {
+ const signer = new TokenSigner('test-secret', 30);
+ const invalidToken = Buffer.from('missing-dot').toString('base64url');
+
+ const result = signer.validate(invalidToken, 'route1');
+ expect(result.left().present()).toBe(true);
+ expect(result.left().get().message).toBe('Invalid token format');
+ });
+});
diff --git a/test/types.test.ts b/test/types.test.ts
new file mode 100644
index 0000000..1823c15
--- /dev/null
+++ b/test/types.test.ts
@@ -0,0 +1,73 @@
+import { describe, expect, it } from 'vitest';
+
+import { ContentType, isRouteConfig, isSafeRouteName } from '../src/types/index.js';
+
+describe('isSafeRouteName', () => {
+ it('accepts typical route names', () => {
+ expect(isSafeRouteName('route1')).toBe(true);
+ expect(isSafeRouteName('Route_1')).toBe(true);
+ expect(isSafeRouteName('route-1')).toBe(true);
+ });
+
+ it('enforces length and character rules', () => {
+ expect(isSafeRouteName('a'.repeat(64))).toBe(true);
+ expect(isSafeRouteName('a'.repeat(65))).toBe(false);
+ expect(isSafeRouteName('bad!name')).toBe(false);
+ });
+
+ it('rejects unsafe or ambiguous names', () => {
+ expect(isSafeRouteName('')).toBe(false);
+ expect(isSafeRouteName(' route')).toBe(false);
+ expect(isSafeRouteName('route ')).toBe(false);
+ expect(isSafeRouteName('.')).toBe(false);
+ expect(isSafeRouteName('..')).toBe(false);
+ expect(isSafeRouteName('foo/bar')).toBe(false);
+ expect(isSafeRouteName('foo\\bar')).toBe(false);
+ });
+});
+
+describe('isRouteConfig', () => {
+ it('accepts a valid configuration', () => {
+ const config = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ ntfy: { enabled: false },
+ requireToken: true,
+ };
+
+ expect(isRouteConfig(config)).toBe(true);
+ });
+
+ it('requires an hCaptcha secret when protected', () => {
+ const config = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: true,
+ };
+
+ expect(isRouteConfig(config)).toBe(false);
+ });
+
+ it('validates ntfy config when enabled', () => {
+ const config = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ ntfy: { enabled: true },
+ };
+
+ expect(isRouteConfig(config)).toBe(false);
+ });
+
+ it('validates requireToken type when present', () => {
+ const config = {
+ name: 'route1',
+ contentType: ContentType.JSON,
+ hcaptchaProtected: false,
+ requireToken: 'yes',
+ };
+
+ expect(isRouteConfig(config)).toBe(false);
+ });
+});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..fc569f5
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['test/**/*.test.ts'],
+ restoreMocks: true,
+ clearMocks: true,
+ },
+});