{"_id":"@ai2070/net-sdk","_rev":"10-f95aba3695a3648e2a5c24f3989ca5af","name":"@ai2070/net-sdk","dist-tags":{"latest":"0.18.0"},"versions":{"0.9.0":{"name":"@ai2070/net-sdk","version":"0.9.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.9.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"d56566db4fb049aece3a35a2eb13a4214978168f","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.9.0.tgz","fileCount":30,"integrity":"sha512-GM9t38wm799MTbYSkUNYVOrsPejxpo9U14YFx9S5AX+zrBOGLM00v7RYeit4B6douM6HjMmz4q0ROLCAtTDKtA==","signatures":[{"sig":"MEUCIQCoJaLdHN7r2/4GPhnv1TG62pYwsENJQ9EgMXVum2RHAgIgGMJHKxWxWXFUgkioFnU6uI4RYpzb7q8Elbu5RMFRqRM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":243033},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"e081b34a7d5d7af137a7428b9cea0f10ebff4ed4","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.9.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.9.0_1777736669820_0.9933669713236839","host":"s3://npm-registry-packages-npm-production"}},"0.10.0":{"name":"@ai2070/net-sdk","version":"0.10.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.10.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"a3a2f4d360aa116d5abc1fa188f70bdb379b7d1a","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.10.0.tgz","fileCount":30,"integrity":"sha512-G2897yIVEvY2lNpQTwOytVVJ+VAa3w9+c6Wm4RixclFvFeoFCI2WMjiD5Lcw3zTmPPxjeq5LTZnDKP+iNVsWdA==","signatures":[{"sig":"MEQCICHA1uruEBSSKbSg97/iv5aO04Gw9puxVQUR+eUixwOoAiBWWEN+3plppE58NCmrAfkI7u5164c7rrpnH6aF3NujZw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":243035},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"eb447307c69d65d1ced0741fc20f1e71f0228784","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.10.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.10.0_1777842631159_0.9603218295237124","host":"s3://npm-registry-packages-npm-production"}},"0.11.0":{"name":"@ai2070/net-sdk","version":"0.11.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.11.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"0b047daab72871cdb34d4516ab3e8dd6b6c5fae8","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.11.0.tgz","fileCount":30,"integrity":"sha512-4UtDu3cGS/5LwJBzmPID/+OhFPVkmXpNga/fwoMHHqDIZQr1t2eQs0U7XilfkkRNO1hwSYdlKOkqr7NrIK4b/A==","signatures":[{"sig":"MEUCIC3CczA3D1rcueEXCaSQ+HFY0vtOZA3Vi7CLNMXe9emyAiEAvZD4vU+YwR4rKTaaVC67pmjSlKXUBDGsXehrdFM7NiU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":243035},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"f99272593aa1a1bb89e52f312cb5431a8276145d","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.11.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.11.0_1777944249498_0.3902996573192248","host":"s3://npm-registry-packages-npm-production"}},"0.12.0":{"name":"@ai2070/net-sdk","version":"0.12.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.12.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"b7e7f19ad4632ba4f4f4a1e72fe3fce4a9c40238","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.12.0.tgz","fileCount":30,"integrity":"sha512-pNk1ZgWWEMth9xbTeU6Hr8vWEofcX92GiwK65ZB9Jbd/ZlKN/FJrFkFhEBhhbFW3OA1e4/MQNf1oXKrj2Y46CA==","signatures":[{"sig":"MEQCIE2v9kbo76L1d++ikj53thp19GexpGZLPbc2aC12tXGkAiA+9hCpOjATzpJShHehpluTEKDe8hL71u8N78u+U/5tJw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":250996},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"898f4f9645e9b528f03358020152db5c21fc5e96","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.12.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.12.0_1778027143825_0.9649545288180261","host":"s3://npm-registry-packages-npm-production"}},"0.13.0":{"name":"@ai2070/net-sdk","version":"0.13.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.13.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"99bf2ecea6c804aaf7794adb36368b2e944a5bc9","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.13.0.tgz","fileCount":34,"integrity":"sha512-yxV9aZF9XkFUA1K6xXxV41Kh70CKJIB6/kX3TdmQdNdhI70tA/VFo/Qf5kLFGKyYO4f2Gs5RDDovFLPW+dR3sQ==","signatures":[{"sig":"MEQCIC1lScB5tX6gKAwd87FJLC8fwvElF/EiSnoj6VLUvM9OAiAWMuE001c/L9YFX9yPIPRI2HM5DsyQbeG1rqHijjpitg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":347428},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"15fb6c9f872d07e480c871e0a79d3728404afd8e","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.13.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.13.0_1778454102256_0.16274760620162065","host":"s3://npm-registry-packages-npm-production"}},"0.14.0":{"name":"@ai2070/net-sdk","version":"0.14.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.14.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"3cb7bb59c9a6fe17605905c4752eae7174e88f71","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.14.0.tgz","fileCount":34,"integrity":"sha512-ajflYSI3tN1jFqiTta5RKNFJ1oz0AzY+8QGk7cMex4NXBtg7ortigC4kaz5Al8ckuV54DdQ20MhGAuZbOdslag==","signatures":[{"sig":"MEYCIQCs8HgcVp765D6j8tpzoLvpqdKgfd/Ogjm7GdY+/yHNSAIhAM6pKxRJkWftyH2MNt7gt+D/WBTWRv57yv5WDqfp94Nm","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":350047},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"280c931ba1e1bdc2c67d656bacd9709a4a8e2759","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.14.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.14.0_1778511665437_0.07269489059554113","host":"s3://npm-registry-packages-npm-production"}},"0.15.0":{"name":"@ai2070/net-sdk","version":"0.15.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.15.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"766131890465592076bcd2da0580e800a23cba89","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.15.0.tgz","fileCount":34,"integrity":"sha512-0oI2Ez8JcvWsTNxHHHMc4VspofS/0sg7olrCxljWjk+PbM2+ByYYXb7neWPiEa669hbJHYYKd35qrPazaehXJQ==","signatures":[{"sig":"MEQCIDWXLScAYncwWazAj9U0GECJeXCrtbyR2oYKq0S85kDTAiBwBb9FY4HMvQDHxiL1nM9IfrMTAlZoQSZfuYs7kcvFHQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":356706},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"a6c444fe0305c9e96b8f85091f8f825a7b23fd6b","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.15.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.15.0_1778612071011_0.5159401692553229","host":"s3://npm-registry-packages-npm-production"}},"0.16.0":{"name":"@ai2070/net-sdk","version":"0.16.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.16.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"a86ad8c926d200c906e8ce9ee989b2a58c7cb456","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.16.0.tgz","fileCount":34,"integrity":"sha512-qHKrTIuNU5gtmjGCdinV/nY1kmxSuzf3V2GemUgUj8jXQQOQV6StxE/9hntfTPyKTVzA+fvR7HH0q4LXixCGeQ==","signatures":[{"sig":"MEUCIQCnmgiaAaS+tTMff7Vc8xXU7wm5EULaUqGhil6K5RaHvAIgbu5GEE5y3nZ+aiQWeLPwcxbxqT9Af8Ox4zxeq/d0HCU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":362052},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"949b27433c5e5d030a383c3dc3d60f2249d2a0eb","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.16.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.16.0_1778695203874_0.024556897138130473","host":"s3://npm-registry-packages-npm-production"}},"0.17.0":{"name":"@ai2070/net-sdk","version":"0.17.0","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"license":"Apache-2.0","_id":"@ai2070/net-sdk@0.17.0","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"homepage":"https://github.com/ai-2070/net#readme","bugs":{"url":"https://github.com/ai-2070/net/issues"},"dist":{"shasum":"ab2fe1185c0ae1806ef2c19a65fc0167e10dc1fc","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.17.0.tgz","fileCount":34,"integrity":"sha512-/O8C0YrtaTKT8ZzYBoiWuxl+fgvoA4hNR2rPLSxmi2XYvqmtCX+uHoltSSZ0SOduy9OZYtjHG45/XpZWDg0SKA==","signatures":[{"sig":"MEUCIQD00npAK6l46oMpveyXUCIUKRkTd9t5UfIsk9PWQnQ0agIgYAnU5GnaV6KJkG3nLKj4iqDMyXOhFBRJoP8C7T1nlUo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":362052},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=20"},"gitHead":"f1ea882b0cc4368f52a929f72e97cd2a830133c1","scripts":{"test":"vitest run","build":"tsc","test:watch":"vitest"},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"repository":{"url":"git+https://github.com/ai-2070/net.git","type":"git"},"_npmVersion":"11.11.0","description":"Ergonomic TypeScript SDK for the Net mesh network","directories":{},"_nodeVersion":"24.14.1","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.0","typescript":"5.9.3","@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0"},"peerDependencies":{"@ai2070/net":">=0.17.0"},"_npmOperationalInternal":{"tmp":"tmp/net-sdk_0.17.0_1778714131664_0.8312685886728286","host":"s3://npm-registry-packages-npm-production"}},"0.18.0":{"name":"@ai2070/net-sdk","version":"0.18.0","description":"Ergonomic TypeScript SDK for the Net mesh network","main":"dist/index.js","types":"dist/index.d.ts","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/ai-2070/net.git"},"keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"scripts":{"prebuild":"node -e \"const fs=require('fs'),cp=require('child_process');if(!fs.existsSync('../bindings/node/index.d.ts')){cp.execSync('npm --prefix ../bindings/node i --ignore-scripts',{stdio:'inherit'});cp.execSync('npm --prefix ../bindings/node run build',{stdio:'inherit'})}\"","build":"tsc","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"@ai2070/net":">=0.18.0"},"devDependencies":{"@ai2070/net":"file:../bindings/node","@types/node":"^24.0.0","typescript":"5.9.3","vitest":"^4.0.0"},"engines":{"node":">=20"},"gitHead":"ed6ea11d0624ccb1c186f6ae47a90e99d42d3dc9","_id":"@ai2070/net-sdk@0.18.0","bugs":{"url":"https://github.com/ai-2070/net/issues"},"homepage":"https://github.com/ai-2070/net#readme","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-vgfYHUgI6zYMD6j7OmVZM1LOgOV2lAxk3xSoEuT1w/A6R8DwCBOG9BXURR2yUHjjnZprMzXT4468OivuERT+Yw==","shasum":"e93f9851cd744cef5a68368d15bfb348806fd8b1","tarball":"https://registry.npmjs.org/@ai2070/net-sdk/-/net-sdk-0.18.0.tgz","fileCount":40,"unpackedSize":438690,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEE2bKaujD6hcPj47d4bcH/snpYG5ereFujSLGCzLaABAiEAk2K0z7E6l4K6nzdc45BFGOef9SGGro9s2AMk3ymvxDM="}]},"_npmUser":{"name":"lzl0","email":"makerseven7@gmail.com"},"directories":{},"maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/net-sdk_0.18.0_1779055645741_0.5187958938247292"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-02T15:44:29.739Z","modified":"2026-05-17T22:07:26.031Z","0.9.0":"2026-05-02T15:44:30.047Z","0.10.0":"2026-05-03T21:10:31.303Z","0.11.0":"2026-05-05T01:24:09.695Z","0.12.0":"2026-05-06T00:25:44.010Z","0.13.0":"2026-05-10T23:01:42.447Z","0.14.0":"2026-05-11T15:01:05.662Z","0.15.0":"2026-05-12T18:54:31.225Z","0.16.0":"2026-05-13T18:00:04.023Z","0.17.0":"2026-05-13T23:15:31.821Z","0.18.0":"2026-05-17T22:07:25.892Z"},"bugs":{"url":"https://github.com/ai-2070/net/issues"},"license":"Apache-2.0","homepage":"https://github.com/ai-2070/net#readme","keywords":["net","mesh","event-bus","streaming","ai","llm","inference","rust"],"repository":{"type":"git","url":"git+https://github.com/ai-2070/net.git"},"description":"Ergonomic TypeScript SDK for the Net mesh network","maintainers":[{"name":"lzl0","email":"makerseven7@gmail.com"}],"readme":"# Net TypeScript SDK\n\nErgonomic TypeScript SDK for the Net mesh network.\n\nWraps the `@ai2070/net` NAPI bindings with streaming, typed channels, and a developer-friendly API.\n\n## Install\n\n```bash\nnpm install @ai2070/net-sdk @ai2070/net\n```\n\n## Cargo features (transitive)\n\n`@ai2070/net-sdk` is pure TypeScript; every wrapper class dispatches into the underlying `@ai2070/net` napi-rs binding. Published `.node` artifacts ship every feature enabled, but anyone building from source via `napi build` needs to pass them — symbols from a disabled feature are absent at runtime and the TypeScript wrapper's `import` will fail with `undefined`.\n\n| Cargo feature | sdk-ts wrapper module | Surface |\n|---|---|---|\n| `cortex` | `@ai2070/net-sdk/cortex` (also re-exported top-level) | `Redex`, `RedexFile`, `TasksAdapter`, `MemoriesAdapter`, `NetDb`, error types |\n| `meshdb` | `@ai2070/net-sdk/meshdb` | `MeshQuery`, `MeshQueryRunner`, `MeshQueryStream`, `QueryBuilder`, `InMemoryChainReader`, result + config types |\n| `meshos` | `@ai2070/net-sdk/meshos` | `MeshOsDaemonSdk`, `MeshOsDaemonHandle`, `MeshOsDaemon` interface, `DaemonHealth`, `CapabilityAdvert` |\n| `compute` | `@ai2070/net-sdk/compute` | `DaemonRuntime`, `DaemonHandle`, `MigrationHandle`, daemon trait shapes |\n| `groups` | `@ai2070/net-sdk/groups` | `ReplicaGroup`, `ForkGroup`, `StandbyGroup`, group config types |\n| `deck` | `@ai2070/net-sdk/deck` | `DeckClient`, `OperatorIdentity`, admin / snapshot / status streams, ICE break-glass |\n| `redis` | `@ai2070/net-sdk` top-level | `RedisStreamDedup` |\n| `net` | `@ai2070/net-sdk/mesh` | `MeshNode`, `NetStream`, channel auth |\n\nThe bus surface (`NetNode`, `EventStream`, capabilities, identity, predicates) is always present.\n\nThe `default` Cargo feature set enables every flag, so `npm install` users get full functionality. If you're building from source for an embedded target, slim the feature set in `bindings/node/Cargo.toml` and rebuild via `npm run build:debug` (or `build` for release).\n\n## Quick Start\n\n```typescript\nimport { NetNode } from '@ai2070/net-sdk';\n\nconst node = await NetNode.create({ shards: 4 });\n\n// Emit events\nnode.emit({ token: 'hello', index: 0 });\nnode.emitRaw('{\"token\": \"world\"}');\nnode.emitBuffer(Buffer.from('{\"token\": \"foo\"}'));\n\n// Batch\nnode.emitBatch([{ a: 1 }, { a: 2 }, { a: 3 }]);\n\nawait node.flush();\n\n// Poll\nconst response = await node.poll({ limit: 100 });\nfor (const event of response.events) {\n  console.log(event.raw);\n}\n\n// Stream (async iterator)\nfor await (const event of node.subscribe({ limit: 100 })) {\n  console.log(event.raw);\n}\n\nawait node.shutdown();\n```\n\n## Typed Streams\n\n```typescript\ninterface TokenEvent {\n  token: string;\n  index: number;\n}\n\nfor await (const token of node.subscribeTyped<TokenEvent>({ limit: 100 })) {\n  console.log(`${token.index}: ${token.token}`);\n}\n```\n\n## Typed Channels\n\n```typescript\ninterface TemperatureReading {\n  sensor_id: string;\n  celsius: number;\n  timestamp: number;\n}\n\nconst temps = node.channel<TemperatureReading>('sensors/temperature');\n\n// Publish\ntemps.publish({ sensor_id: 'A1', celsius: 22.5, timestamp: Date.now() });\n\n// Subscribe\nfor await (const reading of temps.subscribe()) {\n  console.log(`${reading.sensor_id}: ${reading.celsius}°C`);\n}\n```\n\n## Ingestion Methods\n\n| Method | Input | Speed | Returns |\n|--------|-------|-------|---------|\n| `emit(obj)` | Object | Fast | `Receipt` |\n| `emitRaw(json)` | String | Fast | `Receipt` |\n| `emitBuffer(buf)` | Buffer | Fastest | `boolean` |\n| `emitBatch(objs)` | Object[] | Bulk | `number` |\n| `emitRawBatch(jsons)` | String[] | Bulk | `number` |\n| `fire(json)` | String | Fire-and-forget | `boolean` |\n| `fireBatch(jsons)` | String[] | Fire-and-forget | `number` |\n\n## Transports\n\n```typescript\n// In-memory (default)\nawait NetNode.create({ shards: 4 });\n\n// Redis\nawait NetNode.create({ transport: { type: 'redis', url: 'redis://localhost:6379' } });\n\n// JetStream\nawait NetNode.create({ transport: { type: 'jetstream', url: 'nats://localhost:4222' } });\n\n// Encrypted mesh\nawait NetNode.create({\n  transport: {\n    type: 'mesh',\n    bind: '0.0.0.0:9000',\n    peer: '192.168.1.10:9001',\n    psk: '...',\n    peerPublicKey: '...',\n  },\n});\n```\n\n### Persistent producer nonce (cross-restart dedup)\n\nJetStream and Redis adapters key dedup on `(producer_nonce, shard,\nsequence_start, i)`. Without persistence the nonce is fresh per\nprocess — a producer that crashes mid-batch and restarts gets a\nnew nonce, retransmits look fresh, and the backend persists the\npartial half twice. Configure\n`producerNoncePath` to make the nonce durable:\n\n```typescript\nawait NetNode.create({\n  shards: 4,\n  transport: { type: 'redis', url: 'redis://localhost:6379' },\n  producerNoncePath: '/var/lib/myapp/producer.nonce',\n});\n```\n\nThe bus loads (or creates on first run) a u64 nonce at this\npath. JetStream gets cross-restart dedup automatically;\nRedis Streams ships the same id as a `dedup_id` field on every\nXADD, filterable via the helper below.\n\n## Redis Streams consumer-side dedup helper\n\nThe Redis adapter writes a stable `dedup_id` field on every XADD\nentry (`{producer_nonce:hex}:{shard_id}:{sequence_start}:{i}`).\nCombined with `producerNoncePath` above, the id is stable across\nboth retries and process restart, so the `MULTI/EXEC` timeout\nrace becomes filterable consumer-side.\n\n`RedisStreamDedup` is exposed on the underlying `@ai2070/net`\nNAPI module:\n\n```typescript\nimport { RedisStreamDedup } from '@ai2070/net';\nimport { createClient } from 'redis';\n\n// Sizing: ~10k events/sec * 1 min dedup window → ~600,000.\nconst dedup = new RedisStreamDedup(600_000);\n\nconst r = createClient();\nawait r.connect();\n\nlet cursor = '0';\nwhile (true) {\n  // XRANGE bounds are INCLUSIVE on both ends. After the first\n  // page we must use the exclusive form `(<id>` so we don't\n  // re-read the entry the cursor points at — a vanilla\n  // `xRange(stream, cursor, '+')` loop spins forever once the\n  // cursor reaches the tail and the same entry is returned every\n  // iteration.\n  const start = cursor === '0' ? cursor : `(${cursor}`;\n  const entries = await r.xRange('net:shard:0', start, '+', { COUNT: 100 });\n  if (entries.length === 0) break;\n  for (const entry of entries) {\n    const dedupId = entry.message.dedup_id;\n    if (!dedupId) {\n      // Older entries / non-Net producers: skip dedup.\n      await process(entry);\n      continue;\n    }\n    if (!dedup.isDuplicate(dedupId)) {\n      await process(entry);\n    }\n    cursor = entry.id;\n  }\n}\n```\n\nSurface (NAPI class):\n\n```typescript\nnew RedisStreamDedup(capacity?: number)   // defaults to 4096\ndedup.isDuplicate(id: string): boolean\ndedup.len: number       // readonly\ndedup.capacity: number  // readonly\ndedup.isEmpty: boolean  // readonly\ndedup.clear(): void\n```\n\nThe helper is transport-agnostic — bring your own `redis` /\n`ioredis` / equivalent client; it just answers the dedup\nquestion against an in-memory LRU. Concurrency: the underlying\nhandle wraps a Rust mutex, so concurrent calls from worker\nthreads serialize but are safe. Production-shape is one helper\nper consumer worker.\n\n## NAT Traversal (optimization, not correctness)\n\nTwo NATed peers already reach each other through the mesh's routed-handshake path. NAT traversal opens a shorter direct path when the NAT shape allows it; it's never required for connectivity. The TS SDK doesn't yet wrap this surface — it's a planned follow-up. For now, construct a `NetMesh` from `@ai2070/net` directly to access the NAPI methods:\n\n```ts\nimport { NetMesh } from '@ai2070/net';\n\nconst mesh = await NetMesh.create({\n  bindAddr: '0.0.0.0:9000',\n  psk: '00'.repeat(32),\n});\n\nawait mesh.reclassifyNat();\n\nconst klass  = mesh.natType();            // \"open\" | \"cone\" | \"symmetric\" | \"unknown\"\nconst reflex = mesh.reflexAddr();         // \"203.0.113.5:9001\" | null\n\nconst observed = await mesh.probeReflex(peerNodeId); // \"ip:port\"\n\n// Attempt a direct connection via the pair-type matrix.\n// `coordinator` mediates the punch when the matrix picks one.\n// Always resolves — stats tell you which path won.\nawait mesh.connectDirect(peerNodeId, peerPubkeyHex, coordinatorNodeId);\n\n// Cumulative counters — all BigInt, monotonic.\nconst s = mesh.traversalStats();\ns.punchesAttempted;   // coordinator mediated a PunchRequest + Introduce\ns.punchesSucceeded;   // ack arrived AND direct handshake landed\ns.relayFallbacks;     // landed on the routed path after skip/fail\n```\n\nOperators with a known-public address skip the classifier sweep entirely. The override pins `\"open\"` + the supplied address on every capability announcement; call `announceCapabilities()` after to propagate (the setter resets the rate-limit floor so the next announce is guaranteed to broadcast).\n\n```ts\nmesh.setReflexOverride('203.0.113.5:9001');\nawait mesh.announceCapabilities(/* caps */);\n// later:\nmesh.clearReflexOverride();\nawait mesh.announceCapabilities(/* caps */);\n```\n\nTraversal failures surface as `Error` instances whose `message` follows the stable `traversal: <kind>[: <detail>]` convention. The `<kind>` discriminator is one of `reflex-timeout` | `peer-not-reachable` | `transport` | `rendezvous-no-relay` | `rendezvous-rejected` | `punch-failed` | `port-map-unavailable` | `unsupported`. Match on the prefix:\n\n```ts\ntry {\n  await mesh.connectDirect(peerNodeId, peerPubkeyHex, coordId);\n} catch (e) {\n  const msg = (e as Error).message;\n  if (msg.startsWith('traversal: unsupported')) {\n    // native library built without --features nat-traversal\n  } else if (msg.startsWith('traversal: peer-not-reachable')) {\n    // ...\n  }\n}\n```\n\nA build without the `nat-traversal` feature raises `traversal: unsupported` for every NAT call — the routed path keeps working regardless. The NAPI type declarations for these methods are only generated when the build-time type-gen runs against a build *with* the feature, so a feature-off cdylib may require an `as any` cast or a local `.d.ts` augmentation.\n\n## Mesh Streams (multi-peer + back-pressure)\n\nFor direct peer-to-peer messaging — open a stream to a specific peer\nand react to back-pressure with first-class error classes:\n\n```typescript\nimport { MeshNode, BackpressureError, NotConnectedError } from '@ai2070/net-sdk';\n\nconst node = await MeshNode.create({\n  bindAddr: '127.0.0.1:9000',\n  psk: '0'.repeat(64),\n});\n// ... handshake (node.connect(...) / node.accept(...)) ...\n\nconst stream = node.openStream(peerNodeId, {\n  streamId: 0x42n,\n  reliability: 'reliable',\n  windowBytes: 256,   // max in-flight packets before BackpressureError\n});\n\n// Three canonical daemon patterns:\n\n// 1. Drop on pressure.\ntry {\n  await node.sendOnStream(stream, [Buffer.from('{}')]);\n} catch (e) {\n  if (e instanceof BackpressureError) {\n    metrics.inc('stream.backpressure_drops');\n  } else if (e instanceof NotConnectedError) {\n    // peer gone or stream closed — re-open if needed\n  } else {\n    throw e;\n  }\n}\n\n// 2. Retry with exponential backoff (5 ms → 200 ms, up to maxRetries).\nawait node.sendWithRetry(stream, [Buffer.from('{}')], 8);\n\n// 3. Block until the network lets up (bounded retry, ~13 min worst case).\nawait node.sendBlocking(stream, [Buffer.from('{}')]);\n\n// Live stats — tx/rx seq, in-flight, window, backpressure count (BigInts).\nconst stats = node.streamStats(peerNodeId, 0x42n);\n```\n\n`BackpressureError` and `NotConnectedError` both extend `Error`, so\n`instanceof` and `try/catch` work as expected. The transport never\nretries or buffers on its own behalf — the helper methods are\nopt-in policies, not defaults. See `../docs/TRANSPORT.md` for the full\ncontract.\n\n## Security (identity, tokens, capabilities, subnets)\n\nIdentity, capabilities, and subnets ride the underlying NAPI bindings\nas a single security unit — the mesh's subprotocol dispatch threads\nidentity + capabilities + subnets + channel auth together at runtime,\nand the TS SDK surfaces all of it through one type hierarchy.\n\n```typescript\nimport { randomBytes } from 'node:crypto';\nimport { Identity, MeshNode } from '@ai2070/net-sdk';\n\n// Load once from caller-owned storage (vault / KMS / env secret).\n// The persisted form IS the 32-byte seed; treat as secret material.\nconst seed = randomBytes(32);\nconst identity = Identity.fromSeed(seed);\n\n// Stable entity_id / node_id across restarts — derived from the seed.\nconst mesh = await MeshNode.create({\n  bindAddr: '127.0.0.1:9001',\n  psk: '42'.repeat(32),\n  identitySeed: seed,          // mesh and identity share the keypair\n});\n\n// mesh.entityId().equals(identity.entityId) // true — compare via\n// Buffer.equals(), since `===` on Buffers checks reference identity\n// not byte equality.\n\n// Issue a scoped subscribe grant for another entity.\nconst grantee = Identity.generate();\nconst token = identity.issueToken({\n  subject: grantee.entityId,\n  scope: ['subscribe'],\n  channel: 'sensors/temp',\n  ttlSeconds: 300,             // `0` throws — zero TTL would mint a born-expired token\n  delegationDepth: 0,          // 0 forbids re-delegation\n});\n\n// `token.bytes` is the transport-ready 161-byte blob.\n// Ship it to the grantee; they hand it back on subscribe.\n```\n\nErrors surface as `IdentityError` (malformed inputs — bad seed\nlength, unknown scope, invalid channel name) and `TokenError` whose\n`kind` discriminator is one of `invalid_format` | `invalid_signature`\n| `expired` | `not_yet_valid` | `delegation_exhausted` |\n`delegation_not_allowed` | `not_authorized`. Both extend `Error`,\nso `try/catch` + `instanceof` work as expected.\n\n### Capability announcements\n\n`mesh.announceCapabilities(caps)` broadcasts a `CapabilitySet` to\nevery directly-connected peer and self-indexes locally.\n`mesh.findNodes(filter)` queries the local index — results include\nthis node's own id when self matches.\n\n```typescript\nimport { MeshNode } from '@ai2070/net-sdk';\n\nconst mesh = await MeshNode.create({\n  bindAddr: '127.0.0.1:9002',\n  psk: '42'.repeat(32),\n});\n\nawait mesh.announceCapabilities({\n  hardware: {\n    cpuCores: 16,\n    memoryGb: 64,\n    gpu: { vendor: 'nvidia', model: 'h100', vramGb: 80 },\n  },\n  models: [\n    { modelId: 'llama-3.1-70b', family: 'llama', contextLength: 128_000 },\n  ],\n  tags: ['gpu', 'prod'],\n});\n\nconst gpuPeers = mesh.findNodes({\n  requireGpu: true,\n  gpuVendor: 'nvidia',\n  minVramMb: 40_000,\n});\n// gpuPeers includes mesh.nodeId() on self-match.\n```\n\n#### Scoped discovery (reserved `scope:*` tags)\n\nA provider can narrow *who its query result reaches* by tagging\nits `CapabilitySet` with reserved `scope:*` tags. Queries call\n`mesh.findNodesScoped(filter, scope)` to filter candidates. The\nwire format and forwarders are untouched — enforcement is\npurely query-side.\n\n```typescript\nimport { withTenantScope } from '@ai2070/net-sdk';\n\n// GPU pool advertised to one tenant only.\nawait mesh.announceCapabilities({\n  tags: withTenantScope(['model:llama3-70b'], 'oem-123'),\n});\n\n// Tenant-scoped query — returns this node + any Global (untagged) peers.\nconst oemNodes = mesh.findNodesScoped(\n  { requireTags: ['model:llama3-70b'] },\n  { kind: 'tenant', tenant: 'oem-123' },\n);\n```\n\n`ScopeFilter` is a tagged union by `kind`:\n`{ kind: 'any' }` (default), `{ kind: 'globalOnly' }`,\n`{ kind: 'sameSubnet' }`, `{ kind: 'tenant', tenant }`,\n`{ kind: 'tenants', tenants: [...] }`,\n`{ kind: 'region', region }`,\n`{ kind: 'regions', regions: [...] }`. Reserved announcement\ntags: `scope:subnet-local` (visible only under `sameSubnet`),\n`scope:tenant:<id>`, `scope:region:<name>` — strictest scope\nwins. Helpers `withTenantScope`, `withRegionScope`,\n`withSubnetLocalScope` build the tag list idempotently.\nUntagged peers resolve to `Global` and stay visible under\npermissive queries. Full design:\n[`docs/SCOPED_CAPABILITIES_PLAN.md`](../docs/SCOPED_CAPABILITIES_PLAN.md).\n\nPropagation is multi-hop, bounded by `MAX_CAPABILITY_HOPS = 16`.\nForwarders re-broadcast every received announcement to their other\npeers; dedup on `(origin, version)` drops duplicates at convergence\npoints, and `hop_count` sits outside the signed envelope so the\norigin's signature verifies at every hop.\n`capabilityGcIntervalMs` + TTL-driven eviction are configurable on\n`MeshNode.create`. See\n[`docs/MULTIHOP_CAPABILITY_PLAN.md`](../docs/MULTIHOP_CAPABILITY_PLAN.md).\n\n#### Capability enhancements (typed taxonomy + predicates + validation)\n\nBeyond announce / find-peers, the SDK exposes a caller-local\nenhancement layer mirroring the substrate's `CapabilityEnhancements`:\n\n```typescript\nimport {\n  // Typed taxonomy\n  tagFromUserString, RESERVED_PREFIXES,\n  // Chain helpers\n  emptyCapabilities, requireTag, requireAxisValue, withMetadata,\n  // Predicates\n  p, evaluatePredicate, predicateToRpcHeader, predicateFromRpcHeader,\n  RPC_WHERE_HEADER,\n  // Predicate trace + debug\n  evaluatePredicateWithTrace,\n  predicateDebugReport, redactMetadataKeys, renderDebugReport,\n  // Validation\n  validateCapabilities, isReportValid,\n  // Diff\n  diffCapabilities,\n  // Placement filters\n  standardPlacement, placementFilterFromFn,\n} from '@ai2070/net-sdk';\n\n// Build a capability set in the wire shape `{ tags, metadata }`.\nlet caps = emptyCapabilities();\ncaps = requireTag(caps, 'hardware', 'gpu');\ncaps = requireAxisValue(caps, 'software', 'os', 'linux');\ncaps = withMetadata(caps, 'intent', 'ml-training');\n\n// Author a predicate.\nconst pred = p.and(\n  p.exists({ axis: 'hardware', key: 'gpu' }),\n  p.numericAtLeast({ axis: 'hardware', key: 'memory_gb' }, 64),\n  p.metadataEquals('intent', 'ml-training'),\n);\n\n// Local evaluation (no mesh round-trip).\nconst matched = evaluatePredicate(pred, caps.tags, caps.metadata);\n\n// Wire form for nRPC `net-where:` headers — pair with the\n// header-bearing call variants (`callWithHeaders` etc.) so a\n// server-side filter can match candidates without running the\n// predicate over the whole route.\nconst headerValue = predicateToRpcHeader(pred);\n// Reverse direction: parse a peer-supplied header back into the AST.\nconst decoded = predicateFromRpcHeader(headerValue);\n\n// Validate against the canonical schema (catches typos, type\n// mismatches, oversize metadata, legacy tags).\nconst report = validateCapabilities(caps);\nif (!isReportValid(report)) {\n  console.error('schema errors:', report.errors);\n}\n\n// Detect what changed between two snapshots — drives placement\n// re-evaluation when a daemon's CapabilitySet updates.\nconst delta = diffCapabilities(prevCaps, caps);\n\n// Single-evaluation trace — every clause's verdict + skipped\n// children for short-circuit AND/OR.\nconst { result, trace } = evaluatePredicateWithTrace(pred, tags, metadata);\n\n// Profile a predicate across a corpus + render a per-clause report.\nconst debug = predicateDebugReport(pred, contexts);\nconst safe = redactMetadataKeys(debug, ['intent']); // scrub before persisting\nconsole.log(renderDebugReport(safe));\n\n// Wrap a predicate as a placement-filter callback the substrate\n// invokes per candidate. Pair with `standardPlacement` to\n// install a custom scoring axis driven by the JS predicate.\nconst filter = placementFilterFromFn((cand) =>\n  evaluatePredicate(pred, cand.tags, cand.metadata),\n);\nconst placement = standardPlacement().withCustomFilterId(filter.id).build();\n```\n\nThe wire format is byte-identical across all five bindings (Rust /\nTS / Python / Go / C) — pinned by JSON fixtures under\n`tests/cross_lang_capability/`. A predicate authored in TS and\nshipped to a Go service via nRPC headers decodes losslessly.\n\n### Subnets (visibility partitioning)\n\n`subnet` pins a node to a specific 4-level `SubnetId`; `subnetPolicy`\nderives each *peer's* subnet from their inbound capability tags so\nevery node in the mesh agrees on the geometry without a central\ndirectory.\n\n```typescript\nimport { MeshNode } from '@ai2070/net-sdk';\n\nconst policy = {\n  rules: [\n    { tagPrefix: 'region:', level: 0, values: { us: 3, eu: 4 } },\n    { tagPrefix: 'fleet:',  level: 1, values: { blue: 7, green: 8 } },\n  ],\n};\n\nconst mesh = await MeshNode.create({\n  bindAddr: '127.0.0.1:9003',\n  psk: '42'.repeat(32),\n  subnet: { levels: [3, 7] },    // us/blue\n  subnetPolicy: policy,\n});\n\n// Announce tags matching the policy so peers derive the same\n// SubnetId [3, 7] when they apply their own policy to our caps.\nawait mesh.announceCapabilities({ tags: ['region:us', 'fleet:blue'] });\n```\n\nChannel `visibility` gates publish fan-out and subscribe\nauthorization against the derived geometry. Cross-subnet subscribes\nto a `SubnetLocal` channel reject with `Unauthorized`.\n\n### Channel authentication\n\n`ChannelConfig` carries three auth knobs, enforced end-to-end at\nboth the subscribe gate and the publish path:\n\n- `publishCaps: CapabilityFilter` — publisher must satisfy before\n  fan-out. Failing publishes raise an error; no peers are attempted.\n- `subscribeCaps: CapabilityFilter` — subscribers must satisfy\n  before being added to the roster. Failures surface as\n  `ChannelAuthError`.\n- `requireToken: true` — subscribers must present a valid `Token`\n  whose subject matches their `entityId`. The publisher verifies\n  the ed25519 signature, installs the token in its local cache,\n  then runs `can_subscribe`.\n\n```typescript\nimport { Identity, MeshNode } from '@ai2070/net-sdk';\n\nconst pubIdentity = Identity.generate();\nconst subIdentity = Identity.generate();\n\nconst publisher = await MeshNode.create({\n  bindAddr: '127.0.0.1:9004',\n  psk: '42'.repeat(32),\n  identitySeed: pubIdentity.toBytes(),\n});\n\n// Subscriber-side mesh, pinned to subIdentity so the publisher's\n// `require_token` check matches the token's subject against the\n// subscribing peer's entityId.\nconst subscriber = await MeshNode.create({\n  bindAddr: '127.0.0.1:9005',\n  psk: '42'.repeat(32),\n  identitySeed: subIdentity.toBytes(),\n});\n// Handshake the pair + start receive loops before any subscribe —\n// omitted here for brevity; see the `Mesh Streams` section.\n\npublisher.registerChannel({\n  name: 'events/inference',\n  subscribeCaps: { requireTags: ['gpu'] },\n  requireToken: true,\n});\n\n// Issue a SUBSCRIBE-scope token for the subscriber.\nconst token = pubIdentity.issueToken({\n  subject: subIdentity.entityId,\n  scope: ['subscribe'],\n  channel: 'events/inference',\n  ttlSeconds: 300,\n});\n\n// Subscriber attaches the token on subscribe.\nawait subscriber.subscribeChannel(\n  publisher.nodeId(),\n  'events/inference',\n  { token },\n);\n```\n\nDenied subscribes surface as `ChannelAuthError` (a subclass of\n`ChannelError`); malformed token bytes raise `TokenError` before\nany network I/O. Successful subscribes populate an `AuthGuard`\nbloom filter on the publisher so every subsequent publish admits\nthe subscriber in constant time (~20 ns per check,\nsingle-threaded). Expired tokens evict within the publisher's\n`token_sweep_interval` (default 30 s); repeated subscribe\nfailures from the same peer throttle via `RateLimited` acks so\nbad-token storms never tie up ed25519 verification. Cross-SDK\nbehaviour is fixed by the Rust integration suite — see\n[`SDK_SECURITY_SURFACE_PLAN.md`](../docs/SDK_SECURITY_SURFACE_PLAN.md)\nand\n[`CHANNEL_AUTH_GUARD_PLAN.md`](../docs/CHANNEL_AUTH_GUARD_PLAN.md)\nfor the full contract.\n\n## Channels (distributed pub/sub)\n\nNamed pub/sub across the encrypted mesh. The publisher registers a\nchannel config; subscribers ask to join via `subscribeChannel` (the\nsubscribe goes through a dedicated subprotocol with an Ack round-trip);\n`publish` fans one payload out to every current subscriber.\n\n```typescript\nimport { MeshNode, ChannelAuthError } from '@ai2070/net-sdk';\n\nconst psk = '0'.repeat(64);\n\n// Publisher side.\nconst b = await MeshNode.create({ bindAddr: '127.0.0.1:9001', psk });\nb.registerChannel({\n  name: 'sensors/temp',\n  visibility: 'global',           // or 'subnet-local' / 'parent-visible' / 'exported'\n  reliable: true,\n  priority: 2,\n  maxRatePps: 1000,\n});\n\n// Subscriber side + full handshake.\nconst a = await MeshNode.create({ bindAddr: '127.0.0.1:9002', psk });\nconst aNodeId = a.nodeId();\nconst bNodeId = b.nodeId();\n// connect/accept must race: the initiator blocks on a handshake reply\n// that only shows up once the responder is in accept(). Then both\n// sides must start() their receive loops before app traffic flows.\nawait Promise.all([\n  b.accept(aNodeId),\n  a.connect('127.0.0.1:9001', b.publicKey(), bNodeId),\n]);\nawait a.start();\nawait b.start();\nawait a.subscribeChannel(bNodeId, 'sensors/temp');\n\n// Fan out.\nconst report = await b.publish(\n  'sensors/temp',\n  Buffer.from(JSON.stringify({ celsius: 22.5 })),\n  { reliability: 'reliable', onFailure: 'best_effort', maxInflight: 32 },\n);\nconsole.log(`${report.delivered}/${report.attempted} subscribers received`);\n\n// Rejections surface with typed errors:\ntry {\n  await a.subscribeChannel(bNodeId, 'restricted');\n} catch (e) {\n  if (e instanceof ChannelAuthError) { /* ACL rejected */ }\n}\n```\n\n**Channel names always cross the boundary as strings.** The u16 hash\nis a transport-layer index only; ACL lookups key on the canonical\nname to avoid bypass via hash collision (see `../docs/CHANNELS.md`).\n\nSubscribers today receive payloads through the existing event-bus\n`poll()` surface — a dedicated per-channel `AsyncIterable` receive\nmethod is a follow-up.\n\n## CortEX & NetDb (event-sourced state)\n\nTyped, event-sourced state on top of RedEX — tasks and memories with\nfilterable queries and reactive `AsyncIterable` watches. Includes the\n`snapshotAndWatch` primitive whose race fix landed on v2, so you can\nsafely \"paint what's there now, then react to changes\" without losing\nupdates that race during construction.\n\n```typescript\nimport { NetDb, TaskStatus, CortexError } from '@ai2070/net-sdk';\n\nconst db = await NetDb.open({\n  originHash: 0xABCDEF01,\n  withTasks: true,\n  withMemories: true,\n  // persistentDir + persistent: true for disk-backed files\n});\n\n// CRUD through the domain API — no EventMeta plumbing.\ntry {\n  const seq = db.tasks!.create(1n, 'write docs', 100n);\n  await db.tasks!.waitForSeq(seq);  // wait for the fold to apply\n} catch (e) {\n  if (e instanceof CortexError) { /* handle adapter error */ }\n  else { throw e; }\n}\n\n// Snapshot + watch: one atomic call, no race.\nconst { snapshot, updates } = await db.tasks!.snapshotAndWatch({\n  status: TaskStatus.Pending,\n});\nrender(snapshot);\nfor await (const next of updates) {\n  render(next);\n  if (shouldStop) break;   // automatically closes the native iterator\n}\n\ndb.close();\n```\n\n### Plain watches\n\n`watch()` returns the same `AsyncIterable<T[]>` shape without a\nsnapshot. Prefer `snapshotAndWatch` when the caller needs the initial\nresult — calling `listTasks()` + `watch()` separately races, and a\nmutation landing between them can be silently lost.\n\n```typescript\nfor await (const batch of await db.tasks!.watch({ titleContains: 'ship' })) {\n  // each batch is the current filter result after a deduplicated fold tick\n}\n```\n\n### Standalone adapters\n\nIf you only need one model, skip the `NetDb` facade and open the\nadapter directly against a `Redex`:\n\n```typescript\nimport { Redex, TasksAdapter } from '@ai2070/net-sdk';\n\nconst redex = new Redex({ persistentDir: '/var/lib/net/redex' });\nconst tasks = await TasksAdapter.open(redex, 0xABCDEF01, { persistent: true });\n```\n\n### Raw RedEX file (no CortEX fold)\n\nFor domain-agnostic persistent logs — your own event schema, no fold,\nno typed adapter — open a `RedexFile` directly from a `Redex`. The\ntail iterator is the same `AsyncIterable` shape as the CortEX\nwatches, so `for await` + `break` cleans up native resources.\n\n```typescript\nimport { Redex, RedexError } from '@ai2070/net-sdk';\n\nconst redex = new Redex({ persistentDir: '/var/lib/net/events' });\nconst file = redex.openFile('analytics/clicks', {\n  persistent: true,\n  fsyncIntervalMs: 100,           // or fsyncEveryN: 1000n\n  retentionMaxEvents: 1_000_000n,\n});\n\n// Append (or batch-append).\nconst seq = file.append(Buffer.from(JSON.stringify({ url: '/home' })));\n// `appendBatch` returns the first-seq `bigint` of the batch, or\n// `null` for an empty input. The `null` return is the explicit\n// \"I appended nothing\" signal — pre-`bugfixes-8` it returned `0n`,\n// which collided with the legitimate \"first event of a non-empty\n// batch landed at seq 0\" return.\nconst firstSeq = file.appendBatch(payloadBuffers);\n\n// Tail — backfills the retained range, then streams live appends.\nconst stream = await file.tail(0n);\ntry {\n  for await (const event of stream) {\n    const parsed = JSON.parse(event.payload.toString());\n    console.log(event.seq, parsed);\n    if (shouldStop) break;   // automatically closes the native iterator\n  }\n} catch (e) {\n  if (e instanceof RedexError) { /* ... */ }\n  throw e;\n} finally {\n  // Ensure the file is closed even if tailing / parsing throws.\n  file.close();\n}\n```\n\n### Cross-node RedEX replication\n\nRedEX channels can replicate across the mesh. Opt in per channel by\nsetting `replication` on the file config. The default — omitting\n`replication` — keeps the channel single-node and adds zero wire\ntraffic. Replicated channels carry N copies of the log; the leader is\nthe single writer, replicas catch up via pull-based sync. Failover\nuses a deterministic nearest-RTT election with NodeId tie-break.\n\n```typescript\nimport { NetMesh, Redex } from '@ai2070/net';\n\nconst mesh = await NetMesh.create({\n  bindAddr: '127.0.0.1:0',\n  psk: '...',\n});\nconst redex = new Redex({ persistentDir: '/var/lib/net/events' });\n\n// Install the per-Redex replication router on the mesh.\n// Idempotent — safe to call from multiple paths.\nredex.enableReplication(mesh);\n\nconst file = redex.openFile('orders/audit', {\n  persistent: true,\n  replication: {\n    factor: 3,                     // 1..16; default 3\n    heartbeatMs: 500n,             // min 100; default 500\n    placement: 'standard',         // 'standard' | 'pinned' | 'colocation-strict'\n    // pinnedNodes: [nodeIdA, nodeIdB, nodeIdC],   // required when placement = 'pinned'\n    // leaderPinned: someNodeId,\n    onUnderCapacity: 'withdraw',   // 'withdraw' (default) | 'evict-oldest'\n    replicationBudgetFraction: 0.5,\n  },\n});\nfile.append(Buffer.from('event payload'));\n```\n\nThe leader handles every append locally; replicas observe the\nleader's heartbeat `tail_seq`, issue `SYNC_REQUEST` on lag, apply\nchunks via `SYNC_RESPONSE`. When the leader closes (or the replica's\nbelieved leader goes silent past `3 × heartbeatMs`), the surviving\nreplicas run the deterministic election and one becomes the new\nleader within microseconds.\n\n`Redex.replicationPrometheusText()` renders the seven per-channel\nmetric shapes — `*_lag_seconds`, `*_sync_bytes_total`,\n`*_leader_changes_total`, `*_under_capacity_total`,\n`*_skip_ahead_total`, `*_election_thrash_total`,\n`*_witness_withdrawals_total` — for an HTTP scrape endpoint. Returns\nthe empty string when replication isn't enabled; pipe directly into a\nresponse body without branching. `replicationRuntimeCount()` returns\nthe count of registered per-channel runtimes.\n\n```typescript\n// HTTP scrape handler\napp.get('/metrics', (req, res) => {\n  res.type('text/plain').send(redex.replicationPrometheusText());\n});\n```\n\nDisk-pressure handling: when a replica's local file rejects an\nappend (heap-segment cap or disk write-fail), the configured\n`onUnderCapacity` policy fires — `withdraw` drops the replica role\n(capability tag withdrawn; peers re-route to a healthy holder),\n`evict-oldest` runs retention sweep + retries (requires\n`retentionMax*` caps to be set on the same file config).\n\n### Error classes\n\nCortEX-boundary errors are typed and catchable via `instanceof`:\n\n- `CortexError` — adapter errors (fold halted, RedEX I/O, decode failures).\n- `NetDbError` — snapshot/restore bundle errors, missing-model lookups.\n- `RedexError` — raw file errors (invalid channel name, bad config,\n  append / tail / sync / close failures).\n\nAll three are re-exported from `@ai2070/net-sdk`; you don't need a\nseparate import path.\n\n## Dataforts (greedy cache, gravity, blob refs, read-your-writes)\n\nDataforts is the compositional data plane on top of RedEX / CortEX\n/ capability-index / proximity-graph. The TypeScript surface exposes\ngreedy + gravity through `Redex` methods, blob registration through\ntop-level helpers, and read-your-writes through `WriteToken` +\n`waitForToken` on `Tasks` / `Memories`. The underlying native module\nis built with the `dataforts` Cargo feature; pre-built `@ai2070/net`\nrelease artifacts ship with the feature on.\n\nFour phases:\n\n- **Phase 1 — Greedy-LRU caching.** Per-node speculative caching\n  of in-scope chains observed via the tail-subscription path.\n  Five-axis admission (scope + proximity + capability-preference\n  + colocation + storage-cap) plus a bandwidth budget gate decide\n  whether to admit each inbound event. Cold channels evict under\n  cluster-cap pressure and withdraw their `causal:<hex>`\n  advertisement. The runtime also observes `BlobRef`-shaped\n  payloads + runs the `should_pull_blob` admission gate; on\n  admit the wired `BlobAdapter::prefetch` spawns a best-effort\n  pull via the per-chunk replication runtime.\n- **Phase 3 — `BlobRef` + blob adapters.** Two shapes:\n  - **External-hook variant (v0.15):** a `[0xB0, 0xB1, 0xB2,\n    0xB3]` magic + version + 32-byte BLAKE3 + size + URI\n    reference whose bytes live in the caller's storage (S3 /\n    Ceph / IPFS / local FS). Exposed today via\n    `registerFilesystemBlobAdapter` + `blobPublish` /\n    `blobResolve`.\n  - **Substrate-owned variant (v0.2):** the substrate stores\n    each chunk as a content-addressed `RedexFile`, riding the\n    existing replication runtime for cross-node placement.\n    `MeshBlobAdapter` is now available as a TypeScript class\n    on the `@ai2070/net` Node binding (CRUD path: `store` /\n    `fetch` / `fetchRange` / `exists` / `prometheusText`).\n    The deeper integration points (`publish_with_blob`,\n    `BlobRefcountTable`, `BlobMetrics`, `BlobAdapter::prefetch`)\n    are still Rust-only — operator scripts that need them\n    from TypeScript call out to the `net-blob` CLI or a Rust-\n    side daemon RPC until each follow-up wrapper lands. See\n    [`docs/plans/DATAFORTS_BLOB_STORAGE_PLAN.md`](../docs/plans/DATAFORTS_BLOB_STORAGE_PLAN.md)\n    for the shipping status.\n- **Phase 3.5 — Active blob overflow (v0.3 blob track).** Push-\n  side complement of Phase 4's pull-driven migration. Disabled\n  by default; opt in via the `MeshBlobAdapter` constructor's\n  `overflow` option or the runtime `setOverflowEnabled(true)`\n  method. The full counter family\n  (`dataforts_blob_overflow_*` — admitted / 6-label per-reason\n  rejected / hysteresis edges / `active` gauge / `disk_ratio`)\n  lands in `prometheusText()`. See\n  [`docs/plans/DATAFORTS_BLOB_OVERFLOW_PLAN.md`](../docs/plans/DATAFORTS_BLOB_OVERFLOW_PLAN.md)\n  for design + per-PR shipping status.\n- **Phase 4 — Data gravity.** Per-chain read-rate counters with\n  exponential decay. Threshold-crossing emissions stamp\n  `heat:<hex>=<rate>` onto the chain's capability announcement;\n  greedy weights cache pulls by `heat × scope-match × proximity`.\n  The v0.2 blob track adds parallel `BlobHeatRegistry` keyed on\n  chunk hash + `heat:blob:<hex>=<rate>` tag emission +\n  `drive_blob_migration_tick` consumer — exposed from Rust\n  today; Node wrapper deferred.\n- **Phase 5 — Read-your-writes.** Every `tasks.create`,\n  `memories.insert`, etc. returns a `WriteToken`. Pass it to\n  `tasks.waitForToken(token, deadlineMs)` and the call resolves\n  only after the local fold has *applied* that seq — tracking\n  both `appliedThroughSeq` and `foldedThroughSeq` so a stalled\n  fold surfaces a typed error, not a silent resolve.\n\n```ts\nimport { Redex, Tasks, BlobRef, registerFilesystemBlobAdapter,\n         blobPublish, blobResolve, MeshNode } from '@ai2070/net';\n\nconst mesh = new MeshNode({ bindAddr: '0.0.0.0:7000', psk: '…' });\nconst redex = new Redex({ persistentDir: '/var/lib/net/redex' });\n\n// Phase 1 — wire greedy into the mesh inbound dispatch.\nredex.enableGreedyDataforts(mesh, {\n  scopes: ['region:us'],\n  totalCapBytes: 1n << 30n,   // 1 GiB cluster-cap\n  perChannelCapBytes: 64n << 20n,\n});\n\n// Phase 4 — layer gravity on top.\nredex.enableGravityForGreedy(mesh, {\n  enabled: true,\n  emitThresholdRatio: 1.5,\n  decayHalfLifeSecs: 300n,\n});\n\n// Phase 3 — register an adapter (filesystem ships in-tree).\nregisterFilesystemBlobAdapter('local', '/var/blobs');\nconst ref = await blobPublish('local', 'local://obj/payload', someBytes);\nconst back = await blobResolve(ref);\n\n// Phase 3 v0.2 — substrate-owned `MeshBlobAdapter`.\nimport { MeshBlobAdapter, BlobRef } from '@ai2070/net';\nconst meshBlob = new MeshBlobAdapter(redex, 'mesh-app', {\n  persistent: true,\n});\nconst hash = /* 32-byte BLAKE3 of `someBytes` */ Buffer.alloc(32);\nconst blobRef = new BlobRef('mesh://demo', hash, BigInt(someBytes.length));\nawait meshBlob.store(blobRef, someBytes);\nconst fetched = await meshBlob.fetch(blobRef);\n\n// Phase 3.5 / v0.3 — active blob overflow.\n// At construction:\nconst overflowed = new MeshBlobAdapter(redex, 'mesh-overflow', {\n  persistent: true,\n  overflow: {\n    enabled: true,\n    highWaterRatio: 0.80,\n    lowWaterRatio: 0.65,\n    maxPushesPerTick: 8,\n    scope: 'zone',\n    tickIntervalMs: 30000,\n  },\n});\n\n// Or flip the master switch at runtime — no rebuild required:\noverflowed.setOverflowEnabled(false);\noverflowed.setOverflowEnabled(true);\n\n// Inspection (read-only getters):\nconsole.log(overflowed.overflowEnabled);   // boolean\nconsole.log(overflowed.overflowActive);    // boolean — hysteresis state\nconsole.log(overflowed.overflowConfig);    // typed snapshot\nconsole.log(overflowed.prometheusText());  // includes dataforts_blob_overflow_*\n\n// Phase 5 — read-your-writes.\nconst tasks = await Tasks.open(redex, { originHash: mesh.originHash });\nconst { token } = await tasks.create(1, 'first', 100);\nawait tasks.waitForToken(token, 250); // ms deadline; throws CortexError on timeout\n\n// Diagnostics.\nconsole.log(redex.greedyCachedChannelCount());\nconsole.log(redex.greedyPrometheusText());\n```\n\nThe canonical channel hash is 32-bit (`channelHash(name)` returns\n`number` in the u32 range). The per-packet wire `NetHeader`\n`channel_hash` stays `u16` — fast-path filter hint, may\nbucket-collide at scale; ACL / config / cache / RYW decisions key on\nthe canonical 32-bit hash via registry disambiguation. The\n`PermissionToken` wire form is 161 bytes (the 2-byte → 4-byte\nchannel-hash widening grew it from 159).\n\n## nRPC (request / response over the mesh)\n\nnRPC is the request/response convention layer riding on top of the\npub/sub mesh. It turns a directed channel pair\n(`<service>.requests` / `<service>.replies.<caller_origin>`) into\na typed RPC surface with deadlines, queue-group fan-out, response\nstreaming, and end-to-end cancellation.\n\nThe typed surface ships in the napi binding at\n`@ai2070/net/mesh_rpc` (the SDK's `MeshNode` wraps a `NetMesh`\nthat nRPC consumes directly):\n\n```typescript\nimport { MeshNode } from '@ai2070/net-sdk'\nimport {\n  classifyError,\n  RpcCancelledError,\n  RpcServerError,\n} from '@ai2070/net/errors'\nimport {\n  appError,\n  CircuitBreaker,\n  HedgePolicy,\n  NRPC_TYPED_BAD_REQUEST,\n  RetryPolicy,\n  TypedMeshRpc,\n} from '@ai2070/net/mesh_rpc'\n\nconst server = await MeshNode.create({ bindAddr: '127.0.0.1:9001', psk })\nconst client = await MeshNode.create({ bindAddr: '127.0.0.1:9000', psk })\n// (handshake omitted — see Mesh Streams example)\n\ninterface EchoSumRequest  { text: string; numbers: number[] }\ninterface EchoSumResponse { echo: string; sum: number }\n\n// Server side: register a typed handler. Returned `serveHandle`\n// MUST be `close()`d to stop accepting new requests; in-flight\n// handlers complete (no abort).\nconst serverRpc = TypedMeshRpc.fromMesh((server as any)._native)\nconst serveHandle = serverRpc.serve<EchoSumRequest, EchoSumResponse>(\n  'echo_sum',\n  async (req) => ({ echo: req.text, sum: req.numbers.reduce((a, b) => a + b, 0) }),\n)\n\n// Client side: typed call with a 200ms deadline.\nconst clientRpc = TypedMeshRpc.fromMesh((client as any)._native)\ntry {\n  const reply = await clientRpc.call<EchoSumRequest, EchoSumResponse>(\n    server.nodeId(),\n    'echo_sum',\n    { text: 'hi', numbers: [1, 2, 3] },\n    { deadlineMs: 200 },\n  )\n  // reply.sum === 6\n} catch (e) {\n  // Errors carry a stable `nrpc:` prefix; classifyError() routes\n  // them to typed subclasses for instanceof checks.\n  const typed = classifyError(e)\n  if (typed instanceof RpcServerError && typed.status === NRPC_TYPED_BAD_REQUEST) {\n    // handler bad-request\n  }\n}\n\nawait serveHandle.close()\n```\n\n### Streaming responses\n\n```typescript\nconst stream = await clientRpc.callStreaming<MyReq, MyChunk>(\n  targetNodeId, 'tail', { tail: 'events' },\n  { deadlineMs: 5_000, streamWindowInitial: 8 },  // optional flow control\n)\nfor await (const chunk of stream) {\n  // chunk is decoded MyChunk\n}\n// stream.close() emits CANCEL to the server (best-effort);\n// in-flight chunks are silently discarded.\n// stream.grant(n) issues an explicit credit publish for batched\n// cadence (no-op on streams without flow control).\n// stream.flowControlled() reports whether streamWindowInitial was\n// set on the call — useful for code that conditionally grants.\n```\n\n### Cancellation (`AbortSignal`)\n\n`call` / `callService` accept an `AbortSignal` via `opts.signal`.\nThe wrapper mints a cancel token, attaches a one-shot abort\nlistener, and detaches it on settle so the same signal can be\nreused. Aborting publishes CANCEL to the server and rejects with\n`RpcCancelledError` (caller-fixable; **not** retried by the\ndefault `RetryPolicy` predicate).\n\n```typescript\nconst ac = new AbortController()\nsetTimeout(() => ac.abort(), 100)\n\ntry {\n  await clientRpc.call(targetNodeId, 'slow', {}, { signal: ac.signal })\n} catch (e) {\n  if (classifyError(e) instanceof RpcCancelledError) {\n    // CANCEL fired on the wire; server-side handler observes\n    // its `ctx.cancellation` token.\n  }\n}\n```\n\nPre-aborted signals fail fast — the call rejects with\n`nrpc:cancelled:` before any tokio spawn / registry overhead.\n\n### Resilience helpers\n\nDefaults mirror the Rust SDK (`mesh_rpc_resilience`): 3 attempts,\n50ms→1s exponential backoff with full-half jitter, retryable\npredicate skips `RpcCodecError` / `RpcNoRouteError` /\n`RpcCancelledError` and non-transient `RpcServerError` statuses.\n\n```typescript\n// RetryPolicy. `jitter` is a boolean (full-half jitter on/off);\n// override `retryable` to gate which errors retry.\nconst policy = new RetryPolicy({\n  maxAttempts: 4,\n  initialBackoffMs: 50,\n  maxBackoffMs: 1000,\n  jitter: true,\n})\nconst reply = await clientRpc.callWithRetry(\n  targetNodeId, 'echo', { hello: 'world' }, undefined /* opts */, policy,\n)\n\n// HedgePolicy fans out parallel attempts on a delay; primary at\n// t=0, additional hedges at t=delayMs * idx. First reply (Ok or\n// Err) wins; if every hedge fails, the primary's error surfaces\n// deterministically.\nconst hedge = new HedgePolicy({ delayMs: 50, hedges: 2 })  // primary + 2 hedges\nawait clientRpc.callWithHedgeTo(targetNodeIds, 'echo', { /*...*/ }, undefined, hedge)\n\n// CircuitBreaker — closed → open → half-open with a configurable\n// failure predicate. Open breakers throw `BreakerOpenError` carrying\n// the `nrpc:breaker_open:` prefix.\nconst breaker = new CircuitBreaker({ failureThreshold: 5, resetAfterMs: 1000 })\nawait breaker.call(() => clientRpc.call(targetNodeId, 'echo', {}))\n```\n\n### Typed handler bad-request\n\n`appError(code, body)` builds an `Error` whose message follows the\n`nrpc:app_error:0x<code>:<body>` contract the napi binding parses\ninto `RpcStatus::Application(code)`. Mirrors the Python binding's\n`RpcAppError`:\n\n```typescript\nserverRpc.serve<EchoSumRequest, EchoSumResponse>('echo_sum', (req) => {\n  if (typeof req.text !== 'string') {\n    throw appError(NRPC_TYPED_BAD_REQUEST, JSON.stringify({\n      error: 'invalid_request',\n      detail: 'text must be a string',\n    }))\n  }\n  return { echo: req.text, sum: req.numbers.reduce((a, b) => a + b, 0) }\n})\n```\n\n### Errors\n\nCaller-side failures throw a plain `Error` whose `.message`\nstarts with the stable `nrpc:` prefix (the binding throws plain\n`Error` rather than typed classes to sidestep vitest's\ndual-module-instance hazard; `classifyError(e)` reconstructs the\ntyped subclass at the catch site):\n\n| Kind segment    | Typed class           | Retried by default? |\n| --------------- | --------------------- | ------------------- |\n| `no_route`      | `RpcNoRouteError`     | no                  |\n| `timeout`       | `RpcTimeoutError`     | yes                 |\n| `server_error`  | `RpcServerError`      | only `0x0003` / `0x0004` / `0x0006` |\n| `transport`     | `RpcTransportError`   | yes                 |\n| `codec_encode`  | `RpcCodecError`       | no (caller-fixable) |\n| `codec_decode`  | `RpcCodecError`       | no (caller-fixable) |\n| `cancelled`     | `RpcCancelledError`   | no (caller-driven)  |\n| any other       | `RpcError` (base)     | yes (forward-compat fallback) |\n\n`BreakerOpenError` is thrown directly by `CircuitBreaker.call`\nwhen the breaker is open — catch it via\n`instanceof BreakerOpenError` (imported from `@ai2070/net/mesh_rpc`).\nIt carries the `nrpc:breaker_open:` prefix for log filtering, but\n`classifyError` routes it through the base `RpcError` rather than\nits own subclass. Server-side `appError(code, body)` rejections\narrive at the caller as `nrpc:server_error: status=0x<code>`, so\nthey classify as `RpcServerError` with `err.status === code`\n(check against `NRPC_TYPED_BAD_REQUEST` etc.).\n\n`classifyError` is duck-typed on `.message`: it accepts real\n`Error` instances, plain `{message: string}` objects, and string\nrejections — so top-level catch handlers reconstruct typed\nerrors regardless of what the throw site emitted.\n\nTwo stable status constants exposed by `@ai2070/net/mesh_rpc`:\n\n| Constant                       | Hex      | Meaning                                          |\n| ------------------------------ | -------- | ------------------------------------------------ |\n| `NRPC_TYPED_BAD_REQUEST`       | `0x8000` | Typed handler couldn't decode the request body.  |\n| `NRPC_TYPED_HANDLER_ERROR`     | `0x8001` | Typed handler ran but returned an exception.     |\n\nCross-binding contract spec — including the canonical\n`cross_lang_echo_sum` service used by every binding's wire-format\ncompat test — lives in [`../README.md#nrpc`](../README.md#nrpc).\n\n## MeshDB (federated query layer)\n\nMeshDB is the typed query layer above the RedEX / CortEX /\ncapability-index substrate. The native binding builds with\n`--features meshdb`; MeshDB classes import from `@ai2070/net`.\nArchitectural overview:\n[`../README.md#meshdb`](../README.md#meshdb).\n\n### Quick start\n\n```ts\nimport {\n  InMemoryChainReader,\n  MeshQuery,\n  MeshQueryRunner,\n} from '@ai2070/net';\n\nconst reader = new InMemoryChainReader();\nreader.append(0xabn, 1n, Buffer.from('v1'));\nreader.append(0xabn, 2n, Buffer.from('v2'));\nreader.append(0xabn, 3n, Buffer.from('v3'));\n\nconst runner = new MeshQueryRunner(reader);\n\n// Atomic operator — emits the tip row.\nconst stream = await runner.execute(MeshQuery.latest(0xabn));\nconst rows = await stream.toArray();\nconsole.log(rows[0].seq, Buffer.from(rows[0].payload).toString());\n// 3n \"v3\"\n```\n\n`runner.execute(query)` returns a `Promise<MeshQueryStream>`;\n`.toArray()` drains the stream eagerly, `.next()` pulls one row\nat a time, and the `@ai2070/net/meshdb` re-export installs a\n`Symbol.asyncIterator` shim so `for await` works directly:\n\n```ts\nimport '@ai2070/net/meshdb';  // installs the async-iterator shim\nimport { MeshQuery, MeshQueryRunner } from '@ai2070/net';\n\nconst stream = await runner.execute(MeshQuery.between(0xabn, 1n, 10n));\nfor await (const row of stream as unknown as AsyncIterable<{ seq: bigint }>) {\n  console.log(row.seq);\n}\n```\n\n### Operator surface\n\n```ts\nimport {\n  MeshQuery,\n  predicateEquals,\n  predicateAnd,\n  predicateNumericAtLeast,\n} from '@ai2070/net';\n\n// Fluent builder (common-ops shortcut).\nconst query = MeshQuery.builder()\n  .between(0xabn, 1n, 100n)\n  .filter(\n    predicateAnd([\n      predicateEquals('severity', 'high'),\n      predicateNumericAtLeast('seq', 5),\n    ]),\n  )\n  .count(['origin'])\n  .build();\n\n// Or compose static factories directly.\nconst between = MeshQuery.between(0xabn, 1n, 100n);\nconst filtered = MeshQuery.filter(between, predicateEquals('severity', 'high'));\nconst grouped = MeshQuery.count(filtered, ['origin']);\n```\n\n| Family | Factories / builder methods |\n|---|---|\n| Atomic | `MeshQuery.at`, `MeshQuery.between`, `MeshQuery.latest`, `MeshQuery.lineageEmit` |\n| Composite | `MeshQuery.filter`, `MeshQuery.window`, `MeshQuery.count`, `MeshQuery.sum/avg/min/max/percentile`, `MeshQuery.distinctCount`, `MeshQuery.join` |\n| Fluent builder | `MeshQuery.builder().<at|between|latest>(...).<filter|window|count|...>(...).build()` |\n| Predicate factories | `predicateExists`, `predicateEquals`, `predicateNumericAtLeast/AtMost/InRange`, `predicateStringPrefix/Matches`, `predicateSemverAtLeast`, `predicateAnd/Or/Not` |\n\nField paths target row-intrinsic names (`\"origin\"` / `\"seq\"`) or\ndotted JSON-payload paths (`\"a.b.c\"`).\n\n### Sentinel row decoders\n\nAtomic rows expose `.payload` directly as `Uint8Array`. Composite\nrows carry postcard-encoded sentinel envelopes — decode via the\nmodule-level helpers:\n\n```ts\nimport { decodeAggregate, decodeJoined, decodeWindow } from '@ai2070/net';\n\nconst [aggRow] = await (\n  await runner.execute(MeshQuery.count(MeshQuery.between(0xabn, 1n, 4n)))\n).toArray();\nconst result = decodeAggregate(aggRow);\n// { group: null, kind: 'count', value: 3, count: 3n }\n\nconst [pair] = await (await runner.execute(joinQuery)).toArray();\nconst joined = decodeJoined(pair);\n// { left: ResultRow|null, right: ResultRow|null }\n\nconst [bucket] = await (await runner.execute(windowQuery)).toArray();\nconst window = decodeWindow(bucket);\n// { start: bigint, end: bigint, rows: ResultRow[] }\n```\n\nEach decoder returns `null` for non-sentinel rows (atomic\noperator output), so callers branch on \"did this row deserialise?\"\nwithout a separate type query.\n\n### Phase F result cache\n\nPass `enableCache: true` at runner construction; tune per-call via\nthe optional second argument to `execute`:\n\n```ts\nimport {\n  cachePolicyPermanent,\n  cachePolicyTimeBound,\n  MeshQueryRunner,\n} from '@ai2070/net';\n\nconst runner = new MeshQueryRunner(reader, /* enableCache */ true);\n\n// Default — TimeBound TTL = 5 s (mirrors the join watermark).\nawait runner.execute(query);\n\n// Explicit per-call policy.\nawait runner.execute(query, { cachePolicy: cachePolicyPermanent() });\nawait runner.execute(query, { cachePolicy: cachePolicyTimeBound(30) });\nawait runner.execute(query, { bypassCache: true });\n```\n\n`cachePolicyPermanent()` is safe only when the query result is\nimmutable under substrate semantics.\n\n### Lineage emit\n\nThe SDK doesn't walk the `fork-of:` graph itself — callers supply\npre-walked entries in walk order:\n\n```ts\nimport { MeshQuery } from '@ai2070/net';\n\nconst query = MeshQuery.lineageEmit(\n  0xaan,\n  [\n    { originHash: 0xaan, depth: 0, tipSeq: 5n },\n    { originHash: 0xbbn, depth: 1, tipSeq: 3n },\n    { originHash: 0xccn, depth: 2 }, // tipSeq omitted -> emits seq=0n\n  ],\n  'back',\n);\n// Compose with .at / .between to fetch event bodies per chain.\n```\n\n### Errors\n\nEvery factory and runner method throws a plain `Error` whose\n`.message` carries a stable kind prefix on failure (planner /\nexecutor / invalid argument). The native binding pre-validates\nthe AST at construction time, so most errors surface at the\nfactory call rather than at `execute`.\n\n> **Note.** The `@ai2070/net-sdk` wrapper doesn't yet re-export\n> the MeshDB surface — import directly from `@ai2070/net` /\n> `@ai2070/net/meshdb`.\n\n## Compute (daemons + migration)\n\nRun `MeshDaemon`s directly from TypeScript. `DaemonRuntime` owns\nthe factory table, per-daemon hosts, and the\n`Registering → Ready → ShuttingDown` lifecycle gate that decides\nwhen inbound migrations may land. Daemons are plain JS objects\n(or class instances) whose `process(event)` returns an array of\noutput `Buffer`s — the runtime wraps each output in a causal link\nautomatically.\n\nBuild the `@ai2070/net` NAPI module with `--features compute`\n(auto-enabled in the default `local` bundle) to expose the\nsurface; everything below is re-exported from `@ai2070/net-sdk`.\nFull design notes:\n[`docs/SDK_COMPUTE_SURFACE_PLAN.md`](../docs/SDK_COMPUTE_SURFACE_PLAN.md).\n\n```typescript\nimport {\n  DaemonRuntime, DaemonError, Identity, MeshNode,\n  type CausalEvent, type MeshDaemon,\n} from '@ai2070/net-sdk';\n\n// 1. Build a mesh + runtime.\nconst mesh = await MeshNode.create({ bindAddr: '127.0.0.1:0', psk: '42'.repeat(32) });\nconst rt = DaemonRuntime.create(mesh);\n\n// 2. Register factories BEFORE flipping the runtime to Ready.\nrt.registerFactory('echo', (): MeshDaemon => ({\n  name: 'echo',\n  process: (event: CausalEvent) => [event.payload],\n  // optional: snapshot() / restore(state) for migration-capable daemons\n}));\n\n// 3. Ready the runtime — after this point spawns + migrations accept.\nawait rt.start();\n\n// 4. Spawn a daemon. `Identity` pins its ed25519 keypair so\n//    `originHash` / `entityId` stay stable across migrations.\nconst handle = await rt.spawn('echo', Identity.generate());\nconsole.log('origin =', handle.originHash.toString(16));\n\n// 5. Inspect / stop when done.\nconst stats = handle.stats();       // eventsProcessed / eventsEmitted / ...\nawait rt.stop(handle.originHash);\nawait rt.shutdown();\n```\n\n`MeshDaemon.process` is synchronous by contract — the NAPI TSFN\nbridge blocks the calling tokio task until it returns, so\nreturning a `Promise` will break event dispatch. Stateful daemons\nopt into migration by adding `snapshot(): Buffer | null` and\n`restore(state: Buffer): void`.\n\n### Migration\n\n`startMigration(origin, sourceNode, targetNode)` orchestrates the\nsix-phase cutover (`Snapshot → Transfer → Restore → Replay →\nCutover → Complete`). The source seals the daemon's seed into the\noutbound snapshot using the target's X25519 static pubkey; the\ntarget's factory for the same `kind` rebuilds the daemon, replays\nany events that arrived during transfer, then activates.\n\n```typescript\nimport { MigrationError } from '@ai2070/net-sdk';\n\ntry {\n  const mig = await rtA.startMigration(handle.originHash, nodeA, nodeB);\n  console.log('phase =', mig.phase);        // 'snapshot' | 'transfer' | ...\n  await mig.wait();                         // drive to completion\n} catch (e) {\n  if (e instanceof MigrationError) {\n    switch (e.kind) {\n      case 'not-ready':                 break; // target not started yet\n      case 'factory-not-found':         break; // target missing `kind`\n      case 'compute-not-supported':     break; // target has no DaemonRuntime\n      case 'state-failed':              break; // snapshot / restore threw\n      case 'identity-transport-failed': break; // seal / unseal failed\n      // ... see MigrationErrorKind for the full set\n    }\n  }\n}\n```\n\n`startMigrationWith(origin, src, dst, { sealSeed, ... })` exposes\nthe advanced knobs. On the target node, call\n`rt.registerMigrationTargetIdentity(identity)` before a migration\nlands — without it, the runtime rejects sealed-seed envelopes with\n`MigrationError.kind === 'identity-transport-failed'`.\n\n### Surface at a glance\n\n| Method | Description |\n|---|---|\n| `DaemonRuntime.create(mesh)` | Construct a runtime against an existing `MeshNode` |\n| `rt.registerFactory(kind, fn)` | Install a factory (must run before `start()`) |\n| `rt.start() / rt.shutdown()` | Flip the lifecycle gate |\n| `rt.spawn(kind, identity, cfg?)` | Spawn a local daemon |\n| `rt.spawnFromSnapshot(kind, identity, bytes, cfg?)` | Rehydrate from a snapshot |\n| `rt.stop(origin)` | Stop a local daemon |\n| `rt.snapshot(origin)` | Capture a `Buffer` for persistence / migration |\n| `rt.deliver(origin, event)` | Feed an event (returns output buffers) |\n| `rt.startMigration(origin, src, dst)` | Orchestrate a live migration |\n| `rt.registerMigrationTargetIdentity(id)` | Pin the unseal keypair on target nodes |\n| `handle.originHash` / `entityId` / `stats()` | Per-daemon identity + observability |\n| `DaemonError` / `MigrationError` | Typed catch classes (`instanceof` + `err.kind`) |\n\n## Groups (replica / fork / standby)\n\nHA / scaling overlays on top of `DaemonRuntime`. Build the NAPI\ncrate with `--features groups` (implies `compute`) to expose\n`ReplicaGroup`, `ForkGroup`, and `StandbyGroup`.\n\n- **ReplicaGroup** — N interchangeable copies with deterministic\n  identity per index; load-balances inbound events across healthy\n  members; auto-replaces on node failure.\n- **ForkGroup** — N independent daemons forked from a common parent\n  at `forkSeq`. Unique identities, shared ancestry via a verifiable\n  `ForkRecord`.\n- **StandbyGroup** — active-passive replication. One member processes\n  events; standbys hold snapshots via `sync()`. Most-synced standby\n  promotes on active failure and replays buffered events.\n\n```typescript\nimport {\n  DaemonRuntime, ForkGroup, GroupError, ReplicaGroup, StandbyGroup,\n} from '@ai2070/net-sdk';\n\nconst rt = await DaemonRuntime.create(mesh);\nrt.registerFactory('counter', () => new CounterDaemon());\n\n// ReplicaGroup — async because the factory round-trips through the\n// Node main thread (TSFN).\nconst replicas = await ReplicaGroup.spawn(rt, 'counter', {\n  replicaCount: 3,\n  groupSeed: Buffer.alloc(32, 0x11),\n  lbStrategy: 'consistent-hash',        // or 'round-robin' | 'least-load' | ...\n});\n\nconst origin = replicas.routeEvent({ routingKey: 'user:42' });\nawait rt.deliver(origin, event);\n\nawait replicas.scaleTo(5);               // grow\nawait replicas.onNodeFailure(failedNodeId);   // respawn elsewhere\n\n// ForkGroup\nconst forks = await ForkGroup.fork(rt, 'counter',\n  /* parentOrigin */ 0xabcdef01,\n  /* forkSeq     */ 42n,\n  { forkCount: 3, lbStrategy: 'round-robin' });\nconsole.log(forks.verifyLineage(), forks.forkRecords.length);\n\n// StandbyGroup — manual event buffering for replay on promotion.\nconst hot = await StandbyGroup.spawn(rt, 'counter', {\n  memberCount: 3,                        // 1 active + 2 standbys\n  groupSeed: Buffer.alloc(32, 0x77),\n});\nawait rt.deliver(hot.activeOrigin, event);\nhot.onEventDelivered(event);             // keep standbys' replay buffer accurate\nawait hot.sync();                        // periodic catchup\n// await hot.onNodeFailure(failedNodeId); // auto-promotes the most-synced standby\n```\n\n### Typed errors\n\nFailures surface as `GroupError` (a subclass of `DaemonError`) with\na stable `kind` discriminator parsed from the Rust side's\n`daemon: group: <kind>[: detail]` prefix:\n\n```typescript\nimport { GroupError } from '@ai2070/net-sdk';\n\ntry {\n  await ReplicaGroup.spawn(rt, 'never-registered', cfg);\n} catch (e) {\n  if (e instanceof GroupError) {\n    switch (e.kind) {\n      case 'not-ready':           break; // runtime.start() hasn't run\n      case 'factory-not-found':   break; // e.requestedKind tells you which\n      case 'no-healthy-member':   break; // routeEvent on an all-down group\n      case 'invalid-config':      break; // e.detail has the specifics\n      case 'placement-failed':    break;\n      case 'registry-failed':     break;\n    }\n  }\n}\n```\n\nFull staging, wire formats, and rationale:\n[`docs/SDK_GROUPS_SURFACE_PLAN.md`](../docs/SDK_GROUPS_SURFACE_PLAN.md).\nCore semantics (placement spread, health aggregation, failure\ndomains): [`../README.md#daemons`](../README.md#daemons).\n\n## API\n\n| Method | Description |\n|--------|-------------|\n| `NetNode.create(config)` | Create a new node |\n| `emit(obj)` | Emit a typed event |\n| `emitRaw(json)` | Emit a JSON string |\n| `emitBuffer(buf)` | Emit a Buffer (fastest) |\n| `emitBatch(objs)` | Batch emit |\n| `emitRawBatch(jsons)` | Batch emit strings |\n| `fire(json)` | Fire-and-forget |\n| `fireBatch(jsons)` | Fire-and-forget batch |\n| `poll(request)` | One-shot poll |\n| `pollOne()` | Poll a single event |\n| `subscribe(opts)` | Async iterable stream |\n| `subscribeTyped<T>(opts)` | Typed async iterable |\n| `channel<T>(name)` | Create a typed channel |\n| `stats()` | Ingestion statistics |\n| `shards()` | Number of active shards |\n| `flush()` | Flush pending batches |\n| `shutdown()` | Graceful shutdown |\n| `napi` | Access underlying NAPI binding |\n\n### CortEX surface\n\n| Entry point | Description |\n|---|---|\n| `new Redex({ persistentDir? })` | Local event-log manager |\n| `NetDb.open({ originHash, withTasks?, withMemories?, ... })` | Unified handle |\n| `NetDb.openFromSnapshot(config, bundle)` | Restore from `db.snapshot()` bundle |\n| `db.tasks` / `db.memories` | Typed adapter handles |\n| `TasksAdapter.open(redex, origin, opts?)` | Standalone tasks adapter |\n| `MemoriesAdapter.open(redex, origin, opts?)` | Standalone memories adapter |\n| `adapter.create/rename/complete/delete/...` | Domain CRUD |\n| `adapter.listTasks(filter?)` / `listMemories` | Sync snapshot query |\n| `adapter.watch(filter?)` | `Promise<AsyncIterable<T[]>>` over deduplicated fold results |\n| `adapter.snapshotAndWatch(filter?)` | `Promise<SnapshotAndWatch<T>>` — atomic paint+react |\n| `adapter.snapshot()` / `openFromSnapshot` | Model-level persistence |\n| `db.snapshot()` / `NetDb.openFromSnapshot` | Bundled multi-model persistence |\n| `redex.openFile(name, config?)` | Raw RedEX file — append-only log |\n| `file.append(buffer)` / `appendBatch(buffers)` | Append one / many payloads |\n| `file.readRange(start, end)` | Range read over retained entries |\n| `file.tail(fromSeq?)` | `AsyncIterable<RedexEvent>` |\n| `file.sync()` / `file.close()` | Explicit fsync / close |\n\n## Cargo features\n\n`@ai2070/net-sdk` wraps `@ai2070/net` (the napi-rs binding), so its reachable surface matches whatever Cargo features the underlying `.node` artifact was built with. The five feature flags relevant to building from source:\n\n| Feature | What it enables on the underlying `@ai2070/net` binding |\n|---|---|\n| `cortex` | `Redex`, `RedexFile`, `TasksAdapter`, `MemoriesAdapter`, `NetDb`, `Task`, `Memory`, watch iterators, `RedexError`, `CortexError`, `NetDbError` |\n| `redex-disk` | Disk-backed RedEX persistence — the `persistentDir` ctor option and `persistent: true` on `openFile`. Without it the persistent path rejects with `RedexError`. |\n| `netdb` | `NetDb` composition (requires `cortex`); the `net_netdb_*` FFI entry points ship with this feature. |\n| `meshdb` | `MeshQuery`, `MeshQueryRunner`, `MeshQueryStream`, `QueryBuilder`, `InMemoryChainReader`, plus the `libnet_meshdb` cdylib. |\n| `meshos` | `MeshOsDaemonSdk`, `MeshOsDaemonHandle`, plus the `libnet_meshos` cdylib. |\n\nA `.node` artifact built without a feature silently omits its symbols — there is no build warning. The TypeScript wrapper destructures the napi exports lazily, so a missing feature surfaces as `undefined` at the import site rather than a load-time error.\n\nEnable at build time (rebuild the underlying `@ai2070/net` artifact, then re-link / reinstall in the consumer):\n\n```bash\ncd net/crates/net/bindings/node\nnapi build --platform --release --features \"cortex netdb redex-disk meshdb meshos\"\n# The repo's `npm run build` script already passes a full feature\n# set; see `bindings/node/package.json` -> scripts.build for the\n# canonical list of flags shipped to npm.\n```\n\nPre-built npm artifacts ship with every feature enabled; the flags above only matter for source builds.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}