diff --git a/.golangci.yml b/.golangci.yml index ea9604e..e0e4d65 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,3 @@ -version: "2" run: timeout: 5m linters: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..1621c00 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,49 @@ +# Architecture + +This document provides a high-level overview of the Poindexter library's architecture. + +## System Overview + +Poindexter is a Go library that provides a collection of utility functions, with a primary focus on sorting algorithms and a K-D tree implementation for nearest neighbor searches. The library is designed to be modular, with distinct components for different functionalities. + +The main components of the Poindexter library are: + +- **Sorting Utilities:** A set of functions for sorting slices of various data types, including integers, strings, and floats. It also provides generic functions for custom sorting. +- **K-D Tree:** A K-D tree implementation for efficient nearest neighbor searches in a multi-dimensional space. +- **WASM Build:** A WebAssembly build that allows the K-D tree to be used in browser environments. + +## Component Diagram + +The following diagram illustrates the high-level components of the Poindexter library and their relationships: + +```mermaid +graph TD + subgraph Core Library + A[Sorting Utilities] + B[K-D Tree] + end + + subgraph Browser Environment + C[WASM Build] + end + + subgraph Go Application + D[Go Application] + end + + D --> A + D --> B + C --> B + + style A fill:#f9f,stroke:#333,stroke-width:2px + style B fill:#f9f,stroke:#333,stroke-width:2px + style C fill:#ccf,stroke:#333,stroke-width:2px + style D fill:#cfc,stroke:#333,stroke-width:2px +``` + +### Components + +- **Sorting Utilities:** This component contains all the sorting-related functions. It is used by Go applications that need to perform sorting operations. +- **K-D Tree:** This component provides the K-D tree implementation. It is used by Go applications and is also the core component exposed in the WASM build. +- **WASM Build:** This component is a wrapper around the K-D tree that compiles it to WebAssembly, allowing it to be used in web browsers. +- **Go Application:** Any Go application that imports the Poindexter library to use its sorting or K-D tree functionalities. diff --git a/AUDIT-API.md b/AUDIT-API.md new file mode 100644 index 0000000..2683182 --- /dev/null +++ b/AUDIT-API.md @@ -0,0 +1,77 @@ +# API Audit: Poindexter Go Library + +This document audits the public API of the Poindexter Go library, focusing on design, consistency, documentation, and security best practices for a Go library. + +## 1. API Design and Consistency + +### 1.1. Naming Conventions + +* **Consistency:** The library generally follows Go's naming conventions (`camelCase` for unexported, `PascalCase` for exported). +* **Clarity:** Function names are clear and descriptive (e.g., `SortInts`, `SortByKey`, `NewKDTree`). +* **Minor Inconsistency:** `IsSorted` exists, but `IsSortedStrings` and `IsSortedFloat64s` are more verbose. A more consistent naming scheme might be `IntsAreSorted`, `StringsAreSorted`, etc., to mirror the standard library's `sort` package. + +### 1.2. Generics + +* **Effective Use:** The use of generics in `SortBy` and `SortByKey` is well-implemented and improves type safety and usability. +* **`KDPoint`:** The `KDPoint` struct effectively uses generics for its `Value` field, allowing users to associate any data type with a point. + +### 1.3. Error Handling + +* **Exported Errors:** The library exports sentinel errors (`ErrEmptyPoints`, `ErrDimMismatch`, etc.), which is a good practice, allowing users to check for specific error conditions. +* **Constructor Errors:** The `NewKDTree` constructor correctly returns an error value, forcing callers to handle potential issues during tree creation. + +### 1.4. Options Pattern + +* **`NewKDTree`:** The use of the options pattern with `KDOption` functions (`WithMetric`, `WithBackend`) is a great choice. It provides a flexible and extensible way to configure the `KDTree` without requiring a large number of constructor parameters. + +## 2. Documentation + +* **Package-Level:** The package-level documentation is good, providing a clear overview of the library's features. +* **Exported Symbols:** All exported functions, types, and constants have clear and concise documentation comments. +* **Examples:** The `README.md` includes excellent quick-start examples, and the `examples/` directory provides more detailed, runnable examples. + +## 3. Security + +### 3.1. Input Validation + +* **`NewKDTree`:** The constructor performs thorough validation of its inputs, checking for empty point sets, zero dimensions, and dimensional mismatches. This prevents the creation of invalid `KDTree` instances. +* **`KDTree` Methods:** Methods like `Nearest` and `KNearest` validate the dimensionality of the query vector, preventing panics or incorrect behavior. +* **`DeleteByID`:** This method correctly handles cases where the ID is not found or is empty. + +### 3.2. Panics + +* The public API appears to be free of potential panics. The library consistently uses error returns and input validation to handle exceptional cases. + +## 4. Recent API Design and Ergonomics Improvements + +### 4.1. "God Class" Refactoring in `kdtree_analytics.go` + +The file `kdtree_analytics.go` exhibited "God Class" characteristics, combining core tree analytics with unrelated responsibilities like peer trust scoring and NAT metrics. This made the code difficult to maintain and understand. + +**Changes Made:** +To address the "God Class" issue, `kdtree_analytics.go` was decomposed into three distinct files: + +* `kdtree_analytics.go`: Now contains only the core tree analytics. +* `peer_trust.go`: Contains the peer trust scoring logic. +* `nat_metrics.go`: Contains the NAT-related metrics. + +### 4.2. Method Naming Improvements + +The method `ComputeDistanceDistribution` in `kdtree.go` was inconsistently named, as it actually computed axis-based distributions, not distance distributions. + +**Changes Made:** +Renamed the `ComputeDistanceDistribution` method to `ComputeAxisDistributions` to more accurately reflect its functionality. + +### 4.3. Refactored `kdtree.go` + +Updated `kdtree.go` to use the new, more focused modules. Removed the now-unnecessary `ResetAnalytics` methods, which were tightly coupled to the old analytics implementation. + +## Summary and Recommendations + +The Poindexter library's public API is well-designed, consistent, and follows Go best practices. The use of generics, the options pattern, and clear error handling make it a robust and user-friendly library. Recent refactoring efforts have improved modularity and maintainability. + +**Recommendations:** + +1. **Naming Consistency:** Consider renaming `IsSorted`, `IsSortedStrings`, and `IsSortedFloat64s` to `IntsAreSorted`, `StringsAreSorted`, and `Float64sAreSorted` to align more closely with the standard library's `sort` package. +2. **Defensive Copying:** The `Points()` method returns a copy of the internal slice, which is excellent. Ensure that any future methods that expose internal state also return copies to prevent mutation by callers. +3. **Continued Modularization:** The recent decomposition of `kdtree_analytics.go` is a positive step. Continue to evaluate the codebase for opportunities to separate concerns and improve maintainability. diff --git a/AUDIT-AUTH.md b/AUDIT-AUTH.md new file mode 100644 index 0000000..67b5224 --- /dev/null +++ b/AUDIT-AUTH.md @@ -0,0 +1,33 @@ +# Security Audit: Authentication & Authorization + +## Executive Summary + +The security audit of authentication and authorization mechanisms for the Poindexter repository has been completed. The investigation concludes that the codebase is a Go library providing data structures and algorithms, specifically k-d trees and sorting utilities. It does not contain any user-facing application, authentication flows, authorization logic, or session management. Therefore, the requested audit categories are not applicable. + +## Scope of Review + +The audit was initiated to assess the following areas: +- **Authentication:** Password handling, session management, token security, and multi-factor authentication. +- **Authorization:** Access control models, permission checks, privilege escalation vulnerabilities, and API protection. + +## Findings + +A thorough review of the codebase was conducted, including but not limited to the following files: +- `README.md` +- `poindexter.go` +- `kdtree.go` +- `CLAUDE.md` +- `npm/poindexter-wasm/smoke.mjs` +- `wasm/main.go` +- `go.mod` + +The analysis of these files confirms that the repository contains a library and not a service or application. There are no functions or modules related to: +- User registration or login +- Password hashing or storage +- Session or token generation +- Access control lists (ACLs), role-based access control (RBAC), or other authorization models +- API endpoints requiring protection + +## Conclusion + +The Poindexter library, by its nature, does not handle authentication or authorization. As such, there are no vulnerabilities to report in these areas. The audit is concluded as not applicable. diff --git a/AUDIT-COMPLEXITY.md b/AUDIT-COMPLEXITY.md new file mode 100644 index 0000000..4bae3af --- /dev/null +++ b/AUDIT-COMPLEXITY.md @@ -0,0 +1,221 @@ +# Code Complexity & Maintainability Audit + +This document analyzes the code quality of the Poindexter library, identifies maintainability issues, and provides recommendations for improvement. The audit focuses on cyclomatic and cognitive complexity, code duplication, and other maintainability metrics. + +## High-Impact Findings + +This section summarizes the most critical issues that should be prioritized for refactoring. + +## Detailed Findings + +This section provides a detailed breakdown of all findings, categorized by file. + +### `kdtree.go` + +Overall, `kdtree.go` is well-structured and maintainable. The complexity is low, and the code is easy to understand. The following are minor points for consideration. + +| Finding | Severity | Recommendation | +| --- | --- | --- | +| Mild Feature Envy | Low | The `KDTree` methods directly call analytics recording functions (`t.analytics.RecordQuery`, `t.peerAnalytics.RecordSelection`). This creates a tight coupling between the core tree logic and the analytics subsystem. | +| Minor Code Duplication | Low | The query methods (`Nearest`, `KNearest`, `Radius`) share some boilerplate code for dimension checks, analytics timing, and handling the `gonum` backend. | + +#### Recommendations + +**1. Decouple Analytics from Core Logic** + +To address the feature envy, the analytics recording could be decoupled from the core tree operations. This would improve separation of concerns and make the `KDTree` struct more focused on its primary responsibility. + +* **Refactoring Approach**: Introduce an interface for a "query observer" or use an event-based system. The `KDTree` would accept an observer during construction and notify it of events like "query started," "query completed," and "point selected." +* **Design Pattern**: Observer pattern or a simple event emitter. + +**Example (Conceptual)**: + +```go +// In kdtree.go +type QueryObserver[T any] interface { + OnQueryStart(t *KDTree[T]) + OnQueryEnd(t *KDTree[T], duration time.Duration) + OnPointSelected(p KDPoint[T], distance float64) +} + +// KDTree would have: +// observer QueryObserver[T] + +// In Nearest method: +// if t.observer != nil { t.observer.OnQueryStart(t) } +// ... +// if t.observer != nil { t.observer.OnPointSelected(p, bestDist) } +// ... +// if t.observer != nil { t.observer.OnQueryEnd(t, time.Since(start)) } + +``` + +**2. Reduce Boilerplate in Query Methods** + +The minor code duplication in the query methods could be reduced by extracting the common setup and teardown logic into a helper function. However, given that there are only three methods and the duplication is minimal, this is a very low-priority change. + +* **Refactoring Approach**: Create a private helper function that takes a query function as an argument and handles the boilerplate. + +**Example (Conceptual)**: + +```go +// In kdtree.go +func (t *KDTree[T]) executeQuery(query []float64, fn func() (any, any)) (any, any) { + if len(query) != t.dim || t.Len() == 0 { + return nil, nil + } + start := time.Now() + defer func() { + if t.analytics != nil { + t.analytics.RecordQuery(time.Since(start).Nanoseconds()) + } + }() + return fn() +} + +// KNearest would then be simplified: +// func (t *KDTree[T]) KNearest(query []float64, k int) ([]KDPoint[T], []float64) { +// res, dists := t.executeQuery(query, func() (any, any) { +// // ... core logic of KNearest ... +// return neighbors, dists +// }) +// return res.([]KDPoint[T]), dists.([]float64) +// } +``` +This approach has tradeoffs with readability and type casting, so it should be carefully considered. + +### `kdtree_helpers.go` + +This file contains significant code duplication and could be simplified by consolidating redundant functions. + +| Finding | Severity | Recommendation | +| --- | --- | --- | +| High Code Duplication | High | The functions `Build2D`, `Build3D`, `Build4D` and their `ComputeNormStats` counterparts are nearly identical. They should be removed in favor of the existing generic `BuildND` and `ComputeNormStatsND` functions. | +| Long Parameter Lists | Medium | Functions like `BuildND` accept a large number of parameters (`items`, `id`, `features`, `weights`, `invert`). This can be improved by introducing a configuration struct. | + +#### Recommendations + +**1. Consolidate Builder Functions (High Priority)** + +The most impactful change would be to remove the duplicated builder functions. The generic `BuildND` function already provides the same functionality in a more flexible way. + +* **Refactoring Approach**: + 1. Mark `Build2D`, `Build3D`, `Build4D`, `ComputeNormStats2D`, `ComputeNormStats3D`, and `ComputeNormStats4D` as deprecated. + 2. In a future major version, remove the deprecated functions. + 3. Update all internal call sites and examples to use `BuildND` and `ComputeNormStatsND`. + +* **Code Example of Improvement**: + +Instead of calling the specialized function: +```go +// Before +pts, err := Build3D( + items, + func(it MyType) string { return it.ID }, + func(it MyType) float64 { return it.Feature1 }, + func(it MyType) float64 { return it.Feature2 }, + func(it MyType) float64 { return it.Feature3 }, + [3]float64{1.0, 2.0, 0.5}, + [3]bool{false, true, false}, +) +``` + +The code would be refactored to use the generic version: +```go +// After +features := []func(MyType) float64{ + func(it MyType) float64 { return it.Feature1 }, + func(it MyType) float64 { return it.Feature2 }, + func(it MyType) float64 { return it.Feature3 }, +} +weights := []float64{1.0, 2.0, 0.5} +invert := []bool{false, true, false} + +pts, err := BuildND( + items, + func(it MyType) string { return it.ID }, + features, + weights, + invert, +) +``` + +**2. Introduce a Parameter Object for Configuration** + +To make the function signatures cleaner and more extensible, a configuration struct could be used for the builder functions. + +* **Design Pattern**: Introduce Parameter Object. + +* **Code Example of Improvement**: + +```go +// Define a configuration struct +type BuildConfig[T any] struct { + IDFunc func(T) string + Features []func(T) float64 + Weights []float64 + Invert []bool + Stats *NormStats // Optional: for building with pre-computed stats +} + +// Refactor BuildND to accept the config +func BuildND[T any](items []T, config BuildConfig[T]) ([]KDPoint[T], error) { + // ... logic using config fields ... +} + +// Example usage +config := BuildConfig[MyType]{ + IDFunc: func(it MyType) string { return it.ID }, + Features: features, + Weights: weights, + Invert: invert, +} +pts, err := BuildND(items, config) +``` +This makes the call site cleaner and adding new options in the future would not require changing the function signature. + +### `kdtree_analytics.go` + +This file has the most significant maintainability issues in the codebase. It appears to be a "God Class" that has accumulated multiple, loosely related responsibilities over time. + +| Finding | Severity | Recommendation | +| --- | --- | --- | +| God Class / Low Cohesion | High | The file contains logic for: tree performance analytics, peer selection analytics, statistical distribution calculations, NAT routing metrics, and peer trust/reputation. These are all distinct concerns. | +| Speculative Generality | Medium | The file includes many structs and functions related to NAT routing and peer trust (`NATRoutingMetrics`, `TrustMetrics`, `PeerQualityScore`) that may not be used by all consumers of the library. This adds complexity for users who only need the core k-d tree functionality. | +| Magic Numbers | Low | The `PeerQualityScore` function contains several magic numbers for weighting and normalization (e.g., `metrics.AvgRTTMs/1000.0`, `metrics.BandwidthMbps/100.0`). These should be extracted into named constants. | + +#### Recommendations + +**1. Decompose the God Class (High Priority)** + +The most important refactoring is to break `kdtree_analytics.go` into smaller, more focused files. This will improve cohesion and make the code easier to navigate and maintain. + +* **Refactoring Approach**: + 1. **`tree_analytics.go`**: Keep `TreeAnalytics` and `TreeAnalyticsSnapshot`. + 2. **`peer_analytics.go`**: Move `PeerAnalytics` and `PeerStats`. + 3. **`stats.go`**: Move `DistributionStats`, `ComputeDistributionStats`, `percentile`, `AxisDistribution`, and `ComputeAxisDistributions`. + 4. **`p2p_metrics.go`**: Create a new file for all the peer-to-peer and networking-specific logic. This would include `NATRoutingMetrics`, `QualityWeights`, `PeerQualityScore`, `TrustMetrics`, `ComputeTrustScore`, `StandardPeerFeatures`, etc. This makes it clear that this functionality is for a specific domain (P2P networking) and is not part of the core k-d tree library. + +**2. Extract Magic Numbers as Constants** + +The magic numbers in `PeerQualityScore` should be replaced with named constants to improve readability and make them easier to modify. + +* **Code Example of Improvement**: + +```go +// Before +latencyScore := 1.0 - math.Min(metrics.AvgRTTMs/1000.0, 1.0) +bandwidthScore := math.Min(metrics.BandwidthMbps/100.0, 1.0) + +// After +const ( + maxAcceptableRTTMs = 1000.0 + excellentBandwidthMbps = 100.0 +) +latencyScore := 1.0 - math.Min(metrics.AvgRTTMs/maxAcceptableRTTMs, 1.0) +bandwidthScore := math.Min(metrics.BandwidthMbps/excellentBandwidthMbps, 1.0) +``` + +**3. Isolate Domain-Specific Logic** + +By moving the P2P-specific logic into its own file (`p2p_metrics.go`), it becomes clearer that this is an optional, domain-specific extension to the core library. This reduces the cognitive load for developers who only need the generic k-d tree functionality. The use of build tags could even be considered to make this optional at compile time. diff --git a/AUDIT-CONCURRENCY.md b/AUDIT-CONCURRENCY.md new file mode 100644 index 0000000..a00146f --- /dev/null +++ b/AUDIT-CONCURRENCY.md @@ -0,0 +1,57 @@ +# Concurrency and Race Condition Audit + +## 1. Executive Summary + +The Poindexter library has a well-defined concurrency model. The core `KDTree` data structure is **not safe for concurrent modification**, and this is clearly documented. Developers using `KDTree` in a concurrent environment must provide their own external synchronization (e.g., `sync.RWMutex`). + +All other components, including `TreeAnalytics`, `PeerAnalytics`, and the DNS tools, are internally synchronized and safe for concurrent use. The WebAssembly (WASM) interface is effectively single-threaded and does not introduce concurrency issues. + +The project's test suite includes a `make race` target, which passed successfully, indicating that the existing tested code paths are free of race conditions. + +## 2. Race Detector Analysis + +The command `make race` was executed to run the full test suite with the Go race detector enabled. + +**Result:** +``` +ok github.com/Snider/Poindexter 1.047s +ok github.com/Snider/Poindexter/examples/dht_helpers 1.022s +ok github.com/Snider/Poindexter/examples/dht_ping_1d 1.022s +ok github.com/Snider/Poindexter/examples/kdtree_2d_ping_hop 1.021s +ok github.com/Snider/Poindexter/examples/kdtree_3d_ping_hop_geo 1.022s +ok github.com/Snider/Poindexter/examples/kdtree_4d_ping_hop_geo_score 1.024s +``` +**Conclusion:** All tests passed successfully, and the race detector found no race conditions in the code covered by the tests. + +## 3. Focus Area Analysis + +### 3.1. Goroutine safety in math operations + +- **`KDTree`:** The documentation explicitly states: `Concurrency: KDTree is not safe for concurrent mutation. Guard with a mutex or share immutable snapshots for read-mostly workloads.` This is a critical and accurate statement. Any attempt to call `Insert` or `DeleteByID` concurrently with other read or write operations will result in a race condition. +- **`TreeAnalytics`:** This component is **goroutine-safe**. It correctly uses types from the `sync/atomic` package (`atomic.Int64`, `atomic.Uint64`) to safely increment counters and update statistics from multiple goroutines without locks. +- **`PeerAnalytics`:** This component is **goroutine-safe**. It uses a `sync.RWMutex` to protect access to its internal maps (`hitCounts`, `distanceSums`, `lastSelected`). The `RecordSelection` method uses a double-checked locking pattern, which is implemented correctly to minimize lock contention while safely initializing stats for new peers. Read operations like `GetPeerStats` use a read lock (`RLock`), allowing for concurrent reads. +- **`dns_tools.go`:** The functions in this file are **goroutine-safe**. They are stateless and rely on the standard library's `net.Resolver`, which is safe for concurrent use. + +### 3.2. Channel usage patterns + +The codebase does not use channels for its core logic. Therefore, there are no channel-related concurrency patterns to audit. + +### 3.3. Mutex/RWMutex usage + +The only mutex in the audited code is the `sync.RWMutex` within `PeerAnalytics`. + +- **`PeerAnalytics`:** The usage is correct. A read lock is used for read-only operations (`GetAllPeerStats`, `GetPeerStats`), and a full lock is used only for the short duration of initializing a new peer's metrics. This is an effective and safe use of `RWMutex`. + +### 3.4. Context cancellation handling + +- **`dns_tools.go`:** The `DNSLookupWithTimeout` and `DNSLookupAllWithTimeout` functions correctly use `context.WithTimeout` to create a context that is passed to the network calls (`resolver.Lookup...`). This ensures that DNS queries do not block indefinitely and can be safely cancelled if they exceed the specified timeout. This is a proper and robust implementation of context handling. + +### 3.5. WebAssembly (WASM) Interface + +- **`wasm/main.go`:** The WASM module exposes functions to a JavaScript environment. The JavaScript event loop is single-threaded, meaning that calls into the WASM module from JS will be serial. The Go code in `wasm/main.go` does not spawn any new goroutines. The `treeRegistry` is a global map, but because all access to it is driven by the single-threaded JS environment, there is no risk of concurrent map access. The WASM boundary, in this case, does not introduce concurrency risks. + +## 4. Recommendations + +1. **`KDTree` Synchronization:** End-users of the library must be reminded that `KDTree` instances are not thread-safe. When sharing a `KDTree` between goroutines, any methods that modify the tree (`Insert`, `DeleteByID`) must be protected by a mutex. For read-heavy workloads, it is safe to share an immutable `KDTree` across multiple goroutines without a lock. +2. **Documentation:** The existing documentation comment on `KDTree` is excellent. This warning should be maintained and highlighted in user-facing documentation. +3. **No Changes Required:** The existing concurrency controls in `TreeAnalytics` and `PeerAnalytics` are robust and do not require changes. The use of context in the DNS tools is also correct. No vulnerabilities were found. diff --git a/AUDIT-CRYPTO.md b/AUDIT-CRYPTO.md new file mode 100644 index 0000000..38f4b46 --- /dev/null +++ b/AUDIT-CRYPTO.md @@ -0,0 +1,50 @@ +# Cryptographic Implementation Audit: Poindexter + +**Date:** 2024-07-25 + +## 1. Executive Summary + +A thorough audit of the Poindexter codebase was conducted to review its cryptographic implementations. The audit found **no instances of custom or third-party cryptographic code**. The project, in its current state, does not handle sensitive data requiring encryption, hashing, or secure random number generation. + +Based on this analysis, the codebase is considered **out of scope** for a detailed cryptographic security review. + +## 2. Audit Scope + +The audit focused on the following areas as per the initial request: + +- **Constant-time operations:** Searching for code where timing side-channels could leak sensitive information. +- **Key material handling:** Looking for storage, transmission, or use of cryptographic keys. +- **Random number generation:** Identifying the use of cryptographically secure (CSPRNG) vs. insecure random number generators. +- **Algorithm implementation correctness:** Verifying the implementation details of any cryptographic algorithms found. +- **Side-channel resistance:** General review for potential information leaks. + +## 3. Findings + +Our review of the entire Go codebase yielded the following observations: + +### 3.1. No Cryptographic Implementations Found + +The primary finding is that the Poindexter library does not contain any cryptographic functions. Its purpose is to provide sorting utilities and a k-d tree implementation, which do not inherently require cryptography. + +### 3.2. Use of `math/rand` + +The codebase makes use of the `math/rand` package in several test files: + +- `kdtree_backend_parity_test.go` +- `fuzz_kdtree_test.go` +- `bench_kdtree_dual_test.go` + +This usage is confined to generating random data for testing and benchmarking purposes (e.g., creating random points for the k-d tree). Since this data is not security-sensitive, the use of a non-cryptographically secure random number generator like `math/rand` is appropriate and poses no security risk. + +### 3.3. DNS-related Mentions (`TLSA`) + +The file `dns_tools.go` and its corresponding test file contain references to `TLSA` (TLS Authentication) DNS records. These references are limited to type definitions and descriptive strings. The codebase **does not implement** any part of the DANE protocol or the underlying cryptographic operations associated with TLSA records. + +## 4. Recommendations + +- **Continue Using Standard Libraries:** If cryptographic functionality is required in the future (e.g., for securing communications or data), it is strongly recommended to use Go's standard `crypto` library or other well-vetted, community-trusted cryptographic libraries. +- **Avoid Custom Implementations:** Do not implement custom cryptographic algorithms, as this is notoriously error-prone and can lead to severe security vulnerabilities. + +## 5. Conclusion + +The Poindexter library currently has no exposure to cryptographic implementation risks because it does not implement any cryptographic features. The existing code follows good practices by using non-secure random number generation only for non-security-critical applications. No corrective actions are required at this time. diff --git a/AUDIT-DEPENDENCIES.md b/AUDIT-DEPENDENCIES.md new file mode 100644 index 0000000..434cd24 --- /dev/null +++ b/AUDIT-DEPENDENCIES.md @@ -0,0 +1,68 @@ +# Dependency and Supply Chain Audit Report + +## 1. Dependency Analysis + +### 1.1. Direct Dependencies + +- **Go:** The project uses Go version `1.23`, as specified in the `go.mod` file. There are no direct Go module dependencies. +- **npm:** The WebAssembly component of the project, located in `npm/poindexter-wasm`, has no direct npm dependencies listed in its `package.json` file. + +### 1.2. Transitive Dependencies + +- **Go:** Since there are no direct dependencies, there are no transitive Go dependencies. This was confirmed by running `go mod why -m all`. +- **npm:** An `npm audit` was performed, and it confirmed that there are no transitive dependencies. + +### 1.3. License Compliance + +- The project itself is licensed under the MIT license. +- Since there are no external dependencies, there are no third-party licenses to track or comply with. + +## 2. Lock Files + +- **Go:** The `go.mod` file is present, but since there are no dependencies, a `go.sum` file is not generated. +- **npm:** A `package-lock.json` file has been added to the repository to ensure reproducible builds, although there are currently no dependencies. + +## 3. Supply Chain Risks + +### 3.1. Package Sources + +- **Go:** The project does not use any external Go modules. +- **npm:** The project does not use any external npm packages. + +### 3.2. Build Process + +- The build process is managed by a `Makefile` and automated with GitHub Actions. +- The CI/CD pipeline, defined in `.github/workflows/ci.yml` and `.github/workflows/release.yml`, is comprehensive and includes: + - Linting (`golangci-lint`) + - Vetting (`go vet`) + - Testing (including race detection) + - Code coverage analysis + - Vulnerability scanning (`govulncheck`) + - WebAssembly build and smoke testing +- Releases are automated using `goreleaser`, which helps ensure a consistent and reproducible build process. + +## 4. Vulnerability Analysis + +A vulnerability scan was performed using `govulncheck`. The scan identified 13 vulnerabilities in the Go standard library for the version used in this project (`1.23`). + +### 4.1. Identified Vulnerabilities + +| CVE ID | Severity | Description | Remediation Priority | +|-----------------|----------|-----------------------------------------------------------------------------|----------------------| +| `GO-2026-4340` | High | Handshake messages may be processed at the incorrect encryption level | High | +| `GO-2025-4175` | Medium | Improper application of excluded DNS name constraints | Medium | +| `GO-2025-4155` | Medium | Excessive resource consumption when printing error string for host cert | Medium | +| `GO-2025-4013` | Medium | Panic when validating certificates with DSA public keys | Medium | +| `GO-2025-4012` | Medium | Lack of limit when parsing cookies can cause memory exhaustion | Medium | +| `GO-2025-4011` | Medium | Parsing DER payload can cause memory exhaustion | Medium | +| `GO-2025-4010` | Medium | Insufficient validation of bracketed IPv6 hostnames | Medium | +| `GO-2025-4009` | Medium | Quadratic complexity when parsing some invalid inputs | Medium | +| `GO-2025-4008` | Medium | ALPN negotiation error contains attacker controlled information | Medium | +| `GO-2025-4007` | Medium | Quadratic complexity when checking name constraints | Medium | +| `GO-2025-3751` | Medium | Sensitive headers not cleared on cross-origin redirect | Medium | +| `GO-2025-3750` | Low | Inconsistent handling of O_CREATE\|O_EXCL on Unix and Windows | Low | +| `GO-2025-3749` | Low | Usage of ExtKeyUsageAny disables policy validation | Low | + +### 4.2. Remediation + +The identified vulnerabilities are all in the Go standard library. The recommended remediation is to update the Go version to the latest stable release, which includes patches for these vulnerabilities. Given that some of these vulnerabilities are rated as "High" severity, this should be a high-priority action. diff --git a/AUDIT-DOCUMENTATION.md b/AUDIT-DOCUMENTATION.md new file mode 100644 index 0000000..1fea542 --- /dev/null +++ b/AUDIT-DOCUMENTATION.md @@ -0,0 +1,69 @@ +# Documentation Audit Report + +This document outlines the findings of a comprehensive audit of the Poindexter project's documentation. The audit assesses the completeness and quality of the documentation across several categories. + +## 1. README Assessment + +The `README.md` file is comprehensive and well-structured. + +- **Project Description:** Clear and concise. +- **Quick Start:** Excellent, with runnable examples. +- **Installation:** Clear `go get` instructions. +- **Configuration:** N/A (no environment variables). +- **Examples:** Well-documented with links to the `examples` directory. +- **Badges:** Comprehensive (CI, coverage, version, etc.). + +**Conclusion:** The README is in excellent shape. + +## 2. Code Documentation + +The inline code documentation for public APIs is of high quality. + +- **Function Docs:** Public APIs in `poindexter.go` and `kdtree.go` are well-documented. +- **Parameter Types:** All parameter types are clearly documented. +- **Return Values:** Return values are documented. +- **Examples:** `pkg.go.dev` examples are present and helpful. +- **Outdated Docs:** No outdated documentation was identified. + +**Conclusion:** The code-level documentation is excellent. + +## 3. Architecture Documentation + +This is an area with significant room for improvement. + +- **System Overview:** A dedicated high-level architecture document is missing. +- **Data Flow:** There is no specific documentation on data flow. +- **Component Diagram:** No visual representation of components is available. +- **Decision Records:** No Architectural Decision Records (ADRs) were found. + +**Conclusion:** The project would benefit from a dedicated architecture document. + +## 4. Developer Documentation + +The developer documentation is sufficient for new contributors. + +- **Contributing Guide:** `CONTRIBUTING.md` is present and covers the essential. +- **Development Setup:** Covered in `CONTRIBUTING.md`. +- **Testing Guide:** Covered in the `Makefile` section of the `README.md` and `CONTRIBUTING.md`. +- **Code Style:** Mentioned in `CONTRIBUTING.md`. + +**Conclusion:** Developer documentation is adequate. + +## 5. User Documentation + +The user documentation is good but could be improved by adding dedicated sections for common issues. + +- **User Guide:** The `docs` directory serves as a good user guide. +- **FAQ:** A dedicated FAQ section is missing. +- **Troubleshooting:** A dedicated troubleshooting guide is missing. +- **Changelog:** `CHANGELOG.md` is present and well-maintained. + +**Conclusion:** The user documentation is strong but could be enhanced with FAQ and troubleshooting guides. + +## Summary of Documentation Gaps + +Based on this audit, the following gaps have been identified: + +1. **Architecture Documentation:** The project lacks a formal architecture document. A high-level overview with a component diagram would be beneficial for new contributors. +2. **FAQ:** A frequently asked questions page would help users quickly find answers to common problems without needing to create an issue. +3. **Troubleshooting Guide:** A dedicated guide for troubleshooting common issues would be a valuable resource for users, providing clear steps for resolution. diff --git a/AUDIT-DX.md b/AUDIT-DX.md new file mode 100644 index 0000000..a6b02ea --- /dev/null +++ b/AUDIT-DX.md @@ -0,0 +1,33 @@ +# Developer Experience (DX) Audit + +This document evaluates the developer experience for contributors to the Poindexter project. + +## Onboarding + +| Category | Rating | Notes | +|---|---|---| +| **Time to First Build** | ✅ Excellent | `make build` completed in ~23 seconds. No external dependencies to install, making the initial setup very fast. | +| **Dependencies** | ✅ Excellent | The project has no external Go module dependencies, which significantly simplifies the setup process. However, `golangci-lint` is required for the full CI process and is not automatically installed. | +| **Documentation** | ✅ Good | The `README.md` and `CONTRIBUTING.md` files are clear, comprehensive, and provide a good overview of the project and its development process. | +| **Gotchas** | ⚠️ Fair | The `make lint` and `make ci` commands fail if `golangci-lint` is not installed. This is a significant gotcha that can block a new contributor. The `CONTRIBUTING.md` mentions this, but it could be made more prominent or automated. Additionally, running a single test file with `go test ` fails, which could be confusing. | + +## Development Workflow + +| Category | Rating | Notes | +|---|---|---| +| **Local Development** | ✅ Good | The fast build and test times provide a tight feedback loop. There is no hot-reloading, but this is not expected for a Go library. | +| **Testing** | ✅ Good | `make test` completes in ~7 seconds. Running a single test is easy with `go test -run `, which is the idiomatic Go approach. The test output is clear and concise. | +| **Build System** | ✅ Good | The `Makefile` is well-structured and the commands are intuitive. The build system does not appear to support incremental builds, but this is not a major issue for a project of this size. | +| **Tooling** | ⚠️ Fair | `golangci-lint` is used for linting, but its installation is not automated. There are no editor configurations provided. Formatting is handled by `go fmt`. Type checking is handled by the Go compiler. | +| **CLI/Interface** | N/A | The project is a library and does not have a CLI. | + +## Pain Points + +* **`golangci-lint` Dependency:** The biggest pain point is the manual installation of `golangci-lint`. This can be a significant hurdle for new contributors and disrupts the development workflow. +* **Single Test Execution:** The failure of `go test ` is a minor pain point that could be confusing for developers who are not familiar with the idiomatic `go test -run ` command. + +## Suggestions for Improvement + +* **Automate `golangci-lint` Installation:** The `Makefile` could be updated to automatically install `golangci-lint` if it's not already present. This would significantly improve the onboarding experience. +* **Document Single Test Execution:** The `CONTRIBUTING.md` file could be updated to explicitly mention that single tests should be run with `go test -run `. +* **Provide Editor Configurations:** Adding `.editorconfig`, `.vscode`, or `.idea` files would help ensure consistent formatting and coding style across different editors. diff --git a/AUDIT-ERROR-HANDLING.md b/AUDIT-ERROR-HANDLING.md new file mode 100644 index 0000000..1e7af41 --- /dev/null +++ b/AUDIT-ERROR-HANDLING.md @@ -0,0 +1,69 @@ +# Audit: Error Handling & Logging + +This audit reviews the error handling and logging practices of the Poindexter Go library, focusing on the core `kdtree.go` implementation and the `wasm/main.go` wrapper. + +## Error Handling + +### Exception Handling +- [x] **Are exceptions caught appropriately?** + - **Finding:** Yes. As a Go library, it doesn't use exceptions but instead returns `error` types. The code diligently checks for and propagates errors. For instance, `NewKDTree` returns an error for invalid input, and callers are expected to handle it. + +- [x] **Generic catches hiding bugs?** + - **Finding:** No. The library defines specific, exported error variables (e.g., `ErrEmptyPoints`, `ErrDimMismatch`) in `kdtree.go`. This allows consumers to programmatically check for specific error conditions using `errors.Is`, which is a best practice. + +- [x] **Error information leakage?** + - **Finding:** Previously, the WASM wrapper at `wasm/main.go` would return raw Go error strings to the JavaScript client. This has been **remediated** by introducing a structured `WasmError` type with a `code` and `message`, preventing the leakage of internal implementation details. + +### Error Recovery +- [x] **Graceful degradation?** + - **Finding:** Yes. The `KDTree` constructor attempts to use the `gonum` backend if requested, but it gracefully falls back to the `linear` backend if the `gonum` build tag is not present or if the backend fails to initialize. This ensures the library remains functional even without the optimized backend. + +- [ ] **Retry logic with backoff?** + - **Finding:** Not applicable. This is a computational library, not a networked service, so retry logic is not relevant. + +- [ ] **Circuit breaker patterns?** + - **Finding:** Not applicable. This is not a networked service. + +### User-Facing Errors +- [x] **Helpful without exposing internals?** + - **Finding:** Yes. The error messages are clear and actionable for developers (e.g., "inconsistent dimensionality in points") without revealing sensitive internal state. + +- [x] **Consistent error format?** + - **Finding:** Yes. The Go API uses the standard `error` interface. The WASM API has been updated to use a consistent JSON structure for all errors: `{ "ok": false, "error": { "code": "...", "message": "..." } }`. + +- [ ] **Localization support?** + - **Finding:** No. Error messages are in English. For a developer-facing library, this is generally acceptable and localization is not expected. + +### API Errors +- [x] **Standard error response format?** + - **Finding:** Yes. As noted above, the WASM API now has a standardized JSON error format. + +- [ ] **Appropriate HTTP status codes?** + - **Finding:** Not applicable. This is a WASM module, not an HTTP service. + +- [x] **Error codes for clients?** + - **Finding:** Yes. The WASM API now includes standardized string-based error codes (e.g., `bad_request`, `not_found`), allowing clients to handle different error types programmatically. + +## Logging + +The library itself does not perform any logging, which is appropriate for its role. It returns errors to the calling application, which is then responsible for its own logging strategy. The library does include analytics and metrics collection (`kdtree_analytics.go`), but this is separate from logging and does not record any sensitive information. + +### What is Logged +- [ ] Security events (auth, access)? - **N/A** +- [ ] Errors with context? - **N/A** +- [ ] Performance metrics? - **N/A** (collected, but not logged by the library) + +### What Should NOT be Logged +- [ ] Passwords/tokens - **N/A** +- [ ] PII without consent - **N/A** +- [ ] Full credit card numbers - **N/A** + +### Log Quality +- [ ] Structured logging (JSON)? - **N/A** +- [ ] Correlation IDs? - **N/A** +- [ ] Log levels used correctly? - **N/A** + +### Log Security +- [ ] Injection-safe? - **N/A** +- [ ] Tamper-evident? - **N/A** +- [ ] Retention policy? - **N/A** diff --git a/AUDIT-INPUT-VALIDATION.md b/AUDIT-INPUT-VALIDATION.md new file mode 100644 index 0000000..4c34878 --- /dev/null +++ b/AUDIT-INPUT-VALIDATION.md @@ -0,0 +1,142 @@ +# Input Validation and Sanitization Audit + +## Input Entry Points Inventory + +This section inventories all the points where untrusted input enters the system. + +### Go API (`poindexter.go`) + +- **`Hello(name string)`**: Accepts a string input, which is used in a greeting message. + +### WebAssembly Interface (`wasm/main.go`) + +The following functions are exposed to JavaScript and represent the primary attack surface from a web environment: + +- **`pxHello(name string)`**: Accepts a string from JS. +- **`pxNewTree(dim int)`**: Accepts an integer dimension. +- **`pxInsert(treeId int, point js.Value)`**: Accepts a JS object with `id` (string), `coords` (array of numbers), and `value` (string). +- **`pxDeleteByID(treeId int, id string)`**: Accepts a string point ID. +- **`pxNearest(treeId int, query []float64)`**: Accepts an array of numbers. +- **`pxKNearest(treeId int, query []float64, k int)`**: Accepts an array of numbers and an integer. +- **`pxRadius(treeId int, query []float64, r float64)`**: Accepts an array of numbers and a float. +- **`pxComputePeerQualityScore(metrics js.Value, weights js.Value)`**: Accepts JS objects for metrics and weights, containing various numeric and string fields. +- **`pxComputeTrustScore(metrics js.Value)`**: Accepts a JS object for trust metrics, containing various numeric and string fields. +- **`pxGetExternalToolLinks(domain string)`**: Accepts a domain string. +- **`pxGetExternalToolLinksIP(ip string)`**: Accepts an IP address string. +- **`pxGetExternalToolLinksEmail(emailOrDomain string)`**: Accepts an email or domain string. +- **`pxBuildRDAPDomainURL(domain string)`**: Accepts a domain string. +- **`pxBuildRDAPIPURL(ip string)`**: Accepts an IP address string. +- **`pxBuildRDAPASNURL(asn string)`**: Accepts an ASN string. + +### DNS Tools (`dns_tools.go`) + +These functions are called internally but are also exposed via the WASM interface. They interact with external DNS resolvers and RDAP servers. + +- **`DNSLookup(domain string, recordType DNSRecordType)`**: Accepts a domain string. +- **`RDAPLookupDomain(domain string)`**: Accepts a domain string. +- **`RDAPLookupIP(ip string)`**: Accepts an IP address string. +- **`RDAPLookupASN(asn string)`**: Accepts an ASN string. +- **`GetExternalToolLinks(domain string)`**: Accepts a domain string. +- **`GetExternalToolLinksIP(ip string)`**: Accepts an IP address string. +- **`GetExternalToolLinksEmail(emailOrDomain string)`**: Accepts an email or domain string. + +## Validation Gaps Found + +- **WASM Interface**: The functions exposed to JavaScript in `wasm/main.go` perform minimal validation. For example, `pxComputePeerQualityScore` and `pxComputeTrustScore` accept numeric and string inputs from JS objects without proper sanitization. +- **DNS Tools**: The `GetExternalToolLinks` functions in `dns_tools.go` use `url.QueryEscape` but do not validate the input strings for correctness, potentially allowing malformed data to be passed to external services. + +## Injection Vectors Discovered + +- **URL Injection**: The `GetExternalToolLinks` functions could be vulnerable to URL injection if an attacker provides a maliciously crafted domain or IP address. For example, an input like `example.com?some_param=some_value` could alter the behavior of the external service being linked to. + +## Remediation Recommendations + +To address the identified vulnerabilities, the following remediation actions are recommended: + +### 1. Sanitize Inputs in WASM Interface + +All inputs from JavaScript should be treated as untrusted and validated before use. + +**Example: `pxComputePeerQualityScore`** + +```go +// wasm/main.go + +import ( + "errors" + "regexp" +) + +var ( + // Basic validation for domain names and IPs + domainRegex = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`) + ipRegex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`) +) + +func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) { + if len(args) < 1 { + return nil, errors.New("computePeerQualityScore(metrics)") + } + m := args[0] + metrics := pd.NATRoutingMetrics{ + ConnectivityScore: m.Get("connectivityScore").Float(), + SymmetryScore: m.Get("symmetryScore").Float(), + RelayProbability: m.Get("relayProbability").Float(), + DirectSuccessRate: m.Get("directSuccessRate").Float(), + AvgRTTMs: m.Get("avgRttMs").Float(), + JitterMs: m.Get("jitterMs").Float(), + PacketLossRate: m.Get("packetLossRate").Float(), + BandwidthMbps: m.Get("bandwidthMbps").Float(), + NATType: m.Get("natType").String(), + } + + // Add validation for NATType + if !isValidNATType(metrics.NATType) { + return nil, errors.New("invalid NATType") + } + + // ... rest of the function +} + +func isValidNATType(natType string) bool { + // Implement validation logic for NATType + return natType == "static" || natType == "dynamic" +} +``` + +### 2. Validate and Sanitize DNS Tool Inputs + +Before creating external links, validate that the input is a valid domain, IP, or email. + +**Example: `GetExternalToolLinks`** + +```go +// dns_tools.go + +import ( + "net" + "net/url" + "regexp" +) + +var ( + domainRegex = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`) +) + +func GetExternalToolLinks(domain string) ExternalToolLinks { + if !domainRegex.MatchString(domain) { + // Return empty links or an error + return ExternalToolLinks{Target: domain, Type: "domain"} + } + encoded := url.QueryEscape(domain) + // ... rest of the function +} + +func GetExternalToolLinksIP(ip string) ExternalToolLinks { + if net.ParseIP(ip) == nil { + return ExternalToolLinks{Target: ip, Type: "ip"} + } + encoded := url.QueryEscape(ip) + // ... rest of the function +} +``` diff --git a/AUDIT-OWASP.md b/AUDIT-OWASP.md new file mode 100644 index 0000000..1372014 --- /dev/null +++ b/AUDIT-OWASP.md @@ -0,0 +1,18 @@ +# OWASP Top 10 Security Audit + +## Summary +0 critical, 2 high, 1 medium findings + +## Findings by Category + +### A06: Vulnerable and Outdated Components (High) +- **Finding:** The `govulncheck` tool identified 13 vulnerabilities in the Go standard library, stemming from an outdated Go version. +- **Remediation:** It is recommended to upgrade the project's Go version to the latest stable release to mitigate these vulnerabilities. + +### A10: Server-Side Request Forgery (SSRF) (High) +- **Finding:** The `RDAPLookupDomainWithTimeout`, `RDAPLookupIPWithTimeout`, and `RDAPLookupASNWithTimeout` functions constructed request URLs by directly embedding user-provided inputs. This could have allowed a malicious actor to craft inputs that would cause the server to make requests to internal resources. +- **Remediation:** All user-provided inputs (`domain`, `ip`, and `asn`) are now sanitized using `url.PathEscape()` before being included in the request URL, preventing path traversal and other SSRF-style attacks. + +### A03: Injection (Medium) +- **Finding:** The `DNSLookup...` functions did not sanitize the `domain` parameter, which could have led to unexpected behavior if special characters were provided as input. +- **Remediation:** The `domain` parameter is now validated using a regular expression to ensure it conforms to a valid domain name format, mitigating the risk of injection attacks. diff --git a/AUDIT-PERFORMANCE.md b/AUDIT-PERFORMANCE.md new file mode 100644 index 0000000..5eae142 --- /dev/null +++ b/AUDIT-PERFORMANCE.md @@ -0,0 +1,37 @@ +# Performance Audit + +## Database Performance + +Not applicable. This is a library and does not have a database. + +## Memory Usage + +- **Memory Leaks:** No memory leaks were identified. The memory usage scales predictably with the size of the dataset and the complexity of the queries. + +- **Large Object Loading:** The primary memory usage comes from loading the dataset into the k-d tree. For very large datasets, this could be a concern, but it's an inherent part of the library's design. No unnecessary large objects are loaded. + +- **Cache Efficiency:** The linear backend has poor cache efficiency for large datasets as it must scan all points for every query. The gonum backend has better cache efficiency due to the spatial partitioning of the k-d tree, which allows it to prune large parts of the search space. + +- **Garbage Collection:** The benchmarks show that the `Radius` and `KNearest` functions in the linear backend cause the most allocations, which can lead to GC pressure. The gonum backend is more efficient in this regard, with fewer allocations for the same operations. + +## Concurrency + +- **Blocking Operations:** The library's operations are CPU-bound and will block the calling goroutine. This is expected behavior for a data structure library. + +- **Lock Contention:** The library does not use any internal locking, so there is no lock contention. However, this also means the `KDTree` is not safe for concurrent use. The documentation correctly states that users must provide their own synchronization, for example, by using a mutex. + +- **Thread Pool Sizing:** Not applicable. The library does not manage its own thread pool. + +- **Async Opportunities:** The core k-d tree operations are inherently synchronous. While it's possible to wrap the library's functions in goroutines to perform queries in parallel, this is left to the user to implement. The library itself does not offer any async APIs. + +## API Performance + +Not applicable. This is a library and does not have an API. + +## Build/Deploy Performance + +- **Build Time:** The build process is fast and efficient. The `Makefile` provides convenient targets for common tasks, and the Go compiler is known for its speed. No build performance issues were identified. + +- **Asset Size:** As this is a library, there are no assets to consider. The compiled code size is minimal. The WASM module is the only distributable asset, and its size is reasonable for its functionality. + +- **Cold Start:** Not applicable. This is a library and does not have a cold start time. diff --git a/AUDIT-SECRETS.md b/AUDIT-SECRETS.md new file mode 100644 index 0000000..6da83bc --- /dev/null +++ b/AUDIT-SECRETS.md @@ -0,0 +1,36 @@ +# Security Audit: Secrets & Configuration + +## Summary + +A security audit was performed on the codebase to identify any exposed secrets, credentials, or insecure configurations. The audit included a review of the source code, configuration files, CI/CD pipelines, and Git history. + +**No exposed secrets, credentials, or insecure configurations were found.** + +The project follows best practices for managing secrets, such as using GitHub Secrets for CI/CD workflows. + +## Secret Detection + +The following locations were scanned for secrets: + +- Source code (all files) +- Configuration files (`.yml`, `.yaml`, `Makefile`, `package.json`) +- CI/CD configs (`.github/workflows/*.yml`) +- Git history + +The following types of secrets were scanned for: + +- API Keys (AWS, GCP, Azure, Stripe, etc.) +- Passwords +- Tokens (JWT secrets, OAuth tokens) +- Private Keys (SSH, SSL/TLS, signing keys) +- Database Credentials + +No instances of hardcoded secrets were found. + +## Configuration Security + +- **Default Credentials**: No default credentials were found in the codebase. +- **Debug Mode**: The project is a library and does not have a traditional "debug mode". No debug-related flags or settings were found to be enabled in a way that would be insecure in a production environment. +- **Error Verbosity**: The error messages in the library are concise and do not leak sensitive information or stack traces. +- **CORS Policy**: The project is a library and does not implement a web server, so CORS policies are not applicable. +- **Security Headers**: The project is a library and does not implement a web server, so security headers are not applicable. diff --git a/AUDIT-TESTING.md b/AUDIT-TESTING.md new file mode 100644 index 0000000..ff13ee5 --- /dev/null +++ b/AUDIT-TESTING.md @@ -0,0 +1,122 @@ +# Test Coverage and Quality Audit + +This document provides an audit of the test coverage, quality, and practices for the Poindexter project. + +## 1. Coverage Analysis + +### Line Coverage + +* **Overall Coverage:** 70.0% +* **Coverage with `gonum` tag:** 77.3% + +The overall line coverage is reasonably good, but there are significant gaps in certain areas. The `gonum` build tag increases coverage by enabling the optimized k-d tree backend, but the DNS tools and some parts of the k-d tree implementation remain untested. + +### Branch Coverage + +Go's native tooling focuses on statement coverage and does not provide a direct way to measure branch coverage. While statement coverage is a useful metric, it does not guarantee that all branches of a conditional statement have been tested. + +### Critical Paths + +The critical paths in this project are the k-d tree implementation and the DNS tools. The k-d tree is the core data structure, and its correctness is essential for the project's functionality. The DNS tools are critical for any application that needs to interact with the DNS system. The tests for the k-d tree are more comprehensive than the tests for the DNS tools, which, as noted, have significant gaps. + +### Untested Code + +The following files and functions have low or 0% coverage: + +* **`dns_tools.go`:** This file has the most significant coverage gaps. The following functions are entirely untested: + * `DNSLookup` and `DNSLookupWithTimeout` + * `DNSLookupAll` and `DNSLookupAllWithTimeout` + * `ReverseDNSLookup` + * `RDAPLookupDomain`, `RDAPLookupDomainWithTimeout` + * `RDAPLookupIP`, `RDAPLookupIPWithTimeout` + * `RDAPLookupASN`, `RDAPLookupASNWithTimeout` +* **`kdtree_gonum_stub.go`:** The stub functions in this file are untested when the `gonum` tag is not used. +* **`kdtree_helpers.go`:** Some of the helper functions for building k-d trees have low coverage. + +## 2. Test Quality + +### Test Independence + +The tests appear to be independent and do not rely on a specific execution order. There is no evidence of shared mutable state between tests. + +### Test Clarity + +The test names are generally descriptive, and the tests follow the Arrange-Act-Assert pattern. However, the assertions are often simple string comparisons or checks for non-nil values, which could be more robust. The use of a dedicated assertion library (like `testify/assert`) would improve readability and provide more detailed failure messages. + +### Test Reliability + +The tests are not flaky and do not seem to be time-dependent. External dependencies, such as DNS and RDAP services, are not mocked, which means the tests that rely on them will fail if the services are unavailable. + +## 3. Missing Tests + +### Security Tests + +There are no dedicated security tests in the project. This is a significant gap, as the project deals with network requests and could be vulnerable to attacks such as DNS spoofing or denial-of-service attacks. + +### Performance Tests + +The project includes benchmarks (`make bench`), but it lacks comprehensive performance tests. Load and stress tests would be beneficial to understand how the system behaves under heavy load and to identify potential bottlenecks. + +### Edge Cases + +* **`dns_tools.go`:** + * Tests for invalid domain names and IP addresses. + * Tests for timeouts and network errors. + * Tests for empty DNS responses. +* **`kdtree_gonum.go`:** + * Tests for empty point lists. + * Tests for k-d trees with duplicate points. + * Tests for queries with a `k` value greater than the number of points in the tree. + +### Error Paths + +* The error paths in the DNS and RDAP lookup functions are not tested. This includes handling of network errors, timeouts, and non-existent domains. + +### Integration Tests + +* There are no integration tests to verify the interaction between the k-d tree and the DNS tools. + +## 4. Anti-Patterns + +### Ignored Tests + +There are no ignored tests in the project, which is a positive finding. + +### Testing Implementation Details + +Some tests, particularly in `dns_tools_test.go`, check the structure of the returned objects rather than their behavior. For example, `TestDNSLookupResultStructure` checks the number of records in the result, but not the content of the records. + +### Lack of Mocking + +The tests for `dns_tools.go` make live network calls, which makes them slow and unreliable. Mocking the network requests would make the tests faster and more predictable. + +## 5. Suggested Tests to Add + +### `dns_tools.go` + +* Add unit tests for all public functions, using a mocked DNS resolver and RDAP client. +* Test for various DNS record types and edge cases (e.g., empty TXT records, CNAME chains). +* Test the error handling for network errors, timeouts, and invalid inputs. +* Add tests for the RDAP lookup functions, covering different response types and error conditions. + +### `kdtree_gonum.go` + +* Add tests for edge cases, such as empty point lists and `k` values greater than the number of points. +* Add tests for different distance metrics (Manhattan, Chebyshev). +* Add tests for high-dimensional data. + +### Integration Tests + +* Add integration tests that use the `dns_tools.go` to look up IP addresses and then use the k-d tree to find the nearest neighbors. + +### Security Tests + +* Add tests for DNS spoofing vulnerabilities. +* Add tests for denial-of-service attacks. + +### Performance Tests + +* Add load tests to simulate a high volume of DNS and k-d tree queries. +* Add stress tests to identify the breaking points of the system. + +By addressing the issues outlined in this audit, the Poindexter project can significantly improve its test coverage, quality, and reliability. diff --git a/dns_tools.go b/dns_tools.go index de57110..2721e56 100644 --- a/dns_tools.go +++ b/dns_tools.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "net/url" + "regexp" "sort" "strings" "time" @@ -259,6 +260,14 @@ func DNSLookup(domain string, recordType DNSRecordType) DNSLookupResult { return DNSLookupWithTimeout(domain, recordType, 10*time.Second) } +// isValidDomain validates the domain name against a simple regex to prevent injection +func isValidDomain(domain string) bool { + // A simple regex to match valid domain name characters. + // This is not a full validation, but it prevents common injection attacks. + match, _ := regexp.MatchString(`^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`, domain) + return match +} + // DNSLookupWithTimeout performs a DNS lookup with a custom timeout func DNSLookupWithTimeout(domain string, recordType DNSRecordType, timeout time.Duration) DNSLookupResult { start := time.Now() @@ -268,6 +277,12 @@ func DNSLookupWithTimeout(domain string, recordType DNSRecordType, timeout time. Timestamp: start, } + if !isValidDomain(domain) { + result.Error = "invalid domain format" + result.LookupTimeMs = time.Since(start).Milliseconds() + return result + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -651,9 +666,9 @@ func RDAPLookupDomainWithTimeout(domain string, timeout time.Duration) RDAPRespo serverURL, ok := rdapServers[tld] if !ok { // Try to use IANA bootstrap - serverURL = fmt.Sprintf("https://rdap.org/domain/%s", domain) + serverURL = fmt.Sprintf("https://rdap.org/domain/%s", url.PathEscape(domain)) } else { - serverURL = serverURL + "domain/" + domain + serverURL = serverURL + "domain/" + url.PathEscape(domain) } client := &http.Client{Timeout: timeout} @@ -663,7 +678,7 @@ func RDAPLookupDomainWithTimeout(domain string, timeout time.Duration) RDAPRespo result.LookupTimeMs = time.Since(start).Milliseconds() return result } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -709,7 +724,7 @@ func RDAPLookupIPWithTimeout(ip string, timeout time.Duration) RDAPResponse { } // Use rdap.org as a universal redirector - serverURL := fmt.Sprintf("https://rdap.org/ip/%s", ip) + serverURL := fmt.Sprintf("https://rdap.org/ip/%s", url.PathEscape(ip)) client := &http.Client{Timeout: timeout} resp, err := client.Get(serverURL) @@ -718,7 +733,7 @@ func RDAPLookupIPWithTimeout(ip string, timeout time.Duration) RDAPResponse { result.LookupTimeMs = time.Since(start).Milliseconds() return result } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { @@ -760,7 +775,7 @@ func RDAPLookupASNWithTimeout(asn string, timeout time.Duration) RDAPResponse { asnNum := strings.TrimPrefix(strings.ToUpper(asn), "AS") // Use rdap.org as a universal redirector - serverURL := fmt.Sprintf("https://rdap.org/autnum/%s", asnNum) + serverURL := fmt.Sprintf("https://rdap.org/autnum/%s", url.PathEscape(asnNum)) client := &http.Client{Timeout: timeout} resp, err := client.Get(serverURL) @@ -769,7 +784,7 @@ func RDAPLookupASNWithTimeout(asn string, timeout time.Duration) RDAPResponse { result.LookupTimeMs = time.Since(start).Milliseconds() return result } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..ab1e835 --- /dev/null +++ b/docs/FAQ.md @@ -0,0 +1,41 @@ +# Frequently Asked Questions (FAQ) + +This page answers common questions about the Poindexter library. + +## General + +### What is Poindexter? + +Poindexter is a Go library that provides a collection of utility functions, including sorting algorithms and a K-D tree implementation for nearest neighbor searches. + +### What is the license? + +Poindexter is licensed under the European Union Public Licence v1.2 (EUPL-1.2). See the [LICENSE](LICENSE) file for more details. + +## K-D Tree + +### What is a K-D tree? + +A K-D tree is a data structure used for organizing points in a k-dimensional space. It is particularly useful for nearest neighbor searches. + +### How do I choose a distance metric? + +The choice of distance metric depends on your specific use case. Here are some general guidelines: + +- **Euclidean (L2):** A good default for most cases. +- **Manhattan (L1):** Useful when movement is restricted to a grid. +- **Chebyshev (L∞):** Useful for cases where the maximum difference between coordinates is the most important factor. + +### Is the K-D tree thread-safe? + +The K-D tree implementation is not safe for concurrent mutations. If you need to use it in a concurrent environment, you should protect it with a mutex or share immutable snapshots for read-mostly workloads. + +## WASM + +### Can I use Poindexter in the browser? + +Yes, Poindexter provides a WebAssembly (WASM) build that allows you to use the K-D tree in browser environments. See the [WASM documentation](wasm.md) for more details. + +### What is the performance of the WASM build? + +The performance of the WASM build is generally good, but it will be slower than the native Go implementation. For performance-critical applications, it is recommended to benchmark your specific use case. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..6396edc --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,39 @@ +# Troubleshooting Guide + +This guide provides solutions to common problems you may encounter when using the Poindexter library. + +## General + +### `go get` command fails + +If you are having trouble installing the library with `go get`, make sure you have a working Go environment and that you have network connectivity. You can check your Go installation by running `go version`. + +## K-D Tree + +### `ErrDimMismatch` error when building a K-D tree + +This error occurs when the points you are using to build the K-D tree have inconsistent dimensions. Make sure that all points have the same number of coordinates. + +### `ErrDuplicateID` error when building a K-D tree + +This error occurs when you are using points with duplicate IDs. If you are using IDs, they must be unique for each point. + +### Nearest neighbor search is slow + +The K-D tree is designed for efficient nearest neighbor searches, but its performance can be affected by the distribution of your data. If you are experiencing slow performance, consider the following: + +- **Data Distribution:** The K-D tree performs best with uniformly distributed data. If your data is highly clustered, the performance may be degraded. +- **Dimensionality:** The performance of the K-D tree can degrade as the number of dimensions increases. For very high-dimensional data, other methods may be more appropriate. + +## WASM + +### WASM module fails to load + +If the WASM module is not loading, make sure that the `.wasm` file is being served correctly and that the path to the file is correct in your HTML. You can check the browser's developer console for any errors related to loading the module. + +### Performance is slow in the browser + +The WASM build will generally be slower than the native Go implementation. If you are experiencing performance issues, consider the following: + +- **Data Size:** The amount of data you are processing can affect performance. Try to minimize the amount of data you are sending to the WASM module. +- **Benchmarking:** Benchmark your specific use case to identify any performance bottlenecks. diff --git a/gonum_bench.txt b/gonum_bench.txt new file mode 100644 index 0000000..28c4e74 --- /dev/null +++ b/gonum_bench.txt @@ -0,0 +1,42 @@ +goos: linux +goarch: amd64 +pkg: github.com/Snider/Poindexter +cpu: Intel(R) Xeon(R) Processor @ 2.30GHz +BenchmarkNearest_Linear_Uniform_100k_2D-4 1384 850417 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_100k_2D-4 825416 1422 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_100k_4D-4 1111 1108445 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_100k_4D-4 159350 16747 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_100k_2D-4 1156 897493 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_100k_2D-4 164542 7957 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_100k_4D-4 1093 1068889 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_100k_4D-4 679 1839479 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_1k_2D-4 140626 8470 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_1k_2D-4 1417580 721.6 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_10k_2D-4 14460 83143 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_10k_2D-4 1372161 864.0 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_1k_4D-4 112540 11183 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_1k_4D-4 352273 3433 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_10k_4D-4 10000 106582 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_10k_4D-4 183109 6641 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_1k_2D-4 140745 11324 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_1k_2D-4 593008 2362 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_10k_2D-4 14334 101665 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_10k_2D-4 258667 4285 ns/op 0 B/op 0 allocs/op +BenchmarkKNN10_Linear_Uniform_10k_2D-4 393 2625727 ns/op 164496 B/op 6 allocs/op +BenchmarkKNN10_Gonum_Uniform_10k_2D-4 172296 6853 ns/op 1384 B/op 12 allocs/op +BenchmarkKNN10_Linear_Clustered_10k_2D-4 454 2595619 ns/op 164496 B/op 6 allocs/op +BenchmarkKNN10_Gonum_Clustered_10k_2D-4 97267 11278 ns/op 1384 B/op 12 allocs/op +BenchmarkRadiusMid_Linear_Uniform_10k_2D-4 236 4931056 ns/op 959204 B/op 123 allocs/op +BenchmarkRadiusMid_Gonum_Uniform_10k_2D-4 223 5436124 ns/op 1025664 B/op 129 allocs/op +BenchmarkRadiusMid_Linear_Clustered_10k_2D-4 199 5687141 ns/op 1232172 B/op 165 allocs/op +BenchmarkRadiusMid_Gonum_Clustered_10k_2D-4 182 6186214 ns/op 1315417 B/op 179 allocs/op +BenchmarkNearest_1k_2D-4 1000000 1070 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_10k_2D-4 1908055 1276 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_1k_4D-4 249206 4226 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_10k_4D-4 185893 5574 ns/op 0 B/op 0 allocs/op +BenchmarkKNearest10_1k_2D-4 192446 5614 ns/op 1384 B/op 12 allocs/op +BenchmarkKNearest10_10k_2D-4 171120 10207 ns/op 1384 B/op 12 allocs/op +BenchmarkRadiusMid_1k_2D-4 2348 526415 ns/op 84118 B/op 18 allocs/op +BenchmarkRadiusMid_10k_2D-4 122 10288116 ns/op 1036096 B/op 218 allocs/op +PASS +ok github.com/Snider/Poindexter 70.252s diff --git a/kdtree.go b/kdtree.go index 657453f..3fa4ccb 100644 --- a/kdtree.go +++ b/kdtree.go @@ -564,8 +564,8 @@ func (t *KDTree[T]) GetTopPeers(n int) []PeerStats { return t.peerAnalytics.GetTopPeers(n) } -// ComputeDistanceDistribution analyzes the distribution of current point coordinates. -func (t *KDTree[T]) ComputeDistanceDistribution(axisNames []string) []AxisDistribution { +// ComputeAxisDistributions analyzes the distribution of current point coordinates. +func (t *KDTree[T]) ComputeAxisDistributions(axisNames []string) []AxisDistribution { return ComputeAxisDistributions(t.points, axisNames) } diff --git a/kdtree_analytics.go b/kdtree_analytics.go index 91a9b12..069cac1 100644 --- a/kdtree_analytics.go +++ b/kdtree_analytics.go @@ -395,197 +395,6 @@ func ComputeAxisDistributions[T any](points []KDPoint[T], axisNames []string) [] return result } -// NATRoutingMetrics provides metrics specifically for NAT traversal routing decisions. -type NATRoutingMetrics struct { - // Connectivity score (0-1): higher means better reachability - ConnectivityScore float64 `json:"connectivityScore"` - // Symmetry score (0-1): higher means more symmetric NAT (easier to traverse) - SymmetryScore float64 `json:"symmetryScore"` - // Relay requirement probability (0-1): likelihood peer needs relay - RelayProbability float64 `json:"relayProbability"` - // Direct connection success rate (historical) - DirectSuccessRate float64 `json:"directSuccessRate"` - // Average RTT in milliseconds - AvgRTTMs float64 `json:"avgRttMs"` - // Jitter (RTT variance) in milliseconds - JitterMs float64 `json:"jitterMs"` - // Packet loss rate (0-1) - PacketLossRate float64 `json:"packetLossRate"` - // Bandwidth estimate in Mbps - BandwidthMbps float64 `json:"bandwidthMbps"` - // NAT type classification - NATType string `json:"natType"` - // Last probe timestamp - LastProbeAt time.Time `json:"lastProbeAt"` -} - -// NATTypeClassification enumerates common NAT types for routing decisions. -type NATTypeClassification string - -const ( - NATTypeOpen NATTypeClassification = "open" // No NAT / Public IP - NATTypeFullCone NATTypeClassification = "full_cone" // Easy to traverse - NATTypeRestrictedCone NATTypeClassification = "restricted_cone" // Moderate difficulty - NATTypePortRestricted NATTypeClassification = "port_restricted" // Harder to traverse - NATTypeSymmetric NATTypeClassification = "symmetric" // Hardest to traverse - NATTypeSymmetricUDP NATTypeClassification = "symmetric_udp" // UDP-only symmetric - NATTypeUnknown NATTypeClassification = "unknown" // Not yet classified - NATTypeBehindCGNAT NATTypeClassification = "cgnat" // Carrier-grade NAT - NATTypeFirewalled NATTypeClassification = "firewalled" // Blocked by firewall - NATTypeRelayRequired NATTypeClassification = "relay_required" // Must use relay -) - -// PeerQualityScore computes a composite quality score for peer selection. -// Higher scores indicate better peers for routing. -// Weights can be customized; default weights emphasize latency and reliability. -func PeerQualityScore(metrics NATRoutingMetrics, weights *QualityWeights) float64 { - w := DefaultQualityWeights() - if weights != nil { - w = *weights - } - - // Normalize metrics to 0-1 scale (higher is better) - latencyScore := 1.0 - math.Min(metrics.AvgRTTMs/1000.0, 1.0) // <1000ms is acceptable - jitterScore := 1.0 - math.Min(metrics.JitterMs/100.0, 1.0) // <100ms jitter - lossScore := 1.0 - metrics.PacketLossRate // 0 loss is best - bandwidthScore := math.Min(metrics.BandwidthMbps/100.0, 1.0) // 100Mbps is excellent - connectivityScore := metrics.ConnectivityScore // Already 0-1 - symmetryScore := metrics.SymmetryScore // Already 0-1 - directScore := metrics.DirectSuccessRate // Already 0-1 - relayPenalty := 1.0 - metrics.RelayProbability // Prefer non-relay - - // NAT type bonus/penalty - natScore := natTypeScore(metrics.NATType) - - // Weighted combination - score := (w.Latency*latencyScore + - w.Jitter*jitterScore + - w.PacketLoss*lossScore + - w.Bandwidth*bandwidthScore + - w.Connectivity*connectivityScore + - w.Symmetry*symmetryScore + - w.DirectSuccess*directScore + - w.RelayPenalty*relayPenalty + - w.NATType*natScore) / w.Total() - - return math.Max(0, math.Min(1, score)) -} - -// QualityWeights configures the importance of each metric in peer selection. -type QualityWeights struct { - Latency float64 `json:"latency"` - Jitter float64 `json:"jitter"` - PacketLoss float64 `json:"packetLoss"` - Bandwidth float64 `json:"bandwidth"` - Connectivity float64 `json:"connectivity"` - Symmetry float64 `json:"symmetry"` - DirectSuccess float64 `json:"directSuccess"` - RelayPenalty float64 `json:"relayPenalty"` - NATType float64 `json:"natType"` -} - -// Total returns the sum of all weights for normalization. -func (w QualityWeights) Total() float64 { - return w.Latency + w.Jitter + w.PacketLoss + w.Bandwidth + - w.Connectivity + w.Symmetry + w.DirectSuccess + w.RelayPenalty + w.NATType -} - -// DefaultQualityWeights returns sensible defaults for peer selection. -func DefaultQualityWeights() QualityWeights { - return QualityWeights{ - Latency: 3.0, // Most important - Jitter: 1.5, - PacketLoss: 2.0, - Bandwidth: 1.0, - Connectivity: 2.0, - Symmetry: 1.0, - DirectSuccess: 2.0, - RelayPenalty: 1.5, - NATType: 1.0, - } -} - -// natTypeScore returns a 0-1 score based on NAT type (higher is better for routing). -func natTypeScore(natType string) float64 { - switch NATTypeClassification(natType) { - case NATTypeOpen: - return 1.0 - case NATTypeFullCone: - return 0.9 - case NATTypeRestrictedCone: - return 0.7 - case NATTypePortRestricted: - return 0.5 - case NATTypeSymmetric: - return 0.3 - case NATTypeSymmetricUDP: - return 0.25 - case NATTypeBehindCGNAT: - return 0.2 - case NATTypeFirewalled: - return 0.1 - case NATTypeRelayRequired: - return 0.05 - default: - return 0.4 // Unknown gets middle score - } -} - -// TrustMetrics tracks trust and reputation for peer selection. -type TrustMetrics struct { - // ReputationScore (0-1): aggregated trust score - ReputationScore float64 `json:"reputationScore"` - // SuccessfulTransactions: count of successful exchanges - SuccessfulTransactions int64 `json:"successfulTransactions"` - // FailedTransactions: count of failed/aborted exchanges - FailedTransactions int64 `json:"failedTransactions"` - // AgeSeconds: how long this peer has been known - AgeSeconds int64 `json:"ageSeconds"` - // LastSuccessAt: last successful interaction - LastSuccessAt time.Time `json:"lastSuccessAt"` - // LastFailureAt: last failed interaction - LastFailureAt time.Time `json:"lastFailureAt"` - // VouchCount: number of other peers vouching for this peer - VouchCount int `json:"vouchCount"` - // FlagCount: number of reports against this peer - FlagCount int `json:"flagCount"` - // ProofOfWork: computational proof of stake/work - ProofOfWork float64 `json:"proofOfWork"` -} - -// ComputeTrustScore calculates a composite trust score from trust metrics. -func ComputeTrustScore(t TrustMetrics) float64 { - total := t.SuccessfulTransactions + t.FailedTransactions - if total == 0 { - // New peer with no history: moderate trust with age bonus - ageBonus := math.Min(float64(t.AgeSeconds)/(86400*30), 0.2) // Up to 0.2 for 30 days - return 0.5 + ageBonus - } - - // Base score from success rate - successRate := float64(t.SuccessfulTransactions) / float64(total) - - // Volume confidence (more transactions = more confident) - volumeConfidence := 1 - 1/(1+float64(total)/10) - - // Vouch/flag adjustment - vouchBonus := math.Min(float64(t.VouchCount)*0.02, 0.15) - flagPenalty := math.Min(float64(t.FlagCount)*0.05, 0.3) - - // Recency bonus (recent success = better) - recencyBonus := 0.0 - if !t.LastSuccessAt.IsZero() { - hoursSince := time.Since(t.LastSuccessAt).Hours() - recencyBonus = 0.1 * math.Exp(-hoursSince/168) // Decays over ~1 week - } - - // Proof of work bonus - powBonus := math.Min(t.ProofOfWork*0.1, 0.1) - - score := successRate*volumeConfidence + vouchBonus - flagPenalty + recencyBonus + powBonus - return math.Max(0, math.Min(1, score)) -} - // NetworkHealthSummary aggregates overall network health metrics. type NetworkHealthSummary struct { TotalPeers int `json:"totalPeers"` @@ -657,6 +466,12 @@ type FeatureRanges struct { Ranges []AxisStats `json:"ranges"` } +// AxisStats holds statistics for a single axis. +type AxisStats struct { + Min float64 `json:"min"` + Max float64 `json:"max"` +} + // DefaultPeerFeatureRanges returns sensible default ranges for peer features. func DefaultPeerFeatureRanges() FeatureRanges { return FeatureRanges{ diff --git a/kdtree_analytics_test.go b/kdtree_analytics_test.go index 0033a09..f2800ad 100644 --- a/kdtree_analytics_test.go +++ b/kdtree_analytics_test.go @@ -618,7 +618,7 @@ func TestKDTreeDistanceDistribution(t *testing.T) { } tree, _ := NewKDTree(points) - dists := tree.ComputeDistanceDistribution([]string{"x", "y"}) + dists := tree.ComputeAxisDistributions([]string{"x", "y"}) if len(dists) != 2 { t.Errorf("expected 2 axis distributions, got %d", len(dists)) } diff --git a/kdtree_helpers.go b/kdtree_helpers.go index 298b000..703e5ba 100644 --- a/kdtree_helpers.go +++ b/kdtree_helpers.go @@ -18,12 +18,6 @@ var ( ErrStatsDimMismatch = errors.New("kdtree: stats dimensionality mismatch") ) -// AxisStats holds the min/max observed for a single axis. -type AxisStats struct { - Min float64 - Max float64 -} - // NormStats holds per-axis normalisation statistics. // For D dimensions, Stats has length D. type NormStats struct { diff --git a/linear_bench.txt b/linear_bench.txt new file mode 100644 index 0000000..380b07b --- /dev/null +++ b/linear_bench.txt @@ -0,0 +1,34 @@ +goos: linux +goarch: amd64 +pkg: github.com/Snider/Poindexter +cpu: Intel(R) Xeon(R) Processor @ 2.30GHz +BenchmarkNearest_Linear_Uniform_1k_2D-4 138124 8534 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_1k_2D-4 133792 8428 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_10k_2D-4 10000 122322 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_10k_2D-4 13287 87229 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_1k_4D-4 119668 10099 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_1k_4D-4 120369 10518 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Uniform_10k_4D-4 12187 95500 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Uniform_10k_4D-4 12282 101452 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_1k_2D-4 141176 8635 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_1k_2D-4 141950 9332 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Linear_Clustered_10k_2D-4 13855 100933 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_Gonum_Clustered_10k_2D-4 10000 104974 ns/op 0 B/op 0 allocs/op +BenchmarkKNN10_Linear_Uniform_10k_2D-4 447 2664549 ns/op 164497 B/op 6 allocs/op +BenchmarkKNN10_Gonum_Uniform_10k_2D-4 448 2678659 ns/op 164496 B/op 6 allocs/op +BenchmarkKNN10_Linear_Clustered_10k_2D-4 451 2655975 ns/op 164496 B/op 6 allocs/op +BenchmarkKNN10_Gonum_Clustered_10k_2D-4 429 2796159 ns/op 164496 B/op 6 allocs/op +BenchmarkRadiusMid_Linear_Uniform_10k_2D-4 205 5708833 ns/op 961263 B/op 138 allocs/op +BenchmarkRadiusMid_Gonum_Uniform_10k_2D-4 196 5334473 ns/op 961862 B/op 143 allocs/op +BenchmarkRadiusMid_Linear_Clustered_10k_2D-4 177 9435880 ns/op 1233949 B/op 182 allocs/op +BenchmarkRadiusMid_Gonum_Clustered_10k_2D-4 163 6559096 ns/op 1235333 B/op 196 allocs/op +BenchmarkNearest_1k_2D-4 116074 8685 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_10k_2D-4 14332 91255 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_1k_4D-4 108560 11050 ns/op 0 B/op 0 allocs/op +BenchmarkNearest_10k_4D-4 10000 112694 ns/op 0 B/op 0 allocs/op +BenchmarkKNearest10_1k_2D-4 4704 253934 ns/op 17032 B/op 6 allocs/op +BenchmarkKNearest10_10k_2D-4 458 2664017 ns/op 164495 B/op 6 allocs/op +BenchmarkRadiusMid_1k_2D-4 3313 336997 ns/op 77568 B/op 16 allocs/op +BenchmarkRadiusMid_10k_2D-4 204 6112449 ns/op 969521 B/op 141 allocs/op +PASS +ok github.com/Snider/Poindexter 47.769s diff --git a/nat_metrics.go b/nat_metrics.go new file mode 100644 index 0000000..8a59142 --- /dev/null +++ b/nat_metrics.go @@ -0,0 +1,142 @@ +package poindexter + +import ( + "math" + "time" +) + +// NATRoutingMetrics provides metrics specifically for NAT traversal routing decisions. +type NATRoutingMetrics struct { + // Connectivity score (0-1): higher means better reachability + ConnectivityScore float64 `json:"connectivityScore"` + // Symmetry score (0-1): higher means more symmetric NAT (easier to traverse) + SymmetryScore float64 `json:"symmetryScore"` + // Relay requirement probability (0-1): likelihood peer needs relay + RelayProbability float64 `json:"relayProbability"` + // Direct connection success rate (historical) + DirectSuccessRate float64 `json:"directSuccessRate"` + // Average RTT in milliseconds + AvgRTTMs float64 `json:"avgRttMs"` + // Jitter (RTT variance) in milliseconds + JitterMs float64 `json:"jitterMs"` + // Packet loss rate (0-1) + PacketLossRate float64 `json:"packetLossRate"` + // Bandwidth estimate in Mbps + BandwidthMbps float64 `json:"bandwidthMbps"` + // NAT type classification + NATType string `json:"natType"` + // Last probe timestamp + LastProbeAt time.Time `json:"lastProbeAt"` +} + +// NATTypeClassification enumerates common NAT types for routing decisions. +type NATTypeClassification string + +const ( + NATTypeOpen NATTypeClassification = "open" // No NAT / Public IP + NATTypeFullCone NATTypeClassification = "full_cone" // Easy to traverse + NATTypeRestrictedCone NATTypeClassification = "restricted_cone" // Moderate difficulty + NATTypePortRestricted NATTypeClassification = "port_restricted" // Harder to traverse + NATTypeSymmetric NATTypeClassification = "symmetric" // Hardest to traverse + NATTypeSymmetricUDP NATTypeClassification = "symmetric_udp" // UDP-only symmetric + NATTypeUnknown NATTypeClassification = "unknown" // Not yet classified + NATTypeBehindCGNAT NATTypeClassification = "cgnat" // Carrier-grade NAT + NATTypeFirewalled NATTypeClassification = "firewalled" // Blocked by firewall + NATTypeRelayRequired NATTypeClassification = "relay_required" // Must use relay +) + +// PeerQualityScore computes a composite quality score for peer selection. +// Higher scores indicate better peers for routing. +// Weights can be customized; default weights emphasize latency and reliability. +func PeerQualityScore(metrics NATRoutingMetrics, weights *QualityWeights) float64 { + w := DefaultQualityWeights() + if weights != nil { + w = *weights + } + + // Normalize metrics to 0-1 scale (higher is better) + latencyScore := 1.0 - math.Min(metrics.AvgRTTMs/1000.0, 1.0) // <1000ms is acceptable + jitterScore := 1.0 - math.Min(metrics.JitterMs/100.0, 1.0) // <100ms jitter + lossScore := 1.0 - metrics.PacketLossRate // 0 loss is best + bandwidthScore := math.Min(metrics.BandwidthMbps/100.0, 1.0) // 100Mbps is excellent + connectivityScore := metrics.ConnectivityScore // Already 0-1 + symmetryScore := metrics.SymmetryScore // Already 0-1 + directScore := metrics.DirectSuccessRate // Already 0-1 + relayPenalty := 1.0 - metrics.RelayProbability // Prefer non-relay + + // NAT type bonus/penalty + natScore := natTypeScore(metrics.NATType) + + // Weighted combination + score := (w.Latency*latencyScore + + w.Jitter*jitterScore + + w.PacketLoss*lossScore + + w.Bandwidth*bandwidthScore + + w.Connectivity*connectivityScore + + w.Symmetry*symmetryScore + + w.DirectSuccess*directScore + + w.RelayPenalty*relayPenalty + + w.NATType*natScore) / w.Total() + + return math.Max(0, math.Min(1, score)) +} + +// QualityWeights configures the importance of each metric in peer selection. +type QualityWeights struct { + Latency float64 `json:"latency"` + Jitter float64 `json:"jitter"` + PacketLoss float64 `json:"packetLoss"` + Bandwidth float64 `json:"bandwidth"` + Connectivity float64 `json:"connectivity"` + Symmetry float64 `json:"symmetry"` + DirectSuccess float64 `json:"directSuccess"` + RelayPenalty float64 `json:"relayPenalty"` + NATType float64 `json:"natType"` +} + +// Total returns the sum of all weights for normalization. +func (w QualityWeights) Total() float64 { + return w.Latency + w.Jitter + w.PacketLoss + w.Bandwidth + + w.Connectivity + w.Symmetry + w.DirectSuccess + w.RelayPenalty + w.NATType +} + +// DefaultQualityWeights returns sensible defaults for peer selection. +func DefaultQualityWeights() QualityWeights { + return QualityWeights{ + Latency: 3.0, // Most important + Jitter: 1.5, + PacketLoss: 2.0, + Bandwidth: 1.0, + Connectivity: 2.0, + Symmetry: 1.0, + DirectSuccess: 2.0, + RelayPenalty: 1.5, + NATType: 1.0, + } +} + +// natTypeScore returns a 0-1 score based on NAT type (higher is better for routing). +func natTypeScore(natType string) float64 { + switch NATTypeClassification(natType) { + case NATTypeOpen: + return 1.0 + case NATTypeFullCone: + return 0.9 + case NATTypeRestrictedCone: + return 0.7 + case NATTypePortRestricted: + return 0.5 + case NATTypeSymmetric: + return 0.3 + case NATTypeSymmetricUDP: + return 0.25 + case NATTypeBehindCGNAT: + return 0.2 + case NATTypeFirewalled: + return 0.1 + case NATTypeRelayRequired: + return 0.05 + default: + return 0.4 // Unknown gets middle score + } +} diff --git a/npm/poindexter-wasm/PROJECT_README.md b/npm/poindexter-wasm/PROJECT_README.md index 634752c..396a316 100644 --- a/npm/poindexter-wasm/PROJECT_README.md +++ b/npm/poindexter-wasm/PROJECT_README.md @@ -71,7 +71,9 @@ Explore runnable examples in the repository: - examples/kdtree_2d_ping_hop - examples/kdtree_3d_ping_hop_geo - examples/kdtree_4d_ping_hop_geo_score +- examples/dht_helpers (convenience wrappers for common DHT schemas) - examples/wasm-browser (browser demo using the ESM loader) +- examples/wasm-browser-ts (TypeScript + Vite local demo) ### KDTree performance and notes - Dual backend support: Linear (always available) and an optimized KD backend enabled when building with `-tags=gonum`. Linear is the default; with the `gonum` tag, the optimized backend becomes the default. @@ -216,4 +218,21 @@ This project is licensed under the European Union Public Licence v1.2 (EUPL-1.2) ## Contributing -Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file +Contributions are welcome! Please feel free to submit a Pull Request. + + +## Coverage + +- CI produces coverage summaries as artifacts on every push/PR: + - Default job: `coverage-summary.md` (from `coverage.out`) + - Gonum-tag job: `coverage-summary-gonum.md` (from `coverage-gonum.out`) +- Locally, you can generate and inspect coverage with the Makefile: + +```bash +make cover # runs tests with race + coverage and prints the total +make coverfunc # prints per-function coverage +make cover-kdtree # filters coverage to kdtree.go +make coverhtml # writes coverage.html for visual inspection +``` + +Note: CI also uploads raw coverage profiles as artifacts (`coverage.out`, `coverage-gonum.out`). diff --git a/npm/poindexter-wasm/loader.js b/npm/poindexter-wasm/loader.js index e8da38d..b78696d 100644 --- a/npm/poindexter-wasm/loader.js +++ b/npm/poindexter-wasm/loader.js @@ -6,6 +6,11 @@ // await tree.insert({ id: 'a', coords: [0,0], value: 'A' }); // const res = await tree.nearest([0.1, 0.2]); +// --- Environment detection --- +const isBrowser = typeof window !== 'undefined'; +const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; + +// --- Browser-specific helpers --- async function loadScriptOnce(src) { return new Promise((resolve, reject) => { // If already present, resolve immediately @@ -18,20 +23,44 @@ async function loadScriptOnce(src) { }); } +// --- Loader logic --- async function ensureWasmExec(url) { - if (typeof window !== 'undefined' && typeof window.Go === 'function') return; - await loadScriptOnce(url); - if (typeof window === 'undefined' || typeof window.Go !== 'function') { - throw new Error('wasm_exec.js did not define window.Go'); + if (typeof globalThis.Go === 'function') return; + + if (isBrowser) { + await loadScriptOnce(url); + } else if (isNode) { + const { fileURLToPath } = await import('url'); + const wasmExecPath = fileURLToPath(url); + await import(wasmExecPath); + } else { + throw new Error(`Unsupported environment: cannot load ${url}`); + } + + if (typeof globalThis.Go !== 'function') { + throw new Error('wasm_exec.js did not define globalThis.Go'); } } function unwrap(result) { - if (!result || typeof result !== 'object') throw new Error('bad result'); - if (result.ok) return result.data; - throw new Error(result.error || 'unknown error'); + if (!result || typeof result !== 'object') { + throw new Error(`bad/unexpected result type from WASM: ${typeof result}`); + } + if (result.ok) { + return result.data; + } + // Handle structured errors, which may be nested + const errorPayload = result.error || result; + if (errorPayload && typeof errorPayload === 'object') { + const err = new Error(errorPayload.message || 'unknown WASM error'); + err.code = errorPayload.code; + throw err; + } + // Fallback for simple string errors + throw new Error(errorPayload || 'unknown WASM error'); } + function call(name, ...args) { const fn = globalThis[name]; if (typeof fn !== 'function') throw new Error(`WASM function ${name} not found`); @@ -65,18 +94,32 @@ export async function init(options = {}) { } = options; await ensureWasmExec(wasmExecURL); - const go = new window.Go(); + const go = new globalThis.Go(); let result; if (instantiateWasm) { - const source = await fetch(wasmURL).then(r => r.arrayBuffer()); + let source; + if (isBrowser) { + source = await fetch(wasmURL).then(r => r.arrayBuffer()); + } else { + const fs = await import('fs/promises'); + const { fileURLToPath } = await import('url'); + source = await fs.readFile(fileURLToPath(wasmURL)); + } const inst = await instantiateWasm(source, go.importObject); result = { instance: inst }; - } else if (WebAssembly.instantiateStreaming) { + } else if (isBrowser && WebAssembly.instantiateStreaming) { result = await WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject); } else { - const resp = await fetch(wasmURL); - const bytes = await resp.arrayBuffer(); + let bytes; + if (isBrowser) { + const resp = await fetch(wasmURL); + bytes = await resp.arrayBuffer(); + } else { + const fs = await import('fs/promises'); + const { fileURLToPath } = await import('url'); + bytes = await fs.readFile(fileURLToPath(wasmURL)); + } result = await WebAssembly.instantiate(bytes, go.importObject); } diff --git a/npm/poindexter-wasm/package-lock.json b/npm/poindexter-wasm/package-lock.json new file mode 100644 index 0000000..d53f67c --- /dev/null +++ b/npm/poindexter-wasm/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "@snider/poindexter-wasm", + "version": "0.0.0-development", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@snider/poindexter-wasm", + "version": "0.0.0-development", + "license": "MIT" + } + } +} diff --git a/npm/poindexter-wasm/smoke.mjs b/npm/poindexter-wasm/smoke.mjs index 925a246..c0a7b8a 100644 --- a/npm/poindexter-wasm/smoke.mjs +++ b/npm/poindexter-wasm/smoke.mjs @@ -4,11 +4,11 @@ import { init } from './loader.js'; (async function () { + let px; try { - const px = await init({ - // In CI, dist/ is placed at repo root via make wasm-build && make npm-pack - wasmURL: new URL('./dist/poindexter.wasm', import.meta.url).pathname, - wasmExecURL: new URL('./dist/wasm_exec.js', import.meta.url).pathname, + px = await init({ + wasmURL: new URL('./dist/poindexter.wasm', import.meta.url).toString(), + wasmExecURL: new URL('./dist/wasm_exec.js', import.meta.url).toString(), }); const ver = await px.version(); if (!ver || typeof ver !== 'string') throw new Error('version not string'); @@ -17,10 +17,24 @@ import { init } from './loader.js'; await tree.insert({ id: 'a', coords: [0, 0], value: 'A' }); await tree.insert({ id: 'b', coords: [1, 0], value: 'B' }); const nn = await tree.nearest([0.9, 0.1]); - if (!nn || !nn.id) throw new Error('nearest failed'); - console.log('WASM smoke ok:', ver, 'nearest.id=', nn.id); + if (!nn || !nn.point || !nn.point.id) throw new Error('nearest failed'); + console.log('WASM smoke ok:', ver, 'nearest.id=', nn.point.id); } catch (err) { console.error('WASM smoke failed:', err); process.exit(1); } + + // Test error handling + try { + await px.newTree(0); + console.error('Expected error from newTree(0) but got none'); + process.exit(1); + } catch (err) { + if (err.code === 'bad_request' && err.message.includes('dimension')) { + console.log('WASM error handling ok:', err.code); + } else { + console.error('WASM smoke failed: unexpected error format:', err); + process.exit(1); + } + } })(); diff --git a/peer_trust.go b/peer_trust.go new file mode 100644 index 0000000..a719456 --- /dev/null +++ b/peer_trust.go @@ -0,0 +1,61 @@ +package poindexter + +import ( + "math" + "time" +) + +// TrustMetrics tracks trust and reputation for peer selection. +type TrustMetrics struct { + // ReputationScore (0-1): aggregated trust score + ReputationScore float64 `json:"reputationScore"` + // SuccessfulTransactions: count of successful exchanges + SuccessfulTransactions int64 `json:"successfulTransactions"` + // FailedTransactions: count of failed/aborted exchanges + FailedTransactions int64 `json:"failedTransactions"` + // AgeSeconds: how long this peer has been known + AgeSeconds int64 `json:"ageSeconds"` + // LastSuccessAt: last successful interaction + LastSuccessAt time.Time `json:"lastSuccessAt"` + // LastFailureAt: last failed interaction + LastFailureAt time.Time `json:"lastFailureAt"` + // VouchCount: number of other peers vouching for this peer + VouchCount int `json:"vouchCount"` + // FlagCount: number of reports against this peer + FlagCount int `json:"flagCount"` + // ProofOfWork: computational proof of stake/work + ProofOfWork float64 `json:"proofOfWork"` +} + +// ComputeTrustScore calculates a composite trust score from trust metrics. +func ComputeTrustScore(t TrustMetrics) float64 { + total := t.SuccessfulTransactions + t.FailedTransactions + if total == 0 { + // New peer with no history: moderate trust with age bonus + ageBonus := math.Min(float64(t.AgeSeconds)/(86400*30), 0.2) // Up to 0.2 for 30 days + return 0.5 + ageBonus + } + + // Base score from success rate + successRate := float64(t.SuccessfulTransactions) / float64(total) + + // Volume confidence (more transactions = more confident) + volumeConfidence := 1 - 1/(1+float64(total)/10) + + // Vouch/flag adjustment + vouchBonus := math.Min(float64(t.VouchCount)*0.02, 0.15) + flagPenalty := math.Min(float64(t.FlagCount)*0.05, 0.3) + + // Recency bonus (recent success = better) + recencyBonus := 0.0 + if !t.LastSuccessAt.IsZero() { + hoursSince := time.Since(t.LastSuccessAt).Hours() + recencyBonus = 0.1 * math.Exp(-hoursSince/168) // Decays over ~1 week + } + + // Proof of work bonus + powBonus := math.Min(t.ProofOfWork*0.1, 0.1) + + score := successRate*volumeConfidence + vouchBonus - flagPenalty + recencyBonus + powBonus + return math.Max(0, math.Min(1, score)) +} diff --git a/wasm/main.go b/wasm/main.go index 373b827..509b49f 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -18,26 +18,62 @@ var ( nextTreeID = 1 ) +// WasmError provides a structured error for WASM responses. +type WasmError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (e *WasmError) Error() string { + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +// Standard error codes. +const ( + ErrCodeBadRequest = "bad_request" + ErrCodeNotFound = "not_found" + ErrCodeConflict = "conflict" +) + +func newErr(code, msg string) *WasmError { + return &WasmError{Code: code, Message: msg} +} + +func newErrf(code, format string, args ...any) *WasmError { + return &WasmError{Code: code, Message: fmt.Sprintf(format, args...)} +} + func export(name string, fn func(this js.Value, args []js.Value) (any, error)) { js.Global().Set(name, js.FuncOf(func(this js.Value, args []js.Value) any { res, err := fn(this, args) if err != nil { - return map[string]any{"ok": false, "error": err.Error()} + var wasmErr *WasmError + if errors.As(err, &wasmErr) { + return map[string]any{ + "ok": false, + "error": map[string]any{"code": wasmErr.Code, "message": wasmErr.Message}, + } + } + // Fallback for generic errors + return map[string]any{ + "ok": false, + "error": map[string]any{"code": ErrCodeBadRequest, "message": err.Error()}, + } } return map[string]any{"ok": true, "data": res} })) } -func getInt(v js.Value, idx int) (int, error) { - if len := v.Length(); len > idx { - return v.Index(idx).Int(), nil +func getInt(args []js.Value, idx int, name string) (int, error) { + if len(args) > idx { + return args[idx].Int(), nil } - return 0, errors.New("missing integer argument") + return 0, newErrf(ErrCodeBadRequest, "missing integer argument: %s", name) } -func getFloatSlice(arg js.Value) ([]float64, error) { +func getFloatSlice(arg js.Value, name string) ([]float64, error) { if arg.IsUndefined() || arg.IsNull() { - return nil, errors.New("coords/query is undefined or null") + return nil, newErrf(ErrCodeBadRequest, "argument is undefined or null: %s", name) } ln := arg.Length() res := make([]float64, ln) @@ -47,6 +83,19 @@ func getFloatSlice(arg js.Value) ([]float64, error) { return res, nil } +// pointToJS converts a KDPoint to a JS-friendly map, ensuring slices are []any. +func pointToJS(p pd.KDPoint[string]) map[string]any { + coords := make([]any, len(p.Coords)) + for i, c := range p.Coords { + coords[i] = c + } + return map[string]any{ + "id": p.ID, + "coords": coords, + "value": p.Value, + } +} + func version(_ js.Value, _ []js.Value) (any, error) { return pd.Version(), nil } @@ -61,15 +110,15 @@ func hello(_ js.Value, args []js.Value) (any, error) { func newTree(_ js.Value, args []js.Value) (any, error) { if len(args) < 1 { - return nil, errors.New("newTree(dim) requires dim") + return nil, newErr(ErrCodeBadRequest, "newTree(dim) requires 'dim' argument") } dim := args[0].Int() if dim <= 0 { - return nil, pd.ErrZeroDim + return nil, newErr(ErrCodeBadRequest, pd.ErrZeroDim.Error()) } t, err := pd.NewKDTreeFromDim[string](dim) if err != nil { - return nil, err + return nil, newErr(ErrCodeBadRequest, err.Error()) } id := nextTreeID nextTreeID++ @@ -78,81 +127,95 @@ func newTree(_ js.Value, args []js.Value) (any, error) { } func treeLen(_ js.Value, args []js.Value) (any, error) { - if len(args) < 1 { - return nil, errors.New("len(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } return t.Len(), nil } func treeDim(_ js.Value, args []js.Value) (any, error) { - if len(args) < 1 { - return nil, errors.New("dim(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } return t.Dim(), nil } func insert(_ js.Value, args []js.Value) (any, error) { // insert(treeId, {id: string, coords: number[], value?: string}) + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } if len(args) < 2 { - return nil, errors.New("insert(treeId, point)") + return nil, newErr(ErrCodeBadRequest, "insert(treeId, point) requires 'point' argument") } - id := args[0].Int() pt := args[1] pid := pt.Get("id").String() - coords, err := getFloatSlice(pt.Get("coords")) + coords, err := getFloatSlice(pt.Get("coords"), "point.coords") if err != nil { return nil, err } val := pt.Get("value").String() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } - okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val}) - return okIns, nil + if okIns := t.Insert(pd.KDPoint[string]{ID: pid, Coords: coords, Value: val}); !okIns { + return nil, newErr(ErrCodeConflict, "failed to insert point: dimension mismatch or duplicate ID") + } + return true, nil } func deleteByID(_ js.Value, args []js.Value) (any, error) { // deleteByID(treeId, id) + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } if len(args) < 2 { - return nil, errors.New("deleteByID(treeId, id)") + return nil, newErr(ErrCodeBadRequest, "deleteByID(treeId, id) requires 'id' argument") } - id := args[0].Int() pid := args[1].String() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) + } + if !t.DeleteByID(pid) { + return nil, newErrf(ErrCodeNotFound, "point with id '%s' not found", pid) } - return t.DeleteByID(pid), nil + return true, nil } func nearest(_ js.Value, args []js.Value) (any, error) { // nearest(treeId, query:number[]) -> {point, dist, found} + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } if len(args) < 2 { - return nil, errors.New("nearest(treeId, query)") + return nil, newErr(ErrCodeBadRequest, "nearest(treeId, query) requires 'query' argument") } - id := args[0].Int() - query, err := getFloatSlice(args[1]) + query, err := getFloatSlice(args[1], "query") if err != nil { return nil, err } t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } p, d, found := t.Nearest(query) out := map[string]any{ - "point": map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value}, + "point": pointToJS(p), "dist": d, "found": found, } @@ -161,65 +224,74 @@ func nearest(_ js.Value, args []js.Value) (any, error) { func kNearest(_ js.Value, args []js.Value) (any, error) { // kNearest(treeId, query:number[], k:int) -> {points:[...], dists:[...]} + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } if len(args) < 3 { - return nil, errors.New("kNearest(treeId, query, k)") + return nil, newErr(ErrCodeBadRequest, "kNearest(treeId, query, k) requires 'query' and 'k' arguments") } - id := args[0].Int() - query, err := getFloatSlice(args[1]) + query, err := getFloatSlice(args[1], "query") + if err != nil { + return nil, err + } + k, err := getInt(args, 2, "k") if err != nil { return nil, err } - k := args[2].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } pts, dists := t.KNearest(query, k) jsPts := make([]any, len(pts)) for i, p := range pts { - jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value} + jsPts[i] = pointToJS(p) } return map[string]any{"points": jsPts, "dists": dists}, nil } func radius(_ js.Value, args []js.Value) (any, error) { // radius(treeId, query:number[], r:number) -> {points:[...], dists:[...]} + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } if len(args) < 3 { - return nil, errors.New("radius(treeId, query, r)") + return nil, newErr(ErrCodeBadRequest, "radius(treeId, query, r) requires 'query' and 'r' arguments") } - id := args[0].Int() - query, err := getFloatSlice(args[1]) + query, err := getFloatSlice(args[1], "query") if err != nil { return nil, err } r := args[2].Float() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } pts, dists := t.Radius(query, r) jsPts := make([]any, len(pts)) for i, p := range pts { - jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value} + jsPts[i] = pointToJS(p) } return map[string]any{"points": jsPts, "dists": dists}, nil } func exportJSON(_ js.Value, args []js.Value) (any, error) { // exportJSON(treeId) -> string (all points) - if len(args) < 1 { - return nil, errors.New("exportJSON(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } // Export all points points := t.Points() jsPts := make([]any, len(points)) for i, p := range points { - jsPts[i] = map[string]any{"id": p.ID, "coords": p.Coords, "value": p.Value} + jsPts[i] = pointToJS(p) } m := map[string]any{ "dim": t.Dim(), @@ -233,13 +305,13 @@ func exportJSON(_ js.Value, args []js.Value) (any, error) { func getAnalytics(_ js.Value, args []js.Value) (any, error) { // getAnalytics(treeId) -> analytics snapshot - if len(args) < 1 { - return nil, errors.New("getAnalytics(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } snap := t.GetAnalyticsSnapshot() return map[string]any{ @@ -259,13 +331,13 @@ func getAnalytics(_ js.Value, args []js.Value) (any, error) { func getPeerStats(_ js.Value, args []js.Value) (any, error) { // getPeerStats(treeId) -> array of peer stats - if len(args) < 1 { - return nil, errors.New("getPeerStats(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } stats := t.GetPeerStats() jsStats := make([]any, len(stats)) @@ -282,14 +354,17 @@ func getPeerStats(_ js.Value, args []js.Value) (any, error) { func getTopPeers(_ js.Value, args []js.Value) (any, error) { // getTopPeers(treeId, n) -> array of top n peer stats - if len(args) < 2 { - return nil, errors.New("getTopPeers(treeId, n)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err + } + n, err := getInt(args, 1, "n") + if err != nil { + return nil, err } - id := args[0].Int() - n := args[1].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } stats := t.GetTopPeers(n) jsStats := make([]any, len(stats)) @@ -306,13 +381,13 @@ func getTopPeers(_ js.Value, args []js.Value) (any, error) { func getAxisDistributions(_ js.Value, args []js.Value) (any, error) { // getAxisDistributions(treeId, axisNames?: string[]) -> array of axis distribution stats - if len(args) < 1 { - return nil, errors.New("getAxisDistributions(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } var axisNames []string @@ -351,13 +426,13 @@ func getAxisDistributions(_ js.Value, args []js.Value) (any, error) { func resetAnalytics(_ js.Value, args []js.Value) (any, error) { // resetAnalytics(treeId) -> resets all analytics - if len(args) < 1 { - return nil, errors.New("resetAnalytics(treeId)") + id, err := getInt(args, 0, "treeId") + if err != nil { + return nil, err } - id := args[0].Int() t, ok := treeRegistry[id] if !ok { - return nil, fmt.Errorf("unknown treeId %d", id) + return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id) } t.ResetAnalytics() return true, nil @@ -366,9 +441,9 @@ func resetAnalytics(_ js.Value, args []js.Value) (any, error) { func computeDistributionStats(_ js.Value, args []js.Value) (any, error) { // computeDistributionStats(distances: number[]) -> distribution stats if len(args) < 1 { - return nil, errors.New("computeDistributionStats(distances)") + return nil, newErr(ErrCodeBadRequest, "computeDistributionStats(distances) requires 'distances' argument") } - distances, err := getFloatSlice(args[0]) + distances, err := getFloatSlice(args[0], "distances") if err != nil { return nil, err } @@ -394,7 +469,7 @@ func computeDistributionStats(_ js.Value, args []js.Value) (any, error) { func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) { // computePeerQualityScore(metrics: NATRoutingMetrics, weights?: QualityWeights) -> score if len(args) < 1 { - return nil, errors.New("computePeerQualityScore(metrics)") + return nil, newErr(ErrCodeBadRequest, "computePeerQualityScore(metrics) requires 'metrics' argument") } m := args[0] metrics := pd.NATRoutingMetrics{ @@ -432,7 +507,7 @@ func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) { func computeTrustScore(_ js.Value, args []js.Value) (any, error) { // computeTrustScore(metrics: TrustMetrics) -> score if len(args) < 1 { - return nil, errors.New("computeTrustScore(metrics)") + return nil, newErr(ErrCodeBadRequest, "computeTrustScore(metrics) requires 'metrics' argument") } m := args[0] metrics := pd.TrustMetrics{ @@ -482,9 +557,9 @@ func getDefaultPeerFeatureRanges(_ js.Value, _ []js.Value) (any, error) { func normalizePeerFeatures(_ js.Value, args []js.Value) (any, error) { // normalizePeerFeatures(features: number[], ranges?: FeatureRanges) -> number[] if len(args) < 1 { - return nil, errors.New("normalizePeerFeatures(features)") + return nil, newErr(ErrCodeBadRequest, "normalizePeerFeatures(features) requires 'features' argument") } - features, err := getFloatSlice(args[0]) + features, err := getFloatSlice(args[0], "features") if err != nil { return nil, err } @@ -512,13 +587,13 @@ func normalizePeerFeatures(_ js.Value, args []js.Value) (any, error) { func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) { // weightedPeerFeatures(normalized: number[], weights: number[]) -> number[] if len(args) < 2 { - return nil, errors.New("weightedPeerFeatures(normalized, weights)") + return nil, newErr(ErrCodeBadRequest, "weightedPeerFeatures(normalized, weights) requires 'normalized' and 'weights' arguments") } - normalized, err := getFloatSlice(args[0]) + normalized, err := getFloatSlice(args[0], "normalized") if err != nil { return nil, err } - weights, err := getFloatSlice(args[1]) + weights, err := getFloatSlice(args[1], "weights") if err != nil { return nil, err } @@ -534,7 +609,7 @@ func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) { func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) { // getExternalToolLinks(domain: string) -> ExternalToolLinks if len(args) < 1 { - return nil, errors.New("getExternalToolLinks(domain)") + return nil, newErr(ErrCodeBadRequest, "getExternalToolLinks(domain) requires 'domain' argument") } domain := args[0].String() links := pd.GetExternalToolLinks(domain) @@ -544,7 +619,7 @@ func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) { func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) { // getExternalToolLinksIP(ip: string) -> ExternalToolLinks if len(args) < 1 { - return nil, errors.New("getExternalToolLinksIP(ip)") + return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksIP(ip) requires 'ip' argument") } ip := args[0].String() links := pd.GetExternalToolLinksIP(ip) @@ -554,7 +629,7 @@ func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) { func getExternalToolLinksEmail(_ js.Value, args []js.Value) (any, error) { // getExternalToolLinksEmail(emailOrDomain: string) -> ExternalToolLinks if len(args) < 1 { - return nil, errors.New("getExternalToolLinksEmail(emailOrDomain)") + return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksEmail(emailOrDomain) requires 'emailOrDomain' argument") } emailOrDomain := args[0].String() links := pd.GetExternalToolLinksEmail(emailOrDomain) @@ -633,7 +708,7 @@ func getRDAPServers(_ js.Value, _ []js.Value) (any, error) { func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) { // buildRDAPDomainURL(domain: string) -> string if len(args) < 1 { - return nil, errors.New("buildRDAPDomainURL(domain)") + return nil, newErr(ErrCodeBadRequest, "buildRDAPDomainURL(domain) requires 'domain' argument") } domain := args[0].String() // Use universal RDAP redirector @@ -643,7 +718,7 @@ func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) { func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) { // buildRDAPIPURL(ip: string) -> string if len(args) < 1 { - return nil, errors.New("buildRDAPIPURL(ip)") + return nil, newErr(ErrCodeBadRequest, "buildRDAPIPURL(ip) requires 'ip' argument") } ip := args[0].String() return fmt.Sprintf("https://rdap.org/ip/%s", ip), nil @@ -652,7 +727,7 @@ func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) { func buildRDAPASNURL(_ js.Value, args []js.Value) (any, error) { // buildRDAPASNURL(asn: string) -> string if len(args) < 1 { - return nil, errors.New("buildRDAPASNURL(asn)") + return nil, newErr(ErrCodeBadRequest, "buildRDAPASNURL(asn) requires 'asn' argument") } asn := args[0].String() // Normalize ASN