Module: Everywhere::Jump

Defined in:
lib/everywhere/jump.rb

Overview

Jump: the companion app that previews RubyEverywhere apps on a real device (spec: platform/docs/jump.md). The server side is two endpoints, live only while every preview runs (EVERYWHERE_JUMP_TOKEN set):

POST /everywhere/jump/session   {token, device_name} -> {bearer}
GET  /everywhere/jump_v1.json   Authorization: Bearer ... -> manifest

The pairing token is per-run and multi-use (a forwarded link must work for a second coworker's device); each device exchanges it once for a bearer. Bearers are stateless HMACs keyed on the token — no session store, and every bearer dies with the run because the token does.

Constant Summary collapse

PROTOCOL =
1
SESSION_PATH =
"/everywhere/jump/session"
MANIFEST_PATH =
"/everywhere/jump_v1.json"
LEAVE_PATH =
"/everywhere/jump/leave"
CORS_HEADERS =

The connect page lives on the Jump site; the endpoints live on the previewed app — always cross-origin. * is safe here: nothing is cookie-authenticated, the token/bearer IS the credential.

{
  "access-control-allow-origin" => "*",
  "access-control-allow-methods" => "GET, POST, OPTIONS",
  "access-control-allow-headers" => "authorization, content-type",
  "access-control-max-age" => "600"
}.freeze

Class Method Summary collapse

Class Method Details

.connect_log_line(device_name) ⇒ Object



115
116
117
118
119
# File 'lib/everywhere/jump.rb', line 115

def connect_log_line(device_name)
  name = device_name.to_s.gsub(/[[:cntrl:]]/, "").strip[0, 64]
  name = "A device" if name.empty?
  "[everywhere] Jump connected: #{name}"
end

.enabled?Boolean

Returns:

  • (Boolean)


26
# File 'lib/everywhere/jump.rb', line 26

def enabled? = !token.to_s.empty?

.exchange(candidate, device_name) ⇒ Object

nil on a bad token; the session hash otherwise. Logs to stderr so the connect shows up in the terminal every preview is pumping.



52
53
54
55
56
57
# File 'lib/everywhere/jump.rb', line 52

def exchange(candidate, device_name)
  return nil unless valid_token?(candidate)

  warn connect_log_line(device_name)
  { "bearer" => mint_bearer, "jump_protocol" => PROTOCOL }
end

.exit_tab(os) ⇒ Object



109
110
111
112
113
# File 'lib/everywhere/jump.rb', line 109

def exit_tab(os)
  { "title" => "Jump",
    "path" => LEAVE_PATH,
    "icon" => os.to_s == "android" ? "u_turn_left" : "arrow.uturn.backward.circle" }
end

.exit_tabs(config, os, tabs) ⇒ Object

Appended to the previewed app's resolved tabs while preview runs: after instance.set the previewed app's path configuration is the ONLY chrome Jump has, so without this tab there is no way back to the launcher. A tabless app gets a pair — its own content as tab one, the exit as tab two — because a lone exit tab would replace the app's content outright.



100
101
102
103
104
105
106
107
# File 'lib/everywhere/jump.rb', line 100

def exit_tabs(config, os, tabs)
  return tabs + [exit_tab(os)] unless tabs.empty?

  app_tab = { "title" => config.name,
              "path" => config.entry_path || "/",
              "icon" => os.to_s == "android" ? "apps" : "app" }
  [app_tab, exit_tab(os)]
end

.manifest_hash(config) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/everywhere/jump.rb', line 28

def manifest_hash(config)
  {
    "version" => 1,
    "jump_protocol" => PROTOCOL,
    "app" => { "name" => config.name, "identifier" => config.bundle_id },
    "appearance" => {
      "tint_color" => config.tint_color,
      "background_color" => config.background_color
    }.compact,
    "entry_path" => config.entry_path,
    "remote_instances" => (true if config.remote_instances?),
    "path_configuration" => {
      "ios" => "/everywhere/ios_v1.json",
      "android" => "/everywhere/android_v1.json"
    },
    "native_extensions" => [
      ("ios" if config.native_ios?),
      ("android" if config.native_android?)
    ].compact
  }.compact
end

.mint_bearer(tok = token) ⇒ Object



59
60
61
62
# File 'lib/everywhere/jump.rb', line 59

def mint_bearer(tok = token)
  nonce = SecureRandom.hex(12)
  "v1.#{nonce}.#{signature(tok, nonce)}"
end

.signature(tok, nonce) ⇒ Object



79
80
81
# File 'lib/everywhere/jump.rb', line 79

def signature(tok, nonce)
  OpenSSL::HMAC.hexdigest("SHA256", tok, "jump-session-v1:#{nonce}")
end

.tokenObject



25
# File 'lib/everywhere/jump.rb', line 25

def token = ENV["EVERYWHERE_JUMP_TOKEN"]

.valid_bearer?(bearer, tok = token) ⇒ Boolean

Returns:

  • (Boolean)


64
65
66
67
68
69
70
# File 'lib/everywhere/jump.rb', line 64

def valid_bearer?(bearer, tok = token)
  version, nonce, mac = bearer.to_s.split(".", 3)
  return false unless version == "v1" && nonce && mac && !tok.to_s.empty?

  expected = signature(tok, nonce)
  mac.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(mac, expected)
end

.valid_token?(candidate, tok = token) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
75
76
77
# File 'lib/everywhere/jump.rb', line 72

def valid_token?(candidate, tok = token)
  candidate = candidate.to_s
  return false if tok.to_s.empty? || candidate.empty?

  candidate.bytesize == tok.bytesize && OpenSSL.fixed_length_secure_compare(candidate, tok)
end