@axpecter/lync 1.5.2 → 2.1.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 (55) hide show
  1. package/README.md +499 -357
  2. package/package.json +2 -2
  3. package/src/Types.luau +69 -27
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +191 -157
  6. package/src/api/Query.luau +223 -282
  7. package/src/api/Scope.luau +46 -57
  8. package/src/api/Signal.luau +104 -137
  9. package/src/codec/Base.luau +69 -20
  10. package/src/codec/composite/Array.luau +189 -201
  11. package/src/codec/composite/Map.luau +97 -341
  12. package/src/codec/composite/Optional.luau +27 -23
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +201 -339
  15. package/src/codec/composite/Tagged.luau +64 -178
  16. package/src/codec/composite/Tuple.luau +81 -95
  17. package/src/codec/datatype/Buffer.luau +49 -21
  18. package/src/codec/datatype/CFrame.luau +207 -30
  19. package/src/codec/datatype/Color.luau +21 -11
  20. package/src/codec/datatype/Instance.luau +29 -19
  21. package/src/codec/datatype/IntVector.luau +33 -21
  22. package/src/codec/datatype/NumberRange.luau +19 -10
  23. package/src/codec/datatype/Ray.luau +26 -22
  24. package/src/codec/datatype/Rect.luau +20 -16
  25. package/src/codec/datatype/Region.luau +51 -45
  26. package/src/codec/datatype/Sequence.luau +64 -56
  27. package/src/codec/datatype/String.luau +89 -56
  28. package/src/codec/datatype/UDim.luau +40 -22
  29. package/src/codec/datatype/Vector.luau +181 -17
  30. package/src/codec/meta/Auto.luau +295 -253
  31. package/src/codec/meta/Bitfield.luau +104 -148
  32. package/src/codec/meta/Custom.luau +8 -4
  33. package/src/codec/meta/Enum.luau +29 -57
  34. package/src/codec/meta/Float.luau +64 -0
  35. package/src/codec/meta/Nothing.luau +9 -5
  36. package/src/codec/meta/Unknown.luau +44 -35
  37. package/src/codec/primitive/Bool.luau +16 -26
  38. package/src/codec/primitive/Float16.luau +58 -64
  39. package/src/codec/primitive/Int.luau +68 -0
  40. package/src/codec/primitive/Number.luau +21 -43
  41. package/src/codec/primitive/Varint.luau +67 -46
  42. package/src/index.d.ts +249 -306
  43. package/src/init.luau +332 -173
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +346 -161
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +61 -101
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +60 -117
  51. package/src/transport/Gate.luau +282 -133
  52. package/src/transport/Reader.luau +159 -100
  53. package/src/transport/Server.luau +147 -292
  54. package/src/api/Namespace.luau +0 -251
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,214 +1,363 @@
1
1
  --!strict
2
2
  --!native
3
- -- Post-deserialization security: NaN/inf, rate limiting, custom validate.
3
+ -- Schema-aware validation and rate limiting.
4
4
 
5
+ local Middleware = require(script.Parent.Parent.internal.Middleware)
6
+ local Registry = require(script.Parent.Parent.internal.Registry)
5
7
  local Types = require(script.Parent.Parent.Types)
6
8
 
7
- type Registration = Types.Registration
8
- type RateLimitConfig = Types.RateLimitConfig
9
-
10
9
  -- Constants -----------------------------------------------------------
11
10
 
12
- local DEFAULT_MAX_DEPTH = 16
13
-
14
- local isfinite = math.isfinite
15
- local min = math.min
16
- local clock = os.clock
11
+ local DEFAULT_DEPTH = 16
17
12
 
18
13
  -- State ---------------------------------------------------------------
19
14
 
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?) -> () }
15
+ local _depth = DEFAULT_DEPTH
16
+ local _rateLimits: { [number]: { [Player]: Types.RateLimitState } } = {}
17
+ local _globalLimits: { [Player]: Types.RateLimitState } = {}
18
+ local _globalMaxPerSecond: number? = nil
19
+ local _bandwidthSoftLimit: number? = nil
20
+ local _bandwidthMaxStrikes: number? = nil
21
+ local _bandwidthStrikes: { [Player]: number } = {}
29
22
 
30
23
  -- Private -------------------------------------------------------------
31
24
 
32
- local function fireDrop(player: Player, reason: string, packetName: string, data: any?): ()
33
- for _, handler in _dropHandlers do
34
- handler(player, reason, packetName, data)
25
+ local isfinite = math.isfinite
26
+ local floor = math.floor
27
+ local abs = math.abs
28
+ local clock = os.clock
29
+
30
+ --[[
31
+ Checks a number value for NaN/inf and optionally range + integrality.
32
+ Returns true if the value passes all checks.
33
+ ]]
34
+ local function validateNumber(
35
+ value: number,
36
+ isInteger: boolean?,
37
+ minVal: number?,
38
+ maxVal: number?
39
+ ): boolean
40
+ if not isfinite(value) then
41
+ return false
35
42
  end
36
- end
37
43
 
38
- local function isClean(value: any, depth: number): boolean
39
- if depth > _maxDepth then
44
+ if isInteger and floor(value) ~= value then
45
+ return false
46
+ end
47
+
48
+ if minVal and value < minVal then
49
+ return false
50
+ end
51
+ if maxVal and value > maxVal then
40
52
  return false
41
53
  end
42
54
 
43
- local t = type(value)
55
+ return true
56
+ end
57
+
58
+ --[[
59
+ Recursive NaN/inf scan for values without schema metadata.
60
+ Used as fallback for f32, f64, auto, and Roblox types.
61
+ ]]
62
+ local function scanValue(value: any, depth: number): boolean
63
+ if depth <= 0 then
64
+ return true
65
+ end
66
+
67
+ local t = typeof(value)
44
68
 
45
69
  if t == "number" then
46
70
  return isfinite(value)
47
71
  end
48
72
 
49
- -- String, boolean, nil are always clean. Exit before table/vector/userdata checks.
50
- if t == "string" or t == "boolean" or t == "nil" then
51
- return true
73
+ if t == "Vector2" then
74
+ return isfinite(value.X) and isfinite(value.Y)
52
75
  end
53
76
 
54
- if t == "table" then
55
- local deeper = depth + 1
56
- for k, v in value do
57
- if not isClean(k, deeper) or not isClean(v, deeper) then
58
- return false
59
- end
77
+ if t == "Vector3" then
78
+ return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
79
+ end
80
+
81
+ if t == "CFrame" then
82
+ local pos = value.Position
83
+ if not isfinite(pos.X) or not isfinite(pos.Y) or not isfinite(pos.Z) then
84
+ return false
60
85
  end
61
- return true
86
+ local rx, ry, rz = value:ToEulerAnglesXYZ()
87
+ return isfinite(rx) and isfinite(ry) and isfinite(rz)
62
88
  end
63
89
 
64
- if t == "vector" then
65
- return isfinite(value.X) and isfinite(value.Y) and isfinite(value.Z)
90
+ if t == "UDim" then
91
+ return isfinite(value.Scale) and isfinite(value.Offset)
92
+ end
93
+
94
+ if t == "UDim2" then
95
+ return isfinite(value.X.Scale)
96
+ and isfinite(value.X.Offset)
97
+ and isfinite(value.Y.Scale)
98
+ and isfinite(value.Y.Offset)
99
+ end
100
+
101
+ if t == "Color3" then
102
+ return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
66
103
  end
67
104
 
68
- if t == "userdata" then
69
- local ut = typeof(value)
105
+ if t == "NumberRange" then
106
+ return isfinite(value.Min) and isfinite(value.Max)
107
+ end
108
+
109
+ if t == "Rect" then
110
+ return isfinite(value.Min.X)
111
+ and isfinite(value.Min.Y)
112
+ and isfinite(value.Max.X)
113
+ and isfinite(value.Max.Y)
114
+ end
70
115
 
71
- if ut == "Vector2" then
72
- return isfinite(value.X) and isfinite(value.Y)
73
- elseif ut == "Color3" then
74
- return isfinite(value.R) and isfinite(value.G) and isfinite(value.B)
75
- elseif ut == "CFrame" then
76
- local position = value.Position
77
- if not isfinite(position.X) or not isfinite(position.Y) or not isfinite(position.Z) then
116
+ if t == "Ray" then
117
+ local o = value.Origin
118
+ local d = value.Direction
119
+ return isfinite(o.X)
120
+ and isfinite(o.Y)
121
+ and isfinite(o.Z)
122
+ and isfinite(d.X)
123
+ and isfinite(d.Y)
124
+ and isfinite(d.Z)
125
+ end
126
+
127
+ if t == "table" then
128
+ local nextDepth = depth - 1
129
+ for _, v in value do
130
+ if not scanValue(v, nextDepth) then
78
131
  return false
79
132
  end
80
- local axis, angle = value:ToAxisAngle()
81
- return isfinite(angle) and isfinite(axis.X) and isfinite(axis.Y) and isfinite(axis.Z)
82
- elseif ut == "UDim" then
83
- return isfinite(value.Scale)
84
- elseif ut == "UDim2" then
85
- return isfinite(value.X.Scale) and isfinite(value.Y.Scale)
86
- elseif ut == "NumberRange" then
87
- return isfinite(value.Min) and isfinite(value.Max)
88
- elseif ut == "Rect" then
89
- return isfinite(value.Min.X)
90
- and isfinite(value.Min.Y)
91
- and isfinite(value.Max.X)
92
- and isfinite(value.Max.Y)
93
- elseif ut == "Ray" then
94
- local origin = value.Origin
95
- local direction = value.Direction
96
- return isfinite(origin.X)
97
- and isfinite(origin.Y)
98
- and isfinite(origin.Z)
99
- and isfinite(direction.X)
100
- and isfinite(direction.Y)
101
- and isfinite(direction.Z)
102
- elseif ut == "Region3" then
103
- local position = value.CFrame.Position
104
- return isfinite(position.X) and isfinite(position.Y) and isfinite(position.Z)
105
133
  end
134
+ return true
106
135
  end
107
136
 
137
+ -- string, boolean, nil, buffer, Instance, etc. are always clean
108
138
  return true
109
139
  end
110
140
 
111
- local function checkRate(player: Player, reg: Registration): boolean
112
- local cfg = reg.rateLimit
113
- if not cfg then
114
- return true
141
+ --[[
142
+ Schema-aware validation. Dispatches based on codec metadata.
143
+ Returns true if valid, false + reason if not.
144
+ ]]
145
+ local function validateWithCodec(
146
+ value: any,
147
+ codec: Types.InternalCodec<any>,
148
+ depth: number
149
+ ): (boolean, string?)
150
+ if depth <= 0 then
151
+ return true, nil
115
152
  end
116
153
 
117
- local playerBuckets = _buckets[player]
118
- if not playerBuckets then
119
- return true
154
+ local codecAny = codec :: any
155
+
156
+ -- Type check
157
+ local typeCheck = codecAny._typeCheck
158
+ if typeCheck then
159
+ if typeof(value) ~= typeCheck then
160
+ return false, `expected {typeCheck}, got {typeof(value)}`
161
+ end
120
162
  end
121
163
 
122
- local id = reg.id
123
- local now = clock()
124
- local cap = cfg.burstAllowance or cfg.maxPerSecond
125
- local bucket = playerBuckets[id]
164
+ -- Boolean: typeof check is sufficient
165
+ if codecAny._isBool then
166
+ return true, nil
167
+ end
126
168
 
127
- if not bucket then
128
- playerBuckets[id] = {
129
- tokens = cap - 1,
130
- lastRef = now,
131
- }
132
- return true
169
+ -- Integer + range
170
+ if codecAny._isInteger then
171
+ if not isfinite(value) then
172
+ return false, "non-finite"
173
+ end
174
+ if floor(value) ~= value then
175
+ return false, "non-integer"
176
+ end
177
+ local minV = codecAny._min
178
+ local maxV = codecAny._max
179
+ if minV and value < minV then
180
+ return false, `below min {minV}`
181
+ end
182
+ if maxV and value > maxV then
183
+ return false, `above max {maxV}`
184
+ end
185
+ return true, nil
186
+ end
187
+
188
+ -- Float with range (quantized float)
189
+ if codecAny._min ~= nil and codecAny._max ~= nil and not codecAny._isInteger then
190
+ if not isfinite(value) then
191
+ return false, "non-finite"
192
+ end
193
+ return true, nil
133
194
  end
134
195
 
135
- local elapsed = now - bucket.lastRef
136
- bucket.tokens = min(cap, bucket.tokens + elapsed * cfg.maxPerSecond)
137
- bucket.lastRef = now
196
+ -- Struct schema: recursive per-field validation
197
+ if codecAny._schema then
198
+ if typeof(value) ~= "table" then
199
+ return false, "expected table for struct"
200
+ end
138
201
 
139
- if bucket.tokens >= 1 then
140
- bucket.tokens -= 1
141
- return true
202
+ local schema = codecAny._schema
203
+ for key, fieldCodec in schema do
204
+ local fieldVal = value[key]
205
+ if fieldVal == nil and not (fieldCodec :: any)._isBool then
206
+ return false, `missing field "{key}"`
207
+ end
208
+ local ok, reason = validateWithCodec(fieldVal, fieldCodec, depth - 1)
209
+ if not ok then
210
+ return false, `field "{key}": {reason}`
211
+ end
212
+ end
213
+ return true, nil
142
214
  end
143
215
 
144
- return false
216
+ -- Fallback: generic NaN scan for f32, f64, auto, and Roblox types
217
+ if not scanValue(value, depth) then
218
+ return false, "NaN/inf detected"
219
+ end
220
+
221
+ return true, nil
145
222
  end
146
223
 
147
224
  -- Public --------------------------------------------------------------
148
225
 
149
226
  local Gate = {}
150
227
 
151
- -- Override the max recursion depth for NaN/inf scanning. Default 16. Range: 4–32.
152
- function Gate.setMaxDepth(depth: number): ()
153
- if depth < 4 or depth > 32 then
154
- error(`[Lync] Gate max depth must be 4–32, got {depth}`)
155
- end
156
- _maxDepth = depth
228
+ function Gate.setDepth(depth: number): ()
229
+ _depth = depth
157
230
  end
158
231
 
159
- function Gate.getMaxDepth(): number
160
- return _maxDepth
232
+ function Gate.validate(value: any, codec: Types.InternalCodec<any>): (boolean, string?)
233
+ return validateWithCodec(value, codec, _depth)
161
234
  end
162
235
 
163
- function Gate.check(value: any, reg: Registration, player: Player): boolean
164
- if not checkRate(player, reg) then
165
- fireDrop(player, "rate", reg.name, nil)
166
- return false
236
+ function Gate.scan(value: any): boolean
237
+ return scanValue(value, _depth)
238
+ end
239
+
240
+ -- Rate limiting -------------------------------------------------------
241
+
242
+ function Gate.checkRateLimit(reg: Types.Registration, player: Player): boolean
243
+ local config = reg.rateLimit
244
+ if not config then
245
+ return true
167
246
  end
168
247
 
169
- if not isClean(value, 1) then
170
- fireDrop(player, "nan", reg.name, value)
171
- return false
248
+ local now = clock()
249
+
250
+ -- Get or create per-player state
251
+ local perPacket = _rateLimits[reg.id]
252
+ if not perPacket then
253
+ perPacket = {}
254
+ _rateLimits[reg.id] = perPacket
255
+ end
256
+
257
+ local state = perPacket[player]
258
+ if not state then
259
+ state = {
260
+ tokens = config.burst or 1,
261
+ lastRefill = now,
262
+ lastAccepted = 0,
263
+ }
264
+ perPacket[player] = state
172
265
  end
173
266
 
174
- local validate = reg.validate
175
- if validate then
176
- local ok, reason = validate(value, player)
177
- if not ok then
178
- fireDrop(player, reason or "validate", reg.name, value)
267
+ -- Cooldown mode
268
+ if config.cooldown then
269
+ if now - state.lastAccepted < config.cooldown then
179
270
  return false
180
271
  end
272
+ state.lastAccepted = now
273
+ return true
181
274
  end
182
275
 
183
- return true
276
+ -- Token bucket mode
277
+ local maxPerSecond = config.maxPerSecond
278
+ if not maxPerSecond then
279
+ return true
280
+ end
281
+
282
+ local elapsed = now - state.lastRefill
283
+ state.lastRefill = now
284
+
285
+ local burst = config.burst or 1
286
+ state.tokens = math.min(burst, state.tokens + elapsed * maxPerSecond)
287
+
288
+ if state.tokens >= 1 then
289
+ state.tokens -= 1
290
+ state.lastAccepted = now
291
+ return true
292
+ end
293
+
294
+ return false
184
295
  end
185
296
 
186
- function Gate.onDrop(
187
- callback: (player: Player, reason: string, packetName: string, data: any?) -> ()
188
- ): () -> ()
189
- table.insert(_dropHandlers, callback)
297
+ function Gate.checkGlobalRateLimit(player: Player): boolean
298
+ local maxPS = _globalMaxPerSecond
299
+ if not maxPS then
300
+ return true
301
+ end
302
+
303
+ local now = clock()
304
+ local state = _globalLimits[player]
305
+ if not state then
306
+ state = { tokens = maxPS, lastRefill = now, lastAccepted = 0 }
307
+ _globalLimits[player] = state
308
+ end
190
309
 
191
- return function(): ()
192
- local idx = table.find(_dropHandlers, callback)
193
- if idx then
194
- table.remove(_dropHandlers, idx)
195
- end
310
+ local elapsed = now - state.lastRefill
311
+ state.lastRefill = now
312
+ state.tokens = math.min(maxPS, state.tokens + elapsed * maxPS)
313
+
314
+ if state.tokens >= 1 then
315
+ state.tokens -= 1
316
+ return true
196
317
  end
318
+
319
+ return false
197
320
  end
198
321
 
199
- -- Fires drop handlers without validation. Used by Reader for byte-budget violations.
200
- function Gate.reportDrop(player: Player?, reason: string, packetName: string, data: any?): ()
201
- if player and #_dropHandlers > 0 then
202
- fireDrop(player, reason, packetName, data)
322
+ function Gate.checkBandwidth(player: Player, bytes: number): boolean
323
+ if not _bandwidthSoftLimit then
324
+ return true
203
325
  end
326
+
327
+ if bytes <= _bandwidthSoftLimit then
328
+ local strikes = _bandwidthStrikes[player]
329
+ if strikes and strikes > 0 then
330
+ _bandwidthStrikes[player] = strikes - 1
331
+ end
332
+ return true
333
+ end
334
+
335
+ local current = _bandwidthStrikes[player]
336
+ local strikes = (if current then current else 0) + 1
337
+ _bandwidthStrikes[player] = strikes
338
+
339
+ local maxStrikes = if _bandwidthMaxStrikes then _bandwidthMaxStrikes else 10
340
+ return strikes <= maxStrikes
341
+ end
342
+
343
+ -- Configuration -------------------------------------------------------
344
+
345
+ function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
346
+ _globalMaxPerSecond = maxPerSecond
204
347
  end
205
348
 
206
- function Gate.onPlayerAdded(player: Player): ()
207
- _buckets[player] = {}
349
+ function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
350
+ _bandwidthSoftLimit = softLimit
351
+ _bandwidthMaxStrikes = maxStrikes
208
352
  end
209
353
 
210
- function Gate.onPlayerRemoved(player: Player): ()
211
- _buckets[player] = nil
354
+ function Gate.clearPlayer(player: Player): ()
355
+ _globalLimits[player] = nil
356
+ _bandwidthStrikes[player] = nil
357
+
358
+ for _, perPacket in _rateLimits do
359
+ perPacket[player] = nil
360
+ end
212
361
  end
213
362
 
214
363
  return table.freeze(Gate)