@openclaw/twitch 2026.5.2 → 2026.5.3-beta.2

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 (54) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-plugin-api.js +2 -0
  3. package/dist/index.js +18 -0
  4. package/dist/markdown-MRdI1sR7.js +306 -0
  5. package/dist/monitor-DS0YTAPB.js +333 -0
  6. package/dist/plugin-BQX9GiIn.js +878 -0
  7. package/dist/runtime-QZ5I3GlI.js +8 -0
  8. package/dist/runtime-api.js +1 -0
  9. package/dist/setup-entry.js +11 -0
  10. package/dist/setup-plugin-api.js +2 -0
  11. package/dist/setup-surface-yArVgckI.js +400 -0
  12. package/dist/twitch-CklAMZL5.js +131 -0
  13. package/package.json +20 -3
  14. package/api.ts +0 -21
  15. package/channel-plugin-api.ts +0 -1
  16. package/index.test.ts +0 -13
  17. package/index.ts +0 -16
  18. package/runtime-api.ts +0 -22
  19. package/setup-entry.ts +0 -9
  20. package/setup-plugin-api.ts +0 -3
  21. package/src/access-control.test.ts +0 -388
  22. package/src/access-control.ts +0 -173
  23. package/src/actions.test.ts +0 -74
  24. package/src/actions.ts +0 -175
  25. package/src/client-manager-registry.ts +0 -87
  26. package/src/config-schema.test.ts +0 -46
  27. package/src/config-schema.ts +0 -88
  28. package/src/config.test.ts +0 -233
  29. package/src/config.ts +0 -177
  30. package/src/monitor.ts +0 -314
  31. package/src/outbound.test.ts +0 -468
  32. package/src/outbound.ts +0 -186
  33. package/src/plugin.test.ts +0 -77
  34. package/src/plugin.ts +0 -208
  35. package/src/probe.test.ts +0 -196
  36. package/src/probe.ts +0 -130
  37. package/src/resolver.ts +0 -139
  38. package/src/runtime.ts +0 -9
  39. package/src/send.test.ts +0 -309
  40. package/src/send.ts +0 -139
  41. package/src/setup-surface.test.ts +0 -511
  42. package/src/setup-surface.ts +0 -520
  43. package/src/status.test.ts +0 -237
  44. package/src/status.ts +0 -179
  45. package/src/test-fixtures.ts +0 -30
  46. package/src/token.test.ts +0 -192
  47. package/src/token.ts +0 -93
  48. package/src/twitch-client.test.ts +0 -582
  49. package/src/twitch-client.ts +0 -276
  50. package/src/types.ts +0 -104
  51. package/src/utils/markdown.ts +0 -98
  52. package/src/utils/twitch.ts +0 -84
  53. package/test/setup.ts +0 -7
  54. package/tsconfig.json +0 -16
@@ -1,237 +0,0 @@
1
- /**
2
- * Tests for status.ts module
3
- *
4
- * Tests cover:
5
- * - Detection of unconfigured accounts
6
- * - Detection of disabled accounts
7
- * - Detection of missing clientId
8
- * - Token format warnings
9
- * - Access control warnings
10
- * - Runtime error detection
11
- */
12
-
13
- import { describe, expect, it } from "vitest";
14
- import { collectTwitchStatusIssues } from "./status.js";
15
- import type { ChannelAccountSnapshot } from "./types.js";
16
-
17
- function createSnapshot(overrides: Partial<ChannelAccountSnapshot> = {}): ChannelAccountSnapshot {
18
- return {
19
- accountId: "default",
20
- configured: true,
21
- enabled: true,
22
- running: false,
23
- ...overrides,
24
- };
25
- }
26
-
27
- function createSimpleTwitchConfig(overrides: Record<string, unknown>) {
28
- return {
29
- channels: {
30
- twitch: overrides,
31
- },
32
- };
33
- }
34
-
35
- describe("status", () => {
36
- describe("collectTwitchStatusIssues", () => {
37
- it("should detect unconfigured accounts", () => {
38
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot({ configured: false })];
39
-
40
- const issues = collectTwitchStatusIssues(snapshots);
41
-
42
- expect(issues.length).toBeGreaterThan(0);
43
- expect(issues[0]?.kind).toBe("config");
44
- expect(issues[0]?.message).toContain("not properly configured");
45
- });
46
-
47
- it("should detect disabled accounts", () => {
48
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot({ enabled: false })];
49
-
50
- const issues = collectTwitchStatusIssues(snapshots);
51
-
52
- expect(issues.length).toBeGreaterThan(0);
53
- expect(issues).toContainEqual(
54
- expect.objectContaining({
55
- kind: "config",
56
- message: "Twitch account is disabled",
57
- }),
58
- );
59
- });
60
-
61
- it("should detect missing clientId when account configured (simplified config)", () => {
62
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
63
- const mockCfg = createSimpleTwitchConfig({
64
- username: "testbot",
65
- accessToken: "oauth:test123",
66
- // clientId missing
67
- });
68
-
69
- const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
70
-
71
- expect(issues).toContainEqual(
72
- expect.objectContaining({
73
- kind: "config",
74
- message: "Twitch client ID is required",
75
- }),
76
- );
77
- });
78
-
79
- it("should warn about oauth: prefix in token (simplified config)", () => {
80
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
81
- const mockCfg = createSimpleTwitchConfig({
82
- username: "testbot",
83
- accessToken: "oauth:test123", // has prefix
84
- clientId: "test-id",
85
- });
86
-
87
- const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
88
-
89
- expect(issues).toContainEqual(
90
- expect.objectContaining({
91
- kind: "config",
92
- message: "Token contains 'oauth:' prefix (will be stripped)",
93
- }),
94
- );
95
- });
96
-
97
- it("should detect clientSecret without refreshToken (simplified config)", () => {
98
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
99
- const mockCfg = createSimpleTwitchConfig({
100
- username: "testbot",
101
- accessToken: "oauth:test123",
102
- clientId: "test-id",
103
- clientSecret: "secret123",
104
- // refreshToken missing
105
- });
106
-
107
- const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
108
-
109
- expect(issues).toContainEqual(
110
- expect.objectContaining({
111
- kind: "config",
112
- message: "clientSecret provided without refreshToken",
113
- }),
114
- );
115
- });
116
-
117
- it("should detect empty allowFrom array (simplified config)", () => {
118
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
119
- const mockCfg = createSimpleTwitchConfig({
120
- username: "testbot",
121
- accessToken: "test123",
122
- clientId: "test-id",
123
- allowFrom: [], // empty array
124
- });
125
-
126
- const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
127
-
128
- expect(issues).toContainEqual(
129
- expect.objectContaining({
130
- kind: "config",
131
- message: "allowFrom is configured but empty",
132
- }),
133
- );
134
- });
135
-
136
- it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => {
137
- const snapshots: ChannelAccountSnapshot[] = [createSnapshot()];
138
- const mockCfg = createSimpleTwitchConfig({
139
- username: "testbot",
140
- accessToken: "test123",
141
- clientId: "test-id",
142
- allowedRoles: ["all"],
143
- allowFrom: ["123456"], // conflict!
144
- });
145
-
146
- const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
147
-
148
- expect(issues).toContainEqual(
149
- expect.objectContaining({
150
- kind: "intent",
151
- message: "allowedRoles is set to 'all' but allowFrom is also configured",
152
- }),
153
- );
154
- });
155
-
156
- it("should detect runtime errors", () => {
157
- const snapshots: ChannelAccountSnapshot[] = [
158
- createSnapshot({ lastError: "Connection timeout" }),
159
- ];
160
-
161
- const issues = collectTwitchStatusIssues(snapshots);
162
-
163
- expect(issues).toContainEqual(
164
- expect.objectContaining({
165
- kind: "runtime",
166
- message: "Last error: Connection timeout",
167
- }),
168
- );
169
- });
170
-
171
- it("should detect accounts that never connected", () => {
172
- const snapshots: ChannelAccountSnapshot[] = [
173
- createSnapshot({
174
- lastStartAt: undefined,
175
- lastInboundAt: undefined,
176
- lastOutboundAt: undefined,
177
- }),
178
- ];
179
-
180
- const issues = collectTwitchStatusIssues(snapshots);
181
-
182
- expect(issues).toContainEqual(
183
- expect.objectContaining({
184
- kind: "runtime",
185
- message: "Account has never connected successfully",
186
- }),
187
- );
188
- });
189
-
190
- it("should detect long-running connections", () => {
191
- const oldDate = Date.now() - 8 * 24 * 60 * 60 * 1000; // 8 days ago
192
-
193
- const snapshots: ChannelAccountSnapshot[] = [
194
- createSnapshot({
195
- running: true,
196
- lastStartAt: oldDate,
197
- }),
198
- ];
199
-
200
- const issues = collectTwitchStatusIssues(snapshots);
201
-
202
- expect(issues).toContainEqual(
203
- expect.objectContaining({
204
- kind: "runtime",
205
- message: "Connection has been running for 8 days",
206
- }),
207
- );
208
- });
209
-
210
- it("should handle empty snapshots array", () => {
211
- const issues = collectTwitchStatusIssues([]);
212
-
213
- expect(issues).toEqual([]);
214
- });
215
-
216
- it("should skip non-Twitch accounts gracefully", () => {
217
- const snapshots: ChannelAccountSnapshot[] = [
218
- {
219
- accountId: "unknown",
220
- configured: false,
221
- enabled: true,
222
- running: false,
223
- },
224
- ];
225
-
226
- const issues = collectTwitchStatusIssues(snapshots);
227
-
228
- expect(issues).toEqual([
229
- expect.objectContaining({
230
- accountId: "unknown",
231
- kind: "config",
232
- message: "Twitch account is not properly configured",
233
- }),
234
- ]);
235
- });
236
- });
237
- });
package/src/status.ts DELETED
@@ -1,179 +0,0 @@
1
- /**
2
- * Twitch status issues collector.
3
- *
4
- * Detects and reports configuration issues for Twitch accounts.
5
- */
6
-
7
- import type { ChannelStatusIssue } from "openclaw/plugin-sdk/channel-contract";
8
- import { getAccountConfig } from "./config.js";
9
- import { resolveTwitchToken } from "./token.js";
10
- import type { ChannelAccountSnapshot } from "./types.js";
11
- import { isAccountConfigured } from "./utils/twitch.js";
12
-
13
- /**
14
- * Collect status issues for Twitch accounts.
15
- *
16
- * Analyzes account snapshots and detects configuration problems,
17
- * authentication issues, and other potential problems.
18
- *
19
- * @param accounts - Array of account snapshots to analyze
20
- * @param getCfg - Optional function to get full config for additional checks
21
- * @returns Array of detected status issues
22
- *
23
- * @example
24
- * const issues = collectTwitchStatusIssues(accountSnapshots);
25
- * if (issues.length > 0) {
26
- * console.warn("Twitch configuration issues detected:");
27
- * issues.forEach(issue => console.warn(`- ${issue.message}`));
28
- * }
29
- */
30
- export function collectTwitchStatusIssues(
31
- accounts: ChannelAccountSnapshot[],
32
- getCfg?: () => unknown,
33
- ): ChannelStatusIssue[] {
34
- const issues: ChannelStatusIssue[] = [];
35
-
36
- for (const entry of accounts) {
37
- const accountId = entry.accountId;
38
-
39
- if (!accountId) {
40
- continue;
41
- }
42
-
43
- let account: ReturnType<typeof getAccountConfig> | null = null;
44
- let cfg: Parameters<typeof resolveTwitchToken>[0] | undefined;
45
- if (getCfg) {
46
- try {
47
- cfg = getCfg() as {
48
- channels?: { twitch?: { accounts?: Record<string, unknown> } };
49
- };
50
- account = getAccountConfig(cfg, accountId);
51
- } catch {
52
- // Ignore config access errors
53
- }
54
- }
55
-
56
- if (!entry.configured) {
57
- issues.push({
58
- channel: "twitch",
59
- accountId,
60
- kind: "config",
61
- message: "Twitch account is not properly configured",
62
- fix: "Add required fields: username, accessToken, and clientId to your account configuration",
63
- });
64
- continue;
65
- }
66
-
67
- if (entry.enabled === false) {
68
- issues.push({
69
- channel: "twitch",
70
- accountId,
71
- kind: "config",
72
- message: "Twitch account is disabled",
73
- fix: "Set enabled: true in your account configuration to enable this account",
74
- });
75
- continue;
76
- }
77
-
78
- if (account && account.username && account.accessToken && !account.clientId) {
79
- issues.push({
80
- channel: "twitch",
81
- accountId,
82
- kind: "config",
83
- message: "Twitch client ID is required",
84
- fix: "Add clientId to your Twitch account configuration (from Twitch Developer Portal)",
85
- });
86
- }
87
-
88
- const tokenResolution = cfg
89
- ? resolveTwitchToken(cfg as Parameters<typeof resolveTwitchToken>[0], { accountId })
90
- : { token: "", source: "none" };
91
- if (account && isAccountConfigured(account, tokenResolution.token)) {
92
- if (account.accessToken?.startsWith("oauth:")) {
93
- issues.push({
94
- channel: "twitch",
95
- accountId,
96
- kind: "config",
97
- message: "Token contains 'oauth:' prefix (will be stripped)",
98
- fix: "The 'oauth:' prefix is optional. You can use just the token value, or keep it as-is (it will be normalized automatically).",
99
- });
100
- }
101
-
102
- if (account.clientSecret && !account.refreshToken) {
103
- issues.push({
104
- channel: "twitch",
105
- accountId,
106
- kind: "config",
107
- message: "clientSecret provided without refreshToken",
108
- fix: "For automatic token refresh, provide both clientSecret and refreshToken. Otherwise, clientSecret is not needed.",
109
- });
110
- }
111
-
112
- if (account.allowFrom && account.allowFrom.length === 0) {
113
- issues.push({
114
- channel: "twitch",
115
- accountId,
116
- kind: "config",
117
- message: "allowFrom is configured but empty",
118
- fix: "Either add user IDs to allowFrom, remove the allowFrom field, or use allowedRoles instead.",
119
- });
120
- }
121
-
122
- if (
123
- account.allowedRoles?.includes("all") &&
124
- account.allowFrom &&
125
- account.allowFrom.length > 0
126
- ) {
127
- issues.push({
128
- channel: "twitch",
129
- accountId,
130
- kind: "intent",
131
- message: "allowedRoles is set to 'all' but allowFrom is also configured",
132
- fix: "When allowedRoles is 'all', the allowFrom list is not needed. Remove allowFrom or set allowedRoles to specific roles.",
133
- });
134
- }
135
- }
136
-
137
- if (entry.lastError) {
138
- issues.push({
139
- channel: "twitch",
140
- accountId,
141
- kind: "runtime",
142
- message: `Last error: ${entry.lastError}`,
143
- fix: "Check your token validity and network connection. Ensure the bot has the required OAuth scopes.",
144
- });
145
- }
146
-
147
- if (
148
- entry.configured &&
149
- !entry.running &&
150
- !entry.lastStartAt &&
151
- !entry.lastInboundAt &&
152
- !entry.lastOutboundAt
153
- ) {
154
- issues.push({
155
- channel: "twitch",
156
- accountId,
157
- kind: "runtime",
158
- message: "Account has never connected successfully",
159
- fix: "Start the Twitch gateway to begin receiving messages. Check logs for connection errors.",
160
- });
161
- }
162
-
163
- if (entry.running && entry.lastStartAt) {
164
- const uptime = Date.now() - entry.lastStartAt;
165
- const daysSinceStart = uptime / (1000 * 60 * 60 * 24);
166
- if (daysSinceStart > 7) {
167
- issues.push({
168
- channel: "twitch",
169
- accountId,
170
- kind: "runtime",
171
- message: `Connection has been running for ${Math.floor(daysSinceStart)} days`,
172
- fix: "Consider restarting the connection periodically to refresh the connection. Twitch tokens may expire after long periods.",
173
- });
174
- }
175
- }
176
- }
177
-
178
- return issues;
179
- }
@@ -1,30 +0,0 @@
1
- import { afterEach, beforeEach, vi } from "vitest";
2
- import type { OpenClawConfig } from "../runtime-api.js";
3
-
4
- export const BASE_TWITCH_TEST_ACCOUNT = {
5
- username: "testbot",
6
- clientId: "test-client-id",
7
- channel: "#testchannel",
8
- };
9
-
10
- export function makeTwitchTestConfig(account: Record<string, unknown>): OpenClawConfig {
11
- return {
12
- channels: {
13
- twitch: {
14
- accounts: {
15
- default: account,
16
- },
17
- },
18
- },
19
- } as unknown as OpenClawConfig;
20
- }
21
-
22
- export function installTwitchTestHooks() {
23
- beforeEach(() => {
24
- vi.clearAllMocks();
25
- });
26
-
27
- afterEach(() => {
28
- vi.restoreAllMocks();
29
- });
30
- }
package/src/token.test.ts DELETED
@@ -1,192 +0,0 @@
1
- /**
2
- * Tests for token.ts module
3
- *
4
- * Tests cover:
5
- * - Token resolution from config
6
- * - Token resolution from environment variable
7
- * - Fallback behavior when token not found
8
- * - Account ID normalization
9
- */
10
-
11
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
- import type { OpenClawConfig } from "../api.js";
13
- import { resolveTwitchToken, type TwitchTokenSource } from "./token.js";
14
-
15
- describe("token", () => {
16
- // Multi-account config for testing non-default accounts
17
- const mockMultiAccountConfig = {
18
- channels: {
19
- twitch: {
20
- accounts: {
21
- default: {
22
- username: "testbot",
23
- accessToken: "oauth:config-token",
24
- },
25
- other: {
26
- username: "otherbot",
27
- accessToken: "oauth:other-token",
28
- },
29
- },
30
- },
31
- },
32
- } as unknown as OpenClawConfig;
33
-
34
- // Simplified single-account config
35
- const mockSimplifiedConfig = {
36
- channels: {
37
- twitch: {
38
- username: "testbot",
39
- accessToken: "oauth:config-token",
40
- },
41
- },
42
- } as unknown as OpenClawConfig;
43
-
44
- beforeEach(() => {
45
- vi.clearAllMocks();
46
- });
47
-
48
- afterEach(() => {
49
- vi.restoreAllMocks();
50
- delete process.env.OPENCLAW_TWITCH_ACCESS_TOKEN;
51
- });
52
-
53
- describe("resolveTwitchToken", () => {
54
- it("should resolve token from simplified config for default account", () => {
55
- const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" });
56
-
57
- expect(result.token).toBe("oauth:config-token");
58
- expect(result.source).toBe("config");
59
- });
60
-
61
- it("should resolve token from config for non-default account (multi-account)", () => {
62
- const result = resolveTwitchToken(mockMultiAccountConfig, { accountId: "other" });
63
-
64
- expect(result.token).toBe("oauth:other-token");
65
- expect(result.source).toBe("config");
66
- });
67
-
68
- it("should resolve token from normalized account id", () => {
69
- const result = resolveTwitchToken(
70
- {
71
- channels: {
72
- twitch: {
73
- accounts: {
74
- Secondary: {
75
- username: "secondary",
76
- accessToken: "oauth:secondary-token",
77
- },
78
- },
79
- },
80
- },
81
- } as unknown as OpenClawConfig,
82
- { accountId: "secondary" },
83
- );
84
-
85
- expect(result.token).toBe("oauth:secondary-token");
86
- expect(result.source).toBe("config");
87
- });
88
-
89
- it("should prioritize config token over env var (simplified config)", () => {
90
- process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:env-token";
91
-
92
- const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" });
93
-
94
- // Config token should be used even if env var exists
95
- expect(result.token).toBe("oauth:config-token");
96
- expect(result.source).toBe("config");
97
- });
98
-
99
- it("should use env var when config token is empty (simplified config)", () => {
100
- process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:env-token";
101
-
102
- const configWithEmptyToken = {
103
- channels: {
104
- twitch: {
105
- username: "testbot",
106
- accessToken: "",
107
- },
108
- },
109
- } as unknown as OpenClawConfig;
110
-
111
- const result = resolveTwitchToken(configWithEmptyToken, { accountId: "default" });
112
-
113
- expect(result.token).toBe("oauth:env-token");
114
- expect(result.source).toBe("env");
115
- });
116
-
117
- it("should return empty token when neither config nor env has token (simplified config)", () => {
118
- const configWithoutToken = {
119
- channels: {
120
- twitch: {
121
- username: "testbot",
122
- accessToken: "",
123
- },
124
- },
125
- } as unknown as OpenClawConfig;
126
-
127
- const result = resolveTwitchToken(configWithoutToken, { accountId: "default" });
128
-
129
- expect(result.token).toBe("");
130
- expect(result.source).toBe("none");
131
- });
132
-
133
- it("should not use env var for non-default accounts (multi-account)", () => {
134
- process.env.OPENCLAW_TWITCH_ACCESS_TOKEN = "oauth:env-token";
135
-
136
- const configWithoutToken = {
137
- channels: {
138
- twitch: {
139
- accounts: {
140
- secondary: {
141
- username: "secondary",
142
- accessToken: "",
143
- },
144
- },
145
- },
146
- },
147
- } as unknown as OpenClawConfig;
148
-
149
- const result = resolveTwitchToken(configWithoutToken, { accountId: "secondary" });
150
-
151
- // Non-default accounts shouldn't use env var
152
- expect(result.token).toBe("");
153
- expect(result.source).toBe("none");
154
- });
155
-
156
- it("should handle missing account gracefully", () => {
157
- const configWithoutAccount = {
158
- channels: {
159
- twitch: {
160
- accounts: {},
161
- },
162
- },
163
- } as unknown as OpenClawConfig;
164
-
165
- const result = resolveTwitchToken(configWithoutAccount, { accountId: "nonexistent" });
166
-
167
- expect(result.token).toBe("");
168
- expect(result.source).toBe("none");
169
- });
170
-
171
- it("should handle missing Twitch config section", () => {
172
- const configWithoutSection = {
173
- channels: {},
174
- } as unknown as OpenClawConfig;
175
-
176
- const result = resolveTwitchToken(configWithoutSection, { accountId: "default" });
177
-
178
- expect(result.token).toBe("");
179
- expect(result.source).toBe("none");
180
- });
181
- });
182
-
183
- describe("TwitchTokenSource type", () => {
184
- it("should have correct values", () => {
185
- const sources: TwitchTokenSource[] = ["env", "config", "none"];
186
-
187
- expect(sources).toContain("env");
188
- expect(sources).toContain("config");
189
- expect(sources).toContain("none");
190
- });
191
- });
192
- });