Leash SDK for Ruby
Server-side Ruby SDK for the Leash platform. One client gives you:
- the authenticated user off the request
- runtime env-var resolution from the Leash secret-source registry
- typed integrations (Gmail, Google Calendar, Google Drive, Linear)
- a generic escape hatch for any provider on the platform
Framework-agnostic: works with Rails, Sinatra, Hanami, or plain Rack — no framework gems required.
Installation
Add to your Gemfile:
gem "leash-sdk"
…or install directly:
gem install leash-sdk
Requires Ruby >= 2.7 (Ruby 3.0+ recommended — 2.7 is EOL since 2023-03; system macOS Ruby 2.6.x is below the floor and will need a newer Ruby via rbenv/asdf/homebrew). The only runtime dependency is jwt.
Setup
Set the platform API key in your environment:
export LEASH_API_KEY=lsk_live_...
The constructor reads LEASH_API_KEY automatically; you can also pass api_key: explicitly.
Usage
Rails
# app/controllers/inbox_controller.rb
class InboxController < ApplicationController
def index
leash = Leash.new(request: request)
if leash.auth.authenticated?
@user = leash.auth.user # Leash::User or nil
@messages = leash.integrations.gmail.(max_results: 10)
else
redirect_to "/login"
end
end
end
Sinatra
require "sinatra"
require "leash"
get "/me" do
leash = Leash.new(request: request)
user = leash.auth.user
halt 401 unless user
user.to_h.to_json
end
Plain Rack
class MyApp
def call(env)
leash = Leash.new(request: env) # Rack `env` hash works directly
secret = leash.env.get("OPENAI_API_KEY")
[200, {}, [secret ? "ok" : "missing"]]
end
end
What you get
leash.auth
user = leash.auth.user # Leash::User or nil — never raises
leash.auth.authenticated? # Boolean
leash.env
key = leash.env.get("OPENAI_API_KEY") # String or nil (nil = not declared)
fresh = leash.env.get("STRIPE_KEY", fresh: true)
many = leash.env.get_many(["A", "B"]) # { "A" => "...", "B" => nil }
Per-instance TTL cache: 60 seconds. Pass fresh: true to bypass the cache for one read.
leash.integrations
Typed providers:
# Gmail
leash.integrations.gmail.(max_results: 5)
leash.integrations.gmail.("msg-id")
leash.integrations.gmail.(to: "a@b.com", subject: "Hi", body: "Hello")
leash.integrations.gmail.("from:x@y.com")
leash.integrations.gmail.list_labels
leash.integrations.gmail.get_profile
# Google Calendar (also addressable as leash.integrations.google_calendar)
leash.integrations.calendar.list_calendars
leash.integrations.calendar.list_events(time_min: "2026-01-01T00:00:00Z")
leash.integrations.calendar.create_event(
summary: "Standup",
start: { "dateTime" => "2026-05-15T10:00:00Z" },
end_time: { "dateTime" => "2026-05-15T10:30:00Z" }
)
leash.integrations.calendar.get_event("evt-1")
# Google Drive (also addressable as leash.integrations.google_drive)
leash.integrations.drive.list_files(query: "name contains 'q'")
leash.integrations.drive.get_file("file-id")
leash.integrations.drive.download_file("file-id")
leash.integrations.drive.create_folder("Receipts", parent_id: "p-1")
leash.integrations.drive.upload_file(name: "x.txt", content: "data", mime_type: "text/plain")
leash.integrations.drive.delete_file("file-id")
leash.integrations.drive.search_files("invoice")
# Linear
leash.integrations.linear.list_issues(state_type: "started")
leash.integrations.linear.get_issue("LEA-123")
leash.integrations.linear.create_issue(team_id: "t-1", title: "Build it")
leash.integrations.linear.update_issue("LEA-123", priority: 1)
leash.integrations.linear.add_comment("LEA-123", "ship it")
leash.integrations.linear.list_teams
leash.integrations.linear.list_projects
Generic escape hatch for any platform-registered provider (Slack, GitHub, HubSpot, Jira, …):
leash.integrations.provider("slack").call("post_message",
body: { "channel" => "#general", "text" => "hi" })
Errors
Every call raises a Leash::Error (or one of its subclasses) on failure:
| Class | Raised when |
|---|---|
Leash::UnauthorizedError |
platform returned 401 (missing/invalid creds) |
Leash::ConnectionRequiredError |
403 (provider not connected for this user) |
Leash::UpgradeRequiredError |
402 (feature gated behind a higher plan) |
Leash::KeyNotDeclaredError |
manually raised for env mis-declarations |
Leash::NetworkError |
transport-level failures (DNS, refused, timeout) |
Leash::Error |
base class — also raised for generic 4xx/5xx |
Every error carries code, message, action, see_also, status, and (where present) connect_url.
For convenience, the 0.3 aliases Leash::NotConnectedError and Leash::PlanBlockError are still available.
Authentication precedence
The constructor inspects the request in this order:
LEASH_API_KEYenv var (or explicitapi_key:constructor arg) — server-only, never request-bound.Authorization: Bearer <jwt>header on the request — used byauth.userand as an env-read fallback when no API key is present. Never forwarded on integration POSTs.leash-authcookie on the request — the standard browser → deployed-app session.
The Bearer-token → env fallback is intentional so CLI/agent flows can read env-vars with a user JWT when no app key is provisioned. The same JWT is never sent on integration POSTs because the platform's verifyToken() rejects it before the API-key check runs, producing a misleading 401.
What's NOT in 0.4 yet
create_dev_auth_handler/attach_local_dev_handler— there's no Rack-side equivalent shipped in 0.4. (The TS SDK has aLeash.createDevAuthHandlerfor Next.js routes.) If you need a Ruby local-dev cookie-exchange flow, open an issue.- Legacy
LeashIntegrationsclass — the 0.3Leash::Integrations.new(auth_token: ..., api_key: ...)constructor and itsgmail/calendar/driveaccessors have been replaced byLeash.new(request: ...)+ the namespaced.integrationsaccessor. The TS SDK dropped the equivalent class in 0.4 too. - Browser-mode usage — server-only in 0.4.
If you're upgrading from 0.3.x, the Leash::Auth.get_user(request) helper and the Leash::User / Leash::Error / Leash::NotConnectedError / Leash::TokenExpiredError classes are kept for backwards compatibility.
Testing
bundle install
bundle exec rake test
License
Apache 2.0.