Skip to content

feat(api,proxy,daemon,sdk): pre-signed file download/upload URLs with key rotation across all SDKs#5099

Open
MDzaja wants to merge 1 commit into
mainfrom
feat/download-link
Open

feat(api,proxy,daemon,sdk): pre-signed file download/upload URLs with key rotation across all SDKs#5099
MDzaja wants to merge 1 commit into
mainfrom
feat/download-link

Conversation

@MDzaja

@MDzaja MDzaja commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Adds pre-signed file URLs so callers can download from / upload to a sandbox over plain HTTP without an auth token — the URL itself is the credential. URLs are HMAC-signed, optionally time-limited, and can be invalidated by rotating the sandbox's signing key. Ships end-to-end across the API, daemon, proxy, generated API clients, and all five SDKs (Python sync + async, TypeScript, Go, Ruby, Java).

Motivation

Today, every file transfer goes through the authenticated toolbox path. Pre-signed URLs let users hand a download/upload link to any HTTP client (browser, curl, a third party) without exposing their API key.

How it works

  • Canonical string: v1:files:{METHOD}:{path}:{expires}
  • Signature: HMAC-SHA256(signingKey, canonical) → base64url (no padding) → prefixed v1_
  • URL shape: {toolboxProxyUrl}/{sandboxId}/files/{download|upload-v2}?path=...&expires=...&signature=...
  • Expiry: default TTL 3600s; zero or negative TTL → expires=0 (never expires)
  • Verification: the proxy authenticates the signature instead of the normal auth chain when it detects a signed file request; everything else falls through to the existing auth path.
  • Key distribution / caching:
    • API owns the key in a new sandbox_metadata table and caches it in Redis (300s).
    • Proxy keeps a short signing-key cache and refetches on signature mismatch, so a rotation done elsewhere is picked up immediately for new URLs.
    • SDK clients cache the key locally for 15s to avoid an API round-trip per URL.

Changes by component

API (NestJS)

  • New sandbox_metadata table (sandboxId PK, signingKey, FK → sandbox ON DELETE CASCADE) via pre-deploy migration. Table + FK only — no backfill; rows are lazily created on first key read.
  • SandboxMetadata entity + module registration.
  • sandbox.service.ts: ensureMetadata() (lazy, race-safe), getSigningKey() (Redis cache 300s), rotateSigningKey() (regenerates key, write-through to Redis cache to prevent stale-key race on concurrent reads).
  • Signing-key endpoints on the sandbox + preview controllers (get / rotate), with auth-spec coverage.
  • New audit action ROTATE_SIGNING_KEY.

Daemon (Go)

  • upload_file.go: dual-endpoint design for backward compatibility:
    • POST /files/upload — legacy endpoint, kept alive but undocumented (no swagger annotations). Returns empty 200. Old SDK clients continue working unchanged.
    • POST /files/upload-v2 — new documented endpoint. Returns a single UploadedFile object ({name, path, type}). Both new SDK clients and signed upload URLs use this endpoint.
    • Both share a common handler accepting multipart (file field) and raw application/octet-stream, with parent-dir creation (MkdirAll) and proper Close() error checking.
  • server.go: register HEAD /files/download (proxy already permits HEAD for signed downloads; gin needs it declared explicitly).
  • Regenerated swagger docs.

Proxy (Go)

  • signed_request_auth.go (new): generic signed-request core — verify request, compute/verify signature, parse expiry, strip signature params, signing-key cache + fetch + refetch-on-mismatch. Uses passed context (not context.Background()) for proper cancellation.
  • file_url_auth.go (new): file-URL flavor — isSignedFileUrlRequest, fileUrlCanonical, verifySignedFileUrl.
  • file_url_auth_test.go (new): cross-language signature vectors lock the wire format.
  • get_sandbox_target.go: dispatch — signed file request → signature auth, else normal auth. Non-signed traffic is unaffected (the else if branch is byte-identical to the previous condition).
  • proxy.go: signing-key cache wiring.

Generated API clients (all languages)

  • Get / rotate signing-key endpoints (preview + sandbox APIs).
  • New toolbox UploadedFile model for the upload response shape.
  • Upload endpoint now points to /files/upload-v2.

SDKs (all five)

  • download_url / upload_url / rotate_signing_key (idiomatic naming per language) + a file-URL signing utility + 15s signing-key cache with mutex protection (Go).
  • Error interception on signing-key fetch failures (Python sync + async).
  • Input validation: Number.isFinite guard + Math.floor on TTL (TypeScript).
  • Python (_sync + _async), TypeScript, Go, Ruby, Java.

Docs

  • Regenerated SDK reference pages for all languages.

Backward compatibility

  • Old SDK clients → new daemon: POST /files/upload still returns empty 200 (the legacy endpoint is registered in server.go but has no swagger annotations, so it doesn't appear in regenerated clients). Verified live: old endpoint returns empty body, new /files/upload-v2 returns the UploadedFile object.
  • New SDK clients → old daemon: New SDK features (download_url, upload_url, rotate_signing_key) naturally require the new daemon version. Standard version-skew behavior for new features — not a regression.
  • Existing auth paths: The proxy's signed-URL dispatch is narrowly gated (toolbox request + exact path/method + signature query param). Non-signed traffic follows the identical auth path as before.
  • SandboxDto: signingKey is not exposed on any DTO.

Public SDK surface (example: Python)

url = sandbox.download_url("/home/user/report.pdf")          # GET, no auth header needed
# curl "$url" -o report.pdf

url = sandbox.upload_url("/home/user/data.bin")              # POST multipart, field "file"
# curl -X POST -F "file=@local.bin" "$url"

sandbox.rotate_signing_key()                                  # invalidates previously issued URLs

Docstrings document the bounded-staleness contract: the key is cached for up to 15s, so a URL may be rejected for a few seconds if the key was rotated from another client.

@nx-cloud

nx-cloud Bot commented Jun 19, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit fd6d245

Command Status Duration Result
nx e2e:cleanup daytona-e2e ✅ Succeeded <1s View ↗
nx run sdk-typescript:test:runtime ✅ Succeeded 3m 21s View ↗
nx run-many --target=test:e2e --all --nxBail=true ✅ Succeeded 6m 56s View ↗
nx e2e daytona-e2e ✅ Succeeded 1m 1s View ↗
nx run-many --target=build --projects=api,runne... ✅ Succeeded 1m 6s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-19 14:05:10 UTC

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

21 issues found across 85 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread apps/api/src/sandbox/services/sandbox.service.ts
Comment thread libs/sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java
Comment thread libs/sdk-java/src/main/java/io/daytona/sdk/FileUrlSigning.java
Comment thread libs/sdk-python/tests/test_e2e.py
Comment thread libs/sdk-ruby/lib/daytona/sandbox.rb
Comment thread apps/proxy/pkg/proxy/signed_request_auth.go Outdated
Comment thread apps/api/src/sandbox/services/sandbox.service.ts Outdated
Comment thread libs/sdk-python/tests/test_file_url_signing.py Outdated
Comment thread libs/sdk-go/pkg/daytona/file_url_signing.go Outdated
Comment thread libs/sdk-python/src/daytona/_utils/file_url_signing.py Outdated
@MDzaja MDzaja force-pushed the feat/download-link branch 5 times, most recently from 10cbec8 to bb4b462 Compare June 19, 2026 12:10
… key rotation across all SDKs

Signed-off-by: MDzaja <mirkodzaja0@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant