@moxxy/plugin-channel-slack 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/dist/channel/slack-client.d.ts +53 -0
  3. package/dist/channel/slack-client.d.ts.map +1 -0
  4. package/dist/channel/slack-client.js +82 -0
  5. package/dist/channel/slack-client.js.map +1 -0
  6. package/dist/channel/turn-runner.d.ts +39 -0
  7. package/dist/channel/turn-runner.d.ts.map +1 -0
  8. package/dist/channel/turn-runner.js +81 -0
  9. package/dist/channel/turn-runner.js.map +1 -0
  10. package/dist/channel.d.ts +100 -0
  11. package/dist/channel.d.ts.map +1 -0
  12. package/dist/channel.js +286 -0
  13. package/dist/channel.js.map +1 -0
  14. package/dist/index.d.ts +31 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +207 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/keys.d.ts +50 -0
  19. package/dist/keys.d.ts.map +1 -0
  20. package/dist/keys.js +78 -0
  21. package/dist/keys.js.map +1 -0
  22. package/dist/pair-flow.d.ts +18 -0
  23. package/dist/pair-flow.d.ts.map +1 -0
  24. package/dist/pair-flow.js +106 -0
  25. package/dist/pair-flow.js.map +1 -0
  26. package/dist/permission.d.ts +27 -0
  27. package/dist/permission.d.ts.map +1 -0
  28. package/dist/permission.js +33 -0
  29. package/dist/permission.js.map +1 -0
  30. package/dist/server/dedupe.d.ts +8 -0
  31. package/dist/server/dedupe.d.ts.map +1 -0
  32. package/dist/server/dedupe.js +8 -0
  33. package/dist/server/dedupe.js.map +1 -0
  34. package/dist/server/ingest-server.d.ts +80 -0
  35. package/dist/server/ingest-server.d.ts.map +1 -0
  36. package/dist/server/ingest-server.js +142 -0
  37. package/dist/server/ingest-server.js.map +1 -0
  38. package/dist/server/schema.d.ts +257 -0
  39. package/dist/server/schema.d.ts.map +1 -0
  40. package/dist/server/schema.js +69 -0
  41. package/dist/server/schema.js.map +1 -0
  42. package/dist/server/verify.d.ts +39 -0
  43. package/dist/server/verify.d.ts.map +1 -0
  44. package/dist/server/verify.js +73 -0
  45. package/dist/server/verify.js.map +1 -0
  46. package/dist/setup-wizard.d.ts +17 -0
  47. package/dist/setup-wizard.d.ts.map +1 -0
  48. package/dist/setup-wizard.js +92 -0
  49. package/dist/setup-wizard.js.map +1 -0
  50. package/package.json +97 -0
  51. package/src/channel/slack-client.ts +123 -0
  52. package/src/channel/turn-runner.test.ts +143 -0
  53. package/src/channel/turn-runner.ts +107 -0
  54. package/src/channel.ts +378 -0
  55. package/src/index.ts +254 -0
  56. package/src/keys.ts +96 -0
  57. package/src/pair-flow.ts +119 -0
  58. package/src/permission.test.ts +65 -0
  59. package/src/permission.ts +42 -0
  60. package/src/server/dedupe.ts +7 -0
  61. package/src/server/ingest-server.test.ts +280 -0
  62. package/src/server/ingest-server.ts +205 -0
  63. package/src/server/schema.test.ts +67 -0
  64. package/src/server/schema.ts +77 -0
  65. package/src/server/verify.test.ts +132 -0
  66. package/src/server/verify.ts +88 -0
  67. package/src/setup-wizard.ts +121 -0
  68. package/src/subcommands.test.ts +189 -0
package/dist/keys.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Vault keys + env overrides + token validators shared by the channel, its
3
+ * subcommands, and the interactive setup wizard. Kept in their own module so the
4
+ * wizard / pair-flow helpers can import them without pulling in the plugin's
5
+ * full index.
6
+ */
7
+ import { z } from '@moxxy/sdk';
8
+ import { resolveSecret } from '@moxxy/channel-kit';
9
+ /** Vault key for the Slack bot OAuth token (`xoxb-…`). */
10
+ export const SLACK_BOT_TOKEN_KEY = 'slack_bot_token';
11
+ /** Vault key for the Slack app signing secret (HMAC over the raw request body). */
12
+ export const SLACK_SIGNING_SECRET_KEY = 'slack_signing_secret';
13
+ /**
14
+ * Vault key for the authorized team/channel set (TOFU pairing). Stored as a
15
+ * JSON string of an authorization record so we can grow what we pin without a
16
+ * format change (today: `{ teamId, channelId? }`).
17
+ */
18
+ export const SLACK_AUTHORIZED_KEY = 'slack_authorized';
19
+ /** Env override for the bot token (beats the vault, matching every other channel). */
20
+ export const SLACK_BOT_TOKEN_ENV = 'MOXXY_SLACK_BOT_TOKEN';
21
+ /** Env override for the signing secret. */
22
+ export const SLACK_SIGNING_SECRET_ENV = 'MOXXY_SLACK_SIGNING_SECRET';
23
+ /** A Slack bot token always starts with `xoxb-`. */
24
+ export const SLACK_BOT_TOKEN_RE = /^xoxb-[A-Za-z0-9-]{10,}$/;
25
+ /** zod validator for a bot token (shape only — connectivity is tested via `auth.test`). */
26
+ export const slackBotTokenSchema = z
27
+ .string()
28
+ .trim()
29
+ .regex(SLACK_BOT_TOKEN_RE, 'expected a Slack bot token like "xoxb-…"');
30
+ /** zod validator for a signing secret (hex-ish; Slack uses a 32-byte hex secret). */
31
+ export const slackSigningSecretSchema = z
32
+ .string()
33
+ .trim()
34
+ .min(16, 'signing secret looks too short')
35
+ .max(256, 'signing secret looks too long');
36
+ /**
37
+ * Resolve the bot token: env override first, then the vault (the shared
38
+ * env→vault resolution in @moxxy/channel-kit). Returns null when neither is
39
+ * set. Trimmed; never returns an empty string.
40
+ */
41
+ export async function resolveBotToken(vault) {
42
+ return resolveSecret(vault, { envVar: SLACK_BOT_TOKEN_ENV, vaultKey: SLACK_BOT_TOKEN_KEY });
43
+ }
44
+ /** Resolve the signing secret: env override first, then the vault. */
45
+ export async function resolveSigningSecret(vault) {
46
+ return resolveSecret(vault, {
47
+ envVar: SLACK_SIGNING_SECRET_ENV,
48
+ vaultKey: SLACK_SIGNING_SECRET_KEY,
49
+ });
50
+ }
51
+ /** Parse the stored authorization record. Returns null for missing/corrupt. */
52
+ export function parseAuthorization(raw) {
53
+ if (!raw)
54
+ return null;
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ if (typeof parsed?.teamId === 'string' && parsed.teamId) {
58
+ return parsed.channelId
59
+ ? { teamId: parsed.teamId, channelId: parsed.channelId }
60
+ : { teamId: parsed.teamId };
61
+ }
62
+ }
63
+ catch {
64
+ /* corrupt — treat as unpaired */
65
+ }
66
+ return null;
67
+ }
68
+ /** Does an inbound event from `(teamId, channelId)` match the stored authorization? */
69
+ export function authorizationMatches(auth, teamId, channelId) {
70
+ if (!auth || !teamId)
71
+ return false;
72
+ if (auth.teamId !== teamId)
73
+ return false;
74
+ if (auth.channelId && auth.channelId !== channelId)
75
+ return false;
76
+ return true;
77
+ }
78
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,0DAA0D;AAC1D,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AACrD,mFAAmF;AACnF,MAAM,CAAC,MAAM,wBAAwB,GAAG,sBAAsB,CAAC;AAC/D;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC;AAEvD,sFAAsF;AACtF,MAAM,CAAC,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAC3D,2CAA2C;AAC3C,MAAM,CAAC,MAAM,wBAAwB,GAAG,4BAA4B,CAAC;AAErE,oDAAoD;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAE7D,2FAA2F;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC;KACjC,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,kBAAkB,EAAE,0CAA0C,CAAC,CAAC;AAEzE,qFAAqF;AACrF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC;KACtC,MAAM,EAAE;KACR,IAAI,EAAE;KACN,GAAG,CAAC,EAAE,EAAE,gCAAgC,CAAC;KACzC,GAAG,CAAC,GAAG,EAAE,+BAA+B,CAAC,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAErC;IACC,OAAO,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAC,CAAC;AAC9F,CAAC;AAED,sEAAsE;AACtE,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAE1C;IACC,OAAO,aAAa,CAAC,KAAK,EAAE;QAC1B,MAAM,EAAE,wBAAwB;QAChC,QAAQ,EAAE,wBAAwB;KACnC,CAAC,CAAC;AACL,CAAC;AASD,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAC,GAA8B;IAC/D,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAgC,CAAC;QAC9D,IAAI,OAAO,MAAM,EAAE,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YACxD,OAAO,MAAM,CAAC,SAAS;gBACrB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE;gBACxD,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC;QAChC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,oBAAoB,CAClC,IAA+B,EAC/B,MAA0B,EAC1B,SAA6B;IAE7B,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACnC,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACjE,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { type ChannelSubcommandContext } from '@moxxy/sdk';
2
+ /**
3
+ * Drive the TOFU pairing flow end-to-end.
4
+ *
5
+ * 1. Build a SlackChannel from the subcommand ctx + wire the session's
6
+ * permission resolver.
7
+ * 2. Subscribe to pair candidates BEFORE starting so the first @mention can't
8
+ * race past us.
9
+ * 3. Start in `pair` mode (opens the tunnel; prints the Request URL).
10
+ * 4. Wait (with spinner) for the first verified inbound event.
11
+ * 5. Ask the operator to confirm the team/channel; on yes, persist it.
12
+ * 6. Hand off the running bot until Ctrl+C.
13
+ *
14
+ * Uses the default `proxyTunnel` provider (the channel imports it directly), so
15
+ * the public Request URL is available once the channel starts.
16
+ */
17
+ export declare function runSlackPairFlow(ctx: ChannelSubcommandContext): Promise<number>;
18
+ //# sourceMappingURL=pair-flow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.d.ts","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAOnF;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FrF"}
@@ -0,0 +1,106 @@
1
+ import { confirm, isCancel, log, outro, spinner } from '@clack/prompts';
2
+ import { exitAfterPairRequested } from '@moxxy/sdk';
3
+ import { SlackChannel } from './channel.js';
4
+ const ANSI = process.stdout.isTTY && !process.env.NO_COLOR;
5
+ const dim = (s) => (ANSI ? `\x1b[2m${s}\x1b[22m` : s);
6
+ /**
7
+ * Drive the TOFU pairing flow end-to-end.
8
+ *
9
+ * 1. Build a SlackChannel from the subcommand ctx + wire the session's
10
+ * permission resolver.
11
+ * 2. Subscribe to pair candidates BEFORE starting so the first @mention can't
12
+ * race past us.
13
+ * 3. Start in `pair` mode (opens the tunnel; prints the Request URL).
14
+ * 4. Wait (with spinner) for the first verified inbound event.
15
+ * 5. Ask the operator to confirm the team/channel; on yes, persist it.
16
+ * 6. Hand off the running bot until Ctrl+C.
17
+ *
18
+ * Uses the default `proxyTunnel` provider (the channel imports it directly), so
19
+ * the public Request URL is available once the channel starts.
20
+ */
21
+ export async function runSlackPairFlow(ctx) {
22
+ const session = ctx.session;
23
+ const channel = new SlackChannel({
24
+ vault: ctx.deps.vault,
25
+ logger: ctx.deps.logger,
26
+ });
27
+ session.setPermissionResolver(channel.permissionResolver);
28
+ let candidateResolve = null;
29
+ const firstCandidate = new Promise((resolve) => {
30
+ candidateResolve = resolve;
31
+ });
32
+ const unsubscribe = channel.onPairCandidate((c) => {
33
+ candidateResolve?.(c);
34
+ candidateResolve = null;
35
+ });
36
+ outro(dim('opening pairing window…'));
37
+ let handle;
38
+ try {
39
+ handle = await channel.start({ session, pair: true });
40
+ }
41
+ catch (err) {
42
+ unsubscribe();
43
+ log.error(`Could not start the Slack channel: ${err instanceof Error ? err.message : String(err)}`);
44
+ return 1;
45
+ }
46
+ const stopBot = async () => {
47
+ unsubscribe();
48
+ try {
49
+ await handle.stop('wizard');
50
+ }
51
+ catch {
52
+ /* ignore */
53
+ }
54
+ };
55
+ if (channel.requestUrl) {
56
+ log.info(`Slack Request URL (paste into Event Subscriptions):\n ${channel.requestUrl}\n` +
57
+ 'Then mention the bot (@your-bot) in a channel to pair.');
58
+ }
59
+ const spin = spinner();
60
+ spin.start('Waiting for the first @mention from Slack…');
61
+ let candidate;
62
+ try {
63
+ candidate = await firstCandidate;
64
+ }
65
+ catch (err) {
66
+ spin.stop('pairing aborted');
67
+ log.error(`Pairing aborted: ${err instanceof Error ? err.message : String(err)}`);
68
+ await stopBot();
69
+ return 1;
70
+ }
71
+ spin.stop(`Got an event from team ${candidate.teamId}, channel ${candidate.channelId}.`);
72
+ const ok = await confirm({
73
+ message: `Authorize team ${candidate.teamId} / channel ${candidate.channelId}?`,
74
+ });
75
+ if (isCancel(ok) || !ok) {
76
+ log.warn('Pairing not confirmed.');
77
+ await stopBot();
78
+ return 0;
79
+ }
80
+ await channel.confirmPairing(candidate);
81
+ log.success(`Paired ✓ — team ${candidate.teamId} is authorized.`);
82
+ if (exitAfterPairRequested(ctx)) {
83
+ // Orchestrated pairing (`moxxy onboard`): hand control back — the caller
84
+ // starts the bot under its own service afterwards.
85
+ await stopBot();
86
+ return 0;
87
+ }
88
+ log.info('Bot is running. Press Ctrl+C to stop.');
89
+ const shutdown = async () => {
90
+ await stopBot();
91
+ await session.close('SIGINT').catch(() => undefined);
92
+ process.exit(0);
93
+ };
94
+ const onSignal = () => void shutdown();
95
+ process.once('SIGINT', onSignal);
96
+ process.once('SIGTERM', onSignal);
97
+ try {
98
+ await handle.running;
99
+ return 0;
100
+ }
101
+ finally {
102
+ process.removeListener('SIGINT', onSignal);
103
+ process.removeListener('SIGTERM', onSignal);
104
+ }
105
+ }
106
+ //# sourceMappingURL=pair-flow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pair-flow.js","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EAAE,sBAAsB,EAAiC,MAAM,YAAY,CAAC;AAEnF,OAAO,EAAE,YAAY,EAAsB,MAAM,cAAc,CAAC;AAEhE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;AAC3D,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAA6B;IAClE,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC5B,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC;QAC/B,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAmB;QACnC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,MAAe;KACjC,CAAC,CAAC;IACH,OAAO,CAAC,qBAAqB,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAE1D,IAAI,gBAAgB,GAAwC,IAAI,CAAC;IACjE,MAAM,cAAc,GAAG,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,EAAE;QAC5D,gBAAgB,GAAG,OAAO,CAAC;IAC7B,CAAC,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,EAAE;QAChD,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;QACtB,gBAAgB,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,KAAK,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAEtC,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,WAAW,EAAE,CAAC;QACd,GAAG,CAAC,KAAK,CAAC,sCAAsC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpG,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,WAAW,EAAE,CAAC;QACd,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;QACvB,GAAG,CAAC,IAAI,CACN,0DAA0D,OAAO,CAAC,UAAU,IAAI;YAC9E,wDAAwD,CAC3D,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAEzD,IAAI,SAAwB,CAAC;IAC7B,IAAI,CAAC;QACH,SAAS,GAAG,MAAM,cAAc,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC7B,GAAG,CAAC,KAAK,CAAC,oBAAoB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClF,MAAM,OAAO,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,0BAA0B,SAAS,CAAC,MAAM,aAAa,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC;IAEzF,MAAM,EAAE,GAAG,MAAM,OAAO,CAAC;QACvB,OAAO,EAAE,kBAAkB,SAAS,CAAC,MAAM,cAAc,SAAS,CAAC,SAAS,GAAG;KAChF,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACxB,GAAG,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACnC,MAAM,OAAO,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;IACxC,GAAG,CAAC,OAAO,CAAC,mBAAmB,SAAS,CAAC,MAAM,iBAAiB,CAAC,CAAC;IAElE,IAAI,sBAAsB,CAAC,GAAG,CAAC,EAAE,CAAC;QAChC,yEAAyE;QACzE,mDAAmD;QACnD,MAAM,OAAO,EAAE,CAAC;QAChB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,GAAG,CAAC,IAAI,CAAC,uCAAuC,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,KAAK,IAAmB,EAAE;QACzC,MAAM,OAAO,EAAE,CAAC;QAChB,MAAM,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,GAAS,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjC,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAElC,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC;QACrB,OAAO,CAAC,CAAC;IACX,CAAC;YAAS,CAAC;QACT,OAAO,CAAC,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC3C,OAAO,CAAC,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { PermissionResolver } from '@moxxy/sdk';
2
+ export interface SlackPermissionLogger {
3
+ info?(msg: string, meta?: Record<string, unknown>): void;
4
+ }
5
+ /**
6
+ * Build the Slack channel's autonomous permission resolver.
7
+ *
8
+ * The bot runs hands-off (no human-in-the-loop, like the HTTP channel): the
9
+ * operator declares trust upfront via `channels.slack.allowedTools`, and any
10
+ * tool NOT in that list is denied. The trust check + `'*'` expansion +
11
+ * audit-on-auto-approve wiring is the shared
12
+ * {@link createAuditedAllowListResolver} from `@moxxy/channel-kit` (which in
13
+ * turn reuses the SDK's `createAllowListResolver`: exact-name match →
14
+ * `allow_session`, else `deny`); this wrapper binds it to the Slack channel
15
+ * logger so every auto-approved call leaves a trail of what the autonomous run
16
+ * executed. An empty list denies everything (effectively read-only, since no
17
+ * side-effecting tool can run without a clicker).
18
+ *
19
+ * (Autonomous allow-list safety is a known trade-off; see TECH_DEBT — v1 has no
20
+ * Slack-button approval flow.)
21
+ */
22
+ export declare function buildSlackPermissionResolver(opts: {
23
+ allowedTools: ReadonlyArray<string>;
24
+ allToolNames: ReadonlyArray<string>;
25
+ logger?: SlackPermissionLogger;
26
+ }): PermissionResolver;
27
+ //# sourceMappingURL=permission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.d.ts","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IACjD,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC,GAAG,kBAAkB,CAarB"}
@@ -0,0 +1,33 @@
1
+ import { createAuditedAllowListResolver } from '@moxxy/channel-kit';
2
+ /**
3
+ * Build the Slack channel's autonomous permission resolver.
4
+ *
5
+ * The bot runs hands-off (no human-in-the-loop, like the HTTP channel): the
6
+ * operator declares trust upfront via `channels.slack.allowedTools`, and any
7
+ * tool NOT in that list is denied. The trust check + `'*'` expansion +
8
+ * audit-on-auto-approve wiring is the shared
9
+ * {@link createAuditedAllowListResolver} from `@moxxy/channel-kit` (which in
10
+ * turn reuses the SDK's `createAllowListResolver`: exact-name match →
11
+ * `allow_session`, else `deny`); this wrapper binds it to the Slack channel
12
+ * logger so every auto-approved call leaves a trail of what the autonomous run
13
+ * executed. An empty list denies everything (effectively read-only, since no
14
+ * side-effecting tool can run without a clicker).
15
+ *
16
+ * (Autonomous allow-list safety is a known trade-off; see TECH_DEBT — v1 has no
17
+ * Slack-button approval flow.)
18
+ */
19
+ export function buildSlackPermissionResolver(opts) {
20
+ return createAuditedAllowListResolver({
21
+ name: 'slack-allow-list',
22
+ allowedTools: opts.allowedTools,
23
+ allToolNames: opts.allToolNames,
24
+ onAutoApprove: (call, { wildcard }) => {
25
+ opts.logger?.info?.('slack: auto-approved tool call', {
26
+ tool: call.name,
27
+ callId: call.callId,
28
+ wildcard,
29
+ });
30
+ },
31
+ });
32
+ }
33
+ //# sourceMappingURL=permission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission.js","sourceRoot":"","sources":["../src/permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAOpE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,4BAA4B,CAAC,IAI5C;IACC,OAAO,8BAA8B,CAAC;QACpC,IAAI,EAAE,kBAAkB;QACxB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,aAAa,EAAE,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE;YACpC,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,gCAAgC,EAAE;gBACpD,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slack event idempotency cache. The implementation lives in
3
+ * `@moxxy/channel-kit` (the at-least-once delivery pattern is shared with
4
+ * every inbound-webhook channel and `@moxxy/plugin-webhooks`); re-exported
5
+ * here because it is part of this plugin's public API.
6
+ */
7
+ export { DeliveryDedupeCache } from '@moxxy/channel-kit';
8
+ //# sourceMappingURL=dedupe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/server/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Slack event idempotency cache. The implementation lives in
3
+ * `@moxxy/channel-kit` (the at-least-once delivery pattern is shared with
4
+ * every inbound-webhook channel and `@moxxy/plugin-webhooks`); re-exported
5
+ * here because it is part of this plugin's public API.
6
+ */
7
+ export { DeliveryDedupeCache } from '@moxxy/channel-kit';
8
+ //# sourceMappingURL=dedupe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/server/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,80 @@
1
+ import { DeliveryDedupeCache, type IngestHttpServerHandle } from '@moxxy/channel-kit';
2
+ import { type SlackEventCallback } from './schema.js';
3
+ /**
4
+ * The HTTP front-end for the Slack channel. Binds a `node:http` server to an
5
+ * ephemeral loopback port; the channel exposes it publicly via the proxy
6
+ * tunnel. Slack POSTs every event to `<public>/slack/events`.
7
+ *
8
+ * The transport scaffold (routing, health probe, size-capped raw-body read,
9
+ * verify gate, catch-all error handling) is `@moxxy/channel-kit`'s
10
+ * {@link IngestHttpServer}; Slack's HMAC scheme stays HERE as its verify hook,
11
+ * and this module owns everything after verification.
12
+ *
13
+ * Handler order — every gate runs BEFORE the session is touched (skill A8/A46):
14
+ * 1. POST-only (+ a GET `/slack/health` liveness probe). [kit]
15
+ * 2. read the RAW body bytes (size cap) — required for the HMAC. [kit]
16
+ * 3. signature gate → 401 (verify over the raw bytes, before JSON.parse).
17
+ * 4. zod-validate the envelope → 400.
18
+ * 5. `url_verification` → echo `{ challenge }` (the handshake).
19
+ * 6. dedupe — drop on `X-Slack-Retry-Num` or a seen `event_id`.
20
+ * 7. drop the bot's own messages (`event.user === botUserId` / `event.bot_id`).
21
+ * 8. pairing gate — ignore unless the team/channel is authorized.
22
+ * 9. ACK 200 synchronously, THEN run the turn fire-and-forget (Slack's 3s
23
+ * ack budget; never run `runTurn` on the request path).
24
+ *
25
+ * Every handler error is caught so a bad request can never escalate to a
26
+ * process-level uncaughtException.
27
+ */
28
+ declare const EVENTS_PATH = "/slack/events";
29
+ /** Decision the channel makes about an inbound event. */
30
+ export interface DispatchContext {
31
+ readonly teamId: string | undefined;
32
+ readonly channel: string;
33
+ readonly text: string;
34
+ readonly user: string | undefined;
35
+ readonly threadTs: string;
36
+ readonly eventType: string;
37
+ }
38
+ export interface IngestServerHooks {
39
+ /** The bot's own user id, captured at start via `auth.test` (drop self-messages). */
40
+ readonly botUserId: string;
41
+ /** Is this team/channel authorized to drive the session? (pairing gate) */
42
+ isAuthorized(teamId: string | undefined, channel: string | undefined): boolean;
43
+ /**
44
+ * Observe a verified inbound event from a (possibly unauthorized) team/channel.
45
+ * Used by the TOFU `pair` flow to capture the first event and persist it.
46
+ * Returns true if the event was consumed by pairing (so it should NOT also
47
+ * drive a turn).
48
+ */
49
+ onVerifiedEvent?(ev: SlackEventCallback): boolean | Promise<boolean>;
50
+ /** Run a turn for an authorized, deduped, non-self event (fire-and-forget). */
51
+ dispatch(ctx: DispatchContext): void;
52
+ }
53
+ export interface IngestServerOptions {
54
+ readonly host?: string;
55
+ readonly signingSecret: string;
56
+ readonly hooks: IngestServerHooks;
57
+ /** Max request body size in bytes. Default 1MB. */
58
+ readonly maxBodyBytes?: number;
59
+ /** Override dedupe cache (tests). */
60
+ readonly dedupe?: DeliveryDedupeCache;
61
+ readonly logger?: {
62
+ info?(msg: string, meta?: Record<string, unknown>): void;
63
+ warn?(msg: string, meta?: Record<string, unknown>): void;
64
+ };
65
+ }
66
+ export type IngestServerHandle = IngestHttpServerHandle;
67
+ export declare class IngestServer {
68
+ private readonly opts;
69
+ private readonly inner;
70
+ private readonly dedupe;
71
+ constructor(opts: IngestServerOptions);
72
+ get port(): number;
73
+ /** Bind on an ephemeral loopback port. Resolves once listening. */
74
+ start(): Promise<IngestServerHandle>;
75
+ stop(): Promise<void>;
76
+ /** Steps 4–9: everything after the raw-body + signature gates. */
77
+ private handleVerified;
78
+ }
79
+ export { EVENTS_PATH as SLACK_EVENTS_PATH };
80
+ //# sourceMappingURL=ingest-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-server.d.ts","sourceRoot":"","sources":["../../src/server/ingest-server.ts"],"names":[],"mappings":"AACA,OAAO,EACL,mBAAmB,EAGnB,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAuB,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAG3E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,QAAA,MAAM,WAAW,kBAAkB,CAAC;AAGpC,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IAC/E;;;;;OAKG;IACH,eAAe,CAAC,CAAC,EAAE,EAAE,kBAAkB,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,+EAA+E;IAC/E,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,mDAAmD;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QACzD,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC1D,CAAC;CACH;AAED,MAAM,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAExD,qBAAa,YAAY;IAIX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;gBAEhB,IAAI,EAAE,mBAAmB;IAgBtD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,mEAAmE;IACnE,KAAK,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAIpC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,kEAAkE;YACpD,cAAc;CAuF7B;AAED,OAAO,EAAE,WAAW,IAAI,iBAAiB,EAAE,CAAC"}
@@ -0,0 +1,142 @@
1
+ import { DeliveryDedupeCache, IngestHttpServer, respondJson, } from '@moxxy/channel-kit';
2
+ import { slackEnvelopeSchema } from './schema.js';
3
+ import { verifySlackSignature } from './verify.js';
4
+ /**
5
+ * The HTTP front-end for the Slack channel. Binds a `node:http` server to an
6
+ * ephemeral loopback port; the channel exposes it publicly via the proxy
7
+ * tunnel. Slack POSTs every event to `<public>/slack/events`.
8
+ *
9
+ * The transport scaffold (routing, health probe, size-capped raw-body read,
10
+ * verify gate, catch-all error handling) is `@moxxy/channel-kit`'s
11
+ * {@link IngestHttpServer}; Slack's HMAC scheme stays HERE as its verify hook,
12
+ * and this module owns everything after verification.
13
+ *
14
+ * Handler order — every gate runs BEFORE the session is touched (skill A8/A46):
15
+ * 1. POST-only (+ a GET `/slack/health` liveness probe). [kit]
16
+ * 2. read the RAW body bytes (size cap) — required for the HMAC. [kit]
17
+ * 3. signature gate → 401 (verify over the raw bytes, before JSON.parse).
18
+ * 4. zod-validate the envelope → 400.
19
+ * 5. `url_verification` → echo `{ challenge }` (the handshake).
20
+ * 6. dedupe — drop on `X-Slack-Retry-Num` or a seen `event_id`.
21
+ * 7. drop the bot's own messages (`event.user === botUserId` / `event.bot_id`).
22
+ * 8. pairing gate — ignore unless the team/channel is authorized.
23
+ * 9. ACK 200 synchronously, THEN run the turn fire-and-forget (Slack's 3s
24
+ * ack budget; never run `runTurn` on the request path).
25
+ *
26
+ * Every handler error is caught so a bad request can never escalate to a
27
+ * process-level uncaughtException.
28
+ */
29
+ const EVENTS_PATH = '/slack/events';
30
+ const HEALTH_PATH = '/slack/health';
31
+ export class IngestServer {
32
+ opts;
33
+ inner;
34
+ dedupe;
35
+ constructor(opts) {
36
+ this.opts = opts;
37
+ this.dedupe = opts.dedupe ?? new DeliveryDedupeCache();
38
+ this.inner = new IngestHttpServer({
39
+ ...(opts.host ? { host: opts.host } : {}),
40
+ eventsPath: EVENTS_PATH,
41
+ healthPath: HEALTH_PATH,
42
+ healthBody: () => ({ status: 'ok', listener: 'slack' }),
43
+ ...(opts.maxBodyBytes !== undefined ? { maxBodyBytes: opts.maxBodyBytes } : {}),
44
+ label: 'slack',
45
+ verify: ({ rawBody, headers }) => verifySlackSignature({ rawBody, headers, signingSecret: opts.signingSecret }),
46
+ handleVerified: (raw, req, res) => this.handleVerified(raw, req, res),
47
+ ...(opts.logger ? { logger: opts.logger } : {}),
48
+ });
49
+ }
50
+ get port() {
51
+ return this.inner.port;
52
+ }
53
+ /** Bind on an ephemeral loopback port. Resolves once listening. */
54
+ start() {
55
+ return this.inner.start();
56
+ }
57
+ stop() {
58
+ return this.inner.stop();
59
+ }
60
+ /** Steps 4–9: everything after the raw-body + signature gates. */
61
+ async handleVerified(raw, req, res) {
62
+ // 4) zod-validate the envelope.
63
+ let parsedJson;
64
+ try {
65
+ parsedJson = JSON.parse(raw.toString('utf8'));
66
+ }
67
+ catch {
68
+ respondJson(res, 400, { error: 'invalid_json' });
69
+ return;
70
+ }
71
+ const parsed = slackEnvelopeSchema.safeParse(parsedJson);
72
+ if (!parsed.success) {
73
+ this.opts.logger?.warn?.('slack: malformed envelope', {
74
+ issue: parsed.error.issues[0]?.message,
75
+ });
76
+ respondJson(res, 400, { error: 'bad_request' });
77
+ return;
78
+ }
79
+ const envelope = parsed.data;
80
+ // 5) url_verification handshake.
81
+ if (envelope.type === 'url_verification') {
82
+ respondJson(res, 200, { challenge: envelope.challenge });
83
+ return;
84
+ }
85
+ // From here on it's an event_callback. ACK fast; do everything else after
86
+ // (or fire-and-forget) so we stay inside Slack's 3-second ack budget.
87
+ const callback = envelope;
88
+ // 6) Dedupe — drop retries / repeated event ids. We record the id only for
89
+ // verified events so an unverified request can never poison the cache.
90
+ const isRetry = req.headers['x-slack-retry-num'] !== undefined;
91
+ const eventId = callback.event_id;
92
+ const dupe = isRetry || (eventId ? !this.dedupe.check(eventId) : false);
93
+ // 7) Self-message guard: never re-trigger on our own posts.
94
+ const ev = callback.event;
95
+ const isSelf = ev.bot_id !== undefined ||
96
+ (ev.user !== undefined && ev.user === this.opts.hooks.botUserId);
97
+ // ACK synchronously BEFORE running anything.
98
+ respondJson(res, 200, { status: 'ok' });
99
+ if (dupe || isSelf)
100
+ return;
101
+ // Pairing TOFU hook gets first crack at a verified event (even unauthorized),
102
+ // so it can capture + persist the first team/channel.
103
+ try {
104
+ const consumed = (await this.opts.hooks.onVerifiedEvent?.(callback)) ?? false;
105
+ if (consumed)
106
+ return;
107
+ }
108
+ catch (err) {
109
+ this.opts.logger?.warn?.('slack: pairing hook threw', { err: String(err) });
110
+ }
111
+ // 8) Pairing gate — only authorized team/channel drives the session.
112
+ if (!this.opts.hooks.isAuthorized(callback.team_id, ev.channel)) {
113
+ this.opts.logger?.info?.('slack: dropped event from unauthorized team/channel', {
114
+ team: callback.team_id,
115
+ channel: ev.channel,
116
+ });
117
+ return;
118
+ }
119
+ // Only act on message-ish events (we subscribe to app_mention primarily;
120
+ // message events with an edit/system subtype are ignored).
121
+ const eventType = ev.type;
122
+ if (eventType !== 'app_mention' && eventType !== 'message')
123
+ return;
124
+ if (ev.subtype)
125
+ return; // message_changed / message_deleted / channel_join …
126
+ const channel = ev.channel;
127
+ const text = (ev.text ?? '').trim();
128
+ if (!channel || !text)
129
+ return;
130
+ // 9) Run the turn fire-and-forget (already ACKed).
131
+ this.opts.hooks.dispatch({
132
+ teamId: callback.team_id,
133
+ channel,
134
+ text,
135
+ user: ev.user,
136
+ threadTs: ev.thread_ts ?? ev.ts ?? channel,
137
+ eventType,
138
+ });
139
+ }
140
+ }
141
+ export { EVENTS_PATH as SLACK_EVENTS_PATH };
142
+ //# sourceMappingURL=ingest-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-server.js","sourceRoot":"","sources":["../../src/server/ingest-server.ts"],"names":[],"mappings":"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"}