leash-sdk 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 326ce87e27b0aec22d2cf9175c3ed7b170b493a7e7eb86b55fc115806b5c84a1
4
- data.tar.gz: a4cced54f08f447c0dc703524c6ce84fcd07173f685f6a8ee0e76dc3dda1065a
3
+ metadata.gz: 3ccdeebdeeb2eea92794836b7ae51f02cc1c048a3912483c8c410e662184c5f4
4
+ data.tar.gz: 9626c0261ec8d76cd14323b290c558be5df6707b0c7cdb6a7cc780feaeeee079
5
5
  SHA512:
6
- metadata.gz: fba497998357aaaa579aec3d3c3539f73c1a2871b8dfc9922dc9ce7e7d238f3587bd12c4a6ccf50e0cf9a149e1926915218aa270f775091d39cb6c2d9025d059
7
- data.tar.gz: db78fcf8d0e38fc9f575dc03d0612d2e3ce228dbc0869e23d609e18604f1b634929e3215ac66c480f0730c50e2c8c666203b67eb31dd960d461eadb7ccc66a03
6
+ metadata.gz: 03d22fe090135500591a41e86aadb0fe90409a0b572f43d9671362ce989737e9f238a987d4665e6a32a0d1197c1e9fb21b97039e836cfc2d1d28cea9cf4938a3
7
+ data.tar.gz: 5a0c39178994eff5875f52793d772b52cc0f0304d0c17bd63e6f7fce742909b8ff373ec2545a121f30c4762d0712bb5bcbb09e23e7147c43f43e77294af00485
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
+
7
+ gem "minitest", ">= 5.0"
data/README.md CHANGED
@@ -49,6 +49,25 @@ end
49
49
  - custom integration calls
50
50
  - app env fetch and caching
51
51
 
52
+ ## Server Auth
53
+
54
+ The SDK includes helpers for authenticating users on the server side by reading
55
+ the `leash-auth` cookie set by the Leash platform.
56
+
57
+ ```ruby
58
+ # Rails / Sinatra
59
+ user = Leash::Auth.get_user(request)
60
+ # => #<Leash::User id="usr_123" email="alice@example.com" name="Alice">
61
+ ```
62
+
63
+ ## MCP Calls
64
+
65
+ Execute MCP-backed tools through the platform:
66
+
67
+ ```ruby
68
+ result = client.run_mcp(package: "@some/mcp-package", tool: "tool-name", args: { key: "value" })
69
+ ```
70
+
52
71
  ## Notes
53
72
 
54
73
  - `auth_token` should be a valid Leash platform JWT
data/leash-sdk.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/leash"
3
+ require_relative "lib/leash/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "leash-sdk"
@@ -21,5 +21,5 @@ Gem::Specification.new do |spec|
21
21
  spec.files = Dir["lib/**/*.rb"] + ["leash-sdk.gemspec", "Gemfile", "README.md", "LICENSE"]
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- # No runtime dependencies -- stdlib only (net/http, json, uri).
24
+ spec.add_dependency "jwt", ">= 2.7"
25
25
  end
data/lib/leash/auth.rb ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+ require_relative "errors"
5
+
6
+ module Leash
7
+ # Raised when authentication fails (missing cookie, invalid/expired token, etc.)
8
+ class AuthError < Error
9
+ def initialize(message = "Authentication failed")
10
+ super(message, code: "auth_error")
11
+ end
12
+ end
13
+
14
+ # Simple value object representing an authenticated Leash user.
15
+ class User
16
+ attr_reader :id, :email, :name, :picture
17
+
18
+ # @param id [String]
19
+ # @param email [String]
20
+ # @param name [String, nil]
21
+ # @param picture [String, nil]
22
+ def initialize(id:, email:, name: nil, picture: nil)
23
+ @id = id
24
+ @email = email
25
+ @name = name
26
+ @picture = picture
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(User) &&
31
+ id == other.id &&
32
+ email == other.email &&
33
+ name == other.name &&
34
+ picture == other.picture
35
+ end
36
+ end
37
+
38
+ # Framework-agnostic server auth helper.
39
+ #
40
+ # Works with any request object that exposes either:
41
+ # - request.cookies (Hash) — Rack / Rails / Sinatra
42
+ # - request.env['HTTP_COOKIE'] or request.get_header('HTTP_COOKIE') — raw Rack env
43
+ #
44
+ # Does NOT require rails, sinatra, or rack.
45
+ module Auth
46
+ COOKIE_NAME = "leash-auth"
47
+
48
+ module_function
49
+
50
+ # Read the leash-auth JWT from the request, decode it, and return a {Leash::User}.
51
+ #
52
+ # @param request [#cookies, #env, #get_header] any Rack-like request object
53
+ # @return [Leash::User]
54
+ # @raise [Leash::AuthError] when the cookie is missing or the token is invalid/expired
55
+ def get_user(request)
56
+ token = extract_token(request)
57
+ raise AuthError, "Missing leash-auth cookie" if token.nil? || token.empty?
58
+
59
+ payload = decode_token(token)
60
+ build_user(payload)
61
+ end
62
+
63
+ # Check whether the request carries a valid leash-auth cookie.
64
+ #
65
+ # @param request [#cookies, #env, #get_header]
66
+ # @return [Boolean]
67
+ def authenticated?(request)
68
+ get_user(request)
69
+ true
70
+ rescue AuthError
71
+ false
72
+ end
73
+
74
+ # @api private
75
+ def extract_token(request)
76
+ # Strategy 1: request.cookies hash (Rack / Rails / Sinatra)
77
+ if request.respond_to?(:cookies)
78
+ cookies = request.cookies
79
+ if cookies.is_a?(Hash)
80
+ value = cookies[COOKIE_NAME] || cookies[COOKIE_NAME.to_sym]
81
+ return value if value
82
+ end
83
+ end
84
+
85
+ # Strategy 2: raw Cookie header from env or get_header
86
+ raw = nil
87
+ if request.respond_to?(:env) && request.env.is_a?(Hash)
88
+ raw = request.env["HTTP_COOKIE"]
89
+ end
90
+ if raw.nil? && request.respond_to?(:get_header)
91
+ begin
92
+ raw = request.get_header("HTTP_COOKIE")
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ end
97
+
98
+ parse_cookie_header(raw) if raw
99
+ end
100
+
101
+ # @api private
102
+ def parse_cookie_header(header)
103
+ return nil if header.nil?
104
+
105
+ header.split(";").each do |pair|
106
+ key, value = pair.strip.split("=", 2)
107
+ return value if key == COOKIE_NAME
108
+ end
109
+ nil
110
+ end
111
+
112
+ # @api private
113
+ def decode_token(token)
114
+ secret = ENV["LEASH_JWT_SECRET"]
115
+ if secret && !secret.empty?
116
+ decoded = JWT.decode(token, secret, true, algorithms: ["HS256"])
117
+ else
118
+ decoded = JWT.decode(token, nil, false)
119
+ end
120
+ decoded.first
121
+ rescue JWT::ExpiredSignature
122
+ raise AuthError, "Token has expired"
123
+ rescue JWT::DecodeError => e
124
+ raise AuthError, "Invalid token: #{e.message}"
125
+ end
126
+
127
+ # @api private
128
+ def build_user(payload)
129
+ id = payload["id"] || payload["sub"]
130
+ email = payload["email"]
131
+ raise AuthError, "Token payload missing required fields (id/sub, email)" unless id && email
132
+
133
+ User.new(
134
+ id: id,
135
+ email: email,
136
+ name: payload["name"],
137
+ picture: payload["picture"]
138
+ )
139
+ end
140
+ end
141
+ end
@@ -139,6 +139,67 @@ module Leash
139
139
  url
140
140
  end
141
141
 
142
+ # Get the user's current access token for a provider -- built-in or
143
+ # org-registered (LEA-142). Lets you call third-party APIs directly
144
+ # without proxying every request through Leash. Refresh-on-expiry
145
+ # happens transparently on the platform side.
146
+ #
147
+ # @param provider [String] the provider slug (e.g. "slack", "gmail")
148
+ # @return [String] the access token
149
+ # @raise [Leash::NotConnectedError] if the user hasn't completed the OAuth flow
150
+ # @raise [Leash::TokenExpiredError] if the token is expired and cannot be refreshed
151
+ # @raise [Leash::Error] if the platform returns a non-success response
152
+ def get_access_token(provider)
153
+ uri = URI("#{@platform_url}/api/integrations/token")
154
+
155
+ request = Net::HTTP::Post.new(uri)
156
+ request["Content-Type"] = "application/json"
157
+ request["Authorization"] = "Bearer #{@auth_token}" if @auth_token
158
+ request["X-API-Key"] = @api_key if @api_key
159
+ request.body = { provider: provider }.to_json
160
+
161
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
162
+ http.request(request)
163
+ end
164
+
165
+ data = JSON.parse(response.body)
166
+
167
+ unless data["success"]
168
+ raise_error(data)
169
+ end
170
+
171
+ data["data"]["accessToken"]
172
+ end
173
+
174
+ # Get the resolved config for a customer-registered MCP server (LEA-143).
175
+ # Returns the customer's MCP URL plus auth headers (e.g. +Authorization:
176
+ # Bearer ...+ for bearer-auth servers) -- feed this directly into your
177
+ # MCP client. Leash isn't on the MCP request path.
178
+ #
179
+ # @param slug [String] the MCP server slug
180
+ # @return [Hash] hash with "slug", "displayName", "url", and "headers"
181
+ # @raise [Leash::Error] if the platform returns a non-success response
182
+ # (e.g. code +unknown_mcp_server+)
183
+ def get_custom_mcp_config(slug)
184
+ uri = URI("#{@platform_url}/api/integrations/mcp-config/#{URI.encode_www_form_component(slug)}")
185
+
186
+ request = Net::HTTP::Get.new(uri)
187
+ request["Authorization"] = "Bearer #{@auth_token}" if @auth_token
188
+ request["X-API-Key"] = @api_key if @api_key
189
+
190
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
191
+ http.request(request)
192
+ end
193
+
194
+ data = JSON.parse(response.body)
195
+
196
+ unless data["success"]
197
+ raise_error(data)
198
+ end
199
+
200
+ data["data"]
201
+ end
202
+
142
203
  # Call any MCP server tool directly.
143
204
  #
144
205
  # @param package_name [String] the npm package name of the MCP server
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leash
4
+ VERSION = "0.3.1"
5
+ end
data/lib/leash.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "leash/version"
3
4
  require_relative "leash/integrations"
4
-
5
- module Leash
6
- VERSION = "0.2.1"
7
- end
5
+ require_relative "leash/auth"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leash-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leash
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2026-05-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.7'
13
27
  description: Access Gmail, Google Calendar, Google Drive, and more through the Leash
14
28
  platform proxy. No API keys needed -- uses your Leash auth token.
15
29
  email:
@@ -23,12 +37,14 @@ files:
23
37
  - README.md
24
38
  - leash-sdk.gemspec
25
39
  - lib/leash.rb
40
+ - lib/leash/auth.rb
26
41
  - lib/leash/calendar.rb
27
42
  - lib/leash/custom_integration.rb
28
43
  - lib/leash/drive.rb
29
44
  - lib/leash/errors.rb
30
45
  - lib/leash/gmail.rb
31
46
  - lib/leash/integrations.rb
47
+ - lib/leash/version.rb
32
48
  homepage: https://github.com/leash-build/leash-sdk-ruby
33
49
  licenses:
34
50
  - Apache-2.0