feat(sdk): pre-signed sandbox file download/upload URLs#23
Conversation
Signed-off-by: MDzaja <mirkodzaja0@gmail.com>
There was a problem hiding this comment.
30 issues found across 68 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="toolbox-api-client-go/model_uploaded_file.go">
<violation number="1" location="toolbox-api-client-go/model_uploaded_file.go:163">
P2: Required string fields allow JSON null and are coerced to empty strings. This bypasses required-field validation and can hide invalid API payloads.</violation>
</file>
<file name="openapi-specs/toolbox.json">
<violation number="1" location="openapi-specs/toolbox.json:2838">
P2: Spec advertises raw-body upload support but models only multipart upload. This creates incorrect generated client/docs behavior for upload-v2.</violation>
</file>
<file name="api-client-ruby/lib/daytona_api_client/api/preview_api.rb">
<violation number="1" location="api-client-ruby/lib/daytona_api_client/api/preview_api.rb:145">
P1: Do not log the returned signing key; redact `data` in this debug message to avoid leaking signed-URL secrets into application logs.</violation>
</file>
<file name="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/file_system_api.py">
<violation number="1" location="toolbox-api-client-python-async/daytona_toolbox_api_client_async/api/file_system_api.py:3079">
P2: `upload_file` promises raw request body uploads, but the generated method exposes only the multipart `file` field and never passes a body. Callers using `application/octet-stream` as documented will fail instead of uploading raw bytes.</violation>
</file>
<file name="toolbox-api-client/src/api/file-system-api.ts">
<violation number="1" location="toolbox-api-client/src/api/file-system-api.ts:496">
P1: Raw-body uploads are advertised but impossible through this client because `options.data` is overwritten with FormData when `file` is omitted.</violation>
</file>
<file name="sdk-go/pkg/daytona/e2e_test.go">
<violation number="1" location="sdk-go/pkg/daytona/e2e_test.go:919">
P3: HTTP calls use default client with no timeout. If the server hangs, the test goroutine blocks until the test binary's hard timeout kills it.</violation>
</file>
<file name="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb">
<violation number="1" location="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb:1399">
P1: Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.</violation>
<violation number="2" location="api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb:2561">
P1: Rotated signing key is exposed through debug response logging. Remove/redact `Data` from this log line.</violation>
</file>
<file name="toolbox-api-client-python/daytona_toolbox_api_client/api/file_system_api.py">
<violation number="1" location="toolbox-api-client-python/daytona_toolbox_api_client/api/file_system_api.py:3063">
P2: `upload_file` now allows `file=None`, but the serializer has no raw-body parameter path, so advertised non-multipart uploads are not actually supported. This can send empty uploads or fail when callers set `_content_type` to `application/octet-stream`.</violation>
</file>
<file name="sdk-typescript/src/utils/fileUrlSigning.ts">
<violation number="1" location="sdk-typescript/src/utils/fileUrlSigning.ts:6">
P2: New signing helper is Node-crypto-only and can break browser runtime for `downloadUrl`/`uploadUrl`. Add a browser-compatible HMAC path (Web Crypto) or explicitly gate this API with a clear runtime error.</violation>
</file>
<file name="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java">
<violation number="1" location="toolbox-api-client-java/src/main/java/io/daytona/toolbox/client/api/FileSystemApi.java:1633">
P2: `_file` is now advertised as nullable, but a null upload builds an empty multipart request and fails client-side before the API call. Keep the file required for this overload or add a separate raw-body upload path.</violation>
</file>
<file name="toolbox-api-client-go/api_file_system.go">
<violation number="1" location="toolbox-api-client-go/api_file_system.go:178">
P2: UploadFile advertises raw-body uploads, but this client cannot provide a raw request payload. This makes the new contract unreachable from Go and can send empty invalid requests when no file is set.</violation>
</file>
<file name="sdk-python/src/daytona/_utils/file_url_signing.py">
<violation number="1" location="sdk-python/src/daytona/_utils/file_url_signing.py:27">
P2: `resolve_expires` accepts non-integer TTL values and can generate signed URLs with non-integer `expires` values. That can produce signatures the server rejects even though URL generation succeeded.</violation>
<violation number="2" location="sdk-python/src/daytona/_utils/file_url_signing.py:48">
P3: URL construction does not normalize a trailing slash in `toolbox_proxy_url`, which can emit double-slash paths. Some proxies/routers treat `//` paths differently and may fail routing.</violation>
</file>
<file name="sdk-java/src/test/java/io/daytona/sdk/E2ETest.java">
<violation number="1" location="sdk-java/src/test/java/io/daytona/sdk/E2ETest.java:569">
P2: HttpClient.newHttpClient() creates clients with no timeout — a hung server blocks the test suite indefinitely. Add connect + read timeouts.</violation>
</file>
<file name="sdk-java/src/main/java/io/daytona/sdk/Sandbox.java">
<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/Sandbox.java:359">
P2: Use the resolved toolbox proxy URL for signed file URLs. Otherwise objects created from DTOs with missing/empty toolboxProxyUrl can return invalid relative download/upload URLs even though the constructor already resolved a working proxy for toolbox APIs.</violation>
</file>
<file name="sdk-ruby/lib/daytona/utils/file_url_signing.rb">
<violation number="1" location="sdk-ruby/lib/daytona/utils/file_url_signing.rb:47">
P2: Normalize `toolbox_proxy_url` before appending `sandbox_id`; otherwise DTO values ending in `/` produce signed URLs with a doubled slash before the sandbox ID, which can miss strict proxy routes.</violation>
</file>
<file name="sdk-go/pkg/daytona/file_url_signing.go">
<violation number="1" location="sdk-go/pkg/daytona/file_url_signing.go:48">
P3: Unreachable error branch: `resolveExpires` never returns a non-nil error, so `if err != nil` is dead code. Remove the unused error return path to simplify and clarify control flow.</violation>
<violation number="2" location="sdk-go/pkg/daytona/file_url_signing.go:58">
P2: Signed file URL construction does not normalize trailing slash on toolboxProxyUrl, which can generate `//` in the path and break pre-signed URL routing.</violation>
</file>
<file name="sdk-go/pkg/daytona/sandbox.go">
<violation number="1" location="sdk-go/pkg/daytona/sandbox.go:427">
P3: Use `time.Since(...) <= 15*time.Second` instead of `.Seconds() <= 15` for idiomatic duration comparison, which avoids floating-point comparison and is more readable.</violation>
<violation number="2" location="sdk-go/pkg/daytona/sandbox.go:429">
P2: Thundering herd: when the cached key expires, concurrent goroutines all release the lock and fetch the key in parallel. Consider `singleflight` to coalesce concurrent fetches into one API call.</violation>
</file>
<file name="sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java">
<violation number="1" location="sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java:65">
P1: Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return `/<sandboxId>/files/...` instead of an absolute URL for unauthenticated HTTP clients.</violation>
</file>
<file name="sdk-python/tests/test_e2e.py">
<violation number="1" location="sdk-python/tests/test_e2e.py:936">
P3: Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.</violation>
<violation number="2" location="sdk-python/tests/test_e2e.py:940">
P2: Signed URL tests share implicit filesystem state (`SIGNED_URL_TEST_DIR` directory and files) without ordering enforcement — they break under `pytest-random-order` or any non-definition collection order.</violation>
</file>
<file name="sdk-python/tests/test_async_e2e.py">
<violation number="1" location="sdk-python/tests/test_async_e2e.py:197">
P2: Signed URL tests have implicit ordering dependency without enforcement. Running individual tests via `-k` fails with opaque errors because `test_async_signed_url_setup` hasn't created the directory/file.</violation>
</file>
<file name="sdk-typescript/src/Sandbox.ts">
<violation number="1" location="sdk-typescript/src/Sandbox.ts:795">
P3: Inconsistent trailing-slash handling: constructor normalizes `toolboxProxyUrl` for the axios base URL, but `downloadUrl`/`uploadUrl` pass it raw to `buildSignedFileUrl` which unconditionally inserts a `/` separator. If the API ever returns a URL with a trailing slash, signed URLs will have `//` in the path while regular API calls won't.</violation>
</file>
<file name="sdk-python/src/daytona/_async/sandbox.py">
<violation number="1" location="sdk-python/src/daytona/_async/sandbox.py:275">
P3: Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.</violation>
<violation number="2" location="sdk-python/src/daytona/_async/sandbox.py:277">
P2: Signing-key cache has a race between fetch and rotate. Concurrent calls can overwrite a rotated key with an older key and generate invalid URLs.</violation>
</file>
<file name="sdk-python/src/daytona/_sync/sandbox.py">
<violation number="1" location="sdk-python/src/daytona/_sync/sandbox.py:289">
P3: Magic number `15` (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.</violation>
</file>
<file name="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/api/file_system_api.rb">
<violation number="1" location="toolbox-api-client-ruby/lib/daytona_toolbox_api_client/api/file_system_api.rb:768">
P1: Ruby `upload_file` became incompatible with existing positional `file` calls; passing a File as second argument now causes runtime errors because the method expects an options hash.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
|
|
||
| data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options) | ||
| if @api_client.config.debugging | ||
| @api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" |
There was a problem hiding this comment.
P1: Do not log the returned signing key; redact data in this debug message to avoid leaking signed-URL secrets into application logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/preview_api.rb, line 145:
<comment>Do not log the returned signing key; redact `data` in this debug message to avoid leaking signed-URL secrets into application logs.</comment>
<file context>
@@ -86,6 +86,67 @@ def get_sandbox_id_from_signed_preview_url_token_with_http_info(signed_preview_t
+
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
+ if @api_client.config.debugging
+ @api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+ end
+ return data, status_code, headers
</file context>
| @api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" | |
| @api_client.config.logger.debug "API called: PreviewApi#get_signing_key\nData: [REDACTED]\nStatus code: #{status_code}\nHeaders: #{headers}" |
| * @throws {RequiredError} | ||
| */ | ||
| uploadFile: async (path: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { | ||
| uploadFile: async (path: string, file?: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => { |
There was a problem hiding this comment.
P1: Raw-body uploads are advertised but impossible through this client because options.data is overwritten with FormData when file is omitted.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At toolbox-api-client/src/api/file-system-api.ts, line 496:
<comment>Raw-body uploads are advertised but impossible through this client because `options.data` is overwritten with FormData when `file` is omitted.</comment>
<file context>
@@ -484,19 +486,17 @@ export const FileSystemApiAxiosParamCreator = function (configuration?: Configur
* @throws {RequiredError}
*/
- uploadFile: async (path: string, file: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
+ uploadFile: async (path: string, file?: File, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'path' is not null or undefined
assertParamExists('uploadFile', 'path', path)
</file context>
|
|
||
| data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options) | ||
| if @api_client.config.debugging | ||
| @api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" |
There was a problem hiding this comment.
P1: Rotated signing key is exposed through debug response logging. Remove/redact Data from this log line.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb, line 2561:
<comment>Rotated signing key is exposed through debug response logging. Remove/redact `Data` from this log line.</comment>
<file context>
@@ -2435,6 +2499,70 @@ def revoke_ssh_access_with_http_info(sandbox_id_or_name, opts = {})
+
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
+ if @api_client.config.debugging
+ @api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+ end
+ return data, status_code, headers
</file context>
| @api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" | |
| @api_client.config.logger.debug "API called: SandboxApi#rotate_signing_key\nStatus code: #{status_code}\nHeaders: #{headers}" |
|
|
||
| data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options) | ||
| if @api_client.config.debugging | ||
| @api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" |
There was a problem hiding this comment.
P1: Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api-client-ruby/lib/daytona_api_client/api/sandbox_api.rb, line 1399:
<comment>Sensitive signing key is written to debug logs. Redact/remove response body logging for this endpoint.</comment>
<file context>
@@ -1337,6 +1337,70 @@ def get_sandbox_parent_with_http_info(sandbox_id_or_name, opts = {})
+
+ data, status_code, headers = @api_client.call_api(:GET, local_var_path, new_options)
+ if @api_client.config.debugging
+ @api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
+ end
+ return data, status_code, headers
</file context>
| @api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}" | |
| @api_client.config.logger.debug "API called: SandboxApi#get_sandbox_signing_key\nStatus code: #{status_code}\nHeaders: #{headers}" |
| String encodedExpires = URLEncoder.encode(String.valueOf(expires), StandardCharsets.UTF_8); | ||
| String encodedSignature = URLEncoder.encode(signature, StandardCharsets.UTF_8); | ||
|
|
||
| return trimTrailingSlash(toolboxProxyUrl) + "/" + sandboxId + operationPath |
There was a problem hiding this comment.
P1: Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return /<sandboxId>/files/... instead of an absolute URL for unauthenticated HTTP clients.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java, line 65:
<comment>Missing toolbox proxy URL is silently turned into a relative signed URL. Sandboxes constructed from DTOs without toolboxProxyUrl can return `/<sandboxId>/files/...` instead of an absolute URL for unauthenticated HTTP clients.</comment>
<file context>
@@ -0,0 +1,81 @@
+ String encodedExpires = URLEncoder.encode(String.valueOf(expires), StandardCharsets.UTF_8);
+ String encodedSignature = URLEncoder.encode(signature, StandardCharsets.UTF_8);
+
+ return trimTrailingSlash(toolboxProxyUrl) + "/" + sandboxId + operationPath
+ + "?path=" + encodedPath
+ + "&expires=" + encodedExpires
</file context>
|
|
||
| func (s *Sandbox) ensureSigningKey(ctx context.Context) (string, error) { | ||
| s.signingKeyMu.Lock() | ||
| if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 { |
There was a problem hiding this comment.
P3: Use time.Since(...) <= 15*time.Second instead of .Seconds() <= 15 for idiomatic duration comparison, which avoids floating-point comparison and is more readable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-go/pkg/daytona/sandbox.go, line 427:
<comment>Use `time.Since(...) <= 15*time.Second` instead of `.Seconds() <= 15` for idiomatic duration comparison, which avoids floating-point comparison and is more readable.</comment>
<file context>
@@ -415,6 +422,29 @@ func (s *Sandbox) doRefreshData(ctx context.Context) error {
+func (s *Sandbox) ensureSigningKey(ctx context.Context) (string, error) {
+ s.signingKeyMu.Lock()
+ if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 {
+ key := s.signingKey
+ s.signingKeyMu.Unlock()
</file context>
| if s.signingKey != "" && time.Since(s.signingKeyFetchedAt).Seconds() <= 15 { | |
| if s.signingKey != "" && time.Since(s.signingKeyFetchedAt) <= 15*time.Second { |
| def test_download_url_returns_signed_url(sandbox): | ||
| url = sandbox.download_url(f"{SIGNED_URL_TEST_DIR}/download-test.txt") | ||
| assert isinstance(url, str) and len(url) > 0, "download_url should return a non-empty string" | ||
| assert "signature=" in url, f"URL should contain signature param: {url}" |
There was a problem hiding this comment.
P3: Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/tests/test_e2e.py, line 936:
<comment>Full signed URL is interpolated into assertion error messages — leaks a valid signed URL to CI logs on test failure.</comment>
<file context>
@@ -913,6 +914,53 @@ def test_sandbox_remains_usable_after_archive_cycle(sandbox):
+def test_download_url_returns_signed_url(sandbox):
+ url = sandbox.download_url(f"{SIGNED_URL_TEST_DIR}/download-test.txt")
+ assert isinstance(url, str) and len(url) > 0, "download_url should return a non-empty string"
+ assert "signature=" in url, f"URL should contain signature param: {url}"
+ assert "expires=" in url, f"URL should contain expires param: {url}"
+
</file context>
| assert "signature=" in url, f"URL should contain signature param: {url}" | |
| assert "signature=" in url, "download_url should return a signed URL" |
| @WithInstrumentation() | ||
| public async downloadUrl(path: string, ttlSeconds?: number): Promise<string> { | ||
| return buildSignedFileUrl( | ||
| this.toolboxProxyUrl, |
There was a problem hiding this comment.
P3: Inconsistent trailing-slash handling: constructor normalizes toolboxProxyUrl for the axios base URL, but downloadUrl/uploadUrl pass it raw to buildSignedFileUrl which unconditionally inserts a / separator. If the API ever returns a URL with a trailing slash, signed URLs will have // in the path while regular API calls won't.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-typescript/src/Sandbox.ts, line 795:
<comment>Inconsistent trailing-slash handling: constructor normalizes `toolboxProxyUrl` for the axios base URL, but `downloadUrl`/`uploadUrl` pass it raw to `buildSignedFileUrl` which unconditionally inserts a `/` separator. If the API ever returns a URL with a trailing slash, signed URLs will have `//` in the path while regular API calls won't.</comment>
<file context>
@@ -768,6 +771,78 @@ export class Sandbox {
+ @WithInstrumentation()
+ public async downloadUrl(path: string, ttlSeconds?: number): Promise<string> {
+ return buildSignedFileUrl(
+ this.toolboxProxyUrl,
+ this.id,
+ '/files/download',
</file context>
|
|
||
| async def _ensure_signing_key(self) -> str: | ||
| key = self._signing_key | ||
| if key is None or (time.time() - self._signing_key_fetched_at) > 15: |
There was a problem hiding this comment.
P3: Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/src/daytona/_async/sandbox.py, line 275:
<comment>Signing-key TTL uses wall-clock time instead of a monotonic clock. Clock adjustments can break refresh timing and prolong stale-key usage.</comment>
<file context>
@@ -267,6 +270,100 @@ async def set_labels(self, labels: dict[str, str]) -> dict[str, str]:
+ async def _ensure_signing_key(self) -> str:
+ key = self._signing_key
+ if key is None or (time.time() - self._signing_key_fetched_at) > 15:
+ key = await self._sandbox_api.get_sandbox_signing_key(self.id)
+ self._signing_key = key
</file context>
|
|
||
| def _ensure_signing_key(self) -> str: | ||
| key = self._signing_key | ||
| if key is None or (time.time() - self._signing_key_fetched_at) > 15: |
There was a problem hiding this comment.
P3: Magic number 15 (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-python/src/daytona/_sync/sandbox.py, line 289:
<comment>Magic number `15` (signing key cache TTL in seconds) should be a named constant for maintainability and consistency with the docstrings that reference it.</comment>
<file context>
@@ -281,6 +284,88 @@ def set_labels(self, labels: dict[str, str]) -> dict[str, str]:
+ def _ensure_signing_key(self) -> str:
+ key = self._signing_key
+ if key is None or (time.time() - self._signing_key_fetched_at) > 15:
+ key = self._sandbox_api.get_sandbox_signing_key(self.id)
+ self._signing_key = key
</file context>
Original monorepo PR: daytonaio/daytona#5099
Summary
Client-side counterpart of the pre-signed sandbox file URL feature: SDK helpers for signing file
download/upload URLs and key rotation across all five SDKs, the regenerated API/toolbox clients,
and the vendored openapi specs.
Client changes included
sdk-typescript,sdk-python,sdk-go,sdk-java,sdk-ruby—fileUrlSigningutilities,
Sandbox/FileSystemsigning methods, and e2e tests.api-client*,toolbox-api-client*— signing-key endpoints +UploadedFilemodel.
openapi-specs/api.json,openapi-specs/toolbox.json.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Add pre-signed sandbox file URLs with signing-key fetch/rotation and SDK helpers to generate download/upload links. Also update toolbox upload to accept multipart or raw bodies and return an
UploadedFile, with regeneratedapi-client*andtoolbox-api-client*plus vendored OpenAPI specs.New Features
sdk-typescript,sdk-python,sdk-go,sdk-java,sdk-ruby): file URL signing utilities andSandbox/FileSystemhelpers to create signed download/upload URLs; lightweight signing-key caching; e2e tests.api-client*): addGET /sandbox/{sandboxId}/signing-keyand rotate endpoints; preview API: signing-key getter; includes tests.toolbox-api-client*): upload now accepts multipart file or raw body and returnsUploadedFile.Migration
uploadFilenow returnsUploadedFile(name/path/type). Update callers expecting a generic map/object.toolbox_api.upload_file(path, file: io_or_file)replaces the positional file arg. Other SDKs now treat the file as optional (raw body supported).Written for commit eda6686. Summary will update on new commits.