diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 22:43:24 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2025-12-14 22:43:24 -0800 |
| commit | cdb1a57614068fcfefa145bc6df45c9def7ccc6a (patch) | |
| tree | 92cadbecda8658c143b7625d5925e3411976a892 | |
| parent | 6d318665a08c0d4564d8de23cc39425d2c0bac49 (diff) | |
| download | posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.tar.gz posthook-cdb1a57614068fcfefa145bc6df45c9def7ccc6a.zip | |
Updates
| -rw-r--r-- | README.md | 114 | ||||
| -rwxr-xr-x | examples.sh | 356 | ||||
| -rw-r--r-- | package-lock.json | 2105 | ||||
| -rw-r--r-- | package.json | 11 | ||||
| -rw-r--r-- | src/activity/index.ts | 146 | ||||
| -rw-r--r-- | src/storage/index.ts | 79 | ||||
| -rw-r--r-- | src/types/index.ts | 2 | ||||
| -rw-r--r-- | test/integrations.test.ts | 128 | ||||
| -rw-r--r-- | test/storage.test.ts | 106 | ||||
| -rw-r--r-- | test/token.test.ts | 72 | ||||
| -rw-r--r-- | test/types.test.ts | 73 | ||||
| -rw-r--r-- | vitest.config.ts | 10 |
12 files changed, 3016 insertions, 186 deletions
@@ -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, + }, +}); |
