@axpecter/lync 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +300 -0
  2. package/package.json +38 -0
  3. package/src/Types.luau +63 -0
  4. package/src/api/Group.luau +126 -0
  5. package/src/api/Namespace.luau +226 -0
  6. package/src/api/Packet.luau +147 -0
  7. package/src/api/Query.luau +295 -0
  8. package/src/api/Signal.luau +224 -0
  9. package/src/codec/Base.luau +49 -0
  10. package/src/codec/composite/Array.luau +275 -0
  11. package/src/codec/composite/Map.luau +395 -0
  12. package/src/codec/composite/Optional.luau +47 -0
  13. package/src/codec/composite/Shared.luau +151 -0
  14. package/src/codec/composite/Struct.luau +440 -0
  15. package/src/codec/composite/Tagged.luau +222 -0
  16. package/src/codec/composite/Tuple.luau +143 -0
  17. package/src/codec/datatype/Buffer.luau +44 -0
  18. package/src/codec/datatype/CFrame.luau +51 -0
  19. package/src/codec/datatype/Color.luau +22 -0
  20. package/src/codec/datatype/Instance.luau +48 -0
  21. package/src/codec/datatype/IntVector.luau +25 -0
  22. package/src/codec/datatype/NumberRange.luau +14 -0
  23. package/src/codec/datatype/Ray.luau +27 -0
  24. package/src/codec/datatype/Rect.luau +21 -0
  25. package/src/codec/datatype/Region.luau +58 -0
  26. package/src/codec/datatype/Sequence.luau +129 -0
  27. package/src/codec/datatype/String.luau +87 -0
  28. package/src/codec/datatype/UDim.luau +27 -0
  29. package/src/codec/datatype/Vector.luau +25 -0
  30. package/src/codec/meta/Auto.luau +353 -0
  31. package/src/codec/meta/Bitfield.luau +191 -0
  32. package/src/codec/meta/Custom.luau +27 -0
  33. package/src/codec/meta/Enum.luau +80 -0
  34. package/src/codec/meta/Nothing.luau +9 -0
  35. package/src/codec/meta/Quantized.luau +170 -0
  36. package/src/codec/meta/Unknown.luau +35 -0
  37. package/src/codec/primitive/Bool.luau +30 -0
  38. package/src/codec/primitive/Float16.luau +111 -0
  39. package/src/codec/primitive/Number.luau +48 -0
  40. package/src/codec/primitive/Varint.luau +76 -0
  41. package/src/index.d.ts +279 -0
  42. package/src/init.luau +161 -0
  43. package/src/internal/Baseline.luau +41 -0
  44. package/src/internal/Channel.luau +235 -0
  45. package/src/internal/Middleware.luau +109 -0
  46. package/src/internal/Pool.luau +68 -0
  47. package/src/internal/Registry.luau +146 -0
  48. package/src/transport/Bridge.luau +66 -0
  49. package/src/transport/Client.luau +151 -0
  50. package/src/transport/Gate.luau +222 -0
  51. package/src/transport/Reader.luau +175 -0
  52. package/src/transport/Server.luau +364 -0
@@ -0,0 +1,151 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Client-side transport with batching, frame XOR on reliable.
4
+
5
+ local RunService = game:GetService ("RunService")
6
+
7
+ local Bridge = require (script.Parent.Bridge)
8
+ local Channel = require (script.Parent.Parent.internal.Channel)
9
+ local Middleware = require (script.Parent.Parent.internal.Middleware)
10
+ local Reader = require (script.Parent.Reader)
11
+ local Types = require (script.Parent.Parent.Types)
12
+
13
+ type ChannelState = Types.ChannelState
14
+ type Codec<T> = Types.Codec<T>
15
+
16
+ -- State ------------------------------------------------------------------
17
+
18
+ local NOT_STARTED = "[Lync] Client not started. Call Lync.start() before sending"
19
+ local _reliable = nil :: ChannelState?
20
+ local _unreliable = nil :: ChannelState?
21
+ local _prevRecv = nil :: buffer?
22
+ local _isStarted = false
23
+
24
+ -- Private ----------------------------------------------------------------
25
+
26
+ local function receive (data: any, refs: any, applyXor: boolean): ()
27
+ local buf = Reader.decodeIncoming (data)
28
+ if not buf then
29
+ return
30
+ end
31
+
32
+ local raw: buffer
33
+ if applyXor then
34
+ raw = Channel.xorApply (buf, _prevRecv)
35
+ _prevRecv = raw
36
+ else
37
+ raw = buf
38
+ end
39
+
40
+ local safeRefs: { Instance }? = if typeof (refs) == "table" then refs else nil
41
+ local ok, err = pcall (Reader.process, raw, safeRefs, nil, 0)
42
+ if not ok then
43
+ warn (`[Lync] Read failed: {err}`)
44
+ end
45
+ end
46
+
47
+ local function flushChannel (remote: any, ch: ChannelState, applyXor: boolean): ()
48
+ if ch.cursor == 0 then
49
+ return
50
+ end
51
+
52
+ local raw, refs = Channel.sealAndDump (ch)
53
+
54
+ if applyXor then
55
+ local xored = Channel.xorApply (raw, ch.prevDump)
56
+ ch.prevDump = raw
57
+ remote:FireServer (xored, refs)
58
+ else
59
+ remote:FireServer (raw, refs)
60
+ end
61
+
62
+ Channel.reset (ch)
63
+ end
64
+
65
+ local function flush (): ()
66
+ flushChannel (Bridge.reliable, _reliable :: ChannelState, true)
67
+ flushChannel (Bridge.unreliable, _unreliable :: ChannelState, false)
68
+ end
69
+
70
+ -- Public -----------------------------------------------------------------
71
+
72
+ local Client = {}
73
+
74
+ function Client.start (): ()
75
+ if _isStarted then
76
+ return
77
+ end
78
+ _isStarted = true
79
+
80
+ Bridge.setup ()
81
+
82
+ _reliable = Channel.create ()
83
+ _unreliable = Channel.create ()
84
+
85
+ Bridge.reliable.OnClientEvent:Connect (function (data: any, refs: any): ()
86
+ receive (data, refs, true)
87
+ end)
88
+ Bridge.unreliable.OnClientEvent:Connect (function (data: any, refs: any): ()
89
+ receive (data, refs, false)
90
+ end)
91
+ RunService.Heartbeat:Connect (flush)
92
+ end
93
+
94
+ function Client.write (
95
+ id: number,
96
+ name: string,
97
+ codec: Codec<any>,
98
+ data: any,
99
+ isUnreliable: boolean
100
+ ): ()
101
+ if not _isStarted then
102
+ error (NOT_STARTED)
103
+ end
104
+
105
+ local final: any
106
+ if Middleware.hasSend then
107
+ final = Middleware.runSend (data, name, nil)
108
+ if final == nil then
109
+ return
110
+ end
111
+ else
112
+ final = data
113
+ end
114
+
115
+ local ch = if isUnreliable then _unreliable :: ChannelState else _reliable :: ChannelState
116
+ Channel.writeBatch (ch, id, name, codec, final)
117
+ end
118
+
119
+ -- Shared for both query requests and responses (identical wire format).
120
+ function Client.writeQuery (
121
+ id: number,
122
+ name: string,
123
+ correlationId: number,
124
+ codec: Codec<any>,
125
+ data: any
126
+ ): ()
127
+ if not _isStarted then
128
+ error (NOT_STARTED)
129
+ end
130
+
131
+ local final: any
132
+ if Middleware.hasSend then
133
+ final = Middleware.runSend (data, name, nil)
134
+ if final == nil then
135
+ return
136
+ end
137
+ else
138
+ final = data
139
+ end
140
+
141
+ Channel.writeQuery (_reliable :: ChannelState, id, name, correlationId, codec, final)
142
+ end
143
+
144
+ function Client.writeQueryNil (id: number, name: string, correlationId: number): ()
145
+ if not _isStarted then
146
+ error (NOT_STARTED)
147
+ end
148
+ Channel.writeQuery (_reliable :: ChannelState, id, name, correlationId, nil, nil)
149
+ end
150
+
151
+ return table.freeze (Client)
@@ -0,0 +1,222 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Post-deserialization security: NaN/inf, rate limiting, custom validate.
4
+
5
+ local Types = require (script.Parent.Parent.Types)
6
+
7
+ type Registration = Types.Registration
8
+ type RateLimitConfig = Types.RateLimitConfig
9
+
10
+ -- Constants --------------------------------------------------------------
11
+
12
+ local DEFAULT_MAX_DEPTH = 16
13
+
14
+ local isfinite = math.isfinite
15
+ local min = math.min
16
+ local clock = os.clock
17
+
18
+ -- State ------------------------------------------------------------------
19
+
20
+ type Bucket = {
21
+ tokens: number,
22
+ lastRef: number,
23
+ }
24
+
25
+ local _maxDepth = DEFAULT_MAX_DEPTH
26
+ local _buckets = {} :: { [Player]: { [number]: Bucket } }
27
+ local _dropHandlers =
28
+ {} :: { (player: Player, reason: string, packetName: string, data: any?) -> () }
29
+
30
+ -- Private ----------------------------------------------------------------
31
+
32
+ local function fireDrop (player: Player, reason: string, packetName: string, data: any?): ()
33
+ for _, handler in _dropHandlers do
34
+ handler (player, reason, packetName, data)
35
+ end
36
+ end
37
+
38
+ @native
39
+ local function isClean (value: any, depth: number): boolean
40
+ if depth > _maxDepth then
41
+ return false
42
+ end
43
+
44
+ local t = type (value)
45
+
46
+ if t == "number" then
47
+ return isfinite (value)
48
+ end
49
+
50
+ -- String, boolean, nil are always clean . Exit before table/vector/userdata checks
51
+ if t == "string" or t == "boolean" or t == "nil" then
52
+ return true
53
+ end
54
+
55
+ if t == "table" then
56
+ local deeper = depth + 1
57
+ for k, v in value do
58
+ if not isClean (k, deeper) or not isClean (v, deeper) then
59
+ return false
60
+ end
61
+ end
62
+ return true
63
+ end
64
+
65
+ if t == "vector" then
66
+ return isfinite (value.X) and isfinite (value.Y) and isfinite (value.Z)
67
+ end
68
+
69
+ if t == "userdata" then
70
+ local ut = typeof (value)
71
+
72
+ if ut == "Vector2" then
73
+ return isfinite (value.X) and isfinite (value.Y)
74
+ elseif ut == "Color3" then
75
+ return isfinite (value.R) and isfinite (value.G) and isfinite (value.B)
76
+ elseif ut == "CFrame" then
77
+ local position = value.Position
78
+ if
79
+ not isfinite (position.X)
80
+ or not isfinite (position.Y)
81
+ or not isfinite (position.Z)
82
+ then
83
+ return false
84
+ end
85
+ local axis, angle = value:ToAxisAngle ()
86
+ return isfinite (angle)
87
+ and isfinite (axis.X)
88
+ and isfinite (axis.Y)
89
+ and isfinite (axis.Z)
90
+ elseif ut == "UDim" then
91
+ return isfinite (value.Scale)
92
+ elseif ut == "UDim2" then
93
+ return isfinite (value.X.Scale) and isfinite (value.Y.Scale)
94
+ elseif ut == "NumberRange" then
95
+ return isfinite (value.Min) and isfinite (value.Max)
96
+ elseif ut == "Rect" then
97
+ return isfinite (value.Min.X)
98
+ and isfinite (value.Min.Y)
99
+ and isfinite (value.Max.X)
100
+ and isfinite (value.Max.Y)
101
+ elseif ut == "Ray" then
102
+ local origin = value.Origin
103
+ local direction = value.Direction
104
+ return isfinite (origin.X)
105
+ and isfinite (origin.Y)
106
+ and isfinite (origin.Z)
107
+ and isfinite (direction.X)
108
+ and isfinite (direction.Y)
109
+ and isfinite (direction.Z)
110
+ elseif ut == "Region3" then
111
+ local position = value.CFrame.Position
112
+ return isfinite (position.X) and isfinite (position.Y) and isfinite (position.Z)
113
+ end
114
+ end
115
+
116
+ return true
117
+ end
118
+
119
+ local function checkRate (player: Player, reg: Registration): boolean
120
+ local cfg = reg.rateLimit
121
+ if not cfg then
122
+ return true
123
+ end
124
+
125
+ local playerBuckets = _buckets[player]
126
+ if not playerBuckets then
127
+ return true
128
+ end
129
+
130
+ local id = reg.id
131
+ local now = clock ()
132
+ local cap = cfg.burstAllowance or cfg.maxPerSecond
133
+ local bucket = playerBuckets[id]
134
+
135
+ if not bucket then
136
+ playerBuckets[id] = {
137
+ tokens = cap - 1,
138
+ lastRef = now,
139
+ }
140
+ return true
141
+ end
142
+
143
+ local elapsed = now - bucket.lastRef
144
+ bucket.tokens = min (cap, bucket.tokens + elapsed * cfg.maxPerSecond)
145
+ bucket.lastRef = now
146
+
147
+ if bucket.tokens >= 1 then
148
+ bucket.tokens -= 1
149
+ return true
150
+ end
151
+
152
+ return false
153
+ end
154
+
155
+ -- Public -----------------------------------------------------------------
156
+
157
+ local Gate = {}
158
+
159
+ -- Override the max recursion depth for NaN/inf scanning. Default 16. Range: 4–32.
160
+ function Gate.setMaxDepth (depth: number): ()
161
+ if depth < 4 or depth > 32 then
162
+ error (`[Lync] Gate max depth must be 4–32, got {depth}`)
163
+ end
164
+ _maxDepth = depth
165
+ end
166
+
167
+ function Gate.getMaxDepth (): number
168
+ return _maxDepth
169
+ end
170
+
171
+ function Gate.check (value: any, reg: Registration, player: Player): boolean
172
+ if not checkRate (player, reg) then
173
+ fireDrop (player, "rate", reg.name, nil)
174
+ return false
175
+ end
176
+
177
+ if not isClean (value, 1) then
178
+ fireDrop (player, "nan", reg.name, value)
179
+ return false
180
+ end
181
+
182
+ local validate = reg.validate
183
+ if validate then
184
+ local ok, reason = validate (value, player)
185
+ if not ok then
186
+ fireDrop (player, reason or "validate", reg.name, value)
187
+ return false
188
+ end
189
+ end
190
+
191
+ return true
192
+ end
193
+
194
+ function Gate.onDrop (
195
+ callback: (player: Player, reason: string, packetName: string, data: any?) -> ()
196
+ ): () -> ()
197
+ table.insert (_dropHandlers, callback)
198
+
199
+ return function (): ()
200
+ local idx = table.find (_dropHandlers, callback)
201
+ if idx then
202
+ table.remove (_dropHandlers, idx)
203
+ end
204
+ end
205
+ end
206
+
207
+ -- Fires drop handlers without validation. Used by Reader for byte-budget violations.
208
+ function Gate.reportDrop (player: Player?, reason: string, packetName: string, data: any?): ()
209
+ if player and #_dropHandlers > 0 then
210
+ fireDrop (player, reason, packetName, data)
211
+ end
212
+ end
213
+
214
+ function Gate.onPlayerAdded (player: Player): ()
215
+ _buckets[player] = {}
216
+ end
217
+
218
+ function Gate.onPlayerRemoved (player: Player): ()
219
+ _buckets[player] = nil
220
+ end
221
+
222
+ return table.freeze (Gate)
@@ -0,0 +1,175 @@
1
+ --!strict
2
+ --!optimize 2
3
+ -- Deserializes incoming buffers and routes by registration kind.
4
+
5
+ local Baseline = require (script.Parent.Parent.internal.Baseline)
6
+ local Gate = require (script.Parent.Gate)
7
+ local Middleware = require (script.Parent.Parent.internal.Middleware)
8
+ local Registry = require (script.Parent.Parent.internal.Registry)
9
+
10
+ local registryGet = Registry.get
11
+ local gateCheck = Gate.check
12
+ local gateReportDrop = Gate.reportDrop
13
+
14
+ -- Constants --------------------------------------------------------------
15
+
16
+ local KIND_PACKET = Registry.KIND_PACKET
17
+ local KIND_REQUEST = Registry.KIND_REQUEST
18
+
19
+ local MAX_INCOMING = 65536
20
+
21
+ -- Public -----------------------------------------------------------------
22
+
23
+ local Reader = {}
24
+
25
+ -- Shared incoming-data validation used by both Client and Server receive.
26
+ function Reader.decodeIncoming (data: any): buffer?
27
+ if typeof (data) ~= "buffer" then
28
+ return nil
29
+ end
30
+ if buffer.len (data) > MAX_INCOMING then
31
+ return nil
32
+ end
33
+ return data
34
+ end
35
+
36
+ -- Callers wrap this in pcall. Any throw aborts the entire buffer from this source.
37
+ function Reader.process (
38
+ incoming: buffer,
39
+ refs: { Instance }?,
40
+ player: Player?,
41
+ startOffset: number
42
+ ): ()
43
+ local length = buffer.len (incoming)
44
+ local pos = startOffset
45
+
46
+ if Baseline.hasDelta then
47
+ Baseline.setReadKey (player or false)
48
+ end
49
+
50
+ -- Cache flags once per frame-buffer, not per packet
51
+ local hasReceiveMiddleware = Middleware.hasReceive
52
+ local isServer = player ~= nil
53
+
54
+ while pos < length do
55
+ local id = buffer.readu8 (incoming, pos)
56
+ pos += 1
57
+
58
+ local reg = registryGet (id)
59
+ if not reg then
60
+ warn (`[Lync] Unknown packet ID {id}`)
61
+ break
62
+ end
63
+
64
+ local kind = reg.kind
65
+
66
+ if kind == KIND_PACKET then
67
+ if pos + 2 > length then
68
+ break
69
+ end
70
+
71
+ local count = buffer.readu16 (incoming, pos)
72
+ pos += 2
73
+
74
+ if count == 0 then
75
+ continue
76
+ end
77
+
78
+ local codec = reg.codec
79
+ local signal = reg.signal
80
+ local batchStart = pos
81
+ local maxBytes = reg.maxPayloadBytes
82
+
83
+ -- Hot path: no gate, no middleware, no byte budget
84
+ if not (isServer and reg.needsGate) and not hasReceiveMiddleware and not maxBytes then
85
+ for _ = 1, count do
86
+ if pos >= length then
87
+ break
88
+ end
89
+
90
+ local value, consumed = codec.read (incoming, pos, refs)
91
+ pos += consumed
92
+ signal:fire (value, player)
93
+ end
94
+ else
95
+ -- Cold path: gate and/or middleware and/or byte budget active
96
+ local baseName = reg.baseName
97
+ local needsGate = isServer and reg.needsGate
98
+
99
+ for _ = 1, count do
100
+ if pos >= length then
101
+ break
102
+ end
103
+
104
+ if maxBytes and (pos - batchStart) > maxBytes then
105
+ gateReportDrop (player, "size", reg.name, nil)
106
+ break
107
+ end
108
+
109
+ local value, consumed = codec.read (incoming, pos, refs)
110
+ pos += consumed
111
+
112
+ if needsGate and not gateCheck (value, reg, player :: Player) then
113
+ continue
114
+ end
115
+
116
+ if hasReceiveMiddleware then
117
+ local final = Middleware.runReceive (value, baseName, player)
118
+ if final == nil then
119
+ continue
120
+ end
121
+ signal:fire (final, player)
122
+ else
123
+ signal:fire (value, player)
124
+ end
125
+ end
126
+ end
127
+ else
128
+ -- Query frame: id(u8, already read) + correlationId(u16) + status(u8) = 3 more bytes
129
+ if pos + 2 >= length then
130
+ break
131
+ end
132
+
133
+ local queryCodec = reg.codec
134
+ local querySignal = reg.signal
135
+
136
+ local correlationId = buffer.readu16 (incoming, pos)
137
+ local status = buffer.readu8 (incoming, pos + 2)
138
+ pos += 3
139
+
140
+ if status == 1 then
141
+ if kind == KIND_REQUEST then
142
+ querySignal:fire (nil, player, correlationId)
143
+ else
144
+ querySignal:fire (nil, correlationId)
145
+ end
146
+ continue
147
+ end
148
+
149
+ local value, consumed = queryCodec.read (incoming, pos, refs)
150
+ pos += consumed
151
+
152
+ if isServer and reg.needsGate and not gateCheck (value, reg, player :: Player) then
153
+ continue
154
+ end
155
+
156
+ local final: any
157
+ if hasReceiveMiddleware then
158
+ final = Middleware.runReceive (value, reg.baseName, player)
159
+ if final == nil then
160
+ continue
161
+ end
162
+ else
163
+ final = value
164
+ end
165
+
166
+ if kind == KIND_REQUEST then
167
+ querySignal:fire (final, player, correlationId)
168
+ else
169
+ querySignal:fire (final, correlationId)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ return table.freeze (Reader)