Class: Assistant::EmailRenderToken

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

Overview

Signed, single-use, short-lived token that authorizes a server-side Playwright
session to open the email render harness for ONE EmailTemplate and push the
Redactor getEmail() result back. Modeled on Masquerade::HandoffToken.

The token carries:

  • email_template_id - the template whose body_v4 will be rendered
  • lock_version - the template's optimistic-lock version at mint time;
    the save-back rejects if body_v4 changed underneath
    (stale render must not clobber newer source)
  • nonce - single-use marker (Redis SETNX)

Security properties:

  • Signed with Rails.application.message_verifier - tamper-evident
  • TTL of TOKEN_TTL seconds - stale links rejected
  • Single-use via Rails.cache SETNX on the nonce - replay-rejected
  • Scoped to render-only on one template id - exposes nothing else

Defined Under Namespace

Classes: Error, ExpiredTokenError, InvalidTokenError, ReplayError

Constant Summary collapse

VERIFIER_PURPOSE =

Verifier purpose.

:assistant_email_render
TOKEN_TTL =

Token ttl — generous enough for a headless page load + getEmail().

120.seconds
NONCE_TTL =

Nonce ttl.

10.minutes
NONCE_CACHE_PREFIX =

Nonce cache prefix.

'assistant:email_render:consumed:'

Class Method Summary collapse

Class Method Details

.decode!(token) ⇒ Object

Verify signature + TTL WITHOUT consuming the nonce — used by the harness
GET (page load) so the single POST save-back can consume it. The nonce is
still single-use; only the write path consumes it.



68
69
70
71
72
73
74
75
76
77
# File 'app/services/assistant/email_render_token.rb', line 68

def decode!(token)
  raise InvalidTokenError, 'Missing token' if token.blank?

  payload = verifier.verify(token)
  validate_payload!(payload)
  validate_freshness!(payload)
  payload
rescue ActiveSupport::MessageVerifier::InvalidSignature
  raise InvalidTokenError, 'Invalid or tampered token'
end

.decode_and_consume!(token) ⇒ Object

Verify signature + TTL, then atomically consume the nonce. Raises on any
failure; returns the payload hash (symbol keys) on success.



53
54
55
56
57
58
59
60
61
62
63
# File 'app/services/assistant/email_render_token.rb', line 53

def decode_and_consume!(token)
  raise InvalidTokenError, 'Missing token' if token.blank?

  payload = verifier.verify(token)
  validate_payload!(payload)
  validate_freshness!(payload)
  consume_nonce!(payload[:nonce])
  payload
rescue ActiveSupport::MessageVerifier::InvalidSignature
  raise InvalidTokenError, 'Invalid or tampered token'
end

.encode(email_template_id:, lock_version:, nonce:) ⇒ Object



40
41
42
43
44
45
46
47
48
49
# File 'app/services/assistant/email_render_token.rb', line 40

def encode(email_template_id:, lock_version:, nonce:)
  verifier.generate(
    {
      email_template_id: email_template_id,
      lock_version:      lock_version,
      nonce:             nonce,
      iat:               Time.current.to_i
    }
  )
end