finesse 0.0.1 → 0.1.0.beta1
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/README.md +211 -0
- data/exe/aarch64-linux/finesse +0 -0
- data/exe/arm64-darwin/finesse +0 -0
- data/exe/finesse +21 -0
- data/exe/x86_64-darwin/finesse +0 -0
- data/exe/x86_64-linux/finesse +0 -0
- data/lib/finesse/platforms.rb +75 -0
- data/lib/finesse/version.rb +3 -1
- data/lib/finesse.rb +11 -1
- metadata +31 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4063a626a91837c3ad59b9dd687af6548bcbb16ada46f5b3c01e768bea5dc73b
|
|
4
|
+
data.tar.gz: 25cebe7a2f3a2206ae2704d44a78943b92c1e45b4f4b68d7e3884d9d7f8a50e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz: '
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '068fd87c382d4ca33b17a0687ebed2d742c234bd23f017bb05e83a6728ecc495e8b98f718b09d6e21ee10cc14f45292f709f7abdcb95f0a8ad77666e367832d9'
|
|
7
|
+
data.tar.gz: dc5a0732991394c1cea7abb149e639c12b07b26749923ca281d4f1533136994657fa883fec434f93fb175821b8d240b0ae4331ddebc96c4d52607a375323432e
|
data/README.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="finesse-logo.png" alt="Finesse" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Finesse
|
|
6
|
+
|
|
7
|
+
Finesse replaces ActionCable's WebSocket transport with a lightweight Go binary that polls SolidCable's SQLite table and streams Turbo updates to browsers via Server-Sent Events (SSE). No WebSocket infrastructure needed — just a single binary alongside your Rails app.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
Finesse currently supports **SolidCable with SQLite** only. Your Rails app needs:
|
|
12
|
+
|
|
13
|
+
- **[SolidCable](https://github.com/rails/solid_cable)** as your ActionCable adapter, with a `cable:` database configured in `config/database.yml`
|
|
14
|
+
- **[turbo-rails](https://github.com/hotwired/turbo-rails)** (>= 2.0) — Finesse uses Turbo's signed stream tokens for authentication
|
|
15
|
+
|
|
16
|
+
PostgreSQL and other adapters are on the [roadmap](#roadmap).
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
1. Add to your Gemfile:
|
|
21
|
+
```ruby
|
|
22
|
+
gem "finesse"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. Install:
|
|
26
|
+
```sh
|
|
27
|
+
bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Add to your `Procfile.dev`:
|
|
31
|
+
```
|
|
32
|
+
sse: bundle exec finesse --allow-origin http://localhost:3000
|
|
33
|
+
```
|
|
34
|
+
For multiple origins (e.g., accessing from another device on your network), repeat the flag:
|
|
35
|
+
```
|
|
36
|
+
sse: bundle exec finesse --allow-origin http://localhost:3000 --allow-origin http://192.168.1.10:3000
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
4. Start your app:
|
|
40
|
+
```sh
|
|
41
|
+
bin/dev
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## How It Works
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
Browser <turbo-stream-source> ──GET /events?signed_stream=…──> Finesse (Go binary, :4000)
|
|
48
|
+
│
|
|
49
|
+
Rails app ──broadcast_append_to──> SolidCable ──INSERT──> SQLite ←──POLL─┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Finesse polls the `solid_cable_messages` table for new rows and streams them as SSE events to connected browsers. It uses SQLite WAL mode for concurrent read access alongside Rails writes.
|
|
53
|
+
|
|
54
|
+
Key design choices:
|
|
55
|
+
- **Polling over triggers** — simple, no SQLite extensions required, 10ms default interval
|
|
56
|
+
- **Per-channel fan-out** — each channel gets its own broadcaster with a ring buffer for reconnection catch-up
|
|
57
|
+
- **Pure Go SQLite** — uses `modernc.org/sqlite` (no CGO), enabling easy cross-compilation
|
|
58
|
+
|
|
59
|
+
## CLI Options
|
|
60
|
+
|
|
61
|
+
| Flag | Default | Description |
|
|
62
|
+
|------|---------|-------------|
|
|
63
|
+
| `--port` | `4000` | HTTP listen port |
|
|
64
|
+
| `--db-path` | auto from `config/database.yml` | Path to SolidCable SQLite database |
|
|
65
|
+
| `--table-name` | `solid_cable_messages` | SolidCable messages table name |
|
|
66
|
+
| `--poll-interval` | `10` | Database poll interval (integer, milliseconds) |
|
|
67
|
+
| `--allow-origin` | *(required)* | `Access-Control-Allow-Origin` header value |
|
|
68
|
+
|
|
69
|
+
## Environment Variables
|
|
70
|
+
|
|
71
|
+
| Variable | Description |
|
|
72
|
+
|----------|-------------|
|
|
73
|
+
| `FINESSE_SIGNING_KEY` | Hex-encoded HMAC key for signed stream verification (auto-derived from Rails when using `bundle exec finesse`) |
|
|
74
|
+
| `BINDING` | Bind address (default: `127.0.0.1`) |
|
|
75
|
+
|
|
76
|
+
## Endpoints
|
|
77
|
+
|
|
78
|
+
| Path | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `GET /up` | Health check (returns 200) |
|
|
81
|
+
| `GET /events?signed_stream=<token>` | SSE stream for the given signed stream |
|
|
82
|
+
|
|
83
|
+
The SSE endpoint supports the `Last-Event-ID` header for automatic reconnection catch-up. Stream tokens use the same `ActiveSupport::MessageVerifier` format as Turbo's signed streams.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
### Compiling from source
|
|
88
|
+
|
|
89
|
+
Requires Go 1.24+.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
bundle exec rake build:local # Build for current platform
|
|
93
|
+
bundle exec rake build:all # Cross-compile for all platforms
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Running tests
|
|
97
|
+
|
|
98
|
+
All build and test commands should be run via `bundle exec`:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
bundle exec rake test # Run Go + Ruby tests
|
|
102
|
+
bundle exec rake test:go # Go tests only
|
|
103
|
+
bundle exec rake test:ruby # Ruby tests only
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Running locally
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
./exe/arm64-darwin/finesse --port 4000 --db-path ../your-app/storage/development_cable.sqlite3
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Security
|
|
113
|
+
|
|
114
|
+
Finesse verifies every SSE connection using Turbo's signed stream tokens (HMAC-SHA256). CORS is restricted to the origins you specify with `--allow-origin`, and the server binds to `127.0.0.1` by default.
|
|
115
|
+
|
|
116
|
+
### Signing Key Flow
|
|
117
|
+
|
|
118
|
+
**TL;DR:** Finesse needs the same HMAC key that Turbo uses to sign stream names. The Ruby wrapper extracts it from Rails automatically — zero config in development.
|
|
119
|
+
|
|
120
|
+
The problem: Finesse is a standalone Go binary, but it needs to verify tokens that Rails signs. Rails derives its signing key from `SECRET_KEY_BASE` via PBKDF2, and that secret lives in encrypted credentials (`config/credentials.yml.enc`) which require Ruby's `Marshal` deserializer to read. Reimplementing that in Go would be fragile and version-dependent.
|
|
121
|
+
|
|
122
|
+
The solution: let Ruby do the Ruby parts, then hand the result to Go.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Rails Boot Finesse Boot
|
|
126
|
+
────────── ────────────
|
|
127
|
+
|
|
128
|
+
config/credentials.yml.enc exe/finesse (Ruby wrapper)
|
|
129
|
+
│ │
|
|
130
|
+
▼ ▼
|
|
131
|
+
SECRET_KEY_BASE rails runner "Finesse.signing_key"
|
|
132
|
+
│ │
|
|
133
|
+
▼ ▼
|
|
134
|
+
PBKDF2-HMAC-SHA256 Turbo.signed_stream_verifier_key
|
|
135
|
+
salt: "turbo/signed_ │
|
|
136
|
+
stream_verifier_key" ▼
|
|
137
|
+
iterations: 65,536 hex-encode the derived key
|
|
138
|
+
output: 64 bytes │
|
|
139
|
+
│ ▼
|
|
140
|
+
▼ ENV["FINESSE_SIGNING_KEY"] = hex
|
|
141
|
+
Turbo.signed_stream_ │
|
|
142
|
+
verifier_key ▼
|
|
143
|
+
│ exec(go_binary)
|
|
144
|
+
▼ ├── reads env, decodes hex
|
|
145
|
+
turbo_stream_from @chat └── verifies HMAC on every
|
|
146
|
+
signs channel name ─────────▶ /events?signed_stream=<token>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Token format** (Rails `ActiveSupport::MessageVerifier`):
|
|
150
|
+
```
|
|
151
|
+
base64strict(json(channel_name))--hex(hmac_sha256(base64_data, derived_key))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The Go binary splits the token on `--`, recomputes the HMAC over the base64 data, and compares using constant-time `hmac.Equal`. If it matches, the channel name is trusted.
|
|
155
|
+
|
|
156
|
+
**In development**, this is fully automatic — Rails writes `secret_key_base` to `tmp/local_secret.txt` and the wrapper reads it. **In production**, either let the wrapper derive the key at boot (slower — spawns a `rails runner`), or set `FINESSE_SIGNING_KEY` directly as a hex-encoded env var to skip the Rails boot entirely.
|
|
157
|
+
|
|
158
|
+
## Production Deployment
|
|
159
|
+
|
|
160
|
+
Finesse binds to `127.0.0.1` by default and should **not** be exposed directly on port 80/443. Run it alongside your app server (Puma, etc.) on the same host and proxy to it from your web server. For example, with Nginx:
|
|
161
|
+
|
|
162
|
+
```nginx
|
|
163
|
+
upstream finesse {
|
|
164
|
+
server 127.0.0.1:4000;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
location /finesse/ {
|
|
168
|
+
proxy_pass http://finesse/;
|
|
169
|
+
proxy_set_header Host $host;
|
|
170
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
171
|
+
|
|
172
|
+
# SSE requires unbuffered responses
|
|
173
|
+
proxy_buffering off;
|
|
174
|
+
proxy_cache off;
|
|
175
|
+
proxy_read_timeout 86400s;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Kamal
|
|
180
|
+
|
|
181
|
+
If you deploy with [Kamal](https://kamal-deploy.org), add Finesse as a separate server role in `config/deploy.yml`. kamal-proxy automatically detects `Content-Type: text/event-stream` responses and disables buffering — no extra proxy configuration needed.
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
servers:
|
|
185
|
+
web:
|
|
186
|
+
- <your-public-server-ip>
|
|
187
|
+
sse:
|
|
188
|
+
hosts:
|
|
189
|
+
- <your-public-server-ip>
|
|
190
|
+
cmd: bundle exec finesse --allow-origin "https://yourapp.com"
|
|
191
|
+
proxy:
|
|
192
|
+
ssl: true
|
|
193
|
+
host: sse.yourapp.com
|
|
194
|
+
app_port: 4000
|
|
195
|
+
healthcheck:
|
|
196
|
+
path: /up
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Point a DNS record for `sse.yourapp.com` at your server and kamal-proxy handles TLS and routing.
|
|
200
|
+
|
|
201
|
+
Set `FINESSE_SIGNING_KEY` as a hex-encoded env var in production to avoid a `rails runner` invocation on every boot. The `--allow-origin` flag should match your production domain.
|
|
202
|
+
|
|
203
|
+
## Roadmap
|
|
204
|
+
|
|
205
|
+
- **Rails generator** — `rails g finesse:install` for config, initializer, and binstub
|
|
206
|
+
- **Engine/Railtie** — view helpers (`finesse_sse_url`)
|
|
207
|
+
- **PostgreSQL support** — `LISTEN/NOTIFY` adapter as alternative to SQLite polling. 8KB limit may not be worth it, but polling in PostgreSQL would still be on the roadmap.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
Binary file
|
|
Binary file
|
data/exe/finesse
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'English'
|
|
5
|
+
require_relative '../lib/finesse/platforms'
|
|
6
|
+
|
|
7
|
+
args = ARGV.dup
|
|
8
|
+
|
|
9
|
+
# If --db-path wasn't explicitly provided, try to resolve it from config/database.yml
|
|
10
|
+
unless args.any? { |a| a.start_with?('--db-path') }
|
|
11
|
+
db_path = Finesse::Platforms.cable_db_path
|
|
12
|
+
args.unshift('--db-path', db_path) if db_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Derive FINESSE_SIGNING_KEY via Rails if not already set.
|
|
16
|
+
if ENV['FINESSE_SIGNING_KEY'].nil? || ENV['FINESSE_SIGNING_KEY'].empty?
|
|
17
|
+
key = `bundle exec rails runner "print Finesse.signing_key" 2>/dev/null`.strip
|
|
18
|
+
ENV['FINESSE_SIGNING_KEY'] = key if $CHILD_STATUS.success? && !key.empty?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
exec(Finesse::Platforms.executable, *args)
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Finesse
|
|
4
|
+
# Platform detection, binary resolution, and database.yml parsing.
|
|
5
|
+
module Platforms
|
|
6
|
+
SUPPORTED_PLATFORMS = %w[
|
|
7
|
+
arm64-darwin
|
|
8
|
+
x86_64-darwin
|
|
9
|
+
aarch64-linux
|
|
10
|
+
x86_64-linux
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
class UnsupportedPlatformError < StandardError; end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Root directory of the gem (two levels up from lib/finesse/).
|
|
17
|
+
def root
|
|
18
|
+
File.expand_path('../..', __dir__)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
PLATFORM_MAP = {
|
|
22
|
+
%w[arm64 darwin] => 'arm64-darwin',
|
|
23
|
+
%w[x86_64 darwin] => 'x86_64-darwin',
|
|
24
|
+
%w[aarch64 linux] => 'aarch64-linux',
|
|
25
|
+
%w[x86_64 linux] => 'x86_64-linux'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def platform
|
|
29
|
+
cpu = Gem::Platform.local.cpu
|
|
30
|
+
os = Gem::Platform.local.os
|
|
31
|
+
|
|
32
|
+
PLATFORM_MAP.fetch([cpu, os]) do
|
|
33
|
+
raise UnsupportedPlatformError,
|
|
34
|
+
"Finesse does not support #{cpu}-#{os}. " \
|
|
35
|
+
"Supported platforms: #{SUPPORTED_PLATFORMS.join(', ')}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def executable
|
|
40
|
+
exe = File.join(root, 'exe', platform, 'finesse')
|
|
41
|
+
|
|
42
|
+
unless File.exist?(exe)
|
|
43
|
+
raise UnsupportedPlatformError,
|
|
44
|
+
"Finesse binary not found at #{exe}. " \
|
|
45
|
+
'Run `rake build:go` to compile from source.'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
exe
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Resolve the cable database path from config/database.yml.
|
|
52
|
+
# Returns nil if the file doesn't exist or no cable DB is configured.
|
|
53
|
+
def cable_db_path
|
|
54
|
+
config = load_database_yml
|
|
55
|
+
return nil unless config
|
|
56
|
+
|
|
57
|
+
env_config = config.dig(ENV.fetch('RAILS_ENV', 'development'), 'cable')
|
|
58
|
+
env_config['database'] if env_config.is_a?(Hash)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def load_database_yml
|
|
64
|
+
config_path = File.join(Dir.pwd, 'config', 'database.yml')
|
|
65
|
+
return nil unless File.exist?(config_path)
|
|
66
|
+
|
|
67
|
+
require 'yaml'
|
|
68
|
+
require 'erb'
|
|
69
|
+
|
|
70
|
+
yaml = ERB.new(File.read(config_path)).result
|
|
71
|
+
YAML.safe_load(yaml, permitted_classes: [Symbol], aliases: true)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/finesse/version.rb
CHANGED
data/lib/finesse.rb
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'finesse/version'
|
|
4
|
+
require_relative 'finesse/platforms'
|
|
5
|
+
|
|
6
|
+
# SSE server gem for ActionCable via SolidCable.
|
|
3
7
|
module Finesse
|
|
8
|
+
# Returns the Turbo signed stream verifier key as a hex string.
|
|
9
|
+
# Called by the exe wrapper via `rails runner` to set FINESSE_SIGNING_KEY.
|
|
10
|
+
def self.signing_key
|
|
11
|
+
require 'turbo-rails'
|
|
12
|
+
Turbo.signed_stream_verifier_key.unpack1('H*')
|
|
13
|
+
end
|
|
4
14
|
end
|
metadata
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: finesse
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.1.0.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- scudco
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: turbo-rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
12
26
|
description: Finesse replaces ActionCable's default WebSocket transport with a lightweight
|
|
13
|
-
Go binary
|
|
14
|
-
|
|
27
|
+
Go binary that polls SolidCable's SQLite table and streams Turbo updates to browsers
|
|
28
|
+
via Server-Sent Events.
|
|
29
|
+
executables:
|
|
30
|
+
- finesse
|
|
15
31
|
extensions: []
|
|
16
32
|
extra_rdoc_files: []
|
|
17
33
|
files:
|
|
18
34
|
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- exe/aarch64-linux/finesse
|
|
37
|
+
- exe/arm64-darwin/finesse
|
|
38
|
+
- exe/finesse
|
|
39
|
+
- exe/x86_64-darwin/finesse
|
|
40
|
+
- exe/x86_64-linux/finesse
|
|
19
41
|
- lib/finesse.rb
|
|
42
|
+
- lib/finesse/platforms.rb
|
|
20
43
|
- lib/finesse/version.rb
|
|
21
44
|
homepage: https://github.com/scudco/finesse
|
|
22
45
|
licenses:
|
|
23
46
|
- MIT
|
|
24
|
-
metadata:
|
|
47
|
+
metadata:
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
25
49
|
rdoc_options: []
|
|
26
50
|
require_paths:
|
|
27
51
|
- lib
|
|
@@ -36,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
36
60
|
- !ruby/object:Gem::Version
|
|
37
61
|
version: '0'
|
|
38
62
|
requirements: []
|
|
39
|
-
rubygems_version: 4.0.
|
|
63
|
+
rubygems_version: 4.0.3
|
|
40
64
|
specification_version: 4
|
|
41
65
|
summary: SSE server for ActionCable in Rails
|
|
42
66
|
test_files: []
|