This commit introduces a comprehensive set of improvements to the error handling and loading mechanism of the WebAssembly (WASM) module. The key changes include: - **Structured Error Handling:** Replaced generic string-based errors with a structured `WasmError` type in the Go WASM wrapper. This provides standardized error codes (`bad_request`, `not_found`, `conflict`) and clear messages, allowing JavaScript clients to handle errors programmatically. - **Isomorphic WASM Loader:** Refactored the JavaScript loader (`loader.js`) to be isomorphic, enabling it to run seamlessly in both browser and Node.js environments. The loader now detects the environment and uses the appropriate mechanism for loading the WASM binary and `wasm_exec.js`. - **Type Conversion Fix:** Resolved a panic (`panic: ValueOf: invalid value`) that occurred when returning `[]float64` slices from Go to JavaScript. A new `pointToJS` helper function now correctly converts these slices to `[]any`, ensuring proper data marshalling. - **Improved Smoke Test:** Enhanced the WASM smoke test (`smoke.mjs`) to verify the new structured error handling and to correctly handle the API's response format. - **Configuration Updates:** Updated the `.golangci.yml` configuration to be compatible with the latest version of `golangci-lint`. In addition to these changes, this commit also includes a new `AUDIT-ERROR-HANDLING.md` file, which documents the findings of a thorough audit of the project's error handling and logging practices. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
821 lines
24 KiB
Go
821 lines
24 KiB
Go
//go:build js && wasm
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"syscall/js"
|
|
|
|
pd "github.com/Snider/Poindexter"
|
|
)
|
|
|
|
// Simple registry for KDTree instances created from JS.
|
|
// We keep values as string for simplicity across the WASM boundary.
|
|
var (
|
|
treeRegistry = map[int]*pd.KDTree[string]{}
|
|
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 {
|
|
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(args []js.Value, idx int, name string) (int, error) {
|
|
if len(args) > idx {
|
|
return args[idx].Int(), nil
|
|
}
|
|
return 0, newErrf(ErrCodeBadRequest, "missing integer argument: %s", name)
|
|
}
|
|
|
|
func getFloatSlice(arg js.Value, name string) ([]float64, error) {
|
|
if arg.IsUndefined() || arg.IsNull() {
|
|
return nil, newErrf(ErrCodeBadRequest, "argument is undefined or null: %s", name)
|
|
}
|
|
ln := arg.Length()
|
|
res := make([]float64, ln)
|
|
for i := 0; i < ln; i++ {
|
|
res[i] = arg.Index(i).Float()
|
|
}
|
|
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
|
|
}
|
|
|
|
func hello(_ js.Value, args []js.Value) (any, error) {
|
|
name := ""
|
|
if len(args) > 0 {
|
|
name = args[0].String()
|
|
}
|
|
return pd.Hello(name), nil
|
|
}
|
|
|
|
func newTree(_ js.Value, args []js.Value) (any, error) {
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "newTree(dim) requires 'dim' argument")
|
|
}
|
|
dim := args[0].Int()
|
|
if dim <= 0 {
|
|
return nil, newErr(ErrCodeBadRequest, pd.ErrZeroDim.Error())
|
|
}
|
|
t, err := pd.NewKDTreeFromDim[string](dim)
|
|
if err != nil {
|
|
return nil, newErr(ErrCodeBadRequest, err.Error())
|
|
}
|
|
id := nextTreeID
|
|
nextTreeID++
|
|
treeRegistry[id] = t
|
|
return map[string]any{"treeId": id, "dim": dim}, nil
|
|
}
|
|
|
|
func treeLen(_ js.Value, args []js.Value) (any, error) {
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
return t.Len(), nil
|
|
}
|
|
|
|
func treeDim(_ js.Value, args []js.Value) (any, error) {
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
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, newErr(ErrCodeBadRequest, "insert(treeId, point) requires 'point' argument")
|
|
}
|
|
pt := args[1]
|
|
pid := pt.Get("id").String()
|
|
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, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
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, newErr(ErrCodeBadRequest, "deleteByID(treeId, id) requires 'id' argument")
|
|
}
|
|
pid := args[1].String()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
if !t.DeleteByID(pid) {
|
|
return nil, newErrf(ErrCodeNotFound, "point with id '%s' not found", pid)
|
|
}
|
|
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, newErr(ErrCodeBadRequest, "nearest(treeId, query) requires 'query' argument")
|
|
}
|
|
query, err := getFloatSlice(args[1], "query")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
p, d, found := t.Nearest(query)
|
|
out := map[string]any{
|
|
"point": pointToJS(p),
|
|
"dist": d,
|
|
"found": found,
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
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, newErr(ErrCodeBadRequest, "kNearest(treeId, query, k) requires 'query' and 'k' arguments")
|
|
}
|
|
query, err := getFloatSlice(args[1], "query")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k, err := getInt(args, 2, "k")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
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] = 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, newErr(ErrCodeBadRequest, "radius(treeId, query, r) requires 'query' and 'r' arguments")
|
|
}
|
|
query, err := getFloatSlice(args[1], "query")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := args[2].Float()
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
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] = 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)
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
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] = pointToJS(p)
|
|
}
|
|
m := map[string]any{
|
|
"dim": t.Dim(),
|
|
"len": t.Len(),
|
|
"backend": string(t.Backend()),
|
|
"points": jsPts,
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return string(b), nil
|
|
}
|
|
|
|
func getAnalytics(_ js.Value, args []js.Value) (any, error) {
|
|
// getAnalytics(treeId) -> analytics snapshot
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
snap := t.GetAnalyticsSnapshot()
|
|
return map[string]any{
|
|
"queryCount": snap.QueryCount,
|
|
"insertCount": snap.InsertCount,
|
|
"deleteCount": snap.DeleteCount,
|
|
"avgQueryTimeNs": snap.AvgQueryTimeNs,
|
|
"minQueryTimeNs": snap.MinQueryTimeNs,
|
|
"maxQueryTimeNs": snap.MaxQueryTimeNs,
|
|
"lastQueryTimeNs": snap.LastQueryTimeNs,
|
|
"lastQueryAt": snap.LastQueryAt.UnixMilli(),
|
|
"createdAt": snap.CreatedAt.UnixMilli(),
|
|
"backendRebuildCount": snap.BackendRebuildCnt,
|
|
"lastRebuiltAt": snap.LastRebuiltAt.UnixMilli(),
|
|
}, nil
|
|
}
|
|
|
|
func getPeerStats(_ js.Value, args []js.Value) (any, error) {
|
|
// getPeerStats(treeId) -> array of peer stats
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
stats := t.GetPeerStats()
|
|
jsStats := make([]any, len(stats))
|
|
for i, s := range stats {
|
|
jsStats[i] = map[string]any{
|
|
"peerId": s.PeerID,
|
|
"selectionCount": s.SelectionCount,
|
|
"avgDistance": s.AvgDistance,
|
|
"lastSelectedAt": s.LastSelectedAt.UnixMilli(),
|
|
}
|
|
}
|
|
return jsStats, nil
|
|
}
|
|
|
|
func getTopPeers(_ js.Value, args []js.Value) (any, error) {
|
|
// getTopPeers(treeId, n) -> array of top n peer stats
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := getInt(args, 1, "n")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
stats := t.GetTopPeers(n)
|
|
jsStats := make([]any, len(stats))
|
|
for i, s := range stats {
|
|
jsStats[i] = map[string]any{
|
|
"peerId": s.PeerID,
|
|
"selectionCount": s.SelectionCount,
|
|
"avgDistance": s.AvgDistance,
|
|
"lastSelectedAt": s.LastSelectedAt.UnixMilli(),
|
|
}
|
|
}
|
|
return jsStats, nil
|
|
}
|
|
|
|
func getAxisDistributions(_ js.Value, args []js.Value) (any, error) {
|
|
// getAxisDistributions(treeId, axisNames?: string[]) -> array of axis distribution stats
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
|
|
var axisNames []string
|
|
if len(args) > 1 && !args[1].IsUndefined() && !args[1].IsNull() {
|
|
ln := args[1].Length()
|
|
axisNames = make([]string, ln)
|
|
for i := 0; i < ln; i++ {
|
|
axisNames[i] = args[1].Index(i).String()
|
|
}
|
|
}
|
|
|
|
dists := t.ComputeDistanceDistribution(axisNames)
|
|
jsDists := make([]any, len(dists))
|
|
for i, d := range dists {
|
|
jsDists[i] = map[string]any{
|
|
"axis": d.Axis,
|
|
"name": d.Name,
|
|
"stats": map[string]any{
|
|
"count": d.Stats.Count,
|
|
"min": d.Stats.Min,
|
|
"max": d.Stats.Max,
|
|
"mean": d.Stats.Mean,
|
|
"median": d.Stats.Median,
|
|
"stdDev": d.Stats.StdDev,
|
|
"p25": d.Stats.P25,
|
|
"p75": d.Stats.P75,
|
|
"p90": d.Stats.P90,
|
|
"p99": d.Stats.P99,
|
|
"variance": d.Stats.Variance,
|
|
"skewness": d.Stats.Skewness,
|
|
},
|
|
}
|
|
}
|
|
return jsDists, nil
|
|
}
|
|
|
|
func resetAnalytics(_ js.Value, args []js.Value) (any, error) {
|
|
// resetAnalytics(treeId) -> resets all analytics
|
|
id, err := getInt(args, 0, "treeId")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t, ok := treeRegistry[id]
|
|
if !ok {
|
|
return nil, newErrf(ErrCodeNotFound, "unknown treeId %d", id)
|
|
}
|
|
t.ResetAnalytics()
|
|
return true, nil
|
|
}
|
|
|
|
func computeDistributionStats(_ js.Value, args []js.Value) (any, error) {
|
|
// computeDistributionStats(distances: number[]) -> distribution stats
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "computeDistributionStats(distances) requires 'distances' argument")
|
|
}
|
|
distances, err := getFloatSlice(args[0], "distances")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stats := pd.ComputeDistributionStats(distances)
|
|
return map[string]any{
|
|
"count": stats.Count,
|
|
"min": stats.Min,
|
|
"max": stats.Max,
|
|
"mean": stats.Mean,
|
|
"median": stats.Median,
|
|
"stdDev": stats.StdDev,
|
|
"p25": stats.P25,
|
|
"p75": stats.P75,
|
|
"p90": stats.P90,
|
|
"p99": stats.P99,
|
|
"variance": stats.Variance,
|
|
"skewness": stats.Skewness,
|
|
"sampleSize": stats.SampleSize,
|
|
"computedAt": stats.ComputedAt.UnixMilli(),
|
|
}, nil
|
|
}
|
|
|
|
func computePeerQualityScore(_ js.Value, args []js.Value) (any, error) {
|
|
// computePeerQualityScore(metrics: NATRoutingMetrics, weights?: QualityWeights) -> score
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "computePeerQualityScore(metrics) requires 'metrics' argument")
|
|
}
|
|
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(),
|
|
}
|
|
|
|
var weights *pd.QualityWeights
|
|
if len(args) > 1 && !args[1].IsUndefined() && !args[1].IsNull() {
|
|
w := args[1]
|
|
weights = &pd.QualityWeights{
|
|
Latency: w.Get("latency").Float(),
|
|
Jitter: w.Get("jitter").Float(),
|
|
PacketLoss: w.Get("packetLoss").Float(),
|
|
Bandwidth: w.Get("bandwidth").Float(),
|
|
Connectivity: w.Get("connectivity").Float(),
|
|
Symmetry: w.Get("symmetry").Float(),
|
|
DirectSuccess: w.Get("directSuccess").Float(),
|
|
RelayPenalty: w.Get("relayPenalty").Float(),
|
|
NATType: w.Get("natType").Float(),
|
|
}
|
|
}
|
|
|
|
score := pd.PeerQualityScore(metrics, weights)
|
|
return score, nil
|
|
}
|
|
|
|
func computeTrustScore(_ js.Value, args []js.Value) (any, error) {
|
|
// computeTrustScore(metrics: TrustMetrics) -> score
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "computeTrustScore(metrics) requires 'metrics' argument")
|
|
}
|
|
m := args[0]
|
|
metrics := pd.TrustMetrics{
|
|
ReputationScore: m.Get("reputationScore").Float(),
|
|
SuccessfulTransactions: int64(m.Get("successfulTransactions").Int()),
|
|
FailedTransactions: int64(m.Get("failedTransactions").Int()),
|
|
AgeSeconds: int64(m.Get("ageSeconds").Int()),
|
|
VouchCount: m.Get("vouchCount").Int(),
|
|
FlagCount: m.Get("flagCount").Int(),
|
|
ProofOfWork: m.Get("proofOfWork").Float(),
|
|
}
|
|
|
|
score := pd.ComputeTrustScore(metrics)
|
|
return score, nil
|
|
}
|
|
|
|
func getDefaultQualityWeights(_ js.Value, _ []js.Value) (any, error) {
|
|
w := pd.DefaultQualityWeights()
|
|
return map[string]any{
|
|
"latency": w.Latency,
|
|
"jitter": w.Jitter,
|
|
"packetLoss": w.PacketLoss,
|
|
"bandwidth": w.Bandwidth,
|
|
"connectivity": w.Connectivity,
|
|
"symmetry": w.Symmetry,
|
|
"directSuccess": w.DirectSuccess,
|
|
"relayPenalty": w.RelayPenalty,
|
|
"natType": w.NATType,
|
|
}, nil
|
|
}
|
|
|
|
func getDefaultPeerFeatureRanges(_ js.Value, _ []js.Value) (any, error) {
|
|
ranges := pd.DefaultPeerFeatureRanges()
|
|
jsRanges := make([]any, len(ranges.Ranges))
|
|
for i, r := range ranges.Ranges {
|
|
jsRanges[i] = map[string]any{
|
|
"min": r.Min,
|
|
"max": r.Max,
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"ranges": jsRanges,
|
|
"labels": pd.StandardFeatureLabels(),
|
|
}, nil
|
|
}
|
|
|
|
func normalizePeerFeatures(_ js.Value, args []js.Value) (any, error) {
|
|
// normalizePeerFeatures(features: number[], ranges?: FeatureRanges) -> number[]
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "normalizePeerFeatures(features) requires 'features' argument")
|
|
}
|
|
features, err := getFloatSlice(args[0], "features")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ranges := pd.DefaultPeerFeatureRanges()
|
|
if len(args) > 1 && !args[1].IsUndefined() && !args[1].IsNull() {
|
|
rangesArg := args[1].Get("ranges")
|
|
if !rangesArg.IsUndefined() && !rangesArg.IsNull() {
|
|
ln := rangesArg.Length()
|
|
ranges.Ranges = make([]pd.AxisStats, ln)
|
|
for i := 0; i < ln; i++ {
|
|
r := rangesArg.Index(i)
|
|
ranges.Ranges[i] = pd.AxisStats{
|
|
Min: r.Get("min").Float(),
|
|
Max: r.Get("max").Float(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
normalized := pd.NormalizePeerFeatures(features, ranges)
|
|
return normalized, nil
|
|
}
|
|
|
|
func weightedPeerFeatures(_ js.Value, args []js.Value) (any, error) {
|
|
// weightedPeerFeatures(normalized: number[], weights: number[]) -> number[]
|
|
if len(args) < 2 {
|
|
return nil, newErr(ErrCodeBadRequest, "weightedPeerFeatures(normalized, weights) requires 'normalized' and 'weights' arguments")
|
|
}
|
|
normalized, err := getFloatSlice(args[0], "normalized")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
weights, err := getFloatSlice(args[1], "weights")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
weighted := pd.WeightedPeerFeatures(normalized, weights)
|
|
return weighted, nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// DNS Tools Functions
|
|
// ============================================================================
|
|
|
|
func getExternalToolLinks(_ js.Value, args []js.Value) (any, error) {
|
|
// getExternalToolLinks(domain: string) -> ExternalToolLinks
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinks(domain) requires 'domain' argument")
|
|
}
|
|
domain := args[0].String()
|
|
links := pd.GetExternalToolLinks(domain)
|
|
return externalToolLinksToJS(links), nil
|
|
}
|
|
|
|
func getExternalToolLinksIP(_ js.Value, args []js.Value) (any, error) {
|
|
// getExternalToolLinksIP(ip: string) -> ExternalToolLinks
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksIP(ip) requires 'ip' argument")
|
|
}
|
|
ip := args[0].String()
|
|
links := pd.GetExternalToolLinksIP(ip)
|
|
return externalToolLinksToJS(links), nil
|
|
}
|
|
|
|
func getExternalToolLinksEmail(_ js.Value, args []js.Value) (any, error) {
|
|
// getExternalToolLinksEmail(emailOrDomain: string) -> ExternalToolLinks
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "getExternalToolLinksEmail(emailOrDomain) requires 'emailOrDomain' argument")
|
|
}
|
|
emailOrDomain := args[0].String()
|
|
links := pd.GetExternalToolLinksEmail(emailOrDomain)
|
|
return externalToolLinksToJS(links), nil
|
|
}
|
|
|
|
func externalToolLinksToJS(links pd.ExternalToolLinks) map[string]any {
|
|
return map[string]any{
|
|
"target": links.Target,
|
|
"type": links.Type,
|
|
// MXToolbox
|
|
"mxtoolboxDns": links.MXToolboxDNS,
|
|
"mxtoolboxMx": links.MXToolboxMX,
|
|
"mxtoolboxBlacklist": links.MXToolboxBlacklist,
|
|
"mxtoolboxSmtp": links.MXToolboxSMTP,
|
|
"mxtoolboxSpf": links.MXToolboxSPF,
|
|
"mxtoolboxDmarc": links.MXToolboxDMARC,
|
|
"mxtoolboxDkim": links.MXToolboxDKIM,
|
|
"mxtoolboxHttp": links.MXToolboxHTTP,
|
|
"mxtoolboxHttps": links.MXToolboxHTTPS,
|
|
"mxtoolboxPing": links.MXToolboxPing,
|
|
"mxtoolboxTrace": links.MXToolboxTrace,
|
|
"mxtoolboxWhois": links.MXToolboxWhois,
|
|
"mxtoolboxAsn": links.MXToolboxASN,
|
|
// DNSChecker
|
|
"dnscheckerDns": links.DNSCheckerDNS,
|
|
"dnscheckerPropagation": links.DNSCheckerPropagation,
|
|
// Other tools
|
|
"whois": links.WhoIs,
|
|
"viewdns": links.ViewDNS,
|
|
"intodns": links.IntoDNS,
|
|
"dnsviz": links.DNSViz,
|
|
"securitytrails": links.SecurityTrails,
|
|
"shodan": links.Shodan,
|
|
"censys": links.Censys,
|
|
"builtwith": links.BuiltWith,
|
|
"ssllabs": links.SSLLabs,
|
|
"hstsPreload": links.HSTSPreload,
|
|
"hardenize": links.Hardenize,
|
|
// IP-specific
|
|
"ipinfo": links.IPInfo,
|
|
"abuseipdb": links.AbuseIPDB,
|
|
"virustotal": links.VirusTotal,
|
|
"threatcrowd": links.ThreatCrowd,
|
|
// Email-specific
|
|
"mailtester": links.MailTester,
|
|
"learndmarc": links.LearnDMARC,
|
|
}
|
|
}
|
|
|
|
func getRDAPServers(_ js.Value, _ []js.Value) (any, error) {
|
|
// Returns a list of known RDAP servers for reference
|
|
servers := map[string]any{
|
|
"tlds": map[string]string{
|
|
"com": "https://rdap.verisign.com/com/v1/",
|
|
"net": "https://rdap.verisign.com/net/v1/",
|
|
"org": "https://rdap.publicinterestregistry.org/rdap/",
|
|
"info": "https://rdap.afilias.net/rdap/info/",
|
|
"io": "https://rdap.nic.io/",
|
|
"co": "https://rdap.nic.co/",
|
|
"dev": "https://rdap.nic.google/",
|
|
"app": "https://rdap.nic.google/",
|
|
},
|
|
"rirs": map[string]string{
|
|
"arin": "https://rdap.arin.net/registry/",
|
|
"ripe": "https://rdap.db.ripe.net/",
|
|
"apnic": "https://rdap.apnic.net/",
|
|
"afrinic": "https://rdap.afrinic.net/rdap/",
|
|
"lacnic": "https://rdap.lacnic.net/rdap/",
|
|
},
|
|
"universal": "https://rdap.org/",
|
|
}
|
|
return servers, nil
|
|
}
|
|
|
|
func buildRDAPDomainURL(_ js.Value, args []js.Value) (any, error) {
|
|
// buildRDAPDomainURL(domain: string) -> string
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "buildRDAPDomainURL(domain) requires 'domain' argument")
|
|
}
|
|
domain := args[0].String()
|
|
// Use universal RDAP redirector
|
|
return fmt.Sprintf("https://rdap.org/domain/%s", domain), nil
|
|
}
|
|
|
|
func buildRDAPIPURL(_ js.Value, args []js.Value) (any, error) {
|
|
// buildRDAPIPURL(ip: string) -> string
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "buildRDAPIPURL(ip) requires 'ip' argument")
|
|
}
|
|
ip := args[0].String()
|
|
return fmt.Sprintf("https://rdap.org/ip/%s", ip), nil
|
|
}
|
|
|
|
func buildRDAPASNURL(_ js.Value, args []js.Value) (any, error) {
|
|
// buildRDAPASNURL(asn: string) -> string
|
|
if len(args) < 1 {
|
|
return nil, newErr(ErrCodeBadRequest, "buildRDAPASNURL(asn) requires 'asn' argument")
|
|
}
|
|
asn := args[0].String()
|
|
// Normalize ASN
|
|
asnNum := asn
|
|
if len(asn) > 2 && (asn[:2] == "AS" || asn[:2] == "as") {
|
|
asnNum = asn[2:]
|
|
}
|
|
return fmt.Sprintf("https://rdap.org/autnum/%s", asnNum), nil
|
|
}
|
|
|
|
func getDNSRecordTypes(_ js.Value, _ []js.Value) (any, error) {
|
|
// Returns all available DNS record types
|
|
types := pd.GetAllDNSRecordTypes()
|
|
result := make([]string, len(types))
|
|
for i, t := range types {
|
|
result[i] = string(t)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func getDNSRecordTypeInfo(_ js.Value, _ []js.Value) (any, error) {
|
|
// Returns detailed info about all DNS record types
|
|
info := pd.GetDNSRecordTypeInfo()
|
|
result := make([]any, len(info))
|
|
for i, r := range info {
|
|
result[i] = map[string]any{
|
|
"type": string(r.Type),
|
|
"name": r.Name,
|
|
"description": r.Description,
|
|
"rfc": r.RFC,
|
|
"common": r.Common,
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func getCommonDNSRecordTypes(_ js.Value, _ []js.Value) (any, error) {
|
|
// Returns only commonly used DNS record types
|
|
types := pd.GetCommonDNSRecordTypes()
|
|
result := make([]string, len(types))
|
|
for i, t := range types {
|
|
result[i] = string(t)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func main() {
|
|
// Export core API
|
|
export("pxVersion", version)
|
|
export("pxHello", hello)
|
|
export("pxNewTree", newTree)
|
|
export("pxTreeLen", treeLen)
|
|
export("pxTreeDim", treeDim)
|
|
export("pxInsert", insert)
|
|
export("pxDeleteByID", deleteByID)
|
|
export("pxNearest", nearest)
|
|
export("pxKNearest", kNearest)
|
|
export("pxRadius", radius)
|
|
export("pxExportJSON", exportJSON)
|
|
|
|
// Export analytics API
|
|
export("pxGetAnalytics", getAnalytics)
|
|
export("pxGetPeerStats", getPeerStats)
|
|
export("pxGetTopPeers", getTopPeers)
|
|
export("pxGetAxisDistributions", getAxisDistributions)
|
|
export("pxResetAnalytics", resetAnalytics)
|
|
export("pxComputeDistributionStats", computeDistributionStats)
|
|
|
|
// Export NAT routing / peer quality API
|
|
export("pxComputePeerQualityScore", computePeerQualityScore)
|
|
export("pxComputeTrustScore", computeTrustScore)
|
|
export("pxGetDefaultQualityWeights", getDefaultQualityWeights)
|
|
export("pxGetDefaultPeerFeatureRanges", getDefaultPeerFeatureRanges)
|
|
export("pxNormalizePeerFeatures", normalizePeerFeatures)
|
|
export("pxWeightedPeerFeatures", weightedPeerFeatures)
|
|
|
|
// Export DNS tools API
|
|
export("pxGetExternalToolLinks", getExternalToolLinks)
|
|
export("pxGetExternalToolLinksIP", getExternalToolLinksIP)
|
|
export("pxGetExternalToolLinksEmail", getExternalToolLinksEmail)
|
|
export("pxGetRDAPServers", getRDAPServers)
|
|
export("pxBuildRDAPDomainURL", buildRDAPDomainURL)
|
|
export("pxBuildRDAPIPURL", buildRDAPIPURL)
|
|
export("pxBuildRDAPASNURL", buildRDAPASNURL)
|
|
export("pxGetDNSRecordTypes", getDNSRecordTypes)
|
|
export("pxGetDNSRecordTypeInfo", getDNSRecordTypeInfo)
|
|
export("pxGetCommonDNSRecordTypes", getCommonDNSRecordTypes)
|
|
|
|
// Keep running
|
|
select {}
|
|
}
|