{"id":"nivalis9/secnet","name":"secnet","scope":"nivalis9","platform":"roblox","description":"Security-first networking","version":"1.2.0","latest":"1.2.0","versions":["1.0.0","1.1.0","1.2.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"fe69d0c52fb8a36451d7f32cc3aa20532b4371849612bbd8b03604addb9a7573","likes":0,"downloads":0,"install":"forest install nivalis9/secnet","url":"https://forest.dev/p/roblox/nivalis9/secnet","files":"https://api.forest.dev/ai/package/roblox/nivalis9/secnet/files","readme":"# SecNet\n\nSecNet is a small, server-authoritative Roblox networking module.\n\nIt keeps the API simple:\n\n- `SecNet.On(name, callback, options?)` and `SecNet.Send(...)` for events\n- `SecNet.SendBatch(...)` for high-throughput reliable event batches\n- `SecNet.Stream(name, options?)` for high-rate unreliable updates\n- `SecNet.Request(...)` and `SecNet.HandleRequest(...)` for request/response\n- `SecNet.Sync(...)`, `SecNet.OnSync(...)`, and `SecNet.GetSynced(...)` for server-to-client state\n\nIt does not try to make the client trustworthy. Instead, it makes the server boundary harder to abuse with payload limits, route validation, per-player and per-route rate limits, trust scoring, unknown-route rejection, blocked client-to-server sync, and request response sender checks.\n\nInternally, SecNet uses a compact positional wire format (`kind, name, requestId, success, ...`) instead of wrapping every send in packet/payload tables. That keeps the public API tiny while reducing hot-path allocations and serialization overhead.\n\n## Install\n\nUse one of these layouts:\n\n1. Put the module in `ReplicatedStorage` as `SecNet`.\n2. Use the included Rojo project, which maps `src` to `ReplicatedStorage.SecNet`.\n\nThe server creates `ReplicatedStorage._SecNetRuntime.Backbone` and `ReplicatedStorage._SecNetRuntime.Stream` automatically. Clients wait for those runtime remotes.\n\n## Quick Start\n\n### Server\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal SecNet = require(ReplicatedStorage:WaitForChild(\"SecNet\"))\n\nSecNet.On(\"Damage\", function(player, amount)\n\tprint(player.Name, \"requested damage:\", amount)\nend, {\n\tRateLimit = 12,\n\tValidate = function(player, amount)\n\t\treturn type(amount) == \"number\" and amount >= 0 and amount <= 50\n\tend,\n})\n\nSecNet.HandleRequest(\"GetCoins\", function(player)\n\treturn 500\nend)\n\ntask.spawn(function()\n\twhile true do\n\t\tSecNet.Sync(\"RoundTime\", workspace:GetServerTimeNow())\n\t\ttask.wait(1)\n\tend\nend)\n```\n\n### Client\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal SecNet = require(ReplicatedStorage:WaitForChild(\"SecNet\"))\n\nSecNet.Send(\"Damage\", 25)\n\nlocal coins, err = SecNet.Request(\"GetCoins\")\nif err then\n\twarn(\"request failed:\", err)\nelse\n\tprint(\"coins:\", coins)\nend\n\nSecNet.OnSync(\"RoundTime\", function(value)\n\tprint(\"round time:\", value)\nend)\n```\n\n## Streams\n\nStreams use `UnreliableRemoteEvent`, so they are for data where the newest packet matters more than guaranteed delivery: NPC snapshots, aim rays, cosmetic effects, sensor pings, and other high-rate state.\n\n```lua\n-- server\nlocal NPCs = SecNet.Stream(\"N\", { RateLimit = 20 })\n\ntask.spawn(function()\n\twhile true do\n\t\tlocal snapshot = SecNet.PackNPCSnapshots({\n\t\t\t{ Id = 1, Position = Vector3.new(10, 4, -2), Yaw = 1.2, State = 3 },\n\t\t\t{ Id = 2, Position = Vector3.new(14, 4, -8), Yaw = 2.1, State = 1 },\n\t\t})\n\n\t\tNPCs:SendAll(snapshot)\n\t\ttask.wait(1 / 20)\n\tend\nend)\n\n-- client\nSecNet.OnStream(\"N\", function(snapshot)\n\tSecNet.ReadNPCSnapshots(snapshot, function(id, position, yaw, state)\n\t\t-- apply latest visual state\n\tend)\nend)\n```\n\n`PackNPCSnapshots` stores each NPC in 11 bytes: `u16 id`, three quantized `i16` position axes, `u16 yaw`, and `u8 state`. At the default `MaxStreamBytes = 900`, one packet fits 81 NPCs. The default scale is `10`, meaning one decimal place of positional precision. Pass `{ Origin = someVector, Scale = 20 }` when you want a smaller local coordinate range or finer quantization.\n\n## Events\n\nShortcut API:\n\n```lua\nSecNet.On(\"Hit\", function(player, targetId)\n\tprint(player, targetId)\nend, {\n\tRateLimit = 20,\n\tValidate = function(player, targetId)\n\t\treturn type(targetId) == \"number\"\n\tend,\n})\n\n-- client -> server\nSecNet.Send(\"Hit\", 123)\n\n-- server -> one client\nSecNet.Send(player, \"HitConfirmed\", 123)\n\n-- server -> all clients\nSecNet.SendAll(\"RoundStarted\", 60)\n\n-- client -> server, one callback per record\nSecNet.SendBatch(\"HitMarker\", {\n\t{ Target = 101, Damage = 12 },\n\t{ Target = 102, Damage = 8 },\n})\n\n-- server, one callback for the whole batch\nSecNet.OnBatch(\"HitMarkerBulk\", function(player, records)\n\tfor _, record in ipairs(records) do\n\t\tprint(player, record.Target, record.Damage)\n\tend\nend)\n```\n\nObject API:\n\n```lua\nlocal Hit = SecNet.Event(\"Hit\", {\n\tRateLimit = 20,\n\tValidate = function(player, targetId)\n\t\treturn type(targetId) == \"number\"\n\tend,\n})\n\nHit:On(function(player, targetId)\n\tprint(player, targetId)\nend)\n\nHit:SetRateLimit(10)\nHit:SetValidator(function(player, targetId)\n\treturn type(targetId) == \"number\"\nend)\n```\n\nObject methods:\n\n- `event:Send(player, ...)` on the server\n- `event:Send(...)` on the client\n- `event:SendAll(...)` on the server\n- `event:SendMultiple(players, ...)` on the server\n- `event:SendBatch(player, records)` on the server\n- `event:SendBatch(records)` on the client\n- `event:SendBatchAll(records)` on the server\n- `event:On(callback)`\n- `event:OnBatch(callback)`\n- `event:SetBatchValidator(callback)`\n\nServer listeners receive `callback(player, ...)`. Client listeners receive `callback(...)`.\n\n`OnBatch` receives `callback(player, records)` on the server and `callback(records)` on the client. If a route has an `OnBatch` listener, `SendBatch` is delivered once as the whole records array; otherwise it falls back to one normal `On` callback per record.\n\n## Requests\n\nClient calling the server:\n\n```lua\n-- server\nSecNet.HandleRequest(\"PurchaseItem\", {\n\tRateLimit = 5,\n\tValidate = function(player, itemId)\n\t\treturn type(itemId) == \"string\" and #itemId <= 64\n\tend,\n}, function(player, itemId)\n\treturn true, \"Purchased\", itemId\nend)\n\n-- client\nlocal ok, message, itemId = SecNet.Request(\"PurchaseItem\", \"sword\")\n```\n\nServer calling a client:\n\n```lua\n-- client\nSecNet.HandleRequest(\"GetAimPosition\", function()\n\treturn workspace.CurrentCamera.CFrame.Position\nend)\n\n-- server\nlocal aimPosition, err = SecNet.Request(player, \"GetAimPosition\")\n```\n\nRules:\n\n- Client usage is `SecNet.Request(name, ...)`\n- Server usage is `SecNet.Request(player, name, ...)`\n- On success, response values are returned directly\n- On timeout or handler failure, SecNet returns `nil, errorMessage`\n- Server requests use unguessable request IDs and only accept responses from the requested player\n\n## Sync\n\nSync is server-to-client only. Use events or requests for client-to-server messages.\n\n```lua\n-- server\nSecNet.Sync(\"MatchState\", \"Lobby\")\nSecNet.Sync(player, \"Loadout\", { Primary = \"Rifle\" })\nSecNet.Sync({ playerA, playerB }, \"ZoneWarning\", true)\nSecNet.Sync(workspace.Flag, \"Owner\", player.UserId)\n\n-- client\nSecNet.OnSync(\"MatchState\", function(value, key)\n\tprint(key, value)\nend)\n\nSecNet.OnSync(workspace.Flag, \"Owner\", function(userId)\n\tprint(\"flag owner:\", userId)\nend)\n```\n\n## Config\n\nSet config before heavy traffic starts:\n\n```lua\nSecNet.Config.MaxPacketsPerSecond = 80\nSecNet.Config.MaxRoutePacketsPerSecond = 30\nSecNet.Config.MaxStreamBytes = 900\nSecNet.Config.MaxBatchEvents = 2048\nSecNet.Config.MaxCoalescedEvents = 256\nSecNet.Config.RequestTimeoutSeconds = 5\nSecNet.Config.CoalesceEvents = true\nSecNet.Config.AllowClientInstances = false\nSecNet.Config.ValidateServerPackets = false\n```\n\nCommon options:\n\n- `MaxPacketsPerSecond`\n- `MaxRoutePacketsPerSecond`\n- `BurstMultiplier`\n- `MaxPacketBytes`\n- `MaxStreamBytes`\n- `MaxBatchEvents`\n- `MaxCoalescedEvents`\n- `MaxStringBytes`\n- `MaxTableEntries`\n- `MaxTableDepth`\n- `MaxArguments`\n- `RequestTimeoutSeconds`\n- `CoalesceEvents`\n- `AllowClientInstances`\n- `RejectUnknownClientRoutes`\n- `ValidateServerPackets`\n- `ValidateOutbound`\n- `TrackBytes`\n\n`ValidateServerPackets`, `ValidateOutbound`, and `TrackBytes` default to `false` for reliable events/requests. Client-origin traffic is still validated on the server; `ValidateServerPackets` only enables extra client-side validation for server-origin packets. Stream sends always enforce `MaxStreamBytes` so oversized unreliable packets do not silently disappear.\n\n## Performance Notes\n\nSecNet is optimized for simple runtime ergonomics: no IDL, no generated files, and no packet tables on the wire. For tiny messages, this keeps CPU overhead very low.\n\n`Send` auto-coalesces same-frame reliable events by default, so a burst of logical sends can flush as a few argument-batch packets. Client-origin sends still go through unknown-route checks, rate limits, payload validation, and route validators on the server. Set a route option `{ Coalesce = false }` or `{ Immediate = true }` only for latency-critical control messages.\n\nFor bandwidth-heavy state, use streams with a single buffer payload. The NPC snapshot helper is the intended \"extreme but still simple\" path: one unreliable packet can carry dozens of NPCs with predictable byte cost and almost no Lua allocation.\n\nFor reliable floods, use `SendBatch` instead of thousands of `Send` calls in the same frame. Add `OnBatch` for the hottest routes so the receiver handles the whole chunk in one callback. Without `OnBatch`, each batch record is delivered to the normal `On` listener as one payload.\n\nFor fast client-to-server batches, prefer a route `ValidateBatch` option that checks the whole array's schema once. Without `ValidateBatch`, SecNet keeps the safer generic per-record validation path.\n\nServer-to-client packets skip the deep generic validator by default because that does not protect the server and costs a lot in hot paths. Turn on `ValidateServerPackets` only when debugging server payload shape issues.\n\nZap can still win broad typed workloads because it generates route-specific serializers and packs data into buffers. SecNet does not try to be a general codegen serializer; it gives you a tiny manual buffer path for the hottest updates.\n\n## Security Notes\n\nNo Roblox network wrapper can make client input inherently safe. Treat every client event/request as a suggestion and validate game rules on the server.\n\nSecNet helps by default:\n\n- Unknown client routes are rejected\n- Unknown client streams are rejected\n- Client-to-server sync packets are rejected\n- Client-sent `Instance` values are rejected unless `AllowClientInstances` is enabled\n- Payloads are capped by argument count, string size, table size, table depth, and estimated byte size\n- Per-player and per-route token buckets limit spam\n- Repeated malformed traffic lowers trust and can throttle or block a player\n- Server-to-client request responses must come from the specific requested player\n\n## Stats\n\n```lua\nlocal stats = SecNet.GetStats()\nlocal score, level = SecNet.GetTrust(player)\n```\n\n`SecNet.GetProfilerSnapshot()` is kept as a compatibility alias for `SecNet.GetStats()`.\n\n## Tests\n\nThe Rojo project maps the integration suite into Studio:\n\n- `ServerScriptService.SecNetServerTestRunner`\n- `StarterPlayer.StarterPlayerScripts.SecNetClientTestRunner`\n\nRun Play or Start Server with at least one client. Passing output includes:\n\n```text\n[SecNetTests] Client runner completed.\n[SecNetTests] All integration tests passed.\n```\n\nThe project also includes a SecNet vs BridgeNet2 speed benchmark:\n\n- `ServerScriptService.NetworkSpeedBenchmarkServer`\n- `StarterPlayer.StarterPlayerScripts.NetworkSpeedBenchmarkClient`\n\nBenchmark output is prefixed with `[SecNetBench]`.\n","readmeTruncated":false}