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
- .connect_log_line(device_name) ⇒ Object
- .enabled? ⇒ Boolean
-
.exchange(candidate, device_name) ⇒ Object
nil on a bad token; the session hash otherwise.
- .exit_tab(os) ⇒ Object
-
.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.
- .manifest_hash(config) ⇒ Object
- .mint_bearer(tok = token) ⇒ Object
- .signature(tok, nonce) ⇒ Object
- .token ⇒ Object
- .valid_bearer?(bearer, tok = token) ⇒ Boolean
- .valid_token?(candidate, tok = token) ⇒ Boolean
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
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 |
.token ⇒ Object
25 |
# File 'lib/everywhere/jump.rb', line 25 def token = ENV["EVERYWHERE_JUMP_TOKEN"] |
.valid_bearer?(bearer, tok = token) ⇒ 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
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 |