patchwork-rb 0.1.0 → 0.1.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +85 -68
  3. data/lib/patchwork/version.rb +1 -1
  4. metadata +12 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8b84f16b49af905d391e3f3d47e67339f1fec7cf28f6ee7d002d72b4d4c20aa
4
- data.tar.gz: 9a0d2be627ba90e61da19d3512c1cf0e2520bf132cf11446199f029092958a9d
3
+ metadata.gz: 02aaad7b12e53f7fd6eb119d03cb576a91cb80afa6d488eecd16dcd251839479
4
+ data.tar.gz: 4743b0e1a4841078b6a85db01048bc9a888a76707e9b746b3098657b7f777785
5
5
  SHA512:
6
- metadata.gz: 61db91122e933dfbbe9a96370cd8a2b7db3529b4ffe62f07ba263a7911596cea23cbbc6902dac2a22ad250aca11c4ff185913c756a83dfb04d1d509c65472a52
7
- data.tar.gz: e002d213489d59a7df24aa98aa0f45e5c41b0fbd61e7cffb2323911f04a750566f23c7d5e9c250dd2c030d3401a5c378655ddd10cef44dd06a5d73e276ed5885
6
+ metadata.gz: 865c4584db5879b87c9e6292ef356ec605e04667c072fa535bb2c715e3cfe1d5ee9e28199c06a71d943bb6a30f4dfc9ec1510c5b89cd080109763ceb4c18e40e
7
+ data.tar.gz: 8711b23744bb966e445103a154a5f827bbdd7a149942b3ffcbe24c25b8358a990c455412a74b88bffe0775af6c97df2ec55aae069637ea27d67cf086253b4617
data/README.md CHANGED
@@ -2,18 +2,16 @@
2
2
 
3
3
  The Ruby SDK for [Patchwork](https://usepatchwork.co). It mints session tokens for your users, verifies the tool calls Patchwork makes to your backend, and verifies webhook deliveries.
4
4
 
5
- **Scope, so you know what you are installing.** This version is the *inbound* half: everything Patchwork sends you, and the credentials you hand it. It is the part that is genuinely unpleasant to write by hand — the signature covers the raw body, a rotation puts two `v1=` values in one header, comparisons must be constant-time, and the token allowlist must never accept a symmetric algorithm. A client for *calling* the Patchwork API (`identify`, `extract`, threads, runs) lands in 0.2; until then those are ordinary HTTP calls with a bearer token.
5
+ [Documentation](https://docs.usepatchwork.co) · [Changelog](CHANGELOG.md) · [Issues](https://github.com/chromablue-labs/patchwork-rb/issues)
6
6
 
7
7
  ```ruby
8
8
  # Gemfile
9
9
  gem "patchwork-rb"
10
10
  ```
11
11
 
12
- The gem is `patchwork-rb`, and you `require "patchwork"`. Don't add `gem "patchwork"`: that name belongs to an unrelated gem on RubyGems.
12
+ Install `patchwork-rb` and `require "patchwork"`. Do not add `gem "patchwork"` — that name belongs to an unrelated gem.
13
13
 
14
- Everything here is plain JWT and HMAC that you could write by hand. The gem exists so the security-sensitive parts are right by default: constant-time comparison, secret rotation, bounded clock skew, and an algorithm allowlist that can never accept a symmetric algorithm.
15
-
16
- It holds no opinion about who your users are. The subject you mint for is an opaque string of your choosing. The gem only interprets it when you ask it to, in a format you declare.
14
+ The subject you mint for is an opaque string you choose. Patchwork stores it, partitions threads and memory on it, and hands it back on every tool call, without ever parsing it. Neither does this gem, unless you declare a format (section 2).
17
15
 
18
16
  ## 1. Configure
19
17
 
@@ -51,11 +49,11 @@ A subject is the partition key for a user's threads and memory. Patchwork stores
51
49
  "ws_acme" a whole team, sharing one memory on purpose
52
50
  ```
53
51
 
54
- The **Subjects** guide in the Patchwork docs covers how to choose. The gem supports every shape.
52
+ The [Subjects guide](https://docs.usepatchwork.co/guides/subjects) covers how to choose. Every shape works.
55
53
 
56
54
  **Single-value subjects** need nothing from the gem. Pass the string.
57
55
 
58
- **Composite subjects** are where mistakes happen: a flipped order, or an id that contains the delimiter. You can declare the format once instead:
56
+ **Composite subjects** are worth declaring once, so a flipped order or an id containing the delimiter is caught at the call site:
59
57
 
60
58
  ```ruby
61
59
  # config/initializers/patchwork.rb, next to Patchwork.configure
@@ -74,7 +72,7 @@ Patchwork::Subject.define(:team_id, :user_id, prefix: "slack") # "slac
74
72
  Patchwork::Subject.define(:org_id, :user_id, :project_id, delimiter: "|") # "o1|u2|p3"
75
73
  ```
76
74
 
77
- A format enforces the composition rules so a call site can't get them wrong:
75
+ A format fixes the rules:
78
76
 
79
77
  - **Order comes from the definition.** `encode` is keyword-only, so the order at the call site doesn't matter.
80
78
  - **A part containing the delimiter is refused** at encode time. Otherwise it would be split wrongly on decode.
@@ -91,14 +89,23 @@ If you decode several formats that share a delimiter, try the prefixed ones firs
91
89
 
92
90
  ## 3. Mint tokens for your users
93
91
 
94
- Your frontend asks your backend for a token, and your backend decides who the user is. Patchwork never sees how you authenticate.
92
+ ```ruby
93
+ # config/routes.rb
94
+ post "patchwork/mint", to: "patchwork#mint"
95
+ get "patchwork/jwks", to: "patchwork#jwks"
96
+ post "patchwork/webhook", to: "patchwork#webhook"
97
+ ```
98
+
99
+ `mint` has two callers on one URL, and the signature tells them apart. Your frontend asks for a token for the signed-in user; Patchwork asks, server to server, for a token for a subject it already acts for — that covers relay connections and **every Playground run**. Patchwork's call is a signed `POST` carrying `{"subject": "..."}` and expects a bare `{ token, expires_in }` back. The [Session tokens guide](https://docs.usepatchwork.co/guides/session-tokens) has the wire format, and the [Mint endpoint guide](https://docs.usepatchwork.co/guides/mint-endpoint) covers both branches.
95
100
 
96
101
  ```ruby
97
- class PatchworkTokensController < ApplicationController
98
- before_action :authenticate_user!
102
+ class PatchworkController < ApplicationController
103
+ skip_forgery_protection
104
+
105
+ def mint
106
+ return relay_mint if request.headers["Patchwork-Signature"].present?
99
107
 
100
- def create
101
- # Every component comes from the server session, never from request params.
108
+ authenticate_user!
102
109
  subject = WorkspaceSubject.encode(user_id: current_user.id, workspace_id: current_workspace.id)
103
110
 
104
111
  render json: {
@@ -106,51 +113,38 @@ class PatchworkTokensController < ApplicationController
106
113
  expires_in: Patchwork.config.token_ttl
107
114
  }
108
115
  end
109
- end
110
- ```
111
-
112
- Pass `connection_id:` when the agent has unpinned customer tools. It takes a connection UUID, not a name.
113
-
114
- ### Patchwork minting for you (relay)
115
-
116
- Patchwork also calls your mint endpoint, server to server, when it needs a token for a subject it already acts for. That covers relay connections and **every Playground run**. It sends a signed `POST` with `{"subject": "..."}` and expects a bare `{ token, expires_in }` back. Both callers share your one `mint_url`, and the signature is what tells them apart.
117
-
118
- Give the gateway your mint path and it answers Patchwork's call itself. Your frontend's unsigned request to the same path still reaches your controller above:
119
116
 
120
- ```ruby
121
- Rails.application.config.middleware.use Patchwork::Gateway,
122
- mint_path: "/patchwork/mint",
123
- resolve: ...
124
- ```
117
+ def jwks
118
+ render json: Patchwork::SigningKey.jwks
119
+ end
125
120
 
126
- Without the gateway, verify and mint in your own route:
121
+ private
127
122
 
128
- ```ruby
129
- def create
130
- if request.headers["Patchwork-Signature"].present?
131
- return render json: Patchwork::Mint.relay(body: request.raw_post,
132
- signature: request.headers["Patchwork-Signature"],
133
- path: request.path), status: :created
123
+ def relay_mint
124
+ render json: Patchwork::Mint.relay(
125
+ body: request.raw_post,
126
+ signature: request.headers["Patchwork-Signature"],
127
+ path: request.path
128
+ ), status: :created
129
+ rescue Patchwork::InvalidSignature
130
+ head :unauthorized
131
+ rescue Patchwork::Mint::BadRequest
132
+ head :bad_request
134
133
  end
135
- # ...the direct branch above
136
134
  end
137
135
  ```
138
136
 
139
- Either way the token is minted for exactly the subject Patchwork sent, never a default. An unusable subject is a 400.
137
+ Build the subject from server state, never from request params. On the direct branch that is the whole of your authorisation.
140
138
 
141
- ### Publish your key
139
+ `Mint.relay` mints for exactly the subject Patchwork sent, never a default, and verifies the signature before it reads the subject at all. Pass `connection_id:` to `issue` when the agent has unpinned customer tools; it takes a connection UUID, not a name.
142
140
 
143
- Publish your public key so Patchwork can verify what you signed, and set the URL on your workspace:
141
+ `jwks` publishes the public half of your signing key, so Patchwork can verify what you minted. Set its URL on your workspace.
144
142
 
145
- ```ruby
146
- get "/patchwork/jwks", to: ->(_env) {
147
- [ 200, { "content-type" => "application/json" }, [ Patchwork::SigningKey.jwks.to_json ] ]
148
- }
149
- ```
143
+ If you mount the gateway in section 4, `mint_path: "/patchwork/mint"` makes it answer the relay call before your controller sees it, and you can drop `relay_mint`. Your frontend's unsigned request still reaches `mint`.
150
144
 
151
145
  ## 4. Verify the tool calls Patchwork makes to you
152
146
 
153
- When an agent calls one of your tools, Patchwork sends the request with a `Patchwork-Signature` header and the session token you minted. `Patchwork::Gateway` is Rack middleware that verifies both. It then hands the token's subject to a `resolve` callable, which you write, to decide what the request acts as.
147
+ When an agent calls one of your tools, Patchwork sends the request with a `Patchwork-Signature` header and the session token you minted. `Patchwork::Gateway` is Rack middleware that verifies both. It then hands the token's subject to a `resolve` callable, which you write, to decide what the request acts as. The [Tool calls guide](https://docs.usepatchwork.co/guides/tool-calls) describes what Patchwork sends.
154
148
 
155
149
  ```ruby
156
150
  # config/initializers/patchwork.rb
@@ -236,7 +230,7 @@ Failure behaviour:
236
230
 
237
231
  ### What the signature does and doesn't cover
238
232
 
239
- The signature covers the method, the path and the raw body. **It does not cover the query string**, and Patchwork sends a GET tool's arguments as query parameters. So a GET tool's arguments are not integrity-protected. Anyone who can alter a request in flight (a TLS-terminating proxy, for example) can change them without breaking the signature. Give any tool whose arguments matter, such as ids, amounts or anything that writes, a `POST` binding, where the arguments travel in the signed body.
233
+ The signature covers the method, the path and the raw body, as specified in the [Signing guide](https://docs.usepatchwork.co/guides/signing). **It does not cover the query string**, and Patchwork sends a GET tool's arguments as query parameters. So a GET tool's arguments are not integrity-protected. Anyone who can alter a request in flight (a TLS-terminating proxy, for example) can change them without breaking the signature. Give any tool whose arguments matter, such as ids, amounts or anything that writes, a `POST` binding, where the arguments travel in the signed body.
240
234
 
241
235
  The signed path is the one your app sees. If a proxy rewrites paths before your app does, mount the gateway where the original path is still intact.
242
236
 
@@ -256,31 +250,43 @@ Use a cache that every app process shares. A per-process cache only stops replay
256
250
 
257
251
  ## 5. Health check
258
252
 
259
- The gateway answers `POST /patchwork/up` (also when it's mounted under a prefix) with a proof that only the shared secret can produce. That's how **Test** on a connection in the Patchwork console reports **verified** rather than just reachable. Pass `health_check: false` to handle that route yourself with `Patchwork::HealthCheck.respond(body:)`.
253
+ The gateway answers `POST /patchwork/up` (also when it's mounted under a prefix) with a proof that only the shared secret can produce. That's how **Test** on a connection in the Patchwork console reports **verified** rather than just reachable.
254
+
255
+ Without the gateway, it is another action on the same controller. Route `post "patchwork/up", to: "patchwork#up"`:
256
+
257
+ ```ruby
258
+ def up
259
+ render json: Patchwork::HealthCheck.respond(body: request.raw_post)
260
+ rescue Patchwork::HealthCheck::BadRequest
261
+ head :bad_request
262
+ end
263
+ ```
264
+
265
+ Pass `health_check: false` if you want to own the route while still using the gateway for tool calls.
260
266
 
261
267
  ## 6. Verify webhooks
262
268
 
263
- Webhooks use the same signing scheme, keyed with the **webhook endpoint's own secret**, not the request secret.
269
+ Webhooks use the same signing scheme, keyed with the **webhook endpoint's own secret**, not the request secret. The [Events guide](https://docs.usepatchwork.co/guides/events) lists the event types.
264
270
 
265
- ```ruby
266
- class PatchworkWebhooksController < ActionController::API
267
- def create
268
- event = Patchwork::Webhook.verify!(
269
- body: request.raw_post,
270
- signature: request.headers["Patchwork-Signature"],
271
- secret: ENV.fetch("PATCHWORK_WEBHOOK_SECRET"),
272
- path: "/hooks/patchwork"
273
- )
271
+ Deliveries land on the same controller:
274
272
 
275
- case event.type
276
- when "run.completed" then FulfilJob.perform_later(event.run_id)
277
- when "run.failed" then alert(event.data)
278
- end
273
+ ```ruby
274
+ def webhook
275
+ event = Patchwork::Webhook.verify!(
276
+ body: request.raw_post,
277
+ signature: request.headers["Patchwork-Signature"],
278
+ secret: ENV.fetch("PATCHWORK_WEBHOOK_SECRET"),
279
+ path: "/patchwork/webhook"
280
+ )
279
281
 
280
- head :ok
281
- rescue Patchwork::InvalidSignature
282
- head :unauthorized
282
+ case event.type
283
+ when "run.completed" then FulfilJob.perform_later(event.run_id)
284
+ when "run.failed" then alert(event.data)
283
285
  end
286
+
287
+ head :ok
288
+ rescue Patchwork::InvalidSignature
289
+ head :unauthorized
284
290
  end
285
291
  ```
286
292
 
@@ -299,21 +305,28 @@ end
299
305
 
300
306
  ## MCP login bridge
301
307
 
302
- If you export an agent over MCP, Patchwork sends your user to a login bridge you host, and the bridge hands back a signed subject — the one step only you can do. It is signed with the same key you mint with:
308
+ If you export an agent over MCP, Patchwork sends your user to a login bridge you host, and the bridge hands back a signed subject. It is signed with the same key you mint with:
309
+
310
+ Route `get "patchwork/bridge", to: "patchwork#bridge"` and add the action alongside the others:
303
311
 
304
312
  ```ruby
305
- get "/patchwork/bridge" do
313
+ def bridge
314
+ authenticate_user!
306
315
  subject = WorkspaceSubject.encode(user_id: current_user.id, workspace_id: chosen_workspace.id)
307
316
  assertion = Patchwork::BridgeAssertion.issue(
308
317
  subject: subject,
309
318
  nonce: params[:state],
310
319
  audience: PATCHWORK_CALLBACK
311
320
  )
312
- redirect "#{PATCHWORK_CALLBACK}?state=#{params[:state]}&assertion=#{assertion}"
321
+
322
+ redirect_to "#{PATCHWORK_CALLBACK}?state=#{params[:state]}&assertion=#{assertion}",
323
+ allow_other_host: true
313
324
  end
314
325
  ```
315
326
 
316
- Authenticate the user first, and bake the workspace choice into the subject — that decision is yours, and Patchwork never needs to know your tenants. The assertion is bound to the `state` nonce and the callback audience and lives 120 seconds at most, which is the ceiling the callback enforces; asking for longer raises.
327
+ `allow_other_host:` is required — the redirect leaves your domain.
328
+
329
+ Authenticate the user first, and bake the workspace choice into the subject. The assertion is bound to the `state` nonce and the callback audience, and lives 120 seconds at most. That ceiling is enforced at the callback, so asking for longer raises.
317
330
 
318
331
  ## Errors
319
332
 
@@ -343,6 +356,10 @@ Patchwork::SessionToken.verify(token) # => claims, or raises Patchwork::Invali
343
356
 
344
357
  Ruby 3.1+. One runtime dependency: `jwt` (>= 2.10.3, < 4, excluding 3.0.0–3.1.2, which are affected by GHSA-c32j-vqhx-rx3x). `Patchwork::Gateway` also needs `rack`, and is only defined when Rack is loaded. It works with any Rack app: Rails, Sinatra, Hanami or bare Rack. `Patchwork::Rails::Presented` needs `activesupport` and loads only when you require `patchwork/rails`.
345
358
 
359
+ ## Support
360
+
361
+ Bugs and questions: [github.com/chromablue-labs/patchwork-rb/issues](https://github.com/chromablue-labs/patchwork-rb/issues). The platform documentation is at [docs.usepatchwork.co](https://docs.usepatchwork.co).
362
+
346
363
  ## Licence
347
364
 
348
365
  MIT.
@@ -1,3 +1,3 @@
1
1
  module Patchwork
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchwork-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chromablue Labs
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: jwt
@@ -55,9 +56,8 @@ dependencies:
55
56
  version: 3.1.2
56
57
  description: 'Server-side helpers for integrating with Patchwork: RS256 session tokens,
57
58
  HMAC request signing and verification, webhook signature verification, and Rack
58
- middleware for hosted tool calls. This is the inbound half — everything Patchwork
59
- sends you, and the credentials you hand it. A client for calling the Patchwork API
60
- arrives in 0.2.'
59
+ middleware for the tool calls Patchwork makes to your backend.'
60
+ email:
61
61
  executables: []
62
62
  extensions: []
63
63
  extra_rdoc_files: []
@@ -86,9 +86,12 @@ licenses:
86
86
  - MIT
87
87
  metadata:
88
88
  homepage_uri: https://usepatchwork.co
89
- source_code_uri: https://github.com/chromablue-labs/crossbar
90
- bug_tracker_uri: https://github.com/chromablue-labs/crossbar/issues
89
+ documentation_uri: https://github.com/chromablue-labs/patchwork-rb#readme
90
+ source_code_uri: https://github.com/chromablue-labs/patchwork-rb
91
+ changelog_uri: https://github.com/chromablue-labs/patchwork-rb/blob/main/CHANGELOG.md
92
+ bug_tracker_uri: https://github.com/chromablue-labs/patchwork-rb/issues
91
93
  rubygems_mfa_required: 'true'
94
+ post_install_message:
92
95
  rdoc_options: []
93
96
  require_paths:
94
97
  - lib
@@ -103,7 +106,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
106
  - !ruby/object:Gem::Version
104
107
  version: '0'
105
108
  requirements: []
106
- rubygems_version: 3.6.9
109
+ rubygems_version: 3.5.22
110
+ signing_key:
107
111
  specification_version: 4
108
112
  summary: Ruby SDK for Patchwork — mint session tokens, verify tool calls and webhooks
109
113
  test_files: []