nebula-token 1.0.1 → 1.0.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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +1 -1
  4. data/lib/nebula_token.rb +44 -4
  5. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e174bf87f7b2cca663e761f710c4d7caf75795d1fe2077e4bfd781f68ec064a6
4
- data.tar.gz: e1285aa87c3bb87eafd5216204be69cf852b01cd450df9c5329dd9639521fda5
3
+ metadata.gz: 61d4afd2ad50c4081a6242d832a2fa4c6b9658c493671312857cc270acbde9aa
4
+ data.tar.gz: 7fb59167e1ac60a8e7c9c089d5421cc52b58ec57c0209014526baa4a37197cec
5
5
  SHA512:
6
- metadata.gz: e450ff9a8351b508db17d31d251eb560ff73fed504b44746e4c7315d9900d34d7e2073b8119d4d42dc17722fb0685e5374103055bd1d4ec743cc860d396c1712
7
- data.tar.gz: fe19f48e27733096ce103921b5fa20e21faa0aeab3a9d38d4ba5648718ff3b8a1e69c38bb83c21e9c749fccce2943f1f3c4b6cd54226b3831032ffa65d0beb24
6
+ metadata.gz: 246a1592aa4f314982053fa2e07dee50547bd961a56fbcdbf98f28b019ae1e445697e60e3a90c72c9df6b1cbcdb52d598c2cd44f64a117f14e322cfe08a5c21c
7
+ data.tar.gz: 06e23d009ba3686ea6367581103dba8c6dd6961fe7807c543bdb90a7f9bc6cfb791a31a1e3a3b869fb45f65e6306290dd72513c036b2ffb49bd1a5880a0095a9
data/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project are documented in this file.
4
4
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.0.2] - 2026-08-04
8
+
9
+ ### Fixed
10
+ - The engine tested the compare-and-set return value directly, and in Ruby `0`
11
+ is truthy. A store reporting the affected-row count — which `docs/STORE.md`
12
+ told adapters to return — made a **lost** compare-and-set read as applied, so
13
+ two concurrent refreshes each minted a successor and the family forked into
14
+ two independently valid lineages with reuse detection off for it ([N-17],
15
+ [N-18], [N-34] step 5). Counts are normalised now and anything outside the
16
+ contract fails closed as "not applied".
17
+ - A device identifier was decided on the String's encoding tag rather than on
18
+ its bytes, so bytes that every other port accepts were refused when tagged
19
+ `ASCII-8BIT` — from `String#b`, `File.binread` or a socket read. In `refresh`
20
+ that is a sender-binding failure, so the whole family was revoked where the
21
+ other nine rotate normally ([N-11], [N-32]). [N-12]'s treatment of invalid
22
+ Unicode is unchanged. Ruby is the only one of the ten where a string carries a
23
+ declared encoding distinct from its bytes, so it is the only port this could
24
+ affect; erratum E-1 records the clarification to [N-11], and the shared cases
25
+ `dh-08`/`dh-09` now pin it for every implementation.
26
+
7
27
  ## [1.0.1] - 2026-07-30
8
28
 
9
29
  Release automation only; no change to this package's behaviour, API or wire
@@ -24,5 +44,6 @@ workflow failed, and neither permits republishing a used version number.
24
44
  window, family revocation, sender (device) binding, dual expiry clocks,
25
45
  pepper rotation via `kid`.
26
46
 
47
+ [1.0.2]: https://github.com/nebula-token/nebula-token/releases/tag/v1.0.2
27
48
  [1.0.1]: https://github.com/nebula-token/nebula-token/releases/tag/v1.0.1
28
- [1.0.0]: https://github.com/nebula-token/nebula-token/releases/tag/v1.0.0
49
+ [1.0.0]: https://github.com/nebula-token/nebula-token/commit/cb66b3dd897dc968bff8b211f001b94de7531b09
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # nebula-token (Ruby)
2
2
 
3
- Ruby implementation of [NEBULA](../../SPECIFICATION.md) — opaque rotating refresh tokens (RFC 9700 model). Standard library only (`openssl`, `securerandom`), no gem dependencies, Ruby ≥ 3.3.
3
+ Ruby implementation of [NEBULA](../../SPECIFICATION.md) — opaque rotating refresh tokens (RFC 9700 model). Standard library only (`openssl`, `securerandom`), no gem dependencies, Ruby ≥ 3.3. Implements `spec_version = 1`.
4
4
 
5
5
  ```
6
6
  gem install nebula-token
data/lib/nebula_token.rb CHANGED
@@ -138,11 +138,26 @@ module NebulaToken
138
138
  # failure on the attacker-reachable path, a caller error at issue ([N-12]).
139
139
  # Deriving a hash from a replacement character instead would make the same
140
140
  # identifier hash differently across languages.
141
+ # A binary-tagged String is bytes, not text, and is decided on the BYTES for
142
+ # the same reason pepper_bytes is: the identical identifier reaches Ruby
143
+ # tagged UTF-8 from JSON and ASCII-8BIT from String#b, File.binread or a
144
+ # socket read, and ASCII-8BIT -> UTF-8 transcoding raises for every byte above
145
+ # 0x7F. Transcoding it would refuse an identifier the other nine ports accept
146
+ # — and in `refresh` that refusal is a sender-binding failure, so it would
147
+ # revoke the whole family where the other nine rotate normally. A String that
148
+ # carries a real text encoding still transcodes, so its UTF-8 encoding is the
149
+ # one [N-11] names.
141
150
  def utf8_bytes(value)
142
151
  return nil unless value.is_a?(String)
143
152
 
144
- utf8 = value.encoding == Encoding::UTF_8 ? value : value.encode(Encoding::UTF_8)
145
- utf8.valid_encoding? ? utf8.b : nil
153
+ utf8 =
154
+ if value.encoding == Encoding::UTF_8 || value.encoding == Encoding::BINARY
155
+ value
156
+ else
157
+ value.encode(Encoding::UTF_8)
158
+ end
159
+ bytes = utf8.b
160
+ bytes.dup.force_encoding(Encoding::UTF_8).valid_encoding? ? bytes : nil
146
161
  rescue EncodingError
147
162
  nil
148
163
  end
@@ -275,6 +290,30 @@ module NebulaToken
275
290
  STATUS_BY_NAME.fetch(value.to_s, STATUS_REVOKED)
276
291
  end
277
292
 
293
+ # Did a compare-and-set apply? ([N-17], [N-18])
294
+ #
295
+ # The store contract returns a boolean, but `docs/STORE.md` §4 also tells an
296
+ # adapter to derive it from the affected-row count its driver reports —
297
+ # `cmd_tuples` on a `PG::Result`, `affected_rows` on Mysql2 — and in Ruby `0`
298
+ # is truthy. A bare `unless @store.mark_rotated(...)` therefore reads a LOST
299
+ # compare-and-set as applied: two concurrent refreshes each mint a successor,
300
+ # the family forks into two independently valid lineages, and no later
301
+ # presentation of either is a replay. Reuse detection is not weakened for that
302
+ # family, it is switched off — which is the entire failure [N-17] exists to
303
+ # close. Ruby is the only one of the ten ports where a count can read as
304
+ # success, so this is a Ruby-side guard, not a change of contract.
305
+ #
306
+ # Anything outside the contract fails closed as "not applied": a spurious
307
+ # CONFLICT revokes nothing and is retryable ([N-35]), a spurious success is
308
+ # unrecoverable.
309
+ def cas_applied?(value)
310
+ case value
311
+ when true, false then value
312
+ when Integer then value.positive?
313
+ else false
314
+ end
315
+ end
316
+
278
317
  # ── Server-side record (§3) ────────────────────────────────────────────────
279
318
 
280
319
  # One row per issued token ([N-10]).
@@ -787,7 +826,7 @@ module NebulaToken
787
826
 
788
827
  # Compare-and-set: exactly one concurrent retry may consume the unused
789
828
  # successor. The loser rotates nothing and reports CONFLICT ([N-30] 2).
790
- unless @store.revoke_if_active(successor.selector)
829
+ unless NebulaToken.cas_applied?(@store.revoke_if_active(successor.selector))
791
830
  return RefreshResult.failure(ErrorCode::CONFLICT, record)
792
831
  end
793
832
 
@@ -816,7 +855,8 @@ module NebulaToken
816
855
  device_hash, record.family_expires_at, now)
817
856
  @store.insert(successor)
818
857
 
819
- unless @store.mark_rotated(record.selector, from_status, rotated_at, successor.selector)
858
+ unless NebulaToken.cas_applied?(@store.mark_rotated(record.selector, from_status,
859
+ rotated_at, successor.selector))
820
860
  # [N-34] step 5: a concurrent refresh won the compare-and-set. Clean up
821
861
  # the successor we inserted and report a retryable conflict — never a
822
862
  # token. Without the CAS both refreshes would mint a successor and the
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nebula-token
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matteo Teodori