@maroonedsoftware/slack 3.0.0 → 3.0.4

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/AGENTS.md ADDED
@@ -0,0 +1,206 @@
1
+ # AGENTS.md — @maroonedsoftware/slack
2
+
3
+ Machine-oriented guide for AI agents. Human prose and long-form examples live in [README.md](./README.md).
4
+ Repo-wide conventions live in the [root AGENTS.md](../../AGENTS.md).
5
+
6
+ ## Purpose
7
+
8
+ A Slack dispatcher for ServerKit: signature verification as a policy, DI-registered handler maps for
9
+ events, slash commands, and interactive payloads, a `SlackClient` over `@slack/web-api`, and an
10
+ optional adapter that binds it all to the channel-agnostic `@maroonedsoftware/comms` router.
11
+
12
+ Reach for the native handler maps when you want Slack-specific richness (Block Kit modals,
13
+ `view_submission`). Reach for `./comms` when you want one bot that also runs on Discord, Telegram,
14
+ and WhatsApp. The two coexist: `./comms` normalises the common cases and leaves the rest on the
15
+ native maps.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @maroonedsoftware/slack
21
+ pnpm add @maroonedsoftware/comms # only for the ./comms adapter
22
+ pnpm add @maroonedsoftware/cache # only for idempotency
23
+ ```
24
+
25
+ Runtime dependencies: `@maroonedsoftware/errors`, `@maroonedsoftware/logger`,
26
+ `@maroonedsoftware/policies`, `@slack/web-api`, `injectkit`, `luxon`. Optional peers:
27
+ `@maroonedsoftware/comms`, `@maroonedsoftware/cache`.
28
+
29
+ ## Position in the graph
30
+
31
+ - **Depends on:** `errors`, `logger`, `policies`. `comms` and `cache` are **optional** peers.
32
+ - **Depended on by:** nothing internal.
33
+ - **Subpath exports:**
34
+ - `.` — config, errors, signature verification, handler maps, dispatcher, client.
35
+ - `./comms` — the adapter. Pulls in `@maroonedsoftware/comms`. It lives here, not in `comms`,
36
+ because `comms` must stay channel-free; see the root AGENTS.md.
37
+
38
+ **Not a dependency: `koa`.** Your route parses the request and calls the dispatcher.
39
+
40
+ ## API surface
41
+
42
+ ### `.` — config and errors
43
+
44
+ | Export | Kind | Shape | Notes |
45
+ | -------------- | -------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
46
+ | `SlackConfig` | interface + abstract class | `{ botToken, signingSecret, incomingWebhookUrl?, signatureMaxAgeSeconds?, requestTimeoutMs? }` | Declaration-merged so one symbol is type and DI token. |
47
+ | `SlackError` | class | `extends ServerkitError` | — |
48
+ | `IsSlackError` | type guard | `(error: unknown) => error is SlackError` | — |
49
+
50
+ ### `.` — signature verification
51
+
52
+ | Export | Kind | Shape | Notes |
53
+ | ----------------------------------------- | --------- | ----------------------------------------------------------- | ------------------------------------------ |
54
+ | `verifySlackSignature` | function | `(input: VerifySlackSignatureInput) => void` | Pure. **Throws** `SlackError` on failure. |
55
+ | `VerifySlackSignatureInput` | type | Raw body, headers, and `SlackSignatureOptions` | — |
56
+ | `SlackSignatureOptions` | type | Signing secret plus max age | — |
57
+ | `SlackSignatureFailureReason` | type | Reason codes landing in `internalDetails.reason` | — |
58
+ | `SlackSignaturePolicy` | class | `extends Policy<SlackSignaturePolicyContext>` | Policy form — denies rather than throwing. |
59
+ | `SlackSignaturePolicyContext` | interface | Structurally compatible with koa's `SignaturePolicyContext` | What lets `requireSignature` drive it. |
60
+ | `SLACK_SIGNATURE_POLICY` | constant | The `PolicyRegistryMap` key | — |
61
+ | `SLACK_SIGNATURE_HEADER` | constant | `'X-Slack-Signature'` | — |
62
+ | `SLACK_REQUEST_TIMESTAMP_HEADER` | constant | `'X-Slack-Request-Timestamp'` | — |
63
+ | `SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS` | constant | Replay window | — |
64
+
65
+ ### `.` — handlers and dispatch
66
+
67
+ | Export | Kind | Shape | Notes |
68
+ | ---------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
69
+ | `SlackEventHandler<TEvent>` | interface | `handle(event, context: SlackEventContext)` | — |
70
+ | `SlackEventHandlerMap` | class | `extends Map<string, SlackEventHandler>` | Keyed by event type (`app_mention`, `message`). |
71
+ | `SlackCommandHandler` | interface | `handle(payload: SlackCommandPayload)` | — |
72
+ | `SlackCommandHandlerMap` | class | `extends Map<string, SlackCommandHandler>` | Keyed by command name. |
73
+ | `SlackInteractionHandler` | interface | `handle(payload: SlackInteractionPayload)` | — |
74
+ | `SlackInteractionHandlerMap` | class | `extends Map<string, SlackInteractionHandler>` | Keyed by `interactionRouteKey(payload)`. |
75
+ | `interactionRouteKey` | function | `(payload) => string \| undefined` | `block_actions:<action_id>`, `view_submission:<callback_id>`, … |
76
+ | `SlackDispatcher` | class | `dispatchEvent(body, options?)`, `dispatchCommand(payload)`, `dispatchInteraction(payload)` | The entry point. |
77
+ | `slackEventIdempotencyKey` | function | `(envelope: Pick<SlackEventCallback, 'event_id' \| 'team_id'>) => string` | Team-scoped, so ids are unique across workspaces. |
78
+ | Payload types | — | `SlackEventsRequest`, `SlackEventsResponse`, `SlackEventCallback`, `SlackEventContext`, `SlackCommandPayload`, `SlackCommandResponse`, `SlackInteractionPayload`, `SlackInteractionResponse`, `SlackInteractionType`, `IncomingWebhookPayload` | — |
79
+
80
+ `dispatchEvent(body, { idempotency })` wraps the handler in `IdempotencyStore.deduplicate` keyed by
81
+ `slackEventIdempotencyKey`. Slack redelivers events, so this is a real redelivery net, not just a
82
+ guard.
83
+
84
+ ### `.` — client
85
+
86
+ | Export | Kind | Shape | Notes |
87
+ | ---------------------------------- | -------- | --------------------------------------------------------------- | -------------------------------------- |
88
+ | `SlackClient` | class | `postMessage`, `postWebhook`, … | Over `@slack/web-api`. |
89
+ | `adaptLogger` | function | Bridges `@maroonedsoftware/logger` to `@slack/web-api`'s logger | — |
90
+ | `redactSlackUrl` | function | Strips the token from a webhook URL before logging | Use it before logging any webhook URL. |
91
+ | `SLACK_DEFAULT_REQUEST_TIMEOUT_MS` | constant | — | — |
92
+
93
+ ### `./comms`
94
+
95
+ | Export | Kind | Shape | Notes |
96
+ | -------------------------- | -------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
97
+ | `createSlackNotifier` | function | `(client: SlackClient, templates: TemplateRegistry) => Notifier` | Recipient is a `response_url` (webhook) **or** a channel id. |
98
+ | `dispatchSlackEvent` | function | `(router, client, body) => Promise<{ challenge: string } \| undefined>` | Returns the `url_verification` challenge; routes `message`/`app_mention`. |
99
+ | `dispatchSlackCommand` | function | `(router, client, payload) => Promise<void>` | Replies via `response_url` when present. |
100
+ | `dispatchSlackInteraction` | function | `(router, client, payload) => Promise<void>` | **Only `block_actions`** is normalised. |
101
+
102
+ ## Canonical usage
103
+
104
+ ```typescript
105
+ import {
106
+ SlackConfig,
107
+ SlackDispatcher,
108
+ SlackEventHandlerMap,
109
+ SlackSignaturePolicy,
110
+ SLACK_SIGNATURE_POLICY,
111
+ type SlackSignatureOptions,
112
+ } from '@maroonedsoftware/slack';
113
+
114
+ // Composition root
115
+ registry.register(SlackConfig).useValue(appConfig.getAs<SlackConfig>('slack'));
116
+ const events = new SlackEventHandlerMap();
117
+ events.set('app_mention', container.get(MentionHandler));
118
+ registry.register(SlackEventHandlerMap).useValue(events);
119
+ policies.set(SLACK_SIGNATURE_POLICY, SlackSignaturePolicy);
120
+
121
+ // Route — signature first, then dispatch
122
+ router.post('/slack/events', requireSignature<SlackSignatureOptions>('slack', { policy: SLACK_SIGNATURE_POLICY }), async ctx => {
123
+ const body = JSON.parse(ctx.rawBody as string);
124
+ ctx.body = await ctx.container.get(SlackDispatcher).dispatchEvent(body, { idempotency: ctx.container.get(IdempotencyStore) });
125
+ });
126
+ ```
127
+
128
+ Channel-agnostic instead:
129
+
130
+ ```typescript
131
+ import { dispatchSlackCommand, createSlackNotifier } from '@maroonedsoftware/slack/comms';
132
+
133
+ await dispatchSlackCommand(router, client, payload);
134
+ ```
135
+
136
+ ## Rules for generated code
137
+
138
+ - Verify the signature **before** parsing or dispatching, using `requireSignature` with
139
+ `SLACK_SIGNATURE_POLICY`. The verification needs `ctx.rawBody`, so it must run before anything
140
+ that re-serialises the body.
141
+ - Store `SlackConfig` in `AppConfig` and register the typed section. Never inline `signingSecret` or
142
+ `botToken`.
143
+ - Pass an `IdempotencyStore` to `dispatchEvent`. Slack retries deliveries, so without it a flaky
144
+ handler produces duplicate side effects.
145
+ - Scope idempotency keys by team — `slackEventIdempotencyKey` already does, and a raw `event_id` is
146
+ not unique across workspaces.
147
+ - Handle the `url_verification` challenge. `dispatchSlackEvent` returns it; the native path expects
148
+ you to echo it.
149
+ - Ack within 3 seconds and do slow work in a job. Slack times out and retries.
150
+ - Never log a webhook URL without `redactSlackUrl` — the token is in the path.
151
+ - Import `./comms` functions from `@maroonedsoftware/slack/comms`, never from the root.
152
+
153
+ ## Gotchas
154
+
155
+ - **`./comms` normalises only part of the surface.** `dispatchSlackInteraction` handles
156
+ `block_actions` and returns silently for everything else; `view_submission` and `view_closed` stay
157
+ on `SlackInteractionHandlerMap`. Mixing the two paths is expected, not a mistake.
158
+ - **The comms adapter sanitises broadcast sequences.** `<!everyone>`, `<!channel>`, `<!here>` (and
159
+ their `<!channel|label>` forms) in outbound text are rewritten to literal `@everyone` / `@channel`
160
+ / `@here` so user-supplied text cannot ping a workspace. The **native** `SlackClient` path does
161
+ **not** do this — sanitise yourself there.
162
+ - **The recipient string is overloaded.** `createSlackNotifier` sends to a `response_url` when the
163
+ string starts with `http`, and to a channel id otherwise. A channel id that somehow starts with
164
+ `http` would be misrouted.
165
+ - **Bot messages are filtered out** of the comms path (`bot_id`, `subtype: 'bot_message'`) to avoid
166
+ loops. The native event handlers see them.
167
+ - **`verifySlackSignature` throws; `SlackSignaturePolicy` denies.** Same logic, two shapes.
168
+ - **The signature has a max-age replay window.** Clock skew on your host produces spurious
169
+ verification failures.
170
+ - **`@slack/web-api` is a hard dependency**, unlike `comms` and `cache`. Installing this package
171
+ pulls it in even if you only use the comms path.
172
+
173
+ ## Working inside this package
174
+
175
+ ```
176
+ src/
177
+ index.ts Root barrel
178
+ slack.config.ts SlackConfig (interface + token)
179
+ slack.error.ts SlackError, IsSlackError
180
+ slack.signature.ts verifySlackSignature + header/constant exports
181
+ slack.signature.policy.ts SlackSignaturePolicy, SLACK_SIGNATURE_POLICY
182
+ slack.event.handler.ts Event handler + map, slackEventIdempotencyKey
183
+ slack.command.handler.ts Command handler + map
184
+ slack.interaction.handler.ts Interaction handler + map, interactionRouteKey
185
+ slack.dispatcher.ts SlackDispatcher
186
+ client/slack.client.ts SlackClient
187
+ client/slack.logger.adapter.ts adaptLogger, redactSlackUrl
188
+ comms.ts Subpath entry — notifier, render, and the three dispatch functions
189
+ ```
190
+
191
+ Tests are in `tests/`, mirroring `src/`.
192
+
193
+ Invariants a change must not break:
194
+
195
+ - **Nothing reachable from `src/index.ts` may import `@maroonedsoftware/comms` or
196
+ `@maroonedsoftware/cache`.** Both are optional peers; `cache` is imported `type`-only in the
197
+ dispatcher for exactly this reason.
198
+ - No dependency on `@maroonedsoftware/koa`. `SlackSignaturePolicyContext` is structurally
199
+ compatible with koa's context, which is what keeps the arrow out.
200
+ - Signature comparison stays constant-time, and the replay window stays enforced.
201
+ - The comms adapter's broadcast sanitisation is a security control, not formatting.
202
+ - The four chat packages (`slack`, `discord`, `telegram`, `whatsapp`) share a deliberate shape:
203
+ config token, error + guard, verification function + policy, handler maps, dispatcher, client,
204
+ `./comms` adapter. Keep a change consistent across all four.
205
+
206
+ User-visible changes need a changeset in `.changeset/`.
package/README.md CHANGED
@@ -15,19 +15,19 @@ pnpm add @maroonedsoftware/slack
15
15
 
16
16
  ## Exports
17
17
 
18
- | Symbol | Purpose |
19
- |---------------------------------|---------------------------------------------------------------------------------------------------------------|
20
- | `SlackConfig` | Abstract `@Injectable()` token; carries `botToken`, `signingSecret`, optional `incomingWebhookUrl`, optional `signatureMaxAgeSeconds`. Consumer registers a concrete value. |
21
- | `SlackClient` | Wraps `@slack/web-api`'s `WebClient`; routes its diagnostics through ServerKit's `Logger`. Methods: `postMessage`, `updateMessage`, `deleteMessage`, `openView`, `postWebhook`. Underlying SDK reachable at `.web`. |
22
- | `SlackDispatcher` | Three-method service: `dispatchEvent`, `dispatchCommand`, `dispatchInteraction`. |
23
- | `SlackEventHandlerMap` | `Map<eventType, SlackEventHandler>` — register one handler per Slack event type (`app_mention`, `message`, …). |
24
- | `SlackCommandHandlerMap` | `Map<commandKeyword, SlackCommandHandler>` — register one handler per slash command (`/deploy`, …). |
25
- | `SlackInteractionHandlerMap` | `Map<routingKey, SlackInteractionHandler>` — keys are `${type}:${identifier}`; see [interaction routing](#interaction-routing). |
26
- | `SlackError` | `ServerkitError` subclass for non-HTTP domain failures (signature mismatch, webhook POST failed, …). |
27
- | `verifySlackSignature(input)` | Pure helper that validates Slack's v0 HMAC scheme + replay window. No request/context coupling. |
28
- | `SlackSignaturePolicy` | `@maroonedsoftware/policies` form of `verifySlackSignature` (registered under `SLACK_SIGNATURE_POLICY`). Delegates to the helper but answers as a `PolicyResult`, so it slots into ServerKit's policy pipeline. |
29
- | `interactionRouteKey(payload)` | Helper that produces the `SlackInteractionHandlerMap` key for a given payload. |
30
- | `slackEventIdempotencyKey(envelope)` | Pure helper that derives a stable de-dup key (`slack:event:{team_id}:{event_id}`) for an `event_callback` envelope. |
18
+ | Symbol | Purpose |
19
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `SlackConfig` | Abstract `@Injectable()` token; carries `botToken`, `signingSecret`, optional `incomingWebhookUrl`, optional `signatureMaxAgeSeconds`. Consumer registers a concrete value. |
21
+ | `SlackClient` | Wraps `@slack/web-api`'s `WebClient`; routes its diagnostics through ServerKit's `Logger`. Methods: `postMessage`, `updateMessage`, `deleteMessage`, `openView`, `postWebhook`. Underlying SDK reachable at `.web`. |
22
+ | `SlackDispatcher` | Three-method service: `dispatchEvent`, `dispatchCommand`, `dispatchInteraction`. |
23
+ | `SlackEventHandlerMap` | `Map<eventType, SlackEventHandler>` — register one handler per Slack event type (`app_mention`, `message`, …). |
24
+ | `SlackCommandHandlerMap` | `Map<commandKeyword, SlackCommandHandler>` — register one handler per slash command (`/deploy`, …). |
25
+ | `SlackInteractionHandlerMap` | `Map<routingKey, SlackInteractionHandler>` — keys are `${type}:${identifier}`; see [interaction routing](#interaction-routing). |
26
+ | `SlackError` | `ServerkitError` subclass for non-HTTP domain failures (signature mismatch, webhook POST failed, …). |
27
+ | `verifySlackSignature(input)` | Pure helper that validates Slack's v0 HMAC scheme + replay window. No request/context coupling. |
28
+ | `SlackSignaturePolicy` | `@maroonedsoftware/policies` form of `verifySlackSignature` (registered under `SLACK_SIGNATURE_POLICY`). Delegates to the helper but answers as a `PolicyResult`, so it slots into ServerKit's policy pipeline. |
29
+ | `interactionRouteKey(payload)` | Helper that produces the `SlackInteractionHandlerMap` key for a given payload. |
30
+ | `slackEventIdempotencyKey(envelope)` | Pure helper that derives a stable de-dup key (`slack:event:{team_id}:{event_id}`) for an `event_callback` envelope. |
31
31
 
32
32
  ## Configuration
33
33
 
@@ -37,9 +37,7 @@ The package does not read `AppConfig` itself — services take `SlackConfig` dir
37
37
  import { AppConfigBuilder, AppConfigSourceJson } from '@maroonedsoftware/appconfig';
38
38
  import { SlackConfig } from '@maroonedsoftware/slack';
39
39
 
40
- const appConfig = await new AppConfigBuilder()
41
- .addSource(new AppConfigSourceJson('./config.json'))
42
- .build();
40
+ const appConfig = await new AppConfigBuilder().addSource(new AppConfigSourceJson('./config.json')).build();
43
41
 
44
42
  const slackConfig = appConfig.getAs<SlackConfig>('slack');
45
43
  container.register(SlackConfig, { useValue: slackConfig });
@@ -52,17 +50,17 @@ container.register(SlackConfig, { useValue: slackConfig });
52
50
  "botToken": "xoxb-...",
53
51
  "signingSecret": "...",
54
52
  "incomingWebhookUrl": "https://hooks.slack.com/services/...", // optional
55
- "signatureMaxAgeSeconds": 300 // optional
56
- }
53
+ "signatureMaxAgeSeconds": 300, // optional
54
+ },
57
55
  }
58
56
  ```
59
57
 
60
- | Field | Required | Used by |
61
- |---------------------------|----------|-------------------------------------------------------------------------|
62
- | `botToken` | yes | `SlackClient` constructor — passed to `WebClient`. |
63
- | `signingSecret` | yes | Your signature verifier (Slack signs requests with this secret). |
64
- | `incomingWebhookUrl` | no | `SlackClient.postWebhook` default URL when no per-call URL is supplied. |
65
- | `signatureMaxAgeSeconds` | no | Replay-protection window for your signature verifier (default `300`). |
58
+ | Field | Required | Used by |
59
+ | ------------------------ | -------- | ----------------------------------------------------------------------- |
60
+ | `botToken` | yes | `SlackClient` constructor — passed to `WebClient`. |
61
+ | `signingSecret` | yes | Your signature verifier (Slack signs requests with this secret). |
62
+ | `incomingWebhookUrl` | no | `SlackClient.postWebhook` default URL when no per-call URL is supplied. |
63
+ | `signatureMaxAgeSeconds` | no | Replay-protection window for your signature verifier (default `300`). |
66
64
 
67
65
  ## Sending messages
68
66
 
@@ -98,13 +96,7 @@ Examples below use Koa, but any HTTP framework works.
98
96
  ### Events API
99
97
 
100
98
  ```ts
101
- import {
102
- SlackConfig,
103
- SlackDispatcher,
104
- SlackEventHandlerMap,
105
- verifySlackSignature,
106
- type SlackEventHandler,
107
- } from '@maroonedsoftware/slack';
99
+ import { SlackConfig, SlackDispatcher, SlackEventHandlerMap, verifySlackSignature, type SlackEventHandler } from '@maroonedsoftware/slack';
108
100
  import rawBody from 'raw-body';
109
101
 
110
102
  class AppMentionHandler implements SlackEventHandler {
@@ -120,7 +112,7 @@ events.set('app_mention', container.get(AppMentionHandler));
120
112
  container.register(SlackEventHandlerMap, { useValue: events });
121
113
 
122
114
  // Route
123
- router.post('/slack/events', async (ctx) => {
115
+ router.post('/slack/events', async ctx => {
124
116
  const raw = await rawBody(ctx.req, { encoding: 'utf8' });
125
117
  verifySlackSignature({
126
118
  signingSecret: ctx.container.get(SlackConfig).signingSecret,
@@ -129,8 +121,12 @@ router.post('/slack/events', async (ctx) => {
129
121
  signature: ctx.get('x-slack-signature'),
130
122
  });
131
123
  const result = await ctx.container.get(SlackDispatcher).dispatchEvent(JSON.parse(raw));
132
- if (result) ctx.body = result; // url_verification challenge
133
- else { ctx.status = 200; ctx.body = ''; }
124
+ if (result)
125
+ ctx.body = result; // url_verification challenge
126
+ else {
127
+ ctx.status = 200;
128
+ ctx.body = '';
129
+ }
134
130
  });
135
131
  ```
136
132
 
@@ -145,11 +141,15 @@ Slack redelivers an `event_callback` (with an `X-Slack-Retry-Num` header) whenev
145
141
  ```ts
146
142
  // Inside the route, after verifying the signature and parsing the body:
147
143
  const body = JSON.parse(raw);
148
- if (body.type === 'url_verification') { ctx.body = { challenge: body.challenge }; return; }
144
+ if (body.type === 'url_verification') {
145
+ ctx.body = { challenge: body.challenge };
146
+ return;
147
+ }
149
148
  if (body.type === 'event_callback') {
150
149
  await jobBroker.send('slack.event', body, { singletonKey: slackEventIdempotencyKey(body) });
151
150
  }
152
- ctx.status = 200; ctx.body = ''; // ack fast; a worker calls dispatchEvent later
151
+ ctx.status = 200;
152
+ ctx.body = ''; // ack fast; a worker calls dispatchEvent later
153
153
  ```
154
154
 
155
155
  **Edge dedup — one store, one arg.** When you'd rather handle events inline, pass an `IdempotencyStore` and `dispatchEvent` runs the handler at most once per `event_id` (keyed by `slackEventIdempotencyKey`). A duplicate/dropped redelivery skips the handler and acks. Omit the option and behaviour is unchanged. The `url_verification` handshake is never de-duplicated.
@@ -185,7 +185,7 @@ const commands = new SlackCommandHandlerMap();
185
185
  commands.set('/deploy', container.get(DeployCommand));
186
186
  container.register(SlackCommandHandlerMap, { useValue: commands });
187
187
 
188
- router.post('/slack/commands', async (ctx) => {
188
+ router.post('/slack/commands', async ctx => {
189
189
  const raw = await rawBody(ctx.req, { encoding: 'utf8' });
190
190
  verifySlackSignature({
191
191
  signingSecret: ctx.container.get(SlackConfig).signingSecret,
@@ -209,7 +209,10 @@ router.post('/slack/commands', async (ctx) => {
209
209
  } satisfies SlackCommandPayload;
210
210
  const result = await ctx.container.get(SlackDispatcher).dispatchCommand(payload);
211
211
  if (result) ctx.body = result;
212
- else { ctx.status = 200; ctx.body = ''; }
212
+ else {
213
+ ctx.status = 200;
214
+ ctx.body = '';
215
+ }
213
216
  });
214
217
  ```
215
218
 
@@ -232,7 +235,7 @@ interactions.set('block_actions:approve', container.get(ApproveButton));
232
235
  interactions.set('view_submission:create_ticket_modal', container.get(CreateTicketModal));
233
236
  container.register(SlackInteractionHandlerMap, { useValue: interactions });
234
237
 
235
- router.post('/slack/interactions', async (ctx) => {
238
+ router.post('/slack/interactions', async ctx => {
236
239
  const raw = await rawBody(ctx.req, { encoding: 'utf8' });
237
240
  verifySlackSignature({
238
241
  signingSecret: ctx.container.get(SlackConfig).signingSecret,
@@ -243,7 +246,10 @@ router.post('/slack/interactions', async (ctx) => {
243
246
  const payload = JSON.parse(new URLSearchParams(raw).get('payload') ?? '{}');
244
247
  const result = await ctx.container.get(SlackDispatcher).dispatchInteraction(payload);
245
248
  if (result) ctx.body = result;
246
- else { ctx.status = 200; ctx.body = ''; }
249
+ else {
250
+ ctx.status = 200;
251
+ ctx.body = '';
252
+ }
247
253
  });
248
254
  ```
249
255
 
@@ -254,7 +260,7 @@ Slack POSTs interactive payloads as `application/x-www-form-urlencoded` with a s
254
260
  `SlackInteractionHandlerMap` is keyed by `${type}:${identifier}`:
255
261
 
256
262
  | Payload type | Key |
257
- |-------------------|----------------------------------------|
263
+ | ----------------- | -------------------------------------- |
258
264
  | `block_actions` | `block_actions:<actions[0].action_id>` |
259
265
  | `view_submission` | `view_submission:<view.callback_id>` |
260
266
  | `view_closed` | `view_closed:<view.callback_id>` |
@@ -273,7 +279,7 @@ import { verifySlackSignature, SlackError } from '@maroonedsoftware/slack';
273
279
  try {
274
280
  verifySlackSignature({
275
281
  signingSecret: slackConfig.signingSecret,
276
- rawBody, // exactly what Slack sent
282
+ rawBody, // exactly what Slack sent
277
283
  timestamp: req.headers['x-slack-request-timestamp'] as string,
278
284
  signature: req.headers['x-slack-signature'] as string,
279
285
  maxAgeSeconds: slackConfig.signatureMaxAgeSeconds, // optional, default 300
@@ -334,12 +340,17 @@ import { SlackClient, SlackConfig, verifySlackSignature } from '@maroonedsoftwar
334
340
  import { dispatchSlackCommand, dispatchSlackInteraction, dispatchSlackEvent, createSlackNotifier } from '@maroonedsoftware/slack/comms';
335
341
  import { router } from './router.js'; // a shared ChannelRouter
336
342
 
337
- router.post('/slack/commands', async (ctx) => {
343
+ router.post('/slack/commands', async ctx => {
338
344
  const raw = await rawBody(ctx.req, { encoding: 'utf8' });
339
- verifySlackSignature({ signingSecret: ctx.container.get(SlackConfig).signingSecret, rawBody: raw,
340
- timestamp: ctx.get('x-slack-request-timestamp'), signature: ctx.get('x-slack-signature') });
345
+ verifySlackSignature({
346
+ signingSecret: ctx.container.get(SlackConfig).signingSecret,
347
+ rawBody: raw,
348
+ timestamp: ctx.get('x-slack-request-timestamp'),
349
+ signature: ctx.get('x-slack-signature'),
350
+ });
341
351
  await dispatchSlackCommand(router, ctx.container.get(SlackClient), Object.fromEntries(new URLSearchParams(raw)) as never);
342
- ctx.status = 200; ctx.body = '';
352
+ ctx.status = 200;
353
+ ctx.body = '';
343
354
  });
344
355
  ```
345
356
 
@@ -1 +1 @@
1
- {"version":3,"file":"slack.client.d.ts","sourceRoot":"","sources":["../../src/client/slack.client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACjN,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAS5C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;IAC3C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,qBACa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;gBAGL,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM;IAKjC,8CAA8C;IAC9C,WAAW,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAI7E,2CAA2C;IAC3C,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIrE,2CAA2C;IAC3C,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIrE,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAI9D;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,sBAAsB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhF"}
1
+ {"version":3,"file":"slack.client.d.ts","sourceRoot":"","sources":["../../src/client/slack.client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAS5C,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,YAAY,GAAG,WAAW,CAAC;IAC3C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,qBACa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IALzB,0CAA0C;IAC1C,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;gBAGL,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM;IAKjC,8CAA8C;IAC9C,WAAW,CAAC,IAAI,EAAE,wBAAwB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAI7E,2CAA2C;IAC3C,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIrE,2CAA2C;IAC3C,aAAa,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIrE,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAI9D;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,sBAAsB,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhF"}
@@ -1 +1 @@
1
- {"version":3,"file":"slack.logger.adapter.d.ts","sourceRoot":"","sources":["../../src/client/slack.logger.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAY,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,aAAsB,KAAG,WAmBpE,CAAC"}
1
+ {"version":3,"file":"slack.logger.adapter.d.ts","sourceRoot":"","sources":["../../src/client/slack.logger.adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAY,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,aAAsB,KAAG,WAqBpE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"comms.d.ts","sourceRoot":"","sources":["../src/comms.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAyB,KAAK,aAAa,EAAsB,KAAK,QAAQ,EAAwB,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACpK,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAkChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,WAAW,EAAE,WAAW,gBAAgB,KAAG,QASrF,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAU,QAAQ,aAAa,EAAE,QAAQ,WAAW,EAAE,MAAM,kBAAkB,KAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAYxJ,CAAC;AAEF,4GAA4G;AAC5G,eAAO,MAAM,oBAAoB,GAAU,QAAQ,aAAa,EAAE,QAAQ,WAAW,EAAE,SAAS,mBAAmB,KAAG,OAAO,CAAC,IAAI,CAYjI,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAU,QAAQ,aAAa,EAAE,QAAQ,WAAW,EAAE,SAAS,uBAAuB,KAAG,OAAO,CAAC,IAAI,CAgBzI,CAAC"}
1
+ {"version":3,"file":"comms.d.ts","sourceRoot":"","sources":["../src/comms.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAGL,KAAK,aAAa,EAElB,KAAK,QAAQ,EAEb,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAwChE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ,WAAW,EAAE,WAAW,gBAAgB,KAAG,QASrF,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,QAAQ,aAAa,EACrB,QAAQ,WAAW,EACnB,MAAM,kBAAkB,KACvB,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAmB3C,CAAC;AAEF,4GAA4G;AAC5G,eAAO,MAAM,oBAAoB,GAAU,QAAQ,aAAa,EAAE,QAAQ,WAAW,EAAE,SAAS,mBAAmB,KAAG,OAAO,CAAC,IAAI,CAYjI,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAU,QAAQ,aAAa,EAAE,QAAQ,WAAW,EAAE,SAAS,uBAAuB,KAAG,OAAO,CAAC,IAAI,CAgBzI,CAAC"}
package/dist/comms.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/comms.ts"],"sourcesContent":["/**\n * `@maroonedsoftware/slack/comms` — adapter binding the Slack package to the\n * channel-agnostic `@maroonedsoftware/comms` router. Importing this subpath\n * pulls in `@maroonedsoftware/comms` (an optional peer); the slack core does not.\n */\nimport { bindReply, CommsError, type ChannelRouter, type IncomingEvent, type Notifier, type OutgoingMessage, type TemplateRegistry } from '@maroonedsoftware/comms';\nimport { SlackClient } from './client/slack.client.js';\nimport type { SlackCommandPayload } from './slack.command.handler.js';\nimport type { SlackInteractionPayload } from './slack.interaction.handler.js';\nimport type { SlackEventCallback } from './slack.event.handler.js';\nimport type { SlackEventsRequest } from './slack.dispatcher.js';\n\ntype SlackPayload = Record<string, unknown>;\n\n/**\n * Neutralizes Slack broadcast control sequences in user-supplied text so it\n * cannot ping a whole channel/workspace. `<!everyone>`, `<!channel>`, `<!here>`\n * (and their `<!channel|label>` forms) are rewritten to harmless literal\n * `@everyone`/`@channel`/`@here` text.\n */\nconst sanitizeText = (text: string | undefined): string | undefined => (text === undefined ? undefined : text.replace(/<!(everyone|channel|here)(\\|[^>]*)?>/gi, '@$1'));\n\n/** Renders a portable message to a Slack chat payload (`text`, or `text` + Block Kit `actions`). */\nconst render = (message: OutgoingMessage): SlackPayload => {\n const text = sanitizeText(message.text);\n if (!message.buttons?.length) return { text };\n return {\n text,\n blocks: [\n { type: 'section', text: { type: 'mrkdwn', text } },\n {\n type: 'actions',\n elements: message.buttons.map(b => ({ type: 'button', action_id: b.id, text: { type: 'plain_text', text: b.label }, value: b.value ?? b.id })),\n },\n ],\n };\n};\n\n/** Delivers a Slack payload: to a `response_url` (http) via webhook, otherwise to a channel id. */\nconst deliver = (client: SlackClient, to: string, payload: SlackPayload): Promise<unknown> =>\n to.startsWith('http')\n ? client.postWebhook(payload as Parameters<SlackClient['postWebhook']>[0], to)\n : client.postMessage({ channel: to, ...payload } as Parameters<SlackClient['postMessage']>[0]);\n\n/**\n * Builds a {@link Notifier} that sends portable messages and registered templates\n * through {@link SlackClient}. The recipient string is either a `response_url`\n * (used as an incoming webhook) or a channel id (`chat.postMessage`).\n */\nexport const createSlackNotifier = (client: SlackClient, templates: TemplateRegistry): Notifier => ({\n channel: 'slack',\n send: async (to, message) => void (await deliver(client, to, render(message))),\n sendTemplate: async (to, name, data) => {\n const resolved = templates.render(name, 'slack', data);\n if (!resolved) throw new CommsError(`No comms template registered for \"${name}\"`).withInternalDetails({ channel: 'slack', name });\n await deliver(client, to, resolved.kind === 'native' ? (resolved.payload as SlackPayload) : render(resolved.message));\n },\n sendNative: async (to, payload) => void (await deliver(client, to, payload as SlackPayload)),\n});\n\n/**\n * Dispatches a parsed Slack Events API body. Returns the `url_verification`\n * challenge for the handshake; for `message` / `app_mention` events it routes a\n * normalized `message` event to the {@link ChannelRouter} (replying via\n * `chat.postMessage` to the event's channel). Returns `undefined` otherwise.\n */\nexport const dispatchSlackEvent = async (router: ChannelRouter, client: SlackClient, body: SlackEventsRequest): Promise<{ challenge: string } | undefined> => {\n if (body.type === 'url_verification') return { challenge: (body as { challenge: string }).challenge };\n if (body.type !== 'event_callback') return undefined;\n\n const envelope = body as SlackEventCallback;\n const ev = envelope.event as { type: string; channel?: string; user?: string; text?: string; bot_id?: string; subtype?: string };\n if ((ev.type === 'message' || ev.type === 'app_mention') && ev.user && !ev.bot_id && ev.subtype !== 'bot_message') {\n const channel = ev.channel ?? '';\n const event: IncomingEvent = { channel: 'slack', kind: 'message', user: { id: ev.user }, conversation: { id: channel }, text: ev.text, raw: envelope };\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), channel));\n }\n return undefined;\n};\n\n/** Dispatches a parsed slash-command payload as a normalized `command` event (reply via `response_url`). */\nexport const dispatchSlackCommand = async (router: ChannelRouter, client: SlackClient, payload: SlackCommandPayload): Promise<void> => {\n const event: IncomingEvent = {\n channel: 'slack',\n kind: 'command',\n user: { id: payload.user_id, username: payload.user_name },\n conversation: { id: payload.channel_id },\n text: `${payload.command} ${payload.text}`.trim(),\n command: { name: payload.command, args: payload.text },\n raw: payload,\n };\n const to = payload.response_url || payload.channel_id;\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), to));\n};\n\n/**\n * Dispatches a parsed interactive payload. Only `block_actions` is normalized\n * (to an `action` event keyed by the first action's `action_id`); other types\n * (e.g. `view_submission`) stay on the slack package's native handlers.\n */\nexport const dispatchSlackInteraction = async (router: ChannelRouter, client: SlackClient, payload: SlackInteractionPayload): Promise<void> => {\n if (payload.type !== 'block_actions') return;\n const first = payload.actions?.[0];\n if (!first) return;\n\n const channel = (payload as { channel?: { id?: string } }).channel?.id ?? '';\n const event: IncomingEvent = {\n channel: 'slack',\n kind: 'action',\n user: { id: payload.user?.id ?? '', username: payload.user?.name },\n conversation: { id: channel },\n action: { id: first.action_id, value: first.value },\n raw: payload,\n };\n const to = payload.response_url || channel;\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), to));\n};\n"],"mappings":"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","names":["bindReply","CommsError","sanitizeText","text","undefined","replace","render","message","buttons","length","blocks","type","elements","map","b","action_id","id","label","value","deliver","client","to","payload","startsWith","postWebhook","postMessage","channel","createSlackNotifier","templates","send","sendTemplate","name","data","resolved","CommsError","withInternalDetails","kind","sendNative","dispatchSlackEvent","router","body","challenge","envelope","ev","event","user","bot_id","subtype","conversation","raw","dispatch","bindReply","dispatchSlackCommand","user_id","username","user_name","channel_id","command","trim","args","response_url","dispatchSlackInteraction","first","actions","action"]}
1
+ {"version":3,"sources":["../src/comms.ts"],"sourcesContent":["/**\n * `@maroonedsoftware/slack/comms` — adapter binding the Slack package to the\n * channel-agnostic `@maroonedsoftware/comms` router. Importing this subpath\n * pulls in `@maroonedsoftware/comms` (an optional peer); the slack core does not.\n */\nimport {\n bindReply,\n CommsError,\n type ChannelRouter,\n type IncomingEvent,\n type Notifier,\n type OutgoingMessage,\n type TemplateRegistry,\n} from '@maroonedsoftware/comms';\nimport { SlackClient } from './client/slack.client.js';\nimport type { SlackCommandPayload } from './slack.command.handler.js';\nimport type { SlackInteractionPayload } from './slack.interaction.handler.js';\nimport type { SlackEventCallback } from './slack.event.handler.js';\nimport type { SlackEventsRequest } from './slack.dispatcher.js';\n\ntype SlackPayload = Record<string, unknown>;\n\n/**\n * Neutralizes Slack broadcast control sequences in user-supplied text so it\n * cannot ping a whole channel/workspace. `<!everyone>`, `<!channel>`, `<!here>`\n * (and their `<!channel|label>` forms) are rewritten to harmless literal\n * `@everyone`/`@channel`/`@here` text.\n */\nconst sanitizeText = (text: string | undefined): string | undefined =>\n text === undefined ? undefined : text.replace(/<!(everyone|channel|here)(\\|[^>]*)?>/gi, '@$1');\n\n/** Renders a portable message to a Slack chat payload (`text`, or `text` + Block Kit `actions`). */\nconst render = (message: OutgoingMessage): SlackPayload => {\n const text = sanitizeText(message.text);\n if (!message.buttons?.length) return { text };\n return {\n text,\n blocks: [\n { type: 'section', text: { type: 'mrkdwn', text } },\n {\n type: 'actions',\n elements: message.buttons.map(b => ({\n type: 'button',\n action_id: b.id,\n text: { type: 'plain_text', text: b.label },\n value: b.value ?? b.id,\n })),\n },\n ],\n };\n};\n\n/** Delivers a Slack payload: to a `response_url` (http) via webhook, otherwise to a channel id. */\nconst deliver = (client: SlackClient, to: string, payload: SlackPayload): Promise<unknown> =>\n to.startsWith('http')\n ? client.postWebhook(payload as Parameters<SlackClient['postWebhook']>[0], to)\n : client.postMessage({ channel: to, ...payload } as Parameters<SlackClient['postMessage']>[0]);\n\n/**\n * Builds a {@link Notifier} that sends portable messages and registered templates\n * through {@link SlackClient}. The recipient string is either a `response_url`\n * (used as an incoming webhook) or a channel id (`chat.postMessage`).\n */\nexport const createSlackNotifier = (client: SlackClient, templates: TemplateRegistry): Notifier => ({\n channel: 'slack',\n send: async (to, message) => void (await deliver(client, to, render(message))),\n sendTemplate: async (to, name, data) => {\n const resolved = templates.render(name, 'slack', data);\n if (!resolved) throw new CommsError(`No comms template registered for \"${name}\"`).withInternalDetails({ channel: 'slack', name });\n await deliver(client, to, resolved.kind === 'native' ? (resolved.payload as SlackPayload) : render(resolved.message));\n },\n sendNative: async (to, payload) => void (await deliver(client, to, payload as SlackPayload)),\n});\n\n/**\n * Dispatches a parsed Slack Events API body. Returns the `url_verification`\n * challenge for the handshake; for `message` / `app_mention` events it routes a\n * normalized `message` event to the {@link ChannelRouter} (replying via\n * `chat.postMessage` to the event's channel). Returns `undefined` otherwise.\n */\nexport const dispatchSlackEvent = async (\n router: ChannelRouter,\n client: SlackClient,\n body: SlackEventsRequest,\n): Promise<{ challenge: string } | undefined> => {\n if (body.type === 'url_verification') return { challenge: (body as { challenge: string }).challenge };\n if (body.type !== 'event_callback') return undefined;\n\n const envelope = body as SlackEventCallback;\n const ev = envelope.event as { type: string; channel?: string; user?: string; text?: string; bot_id?: string; subtype?: string };\n if ((ev.type === 'message' || ev.type === 'app_mention') && ev.user && !ev.bot_id && ev.subtype !== 'bot_message') {\n const channel = ev.channel ?? '';\n const event: IncomingEvent = {\n channel: 'slack',\n kind: 'message',\n user: { id: ev.user },\n conversation: { id: channel },\n text: ev.text,\n raw: envelope,\n };\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), channel));\n }\n return undefined;\n};\n\n/** Dispatches a parsed slash-command payload as a normalized `command` event (reply via `response_url`). */\nexport const dispatchSlackCommand = async (router: ChannelRouter, client: SlackClient, payload: SlackCommandPayload): Promise<void> => {\n const event: IncomingEvent = {\n channel: 'slack',\n kind: 'command',\n user: { id: payload.user_id, username: payload.user_name },\n conversation: { id: payload.channel_id },\n text: `${payload.command} ${payload.text}`.trim(),\n command: { name: payload.command, args: payload.text },\n raw: payload,\n };\n const to = payload.response_url || payload.channel_id;\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), to));\n};\n\n/**\n * Dispatches a parsed interactive payload. Only `block_actions` is normalized\n * (to an `action` event keyed by the first action's `action_id`); other types\n * (e.g. `view_submission`) stay on the slack package's native handlers.\n */\nexport const dispatchSlackInteraction = async (router: ChannelRouter, client: SlackClient, payload: SlackInteractionPayload): Promise<void> => {\n if (payload.type !== 'block_actions') return;\n const first = payload.actions?.[0];\n if (!first) return;\n\n const channel = (payload as { channel?: { id?: string } }).channel?.id ?? '';\n const event: IncomingEvent = {\n channel: 'slack',\n kind: 'action',\n user: { id: payload.user?.id ?? '', username: payload.user?.name },\n conversation: { id: channel },\n action: { id: first.action_id, value: first.value },\n raw: payload,\n };\n const to = payload.response_url || channel;\n await router.dispatch(event, bindReply(createSlackNotifier(client, router.templates), to));\n};\n"],"mappings":"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","names":["bindReply","CommsError","sanitizeText","text","undefined","replace","render","message","buttons","length","blocks","type","elements","map","b","action_id","id","label","value","deliver","client","to","payload","startsWith","postWebhook","postMessage","channel","createSlackNotifier","templates","send","sendTemplate","name","data","resolved","CommsError","withInternalDetails","kind","sendNative","dispatchSlackEvent","router","body","challenge","envelope","ev","event","user","bot_id","subtype","conversation","raw","dispatch","bindReply","dispatchSlackCommand","user_id","username","user_name","channel_id","command","trim","args","response_url","dispatchSlackInteraction","first","actions","action"]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/slack.config.ts","../src/slack.error.ts","../src/slack.signature.ts","../src/slack.signature.policy.ts","../src/slack.event.handler.ts","../src/slack.interaction.handler.ts","../src/slack.dispatcher.ts","../src/client/slack.client.ts","../src/client/slack.logger.adapter.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */\nimport { Injectable } from 'injectkit';\n\n/**\n * Configuration for the Slack package. Declared as an abstract `@Injectable()`\n * class so it doubles as a DI token (mirrors the `Logger` pattern in\n * `@maroonedsoftware/logger`).\n *\n * Consumers register a concrete value at bootstrap, typically resolved from\n * `AppConfig`:\n *\n * ```ts\n * const slackConfig = appConfig.getAs<SlackConfig>('slack');\n * container.register(SlackConfig, { useValue: slackConfig });\n * ```\n *\n * Services in this package take `SlackConfig` directly in their constructor.\n */\nexport interface SlackConfig {\n /** Bot user OAuth token (`xoxb-...`). Required for Web API calls. */\n botToken: string;\n /** App-level signing secret used to verify request signatures. */\n signingSecret: string;\n /** Optional incoming webhook URL used as the default target for `SlackClient.postWebhook`. */\n incomingWebhookUrl?: string;\n /**\n * Maximum age (in seconds) for request timestamps before signature\n * verification rejects them as replays. Defaults to `300` (5 minutes).\n */\n signatureMaxAgeSeconds?: number;\n /**\n * Per-request timeout (in milliseconds) for outbound `SlackClient.postWebhook`\n * calls. Defaults to\n * {@link import('./client/slack.client.js').SLACK_DEFAULT_REQUEST_TIMEOUT_MS} (10s).\n */\n requestTimeoutMs?: number;\n}\n\n@Injectable()\nexport abstract class SlackConfig implements SlackConfig {}\n","import { ServerkitError } from '@maroonedsoftware/errors';\n\n/**\n * Domain error raised by the Slack package for non-HTTP failures (e.g.\n * incoming-webhook POST failed, unknown handler dispatch).\n *\n * Extends {@link ServerkitError} so `errorMiddleware` renders a 500 with\n * `{ message, details }` if one of these escapes a route handler. Inside\n * route handlers, throw `httpError(...)` directly for status-coded responses.\n */\nexport class SlackError extends ServerkitError {}\n\n/**\n * Type guard for {@link SlackError}. Narrows `unknown` to `SlackError` so\n * `details`, `internalDetails`, and the chainable setters are accessible\n * without further checks. Returns `true` for any subclass.\n */\nexport const IsSlackError = (error: unknown): error is SlackError => error instanceof SlackError;\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { DateTime } from 'luxon';\nimport { SlackError } from './slack.error.js';\n\n/** Default replay-protection window in seconds (5 minutes — matches Slack's recommendation). */\nexport const SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS = 300;\n\n/**\n * Reason codes attached to {@link SlackError.internalDetails} when verification\n * fails. Useful for callers that want to log structured reasons without\n * pattern-matching on error messages.\n */\nexport type SlackSignatureFailureReason =\n | 'missing_timestamp'\n | 'invalid_timestamp'\n | 'stale_timestamp'\n | 'missing_signature'\n | 'invalid_signature';\n\n/**\n * Inputs to {@link verifySlackSignature}. All values are taken verbatim from\n * the request — the helper does no header lookups or body reads of its own.\n */\nexport type VerifySlackSignatureInput = {\n /** App signing secret (`SlackConfig.signingSecret`). */\n signingSecret: string;\n /** Raw, unparsed request body — exactly as Slack sent it. */\n rawBody: string;\n /** Value of the `X-Slack-Request-Timestamp` header. */\n timestamp: string | undefined;\n /** Value of the `X-Slack-Signature` header (e.g. `\"v0=abc123…\"`). */\n signature: string | undefined;\n /**\n * Maximum age in seconds before the request is rejected as a replay.\n * Defaults to {@link SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS}.\n */\n maxAgeSeconds?: number;\n /**\n * Override for the current Unix time in seconds. Mostly useful for tests;\n * defaults to `Math.floor(DateTime.now().toSeconds())`.\n */\n now?: number;\n};\n\n/**\n * Verifies a Slack request signature against the app signing secret.\n *\n * Implements Slack's v0 scheme:\n * 1. Reject the request if `X-Slack-Request-Timestamp` is missing, non-numeric,\n * or older than `maxAgeSeconds` (replay protection).\n * 2. Compute `v0=` + `HMAC-SHA256(signingSecret, \"v0:{timestamp}:{rawBody}\")`\n * as hex.\n * 3. Compare against the provided `X-Slack-Signature` value using a\n * constant-time compare.\n *\n * Pure: no request/context coupling. The caller extracts the headers and raw\n * body from whatever transport it's using and passes them in.\n *\n * @throws {@link SlackError} on any failure. The error's `internalDetails.reason`\n * is one of {@link SlackSignatureFailureReason}; map to HTTP 401 at the route boundary.\n *\n * @example\n * ```ts\n * try {\n * verifySlackSignature({\n * signingSecret: config.signingSecret,\n * rawBody,\n * timestamp: req.headers['x-slack-request-timestamp'],\n * signature: req.headers['x-slack-signature'],\n * });\n * } catch (err) {\n * throw httpError(401).withCause(err);\n * }\n * ```\n */\nexport const verifySlackSignature = (input: VerifySlackSignatureInput): void => {\n const {\n signingSecret,\n rawBody,\n timestamp,\n signature,\n maxAgeSeconds = SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS,\n now = Math.floor(DateTime.now().toSeconds()),\n } = input;\n\n if (!timestamp) {\n throw new SlackError('Slack request missing X-Slack-Request-Timestamp header').withInternalDetails({\n reason: 'missing_timestamp' satisfies SlackSignatureFailureReason,\n });\n }\n\n const ts = Number(timestamp);\n if (!Number.isFinite(ts) || !Number.isInteger(ts)) {\n throw new SlackError('Slack request timestamp is not a valid integer').withInternalDetails({\n reason: 'invalid_timestamp' satisfies SlackSignatureFailureReason,\n timestamp,\n });\n }\n\n if (Math.abs(now - ts) > maxAgeSeconds) {\n throw new SlackError('Slack request timestamp is outside the allowed window').withInternalDetails({\n reason: 'stale_timestamp' satisfies SlackSignatureFailureReason,\n timestamp: ts,\n now,\n maxAgeSeconds,\n });\n }\n\n if (!signature) {\n throw new SlackError('Slack request missing X-Slack-Signature header').withInternalDetails({\n reason: 'missing_signature' satisfies SlackSignatureFailureReason,\n });\n }\n\n // Sign with the raw header value verbatim (not the parsed `ts`): Slack computes\n // its signature over the exact `X-Slack-Request-Timestamp` string it sent, so a\n // non-canonical-but-numeric header (e.g. leading zeros) must round-trip as-is.\n const expected = `v0=${createHmac('sha256', signingSecret).update(`v0:${timestamp}:${rawBody}`).digest('hex')}`;\n const expectedBuf = Buffer.from(expected, 'utf8');\n const providedBuf = Buffer.from(signature, 'utf8');\n\n // timingSafeEqual throws on length mismatch — short-circuit so the caller\n // gets a uniform \"invalid_signature\" error instead of a crypto exception.\n if (expectedBuf.length !== providedBuf.length || !timingSafeEqual(expectedBuf, providedBuf)) {\n throw new SlackError('Slack request signature does not match').withInternalDetails({\n reason: 'invalid_signature' satisfies SlackSignatureFailureReason,\n });\n }\n};\n","import { BinaryLike } from 'node:crypto';\nimport { Injectable } from 'injectkit';\nimport { Policy, PolicyEnvelope, PolicyResult } from '@maroonedsoftware/policies';\nimport { SlackConfig } from './slack.config.js';\nimport { IsSlackError } from './slack.error.js';\nimport { verifySlackSignature, type SlackSignatureFailureReason } from './slack.signature.js';\n\n/**\n * Policy name under which {@link SlackSignaturePolicy} is registered. Use as the\n * key when wiring your `PolicyRegistryMap`, and pass to `PolicyService.check`.\n */\nexport const SLACK_SIGNATURE_POLICY = 'slack.signature.valid' as const;\n\n/** Header carrying the request timestamp Slack signs into the HMAC. */\nexport const SLACK_REQUEST_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp';\n/** Header carrying the `v0=`-prefixed request signature. */\nexport const SLACK_SIGNATURE_HEADER = 'X-Slack-Signature';\n\n/**\n * Configuration the {@link SlackSignaturePolicy} reads. A structural subset of\n * {@link SlackConfig}, so a `SlackConfig` value satisfies it directly — e.g.\n * `requireSignature<SlackSignatureOptions>('slack')` with the Slack config\n * stored under that `AppConfig` key.\n */\nexport type SlackSignatureOptions = Pick<SlackConfig, 'signingSecret' | 'signatureMaxAgeSeconds'>;\n\n/**\n * Context for {@link SlackSignaturePolicy}: the raw request bytes, a\n * case-insensitive header accessor, and the {@link SlackSignatureOptions}.\n *\n * Structurally compatible with `@maroonedsoftware/koa`'s\n * `SignaturePolicyContext<SlackSignatureOptions>`, so the koa `requireSignature`\n * middleware can drive this policy without the slack package depending on koa —\n * register `SlackSignaturePolicy` under the signature policy name and point the\n * middleware at the `AppConfig` key holding the Slack config.\n */\nexport interface SlackSignaturePolicyContext {\n /** Raw, unparsed request body — exactly as Slack sent it (from `ctx.rawBody`). */\n rawBody: BinaryLike;\n /**\n * Case-insensitive request header accessor (Koa's `ctx.get`); returns `''`\n * when the header is absent.\n */\n getHeader: (name: string) => string;\n /** Slack signing configuration. */\n options: SlackSignatureOptions;\n}\n\n/**\n * Policy form of {@link verifySlackSignature}: verifies a Slack request against\n * the app signing secret using Slack's v0 scheme (HMAC over\n * `v0:{timestamp}:{rawBody}`, `v0=`-prefixed, with timestamp replay\n * protection).\n *\n * Delegates to {@link verifySlackSignature} so the crypto/timestamp logic has a\n * single source of truth, but answers as a {@link PolicyResult} rather than\n * throwing: allows on success, denies on failure with the helper's\n * {@link SlackSignatureFailureReason} as the denial `reason` and its diagnostics\n * (timestamps, window) on `internalDetails` — never the signing secret, never\n * on the wire. The replay window is anchored to `envelope.now` so all policies\n * in an evaluation share one clock.\n *\n * Registered by default under {@link SLACK_SIGNATURE_POLICY}.\n *\n * @example\n * ```ts\n * // Direct evaluation in a route handler:\n * const result = await policyService.check(SLACK_SIGNATURE_POLICY, {\n * rawBody: ctx.rawBody,\n * getHeader: name => ctx.get(name),\n * options: ctx.container.get(SlackConfig),\n * });\n * if (isPolicyResultDenied(result)) throw httpError(401);\n * ```\n */\n@Injectable()\nexport class SlackSignaturePolicy extends Policy<SlackSignaturePolicyContext> {\n async evaluate(context: SlackSignaturePolicyContext, envelope: PolicyEnvelope): Promise<PolicyResult> {\n const { rawBody, getHeader, options } = context;\n\n // Slack signs the raw text body; `ctx.rawBody` may arrive as a Buffer.\n const body = typeof rawBody === 'string' ? rawBody : Buffer.from(rawBody as Uint8Array).toString('utf8');\n\n try {\n verifySlackSignature({\n signingSecret: options.signingSecret,\n rawBody: body,\n timestamp: getHeader(SLACK_REQUEST_TIMESTAMP_HEADER),\n signature: getHeader(SLACK_SIGNATURE_HEADER),\n maxAgeSeconds: options.signatureMaxAgeSeconds,\n now: Math.floor(envelope.now.toSeconds()),\n });\n return this.allow();\n } catch (error) {\n if (!IsSlackError(error)) throw error;\n\n const internalDetails = error.internalDetails ?? {};\n const reason = typeof internalDetails.reason === 'string' ? internalDetails.reason : ('invalid_signature' satisfies SlackSignatureFailureReason);\n return this.deny(reason, undefined, { message: error.message, ...internalDetails });\n }\n }\n}\n","/**\n * Metadata accompanying every dispatched Slack event. Includes the wrapping\n * envelope fields (team/event IDs) plus the raw `event_callback` payload for\n * handlers that need fields the typed `event` object doesn't expose.\n */\nexport type SlackEventContext = {\n /** Slack workspace / team ID from the envelope. */\n teamId: string;\n /** Unique event ID Slack assigns to each delivery. */\n eventId: string;\n /** Unix timestamp the event was generated. */\n eventTime: number;\n /** Original `event_callback` envelope, untouched. */\n envelope: SlackEventCallback;\n};\n\n/**\n * Slack `event_callback` envelope. The shape is documented at\n * https://api.slack.com/types/event. We type the wrapper but leave the inner\n * `event` as `Record<string, unknown>` because the union of all Slack event\n * payloads is large and consumers typically narrow per handler.\n */\nexport type SlackEventCallback = {\n type: 'event_callback';\n team_id: string;\n api_app_id: string;\n event: { type: string } & Record<string, unknown>;\n event_id: string;\n event_time: number;\n authorizations?: unknown[];\n is_ext_shared_channel?: boolean;\n event_context?: string;\n [key: string]: unknown;\n};\n\n/**\n * Derive a stable, collision-free idempotency key for a Slack event delivery.\n *\n * Slack redelivers an `event_callback` (with an `X-Slack-Retry-Num` header) when\n * the initial ack is slow or non-2xx. The assigned `event_id` is stable across\n * those redeliveries, so it keys de-duplication. We scope it by `team_id` where\n * present so ids from different workspaces can never collide.\n *\n * @param envelope - The `event_callback` envelope (only `event_id` / `team_id` are read).\n * @returns `slack:event:{team_id}:{event_id}`, or `slack:event:{event_id}` when no team id.\n */\nexport function slackEventIdempotencyKey(envelope: Pick<SlackEventCallback, 'event_id' | 'team_id'>): string {\n return envelope.team_id ? `slack:event:${envelope.team_id}:${envelope.event_id}` : `slack:event:${envelope.event_id}`;\n}\n\n/**\n * Handler for a single Slack event type (e.g. `app_mention`, `message`,\n * `reaction_added`). Registered in {@link SlackEventHandlerMap}.\n *\n * Handlers should ack quickly — Slack retries any event that doesn't get a\n * 2xx response within ~3 seconds. For slow work, enqueue a job\n * (`@maroonedsoftware/jobbroker`) inside `handle` and return immediately.\n */\nexport interface SlackEventHandler<TEvent extends { type: string } & Record<string, unknown> = { type: string } & Record<string, unknown>> {\n handle(event: TEvent, context: SlackEventContext): Promise<void>;\n}\n","/**\n * The supported interactive payload types Slack POSTs to the interactivity\n * endpoint. Each maps to a different identifier shape (see\n * {@link interactionRouteKey}).\n */\nexport type SlackInteractionType = 'block_actions' | 'view_submission' | 'view_closed' | 'shortcut' | 'message_action' | string;\n\n/**\n * Loose typing for the interactive payload; consumers narrow per handler.\n * Slack's payloads vary by type, but every variant has a `type` field plus\n * one of: `actions[].action_id`, `view.callback_id`, or top-level `callback_id`.\n */\nexport type SlackInteractionPayload = {\n type: SlackInteractionType;\n team?: { id: string; domain?: string };\n user?: { id: string; name?: string };\n trigger_id?: string;\n response_url?: string;\n actions?: Array<{ action_id: string; block_id?: string; value?: string; [key: string]: unknown }>;\n view?: { id: string; callback_id: string; [key: string]: unknown };\n callback_id?: string;\n [key: string]: unknown;\n};\n\n/**\n * Optional response Slack accepts for `view_submission` / `view_closed`\n * payloads (e.g. to display validation errors or update a modal).\n */\nexport type SlackInteractionResponse = {\n response_action?: 'errors' | 'update' | 'push' | 'clear';\n errors?: Record<string, string>;\n view?: unknown;\n [key: string]: unknown;\n};\n\n/**\n * Handler for one interactive payload, keyed in {@link SlackInteractionHandlerMap}\n * by `${type}:${identifier}` — see {@link interactionRouteKey}.\n */\nexport interface SlackInteractionHandler {\n handle(payload: SlackInteractionPayload): Promise<SlackInteractionResponse | void>;\n}\n\n/**\n * Computes the routing key used by {@link SlackDispatcher.dispatchInteraction}\n * to look a handler up in {@link SlackInteractionHandlerMap}.\n *\n * - `block_actions` → `block_actions:<first action.action_id>`\n * - `view_submission` / `view_closed` → `<type>:<view.callback_id>`\n * - `shortcut` / `message_action` → `<type>:<callback_id>`\n * - any other type with a `callback_id` → `<type>:<callback_id>`\n *\n * @returns The routing key, or `undefined` if the payload doesn't carry an\n * identifier we can route on (e.g. a `block_actions` payload with no actions).\n */\nexport const interactionRouteKey = (payload: SlackInteractionPayload): string | undefined => {\n switch (payload.type) {\n case 'block_actions': {\n const id = payload.actions?.[0]?.action_id;\n return id ? `block_actions:${id}` : undefined;\n }\n case 'view_submission':\n case 'view_closed': {\n const id = payload.view?.callback_id;\n return id ? `${payload.type}:${id}` : undefined;\n }\n case 'shortcut':\n case 'message_action': {\n return payload.callback_id ? `${payload.type}:${payload.callback_id}` : undefined;\n }\n default: {\n return payload.callback_id ? `${payload.type}:${payload.callback_id}` : undefined;\n }\n }\n};\n","import { Injectable } from 'injectkit';\nimport { Logger } from '@maroonedsoftware/logger';\nimport type { IdempotencyStore } from '@maroonedsoftware/cache';\nimport { slackEventIdempotencyKey } from './slack.event.handler.js';\nimport type { SlackEventCallback, SlackEventHandler } from './slack.event.handler.js';\nimport type { SlackCommandHandler, SlackCommandPayload, SlackCommandResponse } from './slack.command.handler.js';\nimport {\n interactionRouteKey,\n SlackInteractionHandler,\n type SlackInteractionPayload,\n type SlackInteractionResponse,\n} from './slack.interaction.handler.js';\n\n/**\n * Body shape Slack POSTs to the Events API endpoint. The handshake variant\n * (`url_verification`) is sent once during app configuration; the rest of the\n * traffic is `event_callback` envelopes (or other future top-level types).\n */\nexport type SlackEventsRequest =\n | { type: 'url_verification'; challenge: string; token?: string }\n | SlackEventCallback\n | { type: string; [key: string]: unknown };\n\n/**\n * Response Slack expects for the `url_verification` handshake. For\n * `event_callback` and unknown event types, the dispatcher returns\n * `undefined` and the caller should ack with HTTP 200.\n */\nexport type SlackEventsResponse = { challenge: string } | undefined;\n\n/**\n * Injectable map of command keyword (e.g. `/deploy`) → {@link SlackCommandHandler}.\n *\n * @example\n * ```ts\n * const commands = new SlackCommandHandlerMap();\n * commands.set('/deploy', container.get(DeployCommandHandler));\n * container.register(SlackCommandHandlerMap, { useValue: commands });\n * ```\n */\n@Injectable()\nexport class SlackCommandHandlerMap extends Map<string, SlackCommandHandler> {}\n\n/**\n * Injectable map of Slack event type → {@link SlackEventHandler}. Consumers\n * register handlers at bootstrap and place an instance of this map in their\n * DI container; {@link SlackDispatcher.dispatchEvent} resolves it per request.\n *\n * @example\n * ```ts\n * const handlers = new SlackEventHandlerMap();\n * handlers.set('app_mention', container.get(MyAppMentionHandler));\n * container.register(SlackEventHandlerMap, { useValue: handlers });\n * ```\n */\n@Injectable()\nexport class SlackEventHandlerMap extends Map<string, SlackEventHandler> {}\n\n/**\n * Injectable map of interaction routing keys → {@link SlackInteractionHandler}.\n *\n * Keys are produced by `interactionRouteKey(payload)`, which combines the\n * payload `type` with the relevant identifier (`action_id`, `callback_id`,\n * etc.). Register handlers under the same key shape:\n *\n * @example\n * ```ts\n * const interactions = new SlackInteractionHandlerMap();\n * interactions.set('block_actions:approve_button', container.get(ApproveHandler));\n * interactions.set('view_submission:create_ticket_modal', container.get(CreateTicketHandler));\n * container.register(SlackInteractionHandlerMap, { useValue: interactions });\n * ```\n */\n@Injectable()\nexport class SlackInteractionHandlerMap extends Map<string, SlackInteractionHandler> {}\n\n/**\n * Single entry point for dispatching parsed Slack payloads to registered\n * handlers. Transport-agnostic: the consumer is responsible for receiving\n * the HTTP request, verifying the signature, parsing the body, calling the\n * appropriate `dispatch*` method, and serializing the response.\n *\n * @example Koa route\n * ```ts\n * router.post('/slack/events', async (ctx) => {\n * const raw = await rawBody(ctx.req, { encoding: 'utf8' });\n * verifySlackSignature({\n * signingSecret: ctx.container.get(SlackConfig).signingSecret,\n * rawBody: raw,\n * timestamp: ctx.get('x-slack-request-timestamp'),\n * signature: ctx.get('x-slack-signature'),\n * });\n * const result = await ctx.container.get(SlackDispatcher).dispatchEvent(JSON.parse(raw));\n * if (result) ctx.body = result;\n * else { ctx.status = 200; ctx.body = ''; }\n * });\n * ```\n */\n@Injectable()\nexport class SlackDispatcher {\n constructor(\n private readonly events: SlackEventHandlerMap,\n private readonly commands: SlackCommandHandlerMap,\n private readonly interactions: SlackInteractionHandlerMap,\n private readonly logger: Logger,\n ) {}\n\n /**\n * Dispatch a parsed Events API body.\n *\n * - Returns `{ challenge }` for `url_verification` — the caller serializes\n * it as the response body. This handshake is NEVER de-duplicated: it must\n * always echo the challenge.\n * - For `event_callback`, looks up a handler in {@link SlackEventHandlerMap}\n * keyed by `event.type` and invokes it. Returns `undefined`. Slack retries\n * any non-2xx so unknown event types are logged at debug and acked.\n * - For any other top-level type, logs and returns `undefined`.\n *\n * Pass `options.idempotency` to de-duplicate `event_callback` deliveries: Slack\n * redelivers events (with an `X-Slack-Retry-Num` header) on a slow/failed ack,\n * so wrapping the handler in an {@link IdempotencyStore} keyed by\n * {@link slackEventIdempotencyKey} runs it at most once per `event_id`. A\n * `duplicate`/`dropped` outcome skips the handler and acks (returns `undefined`).\n * When `options.idempotency` is omitted, behaviour is unchanged.\n */\n async dispatchEvent(body: SlackEventsRequest, options?: { idempotency?: IdempotencyStore }): Promise<SlackEventsResponse> {\n if (body.type === 'url_verification') {\n return { challenge: (body as { challenge: string }).challenge };\n }\n\n if (body.type === 'event_callback') {\n const envelope = body as SlackEventCallback;\n const handleEvent = async (): Promise<void> => {\n const handler = this.events.get(envelope.event.type);\n if (handler) {\n await handler.handle(envelope.event, {\n teamId: envelope.team_id,\n eventId: envelope.event_id,\n eventTime: envelope.event_time,\n envelope,\n });\n } else {\n this.logger.debug('No Slack event handler registered for event type', { type: envelope.event.type });\n }\n };\n\n if (options?.idempotency) {\n const key = slackEventIdempotencyKey(envelope);\n const outcome = await options.idempotency.deduplicate(key, handleEvent);\n if (outcome.status === 'dropped') {\n this.logger.warn('Slack event dead-lettered after repeated failures', { key, attempts: outcome.attempts });\n }\n return undefined;\n }\n\n await handleEvent();\n return undefined;\n }\n\n this.logger.debug('Unhandled Slack events payload type', { type: body.type });\n return undefined;\n }\n\n /**\n * Dispatch a parsed slash-command payload.\n *\n * Looks up a handler in {@link SlackCommandHandlerMap} keyed by\n * `payload.command` (e.g. `/deploy`). If the handler returns a response,\n * the caller serializes it as JSON; otherwise the caller acks with `200 ''`\n * and the handler is expected to follow up via `payload.response_url`.\n */\n async dispatchCommand(payload: SlackCommandPayload): Promise<SlackCommandResponse | void> {\n const handler = this.commands.get(payload.command);\n if (!handler) {\n this.logger.debug('No Slack command handler registered', { command: payload.command });\n return undefined;\n }\n return await handler.handle(payload);\n }\n\n /**\n * Dispatch a parsed interactive payload (block actions, view submission,\n * shortcut, etc.). Computes a routing key via {@link interactionRouteKey}\n * and looks it up in {@link SlackInteractionHandlerMap}.\n */\n async dispatchInteraction(payload: SlackInteractionPayload): Promise<SlackInteractionResponse | void> {\n const key = interactionRouteKey(payload);\n if (!key) {\n this.logger.debug('Slack interaction payload missing routable identifier', { type: payload.type });\n return undefined;\n }\n const handler = this.interactions.get(key);\n if (!handler) {\n this.logger.debug('No Slack interaction handler registered', { key });\n return undefined;\n }\n return await handler.handle(payload);\n }\n}\n","import { Injectable } from 'injectkit';\nimport { WebClient } from '@slack/web-api';\nimport type { ChatPostMessageArguments, ChatPostMessageResponse, ChatUpdateArguments, ChatUpdateResponse, ChatDeleteArguments, ChatDeleteResponse, ViewsOpenArguments, ViewsOpenResponse } from '@slack/web-api';\nimport { Logger } from '@maroonedsoftware/logger';\nimport { SlackConfig } from '../slack.config.js';\nimport { SlackError } from '../slack.error.js';\nimport { adaptLogger } from './slack.logger.adapter.js';\n\n/** Default per-request timeout (ms) applied to outbound `postWebhook` calls. */\nexport const SLACK_DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Redacts a Slack webhook / `response_url` so it is safe to log. The final path\n * segment is the secret token (and the query string can carry secrets too), so\n * both are stripped, leaving only the host and path prefix.\n */\nexport const redactSlackUrl = (raw: string): string => {\n try {\n const url = new URL(raw);\n const segments = url.pathname.split('/').filter(Boolean);\n if (segments.length > 0) segments[segments.length - 1] = '***';\n return `${url.origin}/${segments.join('/')}`;\n } catch {\n return '***';\n }\n};\n\n/**\n * Payload for an incoming-webhook POST. Mirrors the subset of fields Slack's\n * incoming webhooks accept (text, blocks, attachments, response shaping).\n * The body is JSON-stringified verbatim, so any extra fields are preserved.\n */\nexport type IncomingWebhookPayload = {\n text?: string;\n blocks?: unknown[];\n attachments?: unknown[];\n thread_ts?: string;\n response_type?: 'in_channel' | 'ephemeral';\n replace_original?: boolean;\n delete_original?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n [key: string]: unknown;\n};\n\n/**\n * Thin DI-friendly wrapper around `@slack/web-api`'s `WebClient`. Constructed\n * once per request scope (or as a singleton, depending on how the consumer\n * registers it) and exposes typed passthroughs for the most common Web API\n * methods plus a `postWebhook` helper for incoming-webhook URLs and the\n * `response_url` returned by slash commands and interactive payloads.\n *\n * Reach for {@link SlackClient.web} directly for anything else the underlying\n * client supports.\n *\n * @example\n * ```ts\n * await container.get(SlackClient).postMessage({ channel: '#ops', text: 'hello' });\n * await container.get(SlackClient).postWebhook({ text: 'follow-up' }, payload.response_url);\n * ```\n */\n@Injectable()\nexport class SlackClient {\n /** Underlying `@slack/web-api` client. */\n readonly web: WebClient;\n\n constructor(\n private readonly config: SlackConfig,\n private readonly logger: Logger,\n ) {\n this.web = new WebClient(config.botToken, { logger: adaptLogger(logger) });\n }\n\n /** Posts a message via `chat.postMessage`. */\n postMessage(args: ChatPostMessageArguments): Promise<ChatPostMessageResponse> {\n return this.web.chat.postMessage(args);\n }\n\n /** Updates a message via `chat.update`. */\n updateMessage(args: ChatUpdateArguments): Promise<ChatUpdateResponse> {\n return this.web.chat.update(args);\n }\n\n /** Deletes a message via `chat.delete`. */\n deleteMessage(args: ChatDeleteArguments): Promise<ChatDeleteResponse> {\n return this.web.chat.delete(args);\n }\n\n /** Opens a modal view via `views.open`. */\n openView(args: ViewsOpenArguments): Promise<ViewsOpenResponse> {\n return this.web.views.open(args);\n }\n\n /**\n * POSTs a payload to a Slack incoming-webhook-style URL — either the\n * configured `incomingWebhookUrl` or an explicit URL (e.g. the\n * `response_url` from a slash command or interactive payload).\n *\n * @throws {@link SlackError} if no URL is available or the response is non-2xx.\n */\n async postWebhook(payload: IncomingWebhookPayload, url?: string): Promise<void> {\n const target = url ?? this.config.incomingWebhookUrl;\n if (!target) {\n throw new SlackError('SlackClient.postWebhook called but no incomingWebhookUrl is configured and no url was provided');\n }\n const response = await fetch(target, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? SLACK_DEFAULT_REQUEST_TIMEOUT_MS),\n });\n if (!response.ok) {\n const body = await response.text().catch(() => '');\n // `target` is a response_url / incoming-webhook URL whose last path segment\n // is a secret — redact it before it reaches the log or internalDetails.\n const safeUrl = redactSlackUrl(target);\n this.logger.warn('Slack webhook POST returned non-OK status', { status: response.status, body, url: safeUrl });\n throw new SlackError(`Slack webhook POST returned ${response.status}`).withInternalDetails({ status: response.status, body, url: safeUrl });\n }\n }\n}\n","import type { Logger as SlackLogger, LogLevel } from '@slack/web-api';\nimport { Logger } from '@maroonedsoftware/logger';\n\n/**\n * Adapts a ServerKit {@link Logger} to the `@slack/web-api` {@link SlackLogger}\n * interface so the WebClient can route its diagnostics through the host\n * application's logger.\n *\n * The Slack SDK's logger calls `logger.info(...args)` with a variable number\n * of arguments and no separate \"primary message\"; the adapter forwards them\n * to ServerKit's `(message, ...optionalParams)` shape, with an empty-string\n * primary when no args are passed.\n *\n * `setLevel`, `setName`, and `getLevel` are stored locally — ServerKit\n * loggers do not expose these knobs but the SDK expects them on its logger.\n *\n * @param logger - The ServerKit logger to forward calls to.\n * @param name - Initial value for the SDK logger's name. Defaults to `'slack-web-api'`.\n * @returns A `@slack/web-api`-compatible logger object.\n */\nexport const adaptLogger = (logger: Logger, name = 'slack-web-api'): SlackLogger => {\n const state = { name, level: 'info' as LogLevel };\n const forward = (fn: (message: unknown, ...optionalParams: unknown[]) => void) => (...msg: unknown[]) => {\n const [first, ...rest] = msg;\n fn(first ?? '', ...rest);\n };\n return {\n debug: forward(logger.debug.bind(logger)),\n info: forward(logger.info.bind(logger)),\n warn: forward(logger.warn.bind(logger)),\n error: forward(logger.error.bind(logger)),\n setLevel: (level: LogLevel) => {\n state.level = level;\n },\n getLevel: () => state.level,\n setName: (n: string) => {\n state.name = n;\n },\n };\n};\n"],"mappings":"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","names":["Injectable","SlackConfig","ServerkitError","SlackError","ServerkitError","IsSlackError","error","createHmac","timingSafeEqual","DateTime","SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS","verifySlackSignature","input","signingSecret","rawBody","timestamp","signature","maxAgeSeconds","now","Math","floor","DateTime","toSeconds","SlackError","withInternalDetails","reason","ts","Number","isFinite","isInteger","abs","expected","createHmac","update","digest","expectedBuf","Buffer","from","providedBuf","length","timingSafeEqual","Injectable","Policy","SLACK_SIGNATURE_POLICY","SLACK_REQUEST_TIMESTAMP_HEADER","SLACK_SIGNATURE_HEADER","SlackSignaturePolicy","Policy","evaluate","context","envelope","rawBody","getHeader","options","body","Buffer","from","toString","verifySlackSignature","signingSecret","timestamp","signature","maxAgeSeconds","signatureMaxAgeSeconds","now","Math","floor","toSeconds","allow","error","IsSlackError","internalDetails","reason","deny","undefined","message","slackEventIdempotencyKey","envelope","team_id","event_id","interactionRouteKey","payload","type","id","actions","action_id","undefined","view","callback_id","Injectable","Logger","SlackCommandHandlerMap","Map","SlackEventHandlerMap","SlackInteractionHandlerMap","SlackDispatcher","events","commands","interactions","logger","dispatchEvent","body","options","type","challenge","envelope","handleEvent","handler","get","event","handle","teamId","team_id","eventId","event_id","eventTime","event_time","debug","idempotency","key","slackEventIdempotencyKey","outcome","deduplicate","status","warn","attempts","undefined","dispatchCommand","payload","command","dispatchInteraction","interactionRouteKey","Injectable","WebClient","Logger","adaptLogger","logger","name","state","level","forward","fn","msg","first","rest","debug","bind","info","warn","error","setLevel","getLevel","setName","n","SLACK_DEFAULT_REQUEST_TIMEOUT_MS","redactSlackUrl","raw","url","URL","segments","pathname","split","filter","Boolean","length","origin","join","SlackClient","web","config","logger","WebClient","botToken","adaptLogger","postMessage","args","chat","updateMessage","update","deleteMessage","delete","openView","views","open","postWebhook","payload","target","incomingWebhookUrl","SlackError","response","fetch","method","headers","body","JSON","stringify","signal","AbortSignal","timeout","requestTimeoutMs","ok","text","catch","safeUrl","warn","status","withInternalDetails"]}
1
+ {"version":3,"sources":["../src/slack.config.ts","../src/slack.error.ts","../src/slack.signature.ts","../src/slack.signature.policy.ts","../src/slack.event.handler.ts","../src/slack.interaction.handler.ts","../src/slack.dispatcher.ts","../src/client/slack.client.ts","../src/client/slack.logger.adapter.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */\nimport { Injectable } from 'injectkit';\n\n/**\n * Configuration for the Slack package. Declared as an abstract `@Injectable()`\n * class so it doubles as a DI token (mirrors the `Logger` pattern in\n * `@maroonedsoftware/logger`).\n *\n * Consumers register a concrete value at bootstrap, typically resolved from\n * `AppConfig`:\n *\n * ```ts\n * const slackConfig = appConfig.getAs<SlackConfig>('slack');\n * container.register(SlackConfig, { useValue: slackConfig });\n * ```\n *\n * Services in this package take `SlackConfig` directly in their constructor.\n */\nexport interface SlackConfig {\n /** Bot user OAuth token (`xoxb-...`). Required for Web API calls. */\n botToken: string;\n /** App-level signing secret used to verify request signatures. */\n signingSecret: string;\n /** Optional incoming webhook URL used as the default target for `SlackClient.postWebhook`. */\n incomingWebhookUrl?: string;\n /**\n * Maximum age (in seconds) for request timestamps before signature\n * verification rejects them as replays. Defaults to `300` (5 minutes).\n */\n signatureMaxAgeSeconds?: number;\n /**\n * Per-request timeout (in milliseconds) for outbound `SlackClient.postWebhook`\n * calls. Defaults to\n * {@link import('./client/slack.client.js').SLACK_DEFAULT_REQUEST_TIMEOUT_MS} (10s).\n */\n requestTimeoutMs?: number;\n}\n\n@Injectable()\nexport abstract class SlackConfig implements SlackConfig {}\n","import { ServerkitError } from '@maroonedsoftware/errors';\n\n/**\n * Domain error raised by the Slack package for non-HTTP failures (e.g.\n * incoming-webhook POST failed, unknown handler dispatch).\n *\n * Extends {@link ServerkitError} so `errorMiddleware` renders a 500 with\n * `{ message, details }` if one of these escapes a route handler. Inside\n * route handlers, throw `httpError(...)` directly for status-coded responses.\n */\nexport class SlackError extends ServerkitError {}\n\n/**\n * Type guard for {@link SlackError}. Narrows `unknown` to `SlackError` so\n * `details`, `internalDetails`, and the chainable setters are accessible\n * without further checks. Returns `true` for any subclass.\n */\nexport const IsSlackError = (error: unknown): error is SlackError => error instanceof SlackError;\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { DateTime } from 'luxon';\nimport { SlackError } from './slack.error.js';\n\n/** Default replay-protection window in seconds (5 minutes — matches Slack's recommendation). */\nexport const SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS = 300;\n\n/**\n * Reason codes attached to {@link SlackError.internalDetails} when verification\n * fails. Useful for callers that want to log structured reasons without\n * pattern-matching on error messages.\n */\nexport type SlackSignatureFailureReason = 'missing_timestamp' | 'invalid_timestamp' | 'stale_timestamp' | 'missing_signature' | 'invalid_signature';\n\n/**\n * Inputs to {@link verifySlackSignature}. All values are taken verbatim from\n * the request — the helper does no header lookups or body reads of its own.\n */\nexport type VerifySlackSignatureInput = {\n /** App signing secret (`SlackConfig.signingSecret`). */\n signingSecret: string;\n /** Raw, unparsed request body — exactly as Slack sent it. */\n rawBody: string;\n /** Value of the `X-Slack-Request-Timestamp` header. */\n timestamp: string | undefined;\n /** Value of the `X-Slack-Signature` header (e.g. `\"v0=abc123…\"`). */\n signature: string | undefined;\n /**\n * Maximum age in seconds before the request is rejected as a replay.\n * Defaults to {@link SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS}.\n */\n maxAgeSeconds?: number;\n /**\n * Override for the current Unix time in seconds. Mostly useful for tests;\n * defaults to `Math.floor(DateTime.now().toSeconds())`.\n */\n now?: number;\n};\n\n/**\n * Verifies a Slack request signature against the app signing secret.\n *\n * Implements Slack's v0 scheme:\n * 1. Reject the request if `X-Slack-Request-Timestamp` is missing, non-numeric,\n * or older than `maxAgeSeconds` (replay protection).\n * 2. Compute `v0=` + `HMAC-SHA256(signingSecret, \"v0:{timestamp}:{rawBody}\")`\n * as hex.\n * 3. Compare against the provided `X-Slack-Signature` value using a\n * constant-time compare.\n *\n * Pure: no request/context coupling. The caller extracts the headers and raw\n * body from whatever transport it's using and passes them in.\n *\n * @throws {@link SlackError} on any failure. The error's `internalDetails.reason`\n * is one of {@link SlackSignatureFailureReason}; map to HTTP 401 at the route boundary.\n *\n * @example\n * ```ts\n * try {\n * verifySlackSignature({\n * signingSecret: config.signingSecret,\n * rawBody,\n * timestamp: req.headers['x-slack-request-timestamp'],\n * signature: req.headers['x-slack-signature'],\n * });\n * } catch (err) {\n * throw httpError(401).withCause(err);\n * }\n * ```\n */\nexport const verifySlackSignature = (input: VerifySlackSignatureInput): void => {\n const {\n signingSecret,\n rawBody,\n timestamp,\n signature,\n maxAgeSeconds = SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS,\n now = Math.floor(DateTime.now().toSeconds()),\n } = input;\n\n if (!timestamp) {\n throw new SlackError('Slack request missing X-Slack-Request-Timestamp header').withInternalDetails({\n reason: 'missing_timestamp' satisfies SlackSignatureFailureReason,\n });\n }\n\n const ts = Number(timestamp);\n if (!Number.isFinite(ts) || !Number.isInteger(ts)) {\n throw new SlackError('Slack request timestamp is not a valid integer').withInternalDetails({\n reason: 'invalid_timestamp' satisfies SlackSignatureFailureReason,\n timestamp,\n });\n }\n\n if (Math.abs(now - ts) > maxAgeSeconds) {\n throw new SlackError('Slack request timestamp is outside the allowed window').withInternalDetails({\n reason: 'stale_timestamp' satisfies SlackSignatureFailureReason,\n timestamp: ts,\n now,\n maxAgeSeconds,\n });\n }\n\n if (!signature) {\n throw new SlackError('Slack request missing X-Slack-Signature header').withInternalDetails({\n reason: 'missing_signature' satisfies SlackSignatureFailureReason,\n });\n }\n\n // Sign with the raw header value verbatim (not the parsed `ts`): Slack computes\n // its signature over the exact `X-Slack-Request-Timestamp` string it sent, so a\n // non-canonical-but-numeric header (e.g. leading zeros) must round-trip as-is.\n const expected = `v0=${createHmac('sha256', signingSecret).update(`v0:${timestamp}:${rawBody}`).digest('hex')}`;\n const expectedBuf = Buffer.from(expected, 'utf8');\n const providedBuf = Buffer.from(signature, 'utf8');\n\n // timingSafeEqual throws on length mismatch — short-circuit so the caller\n // gets a uniform \"invalid_signature\" error instead of a crypto exception.\n if (expectedBuf.length !== providedBuf.length || !timingSafeEqual(expectedBuf, providedBuf)) {\n throw new SlackError('Slack request signature does not match').withInternalDetails({\n reason: 'invalid_signature' satisfies SlackSignatureFailureReason,\n });\n }\n};\n","import { BinaryLike } from 'node:crypto';\nimport { Injectable } from 'injectkit';\nimport { Policy, PolicyEnvelope, PolicyResult } from '@maroonedsoftware/policies';\nimport { SlackConfig } from './slack.config.js';\nimport { IsSlackError } from './slack.error.js';\nimport { verifySlackSignature, type SlackSignatureFailureReason } from './slack.signature.js';\n\n/**\n * Policy name under which {@link SlackSignaturePolicy} is registered. Use as the\n * key when wiring your `PolicyRegistryMap`, and pass to `PolicyService.check`.\n */\nexport const SLACK_SIGNATURE_POLICY = 'slack.signature.valid' as const;\n\n/** Header carrying the request timestamp Slack signs into the HMAC. */\nexport const SLACK_REQUEST_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp';\n/** Header carrying the `v0=`-prefixed request signature. */\nexport const SLACK_SIGNATURE_HEADER = 'X-Slack-Signature';\n\n/**\n * Configuration the {@link SlackSignaturePolicy} reads. A structural subset of\n * {@link SlackConfig}, so a `SlackConfig` value satisfies it directly — e.g.\n * `requireSignature<SlackSignatureOptions>('slack')` with the Slack config\n * stored under that `AppConfig` key.\n */\nexport type SlackSignatureOptions = Pick<SlackConfig, 'signingSecret' | 'signatureMaxAgeSeconds'>;\n\n/**\n * Context for {@link SlackSignaturePolicy}: the raw request bytes, a\n * case-insensitive header accessor, and the {@link SlackSignatureOptions}.\n *\n * Structurally compatible with `@maroonedsoftware/koa`'s\n * `SignaturePolicyContext<SlackSignatureOptions>`, so the koa `requireSignature`\n * middleware can drive this policy without the slack package depending on koa —\n * register `SlackSignaturePolicy` under the signature policy name and point the\n * middleware at the `AppConfig` key holding the Slack config.\n */\nexport interface SlackSignaturePolicyContext {\n /** Raw, unparsed request body — exactly as Slack sent it (from `ctx.rawBody`). */\n rawBody: BinaryLike;\n /**\n * Case-insensitive request header accessor (Koa's `ctx.get`); returns `''`\n * when the header is absent.\n */\n getHeader: (name: string) => string;\n /** Slack signing configuration. */\n options: SlackSignatureOptions;\n}\n\n/**\n * Policy form of {@link verifySlackSignature}: verifies a Slack request against\n * the app signing secret using Slack's v0 scheme (HMAC over\n * `v0:{timestamp}:{rawBody}`, `v0=`-prefixed, with timestamp replay\n * protection).\n *\n * Delegates to {@link verifySlackSignature} so the crypto/timestamp logic has a\n * single source of truth, but answers as a {@link PolicyResult} rather than\n * throwing: allows on success, denies on failure with the helper's\n * {@link SlackSignatureFailureReason} as the denial `reason` and its diagnostics\n * (timestamps, window) on `internalDetails` — never the signing secret, never\n * on the wire. The replay window is anchored to `envelope.now` so all policies\n * in an evaluation share one clock.\n *\n * Registered by default under {@link SLACK_SIGNATURE_POLICY}.\n *\n * @example\n * ```ts\n * // Direct evaluation in a route handler:\n * const result = await policyService.check(SLACK_SIGNATURE_POLICY, {\n * rawBody: ctx.rawBody,\n * getHeader: name => ctx.get(name),\n * options: ctx.container.get(SlackConfig),\n * });\n * if (isPolicyResultDenied(result)) throw httpError(401);\n * ```\n */\n@Injectable()\nexport class SlackSignaturePolicy extends Policy<SlackSignaturePolicyContext> {\n async evaluate(context: SlackSignaturePolicyContext, envelope: PolicyEnvelope): Promise<PolicyResult> {\n const { rawBody, getHeader, options } = context;\n\n // Slack signs the raw text body; `ctx.rawBody` may arrive as a Buffer.\n const body = typeof rawBody === 'string' ? rawBody : Buffer.from(rawBody as Uint8Array).toString('utf8');\n\n try {\n verifySlackSignature({\n signingSecret: options.signingSecret,\n rawBody: body,\n timestamp: getHeader(SLACK_REQUEST_TIMESTAMP_HEADER),\n signature: getHeader(SLACK_SIGNATURE_HEADER),\n maxAgeSeconds: options.signatureMaxAgeSeconds,\n now: Math.floor(envelope.now.toSeconds()),\n });\n return this.allow();\n } catch (error) {\n if (!IsSlackError(error)) throw error;\n\n const internalDetails = error.internalDetails ?? {};\n const reason =\n typeof internalDetails.reason === 'string' ? internalDetails.reason : ('invalid_signature' satisfies SlackSignatureFailureReason);\n return this.deny(reason, undefined, { message: error.message, ...internalDetails });\n }\n }\n}\n","/**\n * Metadata accompanying every dispatched Slack event. Includes the wrapping\n * envelope fields (team/event IDs) plus the raw `event_callback` payload for\n * handlers that need fields the typed `event` object doesn't expose.\n */\nexport type SlackEventContext = {\n /** Slack workspace / team ID from the envelope. */\n teamId: string;\n /** Unique event ID Slack assigns to each delivery. */\n eventId: string;\n /** Unix timestamp the event was generated. */\n eventTime: number;\n /** Original `event_callback` envelope, untouched. */\n envelope: SlackEventCallback;\n};\n\n/**\n * Slack `event_callback` envelope. The shape is documented at\n * https://api.slack.com/types/event. We type the wrapper but leave the inner\n * `event` as `Record<string, unknown>` because the union of all Slack event\n * payloads is large and consumers typically narrow per handler.\n */\nexport type SlackEventCallback = {\n type: 'event_callback';\n team_id: string;\n api_app_id: string;\n event: { type: string } & Record<string, unknown>;\n event_id: string;\n event_time: number;\n authorizations?: unknown[];\n is_ext_shared_channel?: boolean;\n event_context?: string;\n [key: string]: unknown;\n};\n\n/**\n * Derive a stable, collision-free idempotency key for a Slack event delivery.\n *\n * Slack redelivers an `event_callback` (with an `X-Slack-Retry-Num` header) when\n * the initial ack is slow or non-2xx. The assigned `event_id` is stable across\n * those redeliveries, so it keys de-duplication. We scope it by `team_id` where\n * present so ids from different workspaces can never collide.\n *\n * @param envelope - The `event_callback` envelope (only `event_id` / `team_id` are read).\n * @returns `slack:event:{team_id}:{event_id}`, or `slack:event:{event_id}` when no team id.\n */\nexport function slackEventIdempotencyKey(envelope: Pick<SlackEventCallback, 'event_id' | 'team_id'>): string {\n return envelope.team_id ? `slack:event:${envelope.team_id}:${envelope.event_id}` : `slack:event:${envelope.event_id}`;\n}\n\n/**\n * Handler for a single Slack event type (e.g. `app_mention`, `message`,\n * `reaction_added`). Registered in {@link SlackEventHandlerMap}.\n *\n * Handlers should ack quickly — Slack retries any event that doesn't get a\n * 2xx response within ~3 seconds. For slow work, enqueue a job\n * (`@maroonedsoftware/jobbroker`) inside `handle` and return immediately.\n */\nexport interface SlackEventHandler<TEvent extends { type: string } & Record<string, unknown> = { type: string } & Record<string, unknown>> {\n handle(event: TEvent, context: SlackEventContext): Promise<void>;\n}\n","/**\n * The supported interactive payload types Slack POSTs to the interactivity\n * endpoint. Each maps to a different identifier shape (see\n * {@link interactionRouteKey}).\n */\nexport type SlackInteractionType = 'block_actions' | 'view_submission' | 'view_closed' | 'shortcut' | 'message_action' | string;\n\n/**\n * Loose typing for the interactive payload; consumers narrow per handler.\n * Slack's payloads vary by type, but every variant has a `type` field plus\n * one of: `actions[].action_id`, `view.callback_id`, or top-level `callback_id`.\n */\nexport type SlackInteractionPayload = {\n type: SlackInteractionType;\n team?: { id: string; domain?: string };\n user?: { id: string; name?: string };\n trigger_id?: string;\n response_url?: string;\n actions?: Array<{ action_id: string; block_id?: string; value?: string; [key: string]: unknown }>;\n view?: { id: string; callback_id: string; [key: string]: unknown };\n callback_id?: string;\n [key: string]: unknown;\n};\n\n/**\n * Optional response Slack accepts for `view_submission` / `view_closed`\n * payloads (e.g. to display validation errors or update a modal).\n */\nexport type SlackInteractionResponse = {\n response_action?: 'errors' | 'update' | 'push' | 'clear';\n errors?: Record<string, string>;\n view?: unknown;\n [key: string]: unknown;\n};\n\n/**\n * Handler for one interactive payload, keyed in {@link SlackInteractionHandlerMap}\n * by `${type}:${identifier}` — see {@link interactionRouteKey}.\n */\nexport interface SlackInteractionHandler {\n handle(payload: SlackInteractionPayload): Promise<SlackInteractionResponse | void>;\n}\n\n/**\n * Computes the routing key used by {@link SlackDispatcher.dispatchInteraction}\n * to look a handler up in {@link SlackInteractionHandlerMap}.\n *\n * - `block_actions` → `block_actions:<first action.action_id>`\n * - `view_submission` / `view_closed` → `<type>:<view.callback_id>`\n * - `shortcut` / `message_action` → `<type>:<callback_id>`\n * - any other type with a `callback_id` → `<type>:<callback_id>`\n *\n * @returns The routing key, or `undefined` if the payload doesn't carry an\n * identifier we can route on (e.g. a `block_actions` payload with no actions).\n */\nexport const interactionRouteKey = (payload: SlackInteractionPayload): string | undefined => {\n switch (payload.type) {\n case 'block_actions': {\n const id = payload.actions?.[0]?.action_id;\n return id ? `block_actions:${id}` : undefined;\n }\n case 'view_submission':\n case 'view_closed': {\n const id = payload.view?.callback_id;\n return id ? `${payload.type}:${id}` : undefined;\n }\n case 'shortcut':\n case 'message_action': {\n return payload.callback_id ? `${payload.type}:${payload.callback_id}` : undefined;\n }\n default: {\n return payload.callback_id ? `${payload.type}:${payload.callback_id}` : undefined;\n }\n }\n};\n","import { Injectable } from 'injectkit';\nimport { Logger } from '@maroonedsoftware/logger';\nimport type { IdempotencyStore } from '@maroonedsoftware/cache';\nimport { slackEventIdempotencyKey } from './slack.event.handler.js';\nimport type { SlackEventCallback, SlackEventHandler } from './slack.event.handler.js';\nimport type { SlackCommandHandler, SlackCommandPayload, SlackCommandResponse } from './slack.command.handler.js';\nimport {\n interactionRouteKey,\n SlackInteractionHandler,\n type SlackInteractionPayload,\n type SlackInteractionResponse,\n} from './slack.interaction.handler.js';\n\n/**\n * Body shape Slack POSTs to the Events API endpoint. The handshake variant\n * (`url_verification`) is sent once during app configuration; the rest of the\n * traffic is `event_callback` envelopes (or other future top-level types).\n */\nexport type SlackEventsRequest =\n { type: 'url_verification'; challenge: string; token?: string } | SlackEventCallback | { type: string; [key: string]: unknown };\n\n/**\n * Response Slack expects for the `url_verification` handshake. For\n * `event_callback` and unknown event types, the dispatcher returns\n * `undefined` and the caller should ack with HTTP 200.\n */\nexport type SlackEventsResponse = { challenge: string } | undefined;\n\n/**\n * Injectable map of command keyword (e.g. `/deploy`) → {@link SlackCommandHandler}.\n *\n * @example\n * ```ts\n * const commands = new SlackCommandHandlerMap();\n * commands.set('/deploy', container.get(DeployCommandHandler));\n * container.register(SlackCommandHandlerMap, { useValue: commands });\n * ```\n */\n@Injectable()\nexport class SlackCommandHandlerMap extends Map<string, SlackCommandHandler> {}\n\n/**\n * Injectable map of Slack event type → {@link SlackEventHandler}. Consumers\n * register handlers at bootstrap and place an instance of this map in their\n * DI container; {@link SlackDispatcher.dispatchEvent} resolves it per request.\n *\n * @example\n * ```ts\n * const handlers = new SlackEventHandlerMap();\n * handlers.set('app_mention', container.get(MyAppMentionHandler));\n * container.register(SlackEventHandlerMap, { useValue: handlers });\n * ```\n */\n@Injectable()\nexport class SlackEventHandlerMap extends Map<string, SlackEventHandler> {}\n\n/**\n * Injectable map of interaction routing keys → {@link SlackInteractionHandler}.\n *\n * Keys are produced by `interactionRouteKey(payload)`, which combines the\n * payload `type` with the relevant identifier (`action_id`, `callback_id`,\n * etc.). Register handlers under the same key shape:\n *\n * @example\n * ```ts\n * const interactions = new SlackInteractionHandlerMap();\n * interactions.set('block_actions:approve_button', container.get(ApproveHandler));\n * interactions.set('view_submission:create_ticket_modal', container.get(CreateTicketHandler));\n * container.register(SlackInteractionHandlerMap, { useValue: interactions });\n * ```\n */\n@Injectable()\nexport class SlackInteractionHandlerMap extends Map<string, SlackInteractionHandler> {}\n\n/**\n * Single entry point for dispatching parsed Slack payloads to registered\n * handlers. Transport-agnostic: the consumer is responsible for receiving\n * the HTTP request, verifying the signature, parsing the body, calling the\n * appropriate `dispatch*` method, and serializing the response.\n *\n * @example Koa route\n * ```ts\n * router.post('/slack/events', async (ctx) => {\n * const raw = await rawBody(ctx.req, { encoding: 'utf8' });\n * verifySlackSignature({\n * signingSecret: ctx.container.get(SlackConfig).signingSecret,\n * rawBody: raw,\n * timestamp: ctx.get('x-slack-request-timestamp'),\n * signature: ctx.get('x-slack-signature'),\n * });\n * const result = await ctx.container.get(SlackDispatcher).dispatchEvent(JSON.parse(raw));\n * if (result) ctx.body = result;\n * else { ctx.status = 200; ctx.body = ''; }\n * });\n * ```\n */\n@Injectable()\nexport class SlackDispatcher {\n constructor(\n private readonly events: SlackEventHandlerMap,\n private readonly commands: SlackCommandHandlerMap,\n private readonly interactions: SlackInteractionHandlerMap,\n private readonly logger: Logger,\n ) {}\n\n /**\n * Dispatch a parsed Events API body.\n *\n * - Returns `{ challenge }` for `url_verification` — the caller serializes\n * it as the response body. This handshake is NEVER de-duplicated: it must\n * always echo the challenge.\n * - For `event_callback`, looks up a handler in {@link SlackEventHandlerMap}\n * keyed by `event.type` and invokes it. Returns `undefined`. Slack retries\n * any non-2xx so unknown event types are logged at debug and acked.\n * - For any other top-level type, logs and returns `undefined`.\n *\n * Pass `options.idempotency` to de-duplicate `event_callback` deliveries: Slack\n * redelivers events (with an `X-Slack-Retry-Num` header) on a slow/failed ack,\n * so wrapping the handler in an {@link IdempotencyStore} keyed by\n * {@link slackEventIdempotencyKey} runs it at most once per `event_id`. A\n * `duplicate`/`dropped` outcome skips the handler and acks (returns `undefined`).\n * When `options.idempotency` is omitted, behaviour is unchanged.\n */\n async dispatchEvent(body: SlackEventsRequest, options?: { idempotency?: IdempotencyStore }): Promise<SlackEventsResponse> {\n if (body.type === 'url_verification') {\n return { challenge: (body as { challenge: string }).challenge };\n }\n\n if (body.type === 'event_callback') {\n const envelope = body as SlackEventCallback;\n const handleEvent = async (): Promise<void> => {\n const handler = this.events.get(envelope.event.type);\n if (handler) {\n await handler.handle(envelope.event, {\n teamId: envelope.team_id,\n eventId: envelope.event_id,\n eventTime: envelope.event_time,\n envelope,\n });\n } else {\n this.logger.debug('No Slack event handler registered for event type', { type: envelope.event.type });\n }\n };\n\n if (options?.idempotency) {\n const key = slackEventIdempotencyKey(envelope);\n const outcome = await options.idempotency.deduplicate(key, handleEvent);\n if (outcome.status === 'dropped') {\n this.logger.warn('Slack event dead-lettered after repeated failures', { key, attempts: outcome.attempts });\n }\n return undefined;\n }\n\n await handleEvent();\n return undefined;\n }\n\n this.logger.debug('Unhandled Slack events payload type', { type: body.type });\n return undefined;\n }\n\n /**\n * Dispatch a parsed slash-command payload.\n *\n * Looks up a handler in {@link SlackCommandHandlerMap} keyed by\n * `payload.command` (e.g. `/deploy`). If the handler returns a response,\n * the caller serializes it as JSON; otherwise the caller acks with `200 ''`\n * and the handler is expected to follow up via `payload.response_url`.\n */\n async dispatchCommand(payload: SlackCommandPayload): Promise<SlackCommandResponse | void> {\n const handler = this.commands.get(payload.command);\n if (!handler) {\n this.logger.debug('No Slack command handler registered', { command: payload.command });\n return undefined;\n }\n return await handler.handle(payload);\n }\n\n /**\n * Dispatch a parsed interactive payload (block actions, view submission,\n * shortcut, etc.). Computes a routing key via {@link interactionRouteKey}\n * and looks it up in {@link SlackInteractionHandlerMap}.\n */\n async dispatchInteraction(payload: SlackInteractionPayload): Promise<SlackInteractionResponse | void> {\n const key = interactionRouteKey(payload);\n if (!key) {\n this.logger.debug('Slack interaction payload missing routable identifier', { type: payload.type });\n return undefined;\n }\n const handler = this.interactions.get(key);\n if (!handler) {\n this.logger.debug('No Slack interaction handler registered', { key });\n return undefined;\n }\n return await handler.handle(payload);\n }\n}\n","import { Injectable } from 'injectkit';\nimport { WebClient } from '@slack/web-api';\nimport type {\n ChatPostMessageArguments,\n ChatPostMessageResponse,\n ChatUpdateArguments,\n ChatUpdateResponse,\n ChatDeleteArguments,\n ChatDeleteResponse,\n ViewsOpenArguments,\n ViewsOpenResponse,\n} from '@slack/web-api';\nimport { Logger } from '@maroonedsoftware/logger';\nimport { SlackConfig } from '../slack.config.js';\nimport { SlackError } from '../slack.error.js';\nimport { adaptLogger } from './slack.logger.adapter.js';\n\n/** Default per-request timeout (ms) applied to outbound `postWebhook` calls. */\nexport const SLACK_DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Redacts a Slack webhook / `response_url` so it is safe to log. The final path\n * segment is the secret token (and the query string can carry secrets too), so\n * both are stripped, leaving only the host and path prefix.\n */\nexport const redactSlackUrl = (raw: string): string => {\n try {\n const url = new URL(raw);\n const segments = url.pathname.split('/').filter(Boolean);\n if (segments.length > 0) segments[segments.length - 1] = '***';\n return `${url.origin}/${segments.join('/')}`;\n } catch {\n return '***';\n }\n};\n\n/**\n * Payload for an incoming-webhook POST. Mirrors the subset of fields Slack's\n * incoming webhooks accept (text, blocks, attachments, response shaping).\n * The body is JSON-stringified verbatim, so any extra fields are preserved.\n */\nexport type IncomingWebhookPayload = {\n text?: string;\n blocks?: unknown[];\n attachments?: unknown[];\n thread_ts?: string;\n response_type?: 'in_channel' | 'ephemeral';\n replace_original?: boolean;\n delete_original?: boolean;\n unfurl_links?: boolean;\n unfurl_media?: boolean;\n [key: string]: unknown;\n};\n\n/**\n * Thin DI-friendly wrapper around `@slack/web-api`'s `WebClient`. Constructed\n * once per request scope (or as a singleton, depending on how the consumer\n * registers it) and exposes typed passthroughs for the most common Web API\n * methods plus a `postWebhook` helper for incoming-webhook URLs and the\n * `response_url` returned by slash commands and interactive payloads.\n *\n * Reach for {@link SlackClient.web} directly for anything else the underlying\n * client supports.\n *\n * @example\n * ```ts\n * await container.get(SlackClient).postMessage({ channel: '#ops', text: 'hello' });\n * await container.get(SlackClient).postWebhook({ text: 'follow-up' }, payload.response_url);\n * ```\n */\n@Injectable()\nexport class SlackClient {\n /** Underlying `@slack/web-api` client. */\n readonly web: WebClient;\n\n constructor(\n private readonly config: SlackConfig,\n private readonly logger: Logger,\n ) {\n this.web = new WebClient(config.botToken, { logger: adaptLogger(logger) });\n }\n\n /** Posts a message via `chat.postMessage`. */\n postMessage(args: ChatPostMessageArguments): Promise<ChatPostMessageResponse> {\n return this.web.chat.postMessage(args);\n }\n\n /** Updates a message via `chat.update`. */\n updateMessage(args: ChatUpdateArguments): Promise<ChatUpdateResponse> {\n return this.web.chat.update(args);\n }\n\n /** Deletes a message via `chat.delete`. */\n deleteMessage(args: ChatDeleteArguments): Promise<ChatDeleteResponse> {\n return this.web.chat.delete(args);\n }\n\n /** Opens a modal view via `views.open`. */\n openView(args: ViewsOpenArguments): Promise<ViewsOpenResponse> {\n return this.web.views.open(args);\n }\n\n /**\n * POSTs a payload to a Slack incoming-webhook-style URL — either the\n * configured `incomingWebhookUrl` or an explicit URL (e.g. the\n * `response_url` from a slash command or interactive payload).\n *\n * @throws {@link SlackError} if no URL is available or the response is non-2xx.\n */\n async postWebhook(payload: IncomingWebhookPayload, url?: string): Promise<void> {\n const target = url ?? this.config.incomingWebhookUrl;\n if (!target) {\n throw new SlackError('SlackClient.postWebhook called but no incomingWebhookUrl is configured and no url was provided');\n }\n const response = await fetch(target, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n signal: AbortSignal.timeout(this.config.requestTimeoutMs ?? SLACK_DEFAULT_REQUEST_TIMEOUT_MS),\n });\n if (!response.ok) {\n const body = await response.text().catch(() => '');\n // `target` is a response_url / incoming-webhook URL whose last path segment\n // is a secret — redact it before it reaches the log or internalDetails.\n const safeUrl = redactSlackUrl(target);\n this.logger.warn('Slack webhook POST returned non-OK status', { status: response.status, body, url: safeUrl });\n throw new SlackError(`Slack webhook POST returned ${response.status}`).withInternalDetails({ status: response.status, body, url: safeUrl });\n }\n }\n}\n","import type { Logger as SlackLogger, LogLevel } from '@slack/web-api';\nimport { Logger } from '@maroonedsoftware/logger';\n\n/**\n * Adapts a ServerKit {@link Logger} to the `@slack/web-api` {@link SlackLogger}\n * interface so the WebClient can route its diagnostics through the host\n * application's logger.\n *\n * The Slack SDK's logger calls `logger.info(...args)` with a variable number\n * of arguments and no separate \"primary message\"; the adapter forwards them\n * to ServerKit's `(message, ...optionalParams)` shape, with an empty-string\n * primary when no args are passed.\n *\n * `setLevel`, `setName`, and `getLevel` are stored locally — ServerKit\n * loggers do not expose these knobs but the SDK expects them on its logger.\n *\n * @param logger - The ServerKit logger to forward calls to.\n * @param name - Initial value for the SDK logger's name. Defaults to `'slack-web-api'`.\n * @returns A `@slack/web-api`-compatible logger object.\n */\nexport const adaptLogger = (logger: Logger, name = 'slack-web-api'): SlackLogger => {\n const state = { name, level: 'info' as LogLevel };\n const forward =\n (fn: (message: unknown, ...optionalParams: unknown[]) => void) =>\n (...msg: unknown[]) => {\n const [first, ...rest] = msg;\n fn(first ?? '', ...rest);\n };\n return {\n debug: forward(logger.debug.bind(logger)),\n info: forward(logger.info.bind(logger)),\n warn: forward(logger.warn.bind(logger)),\n error: forward(logger.error.bind(logger)),\n setLevel: (level: LogLevel) => {\n state.level = level;\n },\n getLevel: () => state.level,\n setName: (n: string) => {\n state.name = n;\n },\n };\n};\n"],"mappings":"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","names":["Injectable","SlackConfig","ServerkitError","SlackError","ServerkitError","IsSlackError","error","createHmac","timingSafeEqual","DateTime","SLACK_SIGNATURE_DEFAULT_MAX_AGE_SECONDS","verifySlackSignature","input","signingSecret","rawBody","timestamp","signature","maxAgeSeconds","now","Math","floor","DateTime","toSeconds","SlackError","withInternalDetails","reason","ts","Number","isFinite","isInteger","abs","expected","createHmac","update","digest","expectedBuf","Buffer","from","providedBuf","length","timingSafeEqual","Injectable","Policy","SLACK_SIGNATURE_POLICY","SLACK_REQUEST_TIMESTAMP_HEADER","SLACK_SIGNATURE_HEADER","SlackSignaturePolicy","Policy","evaluate","context","envelope","rawBody","getHeader","options","body","Buffer","from","toString","verifySlackSignature","signingSecret","timestamp","signature","maxAgeSeconds","signatureMaxAgeSeconds","now","Math","floor","toSeconds","allow","error","IsSlackError","internalDetails","reason","deny","undefined","message","slackEventIdempotencyKey","envelope","team_id","event_id","interactionRouteKey","payload","type","id","actions","action_id","undefined","view","callback_id","Injectable","Logger","SlackCommandHandlerMap","Map","SlackEventHandlerMap","SlackInteractionHandlerMap","SlackDispatcher","events","commands","interactions","logger","dispatchEvent","body","options","type","challenge","envelope","handleEvent","handler","get","event","handle","teamId","team_id","eventId","event_id","eventTime","event_time","debug","idempotency","key","slackEventIdempotencyKey","outcome","deduplicate","status","warn","attempts","undefined","dispatchCommand","payload","command","dispatchInteraction","interactionRouteKey","Injectable","WebClient","Logger","adaptLogger","logger","name","state","level","forward","fn","msg","first","rest","debug","bind","info","warn","error","setLevel","getLevel","setName","n","SLACK_DEFAULT_REQUEST_TIMEOUT_MS","redactSlackUrl","raw","url","URL","segments","pathname","split","filter","Boolean","length","origin","join","SlackClient","web","config","logger","WebClient","botToken","adaptLogger","postMessage","args","chat","updateMessage","update","deleteMessage","delete","openView","views","open","postWebhook","payload","target","incomingWebhookUrl","SlackError","response","fetch","method","headers","body","JSON","stringify","signal","AbortSignal","timeout","requestTimeoutMs","ok","text","catch","safeUrl","warn","status","withInternalDetails"]}
@@ -1 +1 @@
1
- {"version":3,"file":"slack.dispatcher.d.ts","sourceRoot":"","sources":["../src/slack.dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACjH,OAAO,EAEL,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AAExC;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/D,kBAAkB,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE7C;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAEpE;;;;;;;;;GASG;AACH,qBACa,sBAAuB,SAAQ,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC;CAAG;AAE/E;;;;;;;;;;;GAWG;AACH,qBACa,oBAAqB,SAAQ,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC;CAAG;AAE3E;;;;;;;;;;;;;;GAcG;AACH,qBACa,0BAA2B,SAAQ,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAAG;AAEvF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBACa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAHN,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,EAAE,sBAAsB,EAChC,YAAY,EAAE,0BAA0B,EACxC,MAAM,EAAE,MAAM;IAGjC;;;;;;;;;;;;;;;;;OAiBG;IACG,aAAa,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,gBAAgB,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsCzH;;;;;;;OAOG;IACG,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IASzF;;;;OAIG;IACG,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;CAatG"}
1
+ {"version":3,"file":"slack.dispatcher.d.ts","sourceRoot":"","sources":["../src/slack.dispatcher.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACtF,OAAO,KAAK,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACjH,OAAO,EAEL,uBAAuB,EACvB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,gCAAgC,CAAC;AAExC;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAC5B;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAElI;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAAC;AAEpE;;;;;;;;;GASG;AACH,qBACa,sBAAuB,SAAQ,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC;CAAG;AAE/E;;;;;;;;;;;GAWG;AACH,qBACa,oBAAqB,SAAQ,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC;CAAG;AAE3E;;;;;;;;;;;;;;GAcG;AACH,qBACa,0BAA2B,SAAQ,GAAG,CAAC,MAAM,EAAE,uBAAuB,CAAC;CAAG;AAEvF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBACa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAHN,MAAM,EAAE,oBAAoB,EAC5B,QAAQ,EAAE,sBAAsB,EAChC,YAAY,EAAE,0BAA0B,EACxC,MAAM,EAAE,MAAM;IAGjC;;;;;;;;;;;;;;;;;OAiBG;IACG,aAAa,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,gBAAgB,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsCzH;;;;;;;OAOG;IACG,eAAe,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IASzF;;;;OAIG;IACG,mBAAmB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,wBAAwB,GAAG,IAAI,CAAC;CAatG"}
@@ -1 +1 @@
1
- {"version":3,"file":"slack.signature.d.ts","sourceRoot":"","sources":["../src/slack.signature.ts"],"names":[],"mappings":"AAIA,gGAAgG;AAChG,eAAO,MAAM,uCAAuC,MAAM,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GACnC,mBAAmB,GACnB,mBAAmB,GACnB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,CAAC;AAExB;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,yBAAyB,KAAG,IAqDvE,CAAC"}
1
+ {"version":3,"file":"slack.signature.d.ts","sourceRoot":"","sources":["../src/slack.signature.ts"],"names":[],"mappings":"AAIA,gGAAgG;AAChG,eAAO,MAAM,uCAAuC,MAAM,CAAC;AAE3D;;;;GAIG;AACH,MAAM,MAAM,2BAA2B,GAAG,mBAAmB,GAAG,mBAAmB,GAAG,iBAAiB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAEpJ;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,qEAAqE;IACrE,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,oBAAoB,GAAI,OAAO,yBAAyB,KAAG,IAqDvE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"slack.signature.policy.d.ts","sourceRoot":"","sources":["../src/slack.signature.policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAG,uBAAgC,CAAC;AAEvE,uEAAuE;AACvE,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAC1E,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,wBAAwB,CAAC,CAAC;AAElG;;;;;;;;;GASG;AACH,MAAM,WAAW,2BAA2B;IAC1C,kFAAkF;IAClF,OAAO,EAAE,UAAU,CAAC;IACpB;;;OAGG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,mCAAmC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBACa,oBAAqB,SAAQ,MAAM,CAAC,2BAA2B,CAAC;IACrE,QAAQ,CAAC,OAAO,EAAE,2BAA2B,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;CAwBtG"}
1
+ {"version":3,"file":"slack.signature.policy.d.ts","sourceRoot":"","sources":["../src/slack.signature.policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAG,uBAAgC,CAAC;AAEvE,uEAAuE;AACvE,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAC1E,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,GAAG,wBAAwB,CAAC,CAAC;AAElG;;;;;;;;;GASG;AACH,MAAM,WAAW,2BAA2B;IAC1C,kFAAkF;IAClF,OAAO,EAAE,UAAU,CAAC;IACpB;;;OAGG;IACH,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,mCAAmC;IACnC,OAAO,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBACa,oBAAqB,SAAQ,MAAM,CAAC,2BAA2B,CAAC;IACrE,QAAQ,CAAC,OAAO,EAAE,2BAA2B,EAAE,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC;CAyBtG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/slack",
3
- "version": "3.0.0",
3
+ "version": "3.0.4",
4
4
  "description": "Slack utilities for ServerKit.",
5
5
  "author": {
6
6
  "name": "Marooned Software",
@@ -38,26 +38,27 @@
38
38
  },
39
39
  "license": "MIT",
40
40
  "files": [
41
+ "AGENTS.md",
41
42
  "dist/**"
42
43
  ],
43
44
  "dependencies": {
44
45
  "@slack/web-api": "^7.18.0",
45
46
  "injectkit": "^1.6.0",
46
47
  "luxon": "^3.7.2",
47
- "@maroonedsoftware/errors": "1.8.0",
48
- "@maroonedsoftware/logger": "1.1.3",
49
- "@maroonedsoftware/policies": "0.5.3"
48
+ "@maroonedsoftware/errors": "1.8.3",
49
+ "@maroonedsoftware/policies": "0.6.3",
50
+ "@maroonedsoftware/logger": "1.1.6"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/luxon": "^3.7.2",
53
- "@maroonedsoftware/cache": "0.4.0",
54
- "@maroonedsoftware/comms": "0.2.3",
55
- "@repo/config-eslint": "0.2.1",
56
- "@repo/config-typescript": "0.1.0"
54
+ "@maroonedsoftware/comms": "0.2.6",
55
+ "@maroonedsoftware/cache": "0.4.3",
56
+ "@repo/config-typescript": "0.1.0",
57
+ "@repo/config-eslint": "0.2.1"
57
58
  },
58
59
  "peerDependencies": {
59
- "@maroonedsoftware/cache": "0.4.0",
60
- "@maroonedsoftware/comms": "0.2.3"
60
+ "@maroonedsoftware/cache": "0.4.3",
61
+ "@maroonedsoftware/comms": "0.2.6"
61
62
  },
62
63
  "peerDependenciesMeta": {
63
64
  "@maroonedsoftware/cache": {