Files
suaveplan/packages/communication/messaging/slack-client/src/error-codes/error-codes.ts
2026-05-27 23:29:53 +01:00

140 lines
4.4 KiB
TypeScript

import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/slack-client`. Every error
* thrown by the package surfaces one of these eight codes so downstream
* consumers — loggers, problem+json responders, retry wrappers — can map
* to retry policy, HTTP status, and i18n keys without re-implementing the
* classifier.
*
* The registry uses the defineCodes builder from `@suaveplan/error`,
* which produces a frozen `{ REASON: "SLACK.REASON" }` record at compile
* time so the literal-typed union is preserved through the public surface.
*/
export const SLACK_CODES = defineCodes("SLACK", [
"API_ERROR",
"AUTH_ERROR",
"RATE_LIMITED",
"CHANNEL_NOT_FOUND",
"WORKSPACE_NOT_FOUND",
"CONFIG_MISSING",
"WEBHOOK_FAILED",
"TIMEOUT",
] as const);
/** Union of every fully-qualified code emitted by this package. */
export type SlackErrorCode = (typeof SLACK_CODES)[keyof typeof SLACK_CODES];
/** Base class for every typed Slack error. */
export abstract class SlackError extends SuaveplanError<SlackErrorCode> {}
interface BaseOpts extends SuaveplanErrorOptions {
readonly message?: string;
}
function pickOpts(opts: BaseOpts): SuaveplanErrorOptions {
const out: {
cause?: unknown;
context?: Readonly<Record<string, unknown>>;
traceId?: string;
} = {};
if (opts.cause !== undefined) out.cause = opts.cause;
if (opts.context !== undefined) out.context = opts.context;
if (opts.traceId !== undefined) out.traceId = opts.traceId;
return out;
}
/** Slack API returned a non-ok response (default mapping for other 4xx/5xx). */
export class SlackApiError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.API_ERROR,
opts.message ?? "slack.errors.apiError",
pickOpts(opts)
);
}
}
/** Token rejected or missing (401/403, invalid_auth, token_revoked, ...). */
export class SlackAuthError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.AUTH_ERROR,
opts.message ?? "slack.errors.authError",
pickOpts(opts)
);
}
}
/** Slack rate-limited the caller; honour `context.retryAfterMs`. */
export class SlackRateLimitedError extends SlackError {
public constructor(
opts: BaseOpts & { readonly retryAfterMs?: number } = {}
) {
const ctx: Record<string, unknown> = { ...(opts.context ?? {}) };
if (opts.retryAfterMs !== undefined)
ctx["retryAfterMs"] = opts.retryAfterMs;
super(
SLACK_CODES.RATE_LIMITED,
opts.message ?? "slack.errors.rateLimited",
pickOpts({ ...opts, context: ctx })
);
}
}
/** Channel does not exist or is inaccessible (404, channel_not_found, ...). */
export class SlackChannelNotFoundError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.CHANNEL_NOT_FOUND,
opts.message ?? "slack.errors.channelNotFound",
pickOpts(opts)
);
}
}
/** Workspace not found (team_not_found, no_workspace). */
export class SlackWorkspaceNotFoundError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.WORKSPACE_NOT_FOUND,
opts.message ?? "slack.errors.workspaceNotFound",
pickOpts(opts)
);
}
}
/** Required configuration value (token, webhookUrl, ...) is missing. */
export class SlackConfigMissingError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.CONFIG_MISSING,
opts.message ?? "slack.errors.configMissing",
pickOpts(opts)
);
}
}
/** Incoming-webhook POST failed with a non-2xx response. */
export class SlackWebhookFailedError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.WEBHOOK_FAILED,
opts.message ?? "slack.errors.webhookFailed",
pickOpts(opts)
);
}
}
/** Slack request timed out (internal `timeoutMs` or caller `AbortSignal`). */
export class SlackTimeoutError extends SlackError {
public constructor(opts: BaseOpts = {}) {
super(
SLACK_CODES.TIMEOUT,
opts.message ?? "slack.errors.timeout",
pickOpts(opts)
);
}
}