restless-sdk 0.1.0
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 +7 -0
- data/CONFORMANCE.md +112 -0
- data/README.md +148 -0
- data/exe/restless-conformance +108 -0
- data/install.md +299 -0
- data/lib/restless/caches.rb +147 -0
- data/lib/restless/capture.rb +289 -0
- data/lib/restless/client.rb +111 -0
- data/lib/restless/conformance.rb +164 -0
- data/lib/restless/env.rb +82 -0
- data/lib/restless/fingerprint.rb +287 -0
- data/lib/restless/har.rb +113 -0
- data/lib/restless/injection.rb +99 -0
- data/lib/restless/mask.rb +57 -0
- data/lib/restless/rack.rb +374 -0
- data/lib/restless/redact.rb +342 -0
- data/lib/restless/request_id.rb +63 -0
- data/lib/restless/settings.rb +101 -0
- data/lib/restless/stack_frames.rb +146 -0
- data/lib/restless/text.rb +171 -0
- data/lib/restless/uploader.rb +319 -0
- data/lib/restless/version.rb +25 -0
- data/lib/restless.rb +57 -0
- data/spec/VECTORS_VERSION +1 -0
- data/spec/vectors/fingerprint.json +1119 -0
- data/spec/vectors/har.json +500 -0
- data/spec/vectors/mask.json +175 -0
- data/spec/vectors/recovery-slug.json +102 -0
- data/spec/vectors/redact.json +775 -0
- data/spec/vectors/request-id.json +120 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1147b0604f0fdb6ca8b2cdd39ffe03819391d0e5d663b70fa845fbe4b4b72b27
|
|
4
|
+
data.tar.gz: f1bd487a972936ae678151d03f27edecb476b8c1f7b755ff5a88157f5d015e4e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f7508f1e2f878289a16930cd56db0a0c5e150dc6fd5772a0c221cb97792a703f130f15d6a99e401b81bb713edbceba5adc835da4d994600248911f4d7f86b558
|
|
7
|
+
data.tar.gz: c6e94661d61836cdb2dc408beefc0e5323afd17ffb3c5ec2e68beb2ec5ab1687af64ae72a6a8726d97e265d4a1343b04dea320687e0b952a7c392f279851e71d
|
data/CONFORMANCE.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Conformance
|
|
2
|
+
|
|
3
|
+
| | |
|
|
4
|
+
|---|---|
|
|
5
|
+
| **Spec version** | 1.0.0 |
|
|
6
|
+
| **Level** | L2 (core + batching, caches, injection, safety) |
|
|
7
|
+
| **Reference** | `restlesshq/node` (`@restlessai/sdk`) |
|
|
8
|
+
| **Driver** | `ruby exe/restless-conformance` |
|
|
9
|
+
|
|
10
|
+
Declared in `lib/restless/version.rb` (META-001).
|
|
11
|
+
|
|
12
|
+
## Verifying
|
|
13
|
+
|
|
14
|
+
The harness and vectors live in the reference SDK, so the commands below
|
|
15
|
+
assume it is checked out as a sibling (`../node-sdk`), which is how
|
|
16
|
+
`setup.sh` in the install repo arranges things. The vectors in `spec/` here
|
|
17
|
+
are a pinned copy, so `ruby -Ilib -Itest test/all.rb` alone works without it.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# in-process, no Node and no bundler needed
|
|
22
|
+
ruby -Ilib -Itest test/all.rb
|
|
23
|
+
|
|
24
|
+
# the shared cross-language harness
|
|
25
|
+
node ../node-sdk/spec/harness/run-vectors.mjs -- ruby exe/restless-conformance
|
|
26
|
+
|
|
27
|
+
# differential fuzz against the reference implementation
|
|
28
|
+
node ../node-sdk/spec/harness/fuzz.mjs \
|
|
29
|
+
--ref "node ../node-sdk/spec/driver/.build/node.js" \
|
|
30
|
+
--test "ruby exe/restless-conformance" \
|
|
31
|
+
--iterations 8000 --seed 24301
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Current status: **206 vectors, 198 passed, 0 failed, 8 skipped.** Zero
|
|
35
|
+
divergence across 34,918 fuzz comparisons on five seeds (24301, 1, 4242,
|
|
36
|
+
99991, 777).
|
|
37
|
+
|
|
38
|
+
The 8 skips are cases outside this implementation's dialect, not gaps:
|
|
39
|
+
|
|
40
|
+
- 7 `fp/stack-*` cases feed a v8-shaped stack into `fingerprint`. FP-044
|
|
41
|
+
makes frame parsing per-language and FP-046 requires the driver to say so
|
|
42
|
+
rather than guess. Covered natively in `test/test_stack_frames.rb`. This
|
|
43
|
+
includes `fp/stack-not-used-for-4xx`, where the stack is never consulted:
|
|
44
|
+
the driver declares the dialect on the INPUT rather than on whether the
|
|
45
|
+
strategy happens to fire, which is what the Python and Go drivers do too.
|
|
46
|
+
- 1 `redactBody/lone-surrogate` case. See the exemption below.
|
|
47
|
+
|
|
48
|
+
## Ruby-specific decisions
|
|
49
|
+
|
|
50
|
+
Each of these is a place where the obvious Ruby code silently disagrees with
|
|
51
|
+
the reference. They are the reason this SDK is byte-compatible.
|
|
52
|
+
|
|
53
|
+
| Contract | What Ruby needs |
|
|
54
|
+
|---|---|
|
|
55
|
+
| PRIM-002 | `WS` is enumerated as code points in `text.rb`. Ruby's `\s` is ASCII-only and NARROWER than the contract's set: no NBSP, no Zs category, no LS/PS/ZWNBSP. |
|
|
56
|
+
| PRIM-003 | **Ruby's `\b` is Unicode-aware even though its `\w` is not.** Onigmo defines the boundary against the Unicode word property, so `\b[\w-]*[0-9][\w-]*\b` leaves `éa1` untouched where JavaScript, Go and Python-with-`re.ASCII` all strip the `a1`. `Fingerprint.strip_digit_words` applies that one regex to the UTF-8 BYTES, which restores JavaScript's semantics exactly (every non-ASCII character becomes a run of non-word bytes, which is what a non-`u` JS regex sees in UTF-16 too). The pattern can only match ASCII bytes, so the result is still well-formed UTF-8. |
|
|
57
|
+
| PRIM-005 | `\A...\z` everywhere, never `^...$`. Ruby's `$` also matches before a trailing newline, so a route segment of `"5\n"` would normalize to `:id` here and not in JavaScript. |
|
|
58
|
+
| PRIM-006 | Hex digits validated explicitly against `[0-9a-fA-F]`. `String#to_i(16)` returns 0 for garbage rather than failing, so `%zz` would silently decode as a NUL byte. |
|
|
59
|
+
| PRIM-010 vs PRIM-011 | `String#length` is code points (what REDACT-002 and MASK-006 want) and `String#bytesize` is UTF-8 bytes (what HAR-010 and REDACT-030 want). They are never interchangeable. |
|
|
60
|
+
| PRIM-013 | `Text.to_utf8` scrubs invalid bytes to U+FFFD instead of raising. Rack hands out header and body strings tagged UTF-8 that are not valid UTF-8. It also DECODES a BINARY string rather than transcoding it: `IO#read(n)` always returns ASCII-8BIT and the Rack spec requires `rack.input` to be binary, and Ruby's BINARY-to-UTF-8 transcode has no mapping above `0x7F`, so `encode` turns every byte of every multi-byte character into its own U+FFFD. That corrupted every non-ASCII body the SDK captured and was a second redaction bypass on top of REDACT-010, since a mangled key no longer matches the denylist. |
|
|
61
|
+
| PRIM-020 | `String#downcase` is FULL case mapping and is used unmodified everywhere, including for REDACT-010 names. U+0130 becomes two code points (matching JS and Python, unlike Go's simple mapping) and U+212A KELVIN SIGN becomes `k`. It is not `casefold` and applies no locale tailoring. The one thing it does not do is the Final_Sigma contextual rule that `String.prototype.toLowerCase` applies (word-final Σ to ς rather than σ), and per the PRIM-021 note that is **not required**: nothing in the contract can observe it, because Greek is outside `WORD` so FP-020 step 6 replaces both sigma forms with a space before either reaches a token, and the REDACT-010 denylists are ASCII. An earlier `\p{Cased}` / `\p{Case_Ignorable}` implementation of the rule was removed as untestable. |
|
|
62
|
+
| PRIM-030..032 | Ruby's `JSON.generate` is already compact, insertion-ordered and literal-UTF-8, and does not HTML-escape - so the `<REDACTED:...>` sentinel the dashboard pattern-matches on survives. Verified byte-identical to `JSON.stringify` for control characters, quotes, backslashes and astral characters. |
|
|
63
|
+
| PRIM-040 | Hand-built with `strftime("%Y-%m-%dT%H:%M:%S.%LZ")`. `Time#iso8601` needs `require "time"` plus an explicit fractional-digit count and emits `+00:00` unless the receiver is already UTC. |
|
|
64
|
+
| REDACT-010 | Name normalization uses **full Unicode** lowercase (PRIM-020), not an ASCII fold, and this is a security requirement rather than a cosmetic one. Normalization decides whether a value is redacted, so it must fold more aggressively rather than less: a header `x-api-Key` or a body key `toKen` whose `K` is U+212A KELVIN SIGN lowercases to a denylisted name and must be redacted. A hand-written ASCII-only fold is the easy mistake here, and it ships such values to the dashboard in plaintext. Pinned by `redactHeaders/unicode-fold-kelvin`, `redactBody/unicode-fold-kelvin` and `test_middleware.rb#test_kelvin_sign_does_not_bypass_the_denylist`. |
|
|
65
|
+
| REDACT-016 | An explicit code-point scan, never `^(\S+)(\s+)(\S.*)$`. Ruby's `.` excludes only LF while JavaScript's also excludes CR, LS and PS, so a credential containing a stray CR would take the scheme-preserving branch here and the redact-whole branch there. |
|
|
66
|
+
| REDACT-025 | `split("&", -1)` and `split("/", -1)`. Ruby drops trailing empty fields without the negative limit, so `?a=1&` would come back as `?a=1` and `/users/` would lose its trailing segment. |
|
|
67
|
+
| REDACT-028 | The unreserved set is spelled out. `CGI.escape` turns a space into `+` and escapes `~`; `URI.encode_www_form_component` differs again. |
|
|
68
|
+
| REDACT-029 | `bytes.pack("C*").force_encoding("UTF-8").scrub` is byte-identical to Node's `Buffer#toString("utf8")`, including the maximal-subpart replacement rule. Verified differentially. |
|
|
69
|
+
| FP-020 step 9 | `split(/ /, -1)`, never `split(" ")`. A literal single-space argument triggers Ruby's awk-mode split, which collapses runs and drops leading empties. |
|
|
70
|
+
| FP-030 | Hex classes written as `[0-9a-fA-F]` with no `i` flag. Ruby's case-insensitive matching is Unicode case folding, a wider relation than the ASCII-only canonicalization a non-`u` JavaScript regex performs. |
|
|
71
|
+
| FP-042 | `project_relative` splits on `/` and scans BACKWARDS from the second-to-last segment, so the LAST project dir wins and a deployment root named `/app` (Docker `WORKDIR`, Heroku) cannot survive into the key. `split("/", -1)`: without the negative limit Ruby drops the trailing empty field, so `/proj/src/` would miss `src` here and match it in JavaScript. |
|
|
72
|
+
| FP-043 | Ruby's `Exception#backtrace` is innermost-FIRST, like v8 and Go and unlike a Python traceback, so the walk goes forwards. Verified empirically in `test/test_stack_frames.rb` rather than assumed. |
|
|
73
|
+
| FP-044 | Frames are `path:line:in \`method'` (`'method'` from Ruby 3.4); both quote styles parse. `block (N levels) in foo` is normalized to `block in foo`, since the nesting count behaves like a line number. Skips are matched by FILE PATH: the SDK's own directory (resolved from `__dir__`), `RbConfig`'s stdlib dirs, `Gem.path`, any `/gems/` segment, and `<internal:` frames. Never by module or class name - a name check would also skip a customer's own Restless-flavoured code and would fail to skip this gem when vendored under another constant. |
|
|
74
|
+
| INJECT-005 | `Text.ws_trim`, not `String#strip`. Ruby's strip removes NUL and leaves NBSP; the contract wants exactly the PRIM-002 set. |
|
|
75
|
+
| BATCH-003 | The environment indicator is `RESTLESS_ENV`, then `RACK_ENV`, then `RAILS_ENV`. Ruby has three spellings in the wild and no single winner. |
|
|
76
|
+
| BATCH-008 | Test detection keys on those same variables being `test`, plus `RSpec::Core`, `Minitest::Test` and `Test::Unit::TestCase` being defined. |
|
|
77
|
+
| CACHE-* | Both caches are Mutex-guarded. Puma, Falcon and threaded Unicorn all serve requests on many threads, so an unsynchronized Hash is a real race. |
|
|
78
|
+
|
|
79
|
+
## Exemption: PRIM-034 (unpaired surrogates)
|
|
80
|
+
|
|
81
|
+
This SDK claims the **PRIM-035 exemption**, on stronger grounds than Go's.
|
|
82
|
+
|
|
83
|
+
A Go string merely *replaces* an unpaired surrogate with U+FFFD while
|
|
84
|
+
decoding. Ruby is more restrictive in both directions:
|
|
85
|
+
|
|
86
|
+
- `JSON.parse('"\ud83d"')` **raises** `JSON::ParserError: incomplete
|
|
87
|
+
surrogate pair`. The value never reaches SDK code at all.
|
|
88
|
+
- `"\u{d83d}"` is not a legal Ruby literal - the parser rejects it with
|
|
89
|
+
`invalid Unicode codepoint`. `[0xd83d].pack("U")` produces the CESU-8
|
|
90
|
+
bytes `ED A0 BD`, which `String#valid_encoding?` reports as false.
|
|
91
|
+
|
|
92
|
+
So a Ruby `String` has no representation for the value, and per PRIM-035 the
|
|
93
|
+
SDK never raises on such input and substitutes U+FFFD (`Text.to_utf8`).
|
|
94
|
+
|
|
95
|
+
The conformance driver reports `unsupported` for any input LINE containing a
|
|
96
|
+
lone surrogate escape, checked on the raw line **before** parsing, because
|
|
97
|
+
the limitation is at the transport layer rather than in redaction - parsing
|
|
98
|
+
first would produce a `JSON::ParserError` that looks like a conformance
|
|
99
|
+
failure. `test/test_vectors.rb` does the same, swapping each such escape for
|
|
100
|
+
an ASCII marker so the vector documents can be read at all, then skipping the
|
|
101
|
+
cases that contained one.
|
|
102
|
+
|
|
103
|
+
## Residual differences, accepted
|
|
104
|
+
|
|
105
|
+
**Number rendering in a re-serialized body.** REDACT-020 means a body is only
|
|
106
|
+
re-serialized when it actually contains a denylisted key. For those bodies,
|
|
107
|
+
Ruby preserves int64 exactly where JavaScript silently truncates above 2^53,
|
|
108
|
+
and Ruby's `Float#to_s` uses a different exponent form than JavaScript for
|
|
109
|
+
very large and very small magnitudes. This matches the Python SDK and is the
|
|
110
|
+
behaviour PRIM-033 describes as the *correct* one; the reference is the odd
|
|
111
|
+
one out. It is unreachable from the fuzzer, because `JSON.stringify` has
|
|
112
|
+
already collapsed such values before the input is generated.
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# restless-sdk
|
|
2
|
+
|
|
3
|
+
Capture your API traffic and send it to [Restless](https://restless.ai).
|
|
4
|
+
|
|
5
|
+
One Rack middleware covers **Rails**, **Sinatra**, **Hanami**, **Grape**,
|
|
6
|
+
**Roda** and anything else that speaks Rack. Ruby 2.6+. No dependencies.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
gem install restless-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
or in a `Gemfile`:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "restless-sdk"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "restless"
|
|
24
|
+
|
|
25
|
+
CLIENT = Restless.new(ENV["RESTLESS_KEY"])
|
|
26
|
+
|
|
27
|
+
CLIENT.setup do |request|
|
|
28
|
+
{
|
|
29
|
+
api_key: CLIENT.mask(request.header("Authorization")),
|
|
30
|
+
owner: {
|
|
31
|
+
# Permanent and immutable. A workspace id or database primary key,
|
|
32
|
+
# never an API key, email, or anything else that rotates: the
|
|
33
|
+
# dashboard pins a project's whole log history to this.
|
|
34
|
+
id: workspace_id_for(request),
|
|
35
|
+
# Runs once per owner id, then caches for an hour. Put the expensive
|
|
36
|
+
# lookup here, not in the fields above.
|
|
37
|
+
enrich: ->(owner_id) {
|
|
38
|
+
workspace = Workspace.find(owner_id)
|
|
39
|
+
{ label: workspace.name, email: workspace.admin_emails }
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then mount it.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
# config.ru (Sinatra, Roda, Hanami, plain Rack)
|
|
50
|
+
use CLIENT.rack
|
|
51
|
+
run App
|
|
52
|
+
|
|
53
|
+
# config/application.rb (Rails)
|
|
54
|
+
config.middleware.insert_before 0, CLIENT.rack
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`CLIENT.rack` returns a middleware factory: anything that calls `.new(app)`
|
|
58
|
+
on it works. Mount it as far **out** as you can, so it sees the real status
|
|
59
|
+
and body your app produced rather than an inner layer's.
|
|
60
|
+
|
|
61
|
+
## What you get
|
|
62
|
+
|
|
63
|
+
- **Lazy owner enrichment.** The `enrich` callback runs on the first request
|
|
64
|
+
from each owner id and then caches, so 100 requests from one workspace do
|
|
65
|
+
not mean 100 database lookups. The cached value still rides on every
|
|
66
|
+
upload, so no request lands in the dashboard as unauthenticated.
|
|
67
|
+
- **Safe by default.** `Authorization`, `Cookie`, `password`, `token`, `ssn`
|
|
68
|
+
and friends are redacted before anything leaves your process. Bodies with
|
|
69
|
+
nothing to redact are passed through byte for byte, so your payloads are
|
|
70
|
+
never re-serialized on the way out.
|
|
71
|
+
- **Error triage.** 4xx/5xx responses get `x-log-url` and `x-debug` headers
|
|
72
|
+
and a `debug` block in the JSON body. If someone attaches a "next steps"
|
|
73
|
+
message to an error in the dashboard, the SDK injects it as
|
|
74
|
+
`debug.recovery` - read synchronously from an in-process cache, never
|
|
75
|
+
blocking the response on a network call. A 5xx that groups by its throw
|
|
76
|
+
site also reports the key it would have grouped under otherwise, so a
|
|
77
|
+
message you attached before that grouping existed keeps being injected.
|
|
78
|
+
- **Blocking.** Return `{ block: true }` from the setup callback to reject a
|
|
79
|
+
request before your handler runs, or
|
|
80
|
+
`{ block: { status: 402, message: "Payment required" } }` to pick the
|
|
81
|
+
status.
|
|
82
|
+
|
|
83
|
+
## The setup callback
|
|
84
|
+
|
|
85
|
+
It receives a small read-only request view and returns a hash:
|
|
86
|
+
|
|
87
|
+
| field | what |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `api_key` | The **masked** end-user key. Always `CLIENT.mask(raw_header)` - never substitute `"anonymous"`, whose last 4 characters would become the mask tail. |
|
|
90
|
+
| `owner[:id]` | The permanent, immutable workspace/tenant identifier. |
|
|
91
|
+
| `owner[:enrich]` | `->(id) { ... }` returning owner metadata (`label`, `email`, extras). The only channel for owner metadata; anything else inline on `owner` is dropped. |
|
|
92
|
+
| `block` | `true`, or `{ status:, message: }`. |
|
|
93
|
+
| anything else | Carried through onto the log as-is. Keep it cheap - read straight off the request. Anything expensive belongs in `enrich`. |
|
|
94
|
+
|
|
95
|
+
The request view exposes `header(name)` (case-insensitive),
|
|
96
|
+
`request_method`, `path`, `query_string`, `url`, and the raw Rack `env`.
|
|
97
|
+
|
|
98
|
+
## Route patterns
|
|
99
|
+
|
|
100
|
+
The middleware reads the matched route template from the framework so your
|
|
101
|
+
handlers do not have to report it:
|
|
102
|
+
|
|
103
|
+
| framework | source | reported as |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| Sinatra | `env["sinatra.route"]` (`GET /pets/:id`) | `/pets/{id}` |
|
|
106
|
+
| Rails | `env["action_dispatch.route_uri_pattern"]` | `/pets/{id}` |
|
|
107
|
+
| anything | `env["restless.route"]`, set by you | as given |
|
|
108
|
+
|
|
109
|
+
The method prefix is stripped and `:id`-style params are rewritten to
|
|
110
|
+
`{id}`, so the same endpoint produces the same `routePattern` here as it
|
|
111
|
+
does in the Node, Python and Go SDKs. Override the whole thing with a
|
|
112
|
+
lambda:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
use CLIENT.rack(route: ->(env) { env["my_router.pattern"] })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Never breaks your API
|
|
119
|
+
|
|
120
|
+
Observability must not take down a production request path. Upload failures,
|
|
121
|
+
callback exceptions, unserializable bodies and malformed input are all caught
|
|
122
|
+
and swallowed (surfaced only under `DEBUG=restless`). Uploads happen on a
|
|
123
|
+
background thread and are fire-and-forget. Streaming responses
|
|
124
|
+
(`text/event-stream`) and bodies over 1 MiB are never buffered.
|
|
125
|
+
|
|
126
|
+
An exception your app raises is captured with its stack - so the error
|
|
127
|
+
fingerprint keys on the method that raised, not on the middleware - and then
|
|
128
|
+
re-raised untouched, leaving your own error handling completely unaffected.
|
|
129
|
+
|
|
130
|
+
## Environment variables
|
|
131
|
+
|
|
132
|
+
| variable | purpose |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `RESTLESS_KEY` | Your project API key, if you do not pass one explicitly. |
|
|
135
|
+
| `RESTLESS_BASE_URL` | Override the ingest URL (self-hosted or staging). |
|
|
136
|
+
| `RESTLESS_ENV` / `RACK_ENV` / `RAILS_ENV` | `production` enables batching; anything else flushes every request. `test` disables uploads entirely. |
|
|
137
|
+
| `DEBUG=restless` | Print upload diagnostics to stderr. |
|
|
138
|
+
|
|
139
|
+
## Conformance
|
|
140
|
+
|
|
141
|
+
This SDK implements version 1.0.0 of the Restless SDK Contract at level L2,
|
|
142
|
+
and is verified against the shared cross-language conformance vectors and a
|
|
143
|
+
differential fuzzer run against the reference implementation. See
|
|
144
|
+
[CONFORMANCE.md](./CONFORMANCE.md).
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
ISC
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# The Ruby conformance driver. See node-sdk/spec/driver/PROTOCOL.md.
|
|
5
|
+
#
|
|
6
|
+
# Dev-only: it is a thin shell over Restless::Conformance, which
|
|
7
|
+
# test/test_vectors.rb also uses, so the vectors cannot describe behaviour the
|
|
8
|
+
# driver does not exhibit. No customer ever runs it.
|
|
9
|
+
#
|
|
10
|
+
# bundle exec restless-conformance # or just: ruby exe/restless-conformance
|
|
11
|
+
#
|
|
12
|
+
# Reads JSON Lines on stdin, writes JSON Lines on stdout, diagnostics on
|
|
13
|
+
# stderr, and exits 0 when stdin closes.
|
|
14
|
+
|
|
15
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
16
|
+
|
|
17
|
+
require "json"
|
|
18
|
+
require "restless/conformance"
|
|
19
|
+
|
|
20
|
+
UNSUPPORTED_SURROGATE = <<~MSG.tr("\n", " ").strip
|
|
21
|
+
unsupported: input line contains an unpaired surrogate escape. Ruby's JSON
|
|
22
|
+
parser raises on one and a Ruby String has no representation for it, so the
|
|
23
|
+
value is unreachable before any SDK code runs (PRIM-035).
|
|
24
|
+
MSG
|
|
25
|
+
|
|
26
|
+
# PRIM-035 / PROTOCOL.md "Skips".
|
|
27
|
+
#
|
|
28
|
+
# Checked on the RAW LINE, because this is a TRANSPORT limit rather than an SDK
|
|
29
|
+
# one: `JSON.parse('"\ud83d"')` raises `JSON::ParserError: incomplete surrogate
|
|
30
|
+
# pair`, and `"\u{d83d}"` is not even a legal Ruby literal. Reporting
|
|
31
|
+
# `unsupported` for the whole line is what the protocol asks for; reporting a
|
|
32
|
+
# parse error would look like a conformance failure, and silently substituting
|
|
33
|
+
# U+FFFD would compare a value the harness never sent.
|
|
34
|
+
def lone_surrogate_escape?(line)
|
|
35
|
+
hex4 = /\A[0-9a-fA-F]{4}\z/
|
|
36
|
+
index = 0
|
|
37
|
+
while (at = line.index("\\u", index))
|
|
38
|
+
hex = line[at + 2, 4]
|
|
39
|
+
unless hex && hex4.match?(hex)
|
|
40
|
+
index = at + 2
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
code = hex.to_i(16)
|
|
45
|
+
if code >= 0xD800 && code <= 0xDBFF
|
|
46
|
+
# A high surrogate is fine only if a low one follows immediately.
|
|
47
|
+
low_hex = line[at + 8, 4]
|
|
48
|
+
if line[at + 6, 2] == "\\u" && low_hex && hex4.match?(low_hex)
|
|
49
|
+
low = low_hex.to_i(16)
|
|
50
|
+
if low >= 0xDC00 && low <= 0xDFFF
|
|
51
|
+
index = at + 12
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
return true if code >= 0xDC00 && code <= 0xDFFF
|
|
58
|
+
|
|
59
|
+
index = at + 6
|
|
60
|
+
end
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Best-effort extraction so the harness can still correlate a response for a
|
|
65
|
+
# line we refuse to fully decode.
|
|
66
|
+
def raw_id_token(line)
|
|
67
|
+
match = /"id"\s*:\s*("(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?|true|false|null)/.match(line)
|
|
68
|
+
match ? match[1] : "null"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def emit(line)
|
|
72
|
+
$stdout.write("#{line}\n")
|
|
73
|
+
$stdout.flush
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
$stdout.sync = true
|
|
77
|
+
$stdin.set_encoding(Encoding::UTF_8)
|
|
78
|
+
$stdout.set_encoding(Encoding::UTF_8)
|
|
79
|
+
|
|
80
|
+
while (line = $stdin.gets)
|
|
81
|
+
line = line.strip
|
|
82
|
+
next if line.empty?
|
|
83
|
+
|
|
84
|
+
if lone_surrogate_escape?(line)
|
|
85
|
+
emit(%({"id":#{raw_id_token(line)},"error":#{JSON.generate(UNSUPPORTED_SURROGATE)}}))
|
|
86
|
+
next
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
request = JSON.parse(line, max_nesting: false)
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
emit(JSON.generate({ "id" => nil, "error" => "malformed request: #{e.message}" }))
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
id = request["id"]
|
|
97
|
+
begin
|
|
98
|
+
result = Restless::Conformance.dispatch(request["op"], request["input"])
|
|
99
|
+
emit(JSON.generate({ "id" => id, "result" => result }))
|
|
100
|
+
rescue Restless::Conformance::UnknownOp,
|
|
101
|
+
Restless::Conformance::UnsupportedDialect => e
|
|
102
|
+
emit(JSON.generate({ "id" => id, "error" => e.message }))
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
emit(JSON.generate({ "id" => id, "error" => "#{e.class}: #{e.message}" }))
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
exit 0
|
data/install.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# install.md: LLM installation reference for restless-sdk (Ruby)
|
|
2
|
+
|
|
3
|
+
This file is the single source of truth for LLM agents installing or configuring the Restless Ruby SDK. Humans should read `README.md` instead.
|
|
4
|
+
|
|
5
|
+
The document is ordered so an agent can stop as soon as enough context has been loaded: package basics → setup call → where to mount → redaction → settings file → common mistakes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. What this package is
|
|
10
|
+
|
|
11
|
+
`restless-sdk` captures HTTP request/response pairs and ships them in batches to the Restless ingest server for dashboard display.
|
|
12
|
+
|
|
13
|
+
- **Runtime:** Ruby 2.6+. **No dependencies.**
|
|
14
|
+
- **Shape:** one Rack middleware. Rails, Sinatra, Hanami, Grape, Roda and anything else that speaks Rack all use the same one; there are no per-framework adapters.
|
|
15
|
+
- **Install name is not the require name:** you install `restless-sdk` and you `require "restless"`.
|
|
16
|
+
|
|
17
|
+
## 2. Install
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
gem install restless-sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
or in a `Gemfile`:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "restless-sdk"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
No other gems are required.
|
|
30
|
+
|
|
31
|
+
## 3. The one-line setup
|
|
32
|
+
|
|
33
|
+
Construct a client, register a per-request callback, mount the middleware:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require "restless"
|
|
37
|
+
|
|
38
|
+
CLIENT = Restless.new(ENV["RESTLESS_KEY"])
|
|
39
|
+
|
|
40
|
+
CLIENT.setup do |request|
|
|
41
|
+
{ api_key: CLIENT.mask(request.header("Authorization")),
|
|
42
|
+
owner: { id: workspace_id_for(request), enrich: method(:load_workspace) } }
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The client exposes four things:
|
|
47
|
+
|
|
48
|
+
| member | purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `setup(&block)` | Register the per-request callback. Takes a block or any callable. |
|
|
51
|
+
| `mask(key)` | Hash an end-user API key for safe logging. |
|
|
52
|
+
| `rack` | The middleware factory to mount. |
|
|
53
|
+
| `flush` | Force-upload the current batch (e.g. before exit). |
|
|
54
|
+
|
|
55
|
+
`Restless.new` and `Restless::Client.new` are the same thing. `mask` is also available as `Restless.mask` without a client, for scripts and tests.
|
|
56
|
+
|
|
57
|
+
**Construct the client once**, somewhere loaded at boot - `config/application.rb` under Rails, the top of `config.ru` otherwise. Not per request.
|
|
58
|
+
|
|
59
|
+
## 4. Where to mount it
|
|
60
|
+
|
|
61
|
+
`CLIENT.rack` returns a middleware factory: anything that calls `.new(app)` on it works.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
# config.ru - Sinatra, Roda, Hanami, Grape, plain Rack
|
|
65
|
+
use CLIENT.rack
|
|
66
|
+
run App
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# config/application.rb - Rails
|
|
71
|
+
config.middleware.insert_before 0, CLIENT.rack
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# No config.ru at all - wrap where the app is handed to the server
|
|
76
|
+
handler = CLIENT.rack.new(APP)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Mount as far OUT as you can.** An inner mount sees a different status and body than the client did.
|
|
80
|
+
|
|
81
|
+
**Rails: `insert_before 0`, not `use`.** `config.middleware.use` appends to the bottom of the stack, below `ActionDispatch::ShowExceptions`. From there the SDK records the rendered error page rather than the real exception, so a 500 loses its raise site and every crash groups by the wording of your error template.
|
|
82
|
+
|
|
83
|
+
**The mount is not in the file with the routes.** Rails routes live in `config/routes.rb` and the middleware does not go there. This trips up installers coming from frameworks where the two are the same file.
|
|
84
|
+
|
|
85
|
+
### Route patterns
|
|
86
|
+
|
|
87
|
+
The middleware reads the matched route template from the framework, so handlers do not have to report it:
|
|
88
|
+
|
|
89
|
+
| framework | source | reported as |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| Sinatra | `env["sinatra.route"]` (`GET /pets/:id`) | `/pets/{id}` |
|
|
92
|
+
| Rails | `env["action_dispatch.route_uri_pattern"]` | `/pets/{id}` |
|
|
93
|
+
| Grape | `env["grape.routing_args"]` | as matched |
|
|
94
|
+
| anything | `env["restless.route"]`, set by you | as given |
|
|
95
|
+
|
|
96
|
+
The method prefix is stripped, Rails' `(.:format)` is dropped and `:id`-style params are rewritten to `{id}`, so one endpoint produces the same `routePattern` here as it does in the Node, Python and Go SDKs. Override wholesale with `CLIENT.rack(route: ->(env) { ... })`.
|
|
97
|
+
|
|
98
|
+
## 5. The setup callback
|
|
99
|
+
|
|
100
|
+
The callback receives a read-only `RequestInfo` and returns a hash.
|
|
101
|
+
|
|
102
|
+
| accessor | what |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `request.header(name)` | One header, case-insensitive. `request["authorization"]` is an alias. |
|
|
105
|
+
| `request.request_method` | `"GET"`, `"POST"`, ... |
|
|
106
|
+
| `request.path` | `SCRIPT_NAME` + `PATH_INFO`. |
|
|
107
|
+
| `request.query_string` | Raw query string, without `?`. |
|
|
108
|
+
| `request.url` | Full URL. |
|
|
109
|
+
| `request.env` | The raw Rack env, for anything the view does not model. |
|
|
110
|
+
|
|
111
|
+
Use `header`, not `env["HTTP_AUTHORIZATION"]`.
|
|
112
|
+
|
|
113
|
+
Result fields:
|
|
114
|
+
|
|
115
|
+
| field | type | required | notes |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `api_key` | `String` or nil | no | Masked key from `CLIENT.mask`. Never plaintext. |
|
|
118
|
+
| `owner` | `Hash` | yes\* | The workspace / tenant / end-user this request belongs to. |
|
|
119
|
+
| `block` | `true` or `{ status:, message: }` | no | Rejects the request before the handler runs. |
|
|
120
|
+
|
|
121
|
+
\* Technically optional, but omitting it lands every log in the dashboard as "anonymous".
|
|
122
|
+
|
|
123
|
+
Extra top-level keys are carried through onto the log.
|
|
124
|
+
|
|
125
|
+
**The callback runs before your application**, because the middleware is mounted outside everything. `current_user`, Warden, and controller filters have not run yet. Resolve the owner from the credential yourself inside the block.
|
|
126
|
+
|
|
127
|
+
**A callback that raises is silently ignored.** SAFETY-002 requires that observability never breaks the request path, so an exception is caught and the result discarded. That is correct behaviour, and it means a wrong callback does not crash your app - it just quietly attributes nothing. Verify with §12 rather than assuming.
|
|
128
|
+
|
|
129
|
+
### 5.1 The `owner` hash
|
|
130
|
+
|
|
131
|
+
`owner[:id]` is the **permanent, immutable identifier** the dashboard pins a project's entire log history to. Misconfiguring it is the single biggest setup mistake.
|
|
132
|
+
|
|
133
|
+
| API shape | Use as `id` |
|
|
134
|
+
|---|---|
|
|
135
|
+
| Multi-tenant SaaS (`Account`, `Organization`, `Workspace`) | The tenant's id |
|
|
136
|
+
| Key owned by a project or service (`ApiKey belongs_to :project`) | The project id, not its creator |
|
|
137
|
+
| Per-user API (one key per developer) | The user's id |
|
|
138
|
+
| No identity model | Omit `owner` entirely |
|
|
139
|
+
|
|
140
|
+
**Never** an API key, email, username, JWT, or a placeholder literal like `"anonymous"` / `"none"` / `"guest"`. Anything that can rotate, or is a dummy string, is wrong.
|
|
141
|
+
|
|
142
|
+
For requests with no real owner, omit the key. A Ruby hash literal cannot omit a key conditionally, which is why the idiomatic shape builds the hash and then assigns:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
CLIENT.setup do |request|
|
|
146
|
+
result = { api_key: CLIENT.mask(request.header("Authorization")) }
|
|
147
|
+
|
|
148
|
+
workspace_id = resolve_workspace(request.header("Authorization"))
|
|
149
|
+
if workspace_id
|
|
150
|
+
result[:owner] = { id: workspace_id, enrich: method(:load_workspace) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5.2 `owner[:enrich]`
|
|
158
|
+
|
|
159
|
+
The **only** channel for owner metadata. Inline `label` / `email` keys on `owner` are dropped; everything except `id` comes back from `enrich`.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
enrich: ->(owner_id) {
|
|
163
|
+
workspace = Workspace.find(owner_id)
|
|
164
|
+
{ label: workspace.name, email: workspace.admin_emails } # String or Array
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
A lambda or a `method(:name)` reference both work.
|
|
169
|
+
|
|
170
|
+
Behaviour:
|
|
171
|
+
|
|
172
|
+
- Cached by `owner[:id]`. The first request from each owner runs it; the rest skip it.
|
|
173
|
+
- If an upload comes back with `needsEnrichment`, that owner is invalidated and the next request re-runs it.
|
|
174
|
+
- Exceptions are swallowed; the log still ships with the id.
|
|
175
|
+
- Skipped entirely when the same result also returns `block`, so a banned tenant does not cost a database round-trip per owner id forever.
|
|
176
|
+
- The resolved values ride on every subsequent upload, so each log carries full metadata without re-running the lookup.
|
|
177
|
+
|
|
178
|
+
## 6. The `mask()` gotcha
|
|
179
|
+
|
|
180
|
+
`CLIENT.mask(value)` produces `sha512-<base64>?<last4>`. The suffix is the LAST 4 CHARACTERS OF THE INPUT, so substituting a placeholder leaks it.
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
# CORRECT: nil when the header is missing
|
|
184
|
+
api_key: CLIENT.mask(request.header("Authorization"))
|
|
185
|
+
|
|
186
|
+
# WRONG: "mous" ends up as the mask tail
|
|
187
|
+
api_key: CLIENT.mask(request.header("Authorization") || "anonymous")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`mask` returns nil on nil input and the SDK handles it.
|
|
191
|
+
|
|
192
|
+
## 7. `.restless/settings.json`
|
|
193
|
+
|
|
194
|
+
Read at startup, walking up from the working directory. Created and owned by the `api` CLI (`npx api setup`). Every Restless SDK reads the same file with the same camelCase keys, so a polyglot repo needs only one.
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"version": 1,
|
|
199
|
+
"projectId": "<team/workspace uuid>",
|
|
200
|
+
"apis": [
|
|
201
|
+
{
|
|
202
|
+
"id": "<api uuid>",
|
|
203
|
+
"name": "Public API",
|
|
204
|
+
"rootDir": ".",
|
|
205
|
+
"oasFile": ".restless/openapi.yaml",
|
|
206
|
+
"framework": "rails",
|
|
207
|
+
"language": "ruby",
|
|
208
|
+
"baseUrl": "https://api.example.com",
|
|
209
|
+
"requestIdPrefix": "PUB",
|
|
210
|
+
"redact": { "headers": ["x-company-auth"], "bodyKeys": ["ssh_private_key"] }
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The SDK reads `requestIdPrefix` and `redact` from the matching entry. Pick one when several are defined:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
Restless.new(ENV["RESTLESS_KEY"], api: "Public API")
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 8. Redaction (on by default)
|
|
223
|
+
|
|
224
|
+
Sensitive values are redacted BEFORE anything leaves the process.
|
|
225
|
+
|
|
226
|
+
- **Headers:** `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, `x-api-key`, `x-auth-token`
|
|
227
|
+
- **Body keys and query params:** `password`, `pass`, `pwd`, `token`, `secret`, `apikey`, `accesstoken`, `refreshtoken`, `idtoken`, `sessionid`, `ssn`, `creditcard`, `ccnumber`, `cvv`, `cvc`
|
|
228
|
+
|
|
229
|
+
Matching is case-insensitive and ignores `-`/`_`, so `api_key` / `apiKey` / `API-KEY` all match. For `authorization` and `proxy-authorization` the scheme word survives (`Bearer <REDACTED:...>`).
|
|
230
|
+
|
|
231
|
+
Two additive sources on top of the defaults: `apis[].redact` in the settings file, and the `redact:` option. **The option accepts either casing**, so `body_keys:` and `bodyKeys` both work - snake_case reads better in Ruby, camelCase matches the wire format:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
Restless.new(key, redact: { headers: ["x-custom"], body_keys: ["api_secret"] })
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Sentinel format, which the dashboard pattern-matches:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
<REDACTED:<length>> # length < 8
|
|
241
|
+
<REDACTED:<length>:<last-4-chars>> # length >= 8
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Bodies are capped at **256 KiB** (UTF-8 bytes) and truncated with `[...TRUNCATED: original N bytes]`.
|
|
245
|
+
|
|
246
|
+
## 9. Request IDs and response injection
|
|
247
|
+
|
|
248
|
+
- Request IDs are v4 UUIDs, never time-based, so they leak no ordering.
|
|
249
|
+
- Every response gets `x-restless-id`. `x-request-id` is set only if the caller did not send one, and an incoming value is never reused as ours.
|
|
250
|
+
- On status **>= 400** the SDK adds `x-log-url` and `x-debug` headers, and merges a `debug` key into a JSON body. There is no user-configurable hook for this.
|
|
251
|
+
|
|
252
|
+
## 10. Blocking
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
CLIENT.setup do |request|
|
|
256
|
+
next { block: true } if banned?(request) # 403
|
|
257
|
+
next { block: { status: 429, message: "slow down" } } if limited?(request)
|
|
258
|
+
|
|
259
|
+
{ api_key: CLIENT.mask(request.header("Authorization")) }
|
|
260
|
+
end
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The handler never runs. The response body is JSON `{"error": "<message>"}`. `enrich` is not called for a blocked request.
|
|
264
|
+
|
|
265
|
+
## 11. Environment variables and batching
|
|
266
|
+
|
|
267
|
+
| variable | effect |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `RESTLESS_KEY` | Fallback API key when `Restless.new` is called without one |
|
|
270
|
+
| `README_API_KEY` | Secondary fallback |
|
|
271
|
+
| `RESTLESS_BASE_URL` | Override the ingest URL. **Non-localhost `http://` warns loudly** (plaintext auth). |
|
|
272
|
+
| `DEBUG=restless` | Print upload diagnostics to stderr |
|
|
273
|
+
| `RESTLESS_SETUP_MODE=1` | Upload even under a test runner (see below) |
|
|
274
|
+
|
|
275
|
+
Batching is fixed: 10 requests per batch, a 5000 ms flush interval, a 1000-entry queue that drops oldest on overflow, and immediate flushing against a localhost base URL. Uploads never block a response, and failures are swallowed unless `DEBUG=restless`.
|
|
276
|
+
|
|
277
|
+
**Test runs do not upload.** Detected via `RAILS_ENV`/`RACK_ENV` of `test`, or RSpec / Minitest / Test::Unit being loaded. Capture still runs, so your tests exercise the real path.
|
|
278
|
+
|
|
279
|
+
## 12. Common mistakes (don't do these)
|
|
280
|
+
|
|
281
|
+
- **Mounting with `config.middleware.use` in Rails.** That appends to the bottom of the stack, below `ShowExceptions`, so the SDK sees a rendered error page instead of the exception. Use `insert_before 0`.
|
|
282
|
+
- **Adding a `before_action` instead of middleware.** It runs far too late and only for requests that reach a controller, so every 404 and every rejected request goes unrecorded.
|
|
283
|
+
- **Editing `config/routes.rb`.** The middleware does not go there.
|
|
284
|
+
- **Reading `current_user` in the callback.** The middleware is outside the stack, so it has not been set yet. Resolve the owner from the credential.
|
|
285
|
+
- **`CLIENT.mask(header || "anonymous")`** - see §6.
|
|
286
|
+
- **Using an API key, email, username or JWT as `owner[:id]`** - see §5.1.
|
|
287
|
+
- **Inline `label` / `email` on `owner`** - dropped. They come back from `enrich`.
|
|
288
|
+
- **Constructing a client per request.** Once, at boot.
|
|
289
|
+
- **Reading `.env`, `config/master.key` or `config/credentials.yml.enc` to "check" the key.** LLMs: never read these.
|
|
290
|
+
- **`gem install restless`** - the gem is `restless-sdk`. `restless` is only the require name.
|
|
291
|
+
- **Looking for `restless/rails` or `restless/sinatra`.** There is one Rack middleware.
|
|
292
|
+
|
|
293
|
+
## 13. Quick verification after installation
|
|
294
|
+
|
|
295
|
+
1. `restless-sdk` appears in the `Gemfile`, and `bundle list` finds it.
|
|
296
|
+
2. `CLIENT.rack` is mounted in `config.ru` or `config/application.rb`, as far out as possible.
|
|
297
|
+
3. A `CLIENT.setup` block exists and reads its header via `request.header(...)`.
|
|
298
|
+
4. `.restless/settings.json` exists (created by `npx api setup`).
|
|
299
|
+
5. Starting the server and curling any endpoint returns an `x-restless-id` response header.
|