2025-11-03 20:15:55 +00:00
|
|
|
// ESM loader for Poindexter WASM
|
|
|
|
|
// Usage:
|
|
|
|
|
// import { init } from '@snider/poindexter-wasm';
|
|
|
|
|
// const px = await init();
|
|
|
|
|
// const tree = await px.newTree(2);
|
|
|
|
|
// await tree.insert({ id: 'a', coords: [0,0], value: 'A' });
|
|
|
|
|
// const res = await tree.nearest([0.1, 0.2]);
|
|
|
|
|
|
2026-02-02 01:27:55 +00:00
|
|
|
// --- Environment detection ---
|
|
|
|
|
const isBrowser = typeof window !== 'undefined';
|
|
|
|
|
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
|
|
|
|
|
|
|
|
// --- Browser-specific helpers ---
|
2025-11-03 20:15:55 +00:00
|
|
|
async function loadScriptOnce(src) {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
// If already present, resolve immediately
|
|
|
|
|
if (document.querySelector(`script[src="${src}"]`)) return resolve();
|
|
|
|
|
const s = document.createElement('script');
|
|
|
|
|
s.src = src;
|
|
|
|
|
s.onload = () => resolve();
|
|
|
|
|
s.onerror = (e) => reject(new Error(`Failed to load ${src}`));
|
|
|
|
|
document.head.appendChild(s);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 01:27:55 +00:00
|
|
|
// --- Loader logic ---
|
2025-11-03 20:15:55 +00:00
|
|
|
async function ensureWasmExec(url) {
|
2026-02-02 01:27:55 +00:00
|
|
|
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');
|
2025-11-03 20:15:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unwrap(result) {
|
2026-02-02 01:27:55 +00:00
|
|
|
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');
|
2025-11-03 20:15:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 01:27:55 +00:00
|
|
|
|
2025-11-03 20:15:55 +00:00
|
|
|
function call(name, ...args) {
|
|
|
|
|
const fn = globalThis[name];
|
|
|
|
|
if (typeof fn !== 'function') throw new Error(`WASM function ${name} not found`);
|
|
|
|
|
return unwrap(fn(...args));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class PxTree {
|
|
|
|
|
constructor(treeId) { this.treeId = treeId; }
|
2025-12-25 12:18:18 +00:00
|
|
|
// Core operations
|
2025-11-03 20:15:55 +00:00
|
|
|
async len() { return call('pxTreeLen', this.treeId); }
|
|
|
|
|
async dim() { return call('pxTreeDim', this.treeId); }
|
|
|
|
|
async insert(point) { return call('pxInsert', this.treeId, point); }
|
|
|
|
|
async deleteByID(id) { return call('pxDeleteByID', this.treeId, id); }
|
|
|
|
|
async nearest(query) { return call('pxNearest', this.treeId, query); }
|
|
|
|
|
async kNearest(query, k) { return call('pxKNearest', this.treeId, query, k); }
|
|
|
|
|
async radius(query, r) { return call('pxRadius', this.treeId, query, r); }
|
|
|
|
|
async exportJSON() { return call('pxExportJSON', this.treeId); }
|
2025-12-25 12:18:18 +00:00
|
|
|
// Analytics operations
|
|
|
|
|
async getAnalytics() { return call('pxGetAnalytics', this.treeId); }
|
|
|
|
|
async getPeerStats() { return call('pxGetPeerStats', this.treeId); }
|
|
|
|
|
async getTopPeers(n) { return call('pxGetTopPeers', this.treeId, n); }
|
|
|
|
|
async getAxisDistributions(axisNames) { return call('pxGetAxisDistributions', this.treeId, axisNames); }
|
|
|
|
|
async resetAnalytics() { return call('pxResetAnalytics', this.treeId); }
|
2025-11-03 20:15:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function init(options = {}) {
|
|
|
|
|
const {
|
|
|
|
|
wasmURL = new URL('./dist/poindexter.wasm', import.meta.url).toString(),
|
|
|
|
|
wasmExecURL = new URL('./dist/wasm_exec.js', import.meta.url).toString(),
|
|
|
|
|
instantiateWasm // optional custom instantiator: (source, importObject) => WebAssembly.Instance
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
await ensureWasmExec(wasmExecURL);
|
2026-02-02 01:27:55 +00:00
|
|
|
const go = new globalThis.Go();
|
2025-11-03 20:15:55 +00:00
|
|
|
|
|
|
|
|
let result;
|
|
|
|
|
if (instantiateWasm) {
|
2026-02-02 01:27:55 +00:00
|
|
|
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));
|
|
|
|
|
}
|
2025-11-03 20:15:55 +00:00
|
|
|
const inst = await instantiateWasm(source, go.importObject);
|
|
|
|
|
result = { instance: inst };
|
2026-02-02 01:27:55 +00:00
|
|
|
} else if (isBrowser && WebAssembly.instantiateStreaming) {
|
2025-11-03 20:15:55 +00:00
|
|
|
result = await WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject);
|
|
|
|
|
} else {
|
2026-02-02 01:27:55 +00:00
|
|
|
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));
|
|
|
|
|
}
|
2025-11-03 20:15:55 +00:00
|
|
|
result = await WebAssembly.instantiate(bytes, go.importObject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run the Go program (it registers globals like pxNewTree, etc.)
|
2025-11-04 00:38:18 +00:00
|
|
|
// Do not await: the Go WASM main may block (e.g., via select{}), so awaiting never resolves.
|
|
|
|
|
go.run(result.instance);
|
2025-11-03 20:15:55 +00:00
|
|
|
|
|
|
|
|
const api = {
|
2025-12-25 12:18:18 +00:00
|
|
|
// Core functions
|
2025-11-03 20:15:55 +00:00
|
|
|
version: async () => call('pxVersion'),
|
|
|
|
|
hello: async (name) => call('pxHello', name ?? ''),
|
|
|
|
|
newTree: async (dim) => {
|
|
|
|
|
const info = call('pxNewTree', dim);
|
|
|
|
|
return new PxTree(info.treeId);
|
2025-12-25 12:18:18 +00:00
|
|
|
},
|
|
|
|
|
// Statistics utilities
|
|
|
|
|
computeDistributionStats: async (distances) => call('pxComputeDistributionStats', distances),
|
|
|
|
|
// NAT routing / peer quality functions
|
|
|
|
|
computePeerQualityScore: async (metrics, weights) => call('pxComputePeerQualityScore', metrics, weights),
|
|
|
|
|
computeTrustScore: async (metrics) => call('pxComputeTrustScore', metrics),
|
|
|
|
|
getDefaultQualityWeights: async () => call('pxGetDefaultQualityWeights'),
|
|
|
|
|
getDefaultPeerFeatureRanges: async () => call('pxGetDefaultPeerFeatureRanges'),
|
|
|
|
|
normalizePeerFeatures: async (features, ranges) => call('pxNormalizePeerFeatures', features, ranges),
|
feat: Add DNS tools with lookup, RDAP, and external tool links
Add comprehensive DNS tools module for network analysis:
DNS Lookup functionality:
- Support for A, AAAA, MX, TXT, NS, CNAME, SOA, PTR, SRV, CAA records
- DNSLookup() and DNSLookupAll() for single/complete lookups
- Configurable timeouts
- Structured result types for all record types
RDAP (new-style WHOIS) support:
- RDAPLookupDomain() for domain registration data
- RDAPLookupIP() for IP address information
- RDAPLookupASN() for autonomous system info
- Built-in server registry for common TLDs and RIRs
- ParseRDAPResponse() for extracting key domain info
External tool link generators:
- GetExternalToolLinks() - 20+ links for domain analysis
- GetExternalToolLinksIP() - IP-specific analysis tools
- GetExternalToolLinksEmail() - Email/domain verification
Tools include: MXToolbox (DNS, MX, SPF, DMARC, DKIM, blacklist),
DNSChecker, ViewDNS, IntoDNS, DNSViz, SecurityTrails, SSL Labs,
Shodan, Censys, IPInfo, AbuseIPDB, VirusTotal, and more.
WASM bindings expose link generators and RDAP URL builders
for use in TypeScript/browser environments.
2025-12-25 12:26:06 +00:00
|
|
|
weightedPeerFeatures: async (normalized, weights) => call('pxWeightedPeerFeatures', normalized, weights),
|
|
|
|
|
// DNS tools
|
|
|
|
|
getExternalToolLinks: async (domain) => call('pxGetExternalToolLinks', domain),
|
|
|
|
|
getExternalToolLinksIP: async (ip) => call('pxGetExternalToolLinksIP', ip),
|
|
|
|
|
getExternalToolLinksEmail: async (emailOrDomain) => call('pxGetExternalToolLinksEmail', emailOrDomain),
|
|
|
|
|
getRDAPServers: async () => call('pxGetRDAPServers'),
|
|
|
|
|
buildRDAPDomainURL: async (domain) => call('pxBuildRDAPDomainURL', domain),
|
|
|
|
|
buildRDAPIPURL: async (ip) => call('pxBuildRDAPIPURL', ip),
|
|
|
|
|
buildRDAPASNURL: async (asn) => call('pxBuildRDAPASNURL', asn),
|
feat: Add extended DNS record types (ClouDNS compatible)
- Add support for 13 additional record types: ALIAS, RP, SSHFP, TLSA,
DS, DNSKEY, NAPTR, LOC, HINFO, CERT, SMIMEA, WR (Web Redirect), SPF
- Add GetDNSRecordTypeInfo() for metadata with RFC references
- Add GetCommonDNSRecordTypes() for commonly used types
- Add structured types for CAA, SSHFP, TLSA, DS, DNSKEY, NAPTR, RP,
LOC, ALIAS, and WebRedirect records
- Export new functions in WASM bindings
- Update TypeScript definitions and loader.js
- Add comprehensive tests for new record types
2025-12-25 12:38:32 +00:00
|
|
|
getDNSRecordTypes: async () => call('pxGetDNSRecordTypes'),
|
|
|
|
|
getDNSRecordTypeInfo: async () => call('pxGetDNSRecordTypeInfo'),
|
|
|
|
|
getCommonDNSRecordTypes: async () => call('pxGetCommonDNSRecordTypes')
|
2025-11-03 20:15:55 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return api;
|
|
|
|
|
}
|