History

feat: enhance secret management in SDK

- Introduced `SecretRef` class to represent stored secrets without their values. - Updated `SecretStore` to manage `SecretRef` instances, allowing for better handling of secret resolution and status checks. - Added `resolve` and `status` methods to `SecretStore` for retrieving secret values and checking their availability. - Refactored in-memory secret store to support the new structure and added a default in-memory secret provider. - Updated OpenAPI plugin to resolve secret-backed headers during invocation, improving security handling in API requests. - Enhanced tests to cover new secret management features and ensure proper resolution of secrets in headers. All changes are aimed at improving the flexibility and security of secret handling within the SDK.

Rhys Sullivan committed 7 months ago

Commit 8d3cddf1 parent 673821a11 files · +441 −87Browse files

Showing the changes to packages/plugins/openapi/src/plugin.test.ts (1 of 11 files). Show all

Unified · Split

packages/plugins/openapi/src/plugin.test.ts+96−1
@@ -6,11 +6,12 @@ import { · 5 unmodified lines
66 HttpApiEndpoint,
77 HttpApiGroup,
88 HttpClient,
9+ HttpServerRequest,
910 OpenApi,
1011 } from "@effect/platform";
1112 import { NodeHttpServer } from "@effect/platform-node";
1213
13−import { createExecutor, makeTestConfig } from "@executor/sdk";
14+import { createExecutor, makeTestConfig, SecretId } from "@executor/sdk";
1415 import { openApiPlugin } from "./plugin";
1516
1617 // ---------------------------------------------------------------------------
@@ -22,6 +23,11 @@ class Item extends Schema.Class<Item>("Item")({ · 5 unmodified lines
2223 name: Schema.String,
2324 }) {}
2425
26+class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({
27+ authorization: Schema.optional(Schema.String),
28+ "x-static": Schema.optional(Schema.String),
29+}) {}
30+
2531 const ItemsGroup = HttpApiGroup.make("items")
2632 .add(
2733 HttpApiEndpoint.get("listItems", "/items").addSuccess(Schema.Array(Item)),
@@ -30,6 +36,9 @@ const ItemsGroup = HttpApiGroup.make("items") · 2 unmodified lines
3036 HttpApiEndpoint.get("getItem", "/items/:itemId")
3137 .setPath(Schema.Struct({ itemId: Schema.NumberFromString }))
3238 .addSuccess(Item),
39+ )
40+ .add(
41+ HttpApiEndpoint.get("echoHeaders", "/echo-headers").addSuccess(EchoHeaders),
3342 );
3443
3544 const TestApi = HttpApi.make("testApi").add(ItemsGroup);
@@ -57,6 +66,15 @@ const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => · 21 unmodified lines
5766 name: "Unknown",
5867 },
5968 ),
69+ )
70+ .handle("echoHeaders", () =>
71+ Effect.gen(function* () {
72+ const req = yield* HttpServerRequest.HttpServerRequest;
73+ return new EchoHeaders({
74+ authorization: req.headers["authorization"],
75+ "x-static": req.headers["x-static"],
76+ });
77+ }),
6078 ),
6179 );
6280
@@ -98,6 +116,83 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 35 unmodified lines
98116 }),
99117 );
100118
119+ it.effect("resolves secret-backed headers at invocation time", () =>
120+ Effect.gen(function* () {
121+ const httpClient = yield* HttpClient.HttpClient;
122+ const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);
123+
124+ const executor = yield* createExecutor(
125+ makeTestConfig({
126+ plugins: [
127+ openApiPlugin({ httpClientLayer: clientLayer }),
128+ ] as const,
129+ }),
130+ );
131+
132+ // Store a secret
133+ yield* executor.secrets.set({
134+ id: SecretId.make("test-api-token"),
135+ name: "Test API Token",
136+ value: "secret-value-123",
137+ });
138+
139+ // Add spec with secret-backed header
140+ yield* executor.openapi.addSpec({
141+ spec: specJson,
142+ namespace: "authed",
143+ baseUrl: "",
144+ headers: {
145+ "Authorization": { secretId: "test-api-token", prefix: "Bearer " },
146+ "X-Static": "hello",
147+ },
148+ });
149+
150+ // Invoke the echo endpoint — verifies secret was resolved and sent
151+ const result = yield* executor.tools.invoke(
152+ "authed.items.echoHeaders",
153+ {},
154+ );
155+
156+ expect(result.error).toBeNull();
157+ const data = result.data as { authorization?: string; "x-static"?: string };
158+ expect(data.authorization).toBe("Bearer secret-value-123");
159+ expect(data["x-static"]).toBe("hello");
160+ }),
161+ );
162+
163+ it.effect("fails clearly when a secret is missing", () =>
164+ Effect.gen(function* () {
165+ const httpClient = yield* HttpClient.HttpClient;
166+ const clientLayer = Layer.succeed(HttpClient.HttpClient, httpClient);
167+
168+ const executor = yield* createExecutor(
169+ makeTestConfig({
170+ plugins: [
171+ openApiPlugin({ httpClientLayer: clientLayer }),
172+ ] as const,
173+ }),
174+ );
175+
176+ // Add spec with secret-backed header but DON'T store the secret
177+ yield* executor.openapi.addSpec({
178+ spec: specJson,
179+ namespace: "noauth",
180+ baseUrl: "",
181+ headers: {
182+ "Authorization": { secretId: "missing-token", prefix: "Bearer " },
183+ },
184+ });
185+
186+ // Invoke — should fail with a clear error about the missing secret
187+ const error = yield* Effect.flip(
188+ executor.tools.invoke("noauth.items.listItems", {}),
189+ );
190+
191+ expect(error._tag).toBe("ToolInvocationError");
192+ expect((error as { message: string }).message).toContain("missing-token");
193+ }),
194+ );
195+
101196 it.effect("registers tools from an OpenAPI spec", () =>
102197 Effect.gen(function* () {
103198 const httpClient = yield* HttpClient.HttpClient;

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t