nebula-token 1.0.1 → 1.0.3
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 +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +1 -1
- data/lib/nebula_token.rb +44 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c305d22c1173bd48ed2bb63884b2e111e6700e62f8307261d1740bd0fa613a0e
|
|
4
|
+
data.tar.gz: 548cd50e6e4f91edddf47c4c46bc04da5a58f5440863cbe55b90f5afb938f92d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79fb8e5875b481e06fea27c1c0fe716cf8cf4bda38dea43c0b94396b18ae527ca4a8085ca599398184544dc0950796335ffed4922b7d175586c2150b8f364fed
|
|
7
|
+
data.tar.gz: 073e306d9eba7f9b2eb56928971339a8dea7707d9f1f4d2124fcc12d6ea6cd7af35e093220cb42f4ebdaf51f5660afdebb031cd032e95ec4cbfbde8bcd0fa7c4
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.3] - 2026-08-06
|
|
8
|
+
|
|
9
|
+
No change to this package's behaviour, API or wire format. It is republished so
|
|
10
|
+
that all ten packages carry one version, as `VERSIONING.md` section 2 requires.
|
|
11
|
+
|
|
12
|
+
## [1.0.2] - 2026-08-04
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- The engine tested the compare-and-set return value directly, and in Ruby `0`
|
|
16
|
+
is truthy. A store reporting the affected-row count — which `docs/STORE.md`
|
|
17
|
+
told adapters to return — made a **lost** compare-and-set read as applied, so
|
|
18
|
+
two concurrent refreshes each minted a successor and the family forked into
|
|
19
|
+
two independently valid lineages with reuse detection off for it ([N-17],
|
|
20
|
+
[N-18], [N-34] step 5). Counts are normalised now and anything outside the
|
|
21
|
+
contract fails closed as "not applied".
|
|
22
|
+
- A device identifier was decided on the String's encoding tag rather than on
|
|
23
|
+
its bytes, so bytes that every other port accepts were refused when tagged
|
|
24
|
+
`ASCII-8BIT` — from `String#b`, `File.binread` or a socket read. In `refresh`
|
|
25
|
+
that is a sender-binding failure, so the whole family was revoked where the
|
|
26
|
+
other nine rotate normally ([N-11], [N-32]). [N-12]'s treatment of invalid
|
|
27
|
+
Unicode is unchanged. Ruby is the only one of the ten where a string carries a
|
|
28
|
+
declared encoding distinct from its bytes, so it is the only port this could
|
|
29
|
+
affect; erratum E-1 records the clarification to [N-11], and the shared cases
|
|
30
|
+
`dh-08`/`dh-09` now pin it for every implementation.
|
|
31
|
+
|
|
7
32
|
## [1.0.1] - 2026-07-30
|
|
8
33
|
|
|
9
34
|
Release automation only; no change to this package's behaviour, API or wire
|
|
@@ -24,5 +49,7 @@ workflow failed, and neither permits republishing a used version number.
|
|
|
24
49
|
window, family revocation, sender (device) binding, dual expiry clocks,
|
|
25
50
|
pepper rotation via `kid`.
|
|
26
51
|
|
|
52
|
+
[1.0.3]: https://github.com/nebula-token/nebula-token/releases/tag/v1.0.3
|
|
53
|
+
[1.0.2]: https://github.com/nebula-token/nebula-token/releases/tag/v1.0.2
|
|
27
54
|
[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/
|
|
55
|
+
[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 =
|
|
145
|
-
|
|
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,
|
|
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
|