@axpecter/lync 2.0.0 → 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 +475 -426
  2. package/package.json +1 -1
  3. package/src/Types.luau +63 -31
  4. package/src/api/Group.luau +101 -106
  5. package/src/api/Packet.luau +187 -186
  6. package/src/api/Query.luau +223 -284
  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 +179 -270
  11. package/src/codec/composite/Map.luau +97 -347
  12. package/src/codec/composite/Optional.luau +27 -24
  13. package/src/codec/composite/Shared.luau +96 -65
  14. package/src/codec/composite/Struct.luau +200 -360
  15. package/src/codec/composite/Tagged.luau +62 -187
  16. package/src/codec/composite/Tuple.luau +79 -103
  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 +28 -21
  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 -36
  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 -335
  43. package/src/init.luau +319 -207
  44. package/src/internal/Baseline.luau +29 -24
  45. package/src/internal/Channel.luau +333 -278
  46. package/src/internal/Middleware.luau +60 -85
  47. package/src/internal/Pool.luau +19 -30
  48. package/src/internal/Registry.luau +56 -113
  49. package/src/transport/Bridge.luau +64 -43
  50. package/src/transport/Client.luau +59 -170
  51. package/src/transport/Gate.luau +282 -142
  52. package/src/transport/Reader.luau +148 -140
  53. package/src/transport/Server.luau +138 -488
  54. package/src/api/Namespace.luau +0 -256
  55. package/src/codec/meta/Quantized.luau +0 -174
@@ -1,223 +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
40
45
  return false
41
46
  end
42
47
 
43
- local t = type(value)
48
+ if minVal and value < minVal then
49
+ return false
50
+ end
51
+ if maxVal and value > maxVal then
52
+ return false
53
+ end
54
+
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)
103
+ end
104
+
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)
66
114
  end
67
115
 
68
- if t == "userdata" then
69
- local ut = typeof(value)
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
70
126
 
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
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
133
186
  end
134
187
 
135
- local elapsed = now - bucket.lastRef
136
- bucket.tokens = min(cap, bucket.tokens + elapsed * cfg.maxPerSecond)
137
- bucket.lastRef = now
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
194
+ end
138
195
 
139
- if bucket.tokens >= 1 then
140
- bucket.tokens -= 1
141
- return true
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
201
+
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
- if reg.drops then
166
- reg.drops += 1
167
- end
168
- fireDrop(player, "rate", reg.name, nil)
169
- 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
170
246
  end
171
247
 
172
- if not isClean(value, 1) then
173
- if reg.drops then
174
- reg.drops += 1
175
- end
176
- fireDrop(player, "nan", reg.name, value)
177
- 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
178
255
  end
179
256
 
180
- local validate = reg.validate
181
- if validate then
182
- local ok, reason = validate(value, player)
183
- if not ok then
184
- if reg.drops then
185
- reg.drops += 1
186
- end
187
- fireDrop(player, reason or "validate", reg.name, value)
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
265
+ end
266
+
267
+ -- Cooldown mode
268
+ if config.cooldown then
269
+ if now - state.lastAccepted < config.cooldown then
188
270
  return false
189
271
  end
272
+ state.lastAccepted = now
273
+ return true
190
274
  end
191
275
 
192
- 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
193
295
  end
194
296
 
195
- function Gate.onDrop(
196
- callback: (player: Player, reason: string, packetName: string, data: any?) -> ()
197
- ): () -> ()
198
- 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
199
309
 
200
- return function(): ()
201
- local idx = table.find(_dropHandlers, callback)
202
- if idx then
203
- table.remove(_dropHandlers, idx)
204
- 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
205
317
  end
318
+
319
+ return false
206
320
  end
207
321
 
208
- -- Fires drop handlers without validation. Used by Reader for byte-budget violations.
209
- function Gate.reportDrop(player: Player?, reason: string, packetName: string, data: any?): ()
210
- if player and #_dropHandlers > 0 then
211
- fireDrop(player, reason, packetName, data)
322
+ function Gate.checkBandwidth(player: Player, bytes: number): boolean
323
+ if not _bandwidthSoftLimit then
324
+ return true
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
212
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
213
341
  end
214
342
 
215
- function Gate.onPlayerAdded(player: Player): ()
216
- _buckets[player] = {}
343
+ -- Configuration -------------------------------------------------------
344
+
345
+ function Gate.configureGlobalRateLimit(maxPerSecond: number): ()
346
+ _globalMaxPerSecond = maxPerSecond
217
347
  end
218
348
 
219
- function Gate.onPlayerRemoved(player: Player): ()
220
- _buckets[player] = nil
349
+ function Gate.configureBandwidth(softLimit: number, maxStrikes: number): ()
350
+ _bandwidthSoftLimit = softLimit
351
+ _bandwidthMaxStrikes = maxStrikes
352
+ end
353
+
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
221
361
  end
222
362
 
223
363
  return table.freeze(Gate)