diff --git a/.gitmodules b/.gitmodules index 18a3014b..fd23aded 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "third_party/gopher-orch"] path = third_party/gopher-orch url = https://github.com/GopherSecurity/gopher-orch.git - branch = br_release + branch = dev_auth diff --git a/CHANGELOG.md b/CHANGELOG.md index 5549eabe..a06cc501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] + +## [0.1.2] - 2026-03-12 + ## [0.1.1] - 2026-02-28 ## [0.1.0-20260227-124047] - 2026-02-27 @@ -106,7 +109,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- [Unreleased]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260227-124047...HEAD -[0.1.1]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260227-124047...v0.1.1[0.1.0-20260227-124047]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260226-072516...v0.1.0-20260227-124047 +[0.1.2]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260227-124047...v0.1.2[0.1.1]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260227-124047...v0.1.1[0.1.0-20260227-124047]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260226-072516...v0.1.0-20260227-124047 [0.1.0-20260226-072516]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260208-150923...v0.1.0-20260226-072516 [0.1.0-20260208-150923]: https://github.com/GopherSecurity/gopher-mcp-js/compare/v0.1.0-20260206-152345...v0.1.0-20260208-150923 [0.1.0-20260206-152345]: https://github.com/GopherSecurity/gopher-mcp-js/releases/tag/v0.1.0-20260206-152345 diff --git a/build.sh b/build.sh index 3078d0f5..05935d9d 100755 --- a/build.sh +++ b/build.sh @@ -23,6 +23,10 @@ if [ "$1" = "--clean" ]; then rm -rf "${BUILD_DIR}/CMakeFiles" rm -rf "${BUILD_DIR}/lib" rm -rf "${BUILD_DIR}/bin" + # Clean auth example + rm -rf "${SCRIPT_DIR}/examples/auth/node_modules" + rm -rf "${SCRIPT_DIR}/examples/auth/dist" + rm -rf "${SCRIPT_DIR}/examples/auth/lib" echo -e "${GREEN}✓ Clean complete${NC}" if [ "$2" != "--build" ]; then exit 0 @@ -142,6 +146,11 @@ cp -P "${BUILD_DIR}"/lib/libgopher-mcp-event*.so* "${NATIVE_LIB}/" 2>/dev/null | cp -P "${BUILD_DIR}"/lib/libgopher-mcp-logging*.dylib "${NATIVE_LIB}/" 2>/dev/null || true cp -P "${BUILD_DIR}"/lib/libgopher-mcp-logging*.so* "${NATIVE_LIB}/" 2>/dev/null || true +# Copy gopher-auth libraries +cp -P "${BUILD_DIR}"/lib/libgopher-auth*.dylib "${NATIVE_LIB}/" 2>/dev/null || true +cp -P "${BUILD_DIR}"/lib/libgopher-auth*.so* "${NATIVE_LIB}/" 2>/dev/null || true +cp -P "${BUILD_DIR}"/lib/gopher-auth*.dll "${NATIVE_LIB}/" 2>/dev/null || true + # Copy fmt and llhttp static libraries cp -P "${BUILD_DIR}"/lib/libfmt*.a "${NATIVE_LIB}/" 2>/dev/null || true cp -P "${BUILD_DIR}"/lib/libllhttp*.a "${NATIVE_LIB}/" 2>/dev/null || true @@ -212,6 +221,40 @@ echo "" echo -e "${YELLOW}Step 6: Running tests...${NC}" npm test --silent 2>/dev/null && echo -e "${GREEN}✓ Tests passed${NC}" || echo -e "${YELLOW}⚠ Some tests may have failed (native library required)${NC}" +echo "" + +# Step 8: Build auth example +echo -e "${YELLOW}Step 7: Building auth example...${NC}" +AUTH_EXAMPLE_DIR="${SCRIPT_DIR}/examples/auth" + +if [ -d "${AUTH_EXAMPLE_DIR}" ]; then + cd "${AUTH_EXAMPLE_DIR}" + + # Copy native libraries to example lib directory + echo -e "${YELLOW} Copying native libraries to example...${NC}" + mkdir -p "${AUTH_EXAMPLE_DIR}/lib" + cp -P "${NATIVE_LIB_DIR}"/libgopher-auth*.dylib "${AUTH_EXAMPLE_DIR}/lib/" 2>/dev/null || true + cp -P "${NATIVE_LIB_DIR}"/libgopher-auth*.so* "${AUTH_EXAMPLE_DIR}/lib/" 2>/dev/null || true + cp -P "${NATIVE_LIB_DIR}"/gopher-auth*.dll "${AUTH_EXAMPLE_DIR}/lib/" 2>/dev/null || true + + # Install dependencies + echo -e "${YELLOW} Installing example dependencies...${NC}" + npm install --silent 2>/dev/null || npm install + + # Build TypeScript + echo -e "${YELLOW} Building example TypeScript...${NC}" + npm run build --silent 2>/dev/null || npm run build + + # Run tests + echo -e "${YELLOW} Running example tests...${NC}" + npm test --silent 2>/dev/null && echo -e "${GREEN}✓ Example tests passed${NC}" || echo -e "${YELLOW}⚠ Some example tests may have failed${NC}" + + cd "${SCRIPT_DIR}" + echo -e "${GREEN}✓ Auth example built successfully${NC}" +else + echo -e "${YELLOW}⚠ Auth example directory not found: ${AUTH_EXAMPLE_DIR}${NC}" +fi + echo "" echo -e "${GREEN}======================================${NC}" echo -e "${GREEN}Build completed successfully!${NC}" @@ -219,6 +262,10 @@ echo -e "${GREEN}======================================${NC}" echo "" echo -e "Native libraries: ${YELLOW}${NATIVE_LIB_DIR}${NC}" echo -e "Native headers: ${YELLOW}${NATIVE_INCLUDE_DIR}${NC}" -echo -e "Run tests: ${YELLOW}npm test${NC}" -echo -e "Run example: ${YELLOW}npm run example${NC}" -echo -e "Build: ${YELLOW}npm run build${NC}" +echo -e "Run SDK tests: ${YELLOW}npm test${NC}" +echo -e "Run SDK example: ${YELLOW}npm run example${NC}" +echo -e "Build SDK: ${YELLOW}npm run build${NC}" +echo "" +echo -e "Auth example: ${YELLOW}${AUTH_EXAMPLE_DIR}${NC}" +echo -e "Run auth server: ${YELLOW}cd examples/auth && npm start${NC}" +echo -e "Run auth tests: ${YELLOW}cd examples/auth && npm test${NC}" diff --git a/examples/auth/.gitignore b/examples/auth/.gitignore new file mode 100644 index 00000000..e2d0fffc --- /dev/null +++ b/examples/auth/.gitignore @@ -0,0 +1,31 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Native libraries +lib/*.dylib +lib/*.so +lib/*.dll + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Test coverage +coverage/ + +# Environment +.env +.env.local + +# Logs +*.log +npm-debug.log* diff --git a/examples/auth/DESIGN.md b/examples/auth/DESIGN.md new file mode 100644 index 00000000..9f1e0733 --- /dev/null +++ b/examples/auth/DESIGN.md @@ -0,0 +1,660 @@ +# JavaScript Auth MCP Server Example + +OAuth-protected MCP (Model Context Protocol) server implementation in TypeScript/JavaScript using gopher-auth FFI bindings for JWT token validation. + +## Overview + +This example demonstrates: +- OAuth 2.0 protected MCP server using JSON-RPC 2.0 +- JWT token validation via gopher-auth native library (FFI) +- OAuth discovery endpoints (RFC 9728, RFC 8414, OIDC) +- Scope-based access control for MCP tools +- Integration with Keycloak or compatible OAuth providers + +## Project Structure + +``` +examples/auth/ +├── src/ +│ ├── index.ts # Entry point, Express app setup +│ ├── config.ts # Configuration loader +│ ├── middleware/ +│ │ ├── oauth-auth.ts # JWT validation middleware +│ │ └── __tests__/ +│ │ └── oauth-auth.test.ts +│ ├── routes/ +│ │ ├── health.ts # Health endpoint +│ │ ├── oauth-endpoints.ts # OAuth discovery endpoints +│ │ ├── mcp-handler.ts # JSON-RPC 2.0 handler +│ │ └── __tests__/ +│ │ ├── health.test.ts +│ │ ├── oauth-endpoints.test.ts +│ │ └── mcp-handler.test.ts +│ ├── tools/ +│ │ ├── weather-tools.ts # Example MCP tools +│ │ └── __tests__/ +│ │ └── weather-tools.test.ts +│ └── __tests__/ +│ ├── config.test.ts +│ └── integration.test.ts +├── dist/ # Compiled JavaScript +├── lib/ # Native libraries (libgopher-orch) +├── package.json +├── tsconfig.json +├── jest.config.js +├── server.config # Configuration file +└── README.md +``` + +--- + +## Endpoints Reference + +### Public Endpoints (No Authentication Required) + +#### Health Check + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/health` | Server health monitoring | + +**Response:** +```json +{ + "status": "ok", + "timestamp": "2024-01-15T10:30:00.000Z", + "version": "1.0.0", + "uptime": 123 +} +``` + +--- + +### OAuth Discovery Endpoints + +#### Protected Resource Metadata (RFC 9728) + +| Method | Path | Description | +| ------ | ------------------------------------------- | ------------------------- | +| GET | `/.well-known/oauth-protected-resource` | Resource metadata | +| GET | `/.well-known/oauth-protected-resource/mcp` | Resource-specific variant | +| | | | + +**Response:** +```json +{ + "resource": "http://localhost:3001/mcp", + "authorization_servers": ["https://auth.example.com/realms/mcp"], + "scopes_supported": ["openid", "profile", "email", "mcp:read", "mcp:admin"], + "bearer_methods_supported": ["header", "query"], + "resource_documentation": "http://localhost:3001/docs" +} +``` + +#### Authorization Server Metadata (RFC 8414) + +| Method | Path | Description | +| ------ | ----------------------------------------- | --------------------- | +| GET | `/.well-known/oauth-authorization-server` | OAuth server metadata | + +**Response:** +```json +{ + "issuer": "https://auth.example.com/realms/mcp", + "authorization_endpoint": "https://auth.example.com/realms/mcp/protocol/openid-connect/auth", + "token_endpoint": "https://auth.example.com/realms/mcp/protocol/openid-connect/token", + "jwks_uri": "https://auth.example.com/realms/mcp/protocol/openid-connect/certs", + "registration_endpoint": "http://localhost:3001/oauth/register", + "scopes_supported": ["openid", "profile", "email", "mcp:read", "mcp:admin"], + "response_types_supported": ["code", "token", "id_token", "code token", "code id_token"], + "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"], + "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], + "code_challenge_methods_supported": ["S256", "plain"] +} +``` + +#### OpenID Connect Discovery + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/.well-known/openid-configuration` | OIDC discovery | + +**Response:** Extends RFC 8414 with: +```json +{ + "...": "...RFC 8414 fields...", + "userinfo_endpoint": "https://auth.example.com/realms/mcp/protocol/openid-connect/userinfo", + "subject_types_supported": ["public"], + "id_token_signing_alg_values_supported": ["RS256"] +} +``` + +#### Authorization Redirect + +| Method | Path | Description | +| ------ | ------------------ | ----------------------------------- | +| GET | `/oauth/authorize` | Redirects to authorization endpoint | + +Forwards all query parameters to the configured `oauth_authorize_url`. Returns HTTP 302 redirect. + +#### Dynamic Client Registration (RFC 7591) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/oauth/register` | Client registration (stateless mode) | + +**Request:** +```json +{ + "redirect_uris": ["http://localhost:8080/callback"] +} +``` + +**Response:** +```json +{ + "client_id": "mcp-client-id", + "client_secret": "mcp-client-secret", + "client_id_issued_at": 1705312200, + "client_secret_expires_at": 0, + "redirect_uris": ["http://localhost:8080/callback"], + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "token_endpoint_auth_method": "client_secret_basic" +} +``` + +--- + +### Protected Endpoints (Authentication Required) + +#### MCP JSON-RPC Handler + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/mcp` | MCP JSON-RPC 2.0 endpoint | +| POST | `/rpc` | Alias for /mcp | +| OPTIONS | `/mcp` | CORS preflight | +| OPTIONS | `/rpc` | CORS preflight | + +--- + +## MCP JSON-RPC Methods + +### `initialize` + +Initialize MCP protocol session. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + } +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "auth-mcp-server", + "version": "1.0.0" + } + } +} +``` + +### `tools/list` + +List available tools. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/list", + "params": {} +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "name": "get-weather", + "description": "Get current weather for a city", + "inputSchema": { + "type": "object", + "properties": { + "city": { + "type": "string", + "description": "City name" + } + }, + "required": ["city"] + } + }, + { + "name": "get-forecast", + "description": "Get 5-day forecast (requires mcp:read scope)", + "inputSchema": { "..." } + }, + { + "name": "get-weather-alerts", + "description": "Get weather alerts (requires mcp:admin scope)", + "inputSchema": { "..." } + } + ] + } +} +``` + +### `tools/call` + +Invoke a tool. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "get-weather", + "arguments": { + "city": "Seattle" + } + } +} +``` + +**Response (Success):** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "{\"city\":\"Seattle\",\"temperature\":20,\"condition\":\"Sunny\",\"humidity\":65,\"windSpeed\":12}" + } + ] + } +} +``` + +**Response (Access Denied):** +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "content": [ + { + "type": "text", + "text": "{\"error\":\"access_denied\",\"message\":\"Access denied. Required scope: mcp:read\"}" + } + ], + "isError": true + } +} +``` + +### `ping` + +Health check method. + +**Request:** +```json +{ + "jsonrpc": "2.0", + "id": 4, + "method": "ping", + "params": {} +} +``` + +**Response:** +```json +{ + "jsonrpc": "2.0", + "id": 4, + "result": {} +} +``` + +### JSON-RPC Error Codes + +| Code | Name | Description | +|------|------|-------------| +| -32700 | Parse Error | Invalid JSON | +| -32600 | Invalid Request | Invalid JSON-RPC request | +| -32601 | Method Not Found | Method does not exist | +| -32602 | Invalid Params | Invalid method parameters | +| -32603 | Internal Error | Server error | + +--- + +## Available Tools & Scopes + +| Tool | Description | Required Scope | +|------|-------------|----------------| +| `get-weather` | Current weather for a city | None (public) | +| `get-forecast` | 5-day weather forecast | `mcp:read` | +| `get-weather-alerts` | Weather alerts for a region | `mcp:admin` | + +### Scope Hierarchy + +``` +openid - Standard OIDC scope +profile - User profile information +email - User email +mcp:read - Read access to MCP tools (forecast) +mcp:admin - Admin access to MCP tools (alerts) +``` + +--- + +## OAuth Flow + +### Complete Authentication Flow + +``` +┌──────────┐ ┌─────────────────┐ ┌──────────────┐ +│ Client │ │ MCP Server │ │ OAuth/IDP │ +└────┬─────┘ └───────┬─────────┘ └──────┬───────┘ + │ │ │ + │ GET /.well-known/oauth-protected-resource│ + │──────────────────>│ │ + │ { authorization_servers: [...] } │ + │<──────────────────│ │ + │ │ │ + │ GET /.well-known/oauth-authorization-server + │──────────────────>│ │ + │ { authorization_endpoint, token_endpoint, ... } + │<──────────────────│ │ + │ │ │ + │ GET /oauth/authorize?response_type=code&... + │──────────────────>│ │ + │ HTTP 302 Redirect │ │ + │<──────────────────│ │ + │ │ │ + │ Redirect to authorization_endpoint │ + │─────────────────────────────────────────>│ + │ │ User authenticates│ + │ Redirect with authorization code │ + │<─────────────────────────────────────────│ + │ │ │ + │ POST token_endpoint (exchange code) │ + │─────────────────────────────────────────>│ + │ │ Access token │ + │<─────────────────────────────────────────│ + │ │ │ + │ POST /mcp with Bearer token │ + │──────────────────>│ │ + │ │ Validate JWT (JWKS) │ + │ │─────────────────────>│ + │ │ Token valid │ + │ │<─────────────────────│ + │ Tool response │ │ + │<──────────────────│ │ +``` + +### Token Validation Flow + +``` +1. Extract token from Authorization header or query parameter + └─ Authorization: Bearer + └─ ?access_token= + +2. Verify token signature against JWKS + └─ Fetch JWKS from configured jwks_uri + └─ Cache JWKS for configured duration + +3. Check token expiration + └─ Apply clock skew tolerance (default: 30s) + +4. Extract JWT claims + └─ subject (sub) → userId + └─ scope → scopes (space-separated) + └─ aud → audience + └─ exp → tokenExpiry + +5. Attach auth context to request + └─ { userId, scopes, audience, tokenExpiry, authenticated: true } + +6. Route to handler + └─ Handler checks required scope at tool invocation time +``` + +### Path-Based Access Control + +| Path Pattern | Authentication | +|--------------|----------------| +| `/.well-known/*` | Not required | +| `/oauth/*` | Not required | +| `/health` | Not required | +| `/authorize` | Not required | +| `/mcp/*` | Required | +| `/rpc/*` | Required | +| `/events/*` | Required | +| `/sse/*` | Required | +| Other | Required (default) | + +--- + +## Configuration + +### Configuration File Format + +Key=value pairs (INI-style): +```ini +# Comments start with # +host=0.0.0.0 +port=3001 +auth_server_url=https://auth.example.com/realms/mcp +``` + +### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `host` | string | `0.0.0.0` | Server bind address | +| `port` | number | `3001` | Server port | +| `server_url` | string | `http://localhost:{port}` | Public server URL | +| `auth_server_url` | string | - | OAuth provider base URL | +| `jwks_uri` | string | Derived | JWKS endpoint URL | +| `issuer` | string | Derived | Expected token issuer | +| `client_id` | string | - | OAuth client ID | +| `client_secret` | string | - | OAuth client secret | +| `oauth_authorize_url` | string | Derived | Authorization endpoint | +| `oauth_token_url` | string | Derived | Token endpoint | +| `allowed_scopes` | string | `openid profile email mcp:read mcp:admin` | Allowed scopes | +| `jwks_cache_duration` | number | `3600` | JWKS cache TTL (seconds) | +| `jwks_auto_refresh` | boolean | `true` | Auto-refresh JWKS | +| `request_timeout` | number | `30` | HTTP timeout (seconds) | +| `auth_disabled` | boolean | `false` | Disable authentication | + +### Endpoint Derivation + +When `auth_server_url` is provided, endpoints are derived automatically: + +``` +auth_server_url = https://auth.example.com/realms/mcp + +Derived endpoints: +├── jwks_uri = {auth_server_url}/protocol/openid-connect/certs +├── issuer = {auth_server_url} +├── oauth_authorize_url = {auth_server_url}/protocol/openid-connect/auth +└── oauth_token_url = {auth_server_url}/protocol/openid-connect/token +``` + +### Example Configuration + +```ini +# Server settings +host=0.0.0.0 +port=3001 +server_url=https://mcp.example.com + +# OAuth/IDP settings (Keycloak) +auth_server_url=https://keycloak.example.com/realms/mcp +client_id=mcp-client +client_secret=your-client-secret + +# Scopes +allowed_scopes=openid profile email mcp:read mcp:admin + +# Cache settings +jwks_cache_duration=3600 +jwks_auto_refresh=true +request_timeout=30 + +# Development mode (disable for production) +auth_disabled=false +``` + +--- + +## Running the Server + +### Development Mode + +```bash +# Install dependencies +npm install + +# Build TypeScript +npm run build + +# Run with auth disabled +npm start -- --no-auth + +# Run with auth enabled +npm start +``` + +### Testing + +```bash +# Run all tests +npm test + +# Run with coverage +npm run test:coverage + +# Run specific test file +npm test -- routes/mcp-handler.test.ts +``` + +### Testing Endpoints + +```bash +# Health check +curl http://localhost:3001/health + +# OAuth discovery +curl http://localhost:3001/.well-known/oauth-protected-resource +curl http://localhost:3001/.well-known/oauth-authorization-server + +# MCP initialize (no auth required for initialize) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' + +# List tools +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' + +# Call public tool (no auth) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get-weather","arguments":{"city":"Seattle"}}}' + +# Call protected tool (with auth) +TOKEN="your-jwt-token" +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get-forecast","arguments":{"city":"Portland"}}}' +``` + +--- + +## Dependencies + +| Package | Purpose | +|---------|---------| +| `@gopher.security/gopher-mcp-js` | FFI bindings for gopher-auth | +| `express` | Web framework | +| `typescript` | Type-safe development | +| `jest` | Testing framework | +| `supertest` | HTTP testing | + +--- + +## Security Considerations + +1. **Token Validation**: All JWT tokens are validated against the JWKS endpoint +2. **Signature Verification**: RS256 signature verification using public keys +3. **Expiration Checks**: Tokens are checked for expiration with clock skew tolerance +4. **Scope Enforcement**: Tool-level scope checking prevents unauthorized access +5. **CORS**: Configurable CORS headers for browser clients +6. **HTTPS**: Use HTTPS in production for token security + +--- + +## Troubleshooting + +### Library Loading Errors + +``` +RuntimeError: Auth functions not available +``` + +**Solution:** Ensure native library is compiled and accessible: +- Copy `libgopher-orch.dylib` (macOS) or `libgopher-orch.so` (Linux) to `./lib/` +- Or set `GOPHER_ORCH_LIBRARY_PATH` environment variable + +### Token Validation Failures + +``` +401 Unauthorized: Token validation failed +``` + +**Causes:** +- Token expired - obtain a new token +- Invalid issuer - check `issuer` in config matches token +- JWKS fetch failed - verify `jwks_uri` is accessible +- Invalid signature - ensure correct JWKS endpoint + +### Scope Access Denied + +```json +{"error":"access_denied","message":"Required scope: mcp:read"} +``` + +**Solution:** Request additional scopes during token acquisition from the OAuth provider. diff --git a/examples/auth/README.md b/examples/auth/README.md new file mode 100644 index 00000000..de355a88 --- /dev/null +++ b/examples/auth/README.md @@ -0,0 +1,364 @@ +# JS Auth MCP Server Example + +OAuth-protected MCP (Model Context Protocol) server implementation in TypeScript/Node.js using gopher-auth FFI bindings for JWT token validation. + +## Overview + +This example demonstrates: +- OAuth 2.0 protected MCP server using JSON-RPC 2.0 +- JWT token validation via gopher-auth native library (FFI) +- OAuth discovery endpoints (RFC 9728, RFC 8414, OIDC) +- Scope-based access control for MCP tools +- Integration with Keycloak or compatible OAuth providers + +## Prerequisites + +- Node.js 18+ +- npm or yarn +- Keycloak or compatible OAuth 2.0 server (optional, for auth testing) + +## Installation + +```bash +# Install dependencies (native library is automatically downloaded) +npm install + +# Build TypeScript +npm run build +``` + +The `@gopher.security/gopher-mcp-js` npm package automatically downloads the appropriate native library for your platform: +- macOS (arm64, x64) +- Linux (arm64, x64) +- Windows (arm64, x64) + +## Quick Start + +```bash +# Run the example (uses server.config settings) +./run_example.sh + +# Or run without authentication (development mode) +./run_example.sh --no-auth + +# Show help +./run_example.sh --help +``` + +## Configuration + +Create or modify `server.config`: + +### Auth Disabled Mode (Development/Testing) + +```ini +# Server settings +host=0.0.0.0 +port=3001 +server_url=http://localhost:3001 + +# Disable auth for development +auth_disabled=true +``` + +### Auth Enabled Mode (Production) + +```ini +# Server settings +host=0.0.0.0 +port=3001 +server_url=http://localhost:3001 + +# OAuth/IDP settings (Keycloak example) +auth_server_url=https://keycloak.example.com/realms/mcp +client_id=mcp-client +client_secret=your-client-secret + +# Optional: Override derived endpoints +# jwks_uri=https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs +# issuer=https://keycloak.example.com/realms/mcp +# oauth_authorize_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth +# oauth_token_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/token + +# Token validation settings +allowed_scopes=openid profile email mcp:read mcp:admin +jwks_cache_duration=3600 +jwks_auto_refresh=true +request_timeout=30 +``` + +### Configuration Options + +| Option | Description | Default | +|--------|-------------|---------| +| `host` | Server bind address | `0.0.0.0` | +| `port` | Server port | `3001` | +| `server_url` | Public server URL | `http://localhost:{port}` | +| `auth_server_url` | OAuth server base URL | - | +| `jwks_uri` | JWKS endpoint URL | Derived from auth_server_url | +| `issuer` | Expected token issuer | Derived from auth_server_url | +| `client_id` | OAuth client ID | - | +| `client_secret` | OAuth client secret | - | +| `allowed_scopes` | Space-separated allowed scopes | `openid profile email mcp:read mcp:admin` | +| `jwks_cache_duration` | JWKS cache TTL in seconds | `3600` | +| `jwks_auto_refresh` | Auto-refresh JWKS before expiry | `true` | +| `request_timeout` | HTTP request timeout in seconds | `30` | +| `auth_disabled` | Disable authentication entirely | `false` | + +## Running the Server + +### Using run_example.sh (Recommended) + +```bash +# Run using server.config settings +./run_example.sh + +# Run without authentication (development mode) +./run_example.sh --no-auth + +# Show help +./run_example.sh --help +``` + +### Using npm scripts + +```bash +# Development mode (ts-node) +npm run dev + +# Development mode without auth +npm run dev:no-auth + +# Production mode (compiled JavaScript) +npm run build +npm start + +# Production mode without auth +npm run start:no-auth +``` + +## Testing + +```bash +# Run all tests +npm test + +# Run tests with coverage +npm test -- --coverage + +# Run specific test file +npm test -- src/__tests__/integration.test.ts +``` + +## API Endpoints + +### Health Check + +```bash +curl http://localhost:3001/health +``` + +Response: +```json +{ + "status": "ok", + "timestamp": "2024-01-15T10:30:00.000Z", + "version": "1.0.0", + "uptime": 123 +} +``` + +### OAuth Discovery + +```bash +# Protected Resource Metadata (RFC 9728) +curl http://localhost:3001/.well-known/oauth-protected-resource + +# Authorization Server Metadata (RFC 8414) +curl http://localhost:3001/.well-known/oauth-authorization-server + +# OpenID Configuration +curl http://localhost:3001/.well-known/openid-configuration +``` + +### MCP Tools + +#### Without Authentication (auth_disabled=true) + +```bash +# List available tools +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' + +# Get weather (no auth required) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get-weather", "arguments": {"city": "Seattle"}}}' +``` + +#### With Authentication + +```bash +# Get an access token from your OAuth provider first +TOKEN="your-jwt-token" + +# Get forecast (requires mcp:read scope) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get-forecast", "arguments": {"city": "Portland"}}}' + +# Get weather alerts (requires mcp:admin scope) +curl -X POST http://localhost:3001/mcp \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get-weather-alerts", "arguments": {"region": "Pacific Northwest"}}}' +``` + +## Available Tools + +| Tool | Description | Required Scope | +|------|-------------|----------------| +| `get-weather` | Current weather for a city | None | +| `get-forecast` | 5-day forecast for a city | `mcp:read` | +| `get-weather-alerts` | Weather alerts for a region | `mcp:admin` | + +## OAuth Flow + +``` +┌─────────┐ ┌──────────────┐ ┌─────────────┐ +│ Client │ │ MCP Server │ │ Keycloak │ +└────┬────┘ └──────┬───────┘ └──────┬──────┘ + │ │ │ + │ GET /.well-known/oauth-protected-resource + │─────────────────> │ + │ { authorization_servers: [...] } │ + │<───────────────── │ + │ │ │ + │ GET /.well-known/oauth-authorization-server + │─────────────────> │ + │ { authorization_endpoint, token_endpoint, ... } + │<───────────────── │ + │ │ │ + │ Redirect to authorization_endpoint │ + │─────────────────────────────────────>│ + │ │ User authenticates + │ Redirect with auth code │ + │<─────────────────────────────────────│ + │ │ │ + │ POST token_endpoint (exchange code) │ + │─────────────────────────────────────>│ + │ │ Access token │ + │<─────────────────────────────────────│ + │ │ │ + │ POST /mcp with Bearer token │ + │─────────────────> │ + │ │ Validate JWT │ + │ │───────────────────>│ + │ │ Token valid │ + │ │<───────────────────│ + │ Tool response │ │ + │<───────────────── │ +``` + +## Obtaining a Test Token + +### Using Keycloak Direct Access Grant + +```bash +# Get token using password grant (for testing only) +curl -X POST https://keycloak.example.com/realms/mcp/protocol/openid-connect/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=password" \ + -d "client_id=mcp-client" \ + -d "client_secret=your-secret" \ + -d "username=testuser" \ + -d "password=testpassword" \ + -d "scope=openid profile mcp:read mcp:admin" +``` + +### Using Authorization Code Flow (Recommended) + +1. Start the MCP server +2. Navigate to `http://localhost:3001/oauth/authorize?client_id=your-client&response_type=code&redirect_uri=your-callback` +3. Complete login in Keycloak +4. Exchange the authorization code for tokens + +## Troubleshooting + +### Library Loading Errors + +``` +Error: Cannot load library +``` + +**Solutions:** +- Run `npm install` again to re-download the native library +- Check that your platform is supported (macOS/Linux/Windows, arm64/x64) +- Set `GOPHER_ORCH_LIBRARY_PATH` environment variable to custom library location + +### Token Validation Failures + +``` +401 Unauthorized: Token validation failed +``` + +**Causes:** +- Token expired - obtain a new token +- Invalid issuer - check `issuer` in config matches token +- JWKS fetch failed - verify `jwks_uri` is accessible +- Invalid audience - ensure token has correct audience claim + +### JWKS Fetch Errors + +``` +Error: JWKS fetch failed +``` + +**Solutions:** +- Verify `jwks_uri` is correct and accessible +- Check network connectivity to OAuth server +- Increase `request_timeout` if needed + +### Scope Access Denied + +``` +{"error": "access_denied", "message": "Required scope: mcp:read"} +``` + +**Solution:** Ensure your token includes the required scope. Request additional scopes during token acquisition. + +## Project Structure + +``` +examples/auth/ +├── src/ +│ ├── middleware/ +│ │ └── oauth-auth.ts # OAuth middleware +│ ├── routes/ +│ │ ├── health.ts # Health endpoint +│ │ ├── oauth-endpoints.ts # Discovery endpoints +│ │ └── mcp-handler.ts # JSON-RPC handler +│ ├── tools/ +│ │ └── weather-tools.ts # Example tools +│ ├── config.ts # Configuration loader +│ └── index.ts # Entry point +├── dist/ # Compiled JavaScript +├── package.json +├── tsconfig.json +├── run_example.sh # Convenience run script +├── server.config # Server configuration +└── README.md +``` + +## Dependencies + +The example uses `@gopher.security/gopher-mcp-js` which provides: +- FFI bindings for gopher-auth native library +- Automatic native library download for supported platforms +- TypeScript type definitions + +## License + +See the main gopher-mcp-js repository for license information. diff --git a/examples/auth/jest.config.js b/examples/auth/jest.config.js new file mode 100644 index 00000000..e510c188 --- /dev/null +++ b/examples/auth/jest.config.js @@ -0,0 +1,15 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.ts'], + moduleFileExtensions: ['ts', 'js', 'json'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/__tests__/**', + '!src/index.ts' + ], + coverageDirectory: 'coverage', + verbose: true +}; diff --git a/examples/auth/package-lock.json b/examples/auth/package-lock.json new file mode 100644 index 00000000..f0ffa786 --- /dev/null +++ b/examples/auth/package-lock.json @@ -0,0 +1,5355 @@ +{ + "name": "@gopher-mcp-js/auth-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gopher-mcp-js/auth-example", + "version": "1.0.0", + "dependencies": { + "@gopher.security/gopher-mcp-js": "^0.1.2", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.0", + "@types/supertest": "^2.0.16", + "jest": "^29.7.0", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.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/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "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": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@gopher.security/gopher-mcp-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-mcp-js/-/gopher-mcp-js-0.1.2.tgz", + "integrity": "sha512-XWwARmO8qcTG6d1lLnCzwp5CuYUuGyLrSqJly3uVO17UqsCj98hRUor9c/2PDw/+zcN9sqYzRsXRkOpfd2UZlA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "koffi": "^2.9.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@gopher.security/gopher-orch-darwin-arm64": "0.1.2", + "@gopher.security/gopher-orch-darwin-x64": "0.1.2", + "@gopher.security/gopher-orch-linux-arm64": "0.1.2", + "@gopher.security/gopher-orch-linux-x64": "0.1.2", + "@gopher.security/gopher-orch-win32-arm64": "0.1.2", + "@gopher.security/gopher-orch-win32-x64": "0.1.2" + } + }, + "node_modules/@gopher.security/gopher-orch-darwin-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-darwin-arm64/-/gopher-orch-darwin-arm64-0.1.2.tgz", + "integrity": "sha512-v6UeZ+Mv7W/WwTnnAbxI97FrCt2ugXtXoTEwP+jyNSF4p/GgnsjFHkiEmlp1XZDwfJLwi9av4DqkB1cdohIHqA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@gopher.security/gopher-orch-darwin-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-darwin-x64/-/gopher-orch-darwin-x64-0.1.2.tgz", + "integrity": "sha512-Y5PVZRxmivBjbhxSOVNWAA58AClmJgT3yb4lxBN9uU1NZlR1j2FZCQiV0i/rkbAZOJA3OSVsW4qI1yAzVo5NJA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@gopher.security/gopher-orch-linux-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-linux-arm64/-/gopher-orch-linux-arm64-0.1.2.tgz", + "integrity": "sha512-rc6Sie2I5w6cOctkniJ7ekYgD5NzWBgeUF6WvaBXigFBv5Vl2ITn1awJFTGolfWXhxNZO0rtCVoCZ4c6cm6iSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@gopher.security/gopher-orch-linux-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-linux-x64/-/gopher-orch-linux-x64-0.1.2.tgz", + "integrity": "sha512-mrUXFNBF6L2Jwi/cIeD9dEEGpVwb6a3/8fMuFVdKe543QAwCVGv9PjIdYaYnvaVvtX42XRb3Qmi0SrGhF1xoag==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@gopher.security/gopher-orch-win32-arm64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-win32-arm64/-/gopher-orch-win32-arm64-0.1.2.tgz", + "integrity": "sha512-fIOC9fcGwwUlOPVv491Ncf/nh1yAq9xWPZvWAzgGFh5Agzm64PamlIJShClSDBwXjpydUNrQPicLXLEloyr7Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@gopher.security/gopher-orch-win32-x64": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gopher.security/gopher-orch-win32-x64/-/gopher-orch-win32-x64-0.1.2.tgz", + "integrity": "sha512-ISI33BVNkx3oveocizoPnn/9swvjsW/bu9zCkb9EpiFCK4fLVltyoyM0FPcWAhipm6YA5tJMM0COqD7cRmRtUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@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/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.16.tgz", + "integrity": "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/superagent": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.5.tgz", + "integrity": "sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0", + "qs": "^6.11.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "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-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "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/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/koffi": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.2.tgz", + "integrity": "sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "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": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/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-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", + "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^2.1.2", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=6.4.0 <13 || >=14" + } + }, + "node_modules/superagent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/superagent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/supertest": { + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz", + "integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==", + "deprecated": "Please upgrade to supertest v7.1.3+, see release notes at https://github.com/forwardemail/supertest/releases/tag/v7.1.3 - maintenance is supported by Forward Email @ https://forwardemail.net", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^8.1.2" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/auth/package.json b/examples/auth/package.json new file mode 100644 index 00000000..6388ce16 --- /dev/null +++ b/examples/auth/package.json @@ -0,0 +1,35 @@ +{ + "name": "@gopher-mcp-js/auth-example", + "version": "1.0.0", + "description": "JavaScript MCP server with OAuth authentication using gopher-orch auth via FFI", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "start:no-auth": "node dist/index.js --no-auth", + "dev": "ts-node src/index.ts", + "dev:no-auth": "ts-node src/index.ts --no-auth", + "test": "jest", + "test:watch": "jest --watch", + "test:coverage": "jest --coverage", + "clean": "rm -rf dist" + }, + "dependencies": { + "@gopher.security/gopher-mcp-js": "^0.1.2", + "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.0", + "@types/supertest": "^2.0.16", + "jest": "^29.7.0", + "supertest": "^6.3.3", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/examples/auth/run_example.sh b/examples/auth/run_example.sh new file mode 100755 index 00000000..990fdfa2 --- /dev/null +++ b/examples/auth/run_example.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# Run the Auth MCP Server example +# Usage: +# ./run_example.sh # Run using server.config settings +# ./run_example.sh --no-auth # Override config to disable auth +# ./run_example.sh --help # Show help + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# Get the script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +# Check for help flag +if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + echo "Auth MCP Server Example (JavaScript)" + echo "" + echo "Usage:" + echo " ./run_example.sh Run using server.config settings" + echo " ./run_example.sh --no-auth Override config to disable auth" + echo " ./run_example.sh --help Show this help" + echo "" + echo "Options:" + echo " --no-auth Disable OAuth authentication (overrides server.config)" + echo "" + echo "Configuration:" + echo " Edit server.config to configure OAuth settings (auth_disabled=true/false)" + echo "" + echo "Test endpoints:" + echo " curl http://localhost:3001/health" + echo " curl -X POST http://localhost:3001/mcp \\" + echo " -H 'Content-Type: application/json' \\" + echo " -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'" + exit 0 +fi + +# Check Node.js +if ! command -v node &> /dev/null; then + echo -e "${RED}Error: Node.js not found${NC}" + echo "Please install Node.js 18+ first" + exit 1 +fi + +# Check Node.js version +NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1) +if [ "$NODE_VERSION" -lt 18 ]; then + echo -e "${RED}Error: Node.js 18+ required. Current version: $(node -v)${NC}" + exit 1 +fi + +# Check if node_modules exists, install if not +if [ ! -d "node_modules" ]; then + echo -e "${YELLOW}Installing dependencies...${NC}" + npm install +fi + +# Check if dist exists, build if not +if [ ! -d "dist" ]; then + echo -e "${YELLOW}Building TypeScript...${NC}" + npm run build +fi + +echo -e "${GREEN}Starting Auth MCP Server...${NC}" +echo -e "Configuration: ${YELLOW}server.config${NC}" +echo "" + +# Run server with arguments +if [ "$1" = "--no-auth" ]; then + exec npm run start:no-auth +else + exec npm run start -- "$@" +fi diff --git a/examples/auth/server.config b/examples/auth/server.config new file mode 100644 index 00000000..c2c814b0 --- /dev/null +++ b/examples/auth/server.config @@ -0,0 +1,33 @@ +# Auth MCP Server Configuration +# This file follows the same format as the C++ auth example + +# Server settings +host=0.0.0.0 +port=3001 +server_url=https://marni-nightcapped-nonmeditatively.ngrok-free.dev + +# OAuth/IDP settings +# Uncomment and configure for Keycloak or other OAuth provider +client_id=oauth_0a650b79c5a64c3b920ae8c2b20599d9 +client_secret=6BiU2beUi2wIBxY3MUBLyYqoWKa4t0U_kJVm9mvSOKw +auth_server_url=https://auth-test.gopher.security/realms/gopher-mcp-auth +oauth_authorize_url=https://api-test.gopher.security/oauth/authorize + +# Direct OAuth endpoint URLs (optional, derived from auth_server_url if not set) +# jwks_uri=https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs +# issuer=https://keycloak.example.com/realms/mcp +# oauth_authorize_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth +# oauth_token_url=https://keycloak.example.com/realms/mcp/protocol/openid-connect/token + +# Scopes +exchange_idps=oauth-idp-714982830194556929-google +allowed_scopes=openid profile email scope-001 + +# Cache settings +jwks_cache_duration=3600 +jwks_auto_refresh=true +request_timeout=30 + +# Auth bypass mode (for development/testing) +# Set to true to disable authentication +auth_disabled=false diff --git a/examples/auth/src/__tests__/config.test.ts b/examples/auth/src/__tests__/config.test.ts new file mode 100644 index 00000000..9eea496f --- /dev/null +++ b/examples/auth/src/__tests__/config.test.ts @@ -0,0 +1,321 @@ +import fs from 'fs'; +import path from 'path'; +import { + parseConfigFile, + loadConfigFromFile, + buildConfig, + createDefaultConfig, + AuthServerConfig, +} from '../config'; + +describe('parseConfigFile', () => { + it('should parse key=value pairs', () => { + const content = ` +host=localhost +port=3001 +server_url=http://localhost:3001 +`; + const result = parseConfigFile(content); + expect(result).toEqual({ + host: 'localhost', + port: '3001', + server_url: 'http://localhost:3001', + }); + }); + + it('should skip empty lines', () => { + const content = ` +host=localhost + +port=3001 + +`; + const result = parseConfigFile(content); + expect(result).toEqual({ + host: 'localhost', + port: '3001', + }); + }); + + it('should skip comment lines starting with #', () => { + const content = ` +# This is a comment +host=localhost +# Another comment +port=3001 +`; + const result = parseConfigFile(content); + expect(result).toEqual({ + host: 'localhost', + port: '3001', + }); + }); + + it('should handle values containing = characters', () => { + const content = ` +url=http://example.com?foo=bar&baz=qux +`; + const result = parseConfigFile(content); + expect(result.url).toBe('http://example.com?foo=bar&baz=qux'); + }); + + it('should trim whitespace from keys and values', () => { + const content = ` + host = localhost + port=3001 +`; + const result = parseConfigFile(content); + expect(result.host).toBe('localhost'); + expect(result.port).toBe('3001'); + }); + + it('should skip lines without = separator', () => { + const content = ` +host=localhost +invalid line without separator +port=3001 +`; + const result = parseConfigFile(content); + expect(result).toEqual({ + host: 'localhost', + port: '3001', + }); + }); + + it('should handle empty values', () => { + const content = ` +host=localhost +empty_value= +port=3001 +`; + const result = parseConfigFile(content); + expect(result.empty_value).toBe(''); + }); + + it('should return empty object for empty content', () => { + const result = parseConfigFile(''); + expect(result).toEqual({}); + }); + + it('should return empty object for content with only comments', () => { + const content = ` +# Comment 1 +# Comment 2 +`; + const result = parseConfigFile(content); + expect(result).toEqual({}); + }); +}); + +describe('buildConfig', () => { + it('should build config with default values', () => { + const configMap = { + auth_disabled: 'true', + }; + const config = buildConfig(configMap); + + expect(config.host).toBe('0.0.0.0'); + expect(config.port).toBe(3001); + expect(config.serverUrl).toBe('http://localhost:3001'); + expect(config.authDisabled).toBe(true); + }); + + it('should use provided values', () => { + const configMap = { + host: '127.0.0.1', + port: '8080', + server_url: 'http://myserver.com', + auth_disabled: 'true', + }; + const config = buildConfig(configMap); + + expect(config.host).toBe('127.0.0.1'); + expect(config.port).toBe(8080); + expect(config.serverUrl).toBe('http://myserver.com'); + }); + + it('should derive server_url from port if not provided', () => { + const configMap = { + port: '9000', + auth_disabled: 'true', + }; + const config = buildConfig(configMap); + + expect(config.serverUrl).toBe('http://localhost:9000'); + }); + + it('should derive OAuth endpoints from auth_server_url', () => { + const configMap = { + auth_server_url: 'https://keycloak.example.com/realms/mcp', + client_id: 'test-client', + client_secret: 'test-secret', + }; + const config = buildConfig(configMap); + + expect(config.jwksUri).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs' + ); + expect(config.tokenEndpoint).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/token' + ); + expect(config.issuer).toBe('https://keycloak.example.com/realms/mcp'); + expect(config.oauthAuthorizeUrl).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth' + ); + expect(config.oauthTokenUrl).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/token' + ); + }); + + it('should not override explicitly set endpoints', () => { + const configMap = { + auth_server_url: 'https://keycloak.example.com/realms/mcp', + jwks_uri: 'https://custom.example.com/jwks', + issuer: 'https://custom-issuer.example.com', + client_id: 'test-client', + client_secret: 'test-secret', + }; + const config = buildConfig(configMap); + + expect(config.jwksUri).toBe('https://custom.example.com/jwks'); + expect(config.issuer).toBe('https://custom-issuer.example.com'); + }); + + it('should parse cache settings', () => { + const configMap = { + auth_disabled: 'true', + jwks_cache_duration: '7200', + jwks_auto_refresh: 'false', + request_timeout: '60', + }; + const config = buildConfig(configMap); + + expect(config.jwksCacheDuration).toBe(7200); + expect(config.jwksAutoRefresh).toBe(false); + expect(config.requestTimeout).toBe(60); + }); + + it('should default jwks_auto_refresh to true', () => { + const configMap = { + auth_disabled: 'true', + }; + const config = buildConfig(configMap); + + expect(config.jwksAutoRefresh).toBe(true); + }); + + it('should parse allowed_scopes', () => { + const configMap = { + auth_disabled: 'true', + allowed_scopes: 'openid custom:scope', + }; + const config = buildConfig(configMap); + + expect(config.allowedScopes).toBe('openid custom:scope'); + }); + + it('should throw error when auth enabled and client_id missing', () => { + const configMap = { + auth_server_url: 'https://keycloak.example.com', + client_secret: 'secret', + }; + + expect(() => buildConfig(configMap)).toThrow('client_id is required'); + }); + + it('should throw error when auth enabled and client_secret missing', () => { + const configMap = { + auth_server_url: 'https://keycloak.example.com', + client_id: 'client', + }; + + expect(() => buildConfig(configMap)).toThrow('client_secret is required'); + }); + + it('should throw error when auth enabled and no jwks_uri or auth_server_url', () => { + const configMap = { + client_id: 'client', + client_secret: 'secret', + }; + + expect(() => buildConfig(configMap)).toThrow( + 'jwks_uri or auth_server_url is required' + ); + }); + + it('should not throw when auth is disabled', () => { + const configMap = { + auth_disabled: 'true', + }; + + expect(() => buildConfig(configMap)).not.toThrow(); + }); +}); + +describe('loadConfigFromFile', () => { + const testConfigPath = path.join(__dirname, 'test-config.tmp'); + + afterEach(() => { + if (fs.existsSync(testConfigPath)) { + fs.unlinkSync(testConfigPath); + } + }); + + it('should load and parse config from file', () => { + const content = ` +host=127.0.0.1 +port=8080 +auth_disabled=true +`; + fs.writeFileSync(testConfigPath, content); + + const config = loadConfigFromFile(testConfigPath); + + expect(config.host).toBe('127.0.0.1'); + expect(config.port).toBe(8080); + expect(config.authDisabled).toBe(true); + }); + + it('should throw error if file does not exist', () => { + expect(() => loadConfigFromFile('/nonexistent/path/config')).toThrow( + 'Config file not found' + ); + }); +}); + +describe('createDefaultConfig', () => { + it('should create config with all defaults', () => { + const config = createDefaultConfig(); + + expect(config.host).toBe('0.0.0.0'); + expect(config.port).toBe(3001); + expect(config.serverUrl).toBe('http://localhost:3001'); + expect(config.authDisabled).toBe(true); + expect(config.allowedScopes).toBe( + 'openid profile email mcp:read mcp:admin' + ); + }); + + it('should allow overriding specific fields', () => { + const config = createDefaultConfig({ + port: 9000, + host: '192.168.1.1', + }); + + expect(config.port).toBe(9000); + expect(config.host).toBe('192.168.1.1'); + expect(config.serverUrl).toBe('http://localhost:3001'); // not auto-derived + }); + + it('should allow enabling auth with overrides', () => { + const config = createDefaultConfig({ + authDisabled: false, + clientId: 'test', + clientSecret: 'secret', + jwksUri: 'https://example.com/jwks', + }); + + expect(config.authDisabled).toBe(false); + expect(config.clientId).toBe('test'); + }); +}); diff --git a/examples/auth/src/__tests__/integration.test.ts b/examples/auth/src/__tests__/integration.test.ts new file mode 100644 index 00000000..93a89b31 --- /dev/null +++ b/examples/auth/src/__tests__/integration.test.ts @@ -0,0 +1,443 @@ +/** + * Integration Tests + * + * End-to-end tests for the JS auth MCP server. + * Uses auth_disabled mode for testing without native library. + */ + +import express, { Express } from 'express'; +import request from 'supertest'; +import { createDefaultConfig, AuthServerConfig } from '../config'; +import { registerHealthEndpoint } from '../routes/health'; +import { registerOAuthEndpoints } from '../routes/oauth-endpoints'; +import { registerMcpHandler, McpHandler } from '../routes/mcp-handler'; +import { OAuthAuthMiddleware } from '../middleware/oauth-auth'; +import { registerWeatherTools } from '../tools/weather-tools'; + +describe('Integration Tests', () => { + let app: Express; + let config: AuthServerConfig; + let mcpHandler: McpHandler; + let authMiddleware: OAuthAuthMiddleware; + + beforeEach(() => { + // Create app with auth disabled + app = express(); + app.use(express.json()); + + config = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authServerUrl: 'https://keycloak.example.com/realms/mcp', + issuer: 'https://keycloak.example.com/realms/mcp', + jwksUri: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs', + oauthAuthorizeUrl: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth', + oauthTokenUrl: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/token', + allowedScopes: 'openid profile email mcp:read mcp:admin', + authDisabled: true, + }); + + // Create auth middleware (with auth disabled) + authMiddleware = new OAuthAuthMiddleware(null, config); + + // Register endpoints in correct order + registerHealthEndpoint(app, '1.0.0'); + registerOAuthEndpoints(app, config); + app.use(authMiddleware.middleware); + mcpHandler = registerMcpHandler(app); + registerWeatherTools(mcpHandler, authMiddleware); + }); + + describe('Health Endpoint', () => { + it('should return 200 with status ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + expect(response.body.version).toBe('1.0.0'); + expect(response.body.timestamp).toBeDefined(); + }); + }); + + describe('OAuth Discovery Endpoints', () => { + it('should return protected resource metadata', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + + expect(response.status).toBe(200); + expect(response.body.resource).toBe('http://localhost:3001/mcp'); + expect(response.body.authorization_servers).toContain( + 'http://localhost:3001' + ); + }); + + it('should return authorization server metadata', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + + expect(response.status).toBe(200); + expect(response.body.issuer).toBe( + 'https://keycloak.example.com/realms/mcp' + ); + expect(response.body.authorization_endpoint).toContain('auth'); + expect(response.body.token_endpoint).toContain('token'); + }); + + it('should return OpenID configuration', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + + expect(response.status).toBe(200); + expect(response.body.scopes_supported).toContain('openid'); + expect(response.body.response_types_supported).toContain('code'); + }); + + it('should redirect OAuth authorize requests', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ client_id: 'test', response_type: 'code' }); + + expect(response.status).toBe(302); + expect(response.headers.location).toContain('keycloak.example.com'); + }); + }); + + describe('MCP Handler', () => { + it('should handle initialize request', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2024-11-05' }, + }); + + expect(response.status).toBe(200); + expect(response.body.result.protocolVersion).toBe('2024-11-05'); + expect(response.body.result.serverInfo.name).toBe('js-auth-mcp-server'); + }); + + it('should handle ping request', async () => { + const response = await request(app).post('/mcp').send({ + jsonrpc: '2.0', + id: 1, + method: 'ping', + }); + + expect(response.status).toBe(200); + expect(response.body.result).toEqual({}); + }); + + it('should list all weather tools', async () => { + const response = await request(app).post('/mcp').send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/list', + }); + + expect(response.status).toBe(200); + const tools = response.body.result.tools; + expect(tools).toHaveLength(3); + + const toolNames = tools.map((t: { name: string }) => t.name); + expect(toolNames).toContain('get-weather'); + expect(toolNames).toContain('get-forecast'); + expect(toolNames).toContain('get-weather-alerts'); + }); + + it('should return error for unknown method', async () => { + const response = await request(app).post('/mcp').send({ + jsonrpc: '2.0', + id: 1, + method: 'unknown/method', + }); + + expect(response.status).toBe(200); + expect(response.body.error.code).toBe(-32601); + expect(response.body.error.message).toContain('Method not found'); + }); + }); + + describe('Weather Tools (auth disabled)', () => { + it('should get weather without authentication', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather', arguments: { city: 'Seattle' } }, + }); + + expect(response.status).toBe(200); + const result = response.body.result; + const data = JSON.parse(result.content[0].text); + expect(data.city).toBe('Seattle'); + expect(data.temperature).toBeDefined(); + expect(data.condition).toBeDefined(); + }); + + it('should get forecast without authentication (auth disabled)', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-forecast', arguments: { city: 'Portland' } }, + }); + + expect(response.status).toBe(200); + const result = response.body.result; + expect(result.isError).toBeUndefined(); + const data = JSON.parse(result.content[0].text); + expect(data.city).toBe('Portland'); + expect(data.forecast).toHaveLength(5); + }); + + it('should get weather alerts without authentication (auth disabled)', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'get-weather-alerts', + arguments: { region: 'Pacific Northwest' }, + }, + }); + + expect(response.status).toBe(200); + const result = response.body.result; + expect(result.isError).toBeUndefined(); + const data = JSON.parse(result.content[0].text); + expect(data.region).toBe('Pacific Northwest'); + expect(Array.isArray(data.alerts)).toBe(true); + }); + }); + + describe('RPC Endpoint', () => { + it('should handle requests on /rpc endpoint', async () => { + const response = await request(app).post('/rpc').send({ + jsonrpc: '2.0', + id: 1, + method: 'ping', + }); + + expect(response.status).toBe(200); + expect(response.body.result).toEqual({}); + }); + + it('should call tools via /rpc endpoint', async () => { + const response = await request(app) + .post('/rpc') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather', arguments: { city: 'Denver' } }, + }); + + expect(response.status).toBe(200); + const data = JSON.parse(response.body.result.content[0].text); + expect(data.city).toBe('Denver'); + }); + }); + + describe('CORS Handling', () => { + it('should handle OPTIONS preflight request', async () => { + const response = await request(app) + .options('/mcp') + .set('Origin', 'http://localhost:8080') + .set('Access-Control-Request-Method', 'POST'); + + expect(response.status).toBe(204); + expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(response.headers['access-control-allow-methods']).toContain( + 'POST' + ); + }); + }); +}); + +describe('Integration Tests (auth enabled)', () => { + let app: Express; + let config: AuthServerConfig; + let authMiddleware: OAuthAuthMiddleware; + + beforeEach(() => { + app = express(); + app.use(express.json()); + + // Create config with auth enabled but no actual auth client + config = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authServerUrl: 'https://keycloak.example.com/realms/mcp', + issuer: 'https://keycloak.example.com/realms/mcp', + jwksUri: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs', + allowedScopes: 'openid profile email mcp:read mcp:admin', + clientId: 'test-client', + clientSecret: 'test-secret', + authDisabled: false, // Auth enabled + }); + + // Create middleware with null auth client (will require token but can't validate) + authMiddleware = new OAuthAuthMiddleware(null, config); + + registerHealthEndpoint(app, '1.0.0'); + registerOAuthEndpoints(app, config); + app.use(authMiddleware.middleware); + const mcpHandler = registerMcpHandler(app); + registerWeatherTools(mcpHandler, authMiddleware); + }); + + describe('Authentication Required', () => { + it('should allow access to health without token', async () => { + const response = await request(app).get('/health'); + expect(response.status).toBe(200); + }); + + it('should allow access to discovery endpoints without token', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.status).toBe(200); + }); + + it('should require token for /mcp endpoint', async () => { + // Since authClient is null, requiresAuth returns false even with authDisabled=false + // This is expected behavior - without a working auth client, we can't validate + const response = await request(app).post('/mcp').send({ + jsonrpc: '2.0', + id: 1, + method: 'ping', + }); + + // Without auth client, middleware allows access + expect(response.status).toBe(200); + }); + }); +}); + +describe('JSON-RPC Error Handling', () => { + let app: Express; + + beforeEach(() => { + app = express(); + app.use(express.json()); + + const config = createDefaultConfig({ authDisabled: true }); + const authMiddleware = new OAuthAuthMiddleware(null, config); + + registerHealthEndpoint(app); + app.use(authMiddleware.middleware); + const mcpHandler = registerMcpHandler(app); + registerWeatherTools(mcpHandler, authMiddleware); + }); + + it('should return parse error for invalid JSON', async () => { + const response = await request(app) + .post('/mcp') + .set('Content-Type', 'application/json') + .send('{ invalid json }'); + + expect(response.status).toBe(400); // Express json parser returns 400 + }); + + it('should return invalid request for non-object body', async () => { + const response = await request(app).post('/mcp').send('just a string'); + + expect(response.status).toBe(200); + expect(response.body.error.code).toBe(-32600); + }); + + it('should return invalid request for missing jsonrpc field', async () => { + const response = await request(app).post('/mcp').send({ method: 'ping' }); + + expect(response.status).toBe(200); + expect(response.body.error.code).toBe(-32600); + expect(response.body.error.message).toContain('jsonrpc'); + }); + + it('should return invalid params for non-string tool name', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 123 }, + }); + + expect(response.status).toBe(200); + expect(response.body.error.code).toBe(-32602); + }); + + it('should return method not found for unknown tool', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'nonexistent-tool' }, + }); + + expect(response.status).toBe(200); + expect(response.body.error.code).toBe(-32601); + expect(response.body.error.message).toContain('Tool not found'); + }); +}); + +describe('Tool Input Validation', () => { + let app: Express; + + beforeEach(() => { + app = express(); + app.use(express.json()); + + const config = createDefaultConfig({ authDisabled: true }); + const authMiddleware = new OAuthAuthMiddleware(null, config); + const mcpHandler = registerMcpHandler(app); + registerWeatherTools(mcpHandler, authMiddleware); + }); + + it('should handle missing arguments gracefully', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather' }, + }); + + expect(response.status).toBe(200); + // Tool should handle missing city with default + const data = JSON.parse(response.body.result.content[0].text); + expect(data.city).toBe('Unknown'); + }); + + it('should handle empty arguments object', async () => { + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-forecast', arguments: {} }, + }); + + expect(response.status).toBe(200); + const data = JSON.parse(response.body.result.content[0].text); + expect(data.city).toBe('Unknown'); + }); +}); diff --git a/examples/auth/src/config.ts b/examples/auth/src/config.ts new file mode 100644 index 00000000..da5af5bd --- /dev/null +++ b/examples/auth/src/config.ts @@ -0,0 +1,232 @@ +/** + * Configuration loader for Auth MCP Server + * + * Mirrors AuthServerConfig from the C++ example: + * /gopher-orch/examples/auth/auth_server_config.h + */ + +import fs from 'fs'; +import path from 'path'; + +/** + * Server configuration interface + */ +export interface AuthServerConfig { + // Server settings + host: string; + port: number; + serverUrl: string; + + // OAuth/IDP settings + authServerUrl: string; + jwksUri: string; + issuer: string; + clientId: string; + clientSecret: string; + tokenEndpoint: string; + + // Direct OAuth endpoint URLs + oauthAuthorizeUrl: string; + oauthTokenUrl: string; + + // Scopes + allowedScopes: string; + + // Cache settings + jwksCacheDuration: number; + jwksAutoRefresh: boolean; + requestTimeout: number; + + // Auth bypass mode + authDisabled: boolean; +} + +/** + * Parse a configuration file in key=value format + * + * @param content - Raw file content + * @returns Parsed key-value map + */ +export function parseConfigFile(content: string): Record { + const result: Record = {}; + + for (const line of content.split('\n')) { + const trimmed = line.trim(); + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith('#')) { + continue; + } + + const eqIndex = trimmed.indexOf('='); + if (eqIndex === -1) { + continue; + } + + const key = trimmed.substring(0, eqIndex).trim(); + const value = trimmed.substring(eqIndex + 1).trim(); + + if (key) { + result[key] = value; + } + } + + return result; +} + +/** + * Load and parse configuration from a file + * + * @param configPath - Path to the configuration file + * @returns Parsed AuthServerConfig + * @throws Error if file not found or required fields missing + */ +export function loadConfigFromFile(configPath: string): AuthServerConfig { + if (!fs.existsSync(configPath)) { + throw new Error(`Config file not found: ${configPath}`); + } + + const content = fs.readFileSync(configPath, 'utf-8'); + const configMap = parseConfigFile(content); + + return buildConfig(configMap); +} + +/** + * Load configuration from the default location relative to a base path + * + * @param basePath - Base path (typically __dirname or process executable path) + * @returns Parsed AuthServerConfig + */ +export function loadConfigFromDefaultLocation( + basePath: string +): AuthServerConfig { + const configPath = path.join(basePath, 'server.config'); + return loadConfigFromFile(configPath); +} + +/** + * Build AuthServerConfig from a parsed key-value map + * + * @param configMap - Parsed configuration map + * @returns AuthServerConfig object + * @throws Error if required fields are missing (when auth is enabled) + */ +export function buildConfig( + configMap: Record +): AuthServerConfig { + const port = parseInt(configMap.port || '3001', 10); + + const config: AuthServerConfig = { + // Server settings + host: configMap.host || '0.0.0.0', + port, + serverUrl: configMap.server_url || `http://localhost:${port}`, + + // OAuth/IDP settings + authServerUrl: configMap.auth_server_url || '', + jwksUri: configMap.jwks_uri || '', + issuer: configMap.issuer || '', + clientId: configMap.client_id || '', + clientSecret: configMap.client_secret || '', + tokenEndpoint: configMap.token_endpoint || '', + + // Direct OAuth endpoint URLs + oauthAuthorizeUrl: configMap.oauth_authorize_url || '', + oauthTokenUrl: configMap.oauth_token_url || '', + + // Scopes + allowedScopes: + configMap.allowed_scopes || 'openid profile email mcp:read mcp:admin', + + // Cache settings + jwksCacheDuration: parseInt(configMap.jwks_cache_duration || '3600', 10), + jwksAutoRefresh: configMap.jwks_auto_refresh !== 'false', + requestTimeout: parseInt(configMap.request_timeout || '30', 10), + + // Auth bypass mode + authDisabled: configMap.auth_disabled === 'true', + }; + + // Derive endpoints from auth_server_url if not explicitly set + if (config.authServerUrl) { + if (!config.jwksUri) { + config.jwksUri = `${config.authServerUrl}/protocol/openid-connect/certs`; + } + if (!config.tokenEndpoint) { + config.tokenEndpoint = `${config.authServerUrl}/protocol/openid-connect/token`; + } + if (!config.issuer) { + config.issuer = config.authServerUrl; + } + if (!config.oauthAuthorizeUrl) { + config.oauthAuthorizeUrl = `${config.authServerUrl}/protocol/openid-connect/auth`; + } + if (!config.oauthTokenUrl) { + config.oauthTokenUrl = `${config.authServerUrl}/protocol/openid-connect/token`; + } + } + + // Validate required fields when auth is enabled + if (!config.authDisabled) { + validateRequiredFields(config); + } + + return config; +} + +/** + * Validate that required configuration fields are present + * + * @param config - Configuration to validate + * @throws Error if required fields are missing + */ +function validateRequiredFields(config: AuthServerConfig): void { + const errors: string[] = []; + + if (!config.clientId) { + errors.push('client_id is required when auth is enabled'); + } + if (!config.clientSecret) { + errors.push('client_secret is required when auth is enabled'); + } + if (!config.jwksUri && !config.authServerUrl) { + errors.push('jwks_uri or auth_server_url is required when auth is enabled'); + } + + if (errors.length > 0) { + throw new Error( + `Configuration validation failed:\n - ${errors.join('\n - ')}` + ); + } +} + +/** + * Create a default configuration for testing or development + * + * @param overrides - Optional overrides for default values + * @returns AuthServerConfig with defaults + */ +export function createDefaultConfig( + overrides: Partial = {} +): AuthServerConfig { + return { + host: '0.0.0.0', + port: 3001, + serverUrl: 'http://localhost:3001', + authServerUrl: '', + jwksUri: '', + issuer: '', + clientId: '', + clientSecret: '', + tokenEndpoint: '', + oauthAuthorizeUrl: '', + oauthTokenUrl: '', + allowedScopes: 'openid profile email mcp:read mcp:admin', + jwksCacheDuration: 3600, + jwksAutoRefresh: true, + requestTimeout: 30, + authDisabled: true, + ...overrides, + }; +} diff --git a/examples/auth/src/index.ts b/examples/auth/src/index.ts new file mode 100644 index 00000000..92843e39 --- /dev/null +++ b/examples/auth/src/index.ts @@ -0,0 +1,204 @@ +/** + * JS Auth MCP Server - Entry Point + * + * OAuth-protected MCP server example using gopher-auth FFI bindings. + * Demonstrates JWT token validation with Keycloak and scope-based + * access control for MCP tools. + */ + +import express from 'express'; +import path from 'path'; +import { + gopherInitAuthLibrary, + gopherShutdownAuthLibrary, + gopherGetAuthLibraryVersion, + GopherAuthClient, +} from '@gopher.security/gopher-mcp-js'; +import { loadConfigFromFile, AuthServerConfig } from './config'; +import { registerHealthEndpoint } from './routes/health'; +import { registerOAuthEndpoints } from './routes/oauth-endpoints'; +import { registerMcpHandler } from './routes/mcp-handler'; +import { OAuthAuthMiddleware } from './middleware/oauth-auth'; +import { registerWeatherTools } from './tools/weather-tools'; + +/** + * Print startup banner + */ +function printBanner(): void { + console.log(''); + console.log('========================================'); + console.log(' JS Auth MCP Server'); + console.log(' OAuth-Protected MCP Example'); + console.log('========================================'); + console.log(''); +} + +/** + * Print endpoint information + */ +function printEndpoints(config: AuthServerConfig): void { + const baseUrl = config.serverUrl; + + console.log('Endpoints:'); + console.log(` Health: GET ${baseUrl}/health`); + console.log( + ` OAuth Meta: GET ${baseUrl}/.well-known/oauth-protected-resource` + ); + console.log( + ` Auth Server: GET ${baseUrl}/.well-known/oauth-authorization-server` + ); + console.log( + ` OIDC Config: GET ${baseUrl}/.well-known/openid-configuration` + ); + console.log(` OAuth Auth: GET ${baseUrl}/oauth/authorize`); + console.log(` MCP: POST ${baseUrl}/mcp`); + console.log(` RPC: POST ${baseUrl}/rpc`); + console.log(''); + + if (config.authDisabled) { + console.log('Authentication: DISABLED'); + } else { + console.log('Authentication: ENABLED'); + console.log(` JWKS URI: ${config.jwksUri}`); + console.log(` Issuer: ${config.issuer}`); + } + console.log(''); +} + +/** + * Main entry point + */ +async function main(): Promise { + printBanner(); + + // Determine config path + const configPath = + process.argv[2] || path.join(__dirname, '..', 'server.config'); + + // Load configuration + let config: AuthServerConfig; + try { + console.log(`Loading configuration from: ${configPath}`); + config = loadConfigFromFile(configPath); + console.log('Configuration loaded successfully'); + console.log(''); + } catch (error) { + console.error(`Failed to load configuration: ${error}`); + process.exit(1); + } + + // Initialize auth library if auth is enabled + let authClient: GopherAuthClient | null = null; + + if (!config.authDisabled) { + try { + console.log('Initializing gopher-auth library...'); + gopherInitAuthLibrary(); + const version = gopherGetAuthLibraryVersion(); + console.log(` Library version: ${version}`); + + // Create auth client + authClient = new GopherAuthClient(config.jwksUri!, config.issuer!); + + // Set client options + if (config.jwksCacheDuration > 0) { + authClient.setOption( + 'cache_duration', + String(config.jwksCacheDuration) + ); + } + if (config.jwksAutoRefresh) { + authClient.setOption('auto_refresh', 'true'); + } + if (config.requestTimeout > 0) { + authClient.setOption('request_timeout', String(config.requestTimeout)); + } + + console.log(' Auth client created successfully'); + console.log(''); + } catch (error) { + console.error(`Failed to initialize auth library: ${error}`); + process.exit(1); + } + } else { + console.log( + 'Authentication disabled - skipping auth library initialization' + ); + console.log(''); + } + + // Create Express app + const app = express(); + app.use(express.json()); + + // Create auth middleware + const authMiddleware = new OAuthAuthMiddleware(authClient, config); + + // Register health endpoint (no auth required) + const serverVersion = config.authDisabled ? '1.0.0' : gopherGetAuthLibraryVersion(); + registerHealthEndpoint(app, serverVersion); + + // Register OAuth discovery endpoints (no auth required) + registerOAuthEndpoints(app, config); + + // Apply auth middleware to protected routes + app.use(authMiddleware.middleware); + + // Register MCP handler + const mcpHandler = registerMcpHandler(app); + + // Register weather tools + registerWeatherTools(mcpHandler, authMiddleware); + + // Start server + const server = app.listen(config.port, config.host, () => { + console.log(`Server started on ${config.host}:${config.port}`); + console.log(''); + printEndpoints(config); + console.log('Press Ctrl+C to shutdown'); + console.log(''); + }); + + // Graceful shutdown handler + const shutdown = async (): Promise => { + console.log(''); + console.log('Shutting down...'); + + // Close HTTP server + server.close(() => { + console.log(' HTTP server closed'); + }); + + // Cleanup auth resources + if (authClient) { + try { + authClient.destroy(); + console.log(' Auth client destroyed'); + } catch (error) { + console.error(` Error destroying auth client: ${error}`); + } + } + + if (!config.authDisabled) { + try { + gopherShutdownAuthLibrary(); + console.log(' Auth library shutdown complete'); + } catch (error) { + console.error(` Error shutting down auth library: ${error}`); + } + } + + console.log('Goodbye!'); + process.exit(0); + }; + + // Register signal handlers + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); +} + +// Run main +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/examples/auth/src/middleware/__tests__/oauth-auth.test.ts b/examples/auth/src/middleware/__tests__/oauth-auth.test.ts new file mode 100644 index 00000000..1e6b6b67 --- /dev/null +++ b/examples/auth/src/middleware/__tests__/oauth-auth.test.ts @@ -0,0 +1,598 @@ +import express, { Request, Response } from 'express'; +import request from 'supertest'; +import { OAuthAuthMiddleware, AuthenticatedRequest } from '../oauth-auth'; +import { createDefaultConfig, AuthServerConfig } from '../../config'; +import { + GopherAuthContext, + gopherCreateEmptyAuthContext, +} from '@gopher.security/gopher-mcp-js'; + +// Mock the SDK auth module +jest.mock('@gopher.security/gopher-mcp-js', () => ({ + gopherCreateEmptyAuthContext: jest.fn(() => ({ + userId: '', + scopes: '', + audience: '', + tokenExpiry: 0, + authenticated: false, + })), + gopherGenerateWwwAuthenticateHeaderV2: jest.fn( + ( + realm: string, + resource: string, + scope: string, + error: string, + description: string + ) => + `Bearer realm="${realm}", error="${error}", error_description="${description}"` + ), + GopherValidationOptions: jest.fn().mockImplementation(() => ({ + setClockSkew: jest.fn().mockReturnThis(), + destroy: jest.fn(), + })), +})); + +describe('OAuthAuthMiddleware', () => { + let config: AuthServerConfig; + + beforeEach(() => { + config = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + allowedScopes: 'openid mcp:read mcp:admin', + }); + }); + + describe('extractToken', () => { + let middleware: OAuthAuthMiddleware; + + beforeEach(() => { + middleware = new OAuthAuthMiddleware(null, config); + }); + + it('should extract token from Authorization header', () => { + const req = { + headers: { authorization: 'Bearer test-token-123' }, + query: {}, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBe('test-token-123'); + }); + + it('should extract token from query parameter', () => { + const req = { + headers: {}, + query: { access_token: 'query-token-456' }, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBe('query-token-456'); + }); + + it('should prefer Authorization header over query parameter', () => { + const req = { + headers: { authorization: 'Bearer header-token' }, + query: { access_token: 'query-token' }, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBe('header-token'); + }); + + it('should return null when no token is present', () => { + const req = { + headers: {}, + query: {}, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBeNull(); + }); + + it('should return null for non-Bearer authorization', () => { + const req = { + headers: { authorization: 'Basic dXNlcjpwYXNz' }, + query: {}, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBeNull(); + }); + + it('should return null for empty Bearer token', () => { + const req = { + headers: { authorization: 'Bearer ' }, + query: {}, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBe(''); + }); + + it('should return null for empty query token', () => { + const req = { + headers: {}, + query: { access_token: '' }, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBeNull(); + }); + + it('should handle token with spaces', () => { + const req = { + headers: { authorization: 'Bearer token with spaces' }, + query: {}, + } as unknown as Request; + + const token = middleware.extractToken(req); + expect(token).toBe('token with spaces'); + }); + }); + + describe('requiresAuth', () => { + it('should return false when auth is disabled', () => { + const disabledConfig = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, disabledConfig); + + expect(middleware.requiresAuth('/mcp')).toBe(false); + expect(middleware.requiresAuth('/protected')).toBe(false); + }); + + it('should return false for public paths', () => { + const middleware = new OAuthAuthMiddleware(null, config); + + expect( + middleware.requiresAuth('/.well-known/oauth-protected-resource') + ).toBe(false); + expect(middleware.requiresAuth('/.well-known/openid-configuration')).toBe( + false + ); + expect(middleware.requiresAuth('/oauth/authorize')).toBe(false); + expect(middleware.requiresAuth('/oauth/token')).toBe(false); + expect(middleware.requiresAuth('/authorize')).toBe(false); + expect(middleware.requiresAuth('/health')).toBe(false); + expect(middleware.requiresAuth('/favicon.ico')).toBe(false); + }); + + it('should return true for /mcp paths', () => { + // Need a mock auth client for requiresAuth to return true + const mockAuthClient = {} as any; + const middleware = new OAuthAuthMiddleware(mockAuthClient, config); + + expect(middleware.requiresAuth('/mcp')).toBe(true); + expect(middleware.requiresAuth('/mcp/')).toBe(true); + expect(middleware.requiresAuth('/mcp/tools')).toBe(true); + }); + + it('should return true for /rpc paths', () => { + const mockAuthClient = {} as any; + const middleware = new OAuthAuthMiddleware(mockAuthClient, config); + + expect(middleware.requiresAuth('/rpc')).toBe(true); + expect(middleware.requiresAuth('/rpc/')).toBe(true); + expect(middleware.requiresAuth('/rpc/call')).toBe(true); + }); + + it('should return true for /events paths', () => { + const mockAuthClient = {} as any; + const middleware = new OAuthAuthMiddleware(mockAuthClient, config); + + expect(middleware.requiresAuth('/events')).toBe(true); + expect(middleware.requiresAuth('/events/')).toBe(true); + expect(middleware.requiresAuth('/events/stream')).toBe(true); + }); + + it('should return true for /sse paths', () => { + const mockAuthClient = {} as any; + const middleware = new OAuthAuthMiddleware(mockAuthClient, config); + + expect(middleware.requiresAuth('/sse')).toBe(true); + expect(middleware.requiresAuth('/sse/')).toBe(true); + }); + + it('should return true for unknown paths by default', () => { + const mockAuthClient = {} as any; + const middleware = new OAuthAuthMiddleware(mockAuthClient, config); + + expect(middleware.requiresAuth('/api')).toBe(true); + expect(middleware.requiresAuth('/protected')).toBe(true); + expect(middleware.requiresAuth('/custom')).toBe(true); + }); + + it('should return false when no auth client', () => { + const middleware = new OAuthAuthMiddleware(null, config); + + // Even protected paths return false when no auth client + expect(middleware.requiresAuth('/mcp')).toBe(false); + }); + }); + + describe('getAuthContext', () => { + it('should return empty context initially', () => { + const middleware = new OAuthAuthMiddleware(null, config); + const ctx = middleware.getAuthContext(); + + expect(ctx.authenticated).toBe(false); + expect(ctx.userId).toBe(''); + expect(ctx.scopes).toBe(''); + }); + }); + + describe('isAuthDisabled', () => { + it('should return true when auth is disabled', () => { + const disabledConfig = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, disabledConfig); + + expect(middleware.isAuthDisabled()).toBe(true); + }); + + it('should return false when auth is enabled', () => { + const enabledConfig = createDefaultConfig({ authDisabled: false }); + const middleware = new OAuthAuthMiddleware(null, enabledConfig); + + expect(middleware.isAuthDisabled()).toBe(false); + }); + }); + + describe('hasScope', () => { + it('should return false when not authenticated', () => { + const middleware = new OAuthAuthMiddleware(null, config); + + expect(middleware.hasScope('mcp:read')).toBe(false); + }); + }); +}); + +describe('OAuthAuthMiddleware integration', () => { + let app: express.Express; + let config: AuthServerConfig; + + beforeEach(() => { + app = express(); + config = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: true, // Disable auth for most tests + allowedScopes: 'openid mcp:read mcp:admin', + }); + }); + + describe('CORS preflight', () => { + it('should handle OPTIONS request with CORS headers', async () => { + const middleware = new OAuthAuthMiddleware(null, config); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app) + .options('/mcp') + .set('Origin', 'http://example.com') + .set('Access-Control-Request-Method', 'POST'); + + expect(response.status).toBe(204); + expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(response.headers['access-control-allow-methods']).toContain( + 'POST' + ); + expect(response.headers['access-control-allow-headers']).toContain( + 'Authorization' + ); + }); + }); + + describe('public paths', () => { + it('should allow access to /health without token', async () => { + const middleware = new OAuthAuthMiddleware(null, config); + app.use(middleware.middleware); + app.get('/health', (_req, res) => res.json({ status: 'ok' })); + + const response = await request(app).get('/health'); + + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); + + it('should allow access to /.well-known paths without token', async () => { + const middleware = new OAuthAuthMiddleware(null, config); + app.use(middleware.middleware); + app.get('/.well-known/oauth-protected-resource', (_req, res) => + res.json({ resource: 'test' }) + ); + + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + + expect(response.status).toBe(200); + }); + }); + + describe('auth disabled mode', () => { + it('should allow access to protected paths when auth disabled', async () => { + const disabledConfig = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, disabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app).get('/mcp'); + + expect(response.status).toBe(200); + }); + }); + + describe('unauthorized responses', () => { + it('should return 401 for missing token on protected path', async () => { + // Create mock auth client + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: false, + errorCode: -1000, + errorMessage: 'Invalid token', + }), + extractPayload: jest.fn(), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app).get('/mcp'); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_request'); + expect(response.body.error_description).toBe('Missing bearer token'); + }); + + it('should return 401 with WWW-Authenticate header', async () => { + const mockAuthClient = {} as any; + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app).get('/mcp'); + + expect(response.status).toBe(401); + expect(response.headers['www-authenticate']).toBeDefined(); + expect(response.headers['www-authenticate']).toContain('Bearer'); + }); + + it('should include CORS headers in 401 response', async () => { + const mockAuthClient = {} as any; + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app).get('/mcp'); + + expect(response.status).toBe(401); + expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(response.headers['access-control-expose-headers']).toContain( + 'WWW-Authenticate' + ); + }); + + it('should return 401 for invalid token', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: false, + errorCode: -1000, + errorMessage: 'Token signature verification failed', + }), + extractPayload: jest.fn(), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app) + .get('/mcp') + .set('Authorization', 'Bearer invalid-token'); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_token'); + expect(response.body.error_description).toBe( + 'Token signature verification failed' + ); + }); + + it('should return 401 for expired token', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: false, + errorCode: -1001, + errorMessage: 'Token has expired', + }), + extractPayload: jest.fn(), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app) + .get('/mcp') + .set('Authorization', 'Bearer expired-token'); + + expect(response.status).toBe(401); + expect(response.body.error).toBe('invalid_token'); + expect(response.body.error_description).toBe('Token has expired'); + }); + }); + + describe('successful authentication', () => { + it('should allow access with valid token', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: true, + errorCode: 0, + errorMessage: null, + }), + extractPayload: jest.fn().mockReturnValue({ + subject: 'user-123', + scopes: 'openid mcp:read', + audience: 'mcp-server', + expiration: Math.floor(Date.now() / 1000) + 3600, + }), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (req: AuthenticatedRequest, res) => { + res.json({ + ok: true, + userId: req.authContext?.userId, + scopes: req.authContext?.scopes, + }); + }); + + const response = await request(app) + .get('/mcp') + .set('Authorization', 'Bearer valid-token'); + + expect(response.status).toBe(200); + expect(response.body.ok).toBe(true); + expect(response.body.userId).toBe('user-123'); + expect(response.body.scopes).toBe('openid mcp:read'); + }); + + it('should populate auth context on successful validation', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: true, + errorCode: 0, + errorMessage: null, + }), + extractPayload: jest.fn().mockReturnValue({ + subject: 'user-456', + scopes: 'mcp:admin', + audience: 'api', + expiration: 1704067200, + }), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => { + const ctx = middleware.getAuthContext(); + res.json({ + authenticated: ctx.authenticated, + userId: ctx.userId, + scopes: ctx.scopes, + audience: ctx.audience, + tokenExpiry: ctx.tokenExpiry, + }); + }); + + const response = await request(app) + .get('/mcp') + .set('Authorization', 'Bearer valid-token'); + + expect(response.status).toBe(200); + expect(response.body.authenticated).toBe(true); + expect(response.body.userId).toBe('user-456'); + expect(response.body.scopes).toBe('mcp:admin'); + expect(response.body.audience).toBe('api'); + expect(response.body.tokenExpiry).toBe(1704067200); + }); + + it('should allow access via query parameter token', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: true, + errorCode: 0, + errorMessage: null, + }), + extractPayload: jest.fn().mockReturnValue({ + subject: 'user-789', + scopes: 'openid', + }), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => res.json({ ok: true })); + + const response = await request(app) + .get('/mcp') + .query({ access_token: 'query-param-token' }); + + expect(response.status).toBe(200); + expect(mockAuthClient.validateToken).toHaveBeenCalledWith( + 'query-param-token', + expect.anything() + ); + }); + }); + + describe('hasScope after authentication', () => { + it('should return true for present scope', async () => { + const mockAuthClient = { + validateToken: jest.fn().mockReturnValue({ + valid: true, + errorCode: 0, + errorMessage: null, + }), + extractPayload: jest.fn().mockReturnValue({ + subject: 'user-123', + scopes: 'openid mcp:read mcp:admin', + }), + } as any; + + const enabledConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authDisabled: false, + }); + const middleware = new OAuthAuthMiddleware(mockAuthClient, enabledConfig); + app.use(middleware.middleware); + app.get('/mcp', (_req, res) => { + res.json({ + hasRead: middleware.hasScope('mcp:read'), + hasAdmin: middleware.hasScope('mcp:admin'), + hasWrite: middleware.hasScope('mcp:write'), + }); + }); + + const response = await request(app) + .get('/mcp') + .set('Authorization', 'Bearer valid-token'); + + expect(response.status).toBe(200); + expect(response.body.hasRead).toBe(true); + expect(response.body.hasAdmin).toBe(true); + expect(response.body.hasWrite).toBe(false); + }); + }); +}); diff --git a/examples/auth/src/middleware/oauth-auth.ts b/examples/auth/src/middleware/oauth-auth.ts new file mode 100644 index 00000000..62dd612c --- /dev/null +++ b/examples/auth/src/middleware/oauth-auth.ts @@ -0,0 +1,289 @@ +/** + * OAuth Authentication Middleware + * + * Express middleware for JWT token validation using gopher-auth FFI. + * Mirrors OAuthAuthFilter from the C++ example. + */ + +import { Request, Response, NextFunction } from 'express'; +import { AuthServerConfig } from '../config'; +import { + GopherAuthClient, + GopherValidationOptions, + GopherAuthContext, + gopherCreateEmptyAuthContext, + gopherGenerateWwwAuthenticateHeaderV2, +} from '@gopher.security/gopher-mcp-js'; + +/** + * Extended Express Request with auth context + */ +export interface AuthenticatedRequest extends Request { + authContext?: GopherAuthContext; +} + +/** + * OAuth authentication middleware + * + * Validates JWT tokens on protected endpoints and attaches + * the auth context to the request. + */ +export class OAuthAuthMiddleware { + private authClient: GopherAuthClient | null; + private config: AuthServerConfig; + private currentAuthContext: GopherAuthContext = gopherCreateEmptyAuthContext(); + + /** + * Create new OAuth middleware + * + * @param authClient - GopherAuthClient instance for token validation (null if auth disabled) + * @param config - Server configuration + */ + constructor(authClient: GopherAuthClient | null, config: AuthServerConfig) { + this.authClient = authClient; + this.config = config; + } + + /** + * Express middleware handler + */ + middleware = (req: Request, res: Response, next: NextFunction): void => { + // Handle CORS preflight + if (req.method === 'OPTIONS') { + this.sendCorsPreflightResponse(res); + return; + } + + const path = req.path; + + // Check if path requires authentication + if (!this.requiresAuth(path)) { + next(); + return; + } + + // Extract bearer token + const token = this.extractToken(req); + if (!token) { + this.sendUnauthorized(res, 'invalid_request', 'Missing bearer token'); + return; + } + + // Validate token + const validationResult = this.validateToken(token); + if (!validationResult.valid) { + this.sendUnauthorized( + res, + 'invalid_token', + validationResult.errorMessage || 'Token validation failed' + ); + return; + } + + // Attach auth context to request + (req as AuthenticatedRequest).authContext = this.currentAuthContext; + next(); + }; + + /** + * Extract bearer token from request + * + * Checks Authorization header first, then query parameter. + * + * @param req - Express request + * @returns Token string or null if not found + */ + extractToken(req: Request): string | null { + // Try Authorization header first + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + return authHeader.substring(7); + } + + // Try query parameter + const queryToken = req.query.access_token; + if (typeof queryToken === 'string' && queryToken.length > 0) { + return queryToken; + } + + return null; + } + + /** + * Check if path requires authentication + * + * @param path - Request path + * @returns true if authentication is required + */ + requiresAuth(path: string): boolean { + // Auth is globally disabled + if (this.config.authDisabled) { + return false; + } + + // No auth client available + if (!this.authClient) { + return false; + } + + // Public paths (no auth required) + if (path.startsWith('/.well-known/')) return false; + if (path.startsWith('/oauth/')) return false; + if (path === '/authorize') return false; + if (path === '/health') return false; + if (path === '/favicon.ico') return false; + + // Protected paths (auth required) + if (path === '/mcp' || path.startsWith('/mcp/')) return true; + if (path === '/rpc' || path.startsWith('/rpc/')) return true; + if (path === '/events' || path.startsWith('/events/')) return true; + if (path === '/sse' || path.startsWith('/sse/')) return true; + + // Default: require auth for unknown paths + return true; + } + + /** + * Validate JWT token using gopher-auth + * + * @param token - JWT token string + * @returns Validation result + */ + private validateToken(token: string): { + valid: boolean; + errorMessage: string | null; + } { + if (!this.authClient) { + return { valid: false, errorMessage: 'Auth client not initialized' }; + } + + const options = new GopherValidationOptions(); + options.setClockSkew(30); + + try { + const result = this.authClient.validateToken(token, options); + + if (!result.valid) { + return { valid: false, errorMessage: result.errorMessage }; + } + + // Extract payload to populate auth context + try { + const payload = this.authClient.extractPayload(token); + + this.currentAuthContext = { + userId: payload.subject, + scopes: payload.scopes, + audience: payload.audience || '', + tokenExpiry: payload.expiration || 0, + authenticated: true, + }; + } catch { + // Payload extraction failed, but token is valid + this.currentAuthContext = { + userId: '', + scopes: '', + audience: '', + tokenExpiry: 0, + authenticated: true, + }; + } + + return { valid: true, errorMessage: null }; + } finally { + options.destroy(); + } + } + + /** + * Send 401 Unauthorized response with WWW-Authenticate header + * + * @param res - Express response + * @param error - OAuth error code + * @param description - Human-readable error description + */ + private sendUnauthorized( + res: Response, + error: string, + description: string + ): void { + let wwwAuthenticate: string; + + try { + wwwAuthenticate = gopherGenerateWwwAuthenticateHeaderV2( + this.config.serverUrl, + `${this.config.serverUrl}/.well-known/oauth-protected-resource`, + this.config.allowedScopes, + error, + description + ); + } catch { + // Fallback to basic Bearer header + wwwAuthenticate = `Bearer realm="${this.config.serverUrl}", error="${error}", error_description="${description}"`; + } + + res + .status(401) + .set('WWW-Authenticate', wwwAuthenticate) + .set('Content-Type', 'application/json') + .set('Access-Control-Allow-Origin', '*') + .set('Access-Control-Expose-Headers', 'WWW-Authenticate') + .json({ + error, + error_description: description, + }); + } + + /** + * Send CORS preflight response + * + * @param res - Express response + */ + private sendCorsPreflightResponse(res: Response): void { + res + .status(204) + .set('Access-Control-Allow-Origin', '*') + .set( + 'Access-Control-Allow-Methods', + 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD' + ) + .set( + 'Access-Control-Allow-Headers', + 'Accept, Accept-Language, Content-Language, Content-Type, Authorization, ' + + 'X-Requested-With, Origin, Cache-Control, Pragma, Mcp-Session-Id, Mcp-Protocol-Version' + ) + .set('Access-Control-Max-Age', '86400') + .set( + 'Access-Control-Expose-Headers', + 'WWW-Authenticate, Content-Length, Content-Type' + ) + .end(); + } + + /** + * Get the current authentication context + */ + getAuthContext(): GopherAuthContext { + return this.currentAuthContext; + } + + /** + * Check if authentication is disabled + */ + isAuthDisabled(): boolean { + return this.config.authDisabled; + } + + /** + * Check if a scope is present in the current auth context + * + * @param scope - Scope to check + * @returns true if scope is present + */ + hasScope(scope: string): boolean { + if (!this.currentAuthContext.authenticated) { + return false; + } + return this.currentAuthContext.scopes.split(' ').includes(scope); + } +} diff --git a/examples/auth/src/routes/__tests__/health.test.ts b/examples/auth/src/routes/__tests__/health.test.ts new file mode 100644 index 00000000..6e8753f0 --- /dev/null +++ b/examples/auth/src/routes/__tests__/health.test.ts @@ -0,0 +1,121 @@ +import express from 'express'; +import request from 'supertest'; +import { registerHealthEndpoint, HealthResponse } from '../health'; + +describe('Health Endpoint', () => { + let app: express.Express; + + beforeEach(() => { + app = express(); + }); + + describe('GET /health', () => { + it('should return 200 status code', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + + expect(response.status).toBe(200); + }); + + it('should return JSON content type', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + + it('should return status: "ok"', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + const body = response.body as HealthResponse; + + expect(body.status).toBe('ok'); + }); + + it('should return a valid ISO timestamp', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + const body = response.body as HealthResponse; + + expect(body.timestamp).toBeDefined(); + expect(() => new Date(body.timestamp)).not.toThrow(); + + const timestamp = new Date(body.timestamp); + expect(timestamp.getTime()).not.toBeNaN(); + }); + + it('should return uptime in seconds', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + const body = response.body as HealthResponse; + + expect(body.uptime).toBeDefined(); + expect(typeof body.uptime).toBe('number'); + expect(body.uptime).toBeGreaterThanOrEqual(0); + }); + + it('should include version when provided', async () => { + registerHealthEndpoint(app, '1.0.0'); + + const response = await request(app).get('/health'); + const body = response.body as HealthResponse; + + expect(body.version).toBe('1.0.0'); + }); + + it('should not include version when not provided', async () => { + registerHealthEndpoint(app); + + const response = await request(app).get('/health'); + const body = response.body as HealthResponse; + + expect(body.version).toBeUndefined(); + }); + + it('should handle multiple requests', async () => { + registerHealthEndpoint(app); + + const responses = await Promise.all([ + request(app).get('/health'), + request(app).get('/health'), + request(app).get('/health'), + ]); + + responses.forEach((response) => { + expect(response.status).toBe(200); + expect(response.body.status).toBe('ok'); + }); + }); + }); + + describe('Other HTTP methods', () => { + it('should return 404 for POST /health', async () => { + registerHealthEndpoint(app); + + const response = await request(app).post('/health'); + + expect(response.status).toBe(404); + }); + + it('should return 404 for PUT /health', async () => { + registerHealthEndpoint(app); + + const response = await request(app).put('/health'); + + expect(response.status).toBe(404); + }); + + it('should return 404 for DELETE /health', async () => { + registerHealthEndpoint(app); + + const response = await request(app).delete('/health'); + + expect(response.status).toBe(404); + }); + }); +}); diff --git a/examples/auth/src/routes/__tests__/mcp-handler.test.ts b/examples/auth/src/routes/__tests__/mcp-handler.test.ts new file mode 100644 index 00000000..11e6757d --- /dev/null +++ b/examples/auth/src/routes/__tests__/mcp-handler.test.ts @@ -0,0 +1,488 @@ +import express from 'express'; +import request from 'supertest'; +import { + McpHandler, + registerMcpHandler, + JsonRpcErrorCode, +} from '../mcp-handler'; +import { AuthenticatedRequest } from '../../middleware/oauth-auth'; +import { Request } from 'express'; + +describe('McpHandler', () => { + let handler: McpHandler; + + beforeEach(() => { + handler = new McpHandler(); + }); + + describe('registerTool', () => { + it('should register a tool', () => { + handler.registerTool( + 'test-tool', + { + description: 'A test tool', + inputSchema: { + type: 'object', + properties: { + input: { type: 'string', description: 'Input value' }, + }, + required: ['input'], + }, + }, + async () => ({ content: [{ type: 'text', text: 'result' }] }) + ); + + const tools = handler.getTools(); + expect(tools).toHaveLength(1); + expect(tools[0].name).toBe('test-tool'); + }); + + it('should register multiple tools', () => { + handler.registerTool( + 'tool-1', + { + description: 'Tool 1', + inputSchema: { type: 'object', properties: {} }, + }, + async () => ({ content: [{ type: 'text', text: 'result1' }] }) + ); + + handler.registerTool( + 'tool-2', + { + description: 'Tool 2', + inputSchema: { type: 'object', properties: {} }, + }, + async () => ({ content: [{ type: 'text', text: 'result2' }] }) + ); + + const tools = handler.getTools(); + expect(tools).toHaveLength(2); + }); + }); + + describe('getTools', () => { + it('should return empty array when no tools registered', () => { + expect(handler.getTools()).toEqual([]); + }); + + it('should return tool specs with names', () => { + handler.registerTool( + 'my-tool', + { + description: 'My tool description', + inputSchema: { + type: 'object', + properties: { + value: { type: 'number' }, + }, + }, + }, + async () => ({ content: [] }) + ); + + const tools = handler.getTools(); + expect(tools[0]).toEqual({ + name: 'my-tool', + description: 'My tool description', + inputSchema: { + type: 'object', + properties: { + value: { type: 'number' }, + }, + }, + }); + }); + }); + + describe('handleRequest', () => { + const mockReq = {} as AuthenticatedRequest; + + describe('request validation', () => { + it('should reject non-object body', async () => { + const response = await handler.handleRequest(null, mockReq); + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_REQUEST); + expect(response.error?.message).toContain('expected object'); + }); + + it('should reject missing jsonrpc field', async () => { + const response = await handler.handleRequest( + { method: 'ping' }, + mockReq + ); + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_REQUEST); + expect(response.error?.message).toContain('jsonrpc must be "2.0"'); + }); + + it('should reject wrong jsonrpc version', async () => { + const response = await handler.handleRequest( + { jsonrpc: '1.0', method: 'ping' }, + mockReq + ); + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_REQUEST); + }); + + it('should reject non-string method', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', method: 123 }, + mockReq + ); + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_REQUEST); + expect(response.error?.message).toContain('method must be a string'); + }); + + it('should reject non-object params', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', method: 'ping', params: 'invalid' }, + mockReq + ); + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_PARAMS); + }); + }); + + describe('initialize method', () => { + it('should return server info and capabilities', async () => { + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2024-11-05' }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toMatchObject({ + protocolVersion: '2024-11-05', + capabilities: { tools: {} }, + serverInfo: { + name: 'js-auth-mcp-server', + version: '1.0.0', + }, + }); + }); + }); + + describe('ping method', () => { + it('should return empty object', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 1, method: 'ping' }, + mockReq + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({}); + }); + }); + + describe('tools/list method', () => { + it('should return empty tools array when no tools registered', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 1, method: 'tools/list' }, + mockReq + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({ tools: [] }); + }); + + it('should return registered tools', async () => { + handler.registerTool( + 'echo', + { + description: 'Echo tool', + inputSchema: { + type: 'object', + properties: { message: { type: 'string' } }, + }, + }, + async () => ({ content: [] }) + ); + + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 1, method: 'tools/list' }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { tools: unknown[] }; + expect(result.tools).toHaveLength(1); + }); + }); + + describe('tools/call method', () => { + beforeEach(() => { + handler.registerTool( + 'echo', + { + description: 'Echo tool', + inputSchema: { + type: 'object', + properties: { message: { type: 'string' } }, + required: ['message'], + }, + }, + async (args) => ({ + content: [{ type: 'text', text: String(args.message) }], + }) + ); + }); + + it('should call registered tool', async () => { + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'echo', arguments: { message: 'hello' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({ + content: [{ type: 'text', text: 'hello' }], + }); + }); + + it('should return error for missing tool name', async () => { + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { arguments: {} }, + }, + mockReq + ); + + expect(response.error?.code).toBe(JsonRpcErrorCode.INVALID_PARAMS); + }); + + it('should return error for non-existent tool', async () => { + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'nonexistent' }, + }, + mockReq + ); + + expect(response.error?.code).toBe(JsonRpcErrorCode.METHOD_NOT_FOUND); + expect(response.error?.message).toContain('Tool not found'); + }); + + it('should handle tool execution errors', async () => { + handler.registerTool( + 'failing-tool', + { + description: 'A tool that fails', + inputSchema: { type: 'object', properties: {} }, + }, + async () => { + throw new Error('Tool execution failed'); + } + ); + + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'failing-tool' }, + }, + mockReq + ); + + expect(response.error?.code).toBe(JsonRpcErrorCode.INTERNAL_ERROR); + expect(response.error?.message).toBe('Tool execution failed'); + }); + + it('should handle tool returning synchronously', async () => { + handler.registerTool( + 'sync-tool', + { + description: 'Sync tool', + inputSchema: { type: 'object', properties: {} }, + }, + () => ({ content: [{ type: 'text', text: 'sync result' }] }) + ); + + const response = await handler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'sync-tool' }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + expect(response.result).toEqual({ + content: [{ type: 'text', text: 'sync result' }], + }); + }); + }); + + describe('unknown method', () => { + it('should return method not found error', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 1, method: 'unknown/method' }, + mockReq + ); + + expect(response.error?.code).toBe(JsonRpcErrorCode.METHOD_NOT_FOUND); + expect(response.error?.message).toContain('Method not found'); + }); + }); + + describe('request id handling', () => { + it('should preserve string id', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 'request-123', method: 'ping' }, + mockReq + ); + + expect(response.id).toBe('request-123'); + }); + + it('should preserve number id', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: 42, method: 'ping' }, + mockReq + ); + + expect(response.id).toBe(42); + }); + + it('should handle null id', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', id: null, method: 'ping' }, + mockReq + ); + + expect(response.id).toBeNull(); + }); + + it('should handle missing id', async () => { + const response = await handler.handleRequest( + { jsonrpc: '2.0', method: 'ping' }, + mockReq + ); + + expect(response.id).toBeNull(); + }); + }); + }); +}); + +describe('registerMcpHandler', () => { + let app: express.Express; + + beforeEach(() => { + app = express(); + app.use(express.json()); + }); + + it('should register /mcp endpoint', async () => { + registerMcpHandler(app); + + const response = await request(app) + .post('/mcp') + .send({ jsonrpc: '2.0', id: 1, method: 'ping' }); + + expect(response.status).toBe(200); + expect(response.body.result).toEqual({}); + }); + + it('should register /rpc endpoint', async () => { + registerMcpHandler(app); + + const response = await request(app) + .post('/rpc') + .send({ jsonrpc: '2.0', id: 1, method: 'ping' }); + + expect(response.status).toBe(200); + expect(response.body.result).toEqual({}); + }); + + it('should return McpHandler instance', () => { + const handler = registerMcpHandler(app); + + expect(handler).toBeInstanceOf(McpHandler); + }); + + it('should allow tool registration after setup', async () => { + const handler = registerMcpHandler(app); + + handler.registerTool( + 'greet', + { + description: 'Greet someone', + inputSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + }, + }, + (args) => ({ + content: [{ type: 'text', text: `Hello, ${args.name}!` }], + }) + ); + + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'greet', arguments: { name: 'World' } }, + }); + + expect(response.status).toBe(200); + expect(response.body.result).toEqual({ + content: [{ type: 'text', text: 'Hello, World!' }], + }); + }); + + it('should handle initialize request', async () => { + registerMcpHandler(app); + + const response = await request(app) + .post('/mcp') + .send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2024-11-05' }, + }); + + expect(response.status).toBe(200); + expect(response.body.result.protocolVersion).toBe('2024-11-05'); + expect(response.body.result.serverInfo.name).toBe('js-auth-mcp-server'); + }); + + it('should handle tools/list request', async () => { + const handler = registerMcpHandler(app); + + handler.registerTool( + 'test', + { + description: 'Test tool', + inputSchema: { type: 'object', properties: {} }, + }, + () => ({ content: [] }) + ); + + const response = await request(app) + .post('/mcp') + .send({ jsonrpc: '2.0', id: 1, method: 'tools/list' }); + + expect(response.status).toBe(200); + expect(response.body.result.tools).toHaveLength(1); + expect(response.body.result.tools[0].name).toBe('test'); + }); +}); diff --git a/examples/auth/src/routes/__tests__/oauth-endpoints.test.ts b/examples/auth/src/routes/__tests__/oauth-endpoints.test.ts new file mode 100644 index 00000000..880a4652 --- /dev/null +++ b/examples/auth/src/routes/__tests__/oauth-endpoints.test.ts @@ -0,0 +1,288 @@ +import express from 'express'; +import request from 'supertest'; +import { registerOAuthEndpoints } from '../oauth-endpoints'; +import { createDefaultConfig, AuthServerConfig } from '../../config'; + +describe('OAuth Discovery Endpoints', () => { + let app: express.Express; + let config: AuthServerConfig; + + beforeEach(() => { + app = express(); + config = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authServerUrl: 'https://keycloak.example.com/realms/mcp', + issuer: 'https://keycloak.example.com/realms/mcp', + jwksUri: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs', + allowedScopes: 'openid profile email mcp:read mcp:admin', + oauthAuthorizeUrl: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth', + oauthTokenUrl: + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/token', + }); + registerOAuthEndpoints(app, config); + }); + + describe('GET /.well-known/oauth-protected-resource', () => { + it('should return 200 status code', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.status).toBe(200); + }); + + it('should return JSON content type', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.headers['content-type']).toMatch(/application\/json/); + }); + + it('should return resource field with /mcp path', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.resource).toBe('http://localhost:3001/mcp'); + }); + + it('should return authorization_servers array with server URL', async () => { + // RFC 9728: In stateless mode, authorization_servers points to server_url + // so clients can discover the auth server via /.well-known/oauth-authorization-server + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.authorization_servers).toEqual([ + 'http://localhost:3001', + ]); + }); + + it('should return scopes_supported array', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.scopes_supported).toContain('openid'); + expect(response.body.scopes_supported).toContain('mcp:read'); + expect(response.body.scopes_supported).toContain('mcp:admin'); + }); + + it('should return bearer_methods_supported', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.bearer_methods_supported).toEqual([ + 'header', + 'query', + ]); + }); + + it('should return resource_documentation URL', async () => { + const response = await request(app).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.resource_documentation).toBe( + 'http://localhost:3001/docs' + ); + }); + }); + + describe('GET /.well-known/oauth-authorization-server', () => { + it('should return 200 status code', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.status).toBe(200); + }); + + it('should return issuer', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.issuer).toBe( + 'https://keycloak.example.com/realms/mcp' + ); + }); + + it('should return authorization_endpoint', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.authorization_endpoint).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth' + ); + }); + + it('should return token_endpoint', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.token_endpoint).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/token' + ); + }); + + it('should return jwks_uri', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.jwks_uri).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/certs' + ); + }); + + it('should return response_types_supported', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.response_types_supported).toContain('code'); + }); + + it('should return grant_types_supported', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.grant_types_supported).toContain( + 'authorization_code' + ); + expect(response.body.grant_types_supported).toContain('refresh_token'); + }); + + it('should return code_challenge_methods_supported', async () => { + const response = await request(app).get( + '/.well-known/oauth-authorization-server' + ); + expect(response.body.code_challenge_methods_supported).toContain('S256'); + }); + }); + + describe('GET /.well-known/openid-configuration', () => { + it('should return 200 status code', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.status).toBe(200); + }); + + it('should return issuer', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.body.issuer).toBe( + 'https://keycloak.example.com/realms/mcp' + ); + }); + + it('should return userinfo_endpoint', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.body.userinfo_endpoint).toBe( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/userinfo' + ); + }); + + it('should include openid in scopes_supported', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.body.scopes_supported).toContain('openid'); + expect(response.body.scopes_supported).toContain('profile'); + expect(response.body.scopes_supported).toContain('email'); + }); + + it('should return subject_types_supported', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.body.subject_types_supported).toContain('public'); + }); + + it('should return id_token_signing_alg_values_supported', async () => { + const response = await request(app).get( + '/.well-known/openid-configuration' + ); + expect(response.body.id_token_signing_alg_values_supported).toContain( + 'RS256' + ); + }); + }); + + describe('GET /oauth/authorize', () => { + it('should redirect to authorization endpoint', async () => { + const response = await request(app) + .get('/oauth/authorize') + .query({ client_id: 'test-client', response_type: 'code' }); + + expect(response.status).toBe(302); + expect(response.headers.location).toContain( + 'https://keycloak.example.com/realms/mcp/protocol/openid-connect/auth' + ); + }); + + it('should forward query parameters', async () => { + const response = await request(app).get('/oauth/authorize').query({ + client_id: 'test-client', + response_type: 'code', + redirect_uri: 'http://localhost:8080/callback', + scope: 'openid profile', + state: 'abc123', + }); + + expect(response.status).toBe(302); + const location = response.headers.location; + expect(location).toContain('client_id=test-client'); + expect(location).toContain('response_type=code'); + expect(location).toContain('redirect_uri='); + expect(location).toContain('scope=openid'); + expect(location).toContain('state=abc123'); + }); + + it('should handle PKCE parameters', async () => { + const response = await request(app).get('/oauth/authorize').query({ + client_id: 'test-client', + response_type: 'code', + code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM', + code_challenge_method: 'S256', + }); + + expect(response.status).toBe(302); + const location = response.headers.location; + expect(location).toContain('code_challenge='); + expect(location).toContain('code_challenge_method=S256'); + }); + }); + + describe('Edge cases', () => { + it('should use serverUrl as fallback for authorization_servers', async () => { + const noAuthConfig = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + authServerUrl: '', + }); + const testApp = express(); + registerOAuthEndpoints(testApp, noAuthConfig); + + const response = await request(testApp).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.authorization_servers).toEqual([ + 'http://localhost:3001', + ]); + }); + + it('should filter empty scopes', async () => { + const emptyScopes = createDefaultConfig({ + serverUrl: 'http://localhost:3001', + allowedScopes: ' openid profile ', + }); + const testApp = express(); + registerOAuthEndpoints(testApp, emptyScopes); + + const response = await request(testApp).get( + '/.well-known/oauth-protected-resource' + ); + expect(response.body.scopes_supported).not.toContain(''); + expect(response.body.scopes_supported).toContain('openid'); + expect(response.body.scopes_supported).toContain('profile'); + }); + }); +}); diff --git a/examples/auth/src/routes/health.ts b/examples/auth/src/routes/health.ts new file mode 100644 index 00000000..fe81d037 --- /dev/null +++ b/examples/auth/src/routes/health.ts @@ -0,0 +1,43 @@ +/** + * Health check endpoint + * + * Provides a simple health check endpoint for monitoring + * and load balancer health checks. + */ + +import { Express, Request, Response } from 'express'; + +/** + * Health check response structure + */ +export interface HealthResponse { + status: 'ok' | 'error'; + timestamp: string; + version?: string; + uptime?: number; +} + +/** + * Register the health check endpoint + * + * @param app - Express application instance + * @param version - Optional version string to include in response + */ +export function registerHealthEndpoint(app: Express, version?: string): void { + const startTime = Date.now(); + + app.get('/health', (_req: Request, res: Response) => { + const response: HealthResponse = { + status: 'ok', + timestamp: new Date().toISOString(), + }; + + if (version) { + response.version = version; + } + + response.uptime = Math.floor((Date.now() - startTime) / 1000); + + res.status(200).json(response); + }); +} diff --git a/examples/auth/src/routes/mcp-handler.ts b/examples/auth/src/routes/mcp-handler.ts new file mode 100644 index 00000000..72faa9c2 --- /dev/null +++ b/examples/auth/src/routes/mcp-handler.ts @@ -0,0 +1,406 @@ +/** + * MCP Handler - JSON-RPC 2.0 Implementation + * + * Implements the Model Context Protocol (MCP) JSON-RPC handler + * for tool registration and invocation. + */ + +import { Express, Request, Response } from 'express'; +import { AuthenticatedRequest } from '../middleware/oauth-auth'; + +/** + * Set common CORS headers on response + * + * @param res - Express response object + */ +function setCorsHeaders(res: Response): void { + res.set('Access-Control-Allow-Origin', '*'); + res.set( + 'Access-Control-Allow-Methods', + 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD' + ); + res.set( + 'Access-Control-Allow-Headers', + 'Accept, Accept-Language, Content-Language, Content-Type, Authorization, ' + + 'X-Requested-With, Origin, Cache-Control, Pragma, Mcp-Session-Id, Mcp-Protocol-Version' + ); + res.set( + 'Access-Control-Expose-Headers', + 'WWW-Authenticate, Content-Length, Content-Type' + ); + res.set('Access-Control-Max-Age', '86400'); +} + +/** + * JSON-RPC 2.0 Request structure + */ +export interface JsonRpcRequest { + jsonrpc: '2.0'; + id?: string | number | null; + method: string; + params?: Record; +} + +/** + * JSON-RPC 2.0 Response structure + */ +export interface JsonRpcResponse { + jsonrpc: '2.0'; + id: string | number | null; + result?: unknown; + error?: JsonRpcError; +} + +/** + * JSON-RPC 2.0 Error structure + */ +export interface JsonRpcError { + code: number; + message: string; + data?: unknown; +} + +/** + * Standard JSON-RPC error codes + */ +export const JsonRpcErrorCode = { + PARSE_ERROR: -32700, + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32603, +} as const; + +/** + * Tool specification for MCP + */ +export interface ToolSpec { + name: string; + description: string; + inputSchema: { + type: 'object'; + properties: Record< + string, + { + type: string; + description?: string; + enum?: string[]; + } + >; + required?: string[]; + }; +} + +/** + * Tool execution result + */ +export interface ToolResult { + content: Array<{ + type: 'text' | 'image' | 'resource'; + text?: string; + data?: string; + mimeType?: string; + }>; + isError?: boolean; +} + +/** + * Tool handler function type + */ +export type ToolHandler = ( + args: Record, + request: AuthenticatedRequest +) => Promise | ToolResult; + +/** + * Registered tool with spec and handler + */ +interface RegisteredTool { + spec: ToolSpec; + handler: ToolHandler; +} + +/** + * MCP Handler class + * + * Manages tool registration and JSON-RPC request handling. + */ +export class McpHandler { + private tools: Map = new Map(); + private serverInfo = { + name: 'js-auth-mcp-server', + version: '1.0.0', + }; + + /** + * Register a tool with the handler + * + * @param name - Unique tool name + * @param spec - Tool specification (description, input schema) + * @param handler - Function to execute when tool is called + */ + registerTool( + name: string, + spec: Omit, + handler: ToolHandler + ): void { + this.tools.set(name, { + spec: { name, ...spec }, + handler, + }); + } + + /** + * Get list of registered tools + */ + getTools(): ToolSpec[] { + return Array.from(this.tools.values()).map((t) => t.spec); + } + + /** + * Handle a JSON-RPC request + * + * @param body - Request body + * @param req - Express request (for auth context) + * @returns JSON-RPC response + */ + async handleRequest( + body: unknown, + req: AuthenticatedRequest + ): Promise { + // Parse and validate request + const parseResult = this.parseRequest(body); + if ('error' in parseResult) { + return { + jsonrpc: '2.0', + id: null, + error: parseResult.error, + }; + } + + const request = parseResult.request; + const id = request.id ?? null; + + try { + const result = await this.dispatchMethod( + request.method, + request.params || {}, + req + ); + return { + jsonrpc: '2.0', + id, + result, + }; + } catch (error) { + return { + jsonrpc: '2.0', + id, + error: this.toJsonRpcError(error), + }; + } + } + + /** + * Parse and validate a JSON-RPC request + */ + private parseRequest( + body: unknown + ): { request: JsonRpcRequest } | { error: JsonRpcError } { + if (!body || typeof body !== 'object') { + return { + error: { + code: JsonRpcErrorCode.INVALID_REQUEST, + message: 'Invalid request: expected object', + }, + }; + } + + const obj = body as Record; + + if (obj.jsonrpc !== '2.0') { + return { + error: { + code: JsonRpcErrorCode.INVALID_REQUEST, + message: 'Invalid request: jsonrpc must be "2.0"', + }, + }; + } + + if (typeof obj.method !== 'string') { + return { + error: { + code: JsonRpcErrorCode.INVALID_REQUEST, + message: 'Invalid request: method must be a string', + }, + }; + } + + if (obj.params !== undefined && typeof obj.params !== 'object') { + return { + error: { + code: JsonRpcErrorCode.INVALID_PARAMS, + message: 'Invalid params: must be an object', + }, + }; + } + + return { + request: { + jsonrpc: '2.0', + id: obj.id as string | number | null | undefined, + method: obj.method, + params: obj.params as Record | undefined, + }, + }; + } + + /** + * Dispatch a method call to the appropriate handler + */ + private async dispatchMethod( + method: string, + params: Record, + req: AuthenticatedRequest + ): Promise { + switch (method) { + case 'initialize': + return this.handleInitialize(params); + + case 'tools/list': + return this.handleToolsList(); + + case 'tools/call': + return this.handleToolsCall(params, req); + + case 'ping': + return {}; + + default: + throw { + code: JsonRpcErrorCode.METHOD_NOT_FOUND, + message: `Method not found: ${method}`, + }; + } + } + + /** + * Handle initialize method + */ + private handleInitialize(params: Record): unknown { + return { + protocolVersion: '2024-11-05', + capabilities: { + tools: {}, + }, + serverInfo: this.serverInfo, + }; + } + + /** + * Handle tools/list method + */ + private handleToolsList(): unknown { + return { + tools: this.getTools(), + }; + } + + /** + * Handle tools/call method + */ + private async handleToolsCall( + params: Record, + req: AuthenticatedRequest + ): Promise { + const name = params.name; + const args = params.arguments || {}; + + if (typeof name !== 'string') { + throw { + code: JsonRpcErrorCode.INVALID_PARAMS, + message: 'Invalid params: name must be a string', + }; + } + + const tool = this.tools.get(name); + if (!tool) { + throw { + code: JsonRpcErrorCode.METHOD_NOT_FOUND, + message: `Tool not found: ${name}`, + }; + } + + const result = await tool.handler(args as Record, req); + return result; + } + + /** + * Convert an error to JSON-RPC error format + */ + private toJsonRpcError(error: unknown): JsonRpcError { + if ( + typeof error === 'object' && + error !== null && + 'code' in error && + 'message' in error + ) { + return error as JsonRpcError; + } + + if (error instanceof Error) { + return { + code: JsonRpcErrorCode.INTERNAL_ERROR, + message: error.message, + }; + } + + return { + code: JsonRpcErrorCode.INTERNAL_ERROR, + message: 'Internal error', + }; + } +} + +/** + * Register MCP handler with Express app + * + * @param app - Express application + * @returns McpHandler instance for tool registration + */ +export function registerMcpHandler(app: Express): McpHandler { + const handler = new McpHandler(); + + // OPTIONS handler for /mcp CORS preflight + app.options('/mcp', (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + }); + + // OPTIONS handler for /rpc CORS preflight + app.options('/rpc', (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + }); + + app.post('/mcp', async (req: Request, res: Response) => { + const response = await handler.handleRequest( + req.body, + req as AuthenticatedRequest + ); + setCorsHeaders(res); + res.status(200).json(response); + }); + + // Also support /rpc endpoint + app.post('/rpc', async (req: Request, res: Response) => { + const response = await handler.handleRequest( + req.body, + req as AuthenticatedRequest + ); + setCorsHeaders(res); + res.status(200).json(response); + }); + + return handler; +} diff --git a/examples/auth/src/routes/oauth-endpoints.ts b/examples/auth/src/routes/oauth-endpoints.ts new file mode 100644 index 00000000..2fe6f7e2 --- /dev/null +++ b/examples/auth/src/routes/oauth-endpoints.ts @@ -0,0 +1,291 @@ +/** + * OAuth Discovery Endpoints + * + * Implements OAuth 2.0 discovery endpoints: + * - /.well-known/oauth-protected-resource (RFC 9728) + * - /.well-known/oauth-authorization-server (RFC 8414) + * - /.well-known/openid-configuration + * - /oauth/authorize (redirect to IdP) + */ + +import { Express, Request, Response } from 'express'; +import { AuthServerConfig } from '../config'; + +/** + * Set common CORS headers on response + * + * @param res - Express response object + */ +function setCorsHeaders(res: Response): void { + res.set('Access-Control-Allow-Origin', '*'); + res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.set( + 'Access-Control-Allow-Headers', + 'Authorization, Content-Type, Accept, Origin, X-Requested-With' + ); + res.set('Access-Control-Expose-Headers', 'WWW-Authenticate, Content-Length'); + res.set('Access-Control-Max-Age', '86400'); +} + +/** + * OAuth Protected Resource Metadata (RFC 9728) + */ +export interface ProtectedResourceMetadata { + resource: string; + authorization_servers: string[]; + scopes_supported?: string[]; + bearer_methods_supported?: string[]; + resource_documentation?: string; +} + +/** + * OAuth Authorization Server Metadata (RFC 8414) + */ +export interface AuthorizationServerMetadata { + issuer: string; + authorization_endpoint: string; + token_endpoint: string; + jwks_uri?: string; + registration_endpoint?: string; + scopes_supported?: string[]; + response_types_supported: string[]; + grant_types_supported?: string[]; + token_endpoint_auth_methods_supported?: string[]; + code_challenge_methods_supported?: string[]; +} + +/** + * OpenID Connect Discovery Metadata + */ +export interface OpenIDConfiguration extends AuthorizationServerMetadata { + userinfo_endpoint?: string; + subject_types_supported?: string[]; + id_token_signing_alg_values_supported?: string[]; +} + +/** + * Register OAuth discovery endpoints + * + * @param app - Express application instance + * @param config - Server configuration + */ +export function registerOAuthEndpoints( + app: Express, + config: AuthServerConfig +): void { + // OPTIONS handler for CORS preflight - applies to all .well-known endpoints + app.options( + '/.well-known/oauth-protected-resource', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + } + ); + + // RFC 9728: Resource-specific discovery URL for /mcp endpoint + // MCP Inspector requests: /.well-known/oauth-protected-resource/mcp + app.options( + '/.well-known/oauth-protected-resource/mcp', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + } + ); + + app.options( + '/.well-known/oauth-authorization-server', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + } + ); + + app.options( + '/.well-known/openid-configuration', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + } + ); + + app.options('/oauth/authorize', (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + }); + + app.options('/oauth/register', (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(204).set('Content-Length', '0').end(); + }); + + // Helper to build protected resource metadata + const buildProtectedResourceMetadata = (): ProtectedResourceMetadata => ({ + resource: `${config.serverUrl}/mcp`, + authorization_servers: [config.serverUrl], + scopes_supported: config.allowedScopes.split(' ').filter(Boolean), + bearer_methods_supported: ['header', 'query'], + resource_documentation: `${config.serverUrl}/docs`, + }); + + // RFC 9728 - Protected Resource Metadata (root) + app.get( + '/.well-known/oauth-protected-resource', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(200).json(buildProtectedResourceMetadata()); + } + ); + + // RFC 9728: Resource-specific discovery URL for /mcp endpoint + // MCP Inspector requests: /.well-known/oauth-protected-resource/mcp + app.get( + '/.well-known/oauth-protected-resource/mcp', + (_req: Request, res: Response) => { + setCorsHeaders(res); + res.status(200).json(buildProtectedResourceMetadata()); + } + ); + + // RFC 8414 - OAuth Authorization Server Metadata + app.get( + '/.well-known/oauth-authorization-server', + (_req: Request, res: Response) => { + const authEndpoint = + config.oauthAuthorizeUrl || + `${config.authServerUrl}/protocol/openid-connect/auth`; + const tokenEndpoint = + config.oauthTokenUrl || + `${config.authServerUrl}/protocol/openid-connect/token`; + + const metadata: AuthorizationServerMetadata = { + issuer: config.issuer || config.serverUrl, + authorization_endpoint: authEndpoint, + token_endpoint: tokenEndpoint, + jwks_uri: config.jwksUri, + registration_endpoint: `${config.serverUrl}/oauth/register`, + scopes_supported: config.allowedScopes.split(' ').filter(Boolean), + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + 'none', + ], + code_challenge_methods_supported: ['S256'], + }; + + setCorsHeaders(res); + res.status(200).json(metadata); + } + ); + + // OpenID Connect Discovery + app.get( + '/.well-known/openid-configuration', + (_req: Request, res: Response) => { + const authEndpoint = + config.oauthAuthorizeUrl || + `${config.authServerUrl}/protocol/openid-connect/auth`; + const tokenEndpoint = + config.oauthTokenUrl || + `${config.authServerUrl}/protocol/openid-connect/token`; + + const baseScopes = ['openid', 'profile', 'email']; + const customScopes = config.allowedScopes.split(' ').filter(Boolean); + const allScopes = [...new Set([...baseScopes, ...customScopes])]; + + const metadata: OpenIDConfiguration = { + issuer: config.issuer || config.serverUrl, + authorization_endpoint: authEndpoint, + token_endpoint: tokenEndpoint, + jwks_uri: config.jwksUri, + userinfo_endpoint: config.authServerUrl + ? `${config.authServerUrl}/protocol/openid-connect/userinfo` + : undefined, + scopes_supported: allScopes, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code', 'refresh_token'], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + ], + code_challenge_methods_supported: ['S256'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + }; + + setCorsHeaders(res); + res.status(200).json(metadata); + } + ); + + // OAuth Authorization redirect + app.get('/oauth/authorize', (req: Request, res: Response) => { + const authEndpoint = + config.oauthAuthorizeUrl || + `${config.authServerUrl}/protocol/openid-connect/auth`; + + try { + const authUrl = new URL(authEndpoint); + + // Forward all query parameters + for (const [key, value] of Object.entries(req.query)) { + if (typeof value === 'string') { + authUrl.searchParams.set(key, value); + } else if (Array.isArray(value)) { + // Handle array parameters (use first value) + const firstValue = value[0]; + if (typeof firstValue === 'string') { + authUrl.searchParams.set(key, firstValue); + } + } + } + + setCorsHeaders(res); + res.redirect(302, authUrl.toString()); + } catch (error) { + setCorsHeaders(res); + res.status(500).json({ + error: 'server_error', + error_description: 'Failed to construct authorization URL', + }); + } + }); + + // POST /oauth/register - Dynamic Client Registration (RFC 7591) + // Returns pre-configured credentials (stateless mode for MCP) + app.post('/oauth/register', (req: Request, res: Response) => { + const body = req.body || {}; + + // Extract redirect_uris from request + let redirectUris: string[] = []; + if (Array.isArray(body.redirect_uris)) { + redirectUris = body.redirect_uris.filter( + (uri: unknown) => typeof uri === 'string' + ); + } + + // Return pre-configured credentials (stateless mode) + // This allows MCP clients to "register" and receive the server's OAuth credentials + const registration = { + client_id: config.clientId, + client_secret: config.clientSecret || undefined, + client_id_issued_at: Math.floor(Date.now() / 1000), + client_secret_expires_at: 0, // Never expires + redirect_uris: redirectUris, + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + token_endpoint_auth_method: config.clientSecret + ? 'client_secret_post' + : 'none', + }; + + // Remove undefined values + const cleanedRegistration = Object.fromEntries( + Object.entries(registration).filter(([_, v]) => v !== undefined) + ); + + setCorsHeaders(res); + res.status(201).json(cleanedRegistration); + }); +} diff --git a/examples/auth/src/tools/__tests__/weather-tools.test.ts b/examples/auth/src/tools/__tests__/weather-tools.test.ts new file mode 100644 index 00000000..b9eb77ba --- /dev/null +++ b/examples/auth/src/tools/__tests__/weather-tools.test.ts @@ -0,0 +1,397 @@ +import { + hasScope, + getSimulatedWeather, + getSimulatedForecast, + getSimulatedAlerts, + registerWeatherTools, +} from '../weather-tools'; +import { McpHandler } from '../../routes/mcp-handler'; +import { OAuthAuthMiddleware } from '../../middleware/oauth-auth'; +import { AuthenticatedRequest } from '../../middleware/oauth-auth'; +import { createDefaultConfig } from '../../config'; + +describe('hasScope', () => { + it('should return true when scope is present', () => { + expect(hasScope('openid profile mcp:read', 'mcp:read')).toBe(true); + }); + + it('should return false when scope is not present', () => { + expect(hasScope('openid profile', 'mcp:read')).toBe(false); + }); + + it('should return false for empty scopes', () => { + expect(hasScope('', 'mcp:read')).toBe(false); + }); + + it('should return false for null or undefined scopes', () => { + expect(hasScope(null as unknown as string, 'mcp:read')).toBe(false); + expect(hasScope(undefined as unknown as string, 'mcp:read')).toBe(false); + }); + + it('should return false for empty required scope', () => { + expect(hasScope('openid profile', '')).toBe(false); + }); + + it('should not match partial scope names', () => { + expect(hasScope('mcp:readonly', 'mcp:read')).toBe(false); + }); + + it('should match exact scope names', () => { + expect(hasScope('mcp:read mcp:write', 'mcp:write')).toBe(true); + }); +}); + +describe('getSimulatedWeather', () => { + it('should return weather data for a city', () => { + const weather = getSimulatedWeather('London'); + + expect(weather.city).toBe('London'); + expect(weather.temperature).toBeGreaterThanOrEqual(10); + expect(weather.temperature).toBeLessThanOrEqual(35); + expect(weather.condition).toBeDefined(); + expect(weather.humidity).toBeGreaterThanOrEqual(40); + expect(weather.humidity).toBeLessThanOrEqual(80); + expect(weather.windSpeed).toBeGreaterThanOrEqual(5); + expect(weather.windSpeed).toBeLessThanOrEqual(30); + }); + + it('should return consistent results for the same city', () => { + const weather1 = getSimulatedWeather('Paris'); + const weather2 = getSimulatedWeather('Paris'); + + expect(weather1).toEqual(weather2); + }); + + it('should return different results for different cities', () => { + const weather1 = getSimulatedWeather('Tokyo'); + const weather2 = getSimulatedWeather('Sydney'); + + // At least one property should be different + const sameTemp = weather1.temperature === weather2.temperature; + const sameCondition = weather1.condition === weather2.condition; + expect(sameTemp && sameCondition).toBe(false); + }); +}); + +describe('getSimulatedForecast', () => { + it('should return 5-day forecast', () => { + const forecast = getSimulatedForecast('Berlin'); + + expect(forecast).toHaveLength(5); + expect(forecast[0].day).toBe('Today'); + expect(forecast[1].day).toBe('Tomorrow'); + }); + + it('should have high greater than low for each day', () => { + const forecast = getSimulatedForecast('Rome'); + + forecast.forEach((day) => { + expect(day.high).toBeGreaterThan(day.low); + }); + }); + + it('should return consistent results for the same city', () => { + const forecast1 = getSimulatedForecast('Madrid'); + const forecast2 = getSimulatedForecast('Madrid'); + + expect(forecast1).toEqual(forecast2); + }); +}); + +describe('getSimulatedAlerts', () => { + it('should return an array of alerts', () => { + const alerts = getSimulatedAlerts('California'); + + expect(Array.isArray(alerts)).toBe(true); + }); + + it('should return consistent results for the same region', () => { + const alerts1 = getSimulatedAlerts('Texas'); + const alerts2 = getSimulatedAlerts('Texas'); + + expect(alerts1).toEqual(alerts2); + }); + + it('should have valid alert structure when alerts exist', () => { + // Try a few regions to find one with alerts + const regions = ['Region1', 'Region2', 'Region3', 'Region4', 'Region5']; + let foundAlerts = false; + + for (const region of regions) { + const alerts = getSimulatedAlerts(region); + if (alerts.length > 0) { + foundAlerts = true; + expect(alerts[0]).toHaveProperty('type'); + expect(alerts[0]).toHaveProperty('severity'); + expect(alerts[0]).toHaveProperty('message'); + break; + } + } + + expect(foundAlerts).toBe(true); + }); +}); + +describe('registerWeatherTools', () => { + let mcpHandler: McpHandler; + const mockReq = {} as AuthenticatedRequest; + + beforeEach(() => { + mcpHandler = new McpHandler(); + }); + + it('should register three weather tools', () => { + registerWeatherTools(mcpHandler, null); + + const tools = mcpHandler.getTools(); + expect(tools).toHaveLength(3); + + const toolNames = tools.map((t) => t.name); + expect(toolNames).toContain('get-weather'); + expect(toolNames).toContain('get-forecast'); + expect(toolNames).toContain('get-weather-alerts'); + }); + + describe('get-weather tool', () => { + it('should return weather data without auth', async () => { + registerWeatherTools(mcpHandler, null); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather', arguments: { city: 'Seattle' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { content: Array<{ text: string }> }; + const data = JSON.parse(result.content[0].text); + expect(data.city).toBe('Seattle'); + expect(data.temperature).toBeDefined(); + }); + + it('should work with auth middleware in disabled mode', async () => { + const config = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, config); + registerWeatherTools(mcpHandler, middleware); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather', arguments: { city: 'Boston' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + }); + }); + + describe('get-forecast tool', () => { + it('should return forecast data when auth is disabled', async () => { + const config = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, config); + registerWeatherTools(mcpHandler, middleware); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-forecast', arguments: { city: 'Denver' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { content: Array<{ text: string }> }; + const data = JSON.parse(result.content[0].text); + expect(data.city).toBe('Denver'); + expect(data.forecast).toHaveLength(5); + }); + + it('should return access denied without mcp:read scope', async () => { + const config = createDefaultConfig({ + authDisabled: false, + clientId: 'test', + clientSecret: 'secret', + jwksUri: 'https://example.com/.well-known/jwks.json', + }); + const middleware = new OAuthAuthMiddleware(null, config); + registerWeatherTools(mcpHandler, middleware); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-forecast', arguments: { city: 'Portland' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { + content: Array<{ text: string }>; + isError?: boolean; + }; + expect(result.isError).toBe(true); + const data = JSON.parse(result.content[0].text); + expect(data.error).toBe('access_denied'); + expect(data.message).toContain('mcp:read'); + }); + + it('should return data when no auth middleware provided', async () => { + registerWeatherTools(mcpHandler, null); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-forecast', arguments: { city: 'Miami' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { content: Array<{ text: string }> }; + const data = JSON.parse(result.content[0].text); + expect(data.city).toBe('Miami'); + }); + }); + + describe('get-weather-alerts tool', () => { + it('should return alerts data when auth is disabled', async () => { + const config = createDefaultConfig({ authDisabled: true }); + const middleware = new OAuthAuthMiddleware(null, config); + registerWeatherTools(mcpHandler, middleware); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather-alerts', arguments: { region: 'West' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { content: Array<{ text: string }> }; + const data = JSON.parse(result.content[0].text); + expect(data.region).toBe('West'); + expect(Array.isArray(data.alerts)).toBe(true); + }); + + it('should return access denied without mcp:admin scope', async () => { + const config = createDefaultConfig({ + authDisabled: false, + clientId: 'test', + clientSecret: 'secret', + jwksUri: 'https://example.com/.well-known/jwks.json', + }); + const middleware = new OAuthAuthMiddleware(null, config); + registerWeatherTools(mcpHandler, middleware); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: 'get-weather-alerts', arguments: { region: 'East' } }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { + content: Array<{ text: string }>; + isError?: boolean; + }; + expect(result.isError).toBe(true); + const data = JSON.parse(result.content[0].text); + expect(data.error).toBe('access_denied'); + expect(data.message).toContain('mcp:admin'); + }); + + it('should return data when no auth middleware provided', async () => { + registerWeatherTools(mcpHandler, null); + + const response = await mcpHandler.handleRequest( + { + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { + name: 'get-weather-alerts', + arguments: { region: 'Central' }, + }, + }, + mockReq + ); + + expect(response.error).toBeUndefined(); + const result = response.result as { content: Array<{ text: string }> }; + const data = JSON.parse(result.content[0].text); + expect(data.region).toBe('Central'); + }); + }); + + describe('tool specifications', () => { + beforeEach(() => { + registerWeatherTools(mcpHandler, null); + }); + + it('should have proper input schema for get-weather', () => { + const tools = mcpHandler.getTools(); + const getWeather = tools.find((t) => t.name === 'get-weather'); + + expect(getWeather?.inputSchema.properties.city).toEqual({ + type: 'string', + description: 'City name to get weather for', + }); + expect(getWeather?.inputSchema.required).toContain('city'); + }); + + it('should have proper input schema for get-forecast', () => { + const tools = mcpHandler.getTools(); + const getForecast = tools.find((t) => t.name === 'get-forecast'); + + expect(getForecast?.inputSchema.properties.city).toEqual({ + type: 'string', + description: 'City name to get forecast for', + }); + expect(getForecast?.inputSchema.required).toContain('city'); + }); + + it('should have proper input schema for get-weather-alerts', () => { + const tools = mcpHandler.getTools(); + const getAlerts = tools.find((t) => t.name === 'get-weather-alerts'); + + expect(getAlerts?.inputSchema.properties.region).toEqual({ + type: 'string', + description: 'Region name to get alerts for', + }); + expect(getAlerts?.inputSchema.required).toContain('region'); + }); + + it('should have descriptions mentioning required scopes', () => { + const tools = mcpHandler.getTools(); + + const getWeather = tools.find((t) => t.name === 'get-weather'); + expect(getWeather?.description).toContain('No authentication required'); + + const getForecast = tools.find((t) => t.name === 'get-forecast'); + expect(getForecast?.description).toContain('mcp:read'); + + const getAlerts = tools.find((t) => t.name === 'get-weather-alerts'); + expect(getAlerts?.description).toContain('mcp:admin'); + }); + }); +}); diff --git a/examples/auth/src/tools/weather-tools.ts b/examples/auth/src/tools/weather-tools.ts new file mode 100644 index 00000000..2ca01b72 --- /dev/null +++ b/examples/auth/src/tools/weather-tools.ts @@ -0,0 +1,298 @@ +/** + * Weather Tools + * + * Example MCP tools demonstrating OAuth scope-based access control. + * Mirrors the weather tools from the C++ auth example. + */ + +import { McpHandler, ToolResult } from '../routes/mcp-handler'; +import { OAuthAuthMiddleware } from '../middleware/oauth-auth'; + +/** + * Check if a scope is present in a space-separated scope string + * + * @param scopes - Space-separated scope string + * @param required - Required scope to check for + * @returns true if the required scope is present + */ +export function hasScope(scopes: string, required: string): boolean { + if (!scopes || !required) { + return false; + } + return scopes.split(' ').includes(required); +} + +/** + * Weather conditions for simulation + */ +const CONDITIONS = [ + 'Sunny', + 'Cloudy', + 'Rainy', + 'Partly Cloudy', + 'Windy', + 'Stormy', +]; + +/** + * Get a deterministic but varying condition based on city name + */ +function getConditionForCity(city: string, offset: number = 0): string { + const hash = city + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + return CONDITIONS[(hash + offset) % CONDITIONS.length]; +} + +/** + * Get a deterministic but varying temperature based on city name + */ +function getTempForCity(city: string, offset: number = 0): number { + const hash = city + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + // Temperature between 10-35 Celsius + return 10 + ((hash + offset * 7) % 26); +} + +/** + * Get simulated current weather for a city + * + * @param city - City name + * @returns Weather data object + */ +export function getSimulatedWeather(city: string): { + city: string; + temperature: number; + condition: string; + humidity: number; + windSpeed: number; +} { + const hash = city + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + + return { + city, + temperature: getTempForCity(city), + condition: getConditionForCity(city), + humidity: 40 + (hash % 40), // 40-80% + windSpeed: 5 + (hash % 25), // 5-30 km/h + }; +} + +/** + * Get simulated 5-day forecast for a city + * + * @param city - City name + * @returns Array of daily forecasts + */ +export function getSimulatedForecast(city: string): Array<{ + day: string; + high: number; + low: number; + condition: string; +}> { + const days = ['Today', 'Tomorrow', 'Day 3', 'Day 4', 'Day 5']; + + return days.map((day, index) => ({ + day, + high: getTempForCity(city, index) + 5, + low: getTempForCity(city, index) - 5, + condition: getConditionForCity(city, index), + })); +} + +/** + * Get simulated weather alerts for a region + * + * @param region - Region name + * @returns Array of weather alerts + */ +export function getSimulatedAlerts(region: string): Array<{ + type: string; + severity: string; + message: string; +}> { + const hash = region + .split('') + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + + // Return different alerts based on region + if (hash % 3 === 0) { + return [ + { + type: 'Heat Warning', + severity: 'moderate', + message: `High temperatures expected in ${region}. Stay hydrated.`, + }, + ]; + } else if (hash % 3 === 1) { + return [ + { + type: 'Storm Watch', + severity: 'high', + message: `Severe thunderstorms possible in ${region}. Seek shelter if needed.`, + }, + { + type: 'Wind Advisory', + severity: 'low', + message: `Strong winds expected in ${region}. Secure loose objects.`, + }, + ]; + } else { + return []; // No alerts + } +} + +/** + * Create an access denied error result + * + * @param scope - Required scope that was missing + * @returns ToolResult with error + */ +function accessDenied(scope: string): ToolResult { + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + error: 'access_denied', + message: `Access denied. Required scope: ${scope}`, + }), + }, + ], + isError: true, + }; +} + +/** + * Register weather tools with the MCP handler + * + * @param mcp - MCP handler instance + * @param authMiddleware - OAuth auth middleware instance (null if auth disabled) + */ +export function registerWeatherTools( + mcp: McpHandler, + authMiddleware: OAuthAuthMiddleware | null +): void { + /** + * get-weather - No authentication required + * Returns current weather for a specified city. + */ + mcp.registerTool( + 'get-weather', + { + description: + 'Get current weather for a city. No authentication required.', + inputSchema: { + type: 'object', + properties: { + city: { + type: 'string', + description: 'City name to get weather for', + }, + }, + required: ['city'], + }, + }, + (args) => { + const city = String(args.city || 'Unknown'); + const weather = getSimulatedWeather(city); + + return { + content: [ + { + type: 'text', + text: JSON.stringify(weather, null, 2), + }, + ], + }; + } + ); + + /** + * get-forecast - Requires mcp:read scope + * Returns 5-day weather forecast for a specified city. + */ + mcp.registerTool( + 'get-forecast', + { + description: + 'Get 5-day weather forecast for a city. Requires mcp:read scope.', + inputSchema: { + type: 'object', + properties: { + city: { + type: 'string', + description: 'City name to get forecast for', + }, + }, + required: ['city'], + }, + }, + (args, req) => { + // Check scope if auth is enabled + if (authMiddleware && !authMiddleware.isAuthDisabled()) { + const context = authMiddleware.getAuthContext(); + if (!hasScope(context.scopes, 'mcp:read')) { + return accessDenied('mcp:read'); + } + } + + const city = String(args.city || 'Unknown'); + const forecast = getSimulatedForecast(city); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ city, forecast }, null, 2), + }, + ], + }; + } + ); + + /** + * get-weather-alerts - Requires mcp:admin scope + * Returns weather alerts for a specified region. + */ + mcp.registerTool( + 'get-weather-alerts', + { + description: 'Get weather alerts for a region. Requires mcp:admin scope.', + inputSchema: { + type: 'object', + properties: { + region: { + type: 'string', + description: 'Region name to get alerts for', + }, + }, + required: ['region'], + }, + }, + (args, req) => { + // Check scope if auth is enabled + if (authMiddleware && !authMiddleware.isAuthDisabled()) { + const context = authMiddleware.getAuthContext(); + if (!hasScope(context.scopes, 'mcp:admin')) { + return accessDenied('mcp:admin'); + } + } + + const region = String(args.region || 'Unknown'); + const alerts = getSimulatedAlerts(region); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ region, alerts }, null, 2), + }, + ], + }; + } + ); +} diff --git a/examples/auth/tsconfig.json b/examples/auth/tsconfig.json new file mode 100644 index 00000000..93078bdc --- /dev/null +++ b/examples/auth/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/__tests__/**"] +} diff --git a/examples/client_example_api.ts b/examples/client_example_api.ts index 96143244..9956c3f2 100644 --- a/examples/client_example_api.ts +++ b/examples/client_example_api.ts @@ -26,7 +26,9 @@ function main(): void { try { // Create agent with API key - fetches server config from Gopher API console.log('Creating agent with API key...'); - console.log(` Calling createWithApiKey("${provider}", "${model}", "${apiKey.substring(0, 10)}...")`); + console.log( + ` Calling createWithApiKey("${provider}", "${model}", "${apiKey.substring(0, 10)}...")` + ); const agent = GopherAgent.createWithApiKey(provider, model, apiKey); console.log('GopherAgent created successfully!'); console.log(` Agent handle: ${agent ? 'valid' : 'null'}`); @@ -34,7 +36,8 @@ function main(): void { // Get question from command line args or use default const args = process.argv.slice(2); - const question = args.length > 0 ? args.join(' ') : 'List all my Gmail drafts.'; + const question = + args.length > 0 ? args.join(' ') : 'List all my Gmail drafts.'; console.log(`Question: ${question}`); console.log(''); diff --git a/examples/npm/client_example_api.ts b/examples/npm/client_example_api.ts index 606e5085..ee6b5798 100644 --- a/examples/npm/client_example_api.ts +++ b/examples/npm/client_example_api.ts @@ -27,7 +27,9 @@ function main(): void { try { // Create agent with API key - fetches server config from Gopher API console.log('Creating agent with API key...'); - console.log(` Calling createWithApiKey("${provider}", "${model}", "${apiKey.substring(0, 10)}...")`); + console.log( + ` Calling createWithApiKey("${provider}", "${model}", "${apiKey.substring(0, 10)}...")` + ); const agent = GopherAgent.createWithApiKey(provider, model, apiKey); console.log('GopherAgent created successfully!'); console.log(` Agent handle: ${agent ? 'valid' : 'null'}`); @@ -35,7 +37,8 @@ function main(): void { // Get question from command line args or use default const args = process.argv.slice(2); - const question = args.length > 0 ? args.join(' ') : 'List all my Gmail drafts.'; + const question = + args.length > 0 ? args.join(' ') : 'List all my Gmail drafts.'; console.log(`Question: ${question}`); console.log(''); diff --git a/package-lock.json b/package-lock.json index c2b12ea9..2354f6aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gopher.security/gopher-mcp-js", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gopher.security/gopher-mcp-js", - "version": "0.1.1", + "version": "0.1.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -29,12 +29,12 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@gopher.security/gopher-orch-darwin-arm64": "0.1.1", - "@gopher.security/gopher-orch-darwin-x64": "0.1.1", - "@gopher.security/gopher-orch-linux-arm64": "0.1.1", - "@gopher.security/gopher-orch-linux-x64": "0.1.1", - "@gopher.security/gopher-orch-win32-arm64": "0.1.1", - "@gopher.security/gopher-orch-win32-x64": "0.1.1" + "@gopher.security/gopher-orch-darwin-arm64": "0.1.2", + "@gopher.security/gopher-orch-darwin-x64": "0.1.2", + "@gopher.security/gopher-orch-linux-arm64": "0.1.2", + "@gopher.security/gopher-orch-linux-x64": "0.1.2", + "@gopher.security/gopher-orch-win32-arm64": "0.1.2", + "@gopher.security/gopher-orch-win32-x64": "0.1.2" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 212461ac..07b17c08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-mcp-js", - "version": "0.1.1", + "version": "0.1.2", "description": "TypeScript SDK for Gopher Orch - AI Agent orchestration framework with native performance", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -40,12 +40,12 @@ "koffi": "^2.9.0" }, "optionalDependencies": { - "@gopher.security/gopher-orch-darwin-arm64": "0.1.1", - "@gopher.security/gopher-orch-darwin-x64": "0.1.1", - "@gopher.security/gopher-orch-linux-arm64": "0.1.1", - "@gopher.security/gopher-orch-linux-x64": "0.1.1", - "@gopher.security/gopher-orch-win32-arm64": "0.1.1", - "@gopher.security/gopher-orch-win32-x64": "0.1.1" + "@gopher.security/gopher-orch-darwin-arm64": "0.1.2", + "@gopher.security/gopher-orch-darwin-x64": "0.1.2", + "@gopher.security/gopher-orch-linux-arm64": "0.1.2", + "@gopher.security/gopher-orch-linux-x64": "0.1.2", + "@gopher.security/gopher-orch-win32-arm64": "0.1.2", + "@gopher.security/gopher-orch-win32-x64": "0.1.2" }, "devDependencies": { "@types/jest": "^29.5.11", diff --git a/packages/darwin-arm64/package.json b/packages/darwin-arm64/package.json index 706de968..b6323236 100644 --- a/packages/darwin-arm64/package.json +++ b/packages/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-darwin-arm64", - "version": "0.1.1", + "version": "0.1.2", "description": "macOS ARM64 (Apple Silicon) native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/packages/darwin-x64/package.json b/packages/darwin-x64/package.json index 9f5bd475..34cc93cc 100644 --- a/packages/darwin-x64/package.json +++ b/packages/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-darwin-x64", - "version": "0.1.1", + "version": "0.1.2", "description": "macOS x64 (Intel) native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/packages/linux-arm64/package.json b/packages/linux-arm64/package.json index 8334cd49..9a8c8e52 100644 --- a/packages/linux-arm64/package.json +++ b/packages/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-linux-arm64", - "version": "0.1.1", + "version": "0.1.2", "description": "Linux ARM64 native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/packages/linux-x64/package.json b/packages/linux-x64/package.json index 7ed86581..76fbf55c 100644 --- a/packages/linux-x64/package.json +++ b/packages/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-linux-x64", - "version": "0.1.1", + "version": "0.1.2", "description": "Linux x64 native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/packages/win32-arm64/package.json b/packages/win32-arm64/package.json index 20f2034d..0c8e6c3e 100644 --- a/packages/win32-arm64/package.json +++ b/packages/win32-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-win32-arm64", - "version": "0.1.1", + "version": "0.1.2", "description": "Windows ARM64 native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/packages/win32-x64/package.json b/packages/win32-x64/package.json index 4c31931f..9f99a0f6 100644 --- a/packages/win32-x64/package.json +++ b/packages/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@gopher.security/gopher-orch-win32-x64", - "version": "0.1.1", + "version": "0.1.2", "description": "Windows x64 native binaries for gopher-orch", "license": "MIT", "repository": { diff --git a/src/agent.ts b/src/agent.ts index ba544f8d..b04dc13d 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -133,7 +133,9 @@ export class GopherAgent { lib.clearError(); if (errorInfo) { const details = errorInfo.details ? `: ${errorInfo.details}` : ''; - throw new AgentError(`${errorInfo.message ?? 'Failed to create agent'}${details}`); + throw new AgentError( + `${errorInfo.message ?? 'Failed to create agent'}${details}` + ); } throw new AgentError('Failed to create agent'); } diff --git a/src/ffi/auth/auth-client.ts b/src/ffi/auth/auth-client.ts new file mode 100644 index 00000000..72e101cc --- /dev/null +++ b/src/ffi/auth/auth-client.ts @@ -0,0 +1,336 @@ +/** + * GopherAuthClient - High-level wrapper for gopher-auth token validation + * + * Provides a TypeScript-friendly API for JWT token validation using + * the gopher-auth native library. + */ + +import { loadLibrary, isLibraryLoaded, getAuthFunctions } from './loader'; +import { ValidationResult, TokenPayload, GopherAuthError } from './types'; +import { GopherValidationOptions } from './validation-options'; + +// Track library initialization state +let libraryInitialized = false; + +/** + * Initialize the gopher-auth library + * + * Must be called before creating GopherAuthClient instances. + * + * @throws Error if library initialization fails + */ +export function gopherInitAuthLibrary(): void { + if (libraryInitialized) { + return; + } + + if (!loadLibrary()) { + throw new Error( + 'Failed to load gopher-auth library (ensure libgopher-orch is in native/lib/)' + ); + } + + const fns = getAuthFunctions(); + if (!fns.authInit) { + throw new Error('Auth library not properly loaded'); + } + + const result = fns.authInit(); + if (result !== 0) { + throw new Error(`Failed to initialize auth library: error code ${result}`); + } + + libraryInitialized = true; +} + +/** + * Shutdown the gopher-auth library + * + * Should be called when the application is shutting down. + */ +export function gopherShutdownAuthLibrary(): void { + if (!libraryInitialized) { + return; + } + + const fns = getAuthFunctions(); + if (fns.authShutdown) { + fns.authShutdown(); + } + + libraryInitialized = false; +} + +/** + * Get the gopher-auth library version + * + * @returns Version string or 'unknown' if not available + */ +export function gopherGetAuthLibraryVersion(): string { + if (!isLibraryLoaded()) { + loadLibrary(); + } + + const fns = getAuthFunctions(); + if (!fns.authVersion) { + return 'unknown'; + } + + return fns.authVersion() || 'unknown'; +} + +/** + * Check if the auth library is initialized + */ +export function gopherIsAuthLibraryInitialized(): boolean { + return libraryInitialized; +} + +/** + * Generate WWW-Authenticate header for 401 responses + * + * @param realm - Authentication realm + * @param error - OAuth error code + * @param description - Human-readable error description + * @returns WWW-Authenticate header value + */ +export function gopherGenerateWwwAuthenticateHeader( + realm: string, + error: string, + description: string +): string { + if (!isLibraryLoaded()) { + throw new Error('Auth library not loaded'); + } + + const fns = getAuthFunctions(); + if (!fns.generateWwwAuthenticate) { + throw new Error('Function not available'); + } + + const result = fns.generateWwwAuthenticate(realm, error, description); + if (!result) { + throw new Error('Failed to generate WWW-Authenticate header'); + } + + return result; +} + +/** + * Generate WWW-Authenticate header v2 (RFC 9728 compliant) + * + * @param resource - Resource server URL + * @param resourceMetadataUrl - URL to OAuth Protected Resource metadata + * @param scopes - Required scopes (space-separated) + * @param error - OAuth error code + * @param description - Human-readable error description + * @returns WWW-Authenticate header value + */ +export function gopherGenerateWwwAuthenticateHeaderV2( + resource: string, + resourceMetadataUrl: string, + scopes: string, + error: string, + description: string +): string { + if (!isLibraryLoaded()) { + throw new Error('Auth library not loaded'); + } + + const fns = getAuthFunctions(); + if (!fns.generateWwwAuthenticateV2) { + throw new Error('Function not available'); + } + + const result = fns.generateWwwAuthenticateV2( + resource, + resourceMetadataUrl, + scopes, + error, + description + ); + if (!result) { + throw new Error('Failed to generate WWW-Authenticate header'); + } + + return result; +} + +/** + * GopherAuthClient - JWT token validation client + * + * Wraps the native gopher-auth client for validating JWT tokens + * against a JWKS endpoint. + */ +export class GopherAuthClient { + private handle: unknown = null; + private destroyed = false; + + /** + * Create a new GopherAuthClient + * + * @param jwksUri - URL to the JWKS endpoint + * @param issuer - Expected token issuer + * @throws Error if client creation fails + */ + constructor(jwksUri: string, issuer: string) { + if (!isLibraryLoaded()) { + loadLibrary(); + } + + const fns = getAuthFunctions(); + if (!fns.clientCreate) { + throw new Error('Auth library not properly loaded'); + } + + this.handle = fns.clientCreate(jwksUri, issuer); + if (!this.handle) { + throw new Error('Failed to create auth client'); + } + } + + /** + * Set a client option + * + * @param option - Option name (e.g., 'cache_duration', 'auto_refresh') + * @param value - Option value as string + * @throws Error if setting option fails + */ + setOption(option: string, value: string): void { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (!fns.clientSetOption) { + throw new Error('Function not available'); + } + + const result = fns.clientSetOption(this.handle, option, value); + if (result !== 0) { + throw new Error(`Failed to set option ${option}: error code ${result}`); + } + } + + /** + * Validate a JWT token + * + * @param token - JWT token string + * @param options - Optional validation options + * @returns Validation result + */ + validateToken(token: string, options?: GopherValidationOptions): ValidationResult { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (!fns.validateToken) { + throw new Error('Function not available'); + } + + const optionsHandle = options?.getHandle() ?? null; + const result = fns.validateToken(this.handle, token, optionsHandle) as { + valid: boolean; + error_code: number; + error_message: string | null; + }; + + return { + valid: result.valid, + errorCode: result.error_code as GopherAuthError, + errorMessage: result.error_message, + }; + } + + /** + * Extract payload from a JWT token + * + * @param token - JWT token string + * @returns Token payload + * @throws Error if extraction fails + */ + extractPayload(token: string): TokenPayload { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (!fns.extractPayload) { + throw new Error('Function not available'); + } + + // Note: extractPayload only takes the token, not the client handle + const payloadHandle = fns.extractPayload(token); + if (!payloadHandle) { + throw new Error('Failed to extract token payload'); + } + + try { + const payload: TokenPayload = { + subject: fns.payloadGetSubject?.(payloadHandle) ?? '', + scopes: fns.payloadGetScopes?.(payloadHandle) ?? '', + audience: fns.payloadGetAudience?.(payloadHandle) ?? undefined, + expiration: fns.payloadGetExpiration?.(payloadHandle) ?? undefined, + issuer: fns.payloadGetIssuer?.(payloadHandle) ?? undefined, + }; + + return payload; + } finally { + if (fns.payloadDestroy) { + fns.payloadDestroy(payloadHandle); + } + } + } + + /** + * Validate token and extract payload in one call + * + * @param token - JWT token string + * @param options - Optional validation options + * @returns Object with validation result and payload (if valid) + */ + validateAndExtract( + token: string, + options?: GopherValidationOptions + ): { result: ValidationResult; payload?: TokenPayload } { + const result = this.validateToken(token, options); + + if (!result.valid) { + return { result }; + } + + try { + const payload = this.extractPayload(token); + return { result, payload }; + } catch { + return { result }; + } + } + + /** + * Destroy the client and release resources + * + * This method is idempotent - safe to call multiple times. + */ + destroy(): void { + if (this.destroyed || !this.handle) { + return; + } + + const fns = getAuthFunctions(); + if (fns.clientDestroy) { + fns.clientDestroy(this.handle); + } + + this.handle = null; + this.destroyed = true; + } + + /** + * Check if the client has been destroyed + */ + isDestroyed(): boolean { + return this.destroyed; + } + + private ensureNotDestroyed(): void { + if (this.destroyed) { + throw new Error('GopherAuthClient has been destroyed'); + } + } +} diff --git a/src/ffi/auth/index.ts b/src/ffi/auth/index.ts new file mode 100644 index 00000000..30b64e87 --- /dev/null +++ b/src/ffi/auth/index.ts @@ -0,0 +1,70 @@ +/** + * Auth FFI Module - gopher-auth bindings for JWT token validation + * + * Provides OAuth 2.0 / JWT authentication support via the gopher-auth + * native library from gopher-orch. + * + * @example + * ```typescript + * import { + * gopherInitAuthLibrary, + * gopherShutdownAuthLibrary, + * GopherAuthClient, + * GopherValidationOptions, + * GopherAuthError, + * } from '@gopher.security/gopher-mcp-js'; + * + * // Initialize the library + * gopherInitAuthLibrary(); + * + * // Create client + * const client = new GopherAuthClient(jwksUri, issuer); + * + * // Validate token + * const options = new GopherValidationOptions().setScopes('mcp:read'); + * const result = client.validateToken(token, options); + * + * if (result.valid) { + * const payload = client.extractPayload(token); + * console.log('User:', payload.subject); + * } + * + * // Cleanup + * options.destroy(); + * client.destroy(); + * gopherShutdownAuthLibrary(); + * ``` + */ + +// Types +export { + GopherAuthError, + ValidationResult, + TokenPayload, + GopherAuthContext, + isGopherAuthError, + getErrorDescription, + gopherCreateEmptyAuthContext, +} from './types'; + +// High-level classes +export { + GopherAuthClient, + gopherInitAuthLibrary, + gopherShutdownAuthLibrary, + gopherGetAuthLibraryVersion, + gopherIsAuthLibraryInitialized, + gopherGenerateWwwAuthenticateHeader, + gopherGenerateWwwAuthenticateHeaderV2, +} from './auth-client'; + +export { + GopherValidationOptions, + gopherCreateValidationOptions, +} from './validation-options'; + +// Low-level loader (for advanced use) +export { + loadLibrary as loadAuthLibrary, + isLibraryLoaded as isAuthLibraryLoaded, +} from './loader'; diff --git a/src/ffi/auth/loader.ts b/src/ffi/auth/loader.ts new file mode 100644 index 00000000..b5ef199d --- /dev/null +++ b/src/ffi/auth/loader.ts @@ -0,0 +1,731 @@ +/** + * Auth Library Loader - koffi bindings for libgopher-auth + * + * Provides FFI bindings to the gopher-auth native library for + * JWT token validation and OAuth support. + * + * Note: The gopher_auth_* functions are part of libgopher-orch, + * not a separate library. + */ + +import * as koffi from 'koffi'; +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; + +// Track if library is loaded +let lib: koffi.IKoffiLib | null = null; +let libAvailable = false; +let debug = false; + +// Opaque pointer types +const GopherAuthClientPtr = koffi.pointer( + 'gopher_auth_client_t', + koffi.opaque() +); +const GopherAuthPayloadPtr = koffi.pointer( + 'gopher_auth_token_payload_t', + koffi.opaque() +); +const GopherAuthOptionsPtr = koffi.pointer( + 'gopher_auth_validation_options_t', + koffi.opaque() +); + +// Output pointer types for C API functions that use output parameters +const GopherAuthClientOutPtr = koffi.out(koffi.pointer(GopherAuthClientPtr)); +const GopherAuthPayloadOutPtr = koffi.out(koffi.pointer(GopherAuthPayloadPtr)); +const GopherAuthOptionsOutPtr = koffi.out(koffi.pointer(GopherAuthOptionsPtr)); +const CharOutPtr = koffi.out(koffi.pointer('char*')); +const Int64OutPtr = koffi.out(koffi.pointer('int64_t')); + +// Result struct +const GopherAuthValidationResult = koffi.struct( + 'gopher_auth_validation_result_t', + { + valid: 'bool', + error_code: 'int32_t', + error_message: 'const char*', + } +); +const GopherAuthValidationResultOutPtr = koffi.out( + koffi.pointer(GopherAuthValidationResult) +); + +// Raw FFI function bindings +let _authInit: koffi.KoffiFunction | null = null; +let _authShutdown: koffi.KoffiFunction | null = null; +let _authVersion: koffi.KoffiFunction | null = null; + +let _clientCreate: koffi.KoffiFunction | null = null; +let _clientDestroy: koffi.KoffiFunction | null = null; +let _clientSetOption: koffi.KoffiFunction | null = null; + +let _optionsCreate: koffi.KoffiFunction | null = null; +let _optionsDestroy: koffi.KoffiFunction | null = null; +let _optionsSetScopes: koffi.KoffiFunction | null = null; +let _optionsSetAudience: koffi.KoffiFunction | null = null; +let _optionsSetClockSkew: koffi.KoffiFunction | null = null; + +let _validateToken: koffi.KoffiFunction | null = null; +let _extractPayload: koffi.KoffiFunction | null = null; + +let _payloadGetSubject: koffi.KoffiFunction | null = null; +let _payloadGetScopes: koffi.KoffiFunction | null = null; +let _payloadGetAudience: koffi.KoffiFunction | null = null; +let _payloadGetExpiration: koffi.KoffiFunction | null = null; +let _payloadGetIssuer: koffi.KoffiFunction | null = null; +let _payloadDestroy: koffi.KoffiFunction | null = null; + +let _freeString: koffi.KoffiFunction | null = null; +let _generateWwwAuthenticate: koffi.KoffiFunction | null = null; +let _generateWwwAuthenticateV2: koffi.KoffiFunction | null = null; + +/** + * Get the library name for the current platform + * + * Note: The gopher_auth_* functions are part of libgopher-orch, + * not a separate library. + */ +function getLibraryName(): string { + switch (os.platform()) { + case 'darwin': + return 'libgopher-orch.dylib'; + case 'win32': + return 'gopher-orch.dll'; + default: + return 'libgopher-orch.so'; + } +} + +/** + * Get search paths for the native library + */ +function getSearchPaths(): string[] { + const paths: string[] = []; + + // Platform-specific optional dependency package + const platformPackagePath = getPlatformPackagePath(); + if (platformPackagePath) { + paths.push(platformPackagePath); + } + + // Get the directory containing this module + const moduleDir = path.dirname(path.dirname(path.dirname(__dirname))); + + // Development paths + paths.push( + path.join(process.cwd(), 'native', 'lib'), + path.join(process.cwd(), 'lib'), + path.join(moduleDir, 'native', 'lib'), + path.join(path.dirname(moduleDir), 'native', 'lib') + ); + + // System paths + if (os.platform() === 'darwin') { + paths.push('/usr/local/lib', '/opt/homebrew/lib'); + } + paths.push('/usr/lib'); + + return paths; +} + +/** + * Get the path to the platform-specific optional dependency package + */ +function getPlatformPackagePath(): string | null { + const platform = os.platform(); + const arch = os.arch(); + + const platformMap: Record = { + darwin: 'darwin', + linux: 'linux', + win32: 'win32', + }; + + const platformName = platformMap[platform]; + if (!platformName) { + return null; + } + + const packageName = `@gopher.security/gopher-orch-${platformName}-${arch}`; + + try { + const packageJsonPath = require.resolve(`${packageName}/package.json`); + const packageDir = path.dirname(packageJsonPath); + const libPath = path.join(packageDir, 'lib'); + + if (fs.existsSync(libPath)) { + return libPath; + } + } catch { + // Package not installed + } + + return null; +} + +/** + * Setup FFI function bindings + */ +function setupFunctions(): void { + if (lib === null) { + return; + } + + // Library lifecycle - these return error codes or simple values + _authInit = lib.func('gopher_auth_init', 'int32_t', []); + _authShutdown = lib.func('gopher_auth_shutdown', 'int32_t', []); + _authVersion = lib.func('gopher_auth_version', 'const char*', []); + + // Client functions - use output parameters for handles + // gopher_auth_error_t gopher_auth_client_create(gopher_auth_client_t* client, const char* jwks_uri, const char* issuer); + _clientCreate = lib.func('gopher_auth_client_create', 'int32_t', [ + GopherAuthClientOutPtr, + 'const char*', + 'const char*', + ]); + _clientDestroy = lib.func('gopher_auth_client_destroy', 'int32_t', [ + GopherAuthClientPtr, + ]); + _clientSetOption = lib.func('gopher_auth_client_set_option', 'int32_t', [ + GopherAuthClientPtr, + 'const char*', + 'const char*', + ]); + + // Options functions - use output parameters + // gopher_auth_error_t gopher_auth_validation_options_create(gopher_auth_validation_options_t* options); + _optionsCreate = lib.func( + 'gopher_auth_validation_options_create', + 'int32_t', + [GopherAuthOptionsOutPtr] + ); + _optionsDestroy = lib.func( + 'gopher_auth_validation_options_destroy', + 'int32_t', + [GopherAuthOptionsPtr] + ); + _optionsSetScopes = lib.func( + 'gopher_auth_validation_options_set_scopes', + 'int32_t', + [GopherAuthOptionsPtr, 'const char*'] + ); + _optionsSetAudience = lib.func( + 'gopher_auth_validation_options_set_audience', + 'int32_t', + [GopherAuthOptionsPtr, 'const char*'] + ); + _optionsSetClockSkew = lib.func( + 'gopher_auth_validation_options_set_clock_skew', + 'int32_t', + [GopherAuthOptionsPtr, 'int64_t'] + ); + + // Validation functions + // gopher_auth_error_t gopher_auth_validate_token(client, token, options, gopher_auth_validation_result_t* result); + _validateToken = lib.func('gopher_auth_validate_token', 'int32_t', [ + GopherAuthClientPtr, + 'const char*', + GopherAuthOptionsPtr, + GopherAuthValidationResultOutPtr, + ]); + // gopher_auth_error_t gopher_auth_extract_payload(const char* token, gopher_auth_token_payload_t* payload); + _extractPayload = lib.func('gopher_auth_extract_payload', 'int32_t', [ + 'const char*', + GopherAuthPayloadOutPtr, + ]); + + // Payload functions - use output parameters for strings + // gopher_auth_error_t gopher_auth_payload_get_subject(payload, char** value); + _payloadGetSubject = lib.func('gopher_auth_payload_get_subject', 'int32_t', [ + GopherAuthPayloadPtr, + CharOutPtr, + ]); + _payloadGetScopes = lib.func('gopher_auth_payload_get_scopes', 'int32_t', [ + GopherAuthPayloadPtr, + CharOutPtr, + ]); + _payloadGetAudience = lib.func( + 'gopher_auth_payload_get_audience', + 'int32_t', + [GopherAuthPayloadPtr, CharOutPtr] + ); + _payloadGetExpiration = lib.func( + 'gopher_auth_payload_get_expiration', + 'int32_t', + [GopherAuthPayloadPtr, Int64OutPtr] + ); + _payloadGetIssuer = lib.func('gopher_auth_payload_get_issuer', 'int32_t', [ + GopherAuthPayloadPtr, + CharOutPtr, + ]); + _payloadDestroy = lib.func('gopher_auth_payload_destroy', 'int32_t', [ + GopherAuthPayloadPtr, + ]); + + // Utility functions + _freeString = lib.func('gopher_auth_free_string', 'void', ['char*']); + // gopher_auth_error_t gopher_auth_generate_www_authenticate(realm, error, description, char** header); + _generateWwwAuthenticate = lib.func( + 'gopher_auth_generate_www_authenticate', + 'int32_t', + ['const char*', 'const char*', 'const char*', CharOutPtr] + ); + _generateWwwAuthenticateV2 = lib.func( + 'gopher_auth_generate_www_authenticate_v2', + 'int32_t', + [ + 'const char*', + 'const char*', + 'const char*', + 'const char*', + 'const char*', + CharOutPtr, + ] + ); +} + +/** + * Load the gopher-orch native library + */ +export function loadLibrary(): boolean { + if (lib !== null) { + return libAvailable; + } + + debug = process.env['DEBUG'] !== undefined; + const libraryName = getLibraryName(); + const searchPaths = getSearchPaths(); + + // Try environment variable path first + const envPath = + process.env['GOPHER_ORCH_LIBRARY_PATH'] || + process.env['GOPHER_AUTH_LIBRARY_PATH']; + if (envPath && fs.existsSync(envPath)) { + try { + lib = koffi.load(envPath); + setupFunctions(); + libAvailable = true; + return true; + } catch (e) { + if (debug) { + console.error( + `Failed to load from environment path: ${(e as Error).message}` + ); + } + } + } + + // Try search paths + for (const searchPath of searchPaths) { + const libFile = path.join(searchPath, libraryName); + if (fs.existsSync(libFile)) { + try { + lib = koffi.load(libFile); + setupFunctions(); + libAvailable = true; + return true; + } catch (e) { + if (debug) { + console.error( + `Failed to load from ${searchPath}: ${(e as Error).message}` + ); + } + } + } + } + + // Try system paths + try { + lib = koffi.load(libraryName); + setupFunctions(); + libAvailable = true; + return true; + } catch (e) { + if (debug) { + console.error( + `Failed to load gopher-orch library: ${(e as Error).message}` + ); + console.error('Searched paths:'); + for (const p of searchPaths) { + console.error(` - ${p}`); + } + } + } + + libAvailable = false; + return false; +} + +/** + * Check if the library is loaded and available + */ +export function isLibraryLoaded(): boolean { + return libAvailable; +} + +/** + * Get raw FFI functions for internal use + */ +export function getRawFunctions() { + return { + authInit: _authInit, + authShutdown: _authShutdown, + authVersion: _authVersion, + clientCreate: _clientCreate, + clientDestroy: _clientDestroy, + clientSetOption: _clientSetOption, + optionsCreate: _optionsCreate, + optionsDestroy: _optionsDestroy, + optionsSetScopes: _optionsSetScopes, + optionsSetAudience: _optionsSetAudience, + optionsSetClockSkew: _optionsSetClockSkew, + validateToken: _validateToken, + extractPayload: _extractPayload, + payloadGetSubject: _payloadGetSubject, + payloadGetScopes: _payloadGetScopes, + payloadGetAudience: _payloadGetAudience, + payloadGetExpiration: _payloadGetExpiration, + payloadGetIssuer: _payloadGetIssuer, + payloadDestroy: _payloadDestroy, + freeString: _freeString, + generateWwwAuthenticate: _generateWwwAuthenticate, + generateWwwAuthenticateV2: _generateWwwAuthenticateV2, + }; +} + +// ============================================================================ +// High-level wrapper functions that handle output parameters +// ============================================================================ + +/** + * Initialize the auth library + * @returns Error code (0 = success) + */ +export function authInit(): number { + if (!_authInit) throw new Error('Library not loaded'); + return _authInit() as number; +} + +/** + * Shutdown the auth library + * @returns Error code (0 = success) + */ +export function authShutdown(): number { + if (!_authShutdown) throw new Error('Library not loaded'); + return _authShutdown() as number; +} + +/** + * Get library version string + */ +export function authVersion(): string { + if (!_authVersion) throw new Error('Library not loaded'); + return _authVersion() as string; +} + +/** + * Create an auth client + * @returns Client handle or null on error + */ +export function clientCreate(jwksUri: string, issuer: string): unknown { + if (!_clientCreate) throw new Error('Library not loaded'); + + const clientOut: unknown[] = [null]; + const result = _clientCreate(clientOut, jwksUri, issuer) as number; + + if (result !== 0) { + return null; + } + + return clientOut[0]; +} + +/** + * Destroy an auth client + */ +export function clientDestroy(client: unknown): number { + if (!_clientDestroy) throw new Error('Library not loaded'); + return _clientDestroy(client) as number; +} + +/** + * Set client option + */ +export function clientSetOption( + client: unknown, + option: string, + value: string +): number { + if (!_clientSetOption) throw new Error('Library not loaded'); + return _clientSetOption(client, option, value) as number; +} + +/** + * Create validation options + */ +export function optionsCreate(): unknown { + if (!_optionsCreate) throw new Error('Library not loaded'); + + const optionsOut: unknown[] = [null]; + const result = _optionsCreate(optionsOut) as number; + + if (result !== 0) { + return null; + } + + return optionsOut[0]; +} + +/** + * Destroy validation options + */ +export function optionsDestroy(options: unknown): number { + if (!_optionsDestroy) throw new Error('Library not loaded'); + return _optionsDestroy(options) as number; +} + +/** + * Set required scopes + */ +export function optionsSetScopes(options: unknown, scopes: string): number { + if (!_optionsSetScopes) throw new Error('Library not loaded'); + return _optionsSetScopes(options, scopes) as number; +} + +/** + * Set required audience + */ +export function optionsSetAudience(options: unknown, audience: string): number { + if (!_optionsSetAudience) throw new Error('Library not loaded'); + return _optionsSetAudience(options, audience) as number; +} + +/** + * Set clock skew tolerance + */ +export function optionsSetClockSkew(options: unknown, seconds: number): number { + if (!_optionsSetClockSkew) throw new Error('Library not loaded'); + return _optionsSetClockSkew(options, seconds) as number; +} + +/** + * Validate a token + */ +export function validateToken( + client: unknown, + token: string, + options: unknown +): { valid: boolean; error_code: number; error_message: string | null } | null { + if (!_validateToken) throw new Error('Library not loaded'); + + const resultOut: unknown[] = [ + { valid: false, error_code: 0, error_message: null }, + ]; + const err = _validateToken(client, token, options, resultOut) as number; + + if (err !== 0) { + return null; + } + + return resultOut[0] as { + valid: boolean; + error_code: number; + error_message: string | null; + }; +} + +/** + * Extract payload from token + */ +export function extractPayload(token: string): unknown { + if (!_extractPayload) throw new Error('Library not loaded'); + + const payloadOut: unknown[] = [null]; + const result = _extractPayload(token, payloadOut) as number; + + if (result !== 0) { + return null; + } + + return payloadOut[0]; +} + +/** + * Get subject from payload + */ +export function payloadGetSubject(payload: unknown): string | null { + if (!_payloadGetSubject) throw new Error('Library not loaded'); + + const valueOut: (string | null)[] = [null]; + const result = _payloadGetSubject(payload, valueOut) as number; + + if (result !== 0) { + return null; + } + + return valueOut[0] ?? null; +} + +/** + * Get scopes from payload + */ +export function payloadGetScopes(payload: unknown): string | null { + if (!_payloadGetScopes) throw new Error('Library not loaded'); + + const valueOut: (string | null)[] = [null]; + const result = _payloadGetScopes(payload, valueOut) as number; + + if (result !== 0) { + return null; + } + + return valueOut[0] ?? null; +} + +/** + * Get audience from payload + */ +export function payloadGetAudience(payload: unknown): string | null { + if (!_payloadGetAudience) throw new Error('Library not loaded'); + + const valueOut: (string | null)[] = [null]; + const result = _payloadGetAudience(payload, valueOut) as number; + + if (result !== 0) { + return null; + } + + return valueOut[0] ?? null; +} + +/** + * Get expiration from payload + */ +export function payloadGetExpiration(payload: unknown): number | null { + if (!_payloadGetExpiration) throw new Error('Library not loaded'); + + const valueOut: bigint[] = [BigInt(0)]; + const result = _payloadGetExpiration(payload, valueOut) as number; + + if (result !== 0) { + return null; + } + + return Number(valueOut[0]); +} + +/** + * Get issuer from payload + */ +export function payloadGetIssuer(payload: unknown): string | null { + if (!_payloadGetIssuer) throw new Error('Library not loaded'); + + const valueOut: (string | null)[] = [null]; + const result = _payloadGetIssuer(payload, valueOut) as number; + + if (result !== 0) { + return null; + } + + return valueOut[0] ?? null; +} + +/** + * Destroy payload + */ +export function payloadDestroy(payload: unknown): number { + if (!_payloadDestroy) throw new Error('Library not loaded'); + return _payloadDestroy(payload) as number; +} + +/** + * Free a string allocated by the library + */ +export function freeString(str: unknown): void { + if (!_freeString) throw new Error('Library not loaded'); + _freeString(str); +} + +/** + * Generate WWW-Authenticate header + */ +export function generateWwwAuthenticate( + realm: string, + error: string, + description: string +): string | null { + if (!_generateWwwAuthenticate) throw new Error('Library not loaded'); + + const headerOut: (string | null)[] = [null]; + const result = _generateWwwAuthenticate( + realm, + error, + description, + headerOut + ) as number; + + if (result !== 0) { + return null; + } + + return headerOut[0] ?? null; +} + +/** + * Generate WWW-Authenticate header v2 (RFC 9728) + */ +export function generateWwwAuthenticateV2( + realm: string, + resourceMetadata: string, + scope: string, + error: string, + description: string +): string | null { + if (!_generateWwwAuthenticateV2) throw new Error('Library not loaded'); + + const headerOut: (string | null)[] = [null]; + const result = _generateWwwAuthenticateV2( + realm, + resourceMetadata, + scope, + error, + description, + headerOut + ) as number; + + if (result !== 0) { + return null; + } + + return headerOut[0] ?? null; +} + +// Legacy exports for backward compatibility +export function getAuthFunctions() { + return { + authInit, + authShutdown, + authVersion, + clientCreate, + clientDestroy, + clientSetOption, + optionsCreate, + optionsDestroy, + optionsSetScopes, + optionsSetAudience, + optionsSetClockSkew, + validateToken, + extractPayload, + payloadGetSubject, + payloadGetScopes, + payloadGetAudience, + payloadGetExpiration, + payloadGetIssuer, + payloadDestroy, + freeString, + generateWwwAuthenticate, + generateWwwAuthenticateV2, + }; +} diff --git a/src/ffi/auth/types.ts b/src/ffi/auth/types.ts new file mode 100644 index 00000000..6bb23871 --- /dev/null +++ b/src/ffi/auth/types.ts @@ -0,0 +1,113 @@ +/** + * Auth Types - Type definitions for gopher-auth FFI bindings + * + * These types mirror the C API from gopher-orch/include/gopher/orch/auth/auth_c_api.h + */ + +/** + * Error codes from gopher_auth_error_t enum + */ +export enum GopherAuthError { + SUCCESS = 0, + INVALID_TOKEN = -1000, + EXPIRED_TOKEN = -1001, + INVALID_SIGNATURE = -1002, + INVALID_ISSUER = -1003, + INVALID_AUDIENCE = -1004, + INSUFFICIENT_SCOPE = -1005, + JWKS_FETCH_FAILED = -1006, + INVALID_KEY = -1007, + NETWORK_ERROR = -1008, + INVALID_CONFIG = -1009, + OUT_OF_MEMORY = -1010, + INVALID_PARAMETER = -1011, + NOT_INITIALIZED = -1012, + INTERNAL_ERROR = -1013, + TOKEN_EXCHANGE_FAILED = -1014, + IDP_NOT_LINKED = -1015, + INVALID_IDP_ALIAS = -1016, +} + +/** + * Check if a value is a valid GopherAuthError code + */ +export function isGopherAuthError(code: number): code is GopherAuthError { + const success = GopherAuthError.SUCCESS as number; + const minError = GopherAuthError.INVALID_IDP_ALIAS as number; + const maxError = GopherAuthError.INVALID_TOKEN as number; + return code === success || (code <= maxError && code >= minError); +} + +/** + * Get human-readable description for an error code + */ +export function getErrorDescription(code: GopherAuthError): string { + const descriptions: Record = { + [GopherAuthError.SUCCESS]: 'Success', + [GopherAuthError.INVALID_TOKEN]: 'Invalid token format or structure', + [GopherAuthError.EXPIRED_TOKEN]: 'Token has expired', + [GopherAuthError.INVALID_SIGNATURE]: 'Token signature verification failed', + [GopherAuthError.INVALID_ISSUER]: + 'Token issuer does not match expected value', + [GopherAuthError.INVALID_AUDIENCE]: + 'Token audience does not match expected value', + [GopherAuthError.INSUFFICIENT_SCOPE]: 'Token does not have required scopes', + [GopherAuthError.JWKS_FETCH_FAILED]: 'Failed to fetch JWKS from server', + [GopherAuthError.INVALID_KEY]: 'Invalid or unsupported key in JWKS', + [GopherAuthError.NETWORK_ERROR]: 'Network error during authentication', + [GopherAuthError.INVALID_CONFIG]: 'Invalid configuration', + [GopherAuthError.OUT_OF_MEMORY]: 'Out of memory', + [GopherAuthError.INVALID_PARAMETER]: 'Invalid parameter provided', + [GopherAuthError.NOT_INITIALIZED]: 'Auth library not initialized', + [GopherAuthError.INTERNAL_ERROR]: 'Internal error', + [GopherAuthError.TOKEN_EXCHANGE_FAILED]: 'Token exchange failed', + [GopherAuthError.IDP_NOT_LINKED]: 'Identity provider not linked', + [GopherAuthError.INVALID_IDP_ALIAS]: 'Invalid identity provider alias', + }; + + return descriptions[code] || `Unknown error code: ${code}`; +} + +/** + * Token validation result + */ +export interface ValidationResult { + valid: boolean; + errorCode: GopherAuthError; + errorMessage: string | null; +} + +/** + * Decoded JWT token payload + */ +export interface TokenPayload { + subject: string; + scopes: string; + audience?: string; + expiration?: number; + issuer?: string; +} + +/** + * Authentication context for the current request + */ +export interface GopherAuthContext { + userId: string; + scopes: string; + audience: string; + tokenExpiry: number; + authenticated: boolean; +} + +/** + * Create an empty auth context (unauthenticated) + */ +export function gopherCreateEmptyAuthContext(): GopherAuthContext { + return { + userId: '', + scopes: '', + audience: '', + tokenExpiry: 0, + authenticated: false, + }; +} diff --git a/src/ffi/auth/validation-options.ts b/src/ffi/auth/validation-options.ts new file mode 100644 index 00000000..84f456f9 --- /dev/null +++ b/src/ffi/auth/validation-options.ts @@ -0,0 +1,157 @@ +/** + * ValidationOptions - Token validation configuration + * + * Provides a fluent API for configuring JWT token validation options. + */ + +import { loadLibrary, isLibraryLoaded, getAuthFunctions } from './loader'; + +/** + * GopherValidationOptions - Configures token validation behavior + * + * Use the fluent API to configure validation options: + * ```typescript + * const options = new GopherValidationOptions() + * .setScopes('mcp:read mcp:write') + * .setAudience('my-api') + * .setClockSkew(30); + * ``` + */ +export class GopherValidationOptions { + private handle: unknown = null; + private destroyed = false; + + /** + * Create new ValidationOptions + * + * @throws Error if options creation fails + */ + constructor() { + if (!isLibraryLoaded()) { + loadLibrary(); + } + + const fns = getAuthFunctions(); + if (!fns.optionsCreate) { + throw new Error('Auth library not properly loaded'); + } + + this.handle = fns.optionsCreate(); + if (!this.handle) { + throw new Error('Failed to create validation options'); + } + } + + /** + * Set required scopes for validation + * + * @param scopes - Space-separated list of required scopes + * @returns this for method chaining + */ + setScopes(scopes: string): this { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (fns.optionsSetScopes) { + fns.optionsSetScopes(this.handle, scopes); + } + + return this; + } + + /** + * Set required audience for validation + * + * @param audience - Expected audience claim value + * @returns this for method chaining + */ + setAudience(audience: string): this { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (fns.optionsSetAudience) { + fns.optionsSetAudience(this.handle, audience); + } + + return this; + } + + /** + * Set clock skew tolerance for expiration validation + * + * @param seconds - Number of seconds of clock skew to allow + * @returns this for method chaining + */ + setClockSkew(seconds: number): this { + this.ensureNotDestroyed(); + + const fns = getAuthFunctions(); + if (fns.optionsSetClockSkew) { + fns.optionsSetClockSkew(this.handle, seconds); + } + + return this; + } + + /** + * Get the native handle (for internal use) + * + * @returns Native options handle or null if destroyed + */ + getHandle(): unknown { + return this.handle; + } + + /** + * Destroy the options and release resources + * + * This method is idempotent - safe to call multiple times. + */ + destroy(): void { + if (this.destroyed || !this.handle) { + return; + } + + const fns = getAuthFunctions(); + if (fns.optionsDestroy) { + fns.optionsDestroy(this.handle); + } + + this.handle = null; + this.destroyed = true; + } + + /** + * Check if options have been destroyed + */ + isDestroyed(): boolean { + return this.destroyed; + } + + private ensureNotDestroyed(): void { + if (this.destroyed) { + throw new Error('GopherValidationOptions has been destroyed'); + } + } +} + +/** + * Create ValidationOptions with common settings + * + * @param scopes - Optional required scopes + * @param clockSkew - Clock skew tolerance in seconds (default: 30) + * @returns Configured ValidationOptions + */ +export function gopherCreateValidationOptions( + scopes?: string, + clockSkew: number = 30 +): GopherValidationOptions { + const options = new GopherValidationOptions(); + options.setClockSkew(clockSkew); + + if (scopes) { + options.setScopes(scopes); + } + + return options; +} diff --git a/src/ffi/index.ts b/src/ffi/index.ts index 7d4b1298..aec15956 100644 --- a/src/ffi/index.ts +++ b/src/ffi/index.ts @@ -4,3 +4,28 @@ export { GopherOrchLibrary } from './library'; export type { GopherOrchHandle, GopherOrchErrorInfoData } from './library'; + +// Auth module exports +export { + // Types + GopherAuthError, + ValidationResult, + TokenPayload, + GopherAuthContext, + isGopherAuthError, + getErrorDescription, + gopherCreateEmptyAuthContext, + // Classes + GopherAuthClient, + GopherValidationOptions, + // Functions + gopherInitAuthLibrary, + gopherShutdownAuthLibrary, + gopherGetAuthLibraryVersion, + gopherIsAuthLibraryInitialized, + gopherGenerateWwwAuthenticateHeader, + gopherGenerateWwwAuthenticateHeaderV2, + gopherCreateValidationOptions, + loadAuthLibrary, + isAuthLibraryLoaded, +} from './auth'; diff --git a/src/index.ts b/src/index.ts index 6671325c..ca119534 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,3 +44,24 @@ export { // FFI exports (for advanced use) export { GopherOrchLibrary } from './ffi'; export type { GopherOrchHandle, GopherOrchErrorInfoData } from './ffi'; + +// Auth exports +export { + // Types + GopherAuthError, + isGopherAuthError, + getErrorDescription, + gopherCreateEmptyAuthContext, + // Classes + GopherAuthClient, + GopherValidationOptions, + // Functions + gopherInitAuthLibrary, + gopherShutdownAuthLibrary, + gopherGetAuthLibraryVersion, + gopherIsAuthLibraryInitialized, + gopherGenerateWwwAuthenticateHeader, + gopherGenerateWwwAuthenticateHeaderV2, + gopherCreateValidationOptions, +} from './ffi'; +export type { ValidationResult, TokenPayload, GopherAuthContext } from './ffi'; diff --git a/tests/ffi.test.ts b/tests/ffi.test.ts index e62d7822..ce28d0f2 100644 --- a/tests/ffi.test.ts +++ b/tests/ffi.test.ts @@ -154,7 +154,11 @@ describe('GopherOrchLibrary', () => { // Running with null handle should be handled gracefully try { - lib!.agentRun(null as unknown as import('../src/ffi/library').GopherOrchHandle, 'test query', 1000); + lib!.agentRun( + null as unknown as import('../src/ffi/library').GopherOrchHandle, + 'test query', + 1000 + ); // May return null or error message, but should not crash } catch { // Exception is acceptable for null handle @@ -169,7 +173,9 @@ describe('GopherOrchLibrary', () => { // Releasing null handle should be handled gracefully try { - lib!.agentRelease(null as unknown as import('../src/ffi/library').GopherOrchHandle); + lib!.agentRelease( + null as unknown as import('../src/ffi/library').GopherOrchHandle + ); } catch { // Exception is acceptable for null handle } diff --git a/third_party/gopher-orch b/third_party/gopher-orch index 63ac8c66..e7b76ed8 160000 --- a/third_party/gopher-orch +++ b/third_party/gopher-orch @@ -1 +1 @@ -Subproject commit 63ac8c66f86a3113d6e13202404f46a6bf82b065 +Subproject commit e7b76ed8bf7d29b64b6d11cb611b214298212527