@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.
- package/LICENSE +21 -0
- package/dist/channel/slack-client.d.ts +53 -0
- package/dist/channel/slack-client.d.ts.map +1 -0
- package/dist/channel/slack-client.js +82 -0
- package/dist/channel/slack-client.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +39 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +81 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel.d.ts +100 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +286 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +207 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +50 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +78 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +18 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +106 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/permission.d.ts +27 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +33 -0
- package/dist/permission.js.map +1 -0
- package/dist/server/dedupe.d.ts +8 -0
- package/dist/server/dedupe.d.ts.map +1 -0
- package/dist/server/dedupe.js +8 -0
- package/dist/server/dedupe.js.map +1 -0
- package/dist/server/ingest-server.d.ts +80 -0
- package/dist/server/ingest-server.d.ts.map +1 -0
- package/dist/server/ingest-server.js +142 -0
- package/dist/server/ingest-server.js.map +1 -0
- package/dist/server/schema.d.ts +257 -0
- package/dist/server/schema.d.ts.map +1 -0
- package/dist/server/schema.js +69 -0
- package/dist/server/schema.js.map +1 -0
- package/dist/server/verify.d.ts +39 -0
- package/dist/server/verify.d.ts.map +1 -0
- package/dist/server/verify.js +73 -0
- package/dist/server/verify.js.map +1 -0
- package/dist/setup-wizard.d.ts +17 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +92 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +97 -0
- package/src/channel/slack-client.ts +123 -0
- package/src/channel/turn-runner.test.ts +143 -0
- package/src/channel/turn-runner.ts +107 -0
- package/src/channel.ts +378 -0
- package/src/index.ts +254 -0
- package/src/keys.ts +96 -0
- package/src/pair-flow.ts +119 -0
- package/src/permission.test.ts +65 -0
- package/src/permission.ts +42 -0
- package/src/server/dedupe.ts +7 -0
- package/src/server/ingest-server.test.ts +280 -0
- package/src/server/ingest-server.ts +205 -0
- package/src/server/schema.test.ts +67 -0
- package/src/server/schema.ts +77 -0
- package/src/server/verify.test.ts +132 -0
- package/src/server/verify.ts +88 -0
- package/src/setup-wizard.ts +121 -0
- package/src/subcommands.test.ts +189 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { VaultStore, createStaticKeySource, deriveKey, generateSalt } from '@moxxy/plugin-vault';
|
|
6
|
+
import type { ChannelDef } from '@moxxy/sdk';
|
|
7
|
+
import { buildSlackPlugin } from './index.js';
|
|
8
|
+
import {
|
|
9
|
+
SLACK_AUTHORIZED_KEY,
|
|
10
|
+
SLACK_BOT_TOKEN_KEY,
|
|
11
|
+
SLACK_SIGNING_SECRET_KEY,
|
|
12
|
+
} from './keys.js';
|
|
13
|
+
|
|
14
|
+
let tmp: string;
|
|
15
|
+
let vault: VaultStore;
|
|
16
|
+
let slackDef: ChannelDef;
|
|
17
|
+
let writeOut: string[];
|
|
18
|
+
let writeErr: string[];
|
|
19
|
+
let origStdoutWrite: typeof process.stdout.write;
|
|
20
|
+
let origStderrWrite: typeof process.stderr.write;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mox-slack-sub-'));
|
|
24
|
+
vault = new VaultStore({
|
|
25
|
+
filePath: path.join(tmp, 'vault.json'),
|
|
26
|
+
keySource: createStaticKeySource(deriveKey('test', generateSalt())),
|
|
27
|
+
});
|
|
28
|
+
const plugin = buildSlackPlugin({ vault });
|
|
29
|
+
slackDef = plugin.channels![0]!;
|
|
30
|
+
writeOut = [];
|
|
31
|
+
writeErr = [];
|
|
32
|
+
origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
33
|
+
origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
34
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
35
|
+
writeOut.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
|
|
36
|
+
return true;
|
|
37
|
+
}) as typeof process.stdout.write;
|
|
38
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
39
|
+
writeErr.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
|
|
40
|
+
return true;
|
|
41
|
+
}) as typeof process.stderr.write;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
46
|
+
process.stdout.write = origStdoutWrite;
|
|
47
|
+
process.stderr.write = origStderrWrite;
|
|
48
|
+
delete process.env.MOXXY_SLACK_BOT_TOKEN;
|
|
49
|
+
delete process.env.MOXXY_SLACK_SIGNING_SECRET;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function ctx(overrides: { startChannel?: () => Promise<number> } = {}) {
|
|
53
|
+
return {
|
|
54
|
+
deps: { cwd: tmp, vault, logger: undefined, options: {} },
|
|
55
|
+
args: { positional: [], flags: {} },
|
|
56
|
+
startChannel: overrides.startChannel ?? (async () => 0),
|
|
57
|
+
session: { setPermissionResolver: () => {} },
|
|
58
|
+
} as never;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('slack channel subcommands (registered on ChannelDef)', () => {
|
|
62
|
+
it('exposes setup, pair, status, unpair', () => {
|
|
63
|
+
expect(slackDef.subcommands).toBeDefined();
|
|
64
|
+
expect(Object.keys(slackDef.subcommands!)).toEqual(
|
|
65
|
+
expect.arrayContaining(['setup', 'pair', 'status', 'unpair']),
|
|
66
|
+
);
|
|
67
|
+
expect(slackDef.interactiveCommand).toBe('setup');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('`status` reports unconfigured vault state as JSON', async () => {
|
|
71
|
+
const code = await slackDef.subcommands!.status!.run(ctx());
|
|
72
|
+
expect(code).toBe(0);
|
|
73
|
+
const parsed = JSON.parse(writeOut.join(''));
|
|
74
|
+
expect(parsed).toEqual({
|
|
75
|
+
botTokenConfigured: false,
|
|
76
|
+
signingSecretConfigured: false,
|
|
77
|
+
authorized: null,
|
|
78
|
+
tunnelUrl: null,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('`status` surfaces stored token + secret + authorization', async () => {
|
|
83
|
+
await vault.set(SLACK_BOT_TOKEN_KEY, 'xoxb-1111-2222-abcdefghijklmnop');
|
|
84
|
+
await vault.set(SLACK_SIGNING_SECRET_KEY, '0123456789abcdef0123456789abcdef');
|
|
85
|
+
await vault.set(
|
|
86
|
+
SLACK_AUTHORIZED_KEY,
|
|
87
|
+
JSON.stringify({ teamId: 'T1', channelId: 'C1' }),
|
|
88
|
+
);
|
|
89
|
+
const code = await slackDef.subcommands!.status!.run(ctx());
|
|
90
|
+
expect(code).toBe(0);
|
|
91
|
+
const parsed = JSON.parse(writeOut.join(''));
|
|
92
|
+
expect(parsed).toEqual({
|
|
93
|
+
botTokenConfigured: true,
|
|
94
|
+
signingSecretConfigured: true,
|
|
95
|
+
authorized: { teamId: 'T1', channelId: 'C1' },
|
|
96
|
+
tunnelUrl: null,
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('`status` honors env overrides for token/secret', async () => {
|
|
101
|
+
process.env.MOXXY_SLACK_BOT_TOKEN = 'xoxb-env-token-abcdefghijkl';
|
|
102
|
+
process.env.MOXXY_SLACK_SIGNING_SECRET = 'env-signing-secret-0123456789';
|
|
103
|
+
const code = await slackDef.subcommands!.status!.run(ctx());
|
|
104
|
+
expect(code).toBe(0);
|
|
105
|
+
const parsed = JSON.parse(writeOut.join(''));
|
|
106
|
+
expect(parsed.botTokenConfigured).toBe(true);
|
|
107
|
+
expect(parsed.signingSecretConfigured).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('`status` reports null for a corrupt authorization record', async () => {
|
|
111
|
+
await vault.set(SLACK_AUTHORIZED_KEY, 'not-json');
|
|
112
|
+
const code = await slackDef.subcommands!.status!.run(ctx());
|
|
113
|
+
expect(code).toBe(0);
|
|
114
|
+
const parsed = JSON.parse(writeOut.join(''));
|
|
115
|
+
expect(parsed.authorized).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('`unpair` clears the authorization and reports', async () => {
|
|
119
|
+
await vault.set(SLACK_AUTHORIZED_KEY, JSON.stringify({ teamId: 'T1' }));
|
|
120
|
+
const code = await slackDef.subcommands!.unpair!.run(ctx());
|
|
121
|
+
expect(code).toBe(0);
|
|
122
|
+
expect(writeOut.join('')).toContain('unpaired');
|
|
123
|
+
expect(await vault.get(SLACK_AUTHORIZED_KEY)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('`unpair` is a no-op when nothing is paired', async () => {
|
|
127
|
+
const code = await slackDef.subcommands!.unpair!.run(ctx());
|
|
128
|
+
expect(code).toBe(0);
|
|
129
|
+
expect(writeOut.join('')).toContain('no pairing was active');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('`pair` refuses to start without a TTY (interactive-only flow)', async () => {
|
|
133
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
134
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
|
135
|
+
try {
|
|
136
|
+
const startChannel = vi.fn(async () => 0);
|
|
137
|
+
const code = await slackDef.subcommands!.pair!.run(ctx({ startChannel }));
|
|
138
|
+
expect(code).toBe(1);
|
|
139
|
+
expect(startChannel).not.toHaveBeenCalled();
|
|
140
|
+
expect(writeErr.join('')).toMatch(/TTY/);
|
|
141
|
+
} finally {
|
|
142
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('`setup` starts the channel directly when not on a TTY', async () => {
|
|
147
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
148
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
|
149
|
+
try {
|
|
150
|
+
const startChannel = vi.fn(async () => 0);
|
|
151
|
+
const code = await slackDef.subcommands!.setup!.run(ctx({ startChannel }));
|
|
152
|
+
expect(code).toBe(0);
|
|
153
|
+
expect(startChannel).toHaveBeenCalledTimes(1);
|
|
154
|
+
} finally {
|
|
155
|
+
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('subcommands return 1 when vault is unavailable', async () => {
|
|
160
|
+
const badCtx = {
|
|
161
|
+
deps: { cwd: tmp, vault: undefined, logger: undefined, options: {} },
|
|
162
|
+
args: { positional: [], flags: {} },
|
|
163
|
+
startChannel: async () => 0,
|
|
164
|
+
session: { setPermissionResolver: () => {} },
|
|
165
|
+
} as never;
|
|
166
|
+
const code = await slackDef.subcommands!.status!.run(badCtx);
|
|
167
|
+
expect(code).toBe(1);
|
|
168
|
+
expect(writeErr.join('')).toContain('vault unavailable');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('isAvailable gates on BOTH token and secret', async () => {
|
|
172
|
+
// Neither set → unavailable, reason names both.
|
|
173
|
+
let avail = await slackDef.isAvailable!({ cwd: tmp, vault });
|
|
174
|
+
expect(avail.ok).toBe(false);
|
|
175
|
+
expect(avail.reason).toMatch(/bot token/);
|
|
176
|
+
expect(avail.reason).toMatch(/signing secret/);
|
|
177
|
+
|
|
178
|
+
// Only token → still unavailable, reason names the secret.
|
|
179
|
+
await vault.set(SLACK_BOT_TOKEN_KEY, 'xoxb-1111-2222-abcdefghijklmnop');
|
|
180
|
+
avail = await slackDef.isAvailable!({ cwd: tmp, vault });
|
|
181
|
+
expect(avail.ok).toBe(false);
|
|
182
|
+
expect(avail.reason).toMatch(/signing secret/);
|
|
183
|
+
|
|
184
|
+
// Both → available.
|
|
185
|
+
await vault.set(SLACK_SIGNING_SECRET_KEY, '0123456789abcdef0123456789abcdef');
|
|
186
|
+
avail = await slackDef.isAvailable!({ cwd: tmp, vault });
|
|
187
|
+
expect(avail.ok).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
});
|