- 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.
Showing the changes to packages/plugins/openapi/src/plugin.test.ts (1 of 11 files). Show all
Unified · Split
| @@ -6,11 +6,12 @@ import { · 5 unmodified lines | ||
| 6 | 6 | HttpApiEndpoint, |
| 7 | 7 | HttpApiGroup, |
| 8 | 8 | HttpClient, |
| 9 | + HttpServerRequest, | |
| 9 | 10 | OpenApi, |
| 10 | 11 | } from "@effect/platform"; |
| 11 | 12 | import { NodeHttpServer } from "@effect/platform-node"; |
| 12 | 13 | |
| 13 | −import { createExecutor, makeTestConfig } from "@executor/sdk"; | |
| 14 | +import { createExecutor, makeTestConfig, SecretId } from "@executor/sdk"; | |
| 14 | 15 | import { openApiPlugin } from "./plugin"; |
| 15 | 16 | |
| 16 | 17 | // --------------------------------------------------------------------------- |
| @@ -22,6 +23,11 @@ class Item extends Schema.Class<Item>("Item")({ · 5 unmodified lines | ||
| 22 | 23 | name: Schema.String, |
| 23 | 24 | }) {} |
| 24 | 25 | |
| 26 | +class EchoHeaders extends Schema.Class<EchoHeaders>("EchoHeaders")({ | |
| 27 | + authorization: Schema.optional(Schema.String), | |
| 28 | + "x-static": Schema.optional(Schema.String), | |
| 29 | +}) {} | |
| 30 | + | |
| 25 | 31 | const ItemsGroup = HttpApiGroup.make("items") |
| 26 | 32 | .add( |
| 27 | 33 | HttpApiEndpoint.get("listItems", "/items").addSuccess(Schema.Array(Item)), |
| @@ -30,6 +36,9 @@ const ItemsGroup = HttpApiGroup.make("items") · 2 unmodified lines | ||
| 30 | 36 | HttpApiEndpoint.get("getItem", "/items/:itemId") |
| 31 | 37 | .setPath(Schema.Struct({ itemId: Schema.NumberFromString })) |
| 32 | 38 | .addSuccess(Item), |
| 39 | + ) | |
| 40 | + .add( | |
| 41 | + HttpApiEndpoint.get("echoHeaders", "/echo-headers").addSuccess(EchoHeaders), | |
| 33 | 42 | ); |
| 34 | 43 | |
| 35 | 44 | const TestApi = HttpApi.make("testApi").add(ItemsGroup); |
| @@ -57,6 +66,15 @@ const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => · 21 unmodified lines | ||
| 57 | 66 | name: "Unknown", |
| 58 | 67 | }, |
| 59 | 68 | ), |
| 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 | + }), | |
| 60 | 78 | ), |
| 61 | 79 | ); |
| 62 | 80 | |
| @@ -98,6 +116,83 @@ layer(TestLayer)("OpenAPI Plugin", (it) => { · 35 unmodified lines | ||
| 98 | 116 | }), |
| 99 | 117 | ); |
| 100 | 118 | |
| 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 | + | |
| 101 | 196 | it.effect("registers tools from an OpenAPI spec", () => |
| 102 | 197 | Effect.gen(function* () { |
| 103 | 198 | const httpClient = yield* HttpClient.HttpClient; |