From 97256c397ba6e91ac6642fb45b19de0b7e41a4ad Mon Sep 17 00:00:00 2001 From: "hotdata-automation[bot]" <267177015+hotdata-automation[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:00:59 +0000 Subject: [PATCH 1/3] chore(api): stop injecting SessionId into merged spec --- CHANGELOG.md | 1 + docs/QueryApi.md | 2 +- docs/ResultsApi.md | 4 ++-- src/apis/query_api.rs | 8 -------- src/apis/results_api.rs | 16 ---------------- 5 files changed, 4 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 384071a..86f4c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- chore(api): stop injecting SessionId into merged spec - feat(databases): add search parameter to list endpoint ## [0.10.0] - 2026-07-23 diff --git a/docs/QueryApi.md b/docs/QueryApi.md index 7075a0c..ec7e9ab 100644 --- a/docs/QueryApi.md +++ b/docs/QueryApi.md @@ -29,7 +29,7 @@ Name | Type | Description | Required | Notes ### Authorization -[WorkspaceId](../README.md#WorkspaceId), [SessionId](../README.md#SessionId), [BearerAuth](../README.md#BearerAuth) +[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) ### HTTP request headers diff --git a/docs/ResultsApi.md b/docs/ResultsApi.md index aadf961..ec449a3 100644 --- a/docs/ResultsApi.md +++ b/docs/ResultsApi.md @@ -33,7 +33,7 @@ Name | Type | Description | Required | Notes ### Authorization -[WorkspaceId](../README.md#WorkspaceId), [SessionId](../README.md#SessionId), [BearerAuth](../README.md#BearerAuth) +[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) ### HTTP request headers @@ -65,7 +65,7 @@ Name | Type | Description | Required | Notes ### Authorization -[WorkspaceId](../README.md#WorkspaceId), [SessionId](../README.md#SessionId), [BearerAuth](../README.md#BearerAuth) +[WorkspaceId](../README.md#WorkspaceId), [BearerAuth](../README.md#BearerAuth) ### HTTP request headers diff --git a/src/apis/query_api.rs b/src/apis/query_api.rs index 6196aa5..03437e3 100644 --- a/src/apis/query_api.rs +++ b/src/apis/query_api.rs @@ -54,14 +54,6 @@ pub async fn query( }; req_builder = req_builder.header("X-Workspace-Id", value); }; - if let Some(apikey) = configuration.api_keys.get("X-Session-Id") { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{} {}", prefix, key), - None => key, - }; - req_builder = req_builder.header("X-Session-Id", value); - }; if let Some(token) = configuration.resolve_bearer_token().await { req_builder = req_builder.bearer_auth(token); }; diff --git a/src/apis/results_api.rs b/src/apis/results_api.rs index 1442c3d..db33452 100644 --- a/src/apis/results_api.rs +++ b/src/apis/results_api.rs @@ -76,14 +76,6 @@ pub async fn get_result( }; req_builder = req_builder.header("X-Workspace-Id", value); }; - if let Some(apikey) = configuration.api_keys.get("X-Session-Id") { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{} {}", prefix, key), - None => key, - }; - req_builder = req_builder.header("X-Session-Id", value); - }; if let Some(token) = configuration.resolve_bearer_token().await { req_builder = req_builder.bearer_auth(token); }; @@ -158,14 +150,6 @@ pub async fn list_results( }; req_builder = req_builder.header("X-Workspace-Id", value); }; - if let Some(apikey) = configuration.api_keys.get("X-Session-Id") { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{} {}", prefix, key), - None => key, - }; - req_builder = req_builder.header("X-Session-Id", value); - }; if let Some(token) = configuration.resolve_bearer_token().await { req_builder = req_builder.bearer_auth(token); }; From 12e362a02410274bd8c7e26cb6f6fc2c6b47933c Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Wed, 29 Jul 2026 18:52:36 -0700 Subject: [PATCH 2/3] refactor!: remove session scoping (X-Session-Id) --- .openapi-generator-templates/README.md | 5 +- CHANGELOG.md | 8 ++- src/arrow.rs | 71 ++++++-------------------- src/client.rs | 65 ++--------------------- src/query.rs | 22 ++++---- tests/presigned_uploads.rs | 27 +++------- 6 files changed, 47 insertions(+), 151 deletions(-) diff --git a/.openapi-generator-templates/README.md b/.openapi-generator-templates/README.md index 37a4521..80af41a 100644 --- a/.openapi-generator-templates/README.md +++ b/.openapi-generator-templates/README.md @@ -3,9 +3,8 @@ Fixes a limitation in openapi-generator's rust/reqwest target: the stock templates model `Configuration.api_key` as a single `Option` and emit the same value for every `apiKey` security scheme on an operation. -For the Hotdata API — which declares distinct `X-Workspace-Id`, -`X-Sandbox-Id`, and `X-Session-Id` schemes — that means a sandbox id -would be sent as the workspace id and vice versa. +For an API that declares more than one `apiKey` header scheme, that means +one scheme's value would be sent under another scheme's header name. These templates replace that field with `api_keys: HashMap` keyed by header name, and change the per-operation header-emission code diff --git a/CHANGELOG.md b/CHANGELOG.md index 86f4c4b..89757b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **Breaking:** session scoping is gone from the API. `ClientBuilder::session_id`, + the `client::SESSION_ID_HEADER` constant, and the `HOTDATA_SESSION_ID` + environment variable are removed, and no request sends the `X-Session-Id` + header any more. Drop the `.session_id(..)` builder call; nothing replaces it. + ### Changed -- chore(api): stop injecting SessionId into merged spec - feat(databases): add search parameter to list endpoint ## [0.10.0] - 2026-07-23 diff --git a/src/arrow.rs b/src/arrow.rs index a1b3dd6..47206ec 100644 --- a/src/arrow.rs +++ b/src/arrow.rs @@ -8,8 +8,7 @@ //! //! This module mirrors `hotdata/arrow.py` from the Python SDK. It builds the //! request exactly like the generated `get_result` (same URL, user-agent, -//! `X-Workspace-Id`/`X-Session-Id` API keys, and transparent JWT-exchanged -//! bearer token via +//! `X-Workspace-Id` API key, and transparent JWT-exchanged bearer token via //! [`crate::apis::configuration::Configuration::resolve_bearer_token`]), adds //! `Accept: application/vnd.apache.arrow.stream` plus `?format=arrow`, and //! decodes the resulting IPC stream with `arrow-ipc`. @@ -39,7 +38,7 @@ use arrow_schema::{ArrowError as IpcArrowError, SchemaRef}; use bytes::Bytes; use crate::apis::configuration::Configuration; -use crate::client::{SESSION_ID_HEADER, WORKSPACE_ID_HEADER}; +use crate::client::WORKSPACE_ID_HEADER; /// The Arrow IPC stream media type negotiated with the results endpoint. pub const ARROW_STREAM_MEDIA_TYPE: &str = "application/vnd.apache.arrow.stream"; @@ -235,8 +234,8 @@ impl Iterator for ArrowBatchStream { /// without materializing them all at once. /// /// The request is built to match the generated `get_result`: same URL, the -/// `X-Workspace-Id`/`X-Session-Id` API key headers, the user-agent, and a -/// transparently JWT-exchanged bearer token via +/// `X-Workspace-Id` API key header, the user-agent, and a transparently +/// JWT-exchanged bearer token via /// [`Configuration::resolve_bearer_token`](crate::apis::configuration::Configuration::resolve_bearer_token). /// `Accept` and `?format=arrow` are added on top. /// @@ -298,23 +297,19 @@ pub async fn stream_result_arrow( }) } -/// Apply the `X-Workspace-Id` and `X-Session-Id` API-key headers, mirroring the -/// generated `get_result` `isKeyInHeader` blocks so a session-scoped client -/// (one built via [`crate::client::ClientBuilder::session_id`]) behaves -/// identically on the Arrow path. +/// Apply the `X-Workspace-Id` API-key header, mirroring the generated +/// `get_result` `isKeyInHeader` block so the Arrow path is scoped identically. fn apply_apikey_headers( mut req_builder: reqwest::RequestBuilder, configuration: &Configuration, ) -> reqwest::RequestBuilder { - for header in [WORKSPACE_ID_HEADER, SESSION_ID_HEADER] { - if let Some(apikey) = configuration.api_keys.get(header) { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{prefix} {key}"), - None => key, - }; - req_builder = req_builder.header(header, value); - } + if let Some(apikey) = configuration.api_keys.get(WORKSPACE_ID_HEADER) { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{prefix} {key}"), + None => key, + }; + req_builder = req_builder.header(WORKSPACE_ID_HEADER, value); } req_builder } @@ -492,43 +487,10 @@ mod tests { use crate::apis::configuration::ApiKey; use std::sync::Arc; - /// A session-scoped client must send *both* `X-Workspace-Id` and - /// `X-Session-Id` on the Arrow path, matching the generated `get_result`. - /// Guards against silently dropping the session header (the prior behavior). - #[test] - fn apikey_headers_forward_workspace_and_session() { - let mut configuration = Configuration::new(); - configuration.api_keys.insert( - WORKSPACE_ID_HEADER.to_owned(), - ApiKey { - prefix: None, - key: "ws-123".to_owned(), - }, - ); - configuration.api_keys.insert( - SESSION_ID_HEADER.to_owned(), - ApiKey { - prefix: None, - key: "sess-456".to_owned(), - }, - ); - - let req_builder = configuration - .client - .request(reqwest::Method::GET, "https://api.hotdata.dev/v1/results/abc"); - let req = apply_apikey_headers(req_builder, &configuration) - .build() - .unwrap(); - let headers = req.headers(); - - assert_eq!(headers.get(WORKSPACE_ID_HEADER).unwrap(), "ws-123"); - assert_eq!(headers.get(SESSION_ID_HEADER).unwrap(), "sess-456"); - } - - /// Without a session id installed, only `X-Workspace-Id` is sent — the - /// session header is omitted rather than sent empty. + /// The Arrow path forwards `X-Workspace-Id`, matching the generated + /// `get_result`. #[test] - fn apikey_headers_omit_absent_session() { + fn apikey_headers_forward_workspace() { let mut configuration = Configuration::new(); configuration.api_keys.insert( WORKSPACE_ID_HEADER.to_owned(), @@ -547,7 +509,6 @@ mod tests { let headers = req.headers(); assert_eq!(headers.get(WORKSPACE_ID_HEADER).unwrap(), "ws-123"); - assert!(headers.get(SESSION_ID_HEADER).is_none()); } use arrow_array::{Int64Array, StringArray}; diff --git a/src/client.rs b/src/client.rs index b8a153c..b98d126 100644 --- a/src/client.rs +++ b/src/client.rs @@ -42,9 +42,6 @@ pub const DEFAULT_BASE_URL: &str = "https://api.hotdata.dev"; /// `Configuration::api_keys` so the generated apiKey-auth blocks emit it. pub const WORKSPACE_ID_HEADER: &str = "X-Workspace-Id"; -/// Header name used to scope requests to a session (optional). -pub const SESSION_ID_HEADER: &str = "X-Session-Id"; - /// Environment variable holding the API token used for transparent JWT exchange. /// Mirrors the Python SDK's `HOTDATA_API_KEY`. pub const ENV_API_KEY: &str = "HOTDATA_API_KEY"; @@ -99,7 +96,6 @@ impl std::error::Error for ClientError {} pub struct ClientBuilder { api_token: Option, workspace_id: Option, - session_id: Option, base_url: Option, user_agent: Option, client_id: Option, @@ -123,13 +119,6 @@ impl ClientBuilder { self } - /// Set an optional session id. Installed as the `X-Session-Id` header when - /// present. - pub fn session_id(mut self, session_id: impl Into) -> Self { - self.session_id = Some(session_id.into()); - self - } - /// Override the base URL. Defaults to [`DEFAULT_BASE_URL`], or the /// `HOTDATA_SDK_TEST_API_URL` / `HOTDATA_API_URL` environment variables when /// set (in that order of precedence). @@ -217,8 +206,8 @@ impl ClientBuilder { ..Configuration::default() }; - // Scope every request to the workspace (and optionally the session) via - // the generated apiKey-header auth blocks. + // Scope every request to the workspace via the generated apiKey-header + // auth blocks. configuration.api_keys.insert( WORKSPACE_ID_HEADER.to_owned(), ApiKey { @@ -226,19 +215,6 @@ impl ClientBuilder { key: workspace_id, }, ); - if let Some(session_id) = self - .session_id - .clone() - .or_else(|| non_empty_env("HOTDATA_SESSION_ID")) - { - configuration.api_keys.insert( - SESSION_ID_HEADER.to_owned(), - ApiKey { - prefix: None, - key: session_id, - }, - ); - } // Install the transparent api_token -> JWT exchange. The TokenManager // reuses the same reqwest client (so TLS/proxy/timeout settings are @@ -434,9 +410,9 @@ impl Client { /// `database_id` selects the database via the `X-Database-Id` header (the /// spec lets database scope come from that header OR the `database_id` body /// field; pass `None` to use the body field or rely on the default). The - /// request is built wire-identically to the generated op (same workspace / - /// session scope headers, same bearer auth, same `base_path`/`/v1` join, - /// same JSON body) plus the `202` handling. + /// request is built wire-identically to the generated op (same workspace + /// scope header, same bearer auth, same `base_path`/`/v1` join, same JSON + /// body) plus the `202` handling. pub async fn submit_query( &self, request: models::QueryRequest, @@ -465,14 +441,6 @@ impl Client { }; req_builder = req_builder.header("X-Workspace-Id", value); }; - if let Some(apikey) = configuration.api_keys.get("X-Session-Id") { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{} {}", prefix, key), - None => key, - }; - req_builder = req_builder.header("X-Session-Id", value); - }; if let Some(token) = configuration.resolve_bearer_token().await { req_builder = req_builder.bearer_auth(token); }; @@ -1012,7 +980,6 @@ mod tests { ENV_WORKSPACE_ID, ENV_API_URL, ENV_TEST_API_URL, - "HOTDATA_SESSION_ID", "HOTDATA_DISABLE_JWT_EXCHANGE", ] { env::remove_var(key); @@ -1487,28 +1454,6 @@ mod tests { clear_env(); } - #[test] - fn session_id_installed_when_set() { - let _g = env_guard(); - clear_env(); - - let client = Client::builder() - .api_token("hd_x") - .workspace_id("ws_x") - .session_id("sess_123") - .build() - .expect("build ok"); - - assert_eq!( - client - .configuration() - .api_keys - .get(SESSION_ID_HEADER) - .map(|k| k.key.as_str()), - Some("sess_123") - ); - } - #[test] fn default_user_agent_uses_crate_version() { let _g = env_guard(); diff --git a/src/query.rs b/src/query.rs index a5a7880..54367f2 100644 --- a/src/query.rs +++ b/src/query.rs @@ -61,7 +61,7 @@ use crate::apis::configuration::Configuration; use crate::apis::query_api::QueryError as GeneratedQueryError; use crate::apis::results_api::GetResultError; use crate::apis::{query_runs_api, results_api, Error, ResponseContent}; -use crate::client::{SESSION_ID_HEADER, WORKSPACE_ID_HEADER}; +use crate::client::WORKSPACE_ID_HEADER; use crate::http::{backoff_delay, is_pre_response_transport_error, parse_retry_after}; use crate::models::{AsyncQueryResponse, QueryRequest, QueryResponse, ResultsFormatQuery}; use crate::status::ResultStatus; @@ -636,21 +636,19 @@ async fn send_query( }) } -/// Apply the `X-Workspace-Id` / `X-Session-Id` API-key headers, mirroring the -/// generated op's `isKeyInHeader` blocks. +/// Apply the `X-Workspace-Id` API-key header, mirroring the generated op's +/// `isKeyInHeader` block. fn apply_apikey_headers( mut req_builder: reqwest::RequestBuilder, config: &Configuration, ) -> reqwest::RequestBuilder { - for header in [WORKSPACE_ID_HEADER, SESSION_ID_HEADER] { - if let Some(apikey) = config.api_keys.get(header) { - let key = apikey.key.clone(); - let value = match apikey.prefix { - Some(ref prefix) => format!("{prefix} {key}"), - None => key, - }; - req_builder = req_builder.header(header, value); - } + if let Some(apikey) = config.api_keys.get(WORKSPACE_ID_HEADER) { + let key = apikey.key.clone(); + let value = match apikey.prefix { + Some(ref prefix) => format!("{prefix} {key}"), + None => key, + }; + req_builder = req_builder.header(WORKSPACE_ID_HEADER, value); } req_builder } diff --git a/tests/presigned_uploads.rs b/tests/presigned_uploads.rs index 4f1cdda..b2c4b37 100644 --- a/tests/presigned_uploads.rs +++ b/tests/presigned_uploads.rs @@ -14,8 +14,8 @@ //! * multipart happy path (slicing by `part_size`, per-part ETag collection, //! ascending finalize parts); //! * progress callback monotonicity reaching exactly the file size; -//! * storage-PUT header isolation (no SDK bearer/workspace/session headers, and -//! no default headers leaking off the SDK's main client); +//! * storage-PUT header isolation (no SDK bearer/workspace headers, and no +//! default headers leaking off the SDK's main client); //! * finalize exactly-once (no retry) and per-part retry; //! * error surfacing (missing ETag, storage 4xx/5xx, finalize failure, //! 501 PRESIGN_UNSUPPORTED, malformed sessions); @@ -31,11 +31,10 @@ use wiremock::matchers::{method, path, path_regex}; use wiremock::{Mock, MockServer, Request, ResponseTemplate}; const WORKSPACE_HEADER: &str = "X-Workspace-Id"; -const SESSION_HEADER: &str = "X-Session-Id"; /// Build a client pointed at the mock server with a static bearer token and the -/// workspace + session scope headers installed (no JWT-exchange round-trip), so -/// the upload requests carry exactly the headers a real client would. +/// workspace scope header installed (no JWT-exchange round-trip), so the upload +/// requests carry exactly the headers a real client would. fn test_client(base_url: &str) -> Client { let mut configuration = Configuration { base_path: base_url.to_owned(), @@ -50,13 +49,6 @@ fn test_client(base_url: &str) -> Client { key: "ws_test".to_owned(), }, ); - configuration.api_keys.insert( - SESSION_HEADER.to_owned(), - ApiKey { - prefix: None, - key: "sess_test".to_owned(), - }, - ); Client::from_configuration(configuration) } @@ -92,12 +84,7 @@ fn temp_file(contents: &[u8]) -> std::path::PathBuf { /// A presigned URL self-authorizes; an extra signed-ish header makes S3-style /// storage return 403. fn assert_no_sdk_headers(req: &Request) { - for forbidden in [ - "authorization", - "x-workspace-id", - "x-session-id", - "x-upload-finalize-token", - ] { + for forbidden in ["authorization", "x-workspace-id", "x-upload-finalize-token"] { assert!( req.headers.get(forbidden).is_none(), "storage PUT must not carry the `{forbidden}` header, found one" @@ -492,8 +479,8 @@ async fn progress_callback_reaches_total() { #[tokio::test] async fn storage_put_header_isolation_negative_check() { // A focused negative check on the single-PUT path: the storage PUT must not - // carry the SDK bearer or workspace/session scope headers even though the - // client is fully configured with all of them. + // carry the SDK bearer or workspace scope headers even though the client is + // fully configured with all of them. let server = MockServer::start().await; let storage_url = format!("{}/storage/iso", server.uri()); let contents = b"isolation bytes"; From b6fbf4448a5687667844b1982a28d6dc564c6ea3 Mon Sep 17 00:00:00 2001 From: Zac Farrell Date: Wed, 29 Jul 2026 19:04:38 -0700 Subject: [PATCH 3/3] docs: drop stale session scope header mentions --- src/query.rs | 4 ++-- src/uploads.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/query.rs b/src/query.rs index 54367f2..a30882a 100644 --- a/src/query.rs +++ b/src/query.rs @@ -470,8 +470,8 @@ struct RawResponse { /// truncation per `qc`. /// /// Mirrors `hotdata.query.QueryApi.query` from the Python SDK. The request is -/// built wire-identically to the generated `query` op (same workspace/session -/// scope headers, bearer auth, `base_path`/`/v1` join, JSON body); building it +/// built wire-identically to the generated `query` op (same workspace scope +/// header, bearer auth, `base_path`/`/v1` join, JSON body); building it /// here rather than calling the generated op lets the retry path read the /// `Retry-After` header (the generated op discards response headers). pub(crate) async fn execute_query( diff --git a/src/uploads.rs b/src/uploads.rs index 717a0a8..f9cc85e 100644 --- a/src/uploads.rs +++ b/src/uploads.rs @@ -30,7 +30,7 @@ //! (or in the server-provided `headers` map). Object stores (S3 and compatible) //! reject a `PUT` with `403 SignatureDoesNotMatch` if it carries extra //! signed-ish headers, so [`put_to_storage`] sends a *bare* request: NONE of the -//! SDK's bearer / workspace / session headers, only an explicit `Content-Length` +//! SDK's bearer / workspace headers, only an explicit `Content-Length` //! and whatever the server placed in `headers` (currently always empty). It also //! refuses to let reqwest auto-append a charset to a `Content-Type` — a type is //! sent only when the server's `headers` map asks for one. @@ -1050,7 +1050,7 @@ fn part_put_timeout(content_length: u64) -> Duration { /// `PUT` a body to a presigned storage URL with strict header isolation. /// -/// Attaches NONE of the SDK's auth/workspace/session/user-agent headers — a +/// Attaches NONE of the SDK's auth/workspace/user-agent headers — a /// presigned URL already carries its authorization, and an extra signed-ish /// header makes S3-compatible storage return `403`. Only an explicit /// `Content-Length` and the server-provided `headers` map (replayed verbatim;