Skip to content

Commit 5eead34

Browse files
committed
feat(rust,python,typescript): add session secrets API (batch set)
Add native client.sessions().set_secrets() method across all three SDKs so consumers don't need raw HTTP calls for secret management. Endpoint: PUT /v1/sessions/:id/storage/secrets — batch set encrypted secrets Closes #67
1 parent 9471834 commit 5eead34

7 files changed

Lines changed: 197 additions & 0 deletions

File tree

python/everruns_sdk/client.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,18 @@ async def unpin(self, session_id: str) -> None:
411411
"""Unpin a session for the current user."""
412412
await self._client._delete(f"/sessions/{session_id}/pin")
413413

414+
async def set_secrets(self, session_id: str, secrets: dict[str, str]) -> None:
415+
"""Batch-set encrypted secrets for a session.
416+
417+
Args:
418+
session_id: Session ID.
419+
secrets: Map of secret name to secret value.
420+
"""
421+
await self._client._put(
422+
f"/sessions/{session_id}/storage/secrets",
423+
{"secrets": secrets},
424+
)
425+
414426

415427
class MessagesClient:
416428
"""Client for message operations."""

python/tests/test_client.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,3 +1020,46 @@ async def test_connections_remove():
10201020
await client.close()
10211021

10221022
assert route.called
1023+
1024+
1025+
# --- Session Secrets Tests ---
1026+
1027+
1028+
@pytest.mark.asyncio
1029+
@respx.mock
1030+
async def test_session_set_secrets():
1031+
route = respx.put("https://custom.example.com/api/v1/sessions/sess_123/storage/secrets").mock(
1032+
return_value=httpx.Response(200, json={})
1033+
)
1034+
1035+
client = Everruns(api_key="evr_test_key")
1036+
try:
1037+
await client.sessions.set_secrets(
1038+
"sess_123",
1039+
{"OPENAI_API_KEY": "sk-abc123", "DB_PASSWORD": "hunter2"},
1040+
)
1041+
finally:
1042+
await client.close()
1043+
1044+
assert route.called
1045+
body = json.loads(route.calls[0].request.content)
1046+
assert body["secrets"]["OPENAI_API_KEY"] == "sk-abc123"
1047+
assert body["secrets"]["DB_PASSWORD"] == "hunter2"
1048+
1049+
1050+
@pytest.mark.asyncio
1051+
@respx.mock
1052+
async def test_session_set_secrets_empty():
1053+
route = respx.put("https://custom.example.com/api/v1/sessions/sess_123/storage/secrets").mock(
1054+
return_value=httpx.Response(200, json={})
1055+
)
1056+
1057+
client = Everruns(api_key="evr_test_key")
1058+
try:
1059+
await client.sessions.set_secrets("sess_123", {})
1060+
finally:
1061+
await client.close()
1062+
1063+
assert route.called
1064+
body = json.loads(route.calls[0].request.content)
1065+
assert body["secrets"] == {}

rust/src/client.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,19 @@ impl<'a> SessionsClient<'a> {
436436
pub async fn unpin(&self, id: &str) -> Result<()> {
437437
self.client.delete(&format!("/sessions/{}/pin", id)).await
438438
}
439+
440+
/// Batch-set encrypted secrets for a session
441+
pub async fn set_secrets(
442+
&self,
443+
id: &str,
444+
secrets: &std::collections::HashMap<String, String>,
445+
) -> Result<()> {
446+
let req = SetSecretsRequest::new(secrets.clone());
447+
self.client
448+
.put::<serde_json::Value, _>(&format!("/sessions/{}/storage/secrets", id), &req)
449+
.await?;
450+
Ok(())
451+
}
439452
}
440453

441454
/// Client for message operations

rust/src/models.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,20 @@ impl SetConnectionRequest {
953953
}
954954
}
955955

956+
// --- Session Secrets Models ---
957+
958+
/// Request to batch-set session secrets
959+
#[derive(Debug, Clone, Serialize)]
960+
pub struct SetSecretsRequest {
961+
pub secrets: std::collections::HashMap<String, String>,
962+
}
963+
964+
impl SetSecretsRequest {
965+
pub fn new(secrets: std::collections::HashMap<String, String>) -> Self {
966+
Self { secrets }
967+
}
968+
}
969+
956970
#[cfg(test)]
957971
mod tests {
958972
use super::*;

rust/tests/client_test.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,55 @@ async fn test_connections_remove() {
624624
.await
625625
.expect("remove connection should succeed");
626626
}
627+
628+
// --- Session Secrets Tests ---
629+
630+
#[tokio::test]
631+
async fn test_session_set_secrets() {
632+
let server = MockServer::start().await;
633+
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");
634+
635+
Mock::given(method("PUT"))
636+
.and(path("/v1/sessions/sess_123/storage/secrets"))
637+
.and(body_json(serde_json::json!({
638+
"secrets": {
639+
"OPENAI_API_KEY": "sk-abc123",
640+
"DB_PASSWORD": "hunter2"
641+
}
642+
})))
643+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
644+
.mount(&server)
645+
.await;
646+
647+
let mut secrets = std::collections::HashMap::new();
648+
secrets.insert("OPENAI_API_KEY".to_string(), "sk-abc123".to_string());
649+
secrets.insert("DB_PASSWORD".to_string(), "hunter2".to_string());
650+
651+
client
652+
.sessions()
653+
.set_secrets("sess_123", &secrets)
654+
.await
655+
.expect("set_secrets should succeed");
656+
}
657+
658+
#[tokio::test]
659+
async fn test_session_set_secrets_empty() {
660+
let server = MockServer::start().await;
661+
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");
662+
663+
Mock::given(method("PUT"))
664+
.and(path("/v1/sessions/sess_123/storage/secrets"))
665+
.and(body_json(serde_json::json!({
666+
"secrets": {}
667+
})))
668+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
669+
.mount(&server)
670+
.await;
671+
672+
let secrets = std::collections::HashMap::new();
673+
client
674+
.sessions()
675+
.set_secrets("sess_123", &secrets)
676+
.await
677+
.expect("set_secrets with empty map should succeed");
678+
}

typescript/src/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,17 @@ class SessionsClient {
319319
method: "DELETE",
320320
});
321321
}
322+
323+
/** Batch-set encrypted secrets for a session. */
324+
async setSecrets(
325+
sessionId: string,
326+
secrets: Record<string, string>,
327+
): Promise<void> {
328+
await this.client.fetch(`/sessions/${sessionId}/storage/secrets`, {
329+
method: "PUT",
330+
body: JSON.stringify({ secrets }),
331+
});
332+
}
322333
}
323334

324335
class MessagesClient {

typescript/tests/client.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,3 +1016,55 @@ describe("ConnectionsClient", () => {
10161016
);
10171017
});
10181018
});
1019+
1020+
// --- Session Secrets Tests ---
1021+
1022+
describe("SessionsClient.setSecrets", () => {
1023+
it("should batch-set secrets", async () => {
1024+
const fetchMock = vi.fn().mockResolvedValue({
1025+
ok: true,
1026+
status: 200,
1027+
json: async () => ({}),
1028+
});
1029+
vi.stubGlobal("fetch", fetchMock);
1030+
1031+
const client = new Everruns({ apiKey: "evr_test_key" });
1032+
await client.sessions.setSecrets("sess_123", {
1033+
OPENAI_API_KEY: "sk-abc123",
1034+
DB_PASSWORD: "hunter2",
1035+
});
1036+
1037+
expect(fetchMock).toHaveBeenCalledWith(
1038+
"https://custom.example.com/api/v1/sessions/sess_123/storage/secrets",
1039+
expect.objectContaining({
1040+
method: "PUT",
1041+
body: JSON.stringify({
1042+
secrets: {
1043+
OPENAI_API_KEY: "sk-abc123",
1044+
DB_PASSWORD: "hunter2",
1045+
},
1046+
}),
1047+
}),
1048+
);
1049+
});
1050+
1051+
it("should handle empty secrets map", async () => {
1052+
const fetchMock = vi.fn().mockResolvedValue({
1053+
ok: true,
1054+
status: 200,
1055+
json: async () => ({}),
1056+
});
1057+
vi.stubGlobal("fetch", fetchMock);
1058+
1059+
const client = new Everruns({ apiKey: "evr_test_key" });
1060+
await client.sessions.setSecrets("sess_123", {});
1061+
1062+
expect(fetchMock).toHaveBeenCalledWith(
1063+
"https://custom.example.com/api/v1/sessions/sess_123/storage/secrets",
1064+
expect.objectContaining({
1065+
method: "PUT",
1066+
body: JSON.stringify({ secrets: {} }),
1067+
}),
1068+
);
1069+
});
1070+
});

0 commit comments

Comments
 (0)