nebula-token (Ruby)
Ruby implementation of NEBULA — opaque rotating refresh tokens (RFC 9700 model). Standard library only (openssl, securerandom), no gem dependencies, Ruby ≥ 3.3.
gem install nebula-token
require 'nebula_token' # or 'nebula-token'
engine = NebulaToken::Engine.new(
peppers: { 'k1' => ENV.fetch('NEBULA_PEPPER_K1') }, # >= 32 BYTES
active_kid: 'k1',
store: NebulaToken::MemoryRefreshTokenStore.new, # dev only — see examples/pg_store.rb
reuse_grace_seconds: 0 # strict; see [N-30] before raising this
)
issued = engine.issue('usr_1', device_id) # .token .user_id .family_id .generation
# .expires_at .idle_expires_at (unix seconds)
result = engine.refresh(issued.token, device_id)
if result.ok?
result.token # the NEW token; the presented one is dead
else
result.error # 'REUSE_DETECTED', 'CONFLICT', …
end
refresh never raises for a protocol outcome — failures are values ([N-29]) — and a store
error is never converted into one: it propagates, so the caller fails closed ([N-20]).
'CONFLICT' means a concurrent refresh won the compare-and-set; nothing was rotated and the
client may retry once ([N-35]). Treat the code set as open: give every case result.error
an else branch that denies access ([N-40]).
Revocation:
engine.revoke_token(token) # authenticated: proves the verifier; -> .ok? .revoked
engine.revoke_family(family_id) # administrative -> Integer
engine.revoke_all_for_user(user_id) # administrative -> Integer
Store: implement the six methods of NebulaToken::RefreshTokenStore
over your database — find_by_selector, insert, mark_rotated, revoke_if_active,
revoke_family, revoke_user. The two compare-and-set methods must apply their write only
when the current status still matches ([N-17], [N-18]); start from
examples/pg_store.rb, schema in docs/STORE.md.
Test (no bundler, no dependencies):
ruby -Ilib test/conformance_test.rb # spec/test-vectors.json
ruby -Ilib test/behavior_test.rb # spec/behavior-vectors.json
ruby -Ilib test/engine_test.rb # Ruby-specific properties
Agent skill: skills/nebula-token-ruby/SKILL.md teaches an AI coding assistant to integrate this package correctly. Install it with the standard agent-skill installer:
npx skills add nebula-token/nebula-token --skill nebula-token-ruby
That lands in the project's .claude/skills/, checked in and shared with your team; add -g for your personal ~/.claude/skills/ instead. In Claude Code it is equally a plugin — /plugin marketplace add nebula-token/nebula-token, then /plugin install nebula-token-ruby@nebula-token. Neither route copies anything: both read this directory in place, through .claude-plugin/marketplace.json.
As a fallback the skill also ships inside the published artefact, in the same installable shape — after gem install nebula-token the directory is at <gem dir>/gems/nebula-token-X.Y.Z/skills/nebula-token-ruby/, and copying that directory into ~/.claude/skills/ is the whole installation. All ten skills and every install route are in skills/README.md.