@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
@@ -0,0 +1,286 @@
1
+ import { newTurnId } from '@moxxy/core';
2
+ import { TofuPairingWindow, TurnCoordinator } from '@moxxy/channel-kit';
3
+ import { proxyTunnel } from '@moxxy/plugin-tunnel-proxy';
4
+ import { SLACK_AUTHORIZED_KEY, authorizationMatches, parseAuthorization, resolveBotToken, resolveSigningSecret, } from './keys.js';
5
+ import { buildSlackPermissionResolver } from './permission.js';
6
+ import { SlackClient } from './channel/slack-client.js';
7
+ import { runSlackTurn } from './channel/turn-runner.js';
8
+ import { IngestServer, SLACK_EVENTS_PATH, } from './server/ingest-server.js';
9
+ /** Routing label the proxy relay exposes this channel under. */
10
+ const TUNNEL_LABEL = 'slack';
11
+ export class SlackChannel {
12
+ name = 'slack';
13
+ /**
14
+ * Installed on the session by the CLI dispatcher. Replaced in `start()` once
15
+ * we can expand a `['*']` allow-list against the live tool registry; until
16
+ * then it denies everything (safe default before start).
17
+ */
18
+ permissionResolver;
19
+ opts;
20
+ session = null;
21
+ client = null;
22
+ ingest = null;
23
+ tunnel = null;
24
+ handle = null;
25
+ resolveRunning = null;
26
+ botUserId = '';
27
+ authorization = null;
28
+ model;
29
+ // Single-flight turn state (v1: one turn at a time across all threads): the
30
+ // `busy` guard, the per-turn AbortController, and the bounded own-turn-id set
31
+ // mirrorForeignTurn filters on (#8).
32
+ turns = new TurnCoordinator();
33
+ // Per-turn target so foreign-turn mirroring + permission prompts know the thread.
34
+ currentChannelId = null;
35
+ currentThreadTs = null;
36
+ lastChannelId = null;
37
+ lastThreadTs = null;
38
+ logUnsub = null;
39
+ // Pairing (TOFU) window: armed by `start({ pair: true })`, captures the first
40
+ // verified team/channel for the pair flow to confirm.
41
+ tofu;
42
+ publicUrl = null;
43
+ constructor(opts) {
44
+ this.opts = opts;
45
+ this.tofu = new TofuPairingWindow({
46
+ onListenerError: (err) => this.opts.logger?.warn?.('slack: pair listener threw', { err: String(err) }),
47
+ });
48
+ // Pre-start: deny-all. Replaced with the real allow-list resolver in start()
49
+ // once the tool registry is available (for `['*']` expansion).
50
+ this.permissionResolver = buildSlackPermissionResolver({
51
+ allowedTools: [],
52
+ allToolNames: [],
53
+ ...(opts.logger ? { logger: opts.logger } : {}),
54
+ });
55
+ }
56
+ /** The public Request URL to paste into Slack's Event Subscriptions. */
57
+ get requestUrl() {
58
+ return this.publicUrl;
59
+ }
60
+ /** Subscribe to pairing candidates (the setup/pair flows use this). */
61
+ onPairCandidate(listener) {
62
+ return this.tofu.onCandidate(listener);
63
+ }
64
+ /** Persist a team/channel as authorized (called by the pair flow on confirm). */
65
+ async confirmPairing(candidate) {
66
+ const auth = {
67
+ teamId: candidate.teamId,
68
+ channelId: candidate.channelId,
69
+ };
70
+ await this.opts.vault.set(SLACK_AUTHORIZED_KEY, JSON.stringify(auth), ['slack']);
71
+ this.authorization = auth;
72
+ this.tofu.disarm();
73
+ }
74
+ async start(startOpts) {
75
+ if (this.handle)
76
+ return this.handle;
77
+ this.session = startOpts.session;
78
+ this.model = startOpts.model;
79
+ if (startOpts.pair === true)
80
+ this.tofu.arm();
81
+ const token = await resolveBotToken(this.opts.vault);
82
+ if (!token) {
83
+ throw new Error('Slack bot token not found. Run `moxxy channels slack setup`, set MOXXY_SLACK_BOT_TOKEN, ' +
84
+ 'or store one in the vault under slack_bot_token.');
85
+ }
86
+ const signingSecret = await resolveSigningSecret(this.opts.vault);
87
+ if (!signingSecret) {
88
+ throw new Error('Slack signing secret not found. Run `moxxy channels slack setup`, set ' +
89
+ 'MOXXY_SLACK_SIGNING_SECRET, or store one in the vault under slack_signing_secret.');
90
+ }
91
+ // Load any persisted pairing.
92
+ this.authorization = parseAuthorization(await this.opts.vault.get(SLACK_AUTHORIZED_KEY));
93
+ if (!this.tofu.isArmed && !this.authorization) {
94
+ throw new Error('No Slack team/channel is paired yet. Run `moxxy channels slack pair` first.');
95
+ }
96
+ // Validate the token + capture the bot's own user id (for self-message drops).
97
+ this.client = this.opts.makeClient
98
+ ? this.opts.makeClient(token)
99
+ : new SlackClient({ token });
100
+ const auth = await this.client.authTest();
101
+ this.botUserId = auth.botUserId;
102
+ this.opts.logger?.info?.('slack: authenticated', {
103
+ botUserId: this.botUserId,
104
+ team: auth.team,
105
+ });
106
+ // Build the real allow-list resolver now that the tool registry is live, so
107
+ // `['*']` can expand to every registered tool name (mirror prompt.ts).
108
+ const allowedTools = startOpts.allowedTools ?? this.opts.allowedTools ?? [];
109
+ const allToolNames = this.session.tools.list().map((t) => t.name);
110
+ this.permissionResolver = buildSlackPermissionResolver({
111
+ allowedTools,
112
+ allToolNames,
113
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
114
+ });
115
+ // Swap the (now-final) resolver in — start() was called AFTER the CLI
116
+ // installed the pre-start one, so re-install via the documented seam.
117
+ this.session.setPermissionResolver(this.permissionResolver);
118
+ // Mirror-to-thread: post the assistant's prose for a turn this channel did
119
+ // NOT initiate (e.g. a co-attached surface ran one) into the last thread we
120
+ // served. Our OWN turns stream via the frame pump (filtered by turnId).
121
+ this.logUnsub = this.session.log.subscribe((event) => this.mirrorForeignTurn(event));
122
+ // Stand up the local ingest server, then expose it via the proxy tunnel.
123
+ this.ingest = new IngestServer({
124
+ ...(this.opts.host ? { host: this.opts.host } : {}),
125
+ signingSecret,
126
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
127
+ hooks: {
128
+ botUserId: this.botUserId,
129
+ isAuthorized: (teamId, channel) => authorizationMatches(this.authorization, teamId, channel),
130
+ onVerifiedEvent: (ev) => this.handlePairingCandidate(ev),
131
+ dispatch: (ctx) => this.dispatchTurn(ctx),
132
+ },
133
+ });
134
+ const bound = await this.ingest.start();
135
+ await this.openTunnel(bound.host, bound.port);
136
+ const running = new Promise((resolve) => {
137
+ this.resolveRunning = resolve;
138
+ });
139
+ this.handle = {
140
+ running,
141
+ stop: async (reason = 'shutdown') => {
142
+ // Abort the in-flight turn FIRST so the model loop stops the moment the
143
+ // operator asks to shut down (shared/remote Session: spend continues
144
+ // otherwise and only its output is discarded).
145
+ this.turns.abort(reason);
146
+ this.logUnsub?.();
147
+ this.logUnsub = null;
148
+ if (this.tunnel) {
149
+ try {
150
+ await this.tunnel.close();
151
+ }
152
+ catch { /* ignore */ }
153
+ this.tunnel = null;
154
+ }
155
+ if (this.ingest) {
156
+ await this.ingest.stop();
157
+ this.ingest = null;
158
+ }
159
+ this.publicUrl = null;
160
+ this.resolveRunning?.();
161
+ this.resolveRunning = null;
162
+ },
163
+ };
164
+ this.opts.logger?.info?.('slack: channel started', {
165
+ requestUrl: this.publicUrl,
166
+ paired: this.authorization != null,
167
+ pairing: this.tofu.isArmed,
168
+ });
169
+ return this.handle;
170
+ }
171
+ async openTunnel(host, port) {
172
+ // Default to the self-hosted proxy relay (imported directly, like the
173
+ // webhooks tunnel); tests inject a fake provider.
174
+ const provider = this.opts.tunnelProvider ?? proxyTunnel;
175
+ if (provider.name === 'localhost') {
176
+ // A no-op provider can't reach a loopback port from Slack. Surface the
177
+ // local URL so a manual reverse proxy can still be wired.
178
+ this.publicUrl = `http://${host}:${port}${SLACK_EVENTS_PATH}`;
179
+ this.opts.logger?.warn?.('slack: localhost tunnel provider — Slack cannot reach a loopback port', { localUrl: this.publicUrl });
180
+ return;
181
+ }
182
+ try {
183
+ this.tunnel = await provider.open({ port, host, label: TUNNEL_LABEL });
184
+ this.publicUrl = `${this.tunnel.url.replace(/\/+$/, '')}${SLACK_EVENTS_PATH}`;
185
+ this.opts.logger?.info?.('slack: tunnel open', {
186
+ provider: provider.name,
187
+ requestUrl: this.publicUrl,
188
+ });
189
+ }
190
+ catch (err) {
191
+ this.publicUrl = `http://${host}:${port}${SLACK_EVENTS_PATH}`;
192
+ this.opts.logger?.warn?.('slack: tunnel failed; using local URL', {
193
+ provider: provider.name,
194
+ err: String(err),
195
+ });
196
+ }
197
+ }
198
+ /**
199
+ * TOFU pairing hook: while a pairing window is armed, the first verified event
200
+ * captures the team/channel and notifies listeners. It's "consumed" (no turn)
201
+ * so the very first message just establishes trust. Returns true when consumed.
202
+ */
203
+ handlePairingCandidate(ev) {
204
+ if (!this.tofu.isArmed)
205
+ return false;
206
+ const teamId = ev.team_id;
207
+ const channelId = ev.event.channel;
208
+ if (!teamId || !channelId)
209
+ return false;
210
+ return this.tofu.offer({ teamId, channelId });
211
+ }
212
+ /**
213
+ * Fire-and-forget a turn for an authorized event. Single-flight (v1): if a
214
+ * turn is already running we drop the new event with a thread reply rather
215
+ * than corrupting the per-turn state. The ingest handler has already ACKed.
216
+ */
217
+ dispatchTurn(ctx) {
218
+ void this.runTurn(ctx).catch((err) => {
219
+ this.opts.logger?.warn?.('slack: turn dispatch failed', { err: String(err) });
220
+ });
221
+ }
222
+ async runTurn(ctx) {
223
+ if (!this.session || !this.client)
224
+ return;
225
+ // The turnId is minted here so the coordinator records it as an own-turn id
226
+ // — that's what mirrorForeignTurn filters on (#8).
227
+ const lease = this.turns.begin(newTurnId());
228
+ if (!lease) {
229
+ // Politely decline; v1 has no per-thread concurrency (see TECH_DEBT).
230
+ try {
231
+ await this.client.postMessage({
232
+ channel: ctx.channel,
233
+ threadTs: ctx.threadTs,
234
+ text: 'I am still working on a previous request. One moment…',
235
+ });
236
+ }
237
+ catch { /* ignore */ }
238
+ return;
239
+ }
240
+ this.currentChannelId = ctx.channel;
241
+ this.currentThreadTs = ctx.threadTs;
242
+ this.lastChannelId = ctx.channel;
243
+ this.lastThreadTs = ctx.threadTs;
244
+ try {
245
+ await runSlackTurn({
246
+ session: this.session,
247
+ client: this.client,
248
+ editFrameMs: this.opts.editFrameMs ?? 1000,
249
+ ...(this.opts.logger ? { logger: this.opts.logger } : {}),
250
+ }, {
251
+ channel: ctx.channel,
252
+ threadTs: ctx.threadTs,
253
+ text: ctx.text,
254
+ ...(this.model ? { model: this.model } : {}),
255
+ controller: lease.controller,
256
+ turnId: lease.turnId,
257
+ });
258
+ }
259
+ finally {
260
+ lease.end();
261
+ this.currentChannelId = null;
262
+ this.currentThreadTs = null;
263
+ }
264
+ }
265
+ /**
266
+ * Post the assistant's prose for a turn this channel did not initiate. Gated by
267
+ * `!busy` (our own turns stream via the frame pump) and by having served a
268
+ * thread at least once. Skipped for our own turnIds (robust to async ordering /
269
+ * replay, invariant #8).
270
+ */
271
+ mirrorForeignTurn(event) {
272
+ // Skipped for our own turnIds (robust to async ordering / replay, #8) and
273
+ // while a turn of ours is streaming via the frame pump.
274
+ const text = this.turns.mirrorText(event);
275
+ if (text == null)
276
+ return;
277
+ if (!this.client || this.lastChannelId == null || this.lastThreadTs == null)
278
+ return;
279
+ void this.client
280
+ .postMessage({ channel: this.lastChannelId, threadTs: this.lastThreadTs, text })
281
+ .catch((err) => {
282
+ this.opts.logger?.warn?.('slack: mirror failed', { err: String(err) });
283
+ });
284
+ }
285
+ }
286
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { type Plugin } from '@moxxy/sdk';
2
+ import type { VaultStore } from '@moxxy/plugin-vault';
3
+ export { SlackChannel, type SlackChannelOptions, type SlackStartOpts, type PairCandidate, } from './channel.js';
4
+ export { buildSlackPermissionResolver } from './permission.js';
5
+ export { SlackClient } from './channel/slack-client.js';
6
+ export { verifySlackSignature, SLACK_REPLAY_WINDOW_SEC } from './server/verify.js';
7
+ export { slackEnvelopeSchema, eventCallbackSchema, urlVerificationSchema } from './server/schema.js';
8
+ export { DeliveryDedupeCache } from './server/dedupe.js';
9
+ export { IngestServer, SLACK_EVENTS_PATH } from './server/ingest-server.js';
10
+ export { SLACK_BOT_TOKEN_KEY, SLACK_SIGNING_SECRET_KEY, SLACK_AUTHORIZED_KEY, SLACK_BOT_TOKEN_ENV, SLACK_SIGNING_SECRET_ENV, resolveBotToken, resolveSigningSecret, parseAuthorization, authorizationMatches, type SlackAuthorization, } from './keys.js';
11
+ export interface BuildSlackPluginOptions {
12
+ /** Host-injected encrypted secret store (available immediately). */
13
+ readonly vault: VaultStore;
14
+ }
15
+ /**
16
+ * Build the Slack channel plugin. The CLI passes the concrete vault (mirroring
17
+ * the Telegram plugin's vault injection). The channel exposes its own ingest
18
+ * server publicly via the self-hosted `proxyTunnel` (imported directly, like
19
+ * the webhooks tunnel).
20
+ */
21
+ export declare function buildSlackPlugin(opts: BuildSlackPluginOptions): Plugin;
22
+ /**
23
+ * Discovery-loadable default export: resolves the vault from the inter-plugin
24
+ * service registry in `onInit` (the vault plugin publishes `'vault'`). Requires
25
+ * `@moxxy/plugin-vault` to load first (declared in `package.json`
26
+ * `moxxy.requirements`). The channel + subcommands read the vault via
27
+ * `getVault()`, so resolution is deferred to call time — after `onInit` wired it.
28
+ */
29
+ export declare const slackPlugin: Plugin;
30
+ export default slackPlugin;
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAetD,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AACrG,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC5E,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,oBAAoB,EACpB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AAEnB,MAAM,WAAW,uBAAuB;IACtC,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,uBAAuB,GAAG,MAAM,CAEtE;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,MAgBtB,CAAC;AAEL,eAAe,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,207 @@
1
+ import { defineChannel, definePlugin, } from '@moxxy/sdk';
2
+ import { SlackChannel } from './channel.js';
3
+ import { SLACK_AUTHORIZED_KEY, SLACK_BOT_TOKEN_ENV, SLACK_BOT_TOKEN_KEY, SLACK_SIGNING_SECRET_ENV, SLACK_SIGNING_SECRET_KEY, parseAuthorization, resolveBotToken, resolveSigningSecret, } from './keys.js';
4
+ import { runSlackWizard } from './setup-wizard.js';
5
+ import { runSlackPairFlow } from './pair-flow.js';
6
+ export { SlackChannel, } from './channel.js';
7
+ export { buildSlackPermissionResolver } from './permission.js';
8
+ export { SlackClient } from './channel/slack-client.js';
9
+ export { verifySlackSignature, SLACK_REPLAY_WINDOW_SEC } from './server/verify.js';
10
+ export { slackEnvelopeSchema, eventCallbackSchema, urlVerificationSchema } from './server/schema.js';
11
+ export { DeliveryDedupeCache } from './server/dedupe.js';
12
+ export { IngestServer, SLACK_EVENTS_PATH } from './server/ingest-server.js';
13
+ export { SLACK_BOT_TOKEN_KEY, SLACK_SIGNING_SECRET_KEY, SLACK_AUTHORIZED_KEY, SLACK_BOT_TOKEN_ENV, SLACK_SIGNING_SECRET_ENV, resolveBotToken, resolveSigningSecret, parseAuthorization, authorizationMatches, } from './keys.js';
14
+ /**
15
+ * Build the Slack channel plugin. The CLI passes the concrete vault (mirroring
16
+ * the Telegram plugin's vault injection). The channel exposes its own ingest
17
+ * server publicly via the self-hosted `proxyTunnel` (imported directly, like
18
+ * the webhooks tunnel).
19
+ */
20
+ export function buildSlackPlugin(opts) {
21
+ return makeSlackPlugin(() => opts.vault);
22
+ }
23
+ /**
24
+ * Discovery-loadable default export: resolves the vault from the inter-plugin
25
+ * service registry in `onInit` (the vault plugin publishes `'vault'`). Requires
26
+ * `@moxxy/plugin-vault` to load first (declared in `package.json`
27
+ * `moxxy.requirements`). The channel + subcommands read the vault via
28
+ * `getVault()`, so resolution is deferred to call time — after `onInit` wired it.
29
+ */
30
+ export const slackPlugin = (() => {
31
+ let resolved = null;
32
+ const getVault = () => {
33
+ if (!resolved) {
34
+ throw new Error('@moxxy/plugin-channel-slack: the "vault" service is unavailable — @moxxy/plugin-vault must load first');
35
+ }
36
+ return resolved;
37
+ };
38
+ const hooks = {
39
+ onInit: (ctx) => {
40
+ resolved = ctx.services.require('vault');
41
+ },
42
+ };
43
+ return makeSlackPlugin(getVault, hooks);
44
+ })();
45
+ export default slackPlugin;
46
+ function readAllowedTools(options) {
47
+ const raw = options?.['allowedTools'];
48
+ if (Array.isArray(raw))
49
+ return raw.map((x) => String(x));
50
+ return undefined;
51
+ }
52
+ function makeSlackPlugin(getVault, hooks) {
53
+ return definePlugin({
54
+ name: '@moxxy/plugin-channel-slack',
55
+ version: '0.0.0',
56
+ ...(hooks ? { hooks } : {}),
57
+ // NB: we do NOT register a tunnelProvider here. The channel opens its ingest
58
+ // tunnel by importing `proxyTunnel` directly (see ingest-server), exactly like
59
+ // the webhooks plugin. Registering it would throw "already registered" when the
60
+ // web channel (which does register `proxyTunnel`) is also loaded, silently
61
+ // dropping this whole plugin — the tunnel-providers registry rejects duplicates.
62
+ channels: [
63
+ defineChannel({
64
+ name: 'slack',
65
+ description: 'Slack bot channel: ingests the Events API over the proxy relay and streams threaded replies. Autonomous allow-list permissions (no human in the loop).',
66
+ // Runs on its own dedicated, isolated runner (separate socket + sticky
67
+ // session) so the bot operates independently of the user's desktop/TUI.
68
+ // The CLI reads these generically — no per-channel name list.
69
+ dedicatedRunner: true,
70
+ sessionSource: 'slack',
71
+ // Self-described config so a control surface (TUI `/channels`, `moxxy
72
+ // channels start`) can configure + run Slack without a hardcoded table.
73
+ // The vault keys named here are the ones the channel reads at boot.
74
+ config: {
75
+ fields: [
76
+ {
77
+ name: 'botToken',
78
+ label: 'Bot token',
79
+ vaultKey: SLACK_BOT_TOKEN_KEY,
80
+ required: true,
81
+ secret: true,
82
+ placeholder: 'xoxb-…',
83
+ help: 'Slack app → OAuth & Permissions → Bot User OAuth Token',
84
+ },
85
+ {
86
+ name: 'signingSecret',
87
+ label: 'Signing secret',
88
+ vaultKey: SLACK_SIGNING_SECRET_KEY,
89
+ required: true,
90
+ secret: true,
91
+ help: 'Slack app → Basic Information → App Credentials → Signing Secret',
92
+ },
93
+ ],
94
+ hasRequestUrl: true,
95
+ runHint: 'Paste the Request URL into your Slack app → Event Subscriptions, subscribe to the app_mention bot event, then mention the bot in a channel to pair.',
96
+ connect: {
97
+ kind: 'url',
98
+ title: 'Request URL',
99
+ hint: 'Paste this into your Slack app → Event Subscriptions, subscribe to the app_mention bot event, then mention the bot in a channel to pair.',
100
+ },
101
+ },
102
+ create: (deps) => {
103
+ const options = deps.options;
104
+ const allowedTools = readAllowedTools(options);
105
+ const editFrameMs = options?.['editFrameMs'];
106
+ const host = options?.['host'];
107
+ const channelOpts = {
108
+ vault: getVault(),
109
+ ...(allowedTools ? { allowedTools } : {}),
110
+ ...(typeof editFrameMs === 'number' ? { editFrameMs } : {}),
111
+ ...(typeof host === 'string' ? { host } : {}),
112
+ logger: deps.logger,
113
+ };
114
+ return new SlackChannel(channelOpts);
115
+ },
116
+ isAvailable: async () => {
117
+ // Env-first: a fully env-configured bot is available even in a probe
118
+ // context (e.g. the `moxxy channels` listing) where onInit has not yet
119
+ // wired the vault service, so `getVault()` would throw.
120
+ let hasToken = (process.env[SLACK_BOT_TOKEN_ENV]?.trim() ?? '') !== '';
121
+ let hasSecret = (process.env[SLACK_SIGNING_SECRET_ENV]?.trim() ?? '') !== '';
122
+ if (hasToken && hasSecret)
123
+ return { ok: true };
124
+ try {
125
+ const vault = getVault();
126
+ if (!hasToken)
127
+ hasToken = (await resolveBotToken(vault)) != null;
128
+ if (!hasSecret)
129
+ hasSecret = (await resolveSigningSecret(vault)) != null;
130
+ }
131
+ catch {
132
+ // Vault unavailable in a probe/listing context — fall through to the
133
+ // "missing secrets" message below (env was already checked above).
134
+ }
135
+ if (hasToken && hasSecret)
136
+ return { ok: true };
137
+ const missing = [];
138
+ if (!hasToken)
139
+ missing.push('bot token (slack_bot_token / MOXXY_SLACK_BOT_TOKEN)');
140
+ if (!hasSecret)
141
+ missing.push('signing secret (slack_signing_secret / MOXXY_SLACK_SIGNING_SECRET)');
142
+ return {
143
+ ok: false,
144
+ reason: `Missing ${missing.join(' and ')}. Run \`moxxy channels slack setup\`.`,
145
+ };
146
+ },
147
+ interactiveCommand: 'setup',
148
+ subcommands: {
149
+ setup: {
150
+ description: 'Interactive setup: store the bot token + signing secret, validate via auth.test, pick the allow-list, open the tunnel, and print the Slack Request URL. Shown by default for `moxxy slack` on a TTY.',
151
+ run: async (ctx) => {
152
+ if (process.stdin.isTTY !== true) {
153
+ // Headless: just start the channel (secrets must already be set).
154
+ return ctx.startChannel();
155
+ }
156
+ return runSlackWizard(ctx);
157
+ },
158
+ },
159
+ pair: {
160
+ description: 'TOFU pairing: arm a window, then authorize the first team/channel that @mentions the bot.',
161
+ run: async (ctx) => {
162
+ if (process.stdin.isTTY !== true) {
163
+ process.stderr.write('Pairing needs a TTY (you confirm the team/channel). Run `moxxy slack pair` interactively.\n');
164
+ return 1;
165
+ }
166
+ return runSlackPairFlow(ctx);
167
+ },
168
+ },
169
+ status: {
170
+ description: 'Report token/secret/authorization state as JSON.',
171
+ run: async (ctx) => {
172
+ const vault = ctx.deps.vault;
173
+ if (!vault) {
174
+ process.stderr.write('vault unavailable\n');
175
+ return 1;
176
+ }
177
+ const botTokenConfigured = (await resolveBotToken(vault)) != null;
178
+ const signingSecretConfigured = (await resolveSigningSecret(vault)) != null;
179
+ const authorized = parseAuthorization(await vault.get(SLACK_AUTHORIZED_KEY));
180
+ process.stdout.write(JSON.stringify({
181
+ botTokenConfigured,
182
+ signingSecretConfigured,
183
+ authorized,
184
+ tunnelUrl: null,
185
+ }, null, 2) + '\n');
186
+ return 0;
187
+ },
188
+ },
189
+ unpair: {
190
+ description: 'Forget the authorized Slack team/channel.',
191
+ run: async (ctx) => {
192
+ const vault = ctx.deps.vault;
193
+ if (!vault) {
194
+ process.stderr.write('vault unavailable\n');
195
+ return 1;
196
+ }
197
+ const removed = await vault.delete(SLACK_AUTHORIZED_KEY);
198
+ process.stdout.write(removed ? 'unpaired\n' : 'no pairing was active\n');
199
+ return 0;
200
+ },
201
+ },
202
+ },
203
+ }),
204
+ ],
205
+ });
206
+ }
207
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/keys.d.ts ADDED
@@ -0,0 +1,50 @@
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
+ /** Vault key for the Slack bot OAuth token (`xoxb-…`). */
9
+ export declare const SLACK_BOT_TOKEN_KEY = "slack_bot_token";
10
+ /** Vault key for the Slack app signing secret (HMAC over the raw request body). */
11
+ export declare const SLACK_SIGNING_SECRET_KEY = "slack_signing_secret";
12
+ /**
13
+ * Vault key for the authorized team/channel set (TOFU pairing). Stored as a
14
+ * JSON string of an authorization record so we can grow what we pin without a
15
+ * format change (today: `{ teamId, channelId? }`).
16
+ */
17
+ export declare const SLACK_AUTHORIZED_KEY = "slack_authorized";
18
+ /** Env override for the bot token (beats the vault, matching every other channel). */
19
+ export declare const SLACK_BOT_TOKEN_ENV = "MOXXY_SLACK_BOT_TOKEN";
20
+ /** Env override for the signing secret. */
21
+ export declare const SLACK_SIGNING_SECRET_ENV = "MOXXY_SLACK_SIGNING_SECRET";
22
+ /** A Slack bot token always starts with `xoxb-`. */
23
+ export declare const SLACK_BOT_TOKEN_RE: RegExp;
24
+ /** zod validator for a bot token (shape only — connectivity is tested via `auth.test`). */
25
+ export declare const slackBotTokenSchema: z.ZodString;
26
+ /** zod validator for a signing secret (hex-ish; Slack uses a 32-byte hex secret). */
27
+ export declare const slackSigningSecretSchema: z.ZodString;
28
+ /**
29
+ * Resolve the bot token: env override first, then the vault (the shared
30
+ * env→vault resolution in @moxxy/channel-kit). Returns null when neither is
31
+ * set. Trimmed; never returns an empty string.
32
+ */
33
+ export declare function resolveBotToken(vault: {
34
+ get(name: string): Promise<string | null>;
35
+ }): Promise<string | null>;
36
+ /** Resolve the signing secret: env override first, then the vault. */
37
+ export declare function resolveSigningSecret(vault: {
38
+ get(name: string): Promise<string | null>;
39
+ }): Promise<string | null>;
40
+ /** What we persist under {@link SLACK_AUTHORIZED_KEY}. */
41
+ export interface SlackAuthorization {
42
+ readonly teamId: string;
43
+ /** Optionally narrow authorization to a single channel. */
44
+ readonly channelId?: string;
45
+ }
46
+ /** Parse the stored authorization record. Returns null for missing/corrupt. */
47
+ export declare function parseAuthorization(raw: string | null | undefined): SlackAuthorization | null;
48
+ /** Does an inbound event from `(teamId, channelId)` match the stored authorization? */
49
+ export declare function authorizationMatches(auth: SlackAuthorization | null, teamId: string | undefined, channelId: string | undefined): boolean;
50
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAG/B,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AACrD,mFAAmF;AACnF,eAAO,MAAM,wBAAwB,yBAAyB,CAAC;AAC/D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,qBAAqB,CAAC;AAEvD,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAC3D,2CAA2C;AAC3C,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE,oDAAoD;AACpD,eAAO,MAAM,kBAAkB,QAA6B,CAAC;AAE7D,2FAA2F;AAC3F,eAAO,MAAM,mBAAmB,aAGwC,CAAC;AAEzE,qFAAqF;AACrF,eAAO,MAAM,wBAAwB,aAIO,CAAC;AAE7C;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEzB;AAED,sEAAsE;AACtE,wBAAsB,oBAAoB,CAAC,KAAK,EAAE;IAChD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKzB;AAED,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2DAA2D;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,kBAAkB,GAAG,IAAI,CAa5F;AAED,uFAAuF;AACvF,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,kBAAkB,GAAG,IAAI,EAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,GAC5B,OAAO,CAKT"}