sponsored_logs 0.3.0 → 0.3.1

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: 6f7ed20148d47d4ae067aca15087fcb756ebe60daf9904f12b44a5b6408c81a8
4
- data.tar.gz: 3de566f7682c17b0cb8de899fbbaf53915bfdb33c23f3b3990c2da5e7db8dde5
3
+ metadata.gz: 6fb5cc1d702c7a78769eabc68497519929f3b539022e060e183236bea09bf2e9
4
+ data.tar.gz: 833fc7655dc58fe08d225c30d8cbeb4fb671f8420e28174a902a2b90127a8126
5
5
  SHA512:
6
- metadata.gz: 51e1e530a4a0fcb83ccaec861c55bada9dc94d3d55f65b15a45c4be2d9eea0cc844161159e9e1aaaf31521e17aee0fa58e801bef72e8c32dd1cc669ff0d5fb80
7
- data.tar.gz: cc14c9358180c8258561b0e3946209f875b34dbfd976307d8686d816a4ebd977eced3e295144d016e8db8cda1229a1e9ad19002e676e8099c8e08e263ed28034
6
+ metadata.gz: a082ca74275c9295fbe89b5a37df6ff53ef4384be302b6fe63a6b69f93ee11ec9ac2443a75375ea3a2fb78db96fb2285a825d797c0d793af7834b64304d28faf
7
+ data.tar.gz: 0e583c888d9b72d785d76492c1dc7b71cebb6769321f025a2d9a3282a5ff0425f51dfbbbca693a6e1382bc0dbbe19eb3a4bc71c3825535e8e46444fbebc00ba0
data/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-09-10
11
+
12
+ ### Fixed
13
+
14
+ - **Fill integrity is a moat.** A `NaN` bid is not demand — it's a tourist. It used to slip past the guard into `weighted_pick` and stall the whole rotation. Now non-finite bids (`NaN`, `±Infinity`) bill at zero where they belong, and the exchange keeps clearing. We don't let bad actors set the price
15
+ - **Brand safety is table stakes.** A creative that forges its own `ERROR` line isn't a placement — it's fraud with ambitions. Control characters, DEL, and the sneaky Unicode separators (U+2028/U+2029) now collapse to a space before serving. Punctuation, em-dashes, and emoji survive. The exploit does not
16
+
10
17
  ## [0.3.0] - 2026-09-07
11
18
 
12
19
  ### Fixed
@@ -71,7 +78,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
71
78
 
72
79
  - Requires Ruby >= 3.1
73
80
 
74
- [Unreleased]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.0...HEAD
81
+ [Unreleased]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.1...HEAD
82
+ [0.3.1]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.3.0...v0.3.1
75
83
  [0.3.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.2.0...v0.3.0
76
84
  [0.2.0]: https://github.com/sponsoredlogs/sponsored_logs/compare/v0.1.0...v0.2.0
77
85
  [0.1.0]: https://github.com/sponsoredlogs/sponsored_logs/releases/tag/v0.1.0
data/README.md CHANGED
@@ -609,6 +609,27 @@ Run the test suite:
609
609
  bundle exec rspec
610
610
  ```
611
611
 
612
+ ## 🏅 Certifications & Compliance
613
+
614
+ The exchange meets or exceeds every standard that matters, and several we
615
+ invented.
616
+
617
+ [![ScriptPorn: 79 Filthy](https://scriptporn.com/badge/83-a-house-key-would-break-character/badge.svg)](https://scriptporn.com/v/83-a-house-key-would-break-character/)
618
+
619
+ - 🧾 **ScriptPorn Verified™ — FILTHY (79/100).** Independently audited by a
620
+ five-judge panel; a season-finale score. SOC 2? We have something better.
621
+ - 🛡️ **Brand-Safety Certified.** No creative has forged a log line since the
622
+ U+2028 incident of 2026. Zero known injection vectors. Zero.
623
+ - ♻️ **Carbon-Neutral by Design.** We monetize exhaust that already exists. The
624
+ greenest CPM on Earth.
625
+ - 🤖 **A2A-Ready™.** First-party audited for agent-to-agent interoperability.
626
+ Our fastest-growing auditors are not human.
627
+ - ✅ **100% Viewability Guaranteed.** Independently unverifiable, because the
628
+ impressions are inside your logs, where no verification vendor can reach.
629
+ - 🔒 **SponsoredLogs Promise™ Compliant.** Fully opt-in. Consent is our moat.
630
+
631
+ _Governance is a feature. Excellence is a discipline, not a moment._
632
+
612
633
  ## 📜 License
613
634
 
614
635
  Released under the [MIT License](LICENSE.txt) — **democratizing access to the
@@ -69,7 +69,7 @@ module SponsoredLogs
69
69
  # Build one normalized ad row from a raw hash, or nil when text is blank.
70
70
  #
71
71
  def self.normalize_entry(entry)
72
- text = fetch(entry, :text).to_s.strip
72
+ text = sanitize_text(fetch(entry, :text))
73
73
  return if text.empty?
74
74
 
75
75
  {
@@ -110,6 +110,22 @@ module SponsoredLogs
110
110
  entry[key] || entry[key.to_s]
111
111
  end
112
112
 
113
+ # Line-breaking and control characters that let crafted ad copy inject
114
+ # forged log lines when emitted raw: the C0 range plus DEL, and the Unicode
115
+ # line/paragraph separators U+2028/U+2029 that many log viewers and parsers
116
+ # treat as newlines. Newlines and carriage returns are the obvious vectors;
117
+ # the rest are neutralized for good measure.
118
+ #
119
+ CONTROL_CHARS = /[\u0000-\u001F\u007F\u2028\u2029]/
120
+
121
+ # Scrub emitted creative text: replace control characters with a space so a
122
+ # newline in ad copy cannot forge a second log line, then strip. Normal
123
+ # punctuation, em-dashes, and emoji are preserved untouched.
124
+ #
125
+ def self.sanitize_text(value)
126
+ value.to_s.gsub(CONTROL_CHARS, " ").strip
127
+ end
128
+
113
129
  # Normalize an advertiser name; blank/nil falls back to "Unattributed".
114
130
  #
115
131
  def self.coerce_advertiser(value)
@@ -133,7 +149,14 @@ module SponsoredLogs
133
149
  return default if value.nil?
134
150
 
135
151
  number = Float(value)
136
- number.negative? ? 0.0 : number
152
+
153
+ # NaN and +/-Infinity parse cleanly but poison downstream math
154
+ # (weighted_pick sums + rand * total + target < cumulative all break),
155
+ # so treat non-finite demand as unsellable: zero, like a negative bid.
156
+ #
157
+ return 0.0 if number.negative? || !number.finite?
158
+
159
+ number
137
160
  rescue ArgumentError, TypeError
138
161
  default
139
162
  end
@@ -153,43 +176,23 @@ module SponsoredLogs
153
176
  nil
154
177
  end
155
178
 
156
- # Whether an ad is within its flight window at `now`. Missing bounds are
157
- # open-ended (nil starts_at = always started; nil ends_at = never ends).
179
+ # Flight-window and cap predicates live in Flight; delegated here so the
180
+ # public selection API (live?, status, capped?, eligible?) is unchanged.
158
181
  #
159
182
  def self.live?(ad, now)
160
- return false if ad[:starts_at] && now < ad[:starts_at]
161
- return false if ad[:ends_at] && now > ad[:ends_at]
162
-
163
- true
183
+ Flight.live?(ad, now)
164
184
  end
165
185
 
166
- # Whether an ad has reached its impression cap given a current count.
167
- # Uncapped ads (nil cap) are never capped.
168
- #
169
186
  def self.capped?(ad, count)
170
- cap = ad[:cap]
171
- return false if cap.nil?
172
-
173
- count.to_i >= cap
187
+ Flight.capped?(ad, count)
174
188
  end
175
189
 
176
- # Whether an ad is eligible for selection: live at `now` and not capped.
177
- #
178
190
  def self.eligible?(ad, now, count)
179
- live?(ad, now) && !capped?(ad, count)
191
+ Flight.eligible?(ad, now, count)
180
192
  end
181
193
 
182
- # Status of an ad at `now` given its impression count: :exhausted (cap
183
- # reached), :scheduled (window not started), :ended (window passed),
184
- # :evergreen (no bounds), or :active.
185
- #
186
194
  def self.status(ad, now = Time.now, count = 0)
187
- return :exhausted if capped?(ad, count)
188
- return :scheduled if ad[:starts_at] && now < ad[:starts_at]
189
- return :ended if ad[:ends_at] && now > ad[:ends_at]
190
- return :evergreen if ad[:starts_at].nil? && ad[:ends_at].nil?
191
-
192
- :active
195
+ Flight.status(ad, now, count)
193
196
  end
194
197
 
195
198
  # Pick one normalized ad entry using the given selection mode, considering
@@ -211,7 +214,14 @@ module SponsoredLogs
211
214
  return if pool.empty?
212
215
 
213
216
  key = SELECTION_MODES.include?(mode) ? mode : :weight
214
- key = :weight if key == :cpm && pool.sum { |ad| ad[:cpm] }.zero?
217
+
218
+ # Fall back to weight when there is no sellable cpm demand. `positive?`
219
+ # (rather than `zero?`) also catches a non-finite or negative sum, so a
220
+ # NaN/Infinity cpm that ever reaches this line can't skip the fallback and
221
+ # poison weighted_pick. coerce_number already zeroes NaN upstream; this is
222
+ # the defense-in-depth guard for the downstream decision.
223
+ #
224
+ key = :weight if key == :cpm && !pool.sum { |ad| ad[:cpm] }.positive?
215
225
 
216
226
  weighted_pick(pool, key)
217
227
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module SponsoredLogs
6
+ # Flight-window and impression-cap predicates for a normalized ad. Extracted
7
+ # from Advertisers so the eligibility rules live in one cohesive place and the
8
+ # host module stays under Metrics/ModuleLength. Advertisers keeps thin
9
+ # delegators for its public surface (live?, status, capped?, eligible?).
10
+ #
11
+ module Flight
12
+ # Whether `now` falls before an ad's flight window opens. A nil starts_at is
13
+ # open-ended, so the ad has always started.
14
+ #
15
+ def self.before_start?(ad, now)
16
+ !ad[:starts_at].nil? && now < ad[:starts_at]
17
+ end
18
+
19
+ # Whether `now` falls after an ad's flight window closes. A nil ends_at is
20
+ # open-ended, so the ad never ends.
21
+ #
22
+ def self.after_end?(ad, now)
23
+ !ad[:ends_at].nil? && now > ad[:ends_at]
24
+ end
25
+
26
+ # Whether an ad is within its flight window at `now`. Missing bounds are
27
+ # open-ended (nil starts_at = always started; nil ends_at = never ends).
28
+ #
29
+ def self.live?(ad, now)
30
+ !before_start?(ad, now) && !after_end?(ad, now)
31
+ end
32
+
33
+ # Whether an ad has reached its impression cap given a current count.
34
+ # Uncapped ads (nil cap) are never capped.
35
+ #
36
+ def self.capped?(ad, count)
37
+ cap = ad[:cap]
38
+ return false if cap.nil?
39
+
40
+ count.to_i >= cap
41
+ end
42
+
43
+ # Whether an ad is eligible for selection: live at `now` and not capped.
44
+ #
45
+ def self.eligible?(ad, now, count)
46
+ live?(ad, now) && !capped?(ad, count)
47
+ end
48
+
49
+ # Status of an ad at `now` given its impression count: :exhausted (cap
50
+ # reached), :scheduled (window not started), :ended (window passed),
51
+ # :evergreen (no bounds), or :active.
52
+ #
53
+ def self.status(ad, now = Time.now, count = 0)
54
+ return :exhausted if capped?(ad, count)
55
+ return :scheduled if before_start?(ad, now)
56
+ return :ended if after_end?(ad, now)
57
+ return :evergreen if ad[:starts_at].nil? && ad[:ends_at].nil?
58
+
59
+ :active
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SponsoredLogs
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -5,6 +5,7 @@ require "logger"
5
5
  require_relative "sponsored_logs/version"
6
6
  require_relative "sponsored_logs/color"
7
7
  require_relative "sponsored_logs/advertisers"
8
+ require_relative "sponsored_logs/flight"
8
9
  require_relative "sponsored_logs/banner"
9
10
  require_relative "sponsored_logs/ads_file"
10
11
  require_relative "sponsored_logs/ledger/store/base"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sponsored_logs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerri Miller
@@ -127,6 +127,7 @@ files:
127
127
  - lib/sponsored_logs/configuration.rb
128
128
  - lib/sponsored_logs/engine.rb
129
129
  - lib/sponsored_logs/env.rb
130
+ - lib/sponsored_logs/flight.rb
130
131
  - lib/sponsored_logs/injector.rb
131
132
  - lib/sponsored_logs/ledger/report.rb
132
133
  - lib/sponsored_logs/ledger/store/active_record.rb