From 24f2dfda3783feb75928629a4f8f3d081999a9d5 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:29:59 -0400 Subject: [PATCH 1/5] Add Ruby as a supported output language Follows the exact same pattern as python/javascript/typescript: - New prompt partial (prompts/partials/_language_ruby.md): uses net/http and json - both part of Ruby's standard library, so no gem/Bundler dependency is needed at all, no project/dependency file of any kind. Calls out that net/http has no built-in cookie jar (unlike some other languages' HTTP clients), so cookie-based auth needs manual Set-Cookie capture and replay. Same auth-hardcoding/ refresh guidance as the other languages otherwise. - base_engineer.py: added "ruby" to _OUTPUT_LANGUAGE_EXTENSIONS (.rb) and _get_language_name() ("Ruby"). _get_run_command() uses the full path rather than a bare relative "ruby api_client.rb" - the same working-directory fix already applied to Go/Java/C#/PHP, applied proactively here too since ruby has the identical shape (the agent's cwd is scripts_dir.parent.parent, not scripts_dir where the script is actually saved). - cli.py: added Ruby to the interactive `output_language` config picker. - Updated the three docstrings/comments that listed the language set (config.py, engineer.py, prompts/__init__.py) and README.md's output language line. - Tests: mirrored the existing per-language test pattern in test_base_engineer.py (extension, run command, prompt content) and test_prompts.py (partial loading) for Ruby. Checked the partial for the literal-brace str.format_map() bug caught in the Java PR - no literal `{`/`}` anywhere in the new file. Full test suite passes (49/49, run 3x) except the same pre-existing flaky test noted on the other language PRs (test_existing_client_language_falls_back_to_newest_file - a mtime-resolution race condition, unrelated to this change). ruff check is clean on every file this touches (8 pre-existing lint errors elsewhere on main, none in the touched lines). --- CHANGELOG.md | 1 + README.md | 2 +- src/reverse_api/base_engineer.py | 9 +++++++++ src/reverse_api/cli.py | 1 + src/reverse_api/config.py | 2 +- src/reverse_api/engineer.py | 2 +- src/reverse_api/prompts/__init__.py | 2 +- .../prompts/partials/_language_ruby.md | 20 +++++++++++++++++++ tests/test_base_engineer.py | 16 +++++++++++++++ tests/test_prompts.py | 10 ++++++++++ 10 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/reverse_api/prompts/partials/_language_ruby.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dd3c4112..731f63ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Java output language**: `output_language: "java"` is now supported alongside python/javascript/typescript, generating a small Maven project using `java.net.http.HttpClient` (JDK 11+, no HTTP library dependency) and Gson for JSON, with the same auth-hardcoding/refresh guidance as the other languages. - **C# output language**: `output_language: "csharp"` is now supported alongside python/javascript/typescript, generating a minimal .NET project using `System.Net.Http.HttpClient` and `System.Text.Json` (both part of the .NET 5+ base class library — no NuGet dependency needed), with the same auth-hardcoding/refresh guidance as the other languages. - **PHP output language**: `output_language: "php"` is now supported alongside python/javascript/typescript, generating a script using the `curl` and `json_encode`/`json_decode` core extensions (`ext-curl`, `ext-json` — no Composer dependency needed), with the same auth-hardcoding/refresh guidance as the other languages. +- **Ruby output language**: `output_language: "ruby"` is now supported alongside python/javascript/typescript, generating a script using `net/http` and `json` (both part of Ruby's standard library — no gem/Bundler dependency needed), with the same auth-hardcoding/refresh guidance as the other languages. ## [0.10.0] - 2026-06-01 diff --git a/README.md b/README.md index d29998cf..8c576fa9 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Settings live in `~/.reverse-api/config.json` and can be edited via `/settings` - **Models**: Sonnet 4.6 (default), Opus 4.6 (most capable), Haiku 4.5 (fastest). For OpenCode see [models.dev](https://models.dev). - **SDK**: `claude` (default), `opencode`, `cursor`, or `copilot` (GitHub Copilot). -- **Output language**: `python`, `javascript`, `typescript`, `go`, `java`, `csharp`, or `php`. +- **Output language**: `python`, `javascript`, `typescript`, `go`, `java`, `csharp`, `php`, or `ruby`. ## CLI diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index c1fbc1d5..5b31213e 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -38,6 +38,7 @@ class BaseEngineer(ABC): "java": ".java", "csharp": ".cs", "php": ".php", + "ruby": ".rb", } def __init__( @@ -448,6 +449,7 @@ def _get_language_name(self) -> str: "java": "Java", "csharp": "C#", "php": "PHP", + "ruby": "Ruby", }.get(self.output_language, "Python") def _get_existing_client_guidance(self) -> str: @@ -530,6 +532,13 @@ def _get_run_command(self) -> str: # pointing this command at the wrong, doubly-nested location. path = shlex.quote(str(self.scripts_dir.resolve() / self._get_client_filename())) return f"php {path}" + if self.output_language == "ruby": + # Full path, not a bare relative "ruby api_client.rb": the + # agent's cwd for the whole session is scripts_dir.parent.parent + # (see analyze_and_generate's ClaudeAgentOptions), not + # scripts_dir itself where the script is actually saved — the + # same working-directory ambiguity fixed for Go/Java/C#/PHP. + return f'ruby "{self.scripts_dir}/{self._get_client_filename()}"' return { "python": "python api_client.py", "javascript": "node api_client.js", diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index f3966104..33876e1c 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -1087,6 +1087,7 @@ def handle_settings(mode_color=THEME_PRIMARY): Choice(title="java", value="java"), Choice(title="csharp", value="csharp"), Choice(title="php", value="php"), + Choice(title="ruby", value="ruby"), Choice(title="back", value="back"), ] lang = questionary.select( diff --git a/src/reverse_api/config.py b/src/reverse_api/config.py index cc7de3bb..06fa019a 100644 --- a/src/reverse_api/config.py +++ b/src/reverse_api/config.py @@ -20,7 +20,7 @@ "opencode_model": "claude-opus-4-6", "opencode_provider": "anthropic", "output_dir": None, # None means use ~/.reverse-api/runs - "output_language": "python", # "python", "javascript", "typescript", "go", "java", "csharp", or "php" + "output_language": "python", # "python", "javascript", "typescript", "go", "java", "csharp", "php", or "ruby" "real_time_sync": True, # Enable real-time file sync during engineering "sdk": "claude", # "claude", "opencode", "copilot", or "cursor" } diff --git a/src/reverse_api/engineer.py b/src/reverse_api/engineer.py index 5d900276..4b408a87 100644 --- a/src/reverse_api/engineer.py +++ b/src/reverse_api/engineer.py @@ -232,7 +232,7 @@ def run_reverse_engineering( cursor_setting_sources: Optional explicit list (overrides cursor_web_search), e.g. ["project","user","all"]. enable_sync: Enable real-time file syncing during engineering is_fresh: Whether to start fresh (ignore previous scripts) - output_language: Target language - "python", "javascript", "typescript", "go", "java", "csharp", or "php" + output_language: Target language - "python", "javascript", "typescript", "go", "java", "csharp", "php", or "ruby" output_mode: Output mode - "client" for API client code, "docs" for OpenAPI specification """ if sdk == "opencode": diff --git a/src/reverse_api/prompts/__init__.py b/src/reverse_api/prompts/__init__.py index 336458ce..dc317e01 100644 --- a/src/reverse_api/prompts/__init__.py +++ b/src/reverse_api/prompts/__init__.py @@ -54,7 +54,7 @@ def load_language_partial(language: str, **kwargs: str) -> str: """Load the language-specific codegen instructions partial. Args: - language: One of "python", "javascript", "typescript", "go", "java", "csharp", "php". + language: One of "python", "javascript", "typescript", "go", "java", "csharp", "php", "ruby". **kwargs: Placeholder values (scripts_dir, client_filename, run_command). """ return load(f"partials/_language_{language}", **kwargs) diff --git a/src/reverse_api/prompts/partials/_language_ruby.md b/src/reverse_api/prompts/partials/_language_ruby.md new file mode 100644 index 00000000..a4200a79 --- /dev/null +++ b/src/reverse_api/prompts/partials/_language_ruby.md @@ -0,0 +1,20 @@ +**Generate a Ruby script** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: + +- Use `net/http` and `json` for requests and JSON — both are part of Ruby's standard library, so no gem/Bundler dependency is needed +- Reuse one persistent connection (`Net::HTTP.start` held open across calls) rather than opening a new connection per request +- Create a separate method for each distinct API endpoint +- Include example usage at the bottom of the script + +**Authentication & credentials:** +- Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the script +- The user should be able to run the script immediately with zero configuration — no env vars, no config files, no `bundle install` +- `net/http` has no built-in cookie jar, unlike some other languages' HTTP clients — if the API uses cookies, capture the `Set-Cookie` header(s) from each response and send them back as a `Cookie` header on subsequent requests +- If the API uses Bearer tokens or API keys, hardcode them in the request headers +- Handle auth refresh so the script doesn't go stale: if you see a token refresh endpoint, OAuth refresh flow, or login endpoint in the traffic, implement automatic re-authentication when a request returns 401/403. If cookies have expiry, re-fetch them before they expire + +**Testing:** +- Run: `{run_command}` +- You have up to 5 attempts to fix issues + +Save the script to: `{scripts_dir}/{client_filename}` +Save documentation to: `{scripts_dir}/README.md` diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 285af4ef..b615a8e5 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -210,6 +210,10 @@ def test_get_output_extension_php(self, tmp_path): """PHP extension.""" eng = self._make_engineer(tmp_path, output_language="php") assert eng._get_output_extension() == ".php" + def test_get_output_extension_ruby(self, tmp_path): + """Ruby extension.""" + eng = self._make_engineer(tmp_path, output_language="ruby") + assert eng._get_output_extension() == ".rb" def test_get_output_extension_unknown(self, tmp_path): """Unknown language defaults to .py.""" @@ -343,6 +347,12 @@ def test_get_run_command_php_resolves_relative_output_dir(self, tmp_path): script_arg = tokens[1] assert Path(script_arg).is_absolute() assert script_arg == str(eng.scripts_dir.resolve() / "api_client.php") + def test_get_run_command_ruby(self, tmp_path): + """Run command for Ruby uses the full path, not a bare relative + filename — the agent's cwd is scripts_dir.parent.parent (see + analyze_and_generate), not scripts_dir where the script lives.""" + eng = self._make_engineer(tmp_path, output_language="ruby") + assert eng._get_run_command() == f'ruby "{eng.scripts_dir}/api_client.rb"' def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" @@ -413,6 +423,12 @@ def test_php_prompt(self, tmp_path): system_prompt, user_message = eng._build_prompts() assert "PHP script" in system_prompt assert "curl" in system_prompt + def test_ruby_prompt(self, tmp_path): + """Ruby prompt includes Ruby-specific instructions.""" + eng = self._make_engineer(tmp_path, output_language="ruby") + system_prompt, user_message = eng._build_prompts() + assert "Ruby script" in system_prompt + assert "net/http" in system_prompt def test_docs_prompt(self, tmp_path): """Docs mode prompt includes OpenAPI instructions.""" diff --git a/tests/test_prompts.py b/tests/test_prompts.py index f4786e3c..6d2521e2 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -111,6 +111,16 @@ def test_php_partial(self): assert "PHP script" in text assert "curl" in text assert "/tmp/scripts/api_client.php" in text + def test_ruby_partial(self): + text = load_language_partial( + "ruby", + scripts_dir="/tmp/scripts", + client_filename="api_client.rb", + run_command='ruby "/tmp/scripts/api_client.rb"', + ) + assert "Ruby script" in text + assert "net/http" in text + assert "/tmp/scripts/api_client.rb" in text class TestEngineerTemplates: From ac47b99180150fd828f75a4cdecdcfa5863fba1b Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:54:49 -0400 Subject: [PATCH 2/5] Fix shell-quoting in the Ruby run command Manually double-quoting the path (f'ruby "{path}"') does not stop $()/backtick command substitution inside a POSIX shell double-quoted string. Use shlex.quote() instead, matching the fix already applied to the Go/Java/C#/PHP run commands after bot review flagged the same issue on the PHP PR. --- src/reverse_api/base_engineer.py | 5 ++++- tests/test_base_engineer.py | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 5b31213e..4a966238 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -538,7 +538,10 @@ def _get_run_command(self) -> str: # (see analyze_and_generate's ClaudeAgentOptions), not # scripts_dir itself where the script is actually saved — the # same working-directory ambiguity fixed for Go/Java/C#/PHP. - return f'ruby "{self.scripts_dir}/{self._get_client_filename()}"' + # shlex.quote (not manual double-quoting) so shell metacharacters + # in the path can't be interpreted as command substitution. + path = shlex.quote(f"{self.scripts_dir}/{self._get_client_filename()}") + return f"ruby {path}" return { "python": "python api_client.py", "javascript": "node api_client.js", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index b615a8e5..1c2325ad 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -348,11 +348,22 @@ def test_get_run_command_php_resolves_relative_output_dir(self, tmp_path): assert Path(script_arg).is_absolute() assert script_arg == str(eng.scripts_dir.resolve() / "api_client.php") def test_get_run_command_ruby(self, tmp_path): - """Run command for Ruby uses the full path, not a bare relative - filename — the agent's cwd is scripts_dir.parent.parent (see - analyze_and_generate), not scripts_dir where the script lives.""" + """Run command for Ruby uses the full, shell-quoted path, not a bare + relative filename — the agent's cwd is scripts_dir.parent.parent + (see analyze_and_generate), not scripts_dir where the script lives.""" eng = self._make_engineer(tmp_path, output_language="ruby") - assert eng._get_run_command() == f'ruby "{eng.scripts_dir}/api_client.rb"' + expected_path = shlex.quote(f"{eng.scripts_dir}/api_client.rb") + assert eng._get_run_command() == f"ruby {expected_path}" + + def test_get_run_command_ruby_quotes_metacharacters(self, tmp_path): + """A scripts_dir containing shell metacharacters must round-trip + back to the literal path, not be left open to $()/backtick + expansion — what the naive f'"{path}"' approach got wrong.""" + eng = self._make_engineer(tmp_path, output_language="ruby") + eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") + tokens = shlex.split(eng._get_run_command()) + assert tokens[0] == "ruby" + assert tokens[1] == f"{eng.scripts_dir}/api_client.rb" def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 91c9d978c92b5557366cde5bef3c5427754e9115 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 16:00:50 -0400 Subject: [PATCH 3/5] Scope connection reuse and cookie replay to origin in the Ruby partial Both review bots flagged the same two issues: "reuse one persistent connection" doesn't account for captured traffic spanning multiple hosts (Net::HTTP.start is bound to a single host:port), and replaying every captured cookie as one Cookie header ignores Set-Cookie's domain/path/secure scoping, which can leak a session cookie to the wrong origin. Reworded both bullets to scope per origin instead. --- src/reverse_api/prompts/partials/_language_ruby.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reverse_api/prompts/partials/_language_ruby.md b/src/reverse_api/prompts/partials/_language_ruby.md index a4200a79..3d8c80a5 100644 --- a/src/reverse_api/prompts/partials/_language_ruby.md +++ b/src/reverse_api/prompts/partials/_language_ruby.md @@ -1,14 +1,14 @@ **Generate a Ruby script** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: - Use `net/http` and `json` for requests and JSON — both are part of Ruby's standard library, so no gem/Bundler dependency is needed -- Reuse one persistent connection (`Net::HTTP.start` held open across calls) rather than opening a new connection per request +- Reuse persistent connections (`Net::HTTP.start` held open across calls) rather than opening a new connection per request — use a separate connection per distinct scheme/host/port, since one is bound to a single origin and captured traffic may span several (e.g. separate login, API, and upload hosts) - Create a separate method for each distinct API endpoint - Include example usage at the bottom of the script **Authentication & credentials:** - Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the script - The user should be able to run the script immediately with zero configuration — no env vars, no config files, no `bundle install` -- `net/http` has no built-in cookie jar, unlike some other languages' HTTP clients — if the API uses cookies, capture the `Set-Cookie` header(s) from each response and send them back as a `Cookie` header on subsequent requests +- `net/http` has no built-in cookie jar, unlike some other languages' HTTP clients — if the API uses cookies, capture each `Set-Cookie` header's name/value and its domain, path, and secure attributes, and send back only the cookies matching a given request's origin and path in the `Cookie` header, rather than replaying every captured cookie on every request (a multi-origin trace can otherwise leak a session cookie to the wrong host or send an invalid header) - If the API uses Bearer tokens or API keys, hardcode them in the request headers - Handle auth refresh so the script doesn't go stale: if you see a token refresh endpoint, OAuth refresh flow, or login endpoint in the traffic, implement automatic re-authentication when a request returns 401/403. If cookies have expiry, re-fetch them before they expire From 2978715897aba929f621cc08559e805fc11769da Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 16:59:37 -0400 Subject: [PATCH 4/5] Resolve scripts_dir to an absolute path in the Ruby run command cubic flagged that a relative --output-dir breaks this command: scripts_dir stays relative to the original cwd, but once the agent's cwd moves to scripts_dir.parent.parent (see analyze_and_generate), embedding that same relative string re-interprets it from the new location and points at a doubly-nested, nonexistent path. Resolve scripts_dir before interpolating it, same fix as just applied to the C# run command. --- src/reverse_api/base_engineer.py | 6 +++++- tests/test_base_engineer.py | 24 +++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 4a966238..77f05f9b 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -540,7 +540,11 @@ def _get_run_command(self) -> str: # same working-directory ambiguity fixed for Go/Java/C#/PHP. # shlex.quote (not manual double-quoting) so shell metacharacters # in the path can't be interpreted as command substitution. - path = shlex.quote(f"{self.scripts_dir}/{self._get_client_filename()}") + # .resolve(): a relative --output-dir would otherwise be + # re-interpreted against the agent's cwd (scripts_dir.parent. + # parent) instead of the original cwd it was relative to, + # pointing this command at the wrong, doubly-nested location. + path = shlex.quote(str(self.scripts_dir.resolve() / self._get_client_filename())) return f"ruby {path}" return { "python": "python api_client.py", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 1c2325ad..2493b2cc 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -348,11 +348,12 @@ def test_get_run_command_php_resolves_relative_output_dir(self, tmp_path): assert Path(script_arg).is_absolute() assert script_arg == str(eng.scripts_dir.resolve() / "api_client.php") def test_get_run_command_ruby(self, tmp_path): - """Run command for Ruby uses the full, shell-quoted path, not a bare - relative filename — the agent's cwd is scripts_dir.parent.parent - (see analyze_and_generate), not scripts_dir where the script lives.""" + """Run command for Ruby uses the full, resolved, shell-quoted path, + not a bare relative filename — the agent's cwd is scripts_dir. + parent.parent (see analyze_and_generate), not scripts_dir where the + script lives.""" eng = self._make_engineer(tmp_path, output_language="ruby") - expected_path = shlex.quote(f"{eng.scripts_dir}/api_client.rb") + expected_path = shlex.quote(str(eng.scripts_dir.resolve() / "api_client.rb")) assert eng._get_run_command() == f"ruby {expected_path}" def test_get_run_command_ruby_quotes_metacharacters(self, tmp_path): @@ -363,7 +364,20 @@ def test_get_run_command_ruby_quotes_metacharacters(self, tmp_path): eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") tokens = shlex.split(eng._get_run_command()) assert tokens[0] == "ruby" - assert tokens[1] == f"{eng.scripts_dir}/api_client.rb" + assert tokens[1] == str(eng.scripts_dir.resolve() / "api_client.rb") + + def test_get_run_command_ruby_resolves_relative_output_dir(self, tmp_path): + """A relative scripts_dir must be resolved to an absolute path before + being embedded in the command — otherwise, once the agent's cwd + moves to scripts_dir.parent.parent, the same relative string gets + re-interpreted from there and points at the wrong, doubly-nested + location.""" + eng = self._make_engineer(tmp_path, output_language="ruby") + eng.scripts_dir = Path("relative_output/scripts/run123") + tokens = shlex.split(eng._get_run_command()) + script_arg = tokens[1] + assert Path(script_arg).is_absolute() + assert script_arg == str(eng.scripts_dir.resolve() / "api_client.rb") def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 916f68d2d739c711f8a81fc5185d04ad8ef9d19b Mon Sep 17 00:00:00 2001 From: kalil0321 Date: Wed, 22 Jul 2026 13:17:34 +0200 Subject: [PATCH 5/5] Require cookie expiration handling in the Ruby cookie-jar guidance A Max-Age=0 or Expires deletion from a refresh/logout response must evict the stored entry, or the generated script keeps replaying a dead session value. Key entries by name, domain, and path. --- src/reverse_api/prompts/partials/_language_ruby.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reverse_api/prompts/partials/_language_ruby.md b/src/reverse_api/prompts/partials/_language_ruby.md index 3d8c80a5..bb283891 100644 --- a/src/reverse_api/prompts/partials/_language_ruby.md +++ b/src/reverse_api/prompts/partials/_language_ruby.md @@ -8,7 +8,7 @@ **Authentication & credentials:** - Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the script - The user should be able to run the script immediately with zero configuration — no env vars, no config files, no `bundle install` -- `net/http` has no built-in cookie jar, unlike some other languages' HTTP clients — if the API uses cookies, capture each `Set-Cookie` header's name/value and its domain, path, and secure attributes, and send back only the cookies matching a given request's origin and path in the `Cookie` header, rather than replaying every captured cookie on every request (a multi-origin trace can otherwise leak a session cookie to the wrong host or send an invalid header) +- `net/http` has no built-in cookie jar, unlike some other languages' HTTP clients — if the API uses cookies, store each `Set-Cookie` header by name, domain, and path; enforce its secure, `Expires`, and `Max-Age` attributes; remove expired or deleted entries; and send back only cookies matching the request's origin and path in the `Cookie` header (a multi-origin trace can otherwise leak a session cookie to the wrong host or send an invalid header) - If the API uses Bearer tokens or API keys, hardcode them in the request headers - Handle auth refresh so the script doesn't go stale: if you see a token refresh endpoint, OAuth refresh flow, or login endpoint in the traffic, implement automatic re-authentication when a request returns 401/403. If cookies have expiry, re-fetch them before they expire