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 +4 -4
- data/Gemfile +2 -0
- data/README.md +19 -0
- data/leash-sdk.gemspec +2 -2
- data/lib/leash/auth.rb +141 -0
- data/lib/leash/integrations.rb +61 -0
- data/lib/leash/version.rb +5 -0
- data/lib/leash.rb +2 -4
- metadata +19 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ccdeebdeeb2eea92794836b7ae51f02cc1c048a3912483c8c410e662184c5f4
|
|
4
|
+
data.tar.gz: 9626c0261ec8d76cd14323b290c558be5df6707b0c7cdb6a7cc780feaeeee079
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 03d22fe090135500591a41e86aadb0fe90409a0b572f43d9671362ce989737e9f238a987d4665e6a32a0d1197c1e9fb21b97039e836cfc2d1d28cea9cf4938a3
|
|
7
|
+
data.tar.gz: 5a0c39178994eff5875f52793d772b52cc0f0304d0c17bd63e6f7fce742909b8ff373ec2545a121f30c4762d0712bb5bcbb09e23e7147c43f43e77294af00485
|
data/Gemfile
CHANGED
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
|
-
|
|
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
|
data/lib/leash/integrations.rb
CHANGED
|
@@ -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
|
data/lib/leash.rb
CHANGED
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.
|
|
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-
|
|
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
|