* feat(tool-result): add ToolResult union and dual-shape invoker Adds `ToolResult<T>` / `ToolError` to @executor-js/sdk and a `ToolResult.ok` / `ToolResult.fail` constructor pair. Updates the sandbox tool dispatcher to accept both the new typed union (passes through unchanged) and the legacy `{ data, error }` envelope. The legacy branch now walks known upstream shapes (Microsoft Graph, DealCloud, JSON:API, plain message-bearing bodies) before falling back to a clamped JSON.stringify of the body, so structured 4xx payloads no longer collapse to 'Tool execution failed'. The cause/defect branch keeps its strict .message-only discipline. * feat(plugins): emit ToolResult from openapi, graphql, mcp, google-discovery Migrates each dynamic plugin's invokeTool to wrap its result in the typed ToolResult<T> union. OpenAPI and Google Discovery map non-2xx responses to ToolResult.fail with an extracted upstream message and the raw body in error.details; 2xx responses to ToolResult.ok with { status, headers, data }. GraphQL maps 200-with-errors bodies to ToolResult.fail({ code: 'graphql_errors', ... }) and bare data to ToolResult.ok. MCP maps { isError: true } to ToolResult.fail and content arrays to ToolResult.ok. Static plugin tools (executor.openapi.previewSpec / addSource, executor.graphql.addSource) now also wrap their plain values in ToolResult.ok for shape consistency. Updates plugin tests that asserted on the old { status, headers, data, error } envelope or the bare MCP { content } shape. Tests that consume the OpenAPI plugin's invocation result use the new unwrapInvocation helper from @executor-js/plugin-openapi/testing. * feat(tool-result): make ToolResult mandatory and route defects opaquely Removes the legacy { data, error } envelope shim from the sandbox tool dispatcher. The invoker now passes ToolResult<T> through unchanged and wraps any other plain-value plugin return in ToolResult.ok so the sandbox surface is uniform. Plugin/infra defects no longer pass their message into the sandbox. The dispatcher generates a short hex correlation id, logs the full cause with that id under executor.correlation_id, and rejects with `Internal tool error [<corrId>]`. The QuickJS bridge and the dynamic-worker module template defensively re-stamp the same opaque shape; the MCP host server's top-level execute failure path does the same. ExecutionToolError in-band messages from the execution package's built-in validators (tools.search arg checks, etc.) are still passed through at the QuickJS bridge so model-facing input errors keep their useful diagnostic. Tests: - ToolResult.ok / fail / isToolResult constructor unit tests. - repro tests assert structured upstream payloads now reach the sandbox through ToolResult.error.details (not through .message). - leak tests pin the new invariant: plugin defects only escape as the opaque generic + correlation id; no token / connection string / file path leaks into Error.message. - QuickJS end-to-end defect test confirms the same shape at the sandbox boundary. - Cloud HTTP integration tests, MCP host tests, dynamic-worker invocation tests, and plugin tests updated for the new ToolResult wire shape and the opaque-generic defect contract. * Fix tool result contracts
Showing the changes to packages/plugins/openapi/src (8 of 35 files). Show all
Unified · Split
| @@ -26,6 +26,7 @@ import { makeTestConfig, serveOAuthTestServer } from "@executor-js/sdk/testing"; · 25 unmodified lines | |||
| 26 | import { | 26 | import { |
| 27 | addOpenApiTestSource, | 27 | addOpenApiTestSource, |
| 28 | serveOpenApiHttpApiTestServer, | 28 | serveOpenApiHttpApiTestServer, |
| 29 | unwrapInvocation, | ||
| 29 | } from "@executor-js/plugin-openapi/testing"; | 30 | } from "@executor-js/plugin-openapi/testing"; |
| 30 | 31 | ||
| 31 | import { openApiPlugin } from "./plugin"; | 32 | import { openApiPlugin } from "./plugin"; |
| @@ -226,25 +227,21 @@ describe("OpenAPI client_credentials OAuth", () => { · 194 unmodified lines | |||
| 226 | ); | 227 | ); |
| 227 | // Invoking the tool injects the freshly-minted bearer via | 228 | // Invoking the tool injects the freshly-minted bearer via |
| 228 | // ctx.connections.accessToken. | 229 | // ctx.connections.accessToken. |
| 229 | const result = (yield* userExec.tools.invoke( | 230 | const result = unwrapInvocation( |
| 230 | "petstore.items.echoHeaders", | 231 | yield* userExec.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 231 | {}, | 232 | ); |
| 232 | autoApprove, | ||
| 233 | )) as { | ||
| 234 | data: { authorization?: string } | null; | ||
| 235 | error: unknown; | ||
| 236 | }; | ||
| 237 | expect(result.error).toBeNull(); | 233 | expect(result.error).toBeNull(); |
| 238 | const bearer = result.data?.authorization?.replace(/^Bearer\s+/i, ""); | 234 | const data = result.data as EchoHeaders | null; |
| 235 | const bearer = data?.authorization?.replace(/^Bearer\s+/i, ""); | ||
| 239 | expect(bearer).toBeDefined(); | 236 | expect(bearer).toBeDefined(); |
| 240 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); | 237 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); |
| 241 | 238 | ||
| 242 | // The connection lives at the innermost (user) scope, which | 239 | // The connection lives at the innermost (user) scope, which |
| 243 | // preserves per-user credential resolution: if each user has | 240 | // preserves per-user credential resolution: if each user has |
| 244 | // their own `dealcloud_client_id`/`dealcloud_client_secret` | 241 | // their own OAuth client credentials shadowed at their user |
| 245 | // shadowed at their user scope, each user mints their own | 242 | // scope, each user mints their own token. A single shared |
| 246 | // token. A single shared connection slot still lets every caller | 243 | // connection slot still lets every caller reach the right |
| 247 | // reach the right physical row through scoped credential bindings. | 244 | // physical row through scoped credential bindings. |
| 248 | const userConnections = yield* userExec.connections.list(); | 245 | const userConnections = yield* userExec.connections.list(); |
| 249 | const connection = userConnections.find((c) => c.id === completedConnection.connectionId); | 246 | const connection = userConnections.find((c) => c.id === completedConnection.connectionId); |
| 250 | expect(connection).toBeDefined(); | 247 | expect(connection).toBeDefined(); |
| @@ -38,6 +38,7 @@ import { · 37 unmodified lines | |||
| 38 | addOpenApiTestSource, | 38 | addOpenApiTestSource, |
| 39 | makeOpenApiTestSourceConfig, | 39 | makeOpenApiTestSourceConfig, |
| 40 | serveOpenApiHttpApiTestServer, | 40 | serveOpenApiHttpApiTestServer, |
| 41 | unwrapInvocation, | ||
| 41 | } from "@executor-js/plugin-openapi/testing"; | 42 | } from "@executor-js/plugin-openapi/testing"; |
| 42 | 43 | ||
| 43 | import { openApiPlugin } from "./plugin"; | 44 | import { openApiPlugin } from "./plugin"; |
| @@ -277,25 +278,21 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 233 unmodified lines | |||
| 277 | // yields her token; bob's scope yields his. Same source, same | 278 | // yields her token; bob's scope yields his. Same source, same |
| 278 | // tool, different injected bearer. | 279 | // tool, different injected bearer. |
| 279 | // ------------------------------------------------------------- | 280 | // ------------------------------------------------------------- |
| 280 | const aliceResult = (yield* aliceExec.tools.invoke( | 281 | const aliceResult = unwrapInvocation( |
| 281 | "vercel.projects.list", | 282 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 282 | {}, | 283 | ); |
| 283 | autoApprove, | ||
| 284 | )) as { | ||
| 285 | data: { authorization?: string; token?: string } | null; | ||
| 286 | error: unknown; | ||
| 287 | }; | ||
| 288 | expect(aliceResult.error).toBeNull(); | 284 | expect(aliceResult.error).toBeNull(); |
| 289 | expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); | 285 | const aliceData = aliceResult.data as EchoHeaders | null; |
| 290 | expect(aliceResult.data?.token).toBe("alice-team"); | 286 | expect(aliceData?.authorization).toBe("Bearer alice-vercel-token"); |
| 287 | expect(aliceData?.token).toBe("alice-team"); | ||
| 291 | 288 | ||
| 292 | const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { | 289 | const bobResult = unwrapInvocation( |
| 293 | data: { authorization?: string; token?: string } | null; | 290 | yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 294 | error: unknown; | 291 | ); |
| 295 | }; | ||
| 296 | expect(bobResult.error).toBeNull(); | 292 | expect(bobResult.error).toBeNull(); |
| 297 | expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); | 293 | const bobData = bobResult.data as EchoHeaders | null; |
| 298 | expect(bobResult.data?.token).toBe("bob-team"); | 294 | expect(bobData?.authorization).toBe("Bearer bob-vercel-token"); |
| 295 | expect(bobData?.token).toBe("bob-team"); | ||
| 299 | 296 | ||
| 300 | // ------------------------------------------------------------- | 297 | // ------------------------------------------------------------- |
| 301 | // 5. Scope attribution: each user's token is pinned to their | 298 | // 5. Scope attribution: each user's token is pinned to their |
| @@ -450,20 +447,19 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 148 unmodified lines | |||
| 450 | }), | 447 | }), |
| 451 | ); | 448 | ); |
| 452 | 449 | ||
| 453 | const aliceResult = (yield* aliceExec.tools.invoke( | 450 | const aliceResult = unwrapInvocation( |
| 454 | "vercel.projects.list", | 451 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 455 | {}, | 452 | ); |
| 456 | autoApprove, | ||
| 457 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 458 | expect(aliceResult.error).toBeNull(); | 453 | expect(aliceResult.error).toBeNull(); |
| 459 | expect(aliceResult.data?.authorization).toBe("Bearer alice-vercel-token"); | 454 | expect((aliceResult.data as EchoHeaders | null)?.authorization).toBe( |
| 455 | "Bearer alice-vercel-token", | ||
| 456 | ); | ||
| 460 | 457 | ||
| 461 | const bobResult = (yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { | 458 | const bobResult = unwrapInvocation( |
| 462 | data: { authorization?: string } | null; | 459 | yield* bobExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 463 | error: unknown; | 460 | ); |
| 464 | }; | ||
| 465 | expect(bobResult.error).toBeNull(); | 461 | expect(bobResult.error).toBeNull(); |
| 466 | expect(bobResult.data?.authorization).toBe("Bearer bob-vercel-token"); | 462 | expect((bobResult.data as EchoHeaders | null)?.authorization).toBe("Bearer bob-vercel-token"); |
| 467 | }), | 463 | }), |
| 468 | ); | 464 | ); |
| 469 | 465 | ||
| @@ -558,13 +554,11 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 88 unmodified lines | |||
| 558 | }), | 554 | }), |
| 559 | ); | 555 | ); |
| 560 | 556 | ||
| 561 | const sharedResult = (yield* aliceExec.tools.invoke( | 557 | const sharedResult = unwrapInvocation( |
| 562 | "vercel.projects.list", | 558 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 563 | {}, | 559 | ); |
| 564 | autoApprove, | ||
| 565 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 566 | expect(sharedResult.error).toBeNull(); | 560 | expect(sharedResult.error).toBeNull(); |
| 567 | expect(sharedResult.data?.authorization).toBe("Bearer org-token"); | 561 | expect((sharedResult.data as EchoHeaders | null)?.authorization).toBe("Bearer org-token"); |
| 568 | 562 | ||
| 569 | yield* aliceExec.secrets.set( | 563 | yield* aliceExec.secrets.set( |
| 570 | SetSecretInput.make({ | 564 | SetSecretInput.make({ |
| @@ -587,13 +581,13 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 16 unmodified lines | |||
| 587 | }), | 581 | }), |
| 588 | ); | 582 | ); |
| 589 | 583 | ||
| 590 | const overrideResult = (yield* aliceExec.tools.invoke( | 584 | const overrideResult = unwrapInvocation( |
| 591 | "vercel.projects.list", | 585 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 592 | {}, | 586 | ); |
| 593 | autoApprove, | ||
| 594 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 595 | expect(overrideResult.error).toBeNull(); | 587 | expect(overrideResult.error).toBeNull(); |
| 596 | expect(overrideResult.data?.authorization).toBe("Bearer alice-token"); | 588 | expect((overrideResult.data as EchoHeaders | null)?.authorization).toBe( |
| 589 | "Bearer alice-token", | ||
| 590 | ); | ||
| 597 | 591 | ||
| 598 | yield* aliceExec.openapi.removeSourceBinding( | 592 | yield* aliceExec.openapi.removeSourceBinding( |
| 599 | "vercel", | 593 | "vercel", |
| @@ -602,13 +596,11 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 2 unmodified lines | |||
| 602 | String(aliceScope.id), | 596 | String(aliceScope.id), |
| 603 | ); | 597 | ); |
| 604 | 598 | ||
| 605 | const fallbackResult = (yield* aliceExec.tools.invoke( | 599 | const fallbackResult = unwrapInvocation( |
| 606 | "vercel.projects.list", | 600 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 607 | {}, | 601 | ); |
| 608 | autoApprove, | ||
| 609 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 610 | expect(fallbackResult.error).toBeNull(); | 602 | expect(fallbackResult.error).toBeNull(); |
| 611 | expect(fallbackResult.data?.authorization).toBe("Bearer org-token"); | 603 | expect((fallbackResult.data as EchoHeaders | null)?.authorization).toBe("Bearer org-token"); |
| 612 | 604 | ||
| 613 | yield* aliceExec.openapi.setSourceBinding( | 605 | yield* aliceExec.openapi.setSourceBinding( |
| 614 | OpenApiSourceBindingInput.make({ | 606 | OpenApiSourceBindingInput.make({ |
| @@ -812,12 +804,11 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 197 unmodified lines | |||
| 812 | }), | 804 | }), |
| 813 | ); | 805 | ); |
| 814 | 806 | ||
| 815 | const result = (yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { | 807 | const result = unwrapInvocation( |
| 816 | data: { authorization?: string } | null; | 808 | yield* aliceExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 817 | error: unknown; | 809 | ); |
| 818 | }; | ||
| 819 | expect(result.error).toBeNull(); | 810 | expect(result.error).toBeNull(); |
| 820 | expect(result.data?.authorization).toBe("Bearer alice-token"); | 811 | expect((result.data as EchoHeaders | null)?.authorization).toBe("Bearer alice-token"); |
| 821 | }), | 812 | }), |
| 822 | ); | 813 | ); |
| 823 | 814 | ||
| @@ -914,13 +905,12 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 90 unmodified lines | |||
| 914 | }), | 905 | }), |
| 915 | ); | 906 | ); |
| 916 | 907 | ||
| 917 | const result = (yield* userExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { | 908 | const result = unwrapInvocation( |
| 918 | data: { authorization?: string } | null; | 909 | yield* userExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 919 | error: unknown; | 910 | ); |
| 920 | }; | ||
| 921 | 911 | ||
| 922 | expect(result.error).toBeNull(); | 912 | expect(result.error).toBeNull(); |
| 923 | expect(result.data?.authorization).toBe("Bearer org-token"); | 913 | expect((result.data as EchoHeaders | null)?.authorization).toBe("Bearer org-token"); |
| 924 | }), | 914 | }), |
| 925 | ); | 915 | ); |
| 926 | 916 | ||
| @@ -1013,13 +1003,12 @@ describe("OpenAPI multi-scope bearer (Vercel-style)", () => { · 86 unmodified lines | |||
| 1013 | }), | 1003 | }), |
| 1014 | ); | 1004 | ); |
| 1015 | 1005 | ||
| 1016 | const result = (yield* userExec.tools.invoke("vercel.projects.list", {}, autoApprove)) as { | 1006 | const result = unwrapInvocation( |
| 1017 | data: { authorization?: string } | null; | 1007 | yield* userExec.tools.invoke("vercel.projects.list", {}, autoApprove), |
| 1018 | error: unknown; | 1008 | ); |
| 1019 | }; | ||
| 1020 | 1009 | ||
| 1021 | expect(result.error).toBeNull(); | 1010 | expect(result.error).toBeNull(); |
| 1022 | expect(result.data?.authorization).toBe("Bearer org-choice"); | 1011 | expect((result.data as EchoHeaders | null)?.authorization).toBe("Bearer org-choice"); |
| 1023 | }), | 1012 | }), |
| 1024 | ); | 1013 | ); |
| 1025 | }); | 1014 | }); |
| @@ -28,6 +28,7 @@ import { makeTestConfig, serveOAuthTestServer } from "@executor-js/sdk/testing"; · 27 unmodified lines | |||
| 28 | import { | 28 | import { |
| 29 | addOpenApiTestSource, | 29 | addOpenApiTestSource, |
| 30 | serveOpenApiHttpApiTestServer, | 30 | serveOpenApiHttpApiTestServer, |
| 31 | unwrapInvocation, | ||
| 31 | } from "@executor-js/plugin-openapi/testing"; | 32 | } from "@executor-js/plugin-openapi/testing"; |
| 32 | 33 | ||
| 33 | import { openApiPlugin } from "./plugin"; | 34 | import { openApiPlugin } from "./plugin"; |
| @@ -307,23 +308,25 @@ describe("OpenAPI multi-scope OAuth", () => { · 273 unmodified lines | |||
| 307 | // 4. Invoke through each exec — Authorization must carry that | 308 | // 4. Invoke through each exec — Authorization must carry that |
| 308 | // user's token. | 309 | // user's token. |
| 309 | // ------------------------------------------------------------- | 310 | // ------------------------------------------------------------- |
| 310 | const aliceResult = (yield* aliceExec.tools.invoke( | 311 | const aliceResult = unwrapInvocation( |
| 311 | "petstore.items.echoHeaders", | 312 | yield* aliceExec.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 312 | {}, | 313 | ); |
| 313 | autoApprove, | ||
| 314 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 315 | expect(aliceResult.error).toBeNull(); | 314 | expect(aliceResult.error).toBeNull(); |
| 316 | const aliceBearer = aliceResult.data?.authorization?.replace(/^Bearer\s+/i, ""); | 315 | const aliceBearer = (aliceResult.data as EchoHeaders | null)?.authorization?.replace( |
| 316 | /^Bearer\s+/i, | ||
| 317 | "", | ||
| 318 | ); | ||
| 317 | expect(aliceBearer).toBeDefined(); | 319 | expect(aliceBearer).toBeDefined(); |
| 318 | expect(yield* oauth.acceptsAccessToken(aliceBearer!)).toBe(true); | 320 | expect(yield* oauth.acceptsAccessToken(aliceBearer!)).toBe(true); |
| 319 | 321 | ||
| 320 | const bobResult = (yield* bobExec.tools.invoke( | 322 | const bobResult = unwrapInvocation( |
| 321 | "petstore.items.echoHeaders", | 323 | yield* bobExec.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 322 | {}, | 324 | ); |
| 323 | autoApprove, | ||
| 324 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 325 | expect(bobResult.error).toBeNull(); | 325 | expect(bobResult.error).toBeNull(); |
| 326 | const bobBearer = bobResult.data?.authorization?.replace(/^Bearer\s+/i, ""); | 326 | const bobBearer = (bobResult.data as EchoHeaders | null)?.authorization?.replace( |
| 327 | /^Bearer\s+/i, | ||
| 328 | "", | ||
| 329 | ); | ||
| 327 | expect(bobBearer).toBeDefined(); | 330 | expect(bobBearer).toBeDefined(); |
| 328 | expect(yield* oauth.acceptsAccessToken(bobBearer!)).toBe(true); | 331 | expect(yield* oauth.acceptsAccessToken(bobBearer!)).toBe(true); |
| 329 | expect(bobBearer).not.toBe(aliceBearer); | 332 | expect(bobBearer).not.toBe(aliceBearer); |
| @@ -610,23 +613,25 @@ describe("OpenAPI multi-scope OAuth", () => { · 280 unmodified lines | |||
| 610 | // (4) Each user's invocation resolves their OWN row and gets | 613 | // (4) Each user's invocation resolves their OWN row and gets |
| 611 | // their OWN token — not whatever the last signer happened to | 614 | // their OWN token — not whatever the last signer happened to |
| 612 | // mint. This is the core multi-user regression. | 615 | // mint. This is the core multi-user regression. |
| 613 | const aliceResult = (yield* aliceExec.tools.invoke( | 616 | const aliceResult = unwrapInvocation( |
| 614 | "petstore.items.echoHeaders", | 617 | yield* aliceExec.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 615 | {}, | 618 | ); |
| 616 | autoApprove, | ||
| 617 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 618 | expect(aliceResult.error).toBeNull(); | 619 | expect(aliceResult.error).toBeNull(); |
| 619 | const aliceBearer = aliceResult.data?.authorization?.replace(/^Bearer\s+/i, ""); | 620 | const aliceBearer = (aliceResult.data as EchoHeaders | null)?.authorization?.replace( |
| 621 | /^Bearer\s+/i, | ||
| 622 | "", | ||
| 623 | ); | ||
| 620 | expect(aliceBearer).toBeDefined(); | 624 | expect(aliceBearer).toBeDefined(); |
| 621 | expect(yield* oauth.acceptsAccessToken(aliceBearer!)).toBe(true); | 625 | expect(yield* oauth.acceptsAccessToken(aliceBearer!)).toBe(true); |
| 622 | 626 | ||
| 623 | const bobResult = (yield* bobExec.tools.invoke( | 627 | const bobResult = unwrapInvocation( |
| 624 | "petstore.items.echoHeaders", | 628 | yield* bobExec.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 625 | {}, | 629 | ); |
| 626 | autoApprove, | ||
| 627 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 628 | expect(bobResult.error).toBeNull(); | 630 | expect(bobResult.error).toBeNull(); |
| 629 | const bobBearer = bobResult.data?.authorization?.replace(/^Bearer\s+/i, ""); | 631 | const bobBearer = (bobResult.data as EchoHeaders | null)?.authorization?.replace( |
| 632 | /^Bearer\s+/i, | ||
| 633 | "", | ||
| 634 | ); | ||
| 630 | expect(bobBearer).toBeDefined(); | 635 | expect(bobBearer).toBeDefined(); |
| 631 | expect(yield* oauth.acceptsAccessToken(bobBearer!)).toBe(true); | 636 | expect(yield* oauth.acceptsAccessToken(bobBearer!)).toBe(true); |
| 632 | expect(bobBearer).not.toBe(aliceBearer); | 637 | expect(bobBearer).not.toBe(aliceBearer); |
| @@ -35,6 +35,7 @@ import { makeTestConfig, serveOAuthTestServer } from "@executor-js/sdk/testing"; · 34 unmodified lines | |||
| 35 | import { | 35 | import { |
| 36 | addOpenApiTestSource, | 36 | addOpenApiTestSource, |
| 37 | serveOpenApiHttpApiTestServer, | 37 | serveOpenApiHttpApiTestServer, |
| 38 | unwrapInvocation, | ||
| 38 | } from "@executor-js/plugin-openapi/testing"; | 39 | } from "@executor-js/plugin-openapi/testing"; |
| 39 | 40 | ||
| 40 | import { openApiPlugin } from "./plugin"; | 41 | import { openApiPlugin } from "./plugin"; |
| @@ -253,17 +254,16 @@ describe("OpenAPI oauth refresh", () => { · 212 unmodified lines | |||
| 253 | }); | 254 | }); |
| 254 | yield* bindOAuthConnection(executor, scopeId, "conn-refresh-ok", auth); | 255 | yield* bindOAuthConnection(executor, scopeId, "conn-refresh-ok", auth); |
| 255 | 256 | ||
| 256 | const result = (yield* executor.tools.invoke( | 257 | const result = unwrapInvocation( |
| 257 | "petstore.items.echoHeaders", | 258 | yield* executor.tools.invoke("petstore.items.echoHeaders", {}, autoApprove), |
| 258 | {}, | 259 | ); |
| 259 | autoApprove, | ||
| 260 | )) as { data: { authorization?: string } | null; error: unknown }; | ||
| 261 | 260 | ||
| 262 | expect(result.error).toBeNull(); | 261 | expect(result.error).toBeNull(); |
| 262 | const data = result.data as EchoHeaders | null; | ||
| 263 | // Proves the refresh landed: invoke carried the fresh token, | 263 | // Proves the refresh landed: invoke carried the fresh token, |
| 264 | // not the expired one we seeded. | 264 | // not the expired one we seeded. |
| 265 | expect(result.data?.authorization).not.toBe("Bearer expired-access-v1"); | 265 | expect(data?.authorization).not.toBe("Bearer expired-access-v1"); |
| 266 | const bearer = result.data?.authorization?.replace(/^Bearer\s+/i, ""); | 266 | const bearer = data?.authorization?.replace(/^Bearer\s+/i, ""); |
| 267 | expect(bearer).toBeDefined(); | 267 | expect(bearer).toBeDefined(); |
| 268 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); | 268 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); |
| 269 | const calls = refreshTokenRequests(yield* oauth.requests); | 269 | const calls = refreshTokenRequests(yield* oauth.requests); |
| @@ -313,12 +313,9 @@ describe("OpenAPI oauth refresh", () => { · 43 unmodified lines | |||
| 313 | ); | 313 | ); |
| 314 | 314 | ||
| 315 | for (const r of invokes) { | 315 | for (const r of invokes) { |
| 316 | const res = r as { | 316 | const res = unwrapInvocation(r); |
| 317 | data: { authorization?: string } | null; | ||
| 318 | error: unknown; | ||
| 319 | }; | ||
| 320 | expect(res.error).toBeNull(); | 317 | expect(res.error).toBeNull(); |
| 321 | const bearer = res.data?.authorization?.replace(/^Bearer\s+/i, ""); | 318 | const bearer = (res.data as EchoHeaders | null)?.authorization?.replace(/^Bearer\s+/i, ""); |
| 322 | expect(bearer).toBeDefined(); | 319 | expect(bearer).toBeDefined(); |
| 323 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); | 320 | expect(yield* oauth.acceptsAccessToken(bearer!)).toBe(true); |
| 324 | } | 321 | } |
| @@ -25,6 +25,7 @@ import { · 24 unmodified lines | |||
| 25 | addOpenApiTestSource, | 25 | addOpenApiTestSource, |
| 26 | makeOpenApiHttpApiTestSourceConfig, | 26 | makeOpenApiHttpApiTestSourceConfig, |
| 27 | serveOpenApiHttpApiTestServer, | 27 | serveOpenApiHttpApiTestServer, |
| 28 | unwrapInvocation, | ||
| 28 | } from "../testing"; | 29 | } from "../testing"; |
| 29 | 30 | ||
| 30 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; | 31 | const autoApprove: InvokeOptions = { onElicitation: "accept-all" }; |
| @@ -267,13 +268,15 @@ describe("OpenAPI Plugin", () => { · 236 unmodified lines | |||
| 267 | }), | 268 | }), |
| 268 | ); | 269 | ); |
| 269 | 270 | ||
| 270 | const result = (yield* executor.tools.invoke( | 271 | const preview = unwrapInvocation( |
| 271 | "executor.openapi.previewSpec", | 272 | yield* executor.tools.invoke( |
| 272 | { spec: testApiSpec() }, | 273 | "executor.openapi.previewSpec", |
| 273 | autoApprove, | 274 | { spec: testApiSpec() }, |
| 274 | )) as { operationCount: number }; | 275 | autoApprove, |
| 276 | ), | ||
| 277 | ).data as { operationCount: number }; | ||
| 275 | 278 | ||
| 276 | expect(result.operationCount).toBeGreaterThanOrEqual(2); | 279 | expect(preview.operationCount).toBeGreaterThanOrEqual(2); |
| 277 | }), | 280 | }), |
| 278 | ); | 281 | ); |
| 279 | 282 | ||
| @@ -316,11 +319,13 @@ describe("OpenAPI Plugin", () => { · 36 unmodified lines | |||
| 316 | }), | 319 | }), |
| 317 | ); | 320 | ); |
| 318 | 321 | ||
| 319 | const result = (yield* executor.tools.invoke( | 322 | const result = unwrapInvocation( |
| 320 | "executor.openapi.addSource", | 323 | yield* executor.tools.invoke( |
| 321 | testApiSourceConfig({ scope: String(orgScope), namespace: "runtime" }), | 324 | "executor.openapi.addSource", |
| 322 | autoApprove, | 325 | testApiSourceConfig({ scope: String(orgScope), namespace: "runtime" }), |
| 323 | )) as { sourceId: string; toolCount: number }; | 326 | autoApprove, |
| 327 | ), | ||
| 328 | ).data as { sourceId: string; toolCount: number }; | ||
| 324 | 329 | ||
| 325 | expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); | 330 | expect(result).toEqual({ sourceId: "runtime", toolCount: 4 }); |
| 326 | expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull(); | 331 | expect(yield* executor.openapi.getSource("runtime", String(userScope))).toBeNull(); |
| @@ -557,17 +562,12 @@ describe("OpenAPI Plugin", () => { · 230 unmodified lines | |||
| 557 | }, | 562 | }, |
| 558 | }); | 563 | }); |
| 559 | 564 | ||
| 560 | const result = (yield* executor.tools.invoke( | 565 | const result = unwrapInvocation( |
| 561 | "authed.items.echoHeaders", | 566 | yield* executor.tools.invoke("authed.items.echoHeaders", {}, autoApprove), |
| 562 | {}, | 567 | ); |
| 563 | autoApprove, | ||
| 564 | )) as { | ||
| 565 | data: { authorization?: string; "x-static"?: string } | null; | ||
| 566 | error: unknown; | ||
| 567 | }; | ||
| 568 | 568 | ||
| 569 | expect(result.error).toBeNull(); | 569 | expect(result.error).toBeNull(); |
| 570 | const data = result.data!; | 570 | const data = result.data as { authorization?: string; "x-static"?: string }; |
| 571 | expect(data.authorization).toBe("Bearer secret-value-123"); | 571 | expect(data.authorization).toBe("Bearer secret-value-123"); |
| 572 | expect(data["x-static"]).toBe("hello"); | 572 | expect(data["x-static"]).toBe("hello"); |
| 573 | }), | 573 | }), |
| @@ -751,10 +751,9 @@ describe("OpenAPI Plugin", () => { · 177 unmodified lines | |||
| 751 | namespace: "test", | 751 | namespace: "test", |
| 752 | }); | 752 | }); |
| 753 | 753 | ||
| 754 | const result = (yield* executor.tools.invoke("test.items.listItems", {}, autoApprove)) as { | 754 | const result = unwrapInvocation( |
| 755 | data: unknown; | 755 | yield* executor.tools.invoke("test.items.listItems", {}, autoApprove), |
| 756 | error: unknown; | 756 | ); |
| 757 | }; | ||
| 758 | expect(result.error).toBeNull(); | 757 | expect(result.error).toBeNull(); |
| 759 | expect(result.data).toEqual(ITEMS); | 758 | expect(result.data).toEqual(ITEMS); |
| 760 | }), | 759 | }), |
| @@ -780,11 +779,9 @@ describe("OpenAPI Plugin", () => { · 19 unmodified lines | |||
| 780 | namespace: "test", | 779 | namespace: "test", |
| 781 | }); | 780 | }); |
| 782 | 781 | ||
| 783 | const result = (yield* executor.tools.invoke( | 782 | const result = unwrapInvocation( |
| 784 | "test.items.getItem", | 783 | yield* executor.tools.invoke("test.items.getItem", { itemId: "2" }, autoApprove), |
| 785 | { itemId: "2" }, | 784 | ); |
| 786 | autoApprove, | ||
| 787 | )) as { data: unknown; error: unknown }; | ||
| 788 | expect(result.error).toBeNull(); | 785 | expect(result.error).toBeNull(); |
| 789 | expect(result.data).toEqual({ id: 2, name: "Gadget" }); | 786 | expect(result.data).toEqual({ id: 2, name: "Gadget" }); |
| 790 | }), | 787 | }), |
| @@ -810,16 +807,18 @@ describe("OpenAPI Plugin", () => { · 19 unmodified lines | |||
| 810 | namespace: "records", | 807 | namespace: "records", |
| 811 | }); | 808 | }); |
| 812 | 809 | ||
| 813 | const result = (yield* executor.tools.invoke( | 810 | const result = unwrapInvocation( |
| 814 | "records.items.queryRows", | 811 | yield* executor.tools.invoke( |
| 815 | { | 812 | "records.items.queryRows", |
| 816 | entryTypeId: "18538", | 813 | { |
| 817 | query: JSON.stringify([{ DisplayName: "Example" }]), | 814 | entryTypeId: "18538", |
| 818 | limit: 10, | 815 | query: JSON.stringify([{ DisplayName: "Example" }]), |
| 819 | skip: 0, | 816 | limit: 10, |
| 820 | }, | 817 | skip: 0, |
| 821 | autoApprove, | 818 | }, |
| 822 | )) as { data: unknown; error: unknown }; | 819 | autoApprove, |
| 820 | ), | ||
| 821 | ); | ||
| 823 | 822 | ||
| 824 | expect(result.data).toBeNull(); | 823 | expect(result.data).toBeNull(); |
| 825 | expect(result.error).toEqual( | 824 | expect(result.error).toEqual( |
| @@ -7,6 +7,7 @@ import { · 6 unmodified lines | |||
| 7 | SecretId, | 7 | SecretId, |
| 8 | SourceDetectionResult, | 8 | SourceDetectionResult, |
| 9 | StorageError, | 9 | StorageError, |
| 10 | ToolResult, | ||
| 10 | definePlugin, | 11 | definePlugin, |
| 11 | tool, | 12 | tool, |
| 12 | resolveSecretBackedMap, | 13 | resolveSecretBackedMap, |
| @@ -57,6 +58,77 @@ import { · 44 unmodified lines | |||
| 57 | // Plugin config | 58 | // Plugin config |
| 58 | // --------------------------------------------------------------------------- | 59 | // --------------------------------------------------------------------------- |
| 59 | 60 | ||
| 61 | const STRINGIFIED_BODY_CAP = 1024; | ||
| 62 | const UpstreamMessageBody = Schema.Struct({ message: Schema.String }); | ||
| 63 | const UpstreamErrorMessageBody = Schema.Struct({ errorMessage: Schema.String }); | ||
| 64 | const UpstreamNestedErrorBody = Schema.Struct({ error: UpstreamMessageBody }); | ||
| 65 | const UpstreamErrorsArrayBody = Schema.Struct({ | ||
| 66 | errors: Schema.Array( | ||
| 67 | Schema.Struct({ | ||
| 68 | detail: Schema.optional(Schema.String), | ||
| 69 | message: Schema.optional(Schema.String), | ||
| 70 | title: Schema.optional(Schema.String), | ||
| 71 | }), | ||
| 72 | ), | ||
| 73 | }); | ||
| 74 | const UpstreamDescriptionBody = Schema.Struct({ | ||
| 75 | detail: Schema.optional(Schema.String), | ||
| 76 | title: Schema.optional(Schema.String), | ||
| 77 | description: Schema.optional(Schema.String), | ||
| 78 | }); | ||
| 79 | |||
| 80 | const decodeUpstreamMessageBody = Schema.decodeUnknownOption(UpstreamMessageBody); | ||
| 81 | const decodeUpstreamErrorMessageBody = Schema.decodeUnknownOption(UpstreamErrorMessageBody); | ||
| 82 | const decodeUpstreamNestedErrorBody = Schema.decodeUnknownOption(UpstreamNestedErrorBody); | ||
| 83 | const decodeUpstreamErrorsArrayBody = Schema.decodeUnknownOption(UpstreamErrorsArrayBody); | ||
| 84 | const decodeUpstreamDescriptionBody = Schema.decodeUnknownOption(UpstreamDescriptionBody); | ||
| 85 | |||
| 86 | const clampedStringify = (value: unknown): string => { | ||
| 87 | let s: string; | ||
| 88 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: JSON.stringify may throw on cycles; fall back to String() so the upstream body can still be surfaced as ToolError.details fallback text | ||
| 89 | try { | ||
| 90 | s = JSON.stringify(value); | ||
| 91 | } catch { | ||
| 92 | s = String(value); | ||
| 93 | } | ||
| 94 | return s.length > STRINGIFIED_BODY_CAP ? `${s.slice(0, STRINGIFIED_BODY_CAP)}…` : s; | ||
| 95 | }; | ||
| 96 | |||
| 97 | const firstNonEmpty = (...values: readonly (string | undefined)[]): string | undefined => | ||
| 98 | values.find((value) => value !== undefined && value.length > 0); | ||
| 99 | |||
| 100 | // Walk known upstream error-body shapes so ToolError.message stays concise | ||
| 101 | // while ToolError.details preserves the original body. | ||
| 102 | const extractUpstreamMessage = (body: unknown, status: number): string => { | ||
| 103 | if (typeof body === "string") { | ||
| 104 | return body.length > 0 ? body : `Upstream returned HTTP ${status}`; | ||
| 105 | } | ||
| 106 | const nested = Option.getOrUndefined(decodeUpstreamNestedErrorBody(body)); | ||
| 107 | const messageBody = Option.getOrUndefined(decodeUpstreamMessageBody(body)); | ||
| 108 | const errorMessageBody = Option.getOrUndefined(decodeUpstreamErrorMessageBody(body)); | ||
| 109 | const errorsBody = Option.getOrUndefined(decodeUpstreamErrorsArrayBody(body)); | ||
| 110 | const descriptionBody = Option.getOrUndefined(decodeUpstreamDescriptionBody(body)); | ||
| 111 | const arrayMessage = errorsBody?.errors | ||
| 112 | .map(({ detail, message: upstreamMessage, title }) => | ||
| 113 | firstNonEmpty(detail, upstreamMessage, title), | ||
| 114 | ) | ||
| 115 | .find((message) => message !== undefined); | ||
| 116 | const message = firstNonEmpty( | ||
| 117 | nested?.error.message, | ||
| 118 | messageBody?.message, | ||
| 119 | errorMessageBody?.errorMessage, | ||
| 120 | arrayMessage, | ||
| 121 | descriptionBody?.detail, | ||
| 122 | descriptionBody?.title, | ||
| 123 | descriptionBody?.description, | ||
| 124 | ); | ||
| 125 | if (message !== undefined) return message; | ||
| 126 | if (body !== null && typeof body === "object") { | ||
| 127 | return clampedStringify(body); | ||
| 128 | } | ||
| 129 | return `Upstream returned HTTP ${status}`; | ||
| 130 | }; | ||
| 131 | |||
| 60 | export type HeaderValue = HeaderValueValue; | 132 | export type HeaderValue = HeaderValueValue; |
| 61 | export type ConfiguredHeaderValue = ConfiguredHeaderValueValue; | 133 | export type ConfiguredHeaderValue = ConfiguredHeaderValueValue; |
| 62 | export type OpenApiHeaderInput = HeaderValue | ConfiguredHeaderValue; | 134 | export type OpenApiHeaderInput = HeaderValue | ConfiguredHeaderValue; |
| @@ -1193,7 +1265,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 1130 unmodified lines | |||
| 1193 | name: "previewSpec", | 1265 | name: "previewSpec", |
| 1194 | description: "Preview an OpenAPI document before adding it as a source", | 1266 | description: "Preview an OpenAPI document before adding it as a source", |
| 1195 | inputSchema: PreviewSpecInputStandardSchema, | 1267 | inputSchema: PreviewSpecInputStandardSchema, |
| 1196 | execute: (input) => self.previewSpec(input), | 1268 | execute: (input) => Effect.map(self.previewSpec(input), ToolResult.ok), |
| 1197 | }), | 1269 | }), |
| 1198 | tool({ | 1270 | tool({ |
| 1199 | name: "addSource", | 1271 | name: "addSource", |
| @@ -1204,7 +1276,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 4 unmodified lines | |||
| 1204 | }, | 1276 | }, |
| 1205 | inputSchema: AddSourceInputStandardSchema, | 1277 | inputSchema: AddSourceInputStandardSchema, |
| 1206 | outputSchema: AddSourceOutputStandardSchema, | 1278 | outputSchema: AddSourceOutputStandardSchema, |
| 1207 | execute: (input) => self.addSpec(input), | 1279 | execute: (input) => Effect.map(self.addSpec(input), ToolResult.ok), |
| 1208 | }), | 1280 | }), |
| 1209 | ], | 1281 | ], |
| 1210 | }, | 1282 | }, |
| @@ -1283,7 +1355,20 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => { · 72 unmodified lines | |||
| 1283 | httpClientLayer, | 1355 | httpClientLayer, |
| 1284 | ); | 1356 | ); |
| 1285 | 1357 | ||
| 1286 | return result; | 1358 | const ok = result.status >= 200 && result.status < 300; |
| 1359 | if (!ok) { | ||
| 1360 | return ToolResult.fail({ | ||
| 1361 | code: "upstream_http_error", | ||
| 1362 | status: result.status, | ||
| 1363 | message: extractUpstreamMessage(result.error, result.status), | ||
| 1364 | details: result.error, | ||
| 1365 | }); | ||
| 1366 | } | ||
| 1367 | return ToolResult.ok({ | ||
| 1368 | status: result.status, | ||
| 1369 | headers: result.headers, | ||
| 1370 | data: result.data, | ||
| 1371 | }); | ||
| 1287 | }), | 1372 | }), |
| 1288 | 1373 | ||
| 1289 | resolveAnnotations: ({ ctx, sourceId, toolRows }) => | 1374 | resolveAnnotations: ({ ctx, sourceId, toolRows }) => |
| @@ -37,6 +37,7 @@ import { · 36 unmodified lines | |||
| 37 | makeOpenApiTestSourceConfig, | 37 | makeOpenApiTestSourceConfig, |
| 38 | type OpenApiTestServerShape, | 38 | type OpenApiTestServerShape, |
| 39 | serveOpenApiHttpApiTestServer, | 39 | serveOpenApiHttpApiTestServer, |
| 40 | unwrapInvocation, | ||
| 40 | } from "../testing"; | 41 | } from "../testing"; |
| 41 | 42 | ||
| 42 | import { openApiPlugin } from "./plugin"; | 43 | import { openApiPlugin } from "./plugin"; |
| @@ -303,10 +304,10 @@ describe("OpenAPI upstream failure modes", () => { · 260 unmodified lines | |||
| 303 | const { baseUrl } = yield* slowServer; | 304 | const { baseUrl } = yield* slowServer; |
| 304 | const executor = yield* buildExecutor(baseUrl); | 305 | const executor = yield* buildExecutor(baseUrl); |
| 305 | 306 | ||
| 306 | const result = yield* executor.tools.invoke("f.things.listThings", {}, autoApprove); | 307 | const result = unwrapInvocation( |
| 307 | // Empty array via .data envelope or directly — accept either shape. | 308 | yield* executor.tools.invoke("f.things.listThings", {}, autoApprove), |
| 308 | const data = (result as { data?: unknown }).data ?? result; | 309 | ); |
| 309 | expect(data).toEqual([]); | 310 | expect(result.data).toEqual([]); |
| 310 | }), | 311 | }), |
| 311 | ); | 312 | ); |
| 312 | }); | 313 | }); |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { Context, Data, Effect, Layer, Predicate, Ref, Schema, Scope } from "effect"; | 1 | import { Context, Data, Effect, Layer, Option, Predicate, Ref, Schema, Scope } from "effect"; |
| 2 | import { | 2 | import { |
| 3 | HttpClient, | 3 | HttpClient, |
| 4 | HttpRouter, | 4 | HttpRouter, |
| @@ -13,7 +13,7 @@ import { · 8 unmodified lines | |||
| 13 | OpenApi, | 13 | OpenApi, |
| 14 | } from "effect/unstable/httpapi"; | 14 | } from "effect/unstable/httpapi"; |
| 15 | import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing"; | 15 | import { OAuthTestServer, serveTestHttpServerLayer } from "@executor-js/sdk/testing"; |
| 16 | import type { ScopeId } from "@executor-js/sdk/core"; | 16 | import { isToolResult, type ScopeId } from "@executor-js/sdk/core"; |
| 17 | import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; | 17 | import type { OpenApiPluginExtension, OpenApiSpecConfig } from "../sdk/plugin"; |
| 18 | 18 | ||
| 19 | export class OpenApiTestServerAddressError extends Data.TaggedError( | 19 | export class OpenApiTestServerAddressError extends Data.TaggedError( |
| @@ -618,3 +618,55 @@ export const TestLayers = { · 598 unmodified lines | |||
| 618 | echo: OpenApiEchoTestServer.layer, | 618 | echo: OpenApiEchoTestServer.layer, |
| 619 | echoWithOAuth: OpenApiEchoTestServer.layerWithOAuth, | 619 | echoWithOAuth: OpenApiEchoTestServer.layerWithOAuth, |
| 620 | }; | 620 | }; |
| 621 | |||
| 622 | const OpenApiTransportEnvelope = Schema.Struct({ | ||
| 623 | status: Schema.Number, | ||
| 624 | headers: Schema.Record(Schema.String, Schema.String), | ||
| 625 | data: Schema.Unknown, | ||
| 626 | }); | ||
| 627 | |||
| 628 | const decodeOpenApiTransportEnvelope = Schema.decodeUnknownOption(OpenApiTransportEnvelope); | ||
| 629 | |||
| 630 | export interface OpenApiInvocationResult<TData = Record<string, unknown> | unknown[] | null> { | ||
| 631 | readonly status: number | null; | ||
| 632 | readonly headers: Record<string, string> | null; | ||
| 633 | readonly data: TData; | ||
| 634 | readonly error: unknown; | ||
| 635 | } | ||
| 636 | |||
| 637 | export const unwrapInvocation = <TData = Record<string, unknown> | null>( | ||
| 638 | raw: unknown, | ||
| 639 | ): OpenApiInvocationResult<TData> => { | ||
| 640 | if (!isToolResult(raw)) { | ||
| 641 | return { | ||
| 642 | status: null, | ||
| 643 | headers: null, | ||
| 644 | data: raw as TData, | ||
| 645 | error: null, | ||
| 646 | }; | ||
| 647 | } | ||
| 648 | if (raw.ok) { | ||
| 649 | const inner = raw.data; | ||
| 650 | const wrapped = Option.getOrUndefined(decodeOpenApiTransportEnvelope(inner)); | ||
| 651 | if (wrapped !== undefined) { | ||
| 652 | return { | ||
| 653 | status: wrapped.status, | ||
| 654 | headers: wrapped.headers, | ||
| 655 | data: wrapped.data as TData, | ||
| 656 | error: null, | ||
| 657 | }; | ||
| 658 | } | ||
| 659 | return { | ||
| 660 | status: null, | ||
| 661 | headers: null, | ||
| 662 | data: inner as TData, | ||
| 663 | error: null, | ||
| 664 | }; | ||
| 665 | } | ||
| 666 | return { | ||
| 667 | status: raw.error.status ?? null, | ||
| 668 | headers: null, | ||
| 669 | data: null as TData, | ||
| 670 | error: raw.error.details ?? raw.error, | ||
| 671 | }; | ||
| 672 | }; | ||