@openclaw/twitch 2026.2.21 → 2026.5.1-beta.1

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/README.md CHANGED
@@ -5,7 +5,7 @@ Twitch channel plugin for OpenClaw.
5
5
  ## Install (local checkout)
6
6
 
7
7
  ```bash
8
- openclaw plugins install ./extensions/twitch
8
+ openclaw plugins install ./path/to/local/twitch-plugin
9
9
  ```
10
10
 
11
11
  ## Install (npm)
@@ -31,7 +31,7 @@ Minimal config (simplified single-account):
31
31
  accessToken: "oauth:abc123...", // OAuth Access Token (add oauth: prefix)
32
32
  clientId: "xyz789...", // Client ID from Token Generator
33
33
  channel: "vevisk", // Channel to join (required)
34
- allowFrom: ["123456789"], // (recommended) Your Twitch user ID only (Convert your twitch username to ID at https://www.streamweasels.com/tools/convert-twitch-username-%20to-user-id/)
34
+ allowFrom: ["123456789"], // (recommended) Your Twitch user ID only (Convert your twitch username to ID at https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/)
35
35
  },
36
36
  },
37
37
  }
package/api.ts ADDED
@@ -0,0 +1,21 @@
1
+ export {
2
+ type ChannelAccountSnapshot,
3
+ type ChannelCapabilities,
4
+ type ChannelGatewayContext,
5
+ type ChannelLogSink,
6
+ type ChannelMessageActionAdapter,
7
+ type ChannelMessageActionContext,
8
+ type ChannelMeta,
9
+ type ChannelOutboundAdapter,
10
+ type ChannelOutboundContext,
11
+ type ChannelPlugin,
12
+ type ChannelResolveKind,
13
+ type ChannelResolveResult,
14
+ type ChannelStatusAdapter,
15
+ type OpenClawConfig,
16
+ type OutboundDeliveryResult,
17
+ type RuntimeEnv,
18
+ type WizardPrompter,
19
+ } from "./runtime-api.js";
20
+ export { twitchPlugin } from "./src/plugin.js";
21
+ export { setTwitchRuntime } from "./src/runtime.js";
@@ -0,0 +1 @@
1
+ export { twitchPlugin } from "./src/plugin.js";
package/index.test.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { assertBundledChannelEntries } from "openclaw/plugin-sdk/channel-test-helpers";
2
+ import { describe } from "vitest";
3
+ import entry from "./index.js";
4
+ import setupEntry from "./setup-entry.js";
5
+
6
+ describe("twitch bundled entries", () => {
7
+ assertBundledChannelEntries({
8
+ entry,
9
+ expectedId: "twitch",
10
+ expectedName: "Twitch",
11
+ setupEntry,
12
+ });
13
+ });
package/index.ts CHANGED
@@ -1,20 +1,16 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
- import { twitchPlugin } from "./src/plugin.js";
4
- import { setTwitchRuntime } from "./src/runtime.js";
1
+ import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
5
2
 
6
- export { monitorTwitchProvider } from "./src/monitor.js";
7
-
8
- const plugin = {
3
+ export default defineBundledChannelEntry({
9
4
  id: "twitch",
10
5
  name: "Twitch",
11
- description: "Twitch channel plugin",
12
- configSchema: emptyPluginConfigSchema(),
13
- register(api: OpenClawPluginApi) {
14
- setTwitchRuntime(api.runtime);
15
- // oxlint-disable-next-line typescript/no-explicit-any
16
- api.registerChannel({ plugin: twitchPlugin as any });
6
+ description: "Twitch IRC chat channel plugin",
7
+ importMetaUrl: import.meta.url,
8
+ plugin: {
9
+ specifier: "./channel-plugin-api.js",
10
+ exportName: "twitchPlugin",
17
11
  },
18
- };
19
-
20
- export default plugin;
12
+ runtime: {
13
+ specifier: "./api.js",
14
+ exportName: "setTwitchRuntime",
15
+ },
16
+ });
@@ -1,6 +1,12 @@
1
1
  {
2
2
  "id": "twitch",
3
+ "activation": {
4
+ "onStartup": false
5
+ },
3
6
  "channels": ["twitch"],
7
+ "channelEnvVars": {
8
+ "twitch": ["OPENCLAW_TWITCH_ACCESS_TOKEN"]
9
+ },
4
10
  "configSchema": {
5
11
  "type": "object",
6
12
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,20 +1,42 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.2.21",
3
+ "version": "2026.5.1-beta.1",
4
4
  "description": "OpenClaw Twitch channel plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/openclaw/openclaw"
8
+ },
5
9
  "type": "module",
6
10
  "dependencies": {
7
- "@twurple/api": "^8.0.3",
8
- "@twurple/auth": "^8.0.3",
9
- "@twurple/chat": "^8.0.3",
10
- "zod": "^4.3.6"
11
+ "@twurple/api": "^8.1.3",
12
+ "@twurple/auth": "^8.1.3",
13
+ "@twurple/chat": "^8.1.3"
11
14
  },
12
15
  "devDependencies": {
13
- "openclaw": "workspace:*"
16
+ "@openclaw/plugin-sdk": "workspace:*"
14
17
  },
15
18
  "openclaw": {
16
19
  "extensions": [
17
20
  "./index.ts"
18
- ]
21
+ ],
22
+ "setupEntry": "./setup-entry.ts",
23
+ "install": {
24
+ "npmSpec": "@openclaw/twitch",
25
+ "defaultChoice": "npm",
26
+ "minHostVersion": ">=2026.4.10"
27
+ },
28
+ "channel": {
29
+ "id": "twitch",
30
+ "label": "Twitch",
31
+ "selectionLabel": "Twitch (Chat)",
32
+ "docsPath": "/channels/twitch",
33
+ "blurb": "Twitch chat integration",
34
+ "aliases": [
35
+ "twitch-chat"
36
+ ]
37
+ },
38
+ "release": {
39
+ "publishToNpm": true
40
+ }
19
41
  }
20
42
  }
package/runtime-api.ts ADDED
@@ -0,0 +1,22 @@
1
+ // Private runtime barrel for the bundled Twitch extension.
2
+ // Keep this barrel thin and aligned with the local extension surface.
3
+
4
+ export type {
5
+ ChannelAccountSnapshot,
6
+ ChannelCapabilities,
7
+ ChannelGatewayContext,
8
+ ChannelLogSink,
9
+ ChannelMessageActionAdapter,
10
+ ChannelMessageActionContext,
11
+ ChannelMeta,
12
+ ChannelOutboundAdapter,
13
+ ChannelOutboundContext,
14
+ ChannelResolveKind,
15
+ ChannelResolveResult,
16
+ ChannelStatusAdapter,
17
+ } from "openclaw/plugin-sdk/channel-contract";
18
+ export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
19
+ export type { OutboundDeliveryResult } from "openclaw/plugin-sdk/channel-send-result";
20
+ export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
21
+ export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
22
+ export type { WizardPrompter } from "openclaw/plugin-sdk/setup";
package/setup-entry.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+
3
+ export default defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "twitchSetupPlugin",
8
+ },
9
+ });
@@ -0,0 +1,3 @@
1
+ // Keep bundled setup entry imports narrow so setup loads do not pull the
2
+ // broader Twitch channel plugin surface.
3
+ export { twitchSetupPlugin } from "./src/setup-surface.js";
@@ -17,16 +17,79 @@ describe("checkTwitchAccessControl", () => {
17
17
  channel: "testchannel",
18
18
  };
19
19
 
20
- describe("when no restrictions are configured", () => {
21
- it("allows messages that mention the bot (default requireMention)", () => {
22
- const message: TwitchChatMessage = {
20
+ function runAccessCheck(params: {
21
+ account?: Partial<TwitchAccountConfig>;
22
+ message?: Partial<TwitchChatMessage>;
23
+ }) {
24
+ return checkTwitchAccessControl({
25
+ message: {
23
26
  ...mockMessage,
27
+ ...params.message,
28
+ },
29
+ account: {
30
+ ...mockAccount,
31
+ ...params.account,
32
+ },
33
+ botUsername: "testbot",
34
+ });
35
+ }
36
+
37
+ function expectSingleRoleAllowed(params: {
38
+ role: NonNullable<TwitchAccountConfig["allowedRoles"]>[number];
39
+ message: Partial<TwitchChatMessage>;
40
+ }) {
41
+ const result = runAccessCheck({
42
+ account: { allowedRoles: [params.role] },
43
+ message: {
24
44
  message: "@testbot hello",
25
- };
26
- const result = checkTwitchAccessControl({
27
- message,
28
- account: mockAccount,
29
- botUsername: "testbot",
45
+ ...params.message,
46
+ },
47
+ });
48
+ expect(result.allowed).toBe(true);
49
+ return result;
50
+ }
51
+
52
+ function expectAllowedAccessCheck(params: {
53
+ account?: Partial<TwitchAccountConfig>;
54
+ message?: Partial<TwitchChatMessage>;
55
+ }) {
56
+ const result = runAccessCheck({
57
+ account: params.account,
58
+ message: {
59
+ message: "@testbot hello",
60
+ ...params.message,
61
+ },
62
+ });
63
+ expect(result.allowed).toBe(true);
64
+ return result;
65
+ }
66
+
67
+ function expectAllowFromBlocked(params: {
68
+ allowFrom: string[];
69
+ allowedRoles?: NonNullable<TwitchAccountConfig["allowedRoles"]>;
70
+ message?: Partial<TwitchChatMessage>;
71
+ reason: string;
72
+ }) {
73
+ const result = runAccessCheck({
74
+ account: {
75
+ allowFrom: params.allowFrom,
76
+ allowedRoles: params.allowedRoles,
77
+ },
78
+ message: {
79
+ message: "@testbot hello",
80
+ ...params.message,
81
+ },
82
+ });
83
+ expect(result.allowed).toBe(false);
84
+ expect(result.reason).toContain(params.reason);
85
+ }
86
+
87
+ describe("when no restrictions are configured", () => {
88
+ it("allows messages that mention the bot (default requireMention)", () => {
89
+ const result = runAccessCheck({
90
+ message: {
91
+ message: "@testbot hello",
92
+ },
30
93
  });
31
94
  expect(result.allowed).toBe(true);
32
95
  });
@@ -34,30 +97,20 @@ describe("checkTwitchAccessControl", () => {
34
97
 
35
98
  describe("requireMention default", () => {
36
99
  it("defaults to true when undefined", () => {
37
- const message: TwitchChatMessage = {
38
- ...mockMessage,
39
- message: "hello bot",
40
- };
41
-
42
- const result = checkTwitchAccessControl({
43
- message,
44
- account: mockAccount,
45
- botUsername: "testbot",
100
+ const result = runAccessCheck({
101
+ message: {
102
+ message: "hello bot",
103
+ },
46
104
  });
47
105
  expect(result.allowed).toBe(false);
48
106
  expect(result.reason).toContain("does not mention the bot");
49
107
  });
50
108
 
51
109
  it("allows mention when requireMention is undefined", () => {
52
- const message: TwitchChatMessage = {
53
- ...mockMessage,
54
- message: "@testbot hello",
55
- };
56
-
57
- const result = checkTwitchAccessControl({
58
- message,
59
- account: mockAccount,
60
- botUsername: "testbot",
110
+ const result = runAccessCheck({
111
+ message: {
112
+ message: "@testbot hello",
113
+ },
61
114
  });
62
115
  expect(result.allowed).toBe(true);
63
116
  });
@@ -65,52 +118,25 @@ describe("checkTwitchAccessControl", () => {
65
118
 
66
119
  describe("requireMention", () => {
67
120
  it("allows messages that mention the bot", () => {
68
- const account: TwitchAccountConfig = {
69
- ...mockAccount,
70
- requireMention: true,
71
- };
72
- const message: TwitchChatMessage = {
73
- ...mockMessage,
74
- message: "@testbot hello",
75
- };
76
-
77
- const result = checkTwitchAccessControl({
78
- message,
79
- account,
80
- botUsername: "testbot",
121
+ const result = runAccessCheck({
122
+ account: { requireMention: true },
123
+ message: { message: "@testbot hello" },
81
124
  });
82
125
  expect(result.allowed).toBe(true);
83
126
  });
84
127
 
85
128
  it("blocks messages that don't mention the bot", () => {
86
- const account: TwitchAccountConfig = {
87
- ...mockAccount,
88
- requireMention: true,
89
- };
90
-
91
- const result = checkTwitchAccessControl({
92
- message: mockMessage,
93
- account,
94
- botUsername: "testbot",
129
+ const result = runAccessCheck({
130
+ account: { requireMention: true },
95
131
  });
96
132
  expect(result.allowed).toBe(false);
97
133
  expect(result.reason).toContain("does not mention the bot");
98
134
  });
99
135
 
100
136
  it("is case-insensitive for bot username", () => {
101
- const account: TwitchAccountConfig = {
102
- ...mockAccount,
103
- requireMention: true,
104
- };
105
- const message: TwitchChatMessage = {
106
- ...mockMessage,
107
- message: "@TestBot hello",
108
- };
109
-
110
- const result = checkTwitchAccessControl({
111
- message,
112
- account,
113
- botUsername: "testbot",
137
+ const result = runAccessCheck({
138
+ account: { requireMention: true },
139
+ message: { message: "@TestBot hello" },
114
140
  });
115
141
  expect(result.allowed).toBe(true);
116
142
  });
@@ -118,62 +144,35 @@ describe("checkTwitchAccessControl", () => {
118
144
 
119
145
  describe("allowFrom allowlist", () => {
120
146
  it("allows users in the allowlist", () => {
121
- const account: TwitchAccountConfig = {
122
- ...mockAccount,
123
- allowFrom: ["123456", "789012"],
124
- };
125
- const message: TwitchChatMessage = {
126
- ...mockMessage,
127
- message: "@testbot hello",
128
- };
129
-
130
- const result = checkTwitchAccessControl({
131
- message,
132
- account,
133
- botUsername: "testbot",
147
+ const result = expectAllowedAccessCheck({
148
+ account: {
149
+ allowFrom: ["123456", "789012"],
150
+ },
134
151
  });
135
- expect(result.allowed).toBe(true);
136
152
  expect(result.matchKey).toBe("123456");
137
153
  expect(result.matchSource).toBe("allowlist");
138
154
  });
139
155
 
140
156
  it("blocks users not in allowlist when allowFrom is set", () => {
141
- const account: TwitchAccountConfig = {
142
- ...mockAccount,
157
+ expectAllowFromBlocked({
143
158
  allowFrom: ["789012"],
144
- };
145
- const message: TwitchChatMessage = {
146
- ...mockMessage,
147
- message: "@testbot hello",
148
- };
159
+ reason: "allowFrom",
160
+ });
161
+ });
149
162
 
150
- const result = checkTwitchAccessControl({
151
- message,
152
- account,
153
- botUsername: "testbot",
163
+ it("blocks everyone when allowFrom is explicitly empty", () => {
164
+ expectAllowFromBlocked({
165
+ allowFrom: [],
166
+ reason: "allowFrom",
154
167
  });
155
- expect(result.allowed).toBe(false);
156
- expect(result.reason).toContain("allowFrom");
157
168
  });
158
169
 
159
170
  it("blocks messages without userId", () => {
160
- const account: TwitchAccountConfig = {
161
- ...mockAccount,
171
+ expectAllowFromBlocked({
162
172
  allowFrom: ["123456"],
163
- };
164
- const message: TwitchChatMessage = {
165
- ...mockMessage,
166
- message: "@testbot hello",
167
- userId: undefined,
168
- };
169
-
170
- const result = checkTwitchAccessControl({
171
- message,
172
- account,
173
- botUsername: "testbot",
173
+ message: { userId: undefined },
174
+ reason: "user ID not available",
174
175
  });
175
- expect(result.allowed).toBe(false);
176
- expect(result.reason).toContain("user ID not available");
177
176
  });
178
177
 
179
178
  it("bypasses role checks when user is in allowlist", () => {
@@ -197,68 +196,30 @@ describe("checkTwitchAccessControl", () => {
197
196
  });
198
197
 
199
198
  it("blocks user with role when not in allowlist", () => {
200
- const account: TwitchAccountConfig = {
201
- ...mockAccount,
199
+ expectAllowFromBlocked({
202
200
  allowFrom: ["789012"],
203
201
  allowedRoles: ["moderator"],
204
- };
205
- const message: TwitchChatMessage = {
206
- ...mockMessage,
207
- message: "@testbot hello",
208
- userId: "123456",
209
- isMod: true,
210
- };
211
-
212
- const result = checkTwitchAccessControl({
213
- message,
214
- account,
215
- botUsername: "testbot",
202
+ message: { userId: "123456", isMod: true },
203
+ reason: "allowFrom",
216
204
  });
217
- expect(result.allowed).toBe(false);
218
- expect(result.reason).toContain("allowFrom");
219
205
  });
220
206
 
221
207
  it("blocks user not in allowlist even when roles configured", () => {
222
- const account: TwitchAccountConfig = {
223
- ...mockAccount,
208
+ expectAllowFromBlocked({
224
209
  allowFrom: ["789012"],
225
210
  allowedRoles: ["moderator"],
226
- };
227
- const message: TwitchChatMessage = {
228
- ...mockMessage,
229
- message: "@testbot hello",
230
- userId: "123456",
231
- isMod: false,
232
- };
233
-
234
- const result = checkTwitchAccessControl({
235
- message,
236
- account,
237
- botUsername: "testbot",
211
+ message: { userId: "123456", isMod: false },
212
+ reason: "allowFrom",
238
213
  });
239
- expect(result.allowed).toBe(false);
240
- expect(result.reason).toContain("allowFrom");
241
214
  });
242
215
  });
243
216
 
244
217
  describe("allowedRoles", () => {
245
218
  it("allows users with matching role", () => {
246
- const account: TwitchAccountConfig = {
247
- ...mockAccount,
248
- allowedRoles: ["moderator"],
249
- };
250
- const message: TwitchChatMessage = {
251
- ...mockMessage,
252
- message: "@testbot hello",
253
- isMod: true,
254
- };
255
-
256
- const result = checkTwitchAccessControl({
257
- message,
258
- account,
259
- botUsername: "testbot",
219
+ const result = expectSingleRoleAllowed({
220
+ role: "moderator",
221
+ message: { isMod: true },
260
222
  });
261
- expect(result.allowed).toBe(true);
262
223
  expect(result.matchSource).toBe("role");
263
224
  });
264
225
 
@@ -304,98 +265,40 @@ describe("checkTwitchAccessControl", () => {
304
265
  });
305
266
 
306
267
  it("allows all users when role is 'all'", () => {
307
- const account: TwitchAccountConfig = {
308
- ...mockAccount,
309
- allowedRoles: ["all"],
310
- };
311
- const message: TwitchChatMessage = {
312
- ...mockMessage,
313
- message: "@testbot hello",
314
- };
315
-
316
- const result = checkTwitchAccessControl({
317
- message,
318
- account,
319
- botUsername: "testbot",
268
+ const result = expectAllowedAccessCheck({
269
+ account: {
270
+ allowedRoles: ["all"],
271
+ },
320
272
  });
321
- expect(result.allowed).toBe(true);
322
273
  expect(result.matchKey).toBe("all");
323
274
  });
324
275
 
325
276
  it("handles moderator role", () => {
326
- const account: TwitchAccountConfig = {
327
- ...mockAccount,
328
- allowedRoles: ["moderator"],
329
- };
330
- const message: TwitchChatMessage = {
331
- ...mockMessage,
332
- message: "@testbot hello",
333
- isMod: true,
334
- };
335
-
336
- const result = checkTwitchAccessControl({
337
- message,
338
- account,
339
- botUsername: "testbot",
277
+ expectSingleRoleAllowed({
278
+ role: "moderator",
279
+ message: { isMod: true },
340
280
  });
341
- expect(result.allowed).toBe(true);
342
281
  });
343
282
 
344
283
  it("handles subscriber role", () => {
345
- const account: TwitchAccountConfig = {
346
- ...mockAccount,
347
- allowedRoles: ["subscriber"],
348
- };
349
- const message: TwitchChatMessage = {
350
- ...mockMessage,
351
- message: "@testbot hello",
352
- isSub: true,
353
- };
354
-
355
- const result = checkTwitchAccessControl({
356
- message,
357
- account,
358
- botUsername: "testbot",
284
+ expectSingleRoleAllowed({
285
+ role: "subscriber",
286
+ message: { isSub: true },
359
287
  });
360
- expect(result.allowed).toBe(true);
361
288
  });
362
289
 
363
290
  it("handles owner role", () => {
364
- const account: TwitchAccountConfig = {
365
- ...mockAccount,
366
- allowedRoles: ["owner"],
367
- };
368
- const message: TwitchChatMessage = {
369
- ...mockMessage,
370
- message: "@testbot hello",
371
- isOwner: true,
372
- };
373
-
374
- const result = checkTwitchAccessControl({
375
- message,
376
- account,
377
- botUsername: "testbot",
291
+ expectSingleRoleAllowed({
292
+ role: "owner",
293
+ message: { isOwner: true },
378
294
  });
379
- expect(result.allowed).toBe(true);
380
295
  });
381
296
 
382
297
  it("handles vip role", () => {
383
- const account: TwitchAccountConfig = {
384
- ...mockAccount,
385
- allowedRoles: ["vip"],
386
- };
387
- const message: TwitchChatMessage = {
388
- ...mockMessage,
389
- message: "@testbot hello",
390
- isVip: true,
391
- };
392
-
393
- const result = checkTwitchAccessControl({
394
- message,
395
- account,
396
- botUsername: "testbot",
298
+ expectSingleRoleAllowed({
299
+ role: "vip",
300
+ message: { isVip: true },
397
301
  });
398
- expect(result.allowed).toBe(true);
399
302
  });
400
303
  });
401
304
 
@@ -421,21 +324,15 @@ describe("checkTwitchAccessControl", () => {
421
324
  });
422
325
 
423
326
  it("checks allowlist before allowedRoles", () => {
424
- const account: TwitchAccountConfig = {
425
- ...mockAccount,
426
- allowFrom: ["123456"],
427
- allowedRoles: ["owner"],
428
- };
429
- const message: TwitchChatMessage = {
430
- ...mockMessage,
431
- message: "@testbot hello",
432
- isOwner: false,
433
- };
434
-
435
- const result = checkTwitchAccessControl({
436
- message,
437
- account,
438
- botUsername: "testbot",
327
+ const result = runAccessCheck({
328
+ account: {
329
+ allowFrom: ["123456"],
330
+ allowedRoles: ["owner"],
331
+ },
332
+ message: {
333
+ message: "@testbot hello",
334
+ isOwner: false,
335
+ },
439
336
  });
440
337
  expect(result.allowed).toBe(true);
441
338
  expect(result.matchSource).toBe("allowlist");