/** * Slack client implementation. * * Extends RestApiClientBase to inherit shared request infrastructure, * then provides a Slack-specific apiRequest() that returns a * SlackResponse discriminated union instead of throwing on ok:false. */ import type { z } from "zod"; import { RestApiValidationError } from "../../errors.js"; import { RestApiClientBase } from "../base/rest-api-client-base.js"; import type { ApiRequestOptions } from "../base/types.js"; import type { TraceMetadata } from "../registry.js"; import { SlackErrorSchema, SlackSuccessEnvelopeSchema, type SlackApiRequestSchema, type SlackClient, type SlackResponse, } from "./types.js"; /** * Internal implementation of SlackClient. * * Extends RestApiClientBase for request building and execution. * Uses {@link SlackErrorSchema} to parse Slack error responses via Zod, * then falls back to the caller's schema for success responses. */ export class SlackClientImpl extends RestApiClientBase implements SlackClient { async apiRequest( options: ApiRequestOptions, schema: SlackApiRequestSchema, metadata?: TraceMetadata, ): Promise>> { const result = await this.executeApiRequest(options, schema.body, metadata); // Slack returns errors as HTTP 200 with { ok: false, error: "..." }. // Let Zod parse the error shape — if it matches, return the error branch. const errorResult = SlackErrorSchema.safeParse(result); if (errorResult.success) { return errorResult.data; } // Guard: if the raw response has ok: false but didn't match SlackErrorSchema // (e.g. missing error string), don't let it fall through to the success // schema which might accept it and get stamped with ok: true. if ( typeof result === "object" && result !== null && "ok" in result && (result as Record).ok === false ) { throw new RestApiValidationError( `Slack returned ok: false but the response did not match the expected error shape: ${errorResult.error.message}`, { zodError: errorResult.error, data: result, }, ); } // Success responses must carry the Slack envelope discriminant. const successEnvelopeResult = SlackSuccessEnvelopeSchema.safeParse(result); if (!successEnvelopeResult.success) { throw new RestApiValidationError( `Slack success response is missing the expected ok: true envelope: ${successEnvelopeResult.error.message}`, { zodError: successEnvelopeResult.error, data: result, }, ); } // Validate caller-provided payload shape (without the `ok` field). // This keeps strict payload schemas compatible with Slack's envelope. const payloadCandidate: Record = { ...(result as Record), }; delete payloadCandidate.ok; const payloadResult = schema.response.safeParse(payloadCandidate); if (!payloadResult.success) { throw new RestApiValidationError( `Response validation failed: ${payloadResult.error.message}`, { zodError: payloadResult.error, data: result, }, ); } // Defense against `.passthrough()` schemas: strip any leaked `ok` from // the parsed payload before re-injecting the literal discriminant. const payloadWithoutOk: Record = { ...payloadResult.data }; delete payloadWithoutOk.ok; return { ...payloadWithoutOk, ok: true as const, } as z.output & { readonly ok: true }; } }