/** * SDN Node - Main P2P node implementation for browsers */ import type { StoredRecord, QueryFilter } from "./storage"; import { type EnginePublishedShardIngestOptions, type EngineSyncChunkIngestOptions, type EngineSyncIngestResult, type SnapshotPersistence } from "./engine-record-store"; import { type FlatSqlSchemaSyncOptions, type FlatSqlSchemaSyncSummary } from "./datasync-session"; import type { EngineEpochQueryProfilesConfig, EngineEpochQueryRequest } from "./epoch-query-sql"; import type { LocalFlatSqlSchema } from "./local-flatsql"; import { EdgeDiscovery } from "./edge-discovery"; import { type SdnConnectionMonitorConfig } from "./connection-monitor-policy"; import { type DiscoveredSDNAdvertisementPeer, type FindSDNAdvertisementPeersOptions, type ProvideSDNAdvertisementFlagOptions } from "./sdn-advertisement-discovery"; import { SchemaName } from "./schemas"; import type { DerivedIdentity } from "./crypto/types"; import { type DiscoveredProvider, type ModuleDeliveryEvent, type ModuleGrantRequestOptions, type ModuleGrantResult, type EncryptedModuleBundleResult } from "./module-delivery"; import { type FlatSqlPublishedShard, type FlatSqlSyncChunk, type FlatSqlSyncManifest, type FlatSqlSyncQuery } from "./flatsql-sync"; export declare const LEGACY_ID_EXCHANGE_PROTOCOL = "/space-data-network/id-exchange/1.0.0"; export declare const IPFS_BOOTSTRAP_PEERS: readonly ["/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", "/dnsaddr/bootstrap.libp2p.io/p2p/QmZa1sAxajnQjVM8WjWXoMbmPd7NsWhfKsPkErzpm9wGkp", "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt"]; /** * The Kad-DHT is OPT-IN, and it is opt-in because leaving it on WEDGES a * browser renderer permanently. * * MEASURED 2026-08-07 (trace, not inference — `disabled-by-default-v8.cpu_profiler` * over a wedged renderer, which keeps sampling because the browser process * records it): * * - `rf-emitter-coverage-sdn` under a `/private//` link-key page and * the LIVE `spaceaware.io/beta` both stop answering ~11.5-12.4 s after * load, permanently: `evaluate(() => Date.now())` times out in the shell * AND the bucket frame, screenshots time out, tile fetches stop mid-stream. * - the renderer main thread's samples at that point are ~1/3 each in * `QueryManager.run` (`@libp2p/kad-dht`), `mergeSources` (`it-merge`) and * `EventTarget.dispatchEvent`, with idle collapsing from ~93% to ~3%. * - removing ONLY `services.dht` from this config (byte-level A/B on one box, * same build, same page) leaves both surfaces interactive for the full * 60 s / 50 s window. * * WHY it never recovers: a browser DHT client walks the query frontier without * ever awaiting real I/O — every candidate peer returned by a server's routing * table is rejected by `connectionManager.isDialable()` (a browser cannot dial * tcp/quic), so the expansion runs entirely in MICROTASKS. A microtask loop * starves the task queue, which means timers never fire, which means the DHT's * own `DEFAULT_QUERY_TIMEOUT` / `AbortSignal.timeout` NEVER fires either. The * stall is unbounded by construction: no timeout inside a starved event loop * can bound it. The only bound is not starting the service. * * The DHT is needed by exactly two APIs — `discoverProviders()` and * `discoverSDNAdvertisementPeers()` — both of which are fallbacks. Module * delivery dials descriptor relays directly, `fetchCIDBytes()` goes over the * IPFS HTTP API/gateway, and pubsub rides the configured relays: none of them * touch content routing. So the default costs nothing and the failure mode it * removes is total. * * The default is the SAME in every runtime (browser, Node, WasmEdge). This is * deliberate: a runtime-sniffing default would fork behaviour across the * isomorphism boundary, and a caller that wants content routing should say so * once, everywhere. */ export declare function dhtEnabled(config: Pick): boolean; /** * The one message every DHT-dependent API raises when the service is off, so a * missing capability is a bounded, legible failure instead of an empty result * that reads like "no providers exist". */ export declare function dhtRequiredError(api: string): Error; export interface SDNConfig { edgeRelays?: string[]; bootstrapPeers?: string[]; includeIPFSBootstrap?: boolean; /** Enable libp2p Identify service. Disabled by default for lean browser module delivery. */ enableIdentify?: boolean; /** * Enable the libp2p Kad-DHT (content routing). OFF unless explicitly `true` * — see {@link dhtEnabled} for the trace that made it opt-in. Only * `discoverProviders()` and `discoverSDNAdvertisementPeers()` need it; every * other lane (module delivery over descriptor relays, CID fetch over the * IPFS HTTP API/gateway, pubsub over configured relays) works without it. * NEVER enable it on a renderer main thread. */ enableDHT?: boolean; /** Let libp2p auto-dial discovered peers to satisfy minConnections. */ enableAutoDial?: boolean; ipfsApiBaseUrl?: string; ipfsGatewayBaseUrl?: string; ipfsFetchTimeoutMs?: number; idExchangeProtocol?: string; enableStorage?: boolean; storeName?: string; /** * Record store backend. 'flatsql' (default) is THE node store: the * FlatSQL-WASM engine record store (loop D.1 — same engine, SQL, and * source-partition layout as sdn-server), journal-persisted to IndexedDB * under storeName by default or through storagePersistence when given; * or pass a ready store instance. The legacy 'indexeddb' SDNStorage * backend was REMOVED in loop D.5 — passing it now throws. */ storageBackend?: 'flatsql' | NodeRecordStorage; /** Envelope journal persistence for the engine store (e.g. HeliaSnapshotPersistence). */ storagePersistence?: SnapshotPersistence; /** * SDS standards mirrored into per-standard engine databases * (per-provider `registerSource` shadow tables + unified views, mirroring * the server layout). Optional: the envelope store works without them. */ storageSchemas?: LocalFlatSqlSchema[]; /** * Per-standard default query profiles for the engine store (loop D.2) — * the retrieval-module config shape, keyed by schema name (`OMM.fbs`): * e.g. `{ "OMM.fbs": { profile: "nearest", limit: 50000 } }`. Request * fields override; unset fields fall back to `nearest` / epoch=now / * limit 50000. */ storageQueryProfiles?: EngineEpochQueryProfilesConfig; /** Private key for auth challenge signing (32 bytes Ed25519 seed) */ privateKey?: Uint8Array; /** Full HD wallet-derived identity (secp256k1 for PeerID + Ed25519 for auth) */ identity?: DerivedIdentity; /** Enable relay load probing for load balancing (default: true) */ enableRelayProbing?: boolean; /** Interval between relay probes in ms (default: 30000) */ relayProbeIntervalMs?: number; /** * libp2p connection-monitor policy. Omit for the sdn-js default (heartbeat * on, 30 s cadence, 30 s deadline), `false` to switch the heartbeat off, or * an object to override single fields. * * NOT a cosmetic knob: libp2p's own default aborts a connection — and every * stream on it — when one heartbeat misses a FIXED 2000 ms deadline, which a * browser main thread routinely misses while a globe starts. See * `connection-monitor-policy.ts` for the measurement. */ connectionMonitor?: SdnConnectionMonitorConfig; } export interface SDNNodeEvents { onMessage?: (schema: SchemaName, data: unknown, from: string) => void; onPeerConnected?: (peerId: string) => void; onPeerDisconnected?: (peerId: string) => void; onModuleDeliveryEvent?: (event: ModuleDeliveryEvent) => void; } /** The record-store surface SDNNode drives (the FlatSQL-WASM engine record store). */ export interface NodeRecordStorage { store(schema: SchemaName | string, data: Uint8Array, peerId: string, signature: Uint8Array): Promise; get(schema: SchemaName | string, cid: string): Promise; query(schema: SchemaName | string, filter?: QueryFilter): Promise; close(): Promise; } export declare class SDNNode { private libp2p; private storage; private config; private events; private subscriptions; private privateKey; private cryptoReady; private discovery; private constructor(); /** * Create and start a new SDN node */ static create(config?: SDNConfig, events?: SDNNodeEvents): Promise; private init; /** * Get the node's peer ID */ get peerId(): string; /** * Get list of connected peers */ get peers(): string[]; /** * Publish data to a schema topic */ /** * Publish raw binary payloads (e.g. signed PNM/SDS FlatBuffers) to a * schema topic — publish() JSON-wraps objects; FlatBuffer envelopes must * go out verbatim. */ publishRaw(schema: SchemaName | string, data: Uint8Array): Promise; publish(schema: SchemaName, data: object): Promise; /** * Set the private key for auth challenge signing */ setPrivateKey(key: Uint8Array): void; /** * Check if auth challenge signing is available */ get canSign(): boolean; /** * Subscribe to a schema topic */ subscribe(schema: SchemaName, handler?: (data: unknown, from: string) => void): Promise; /** * Subscribe to the raw bytes of a schema topic. * * The counterpart to publishRaw(): subscribe() JSON.parses every payload and * drops anything that is not JSON, but live SDS traffic (PNM announcements on * /spacedatanetwork/sds/PNM.fbs, module-delivery envelopes) is binary * size-prefixed FlatBuffers, so the JSON path silently discards 100% of it. * * This is a connector, not a codec: the payload is handed to the caller * VERBATIM — no decode, no schema validation, and no implicit local storage * (exactly as publishRaw performs no implicit store). Decoding is the * caller's job, which keeps FlatBuffer/IDL knowledge out of the host lib. * * Raw and JSON subscriptions are tracked independently, so a topic may carry * both at once without either cancelling the other. */ subscribeRaw(schema: SchemaName | string, handler: (bytes: Uint8Array, from: string) => void): Promise; /** * Cancel a raw subscription without disturbing a JSON subscribe() on the * same topic (and vice versa): the gossipsub topic is only left once no * subscription of either kind remains. */ unsubscribeRaw(schema: SchemaName | string): Promise; /** * Unsubscribe from a schema topic */ unsubscribe(schema: SchemaName): Promise; /** * Query local storage for records */ query(schema: SchemaName, filter?: { peerId?: string; since?: Date; }): Promise; /** * Get a specific record by CID */ get(schema: SchemaName, cid: string): Promise; /** * PRIMARY data query path (loop D.2): engine-native epoch profile * (`nearest` default / `as_of` / `forward`) over the node store's unified * per-standard view — the server's retrieval profiles with byte-identical * SQL/params — returning the ALIGNED size-prefixed FlatBuffer stream. * Requires the default `flatsql` storage backend with `storageSchemas`. */ queryEpochRawStream(standardId: string, request?: EngineEpochQueryRequest): Uint8Array; /** * Connect to a specific peer */ dial(addr: string): Promise; /** * Dial through a relay to reach a peer behind a firewall */ dialThroughRelay(relayAddr: string, targetPeerId: string): Promise; /** * Dial a specific protocol through a relay circuit and return the first reply chunk. */ dialProtocol(targetPeerId: string, protocolId: string, payload: Uint8Array, candidateAddrs?: string[]): Promise; readFlatSqlSyncChunk(query: FlatSqlSyncQuery): Promise; openFlatSqlSyncManifest(query: FlatSqlSyncQuery): Promise; readFlatSqlPublishedShard(query: FlatSqlSyncQuery & { cid: string; }): Promise; /** * Datasync-fed store (loop D.4): materialize a flatsql-sync chunk into * THE engine record store with its true provider/source/batch provenance * (per-provider engine source partitions + envelope index rows). */ ingestFlatSqlSyncChunk(chunk: FlatSqlSyncChunk, options?: EngineSyncChunkIngestOptions): Promise; /** Datasync-fed store (loop D.4): materialize a published shard with pin-ledger provenance. */ ingestFlatSqlPublishedShard(shard: FlatSqlPublishedShard, options?: EnginePublishedShardIngestOptions): Promise; /** * Bounded datasync session (loop D.4): walk a peer's flatsql-sync cursor * chain (`read_chunk` over the server's rowid-snapshot cursor — the * sdn_record_index rowid space) and materialize every chunk into the * engine store with true provenance. Stops when the peer reports no next * cursor, a chunk comes back empty, or `maxChunks` is reached. */ syncFlatSqlSchema(query: FlatSqlSyncQuery, options?: FlatSqlSchemaSyncOptions): Promise; private requireEngineStore; dialProtocolThroughRelay(relayAddr: string, targetPeerId: string, protocolId: string, payload: Uint8Array | string): Promise; /** * Compatibility helper for the historical id-exchange relay probe script. */ idExchangeThroughRelay(relayAddr: string, targetPeerId: string, message?: string): Promise; discoverProviders(discoveryCID: string): Promise; /** * Discover Go sdn-server peers (and other SDN-flagged browser nodes) on * the public IPFS/Amino DHT by the shared SDN membership rendezvous flag * (loop A3 — see sdn-advertisement-discovery.ts for the exact CID * derivation matched against sdn-server/internal/node/ * advertisement_discovery.go and go-libp2p's routing-discovery). By * default this dials any newly discovered peer through the same libp2p * connection path used by `dial()`. */ discoverSDNAdvertisementPeers(options?: Omit): Promise; /** * Announce this browser node as an SDN member on the public DHT under the * shared rendezvous flag, so Go sdn-server peers (and other browser * nodes) can discover it via `discoverSDNAdvertisementPeers`. Optional: * most browser nodes are content consumers and should not call this. See * sdn-advertisement-discovery.ts for re-announcement/TTL guidance. */ provideSDNAdvertisementFlag(options?: Omit): Promise; fetchCIDBytes(cid: string): Promise; requestModuleGrant(options: Omit & { requesterIdentity?: ModuleGrantRequestOptions["requesterIdentity"]; }): Promise; requestEncryptedModuleBundle(options: Omit & { requesterIdentity?: ModuleGrantRequestOptions["requesterIdentity"]; }): Promise; /** * Stop the node */ stop(): Promise; /** * Get the EdgeDiscovery instance for advanced relay management. */ getDiscovery(): EdgeDiscovery | null; /** * Get supported schemas */ static get schemas(): readonly SchemaName[]; static get ipfsBootstrapPeers(): readonly string[]; static get moduleDeliveryProtocolId(): string; } //# sourceMappingURL=node.d.ts.map