Class: RosettAi::Licensing::LicenseKey

Inherits:
Object
  • Object
show all
Defined in:
lib/rosett_ai/licensing/license_key.rb

Overview

Decodes and validates Ed25519-signed JWT license keys.

The public verification key is safe to embed in source — it can only verify signatures, not forge them. The private signing key is held server-side and NEVER distributed.

Constant Summary collapse

PUBLIC_KEY_HEX =
'91f54336c0d42a7c88642b8d7f8bacab3adba4a4d633a33002fc15f676bfd8f0'
RAI_PREFIX =
'NNCC-'
GRACE_PERIOD_DAYS =
14
OFFLINE_GRACE_DAYS =
30

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(raw_key) ⇒ LicenseKey

Returns a new instance of LicenseKey.

Parameters:

  • raw_key (String)

    JWT license key, optionally prefixed with "NNCC-"



26
27
28
29
30
# File 'lib/rosett_ai/licensing/license_key.rb', line 26

def initialize(raw_key)
  @raw_key = raw_key
  @claims = nil
  @decoded = false
end

Instance Attribute Details

#claimsObject (readonly)

Returns the value of attribute claims.



23
24
25
# File 'lib/rosett_ai/licensing/license_key.rb', line 23

def claims
  @claims
end

Instance Method Details

#decodeself

Decodes and verifies the Ed25519 JWT signature.

Returns:

  • (self)

Raises:



36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/rosett_ai/licensing/license_key.rb', line 36

def decode
  return self if @decoded

  stripped = strip_prefix(@raw_key)
  verify_key = Ed25519::VerifyKey.new([PUBLIC_KEY_HEX].pack('H*'))
  decoded = JWT.decode(stripped, verify_key, true, algorithms: ['EdDSA'], verify_expiration: false)
  @claims = decoded.first
  @decoded = true
  self
rescue Ed25519::VerifyError, JWT::DecodeError, JWT::VerificationError => e
  raise RosettAi::LicenseError, "signature verification failed: #{e.message}"
rescue ArgumentError, TypeError => e
  raise RosettAi::LicenseError, "invalid key format: #{e.message}"
end

#expired?Boolean

Whether the license has passed its expiration timestamp.

Returns:

  • (Boolean)


72
73
74
75
76
77
78
79
80
# File 'lib/rosett_ai/licensing/license_key.rb', line 72

def expired?
  decode
  return false if perpetual?

  exp = claims['exp']
  return false unless exp

  Time.now.to_i > exp
end

#grace_remainingInteger?

Days remaining in the post-expiry grace period.

Returns:

  • (Integer, nil)

    days remaining (0 if exhausted, nil if perpetual or no expiry)



101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/rosett_ai/licensing/license_key.rb', line 101

def grace_remaining
  decode
  return nil if perpetual?

  exp = claims['exp']
  return nil unless exp

  elapsed = (Time.now.to_i - exp) / 86_400
  return GRACE_PERIOD_DAYS if elapsed.negative?

  remaining = GRACE_PERIOD_DAYS - elapsed
  [remaining, 0].max
end

#offline_valid?Boolean

Whether the license is valid for offline use.

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
126
# File 'lib/rosett_ai/licensing/license_key.rb', line 118

def offline_valid?
  decode
  return true if perpetual?

  exp = claims['exp']
  return true unless exp

  (Time.now.to_i - exp) < (OFFLINE_GRACE_DAYS * 86_400)
end

#perpetual?Boolean

Whether the license is perpetual (never expires).

Returns:

  • (Boolean)


85
86
87
88
# File 'lib/rosett_ai/licensing/license_key.rb', line 85

def perpetual?
  decode
  claims['perpetual'] == true
end

#subscriber?Boolean

Whether the license tier is subscriber or enterprise.

Returns:

  • (Boolean)


93
94
95
96
# File 'lib/rosett_ai/licensing/license_key.rb', line 93

def subscriber?
  decode
  ['subscriber', 'enterprise'].include?(claims['tier'])
end

#tierTier

Returns the license tier.

Returns:

  • (Tier)

    tier from claims, defaults to community



64
65
66
67
# File 'lib/rosett_ai/licensing/license_key.rb', line 64

def tier
  decode
  Tier.new(claims['tier'] || 'community')
end

#valid?Boolean

Whether the license is currently valid (not expired or within grace).

Returns:

  • (Boolean)


54
55
56
57
58
59
# File 'lib/rosett_ai/licensing/license_key.rb', line 54

def valid?
  decode
  !expired? || within_grace?
rescue RosettAi::LicenseError
  false
end