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 =

Returns Hex-encoded public key for license verification.

Returns:

  • (String)

    Hex-encoded public key for license verification.

'91f54336c0d42a7c88642b8d7f8bacab3adba4a4d633a33002fc15f676bfd8f0'
RAI_PREFIX =

Returns Prefix for rosett-ai license key strings.

Returns:

  • (String)

    Prefix for rosett-ai license key strings.

'NNCC-'
GRACE_PERIOD_DAYS =

Returns Days of grace period after license expiry.

Returns:

  • (Integer)

    Days of grace period after license expiry.

14
OFFLINE_GRACE_DAYS =

Returns Days of offline operation before license revalidation.

Returns:

  • (Integer)

    Days of offline operation before license revalidation.

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-"



30
31
32
33
34
# File 'lib/rosett_ai/licensing/license_key.rb', line 30

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

Instance Attribute Details

#claimsObject (readonly)

Returns the value of attribute claims.



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

def claims
  @claims
end

Instance Method Details

#decodeself

Decodes and verifies the Ed25519 JWT signature.

Returns:

  • (self)

Raises:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/rosett_ai/licensing/license_key.rb', line 40

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)


76
77
78
79
80
81
82
83
84
# File 'lib/rosett_ai/licensing/license_key.rb', line 76

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)



105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/rosett_ai/licensing/license_key.rb', line 105

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)


122
123
124
125
126
127
128
129
130
# File 'lib/rosett_ai/licensing/license_key.rb', line 122

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)


89
90
91
92
# File 'lib/rosett_ai/licensing/license_key.rb', line 89

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

#subscriber?Boolean

Whether the license tier is subscriber or enterprise.

Returns:

  • (Boolean)


97
98
99
100
# File 'lib/rosett_ai/licensing/license_key.rb', line 97

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

#tierTier

Returns the license tier.

Returns:

  • (Tier)

    tier from claims, defaults to community



68
69
70
71
# File 'lib/rosett_ai/licensing/license_key.rb', line 68

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

#valid?Boolean

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

Returns:

  • (Boolean)


58
59
60
61
62
63
# File 'lib/rosett_ai/licensing/license_key.rb', line 58

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