Class: AssemblyaiCallbackTokenService

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

Overview

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

Supports both call records and video transcriptions.

Tokens are signed with ActiveSupport::MessageVerifier (Rails primitive)
under the namespaced verifier :assemblyai_callback and the explicit
purpose tag 'assemblyai_callback/v1' — defense in depth against a token
signed for a different feature being replayed against this webhook.

No legacy decoder: this service previously used JWT.encode / JWT.decode,
but those tokens cross neither verifier (different signature format). At
deploy time, any AssemblyAI job already in flight will lose its callback
(the transcription itself completes on AssemblyAI's side; we just never
ingest the WebhookLog). Recovery is to re-trigger the transcription from
the affected CallRecord / Video.

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

TOKEN_EXPIRY =

Token expiry.

24.hours
PURPOSE =

Verifier purpose tag.

'assemblyai_callback/v1'
SUPPORTED_RESOURCE_TYPES =

Recognised supported resource types.

%w[CallRecord Video].freeze

Class Method Summary collapse

Class Method Details

.call_record_webhook_url(call_record_id:) ⇒ String

Backwards-compatible webhook URL builder for call records.
Delegates to webhook_url with resource_type: 'CallRecord'.

Parameters:

  • call_record_id (Integer)

    CallRecord ID to embed in the URL

Returns:

  • (String)

    Complete callback URL (production, staging, or dev tunnel)



110
111
112
# File 'app/services/assemblyai_callback_token_service.rb', line 110

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



79
80
81
82
# File 'app/services/assemblyai_callback_token_service.rb', line 79

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=#{CGI.escape(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



88
89
90
91
92
# File 'app/services/assemblyai_callback_token_service.rb', line 88

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=#{CGI.escape(token)}"
end

.generate_token(resource_type:, resource_id:) ⇒ String

Generate a verifier-signed token for callback authentication.

Parameters:

  • resource_type (String)

    'CallRecord' or 'Video'

  • resource_id (Integer)

    The resource ID to embed in the token

Returns:

  • (String)

    signed token

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
57
# File 'app/services/assemblyai_callback_token_service.rb', line 48

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

  payload = {
    'resource_type' => resource_type,
    'resource_id' => resource_id
  }

  verifier.generate(payload, expires_in: TOKEN_EXPIRY, purpose: PURPOSE)
end

.valid_token?(token) ⇒ Boolean

Check if a token is valid.

Parameters:

  • token (String)

    The token to check

Returns:

  • (Boolean)


71
72
73
# File 'app/services/assemblyai_callback_token_service.rb', line 71

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

.validate_token(token) ⇒ Hash?

Validate and decode a token.

Parameters:

  • token (String)

    The token to validate

Returns:

  • (Hash, nil)

    The decoded payload (string-keyed) if valid, nil otherwise



62
63
64
65
66
# File 'app/services/assemblyai_callback_token_service.rb', line 62

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

  verifier.verified(token, purpose: PURPOSE)
end

.video_webhook_url(video_id:) ⇒ String

Webhook URL builder for video transcriptions.
Delegates to webhook_url with resource_type: 'Video'.

Parameters:

  • video_id (Integer)

    Video ID to embed in the URL

Returns:

  • (String)

    Complete callback URL (production, staging, or dev tunnel)



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

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



98
99
100
101
102
103
104
# File 'app/services/assemblyai_callback_token_service.rb', line 98

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