@autofleet/slack-notifier 0.1.2-beta-740ca7cd.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # @autofleet/slack-notifier
2
+
3
+ A small, reusable client for posting messages to a Slack channel from any app or
4
+ microservice in the monorepo. Built on Slack's Web API (`chat.postMessage`) via
5
+ `@slack/web-api`'s `WebClient`, which handles retries, rate limits, and pagination.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @autofleet/slack-notifier --filter @autofleet/<your-project>
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createSlackNotifier } from '@autofleet/slack-notifier';
17
+
18
+ const slack = createSlackNotifier({
19
+ token: process.env.SLACK_BOT_TOKEN ?? '',
20
+ // Sending is opt-in: `enabled` defaults to false, so without this every send is a no-op.
21
+ enabled: process.env.NODE_ENV === 'production', // off in dev/test — sendMessage no-ops
22
+ });
23
+
24
+ await slack.sendMessage({
25
+ channel: '#ordering-alerts', // channel name or ID
26
+ text: 'Order sync failed for fleet 1234',
27
+ });
28
+ ```
29
+
30
+ With Block Kit (rich formatting). `text` is always sent as the notification fallback:
31
+
32
+ ```ts
33
+ await slack.sendMessage({
34
+ channel: '#ordering-alerts',
35
+ text: 'Order sync failed for fleet 1234',
36
+ blocks: [
37
+ { type: 'section', text: { type: 'mrkdwn', text: '*Order sync failed* for fleet `1234`' } },
38
+ ],
39
+ });
40
+ ```
41
+
42
+ ## Configuration
43
+
44
+ | Field | Type | Default | Description |
45
+ | ------------------ | --------- | ------- | --- |
46
+ | `token` | `string` | — | `xoxb-…` bot token, injected by the caller. Validated lazily on first real send. |
47
+ | `enabled` | `boolean` | `false` | Master on/off switch — **sending is opt-in**. When `false` (the default), `sendMessage` is a no-op (no network call, no token required). Set `true` to actually post. |
48
+ | `defaultChannel` | `string` | — | Fallback channel used when `sendMessage` is called without a `channel`. |
49
+ | `defaultUsername` | `string` | — | Default sender name for every message (override per call with `username`). Needs `chat:write.customize`. |
50
+ | `defaultIconEmoji` | `string` | — | Default sender emoji, e.g. `":robot_face:"` (override per call with `iconEmoji`). Needs `chat:write.customize`. |
51
+ | `defaultIconUrl` | `string` | — | Default sender profile picture as an image URL (override per call with `iconUrl`). Takes precedence over `defaultIconEmoji`. Needs `chat:write.customize`. |
52
+ | `logger` | `Logger` | — | Optional logger (compatible with `@autofleet/logger`). |
53
+
54
+ ### Customizing the sender name / picture
55
+
56
+ You can override the name and avatar shown for messages — either as defaults on the
57
+ notifier, or per call. The avatar can be an **emoji** (`iconEmoji`) or an **image URL**
58
+ (`iconUrl`); if both are given, Slack uses the URL.
59
+
60
+ ```ts
61
+ const slack = createSlackNotifier({
62
+ token: process.env.SLACK_BOT_TOKEN ?? '',
63
+ enabled: true,
64
+ defaultUsername: 'ordering-ms',
65
+ defaultIconUrl: 'https://example.com/ordering-bot-avatar.png',
66
+ });
67
+
68
+ // uses the defaults above
69
+ await slack.sendMessage({ channel: '#ops', text: 'hi' });
70
+
71
+ // per-message override (image avatar)
72
+ await slack.sendMessage({
73
+ channel: '#ops',
74
+ text: 'sync failed',
75
+ username: 'xlr8-sync',
76
+ iconUrl: 'https://example.com/xlr8-avatar.png',
77
+ });
78
+
79
+ // per-message override (emoji avatar)
80
+ await slack.sendMessage({
81
+ channel: '#ops',
82
+ text: 'sync failed',
83
+ username: 'xlr8-sync',
84
+ iconEmoji: ':rotating_light:',
85
+ });
86
+ ```
87
+
88
+ > **Per-message vs. permanent avatar.** `username`/`iconEmoji`/`iconUrl` only override the
89
+ > name and picture **for the messages you send** — they do **not** change the bot's
90
+ > permanent profile. The app's standing name and avatar are set once in the Slack app UI
91
+ > (<https://api.slack.com/apps> → your app → _Basic Information → Display Information_);
92
+ > there is no API to change them from code.
93
+
94
+ > All three overrides require the bot to have the **`chat:write.customize`** scope, and
95
+ > changes take effect only after the app is **reinstalled** to the workspace. Without the
96
+ > scope Slack silently ignores them and posts under the app's default identity. An
97
+ > `iconUrl` must point to a **publicly reachable** image (Slack fetches it server-side).
98
+
99
+ The package never reads `process.env` itself — secrets and the enabled flag are injected
100
+ by the caller.
101
+
102
+ ## Behavior
103
+
104
+ - **Disabled** (`enabled: false`, the default): `sendMessage` resolves to `{ ok: true, skipped: true, channelId: '', ts: '' }` without calling Slack. Sending is opt-in — pass `enabled: true` to post.
105
+ - **Errors**: failures throw a typed `SlackNotifierError` with a `code` (e.g.
106
+ `channel_not_found`, `not_in_channel`, `invalid_auth`, `missing_token`).
107
+ - **Channels**: with the `chat:write.public` scope the bot can post to any public channel
108
+ without joining. Private channels require inviting the bot first.
109
+
110
+ ## Slack app setup
111
+
112
+ The bot needs the `chat:write` and `chat:write.public` scopes — plus `chat:write.customize`
113
+ if you use `username`/`iconEmoji`/`iconUrl`. Create the app at
114
+ <https://api.slack.com/apps>, install it to the workspace, and copy the Bot User OAuth
115
+ Token (`xoxb-…`) into your secret manager as `SLACK_BOT_TOKEN`. After changing scopes,
116
+ reinstall the app for them to take effect.
package/lib/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ let e=require(`@slack/web-api`);var t=class extends Error{constructor(e,t,n){super(e),this.name=`SlackNotifierError`,this.code=t,n?.cause!==void 0&&(this.cause=n.cause)}},n=class{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e){this.#e=e.enabled??!1,this.#t=e.token,this.#n=e.defaultChannel,this.#r=e.defaultUsername,this.#i=e.defaultIconEmoji,this.#a=e.defaultIconUrl,this.#o=e.logger}get enabled(){return this.#e}async sendMessage(e){let n=e.channel??this.#n;if(!this.#e)return this.#o?.debug?.(`slack notifier disabled, skipping message`,{channel:n}),{ok:!0,skipped:!0,channelId:``,ts:``};if(!n)throw new t(`No channel provided and no defaultChannel configured`,`channel_required`);try{let t={channel:n,text:e.text,thread_ts:e.threadTs,username:e.username??this.#r,icon_emoji:e.iconEmoji??this.#i,icon_url:e.iconUrl??this.#a,...e.blocks?{blocks:e.blocks}:{}},r=await this.#c().chat.postMessage(t);return this.#o?.info?.(`slack message sent`,{channel:r.channel,ts:r.ts}),{ok:r.ok??!1,channelId:r.channel??``,ts:r.ts??``}}catch(e){throw this.#l(e)}}#c(){if(!this.#t)throw new t(`Slack bot token is required to send messages`,`missing_token`);return this.#s??=new e.WebClient(this.#t),this.#s}#l(e){if(e instanceof t)return e;let n=e,r=n?.data?.error??n?.code??`unknown_error`,i=`Failed to send Slack message: ${r}`;return this.#o?.error?.(i,{code:r}),new t(i,r,{cause:e})}};function r(e){return new n(e)}exports.SlackNotifier=n,exports.SlackNotifierError=t,exports.createSlackNotifier=r;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["#enabled","#token","#defaultChannel","#defaultUsername","#defaultIconEmoji","#defaultIconUrl","#logger","#getClient","#wrapError","#client","WebClient"],"sources":["../src/errors.ts","../src/slack-notifier.ts"],"sourcesContent":["/**\n * Error thrown when sending a Slack message fails. Carries a machine-readable {@link code}\n * (the Slack platform error string where available, e.g. `channel_not_found`,\n * `not_in_channel`, `invalid_auth`) so callers can branch on it.\n */\nexport class SlackNotifierError extends Error {\n /** Slack platform error code (e.g. `channel_not_found`) or a local code (e.g. `missing_token`). */\n readonly code: string;\n\n constructor(message: string, code: string, options?: { cause?: unknown; }) {\n super(message);\n this.name = 'SlackNotifierError';\n this.code = code;\n if (options?.cause !== undefined) {\n (this as { cause?: unknown; }).cause = options.cause;\n }\n }\n}\n","import { WebClient } from '@slack/web-api';\nimport type { ChatPostMessageArguments } from '@slack/web-api';\nimport { SlackNotifierError } from './errors';\nimport type { Logger, SendMessageInput, SendMessageResult, SlackNotifierConfig } from './types';\n\n/**\n * Posts messages to Slack via the Web API (`chat.postMessage`), using `@slack/web-api`'s\n * `WebClient` (which handles retries, rate limits, and pagination).\n *\n * - The bot token is injected by the caller; the `WebClient` is constructed lazily on the\n * first real send, so a disabled notifier can be created without a token (e.g. in dev/test).\n * - Sending is opt-in: `enabled` defaults to `false`, so a notifier created without it is a\n * safe no-op. Set `enabled: true` (e.g. per environment) to actually post to Slack.\n *\n * @example\n * ```typescript\n * const slack = new SlackNotifier({ token: process.env.SLACK_BOT_TOKEN ?? '', enabled: true });\n * await slack.sendMessage({ channel: '#ops-alerts', text: 'Deploy finished ✅' });\n * ```\n */\nexport class SlackNotifier {\n readonly #enabled: boolean;\n readonly #token: string;\n readonly #defaultChannel: string | undefined;\n readonly #defaultUsername: string | undefined;\n readonly #defaultIconEmoji: string | undefined;\n readonly #defaultIconUrl: string | undefined;\n readonly #logger: Logger | undefined;\n #client: WebClient | undefined;\n\n constructor(config: SlackNotifierConfig) {\n this.#enabled = config.enabled ?? false;\n this.#token = config.token;\n this.#defaultChannel = config.defaultChannel;\n this.#defaultUsername = config.defaultUsername;\n this.#defaultIconEmoji = config.defaultIconEmoji;\n this.#defaultIconUrl = config.defaultIconUrl;\n this.#logger = config.logger;\n }\n\n /** Whether this notifier will actually send messages. */\n public get enabled(): boolean {\n return this.#enabled;\n }\n\n /**\n * Send a message to a Slack channel.\n *\n * When the notifier is disabled this resolves to `{ ok: true, skipped: true }` without\n * making any network call. Otherwise it posts via `chat.postMessage` and maps the\n * response, throwing a {@link SlackNotifierError} on failure.\n */\n public async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n const channel = input.channel ?? this.#defaultChannel;\n\n if (!this.#enabled) {\n this.#logger?.debug?.('slack notifier disabled, skipping message', { channel });\n return { ok: true, skipped: true, channelId: '', ts: '' };\n }\n\n if (!channel) {\n throw new SlackNotifierError(\n 'No channel provided and no defaultChannel configured',\n 'channel_required',\n );\n }\n\n try {\n // `ChatPostMessageArguments` is a discriminated union (text | blocks | attachments | …)\n // intersected with several other unions, which doesn't accept a conditionally-present\n // `blocks` key cleanly. We always supply `text` (and a validated `channel`), so the cast\n // is safe; `blocks` is only attached when provided to satisfy its non-optional variant.\n const args = {\n channel,\n text: input.text,\n // eslint-disable-next-line camelcase -- Slack API field\n thread_ts: input.threadTs,\n // username/icon_emoji/icon_url require the bot's `chat:write.customize` scope.\n username: input.username ?? this.#defaultUsername,\n // eslint-disable-next-line camelcase -- Slack API field\n icon_emoji: input.iconEmoji ?? this.#defaultIconEmoji,\n // eslint-disable-next-line camelcase -- Slack API field\n icon_url: input.iconUrl ?? this.#defaultIconUrl,\n ...(input.blocks ? { blocks: input.blocks } : {}),\n } as ChatPostMessageArguments;\n\n const response = await this.#getClient().chat.postMessage(args);\n\n this.#logger?.info?.('slack message sent', { channel: response.channel, ts: response.ts });\n\n return {\n ok: response.ok ?? false,\n channelId: response.channel ?? '',\n ts: response.ts ?? '',\n };\n } catch (error) {\n throw this.#wrapError(error);\n }\n }\n\n /** Lazily construct the WebClient, validating the token on first use. */\n #getClient(): WebClient {\n if (!this.#token) {\n throw new SlackNotifierError('Slack bot token is required to send messages', 'missing_token');\n }\n this.#client ??= new WebClient(this.#token);\n return this.#client;\n }\n\n /** Translate any thrown value into a typed {@link SlackNotifierError}. */\n #wrapError(error: unknown): SlackNotifierError {\n if (error instanceof SlackNotifierError) {\n return error;\n }\n\n // `@slack/web-api` platform errors expose the Slack error string at `data.error`\n // (e.g. `channel_not_found`); transport errors expose an `ErrorCode` at `code`.\n const err = error as { code?: string; data?: { error?: string; }; };\n const code = err?.data?.error ?? err?.code ?? 'unknown_error';\n const message = `Failed to send Slack message: ${code}`;\n\n this.#logger?.error?.(message, { code });\n\n return new SlackNotifierError(message, code, { cause: error });\n }\n}\n\n/** Convenience factory for {@link SlackNotifier}. */\nexport function createSlackNotifier(config: SlackNotifierConfig): SlackNotifier {\n return new SlackNotifier(config);\n}\n"],"mappings":"gCAKA,IAAa,EAAb,cAAwC,KAAM,CAI5C,YAAY,EAAiB,EAAc,EAAgC,CACzE,MAAM,EAAQ,CACd,KAAK,KAAO,qBACZ,KAAK,KAAO,EACR,GAAS,QAAU,IAAA,KACpB,KAA8B,MAAQ,EAAQ,SCMxC,EAAb,KAA2B,CACzB,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GAEA,YAAY,EAA6B,CACvC,MAAA,EAAgB,EAAO,SAAW,GAClC,MAAA,EAAc,EAAO,MACrB,MAAA,EAAuB,EAAO,eAC9B,MAAA,EAAwB,EAAO,gBAC/B,MAAA,EAAyB,EAAO,iBAChC,MAAA,EAAuB,EAAO,eAC9B,MAAA,EAAe,EAAO,OAIxB,IAAW,SAAmB,CAC5B,OAAO,MAAA,EAUT,MAAa,YAAY,EAAqD,CAC5E,IAAM,EAAU,EAAM,SAAW,MAAA,EAEjC,GAAI,CAAC,MAAA,EAEH,OADA,MAAA,GAAc,QAAQ,4CAA6C,CAAE,UAAS,CAAC,CACxE,CAAE,GAAI,GAAM,QAAS,GAAM,UAAW,GAAI,GAAI,GAAI,CAG3D,GAAI,CAAC,EACH,MAAM,IAAI,EACR,uDACA,mBACD,CAGH,GAAI,CAKF,IAAM,EAAO,CACX,UACA,KAAM,EAAM,KAEZ,UAAW,EAAM,SAEjB,SAAU,EAAM,UAAY,MAAA,EAE5B,WAAY,EAAM,WAAa,MAAA,EAE/B,SAAU,EAAM,SAAW,MAAA,EAC3B,GAAI,EAAM,OAAS,CAAE,OAAQ,EAAM,OAAQ,CAAG,EAAE,CACjD,CAEK,EAAW,MAAM,MAAA,GAAiB,CAAC,KAAK,YAAY,EAAK,CAI/D,OAFA,MAAA,GAAc,OAAO,qBAAsB,CAAE,QAAS,EAAS,QAAS,GAAI,EAAS,GAAI,CAAC,CAEnF,CACL,GAAI,EAAS,IAAM,GACnB,UAAW,EAAS,SAAW,GAC/B,GAAI,EAAS,IAAM,GACpB,OACM,EAAO,CACd,MAAM,MAAA,EAAgB,EAAM,EAKhC,IAAwB,CACtB,GAAI,CAAC,MAAA,EACH,MAAM,IAAI,EAAmB,+CAAgD,gBAAgB,CAG/F,MADA,OAAA,IAAiB,IAAIU,EAAAA,UAAU,MAAA,EAAY,CACpC,MAAA,EAIT,GAAW,EAAoC,CAC7C,GAAI,aAAiB,EACnB,OAAO,EAKT,IAAM,EAAM,EACN,EAAO,GAAK,MAAM,OAAS,GAAK,MAAQ,gBACxC,EAAU,iCAAiC,IAIjD,OAFA,MAAA,GAAc,QAAQ,EAAS,CAAE,OAAM,CAAC,CAEjC,IAAI,EAAmB,EAAS,EAAM,CAAE,MAAO,EAAO,CAAC,GAKlE,SAAgB,EAAoB,EAA4C,CAC9E,OAAO,IAAI,EAAc,EAAO"}
@@ -0,0 +1,142 @@
1
+ import { Block, KnownBlock } from "@slack/web-api";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * Minimal structural logger interface — compatible with `@autofleet/logger`'s
7
+ * `LoggerInstanceManager` but without coupling this package to it. Every method is
8
+ * optional so callers can pass a partial logger (or none at all).
9
+ */
10
+ interface Logger {
11
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
12
+ info?: (message: string, meta?: Record<string, unknown>) => void;
13
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
14
+ error?: (message: string, meta?: Record<string, unknown>) => void;
15
+ }
16
+ /**
17
+ * Configuration for a {@link SlackNotifier}. The bot token is injected by the caller —
18
+ * the package never reads `process.env` itself.
19
+ */
20
+ interface SlackNotifierConfig {
21
+ /** `xoxb-…` bot token, injected by the caller. Validated lazily on first real send. */
22
+ token: string;
23
+ /**
24
+ * Master on/off switch (default `false` — sending is opt-in). When `false`,
25
+ * {@link SlackNotifier.sendMessage} is a no-op: no network call is made and a token is not
26
+ * required. Set `enabled: true` to actually post, e.g. per environment with
27
+ * `enabled: process.env.NODE_ENV === 'production'`.
28
+ */
29
+ enabled?: boolean;
30
+ /** Optional fallback channel used when {@link SendMessageInput.channel} is omitted. */
31
+ defaultChannel?: string;
32
+ /**
33
+ * Optional default sender name shown for every message (overridable per call via
34
+ * {@link SendMessageInput.username}). Requires the bot's `chat:write.customize` scope.
35
+ */
36
+ defaultUsername?: string;
37
+ /**
38
+ * Optional default sender emoji icon, e.g. `":robot_face:"` (overridable per call via
39
+ * {@link SendMessageInput.iconEmoji}). Requires the bot's `chat:write.customize` scope.
40
+ */
41
+ defaultIconEmoji?: string;
42
+ /**
43
+ * Optional default sender profile picture as an image URL (overridable per call via
44
+ * {@link SendMessageInput.iconUrl}). Takes precedence over `defaultIconEmoji` when Slack
45
+ * receives both. Requires the bot's `chat:write.customize` scope.
46
+ */
47
+ defaultIconUrl?: string;
48
+ /** Optional logger for request/skip/error logging. */
49
+ logger?: Logger;
50
+ }
51
+ /** Input for a single message send. */
52
+ interface SendMessageInput {
53
+ /**
54
+ * Channel name or ID, e.g. `"#ops-alerts"` or `"C0123456"`. Optional only when a
55
+ * `defaultChannel` was configured.
56
+ */
57
+ channel?: string;
58
+ /** Message body. Always sent (also used as the fallback text when `blocks` is set). */
59
+ text: string;
60
+ /** Optional: reply inside a thread by its parent message `ts`. */
61
+ threadTs?: string;
62
+ /** Optional: Block Kit payload for rich formatting. */
63
+ blocks?: (KnownBlock | Block)[];
64
+ /**
65
+ * Optional: override the sender name shown for this message. Falls back to
66
+ * {@link SlackNotifierConfig.defaultUsername}. Requires the bot's `chat:write.customize` scope.
67
+ */
68
+ username?: string;
69
+ /**
70
+ * Optional: override the sender emoji icon for this message, e.g. `":rotating_light:"`.
71
+ * Falls back to {@link SlackNotifierConfig.defaultIconEmoji}. Requires the bot's
72
+ * `chat:write.customize` scope.
73
+ */
74
+ iconEmoji?: string;
75
+ /**
76
+ * Optional: override the sender profile picture for this message as an image URL.
77
+ * Falls back to {@link SlackNotifierConfig.defaultIconUrl}. Takes precedence over
78
+ * `iconEmoji` when Slack receives both. Requires the bot's `chat:write.customize` scope.
79
+ */
80
+ iconUrl?: string;
81
+ }
82
+ /** Result of a message send. */
83
+ interface SendMessageResult {
84
+ /** `true` when Slack accepted the message (or when the notifier was disabled). */
85
+ ok: boolean;
86
+ /** The resolved channel ID Slack posted to (empty when skipped). */
87
+ channelId: string;
88
+ /** The message timestamp / id within the channel (empty when skipped). */
89
+ ts: string;
90
+ /** `true` when the notifier was disabled and no API call was made. */
91
+ skipped?: boolean;
92
+ }
93
+ //#endregion
94
+ //#region src/slack-notifier.d.ts
95
+ /**
96
+ * Posts messages to Slack via the Web API (`chat.postMessage`), using `@slack/web-api`'s
97
+ * `WebClient` (which handles retries, rate limits, and pagination).
98
+ *
99
+ * - The bot token is injected by the caller; the `WebClient` is constructed lazily on the
100
+ * first real send, so a disabled notifier can be created without a token (e.g. in dev/test).
101
+ * - Sending is opt-in: `enabled` defaults to `false`, so a notifier created without it is a
102
+ * safe no-op. Set `enabled: true` (e.g. per environment) to actually post to Slack.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const slack = new SlackNotifier({ token: process.env.SLACK_BOT_TOKEN ?? '', enabled: true });
107
+ * await slack.sendMessage({ channel: '#ops-alerts', text: 'Deploy finished ✅' });
108
+ * ```
109
+ */
110
+ declare class SlackNotifier {
111
+ #private;
112
+ constructor(config: SlackNotifierConfig);
113
+ /** Whether this notifier will actually send messages. */
114
+ get enabled(): boolean;
115
+ /**
116
+ * Send a message to a Slack channel.
117
+ *
118
+ * When the notifier is disabled this resolves to `{ ok: true, skipped: true }` without
119
+ * making any network call. Otherwise it posts via `chat.postMessage` and maps the
120
+ * response, throwing a {@link SlackNotifierError} on failure.
121
+ */
122
+ sendMessage(input: SendMessageInput): Promise<SendMessageResult>;
123
+ }
124
+ /** Convenience factory for {@link SlackNotifier}. */
125
+ declare function createSlackNotifier(config: SlackNotifierConfig): SlackNotifier;
126
+ //#endregion
127
+ //#region src/errors.d.ts
128
+ /**
129
+ * Error thrown when sending a Slack message fails. Carries a machine-readable {@link code}
130
+ * (the Slack platform error string where available, e.g. `channel_not_found`,
131
+ * `not_in_channel`, `invalid_auth`) so callers can branch on it.
132
+ */
133
+ declare class SlackNotifierError extends Error {
134
+ /** Slack platform error code (e.g. `channel_not_found`) or a local code (e.g. `missing_token`). */
135
+ readonly code: string;
136
+ constructor(message: string, code: string, options?: {
137
+ cause?: unknown;
138
+ });
139
+ }
140
+ //#endregion
141
+ export { type Logger, type SendMessageInput, type SendMessageResult, SlackNotifier, type SlackNotifierConfig, SlackNotifierError, createSlackNotifier };
142
+ //# sourceMappingURL=index.d.cts.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { Block, KnownBlock } from "@slack/web-api";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * Minimal structural logger interface — compatible with `@autofleet/logger`'s
7
+ * `LoggerInstanceManager` but without coupling this package to it. Every method is
8
+ * optional so callers can pass a partial logger (or none at all).
9
+ */
10
+ interface Logger {
11
+ debug?: (message: string, meta?: Record<string, unknown>) => void;
12
+ info?: (message: string, meta?: Record<string, unknown>) => void;
13
+ warn?: (message: string, meta?: Record<string, unknown>) => void;
14
+ error?: (message: string, meta?: Record<string, unknown>) => void;
15
+ }
16
+ /**
17
+ * Configuration for a {@link SlackNotifier}. The bot token is injected by the caller —
18
+ * the package never reads `process.env` itself.
19
+ */
20
+ interface SlackNotifierConfig {
21
+ /** `xoxb-…` bot token, injected by the caller. Validated lazily on first real send. */
22
+ token: string;
23
+ /**
24
+ * Master on/off switch (default `false` — sending is opt-in). When `false`,
25
+ * {@link SlackNotifier.sendMessage} is a no-op: no network call is made and a token is not
26
+ * required. Set `enabled: true` to actually post, e.g. per environment with
27
+ * `enabled: process.env.NODE_ENV === 'production'`.
28
+ */
29
+ enabled?: boolean;
30
+ /** Optional fallback channel used when {@link SendMessageInput.channel} is omitted. */
31
+ defaultChannel?: string;
32
+ /**
33
+ * Optional default sender name shown for every message (overridable per call via
34
+ * {@link SendMessageInput.username}). Requires the bot's `chat:write.customize` scope.
35
+ */
36
+ defaultUsername?: string;
37
+ /**
38
+ * Optional default sender emoji icon, e.g. `":robot_face:"` (overridable per call via
39
+ * {@link SendMessageInput.iconEmoji}). Requires the bot's `chat:write.customize` scope.
40
+ */
41
+ defaultIconEmoji?: string;
42
+ /**
43
+ * Optional default sender profile picture as an image URL (overridable per call via
44
+ * {@link SendMessageInput.iconUrl}). Takes precedence over `defaultIconEmoji` when Slack
45
+ * receives both. Requires the bot's `chat:write.customize` scope.
46
+ */
47
+ defaultIconUrl?: string;
48
+ /** Optional logger for request/skip/error logging. */
49
+ logger?: Logger;
50
+ }
51
+ /** Input for a single message send. */
52
+ interface SendMessageInput {
53
+ /**
54
+ * Channel name or ID, e.g. `"#ops-alerts"` or `"C0123456"`. Optional only when a
55
+ * `defaultChannel` was configured.
56
+ */
57
+ channel?: string;
58
+ /** Message body. Always sent (also used as the fallback text when `blocks` is set). */
59
+ text: string;
60
+ /** Optional: reply inside a thread by its parent message `ts`. */
61
+ threadTs?: string;
62
+ /** Optional: Block Kit payload for rich formatting. */
63
+ blocks?: (KnownBlock | Block)[];
64
+ /**
65
+ * Optional: override the sender name shown for this message. Falls back to
66
+ * {@link SlackNotifierConfig.defaultUsername}. Requires the bot's `chat:write.customize` scope.
67
+ */
68
+ username?: string;
69
+ /**
70
+ * Optional: override the sender emoji icon for this message, e.g. `":rotating_light:"`.
71
+ * Falls back to {@link SlackNotifierConfig.defaultIconEmoji}. Requires the bot's
72
+ * `chat:write.customize` scope.
73
+ */
74
+ iconEmoji?: string;
75
+ /**
76
+ * Optional: override the sender profile picture for this message as an image URL.
77
+ * Falls back to {@link SlackNotifierConfig.defaultIconUrl}. Takes precedence over
78
+ * `iconEmoji` when Slack receives both. Requires the bot's `chat:write.customize` scope.
79
+ */
80
+ iconUrl?: string;
81
+ }
82
+ /** Result of a message send. */
83
+ interface SendMessageResult {
84
+ /** `true` when Slack accepted the message (or when the notifier was disabled). */
85
+ ok: boolean;
86
+ /** The resolved channel ID Slack posted to (empty when skipped). */
87
+ channelId: string;
88
+ /** The message timestamp / id within the channel (empty when skipped). */
89
+ ts: string;
90
+ /** `true` when the notifier was disabled and no API call was made. */
91
+ skipped?: boolean;
92
+ }
93
+ //#endregion
94
+ //#region src/slack-notifier.d.ts
95
+ /**
96
+ * Posts messages to Slack via the Web API (`chat.postMessage`), using `@slack/web-api`'s
97
+ * `WebClient` (which handles retries, rate limits, and pagination).
98
+ *
99
+ * - The bot token is injected by the caller; the `WebClient` is constructed lazily on the
100
+ * first real send, so a disabled notifier can be created without a token (e.g. in dev/test).
101
+ * - Sending is opt-in: `enabled` defaults to `false`, so a notifier created without it is a
102
+ * safe no-op. Set `enabled: true` (e.g. per environment) to actually post to Slack.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const slack = new SlackNotifier({ token: process.env.SLACK_BOT_TOKEN ?? '', enabled: true });
107
+ * await slack.sendMessage({ channel: '#ops-alerts', text: 'Deploy finished ✅' });
108
+ * ```
109
+ */
110
+ declare class SlackNotifier {
111
+ #private;
112
+ constructor(config: SlackNotifierConfig);
113
+ /** Whether this notifier will actually send messages. */
114
+ get enabled(): boolean;
115
+ /**
116
+ * Send a message to a Slack channel.
117
+ *
118
+ * When the notifier is disabled this resolves to `{ ok: true, skipped: true }` without
119
+ * making any network call. Otherwise it posts via `chat.postMessage` and maps the
120
+ * response, throwing a {@link SlackNotifierError} on failure.
121
+ */
122
+ sendMessage(input: SendMessageInput): Promise<SendMessageResult>;
123
+ }
124
+ /** Convenience factory for {@link SlackNotifier}. */
125
+ declare function createSlackNotifier(config: SlackNotifierConfig): SlackNotifier;
126
+ //#endregion
127
+ //#region src/errors.d.ts
128
+ /**
129
+ * Error thrown when sending a Slack message fails. Carries a machine-readable {@link code}
130
+ * (the Slack platform error string where available, e.g. `channel_not_found`,
131
+ * `not_in_channel`, `invalid_auth`) so callers can branch on it.
132
+ */
133
+ declare class SlackNotifierError extends Error {
134
+ /** Slack platform error code (e.g. `channel_not_found`) or a local code (e.g. `missing_token`). */
135
+ readonly code: string;
136
+ constructor(message: string, code: string, options?: {
137
+ cause?: unknown;
138
+ });
139
+ }
140
+ //#endregion
141
+ export { type Logger, type SendMessageInput, type SendMessageResult, SlackNotifier, type SlackNotifierConfig, SlackNotifierError, createSlackNotifier };
142
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{WebClient as e}from"@slack/web-api";var t=class extends Error{constructor(e,t,n){super(e),this.name=`SlackNotifierError`,this.code=t,n?.cause!==void 0&&(this.cause=n.cause)}},n=class{#e;#t;#n;#r;#i;#a;#o;#s;constructor(e){this.#e=e.enabled??!1,this.#t=e.token,this.#n=e.defaultChannel,this.#r=e.defaultUsername,this.#i=e.defaultIconEmoji,this.#a=e.defaultIconUrl,this.#o=e.logger}get enabled(){return this.#e}async sendMessage(e){let n=e.channel??this.#n;if(!this.#e)return this.#o?.debug?.(`slack notifier disabled, skipping message`,{channel:n}),{ok:!0,skipped:!0,channelId:``,ts:``};if(!n)throw new t(`No channel provided and no defaultChannel configured`,`channel_required`);try{let t={channel:n,text:e.text,thread_ts:e.threadTs,username:e.username??this.#r,icon_emoji:e.iconEmoji??this.#i,icon_url:e.iconUrl??this.#a,...e.blocks?{blocks:e.blocks}:{}},r=await this.#c().chat.postMessage(t);return this.#o?.info?.(`slack message sent`,{channel:r.channel,ts:r.ts}),{ok:r.ok??!1,channelId:r.channel??``,ts:r.ts??``}}catch(e){throw this.#l(e)}}#c(){if(!this.#t)throw new t(`Slack bot token is required to send messages`,`missing_token`);return this.#s??=new e(this.#t),this.#s}#l(e){if(e instanceof t)return e;let n=e,r=n?.data?.error??n?.code??`unknown_error`,i=`Failed to send Slack message: ${r}`;return this.#o?.error?.(i,{code:r}),new t(i,r,{cause:e})}};function r(e){return new n(e)}export{n as SlackNotifier,t as SlackNotifierError,r as createSlackNotifier};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#enabled","#token","#defaultChannel","#defaultUsername","#defaultIconEmoji","#defaultIconUrl","#logger","#getClient","#wrapError","#client"],"sources":["../src/errors.ts","../src/slack-notifier.ts"],"sourcesContent":["/**\n * Error thrown when sending a Slack message fails. Carries a machine-readable {@link code}\n * (the Slack platform error string where available, e.g. `channel_not_found`,\n * `not_in_channel`, `invalid_auth`) so callers can branch on it.\n */\nexport class SlackNotifierError extends Error {\n /** Slack platform error code (e.g. `channel_not_found`) or a local code (e.g. `missing_token`). */\n readonly code: string;\n\n constructor(message: string, code: string, options?: { cause?: unknown; }) {\n super(message);\n this.name = 'SlackNotifierError';\n this.code = code;\n if (options?.cause !== undefined) {\n (this as { cause?: unknown; }).cause = options.cause;\n }\n }\n}\n","import { WebClient } from '@slack/web-api';\nimport type { ChatPostMessageArguments } from '@slack/web-api';\nimport { SlackNotifierError } from './errors';\nimport type { Logger, SendMessageInput, SendMessageResult, SlackNotifierConfig } from './types';\n\n/**\n * Posts messages to Slack via the Web API (`chat.postMessage`), using `@slack/web-api`'s\n * `WebClient` (which handles retries, rate limits, and pagination).\n *\n * - The bot token is injected by the caller; the `WebClient` is constructed lazily on the\n * first real send, so a disabled notifier can be created without a token (e.g. in dev/test).\n * - Sending is opt-in: `enabled` defaults to `false`, so a notifier created without it is a\n * safe no-op. Set `enabled: true` (e.g. per environment) to actually post to Slack.\n *\n * @example\n * ```typescript\n * const slack = new SlackNotifier({ token: process.env.SLACK_BOT_TOKEN ?? '', enabled: true });\n * await slack.sendMessage({ channel: '#ops-alerts', text: 'Deploy finished ✅' });\n * ```\n */\nexport class SlackNotifier {\n readonly #enabled: boolean;\n readonly #token: string;\n readonly #defaultChannel: string | undefined;\n readonly #defaultUsername: string | undefined;\n readonly #defaultIconEmoji: string | undefined;\n readonly #defaultIconUrl: string | undefined;\n readonly #logger: Logger | undefined;\n #client: WebClient | undefined;\n\n constructor(config: SlackNotifierConfig) {\n this.#enabled = config.enabled ?? false;\n this.#token = config.token;\n this.#defaultChannel = config.defaultChannel;\n this.#defaultUsername = config.defaultUsername;\n this.#defaultIconEmoji = config.defaultIconEmoji;\n this.#defaultIconUrl = config.defaultIconUrl;\n this.#logger = config.logger;\n }\n\n /** Whether this notifier will actually send messages. */\n public get enabled(): boolean {\n return this.#enabled;\n }\n\n /**\n * Send a message to a Slack channel.\n *\n * When the notifier is disabled this resolves to `{ ok: true, skipped: true }` without\n * making any network call. Otherwise it posts via `chat.postMessage` and maps the\n * response, throwing a {@link SlackNotifierError} on failure.\n */\n public async sendMessage(input: SendMessageInput): Promise<SendMessageResult> {\n const channel = input.channel ?? this.#defaultChannel;\n\n if (!this.#enabled) {\n this.#logger?.debug?.('slack notifier disabled, skipping message', { channel });\n return { ok: true, skipped: true, channelId: '', ts: '' };\n }\n\n if (!channel) {\n throw new SlackNotifierError(\n 'No channel provided and no defaultChannel configured',\n 'channel_required',\n );\n }\n\n try {\n // `ChatPostMessageArguments` is a discriminated union (text | blocks | attachments | …)\n // intersected with several other unions, which doesn't accept a conditionally-present\n // `blocks` key cleanly. We always supply `text` (and a validated `channel`), so the cast\n // is safe; `blocks` is only attached when provided to satisfy its non-optional variant.\n const args = {\n channel,\n text: input.text,\n // eslint-disable-next-line camelcase -- Slack API field\n thread_ts: input.threadTs,\n // username/icon_emoji/icon_url require the bot's `chat:write.customize` scope.\n username: input.username ?? this.#defaultUsername,\n // eslint-disable-next-line camelcase -- Slack API field\n icon_emoji: input.iconEmoji ?? this.#defaultIconEmoji,\n // eslint-disable-next-line camelcase -- Slack API field\n icon_url: input.iconUrl ?? this.#defaultIconUrl,\n ...(input.blocks ? { blocks: input.blocks } : {}),\n } as ChatPostMessageArguments;\n\n const response = await this.#getClient().chat.postMessage(args);\n\n this.#logger?.info?.('slack message sent', { channel: response.channel, ts: response.ts });\n\n return {\n ok: response.ok ?? false,\n channelId: response.channel ?? '',\n ts: response.ts ?? '',\n };\n } catch (error) {\n throw this.#wrapError(error);\n }\n }\n\n /** Lazily construct the WebClient, validating the token on first use. */\n #getClient(): WebClient {\n if (!this.#token) {\n throw new SlackNotifierError('Slack bot token is required to send messages', 'missing_token');\n }\n this.#client ??= new WebClient(this.#token);\n return this.#client;\n }\n\n /** Translate any thrown value into a typed {@link SlackNotifierError}. */\n #wrapError(error: unknown): SlackNotifierError {\n if (error instanceof SlackNotifierError) {\n return error;\n }\n\n // `@slack/web-api` platform errors expose the Slack error string at `data.error`\n // (e.g. `channel_not_found`); transport errors expose an `ErrorCode` at `code`.\n const err = error as { code?: string; data?: { error?: string; }; };\n const code = err?.data?.error ?? err?.code ?? 'unknown_error';\n const message = `Failed to send Slack message: ${code}`;\n\n this.#logger?.error?.(message, { code });\n\n return new SlackNotifierError(message, code, { cause: error });\n }\n}\n\n/** Convenience factory for {@link SlackNotifier}. */\nexport function createSlackNotifier(config: SlackNotifierConfig): SlackNotifier {\n return new SlackNotifier(config);\n}\n"],"mappings":"2CAKA,IAAa,EAAb,cAAwC,KAAM,CAI5C,YAAY,EAAiB,EAAc,EAAgC,CACzE,MAAM,EAAQ,CACd,KAAK,KAAO,qBACZ,KAAK,KAAO,EACR,GAAS,QAAU,IAAA,KACpB,KAA8B,MAAQ,EAAQ,SCMxC,EAAb,KAA2B,CACzB,GACA,GACA,GACA,GACA,GACA,GACA,GACA,GAEA,YAAY,EAA6B,CACvC,MAAA,EAAgB,EAAO,SAAW,GAClC,MAAA,EAAc,EAAO,MACrB,MAAA,EAAuB,EAAO,eAC9B,MAAA,EAAwB,EAAO,gBAC/B,MAAA,EAAyB,EAAO,iBAChC,MAAA,EAAuB,EAAO,eAC9B,MAAA,EAAe,EAAO,OAIxB,IAAW,SAAmB,CAC5B,OAAO,MAAA,EAUT,MAAa,YAAY,EAAqD,CAC5E,IAAM,EAAU,EAAM,SAAW,MAAA,EAEjC,GAAI,CAAC,MAAA,EAEH,OADA,MAAA,GAAc,QAAQ,4CAA6C,CAAE,UAAS,CAAC,CACxE,CAAE,GAAI,GAAM,QAAS,GAAM,UAAW,GAAI,GAAI,GAAI,CAG3D,GAAI,CAAC,EACH,MAAM,IAAI,EACR,uDACA,mBACD,CAGH,GAAI,CAKF,IAAM,EAAO,CACX,UACA,KAAM,EAAM,KAEZ,UAAW,EAAM,SAEjB,SAAU,EAAM,UAAY,MAAA,EAE5B,WAAY,EAAM,WAAa,MAAA,EAE/B,SAAU,EAAM,SAAW,MAAA,EAC3B,GAAI,EAAM,OAAS,CAAE,OAAQ,EAAM,OAAQ,CAAG,EAAE,CACjD,CAEK,EAAW,MAAM,MAAA,GAAiB,CAAC,KAAK,YAAY,EAAK,CAI/D,OAFA,MAAA,GAAc,OAAO,qBAAsB,CAAE,QAAS,EAAS,QAAS,GAAI,EAAS,GAAI,CAAC,CAEnF,CACL,GAAI,EAAS,IAAM,GACnB,UAAW,EAAS,SAAW,GAC/B,GAAI,EAAS,IAAM,GACpB,OACM,EAAO,CACd,MAAM,MAAA,EAAgB,EAAM,EAKhC,IAAwB,CACtB,GAAI,CAAC,MAAA,EACH,MAAM,IAAI,EAAmB,+CAAgD,gBAAgB,CAG/F,MADA,OAAA,IAAiB,IAAI,EAAU,MAAA,EAAY,CACpC,MAAA,EAIT,GAAW,EAAoC,CAC7C,GAAI,aAAiB,EACnB,OAAO,EAKT,IAAM,EAAM,EACN,EAAO,GAAK,MAAM,OAAS,GAAK,MAAQ,gBACxC,EAAU,iCAAiC,IAIjD,OAFA,MAAA,GAAc,QAAQ,EAAS,CAAE,OAAM,CAAC,CAEjC,IAAI,EAAmB,EAAS,EAAM,CAAE,MAAO,EAAO,CAAC,GAKlE,SAAgB,EAAoB,EAA4C,CAC9E,OAAO,IAAI,EAAc,EAAO"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@autofleet/slack-notifier",
3
+ "version": "0.1.2-beta-740ca7cd.0",
4
+ "description": "Send messages to Slack channels via the Web API",
5
+ "type": "module",
6
+ "main": "./lib/index.cjs",
7
+ "module": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./lib/index.d.ts",
13
+ "default": "./lib/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./lib/index.d.cts",
17
+ "default": "./lib/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "lib"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "license": "Proprietary",
28
+ "dependencies": {
29
+ "@slack/web-api": "^7.8.0"
30
+ },
31
+ "peerDependencies": {
32
+ "@autofleet/logger": ">=4.2.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@autofleet/logger": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.9.4",
41
+ "nock": "^14.0.1",
42
+ "@autofleet/logger": "^4.6.1"
43
+ },
44
+ "scripts": {
45
+ "test": "vitest",
46
+ "coverage": "vitest --coverage",
47
+ "build": "pnpm -w tsdown -W ./packages/slack-notifier"
48
+ }
49
+ }