camada 0.2.1 → 0.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ff3ec1e73a6da51f20af06916fb4438fe50fc9fa87a9023445e90561c1a7cf8
4
- data.tar.gz: d9d10163bdb349345ccb3a65666966624640fef0352d4727f848485abc0ea734
3
+ metadata.gz: b73c94df7715601ab82c6a470ccd976e6bb934f6dcf6c8c54c5bfc3fe00b97df
4
+ data.tar.gz: 94bd0d19b9259b6e03ae0e6df41ca717afbd9aa915d7b1f273c99b89b3dab425
5
5
  SHA512:
6
- metadata.gz: 42beca1c8a2d5299814aa3b09963e21963e0e25344238e70cf087d19d484feb7cd91dbaae8930ec0c5236eb77c6906c5f9de1b5cd265704798061a99fae40040
7
- data.tar.gz: 79148a9dd5427db583f916520b64726a6baede851368a2677a55e846ed6f4ed83500ec020544b6ba90c263ee189d42acd8c11a52f8f1a2a713d908d7d867476f
6
+ metadata.gz: 6b6a3f8d9b8185f0979dccfb92498bc6fac1b62fdb1221f0130f69ca606edf5783ef25df6414e11997a13bb1a1dc5b18b874b5e3b570bef8e980230626c55fa6
7
+ data.tar.gz: 0e879615dbc999eed321ca8bc7821498622ee91a0be08345c846508a9cbd74aea4eab42f55eb7468fbc27482d593c190fa8e94b66c19e7482731dc3cf5159100
data/README.md CHANGED
@@ -170,9 +170,7 @@ valid `_cch`, then `nil`. A non-page (non-HTML) request also gets `nil` and is s
170
170
  `sdk-ruby` is an in-app tap: status, latency, session, the beacon's browser signals and your
171
171
  outcomes. Puma hands the env to Rack as binary strings; the middleware reads them as UTF-8 with
172
172
  invalid bytes replaced (U+FFFD) before matching and shipping, so a stray high byte in a header
173
- neither escapes a rule nor costs the batch it rides in. The Rack env carries no wire header order (`hord` is the env's order), so the analyst
174
- reads no HEADER_ORDER signal from this tap, and it never scores the absence of header order, ASN,
175
- country or a TLS fingerprint against a request; ASN and country it resolves itself. Enforcement
173
+ neither escapes a rule nor costs the batch it rides in. The Rack env carries no wire header order (`hord` is the env's order), so this tap ships no header-order signal; camada resolves ASN and country itself. Enforcement
176
174
  at this position covers ip, path, user-agent and header conditions — ASN, country and TLS entries
177
175
  fail open in-app. `matches` patterns are JS regexes read by Ruby's Onigmo: named groups are
178
176
  native, `[^]` and `\cX` are translated, `^`/`$` become `\A`/`\z` (Ruby's are line anchors,
@@ -4,8 +4,8 @@ require "json"
4
4
 
5
5
  module Camada
6
6
  # Tap identifier this SDK claims on the wire. The server validates against its own enum and
7
- # derives the capability mask itself (edge-analyst src/capabilities.js): an SDK can never grant
8
- # itself capability bits, only name its position — and an unknown name is silently read as a
7
+ # decides itself what each tap can see: an SDK can never grant
8
+ # itself more than that, only name its position — and an unknown name is silently read as a
9
9
  # proxy, so this literal is load-bearing.
10
10
  TAP = "sdk-ruby"
11
11
 
@@ -149,9 +149,10 @@ module Camada
149
149
 
150
150
  # A pattern this runtime rejects never matches, and never throws (fail open). Patterns are
151
151
  # authored as JS regexes (the analyst validates them with `new RegExp`), so the JS spellings
152
- # Onigmo reads differently are translated first — see js_to_onigmo; \d \w \b are ASCII in
153
- # Ruby by default, as JS reads them. An astral char is one code point here and two UTF-16 units
154
- # in JS, so `.`, `[^a]` and `\S` count it once (an accepted gap, camada-all-vuvo).
152
+ # Onigmo reads differently are translated first — see js_to_onigmo; \d \w are ASCII in
153
+ # Ruby by default, as JS reads them, and \b \B are emitted as (?a:\b) for the same reason.
154
+ # An astral char is one code point here and two UTF-16 units in JS,
155
+ # so `.`, `[^a]` and `\S` count it once (an accepted gap, camada-all-vuvo).
155
156
  # A quantifier right after a raw astral char repeats the whole char here but only its low unit in JS,
156
157
  # so js_to_onigmo answers as V8 does: the char when the count allows 1, else `(?!)` (camada-all-5lra, see astral_count).
157
158
  # Where a lookbehind ends up holding `(?!)`, Onigmo cannot compile it, so compile_regex returns nil and the rule never matches.
@@ -372,6 +373,7 @@ module Camada
372
373
  # that split (camada-all-ccjo, camada-all-fe1r). A char whose fold is several chars (ß, ff, İ; raw, `\u00df` or `\ß`)
373
374
  # becomes `(?-i:ß)` there, pulled out of a positive class like `\S` unless a `-` is beside it (camada-all-ozix):
374
375
  # JS /i matches it only to itself, and Onigmo would read it as its fold alone (version-dependent).
376
+ # One of the 63 iota-subscript chars in a positive class is pulled out the same way in any context (camada-all-rmje).
375
377
  # In a class `\c` is a control char only before a letter, digit or `_` (ClassControlLetter); before
376
378
  # anything else (`[\c*]`) it is a literal `\` then `c` (camada-all-mqcb).
377
379
  # A `{m,n}` bound above Onigmo's 100000 is clamped: m to 100000, and an n above it drops (`{m,}`,
@@ -398,13 +400,21 @@ module Camada
398
400
  bars = [-1] # per open group, the root first: the index of the last `|` in it or an ancestor
399
401
  lbs = [false] # per open group, the root first: inside a lookbehind (camada-all-ccjo)
400
402
  lit_end = -1 # the pattern index right after the last literal emitted inside a lookbehind
401
- class_start = nil # the out index of an open positive class's `[` inside a lookbehind
403
+ class_start = nil # the out index of an open positive class's `[`
402
404
  class_alts = [] # the `\S`/`\W`/`\D` and multi-fold chars that class held
403
405
  last_named = {} # name -> the index of its last opener
404
406
  while i < n
405
407
  ch = pattern[i]
408
+ w = multi_fold(pattern, i)
409
+ iota = w && IOTA_SUBSCRIPT.match?(w[2])
410
+ if !in_class && !lbs.last && iota
411
+ out << "(?-i:#{w[0]})" # outside a lookbehind (the branch below) and a class; JS /i matches it only to itself (camada-all-zn38)
412
+ i += w[1]
413
+ next
414
+ end
406
415
  # in a lookbehind (positive class too, unless a range end) a multi-fold char becomes `(?-i:c)` (camada-all-ozix)
407
- if lbs.last && (!in_class || class_start) && (w = multi_fold(pattern, i)) &&
416
+ # an iota-subscript char in a positive class is pulled out the same way, in a lookbehind or not (camada-all-rmje)
417
+ if (lbs.last || (in_class && iota)) && (!in_class || class_start) && w &&
408
418
  !(in_class && (pattern[i - 1] == "-" || pattern[i + w[1]] == "-"))
409
419
  in_class ? class_alts << "(?-i:#{w[0]})" : out << "(?-i:#{w[0]})"
410
420
  i += w[1]
@@ -423,7 +433,7 @@ module Camada
423
433
  next
424
434
  end
425
435
  l = pattern[i + 1]
426
- if in_class && class_start && (l == "S" || l == "W" || l == "D")
436
+ if in_class && class_start && lbs.last && (l == "S" || l == "W" || l == "D")
427
437
  class_alts << (l == "S" ? JS_SPACE["S"] : "\\#{l}") # spliced in at the `]`: a positive class holding one gains multi-char folds
428
438
  pull_end = i + 2
429
439
  i += 2
@@ -435,6 +445,11 @@ module Camada
435
445
  i += 2
436
446
  next
437
447
  end
448
+ if !in_class && (l == "b" || l == "B")
449
+ out << "(?a:\\#{l})" # Onigmo's \b reads é as a word char, JS (no u flag) as ASCII only
450
+ i += 2
451
+ next
452
+ end
438
453
  if l == "c" && pattern[i + 2]&.match?(in_class ? /[A-Za-z0-9_]/ : /[A-Za-z]/) # JS Annex B: `[\c1]` too
439
454
  out << Regexp.escape((pattern[i + 2].ord & 0x1F).chr)
440
455
  i += 3
@@ -457,12 +472,13 @@ module Camada
457
472
  i += 1
458
473
  next
459
474
  end
460
- kept = !l.match?(/[A-Za-z]/) || pattern.match?(JS_ESCAPE, i + 1) || (l == "B" && !in_class) ||
475
+ kept = !l.match?(/[A-Za-z]/) || pattern.match?(JS_ESCAPE, i + 1) ||
461
476
  (l == "k" && named)
462
477
  l = "\\\\c" if l == "c" # JS reads `\c` before a non-letter as text
463
478
  if kept
464
479
  e = (pattern.match(/\G\\\d+/n, i) || pattern.match(JS_ATOM, i))[0] # the whole escape: a lookbehind must not split `\x41`
465
- out << e
480
+ # JS reads \xHH above 0x7F as that code point, Onigmo as a raw byte (camada-all-w2w4)
481
+ out << (e.match?(/\A\\x[89a-fA-F]\h\z/) ? "\\u00#{e[2..]}" : e)
466
482
  i += e.length
467
483
  next
468
484
  end
@@ -487,12 +503,12 @@ module Camada
487
503
  else
488
504
  out << "]"
489
505
  end
490
- lit_end = i + 1 if class_start # Onigmo merges a one-char class into the literals beside it
506
+ lit_end = i + 1 if class_start && lbs.last # Onigmo merges a one-char class into the literals beside it
491
507
  class_start = nil
492
508
  i += 1
493
509
  next
494
510
  end
495
- if ch == "-" && (pull_end == i || solo_end == i || pattern.match?(class_start ? /\G-\\[sSWD]/ : /\G-\\[sS]/, i))
511
+ if ch == "-" && (pull_end == i || solo_end == i || pattern.match?(class_start && lbs.last ? /\G-\\[sSWD]/ : /\G-\\[sS]/, i))
496
512
  if pull_end == i # JS reads the atom after that dash as standalone: a dash right after it is text too
497
513
  m = pattern.match(JS_ATOM, i + 1) # nil when the pattern ends on the dash
498
514
  solo_end = m ? i + 1 + m[0].length : pattern.length
@@ -549,10 +565,8 @@ module Camada
549
565
  i = j + 1
550
566
  next
551
567
  end
552
- if !neg && lbs.last
553
- out << "(?:)" if lit_end == i
554
- class_start = out.length
555
- end
568
+ out << "(?:)" if !neg && lbs.last && lit_end == i
569
+ class_start = out.length unless neg
556
570
  out << pattern[i...j]
557
571
  i = j
558
572
  in_class = true
@@ -624,17 +638,21 @@ module Camada
624
638
  str.encoding == Encoding::BINARY ? str : str.b
625
639
  end
626
640
 
627
- # [its text, its length] when the char at i (raw UTF-8, `\uXXXX` or an identity escape `\ß`) has a case fold of
641
+ # The 63 Greek chars whose fold ends in an iota (ᾀ -> ἀι): Onigmo never matches one before a lookaround under /i.
642
+ IOTA_SUBSCRIPT = /\A[\u1F80-\u1FAF\u1FB2-\u1FB4\u1FB7\u1FBC\u1FC2-\u1FC4\u1FC7\u1FCC\u1FF2-\u1FF4\u1FF7\u1FFC]/
643
+
644
+ # [its text, its length, the char] when the char at i (raw UTF-8, `\uXXXX` or an identity escape `\ß`) has a case fold of
628
645
  # several chars (ß, İ, ff: 104 of them), else nil. JS /i without `u` matches each of them only to itself.
629
646
  def self.multi_fold(pattern, i)
630
647
  if pattern[i] == "\\"
631
- return (w = multi_fold(pattern, i + 1)) && [w[0], w[1] + 1] if pattern.getbyte(i + 1).to_i >= 0x80
648
+ return (w = multi_fold(pattern, i + 1)) && [w[0], w[1] + 1, w[2]] if pattern.getbyte(i + 1).to_i >= 0x80
632
649
 
633
- m = pattern.match(/\G\\u(\h{4})/n, i)
634
- return nil unless m && !(cp = m[1].hex).between?(0xD800, 0xDFFF)
650
+ m = pattern.match(/\G\\(?:u(\h{4})|x([89a-fA-F]\h))/n, i) # `\xHH` above 0x7F is the code point (camada-all-w2w4)
651
+ return nil unless m && !(cp = (m[1] || m[2]).hex).between?(0xD800, 0xDFFF)
635
652
 
636
653
  c = cp.chr(Encoding::UTF_8)
637
- len = 6
654
+ len = m[0].length
655
+ return [format("\\u%04x", cp), len] if c.downcase(:fold).length > 1 && m[2]
638
656
  else
639
657
  b = pattern.getbyte(i)
640
658
  return nil unless b.between?(0xC2, 0xEF) # a 2- or 3-byte lead: every such char is in the BMP
@@ -642,7 +660,7 @@ module Camada
642
660
  len = b < 0xE0 ? 2 : 3
643
661
  c = pattern.byteslice(i, len).force_encoding(Encoding::UTF_8)
644
662
  end
645
- [pattern.byteslice(i, len), len] if c.valid_encoding? && c.downcase(:fold).length > 1
663
+ [pattern.byteslice(i, len), len, c] if c.valid_encoding? && c.downcase(:fold).length > 1
646
664
  end
647
665
 
648
666
  def self.brace_quantifier(match)
@@ -6,6 +6,6 @@ module Camada
6
6
  # camada-web, camada-mkt) parse this file with /^\s*VERSION = "([^"]+)"/m the way they parse
7
7
  # a package.json. Plain X.Y.Z only, and @camada/ruby must stay in the analyst's SDK_NAMES:
8
8
  # its sdkId drops anything else.
9
- VERSION = "0.2.1"
9
+ VERSION = "0.2.2"
10
10
  SDK_ID = "@camada/ruby/#{VERSION}".freeze
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: camada
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - camada
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-26 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: 'camada SDK for Ruby: inline enforcement of your snapshot (ordered custom
14
14
  rules, then allow, block, challenge), first-party beacon and proof-of-work challenge,