Class: AssemblyaiCallbackTokenService

Inherits:
Object
  • Object
show all
Defined in:
app/services/assemblyai_callback_token_service.rb

Overview

Generates and validates time-limited JWT tokens for AssemblyAI webhook authentication.
Tokens are embedded in the callback URL to prevent unauthorized submissions.

Supports both call records and video transcriptions.

Uses API_HOSTNAME_WITHOUT_PORT constant for environment-aware URLs:

  • Production: api.warmlyyours.com
  • Staging: api.warmlyyours.ws
  • Development: Uses dev tunnel (api-hostname.warmlyyours.dev)

Examples:

Generate a callback URL for call record

AssemblyaiCallbackTokenService.webhook_url(resource_type: 'CallRecord', resource_id: 123)

Generate a callback URL for video

AssemblyaiCallbackTokenService.webhook_url(resource_type: 'Video', resource_id: 456)

Validate a token

AssemblyaiCallbackTokenService.validate_token(params[:token])

Constant Summary collapse

JWT_SECRET =
Rails.application.secret_key_base
JWT_ALGORITHM =
'HS256'
TOKEN_EXPIRY =
24.hours
SUPPORTED_RESOURCE_TYPES =
%w[CallRecord Video].freeze

Class Method Summary collapse

Class Method Details

.call_record_webhook_url(call_record_id:) ⇒ Object

Backwards-compatible methods for call records



112
113
114
# File 'app/services/assemblyai_callback_token_service.rb', line 112

def call_record_webhook_url(call_record_id:)
  webhook_url(resource_type: 'CallRecord', resource_id: call_record_id)
end

.callback_url(resource_type:, resource_id:) ⇒ String

Generate a callback URL for production/staging

Parameters:

  • resource_type (String)

    'CallRecord' or 'Video'

  • resource_id (Integer)

    Resource ID to embed in URL

Returns:

  • (String)

    Complete callback URL



84
85
86
87
# File 'app/services/assemblyai_callback_token_service.rb', line 84

def callback_url(resource_type:, resource_id:)
  token = generate_token(resource_type: resource_type, resource_id: resource_id)
  "https://#{API_HOSTNAME_WITHOUT_PORT}/webhooks/v1/assemblyai?token=#{token}"
end

.dev_callback_url(resource_type:, resource_id:) ⇒ String

Generate a dev callback URL for testing via Cloudflare tunnel

Parameters:

  • resource_type (String)

    'CallRecord' or 'Video'

  • resource_id (Integer)

    Resource ID to embed in URL

Returns:

  • (String)

    Complete dev callback URL



93
94
95
96
97
# File 'app/services/assemblyai_callback_token_service.rb', line 93

def dev_callback_url(resource_type:, resource_id:)
  hostname = `hostname -s`.strip.downcase
  token = generate_token(resource_type: resource_type, resource_id: resource_id)
  "https://api-#{hostname}.warmlyyours.dev/webhooks/v1/assemblyai?token=#{token}"
end

.generate_token(resource_type:, resource_id:) ⇒ String

Generate a JWT token for callback authentication

Parameters:

  • resource_type (String)

    'CallRecord' or 'Video'

  • resource_id (Integer)

    The resource ID to embed in the token

Returns:

  • (String)

    JWT token

Raises:

  • (ArgumentError)


34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/services/assemblyai_callback_token_service.rb', line 34

def generate_token(resource_type:, resource_id:)
  raise ArgumentError, "Unsupported resource type: #{resource_type}" unless SUPPORTED_RESOURCE_TYPES.include?(resource_type)

  payload = {
    purpose: 'assemblyai_callback',
    resource_type: resource_type,
    resource_id: resource_id,
    exp: TOKEN_EXPIRY.from_now.to_i,
    iat: Time.current.to_i,
    jti: SecureRandom.uuid # Unique token ID
  }

  JWT.encode(payload, JWT_SECRET, JWT_ALGORITHM)
end

.valid_token?(token) ⇒ Boolean

Check if a token is valid

Parameters:

  • token (String)

    The JWT token to check

Returns:

  • (Boolean)


76
77
78
# File 'app/services/assemblyai_callback_token_service.rb', line 76

def valid_token?(token)
  validate_token(token).present?
end

.validate_token(token) ⇒ Hash?

Validate and decode a JWT token

Parameters:

  • token (String)

    The JWT token to validate

Returns:

  • (Hash, nil)

    The decoded payload if valid, nil otherwise



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'app/services/assemblyai_callback_token_service.rb', line 52

def validate_token(token)
  return nil if token.blank?

  decoded_token = JWT.decode(token, JWT_SECRET, true, { algorithm: JWT_ALGORITHM })
  payload = decoded_token.first

  # Check if token is for AssemblyAI callback
  return nil unless payload['purpose'] == 'assemblyai_callback'

  # Normalize for backwards compatibility (old tokens used call_record_id)
  if payload['call_record_id'] && !payload['resource_type']
    payload['resource_type'] = 'CallRecord'
    payload['resource_id'] = payload['call_record_id']
  end

  payload
rescue JWT::DecodeError, JWT::ExpiredSignature => e
  Rails.logger.warn "[AssemblyAI Callback] Token validation failed: #{e.class}"
  nil
end

.video_webhook_url(video_id:) ⇒ Object

Methods for videos



117
118
119
# File 'app/services/assemblyai_callback_token_service.rb', line 117

def video_webhook_url(video_id:)
  webhook_url(resource_type: 'Video', resource_id: video_id)
end

.webhook_url(resource_type:, resource_id:) ⇒ String

Get the appropriate callback URL based on environment

Parameters:

  • resource_type (String)

    'CallRecord' or 'Video'

  • resource_id (Integer)

    Resource ID

Returns:

  • (String)

    Callback URL



103
104
105
106
107
108
109
# File 'app/services/assemblyai_callback_token_service.rb', line 103

def webhook_url(resource_type:, resource_id:)
  if Rails.env.development?
    dev_callback_url(resource_type: resource_type, resource_id: resource_id)
  else
    callback_url(resource_type: resource_type, resource_id: resource_id)
  end
end