restless-sdk 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1147b0604f0fdb6ca8b2cdd39ffe03819391d0e5d663b70fa845fbe4b4b72b27
4
- data.tar.gz: f1bd487a972936ae678151d03f27edecb476b8c1f7b755ff5a88157f5d015e4e
3
+ metadata.gz: a346b73df699fa4b309117f63c42f284a971d392fff96ac0321510c9b5c06327
4
+ data.tar.gz: a57d20173e1a974bc5fde6b9c7dd6c20e4239f4f7c70cd572863f97628d9ddfc
5
5
  SHA512:
6
- metadata.gz: f7508f1e2f878289a16930cd56db0a0c5e150dc6fd5772a0c221cb97792a703f130f15d6a99e401b81bb713edbceba5adc835da4d994600248911f4d7f86b558
7
- data.tar.gz: c6e94661d61836cdb2dc408beefc0e5323afd17ffb3c5ec2e68beb2ec5ab1687af64ae72a6a8726d97e265d4a1343b04dea320687e0b952a7c392f279851e71d
6
+ metadata.gz: 34762d9a43d3294b0448c75a683a158d5ce241d88961177bfbcc215969bf9b4a6f6e0f362cfdb31973e8d05de827a8c56d243ab224132b0f3f3a96c4f7ba0c1b
7
+ data.tar.gz: bbc49f5ffc1a24e563a4cc609cc6f3a95d43b3f3d4090c78826b83c68cdb4ec5d4aab36cfd7b795ce033ed2e5796d56f0a89c3196d40b49b6227affa0ee13487
data/CONFORMANCE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  | | |
4
4
  |---|---|
5
- | **Spec version** | 1.0.0 |
5
+ | **Spec version** | 1.0.1 |
6
6
  | **Level** | L2 (core + batching, caches, injection, safety) |
7
7
  | **Reference** | `restlesshq/node` (`@restlessai/sdk`) |
8
8
  | **Driver** | `ruby exe/restless-conformance` |
@@ -12,7 +12,7 @@ Declared in `lib/restless/version.rb` (META-001).
12
12
  ## Verifying
13
13
 
14
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
15
+ assume it is checked out as a sibling (`../node`), which is how
16
16
  `setup.sh` in the install repo arranges things. The vectors in `spec/` here
17
17
  are a pinned copy, so `ruby -Ilib -Itest test/all.rb` alone works without it.
18
18
 
@@ -22,11 +22,11 @@ are a pinned copy, so `ruby -Ilib -Itest test/all.rb` alone works without it.
22
22
  ruby -Ilib -Itest test/all.rb
23
23
 
24
24
  # the shared cross-language harness
25
- node ../node-sdk/spec/harness/run-vectors.mjs -- ruby exe/restless-conformance
25
+ node ../node/spec/harness/run-vectors.mjs -- ruby exe/restless-conformance
26
26
 
27
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" \
28
+ node ../node/spec/harness/fuzz.mjs \
29
+ --ref "node ../node/spec/driver/.build/node.js" \
30
30
  --test "ruby exe/restless-conformance" \
31
31
  --iterations 8000 --seed 24301
32
32
  ```
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Copyright © 2026 Restless
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the “Software”), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7
+ the Software, and to permit persons to whom the Software is furnished to do so,
8
+ subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,3 +1,16 @@
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="docs/restless-init-dark.svg">
3
+ <img width="100%" src="docs/restless-init.svg" alt="Restless">
4
+ </picture>
5
+
6
+ Run in your codebase to get started:
7
+
8
+ ```sh
9
+ npx restless init
10
+ ```
11
+
12
+ This scans your project, figures out your framework, generates an OpenAPI spec, and automatically wires the SDK into your server.
13
+
1
14
  # restless-sdk
2
15
 
3
16
  Capture your API traffic and send it to [Restless](https://restless.ai).
@@ -5,7 +18,9 @@ Capture your API traffic and send it to [Restless](https://restless.ai).
5
18
  One Rack middleware covers **Rails**, **Sinatra**, **Hanami**, **Grape**,
6
19
  **Roda** and anything else that speaks Rack. Ruby 2.6+. No dependencies.
7
20
 
8
- ## Install
21
+ ## Manual installation
22
+
23
+ If you'd rather wire it up by hand:
9
24
 
10
25
  ```sh
11
26
  gem install restless-sdk
@@ -68,8 +83,8 @@ and body your app produced rather than an inner layer's.
68
83
  and friends are redacted before anything leaves your process. Bodies with
69
84
  nothing to redact are passed through byte for byte, so your payloads are
70
85
  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"
86
+ - **Error triage.** Every response gets `x-log-url` and `x-debug` headers,
87
+ and 4xx/5xx responses also get a `debug` block in the JSON body. If someone attaches a "next steps"
73
88
  message to an error in the dashboard, the SDK injects it as
74
89
  `debug.recovery` - read synchronously from an in-process cache, never
75
90
  blocking the response on a network call. A 5xx that groups by its throw
@@ -144,5 +159,4 @@ differential fuzzer run against the reference implementation. See
144
159
  [CONFORMANCE.md](./CONFORMANCE.md).
145
160
 
146
161
  ## License
147
-
148
- ISC
162
+ MIT
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # The Ruby conformance driver. See node-sdk/spec/driver/PROTOCOL.md.
4
+ # The Ruby conformance driver. See sdk/node/spec/driver/PROTOCOL.md.
5
5
  #
6
6
  # Dev-only: it is a thin shell over Restless::Conformance, which
7
7
  # test/test_vectors.rb also uses, so the vectors cannot describe behaviour the
data/install.md CHANGED
@@ -191,7 +191,7 @@ api_key: CLIENT.mask(request.header("Authorization") || "anonymous")
191
191
 
192
192
  ## 7. `.restless/settings.json`
193
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.
194
+ Read at startup, walking up from the working directory. Created and owned by the `restless` CLI (`npx restless init`). Every Restless SDK reads the same file with the same camelCase keys, so a polyglot repo needs only one.
195
195
 
196
196
  ```json
197
197
  {
@@ -247,7 +247,8 @@ Bodies are capped at **256 KiB** (UTF-8 bytes) and truncated with `[...TRUNCATED
247
247
 
248
248
  - Request IDs are v4 UUIDs, never time-based, so they leak no ordering.
249
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.
250
+ - On **every** status the SDK adds `x-log-url` and `x-debug` headers; on status **>= 400** it also merges a `debug` key into a JSON body. There is no user-configurable hook for this.
251
+ - `x-log-url` points at your project's public docs host, which the server tells the SDK on each upload. Until the first upload round-trips it is omitted rather than guessed: a URL that 404s is worse than no URL. The ingest host is never used for it.
251
252
 
252
253
  ## 10. Blocking
253
254
 
@@ -295,5 +296,5 @@ Batching is fixed: 10 requests per batch, a 5000 ms flush interval, a 1000-entry
295
296
  1. `restless-sdk` appears in the `Gemfile`, and `bundle list` finds it.
296
297
  2. `CLIENT.rack` is mounted in `config.ru` or `config/application.rb`, as far out as possible.
297
298
  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
+ 4. `.restless/settings.json` exists (created by `npx restless init`).
299
300
  5. Starting the server and curling any endpoint returns an `x-restless-id` response header.
@@ -25,7 +25,7 @@ module Restless
25
25
  @redact = redact || {}
26
26
  @enrich_cache = EnrichCache.new
27
27
  @recovery_cache = RecoveryCache.new
28
- @docs_url = nil
28
+ @portal_url = nil
29
29
  @docs_mutex = Mutex.new
30
30
  @callback = nil
31
31
  @uploader = Uploader.new(
@@ -43,8 +43,11 @@ module Restless
43
43
 
44
44
  # INJECT-006. The latest server-resolved docs origin, or nil when no batch
45
45
  # has round-tripped yet.
46
- def docs_url
47
- @docs_mutex.synchronize { @docs_url }
46
+ # INJECT-006. The server-published portal origin every injected URL is
47
+ # built on. Nil before the first upload round-trip, and then nothing is
48
+ # emitted rather than a guess.
49
+ def portal_url
50
+ @docs_mutex.synchronize { @portal_url }
48
51
  end
49
52
 
50
53
  def flush
@@ -61,7 +64,9 @@ module Restless
61
64
  docs = body["docsUrl"]
62
65
  if docs.is_a?(String) && !docs.empty?
63
66
  # Origin only; strip trailing slashes so the server can be lax.
64
- @docs_mutex.synchronize { @docs_url = docs.sub(%r{/+\z}, "") }
67
+ # The wire key stays `docsUrl`: every already-deployed SDK reads it,
68
+ # so renaming would strand them all with no portal origin (WIRE-023).
69
+ @docs_mutex.synchronize { @portal_url = docs.sub(%r{/+\z}, "") }
65
70
  end
66
71
 
67
72
  messages = body["recoveryMessages"].is_a?(Hash) ? body["recoveryMessages"] : {}
@@ -16,7 +16,7 @@ module Restless
16
16
  # through this one file, so it is impossible for the vectors to describe
17
17
  # behaviour the driver does not exhibit.
18
18
  #
19
- # See node-sdk/spec/driver/PROTOCOL.md. Internal: never part of the public
19
+ # See sdk/node/spec/driver/PROTOCOL.md. Internal: never part of the public
20
20
  # API, never shipped behaviour a customer depends on.
21
21
  module Conformance
22
22
  # An input this implementation's language cannot represent or parse. The
@@ -79,6 +79,8 @@ module Restless
79
79
  # --- injection (section 10) ---
80
80
  when "recoverySlug"
81
81
  Injection.recovery_slug(str_or_nil(input["method"]), str_or_nil(input["path"]))
82
+ when "debugInjection"
83
+ debug_injection(input)
82
84
 
83
85
  # --- HAR (section 7) ---
84
86
  when "harEntry"
@@ -89,6 +91,21 @@ module Restless
89
91
  end
90
92
  end
91
93
 
94
+ # The observable surface only: the headers, and the `debug` object the
95
+ # adapter would merge (nil when there is nothing to merge).
96
+ def debug_injection(input)
97
+ built = Injection.build(
98
+ status: input["status"].to_i,
99
+ request_id: str(input["requestId"]),
100
+ prefix: str_or_nil(input["prefix"]),
101
+ recovery: str_or_nil(input["recovery"]),
102
+ method: str_or_nil(input["method"]),
103
+ path: str_or_nil(input["path"]),
104
+ portal_url: str_or_nil(input["portalUrl"])
105
+ )
106
+ { "headers" => built[:headers], "debug" => built[:debug] }
107
+ end
108
+
92
109
  def fingerprint(input)
93
110
  stack = stack_text(input["stackTrace"])
94
111
  if !stack.empty? && v8_dialect?(stack)
@@ -31,19 +31,37 @@ module Restless
31
31
  flat.empty? ? m : "#{m}-#{flat}"
32
32
  end
33
33
 
34
+ # INJECT-002. The debug response headers, which ship on every status.
35
+ #
36
+ # `x-log-url` is omitted with no portal origin; `x-debug` carries no URL,
37
+ # so it always ships.
38
+ def headers(request_id:, prefix: nil, portal_url: nil)
39
+ display = RequestId.format_request_id(request_id, prefix)
40
+ out = { "x-debug" => "npx api debug #{display}" }
41
+ out["x-log-url"] = "#{portal_url}/logs/#{request_id}" unless portal_url.nil? || portal_url.empty?
42
+ out
43
+ end
44
+
34
45
  # INJECT-001..004, INJECT-006. Returns the headers to set plus the `debug`
35
- # object to merge into a JSON body, or nil when nothing should be injected.
36
- def build(status:, request_id:, base_url:, prefix: nil, recovery: nil,
37
- method: nil, path: nil, docs_url: nil)
38
- return nil if status < 400 # INJECT-001
46
+ # object to merge into a JSON body (nil when there is nothing to merge).
47
+ #
48
+ # `portal_url` is the project's public portal origin, published by the
49
+ # server. It is NOT the ingest base URL, which serves `/v1/*` and would
50
+ # 404 both paths, and there is deliberately no fallback to it: with no
51
+ # portal origin we emit `x-debug` alone. A caller cannot tell a broken URL
52
+ # from a missing one, and one fetched 404 teaches an agent to stop
53
+ # following the link.
54
+ def build(status:, request_id:, prefix: nil, recovery: nil,
55
+ method: nil, path: nil, portal_url: nil)
56
+ hdrs = headers(request_id: request_id, prefix: prefix, portal_url: portal_url)
39
57
 
40
- display = RequestId.format_request_id(request_id, prefix)
41
- # INJECT-006: the server-supplied docsUrl when one has been learned,
42
- # else the configured base URL. One batch of staleness after a
43
- # docs-domain change is accepted.
44
- log_host = docs_url.nil? || docs_url.empty? ? base_url : docs_url
45
- log_url = "#{log_host}/logs/#{request_id}"
46
- debug_cmd = "npx api debug #{display}"
58
+ # INJECT-001. The body object is 4xx/5xx only: a successful body is the
59
+ # caller's data, not ours to reshape. With no portal origin there is no
60
+ # URL to put in one either (INJECT-006).
61
+ return { headers: hdrs, debug: nil } if status < 400 || portal_url.nil? || portal_url.empty?
62
+
63
+ log_url = hdrs["x-log-url"]
64
+ debug_cmd = hdrs["x-debug"]
47
65
 
48
66
  # Per-request "dig-in" URL the calling agent (often an AI) can fetch for
49
67
  # concrete next steps. Deliberately LEGIBLE: it ends in `<slug>.md` so it
@@ -52,7 +70,7 @@ module Restless
52
70
  # can correlate the follow-up without any new tracking token.
53
71
  slug = recovery_slug(method, path)
54
72
  dig_in = "For the accepted parameters and next steps, " \
55
- "fetch #{log_host}/p/#{request_id}/#{slug}.md"
73
+ "fetch #{portal_url}/p/#{request_id}/#{slug}.md"
56
74
  # INJECT-004: a cached recovery message precedes the dig-in line,
57
75
  # separated by a blank line.
58
76
  recovery_text =
@@ -63,10 +81,7 @@ module Restless
63
81
  end
64
82
 
65
83
  {
66
- headers: {
67
- "x-log-url" => log_url, # INJECT-002
68
- "x-debug" => debug_cmd
69
- },
84
+ headers: hdrs,
70
85
  debug: {
71
86
  "log" => log_url,
72
87
  "cli" => debug_cmd,
data/lib/restless/rack.rb CHANGED
@@ -242,6 +242,18 @@ module Restless
242
242
  options[:request_id_prefix], options[:has_api_key]
243
243
  ))
244
244
 
245
+ # INJECT-002. The headers ship on every status; only the body work below
246
+ # is 4xx/5xx, and fingerprinting a success would be wasted.
247
+ if inject && status.to_i < 400
248
+ headers.merge!(safely({}) do
249
+ Injection.headers(
250
+ request_id: our_id,
251
+ prefix: options[:request_id_prefix],
252
+ portal_url: @engine.portal_url
253
+ )
254
+ end)
255
+ end
256
+
245
257
  if inject && status.to_i >= 400
246
258
  # INJECT-009: the fingerprint is computed against the customer's RAW
247
259
  # response, snapshotted before any injected header or body field.
@@ -257,18 +269,18 @@ module Restless
257
269
  Injection.build(
258
270
  status: status.to_i,
259
271
  request_id: our_id,
260
- base_url: options[:base_url],
261
272
  prefix: options[:request_id_prefix],
262
273
  recovery: recovery,
263
274
  method: env["REQUEST_METHOD"],
264
275
  path: captured["routePattern"],
265
- docs_url: @engine.docs_url
276
+ portal_url: @engine.portal_url
266
277
  )
267
278
  end
268
279
 
269
280
  if injection
270
281
  headers.merge!(injection[:headers]) # INJECT-002
271
- rewritten = Injection.apply_body(body_text, headers["content-type"],
282
+ rewritten = injection[:debug] &&
283
+ Injection.apply_body(body_text, headers["content-type"],
272
284
  injection[:debug]) # INJECT-003
273
285
  if rewritten && !rewritten.equal?(body_text) && rewritten != body_text
274
286
  body = [rewritten]
@@ -4,7 +4,7 @@ require "json"
4
4
 
5
5
  module Restless
6
6
  # CONTRACT.md section 12. `.restless/settings.json` is created and owned by
7
- # the `api` CLI (`npx api setup`); the SDK consumes exactly two fields of it
7
+ # the `restless` CLI (`npx restless init`); the SDK consumes exactly two fields of it
8
8
  # at runtime (CONFIG-015).
9
9
  module Settings
10
10
  class ConfigError < StandardError; end
@@ -6,7 +6,7 @@ module Restless
6
6
  # META-001: the spec version an SDK implements must be recorded in a
7
7
  # machine-readable form alongside its conformance level.
8
8
 
9
- VERSION = "0.1.0"
9
+ VERSION = "0.1.1"
10
10
 
11
11
  # WIRE-016: distinct per implementation, so the ingest can attribute a
12
12
  # payload to a language.
@@ -17,7 +17,7 @@ module Restless
17
17
  # REDACT-010 requires full Unicode name folding
18
18
  # to require full Unicode lowercase. Keep this in step with
19
19
  # spec/VECTORS_VERSION.
20
- SPEC_VERSION = "1.0.0"
20
+ SPEC_VERSION = "1.0.1"
21
21
 
22
22
  # CONTRACT.md 1.1: L1 is the pure functions, L2 adds batching, caches,
23
23
  # injection and the safety guarantees.
data/spec/VECTORS_VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.0.1
@@ -0,0 +1,219 @@
1
+ {
2
+ "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
+ "$contract": "spec/CONTRACT.md",
4
+ "specVersion": "1.0.1",
5
+ "cases": [
6
+ {
7
+ "id": "inject/2xx-headers-only",
8
+ "requirement": "INJECT-001",
9
+ "op": "debugInjection",
10
+ "input": {
11
+ "status": 200,
12
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
13
+ "portalUrl": "https://acme.restlessdocs.com",
14
+ "method": "GET",
15
+ "path": "/car/{id}"
16
+ },
17
+ "expected": {
18
+ "headers": {
19
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
20
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
21
+ },
22
+ "debug": null
23
+ },
24
+ "note": "Headers on every status; the body object stays 4xx/5xx only."
25
+ },
26
+ {
27
+ "id": "inject/2xx-301",
28
+ "requirement": "INJECT-001",
29
+ "op": "debugInjection",
30
+ "input": {
31
+ "status": 301,
32
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
33
+ "portalUrl": "https://acme.restlessdocs.com"
34
+ },
35
+ "expected": {
36
+ "headers": {
37
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
38
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
39
+ },
40
+ "debug": null
41
+ }
42
+ },
43
+ {
44
+ "id": "inject/4xx-full",
45
+ "requirement": "INJECT-002",
46
+ "op": "debugInjection",
47
+ "input": {
48
+ "status": 404,
49
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
50
+ "portalUrl": "https://acme.restlessdocs.com",
51
+ "method": "GET",
52
+ "path": "/car/{id}"
53
+ },
54
+ "expected": {
55
+ "headers": {
56
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
57
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
58
+ },
59
+ "debug": {
60
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
61
+ "cli": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
62
+ "recovery": "For the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/get-car-id.md"
63
+ }
64
+ },
65
+ "note": "Both URLs share the portal origin."
66
+ },
67
+ {
68
+ "id": "inject/5xx-full",
69
+ "requirement": "INJECT-002",
70
+ "op": "debugInjection",
71
+ "input": {
72
+ "status": 500,
73
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
74
+ "portalUrl": "https://acme.restlessdocs.com",
75
+ "method": "POST",
76
+ "path": "/orders"
77
+ },
78
+ "expected": {
79
+ "headers": {
80
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
81
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
82
+ },
83
+ "debug": {
84
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
85
+ "cli": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
86
+ "recovery": "For the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/post-orders.md"
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "id": "inject/no-portal-2xx",
92
+ "requirement": "INJECT-006",
93
+ "op": "debugInjection",
94
+ "input": {
95
+ "status": 200,
96
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
97
+ },
98
+ "expected": {
99
+ "headers": {
100
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
101
+ },
102
+ "debug": null
103
+ },
104
+ "note": "No portal origin: x-debug only, and never a URL on the ingest host."
105
+ },
106
+ {
107
+ "id": "inject/no-portal-4xx",
108
+ "requirement": "INJECT-006",
109
+ "op": "debugInjection",
110
+ "input": {
111
+ "status": 404,
112
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
113
+ "method": "GET",
114
+ "path": "/car/{id}"
115
+ },
116
+ "expected": {
117
+ "headers": {
118
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
119
+ },
120
+ "debug": null
121
+ },
122
+ "note": "No portal origin means no debug object at all, so no dig-in line."
123
+ },
124
+ {
125
+ "id": "inject/prefix",
126
+ "requirement": "INJECT-002",
127
+ "op": "debugInjection",
128
+ "input": {
129
+ "status": 404,
130
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
131
+ "prefix": "TST",
132
+ "portalUrl": "https://acme.restlessdocs.com"
133
+ },
134
+ "expected": {
135
+ "headers": {
136
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
137
+ "x-debug": "npx api debug TST-9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
138
+ },
139
+ "debug": {
140
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
141
+ "cli": "npx api debug TST-9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
142
+ "recovery": "For the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/unknown.md"
143
+ }
144
+ },
145
+ "note": "x-log-url carries the RAW id; only the CLI hint is prefixed."
146
+ },
147
+ {
148
+ "id": "inject/authored-recovery",
149
+ "requirement": "INJECT-004",
150
+ "op": "debugInjection",
151
+ "input": {
152
+ "status": 402,
153
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
154
+ "portalUrl": "https://acme.restlessdocs.com",
155
+ "recovery": "Try another card.",
156
+ "method": "POST",
157
+ "path": "/charge"
158
+ },
159
+ "expected": {
160
+ "headers": {
161
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
162
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
163
+ },
164
+ "debug": {
165
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
166
+ "cli": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
167
+ "recovery": "Try another card.\n\nFor the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/post-charge.md"
168
+ }
169
+ },
170
+ "note": "The authored hint precedes the dig-in line, separated by a blank line."
171
+ },
172
+ {
173
+ "id": "inject/unknown-slug",
174
+ "requirement": "INJECT-005",
175
+ "op": "debugInjection",
176
+ "input": {
177
+ "status": 404,
178
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
179
+ "portalUrl": "https://acme.restlessdocs.com",
180
+ "method": "GET"
181
+ },
182
+ "expected": {
183
+ "headers": {
184
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
185
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
186
+ },
187
+ "debug": {
188
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
189
+ "cli": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
190
+ "recovery": "For the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/unknown.md"
191
+ }
192
+ }
193
+ },
194
+ {
195
+ "id": "inject/trailing-slash-origin",
196
+ "requirement": "WIRE-023",
197
+ "op": "debugInjection",
198
+ "input": {
199
+ "status": 404,
200
+ "requestId": "9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
201
+ "portalUrl": "https://acme.restlessdocs.com",
202
+ "method": "GET",
203
+ "path": "/x"
204
+ },
205
+ "expected": {
206
+ "headers": {
207
+ "x-log-url": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
208
+ "x-debug": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f"
209
+ },
210
+ "debug": {
211
+ "log": "https://acme.restlessdocs.com/logs/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
212
+ "cli": "npx api debug 9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f",
213
+ "recovery": "For the accepted parameters and next steps, fetch https://acme.restlessdocs.com/p/9f18a0e2-1c3d-4b5a-8e7f-0a1b2c3d4e5f/get-x.md"
214
+ }
215
+ },
216
+ "note": "The origin is stripped of trailing slashes on ingest (WIRE-023), so the SDK appends directly."
217
+ }
218
+ ]
219
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "fp/404-parameterized",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "har/basic",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "mask/ascii",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "slug/basic",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "redactValue/long",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
3
  "$contract": "spec/CONTRACT.md",
4
- "specVersion": "1.0.0",
4
+ "specVersion": "1.0.1",
5
5
  "cases": [
6
6
  {
7
7
  "id": "reqid/format-with-prefix",
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restless-sdk
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
  - Restless
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-08-11 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: |
14
13
  Rack middleware that captures API traffic, redacts secrets before anything
@@ -16,14 +15,14 @@ description: |
16
15
  recovered from, and uploads the result to Restless. Works with Rails,
17
16
  Sinatra, Hanami, Grape, Roda and anything else that speaks Rack.
18
17
 
19
- Implements version 1.0.0 of the Restless SDK Contract at
18
+ Implements version 1.0.1 of the Restless SDK Contract at
20
19
  level L2.
21
- email:
22
20
  executables: []
23
21
  extensions: []
24
22
  extra_rdoc_files: []
25
23
  files:
26
24
  - CONFORMANCE.md
25
+ - LICENSE
27
26
  - README.md
28
27
  - exe/restless-conformance
29
28
  - install.md
@@ -46,6 +45,7 @@ files:
46
45
  - lib/restless/uploader.rb
47
46
  - lib/restless/version.rb
48
47
  - spec/VECTORS_VERSION
48
+ - spec/vectors/debug-injection.json
49
49
  - spec/vectors/fingerprint.json
50
50
  - spec/vectors/har.json
51
51
  - spec/vectors/mask.json
@@ -54,13 +54,12 @@ files:
54
54
  - spec/vectors/request-id.json
55
55
  homepage: https://restless.ai
56
56
  licenses:
57
- - ISC
57
+ - MIT
58
58
  metadata:
59
59
  source_code_uri: https://github.com/restlesshq/ruby
60
60
  bug_tracker_uri: https://github.com/restlesshq/ruby/issues
61
- restless_spec_version: 1.0.0
61
+ restless_spec_version: 1.0.1
62
62
  restless_conformance_level: L2
63
- post_install_message:
64
63
  rdoc_options: []
65
64
  require_paths:
66
65
  - lib
@@ -75,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
74
  - !ruby/object:Gem::Version
76
75
  version: '0'
77
76
  requirements: []
78
- rubygems_version: 3.0.3.1
79
- signing_key:
77
+ rubygems_version: 4.0.16
80
78
  specification_version: 4
81
79
  summary: Capture your API traffic and send it to Restless.
82
80
  test_files: []