simplify config object
This commit is contained in:
parent
bb7b1f1a12
commit
80fc5babd1
@ -11,9 +11,12 @@ Twitch chat support via IRC connection. Clawdbot connects as a Twitch user (bot
|
||||
Status: ready for Twitch chat via IRC connection with @twurple.
|
||||
|
||||
## Quick setup (beginner)
|
||||
1) Install the Twitch plugin and create a Twitch application.
|
||||
2) Generate your OAuth token (recommended: use [Twitch Token Generator](https://twitchtokengenerator.com/)).
|
||||
3) Set the token for Clawdbot:
|
||||
1) Install the Twitch plugin.
|
||||
2) Generate your credentials (recommended: use [Twitch Token Generator](https://twitchtokengenerator.com/)):
|
||||
- Select **Bot Token**
|
||||
- Verify scopes `chat:read` and `chat:write` are selected
|
||||
- Copy the **Client ID** and **Access Token** (and optionally **Refresh Token**)
|
||||
3) Set the credentials for Clawdbot:
|
||||
- Env: `CLAWDBOT_TWITCH_ACCESS_TOKEN=...` (default account only)
|
||||
- Or config: `channels.twitch.accounts.default.token`
|
||||
- If both are set, config takes precedence (env fallback is default-account only).
|
||||
@ -26,27 +29,28 @@ Minimal config:
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot", // Bot's Twitch account
|
||||
token: "oauth:abc123...", // Or omit to use CLAWDBOT_TWITCH_ACCESS_TOKEN env var
|
||||
clientId: "your_client_id_here",
|
||||
channel: "vevisk" // Which Twitch channel's chat to join
|
||||
}
|
||||
}
|
||||
username: "clawdbot", // Bot's Twitch account
|
||||
accessToken: "oauth:abc123...", // OAuth Access Token (or use CLAWDBOT_TWITCH_ACCESS_TOKEN env var)
|
||||
clientId: "your_client_id", // Client ID from Token Generator
|
||||
channel: "vevisk" // Which Twitch channel's chat to join
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** [Twitch Token Generator](https://twitchtokengenerator.com/) provides the Client ID and Access Token (select **Bot Token** and verify `chat:read` + `chat:write` scopes) - no manual app registration needed.
|
||||
|
||||
**Note:** `username` is the bot's account, `channel` is which chat to join.
|
||||
|
||||
**Multi-account setup:** Use `channels.twitch.accounts` for advanced multi-account configurations.
|
||||
|
||||
## How it works
|
||||
1. Create a Twitch application and bot account (or use an existing account).
|
||||
2. Configure Clawdbot with `channels.twitch.accounts.default.token` (or `CLAWDBOT_TWITCH_ACCESS_TOKEN` as a fallback).
|
||||
3. Run the gateway; it auto-starts the Twitch channel when a token is available (config first, env fallback) and `channels.twitch.enabled` is not `false`.
|
||||
4. The bot joins the specified `channel` to send/receive messages.
|
||||
5. Direct chats collapse into the agent's main session (default `agent:main:main`); each account maps to an isolated session key `agent:<agentId>:twitch:<accountName>`.
|
||||
1. Create a bot account (or use an existing Twitch account).
|
||||
2. Generate credentials using [Twitch Token Generator](https://twitchtokengenerator.com/) (provides Client ID, Access Token, and Refresh Token).
|
||||
3. Configure Clawdbot with the credentials.
|
||||
4. Run the gateway; it auto-starts the Twitch channel when a token is available (config first, env fallback) and `channels.twitch.enabled` is not `false`.
|
||||
5. The bot joins the specified `channel` to send/receive messages.
|
||||
6. Direct chats collapse into the agent's main session (default `agent:main:main`); each account maps to an isolated session key `agent:<agentId>:twitch:<accountName>`.
|
||||
|
||||
**Key distinction:** `username` is who the bot authenticates as (the bot's account), `channel` is which chat room it joins.
|
||||
|
||||
@ -70,22 +74,16 @@ Details: [Plugins](/plugin)
|
||||
|
||||
## Setup
|
||||
|
||||
### 1) Create a Twitch application
|
||||
- Go to [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
- Click "Register Your Application"
|
||||
- Set Application Type to "Chat Bot"
|
||||
- Copy the **Client ID**
|
||||
### 1) Generate your credentials (recommended: Twitch Token Generator)
|
||||
- Go to [Twitch Token Generator](https://twitchtokengenerator.com/)
|
||||
- Select **Bot Token**
|
||||
- Verify scopes `chat:read` and `chat:write` are selected
|
||||
- Copy the **Access Token** and **Client ID**
|
||||
|
||||
### 2) Generate your OAuth token (recommended: Twitch Token Generator)
|
||||
- Use [Twitch Token Generator](https://twitchtokengenerator.com/)
|
||||
- Select scopes: `chat:read` and `chat:write`
|
||||
- Copy the token (starts with `oauth:`)
|
||||
|
||||
### 3) Configure credentials
|
||||
### 2) Configure credentials
|
||||
Env (default account only):
|
||||
|
||||
```bash
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:your_token_here
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=your_access_token_here
|
||||
```
|
||||
|
||||
Or config:
|
||||
@ -98,8 +96,8 @@ Or config:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot", // Bot's Twitch account
|
||||
token: "oauth:abc123...", // Or omit to use CLAWDBOT_TWITCH_ACCESS_TOKEN env var
|
||||
clientId: "your_client_id_here",
|
||||
accessToken: "oauth:abc123...", // Access Token from Token Generator (or omit to use env var)
|
||||
clientId: "xyz789...", // Client ID from Token Generator
|
||||
channel: "vevisk" // Which Twitch channel's chat to join
|
||||
}
|
||||
}
|
||||
@ -108,22 +106,25 @@ Or config:
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `username` is the bot's account, `channel` is which chat to join.
|
||||
**Note:** Copy the **Access Token** value to the `accessToken` property (add `oauth:` prefix if needed), and the **Client ID** value to the `clientId` property.
|
||||
|
||||
With env, you still need `clientId` and `channel` in config (or use the minimal config above without `token`).
|
||||
With env, you still need `clientId` and `channel` in config (or use the minimal config above without `accessToken`).
|
||||
|
||||
### 4) Start the gateway
|
||||
### 3) Start the gateway
|
||||
Twitch starts when a token is resolved (config first, env fallback).
|
||||
|
||||
### 5) Join a channel
|
||||
### 4) Join a channel
|
||||
The bot joins the channel specified in `channel`.
|
||||
|
||||
## Token refresh (optional, recommended for long-running bots)
|
||||
## Token refresh (optional, for long-running bots)
|
||||
|
||||
For long-running bots, configure automatic token refresh to avoid expired tokens:
|
||||
**Important:** Tokens from [Twitch Token Generator](https://twitchtokengenerator.com/) cannot be automatically refreshed - you'll need to generate a new token when it expires (typically after several hours).
|
||||
|
||||
1. Use [Twitch Token Generator](https://twitchtokengenerator.com/) with **"Include Refresh Token"** checked
|
||||
2. Get your **Client Secret** from [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
For automatic token refresh, you must create your own Twitch application:
|
||||
|
||||
1. Create a Twitch application at [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
- Copy the **Client ID** and **Client Secret**
|
||||
2. Generate a refresh token using your own app (you'll need to implement the OAuth flow or use a tool that lets you specify your Client ID)
|
||||
3. Add to config:
|
||||
|
||||
```json5
|
||||
@ -133,10 +134,10 @@ For long-running bots, configure automatic token refresh to avoid expired tokens
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot",
|
||||
token: "oauth:abc123...",
|
||||
clientId: "your_client_id",
|
||||
clientSecret: "your_client_secret",
|
||||
refreshToken: "your_refresh_token",
|
||||
accessToken: "oauth:abc123...", // Access Token from your app
|
||||
clientId: "xyz789...", // Client ID from your app
|
||||
clientSecret: "secret123...", // Client Secret from your app
|
||||
refreshToken: "refresh456...", // Refresh Token from your app
|
||||
expiresIn: 14400,
|
||||
obtainmentTimestamp: 1706092800000
|
||||
}
|
||||
@ -146,6 +147,8 @@ For long-running bots, configure automatic token refresh to avoid expired tokens
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** All three values (`accessToken`, `clientId`, `refreshToken`) must come from the same Twitch application you created.
|
||||
|
||||
The bot automatically refreshes tokens before they expire and logs refresh events.
|
||||
|
||||
## Routing model
|
||||
@ -165,14 +168,14 @@ Example (one bot account in two different channels):
|
||||
accounts: {
|
||||
ninjaChannel: {
|
||||
username: "clawdbot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "vevisk"
|
||||
},
|
||||
shroudChannel: {
|
||||
username: "clawdbot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:def456...",
|
||||
clientId: "uvw012...",
|
||||
channel: "secondchannel"
|
||||
}
|
||||
}
|
||||
@ -181,6 +184,66 @@ Example (one bot account in two different channels):
|
||||
}
|
||||
```
|
||||
|
||||
## Migration notes
|
||||
|
||||
### Breaking changes (2026.1.23+)
|
||||
|
||||
**`token` renamed to `accessToken`:** If you have existing Twitch config using `token`, update to `accessToken`:
|
||||
|
||||
**Before:**
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot",
|
||||
token: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "vevisk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After:**
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "vevisk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Simplified config (recommended):** For single-account setups, you can now use base-level properties:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
username: "clawdbot",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "vevisk"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The env var `CLAWDBOT_TWITCH_ACCESS_TOKEN` continues to work for the default account.
|
||||
|
||||
## Access control
|
||||
|
||||
### Role-based restrictions (recommended)
|
||||
@ -194,8 +257,8 @@ Restrict access to specific roles:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel",
|
||||
allowedRoles: ["moderator", "vip"]
|
||||
}
|
||||
@ -223,8 +286,8 @@ Only allow specific Twitch user IDs (most secure):
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel",
|
||||
allowFrom: ["123456789", "987654321"]
|
||||
}
|
||||
@ -252,8 +315,8 @@ Users in `allowFrom` bypass role checks. In this example:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel",
|
||||
allowFrom: ["123456789"],
|
||||
allowedRoles: ["moderator"]
|
||||
@ -275,8 +338,8 @@ Only respond when the bot is mentioned:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel",
|
||||
requireMention: true
|
||||
}
|
||||
@ -290,14 +353,14 @@ Only respond when the bot is mentioned:
|
||||
|
||||
For the default account, you can use environment variables instead of config:
|
||||
|
||||
- `CLAWDBOT_TWITCH_ACCESS_TOKEN` - OAuth token (with `oauth:` prefix)
|
||||
- `CLAWDBOT_TWITCH_ACCESS_TOKEN` - Access Token (without `oauth:` prefix)
|
||||
|
||||
Env fallback only works for the default account. For multi-account setups, use config.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=oauth:abc123def456...
|
||||
export CLAWDBOT_TWITCH_ACCESS_TOKEN=abc123def456...
|
||||
```
|
||||
|
||||
Config with env fallback:
|
||||
@ -310,7 +373,7 @@ Config with env fallback:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
clientId: "your_client_id",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel"
|
||||
// token will be read from CLAWDBOT_TWITCH_ACCESS_TOKEN
|
||||
}
|
||||
@ -380,8 +443,8 @@ clawdbot channels status --probe
|
||||
accounts: {
|
||||
default: {
|
||||
username: "mybot",
|
||||
token: "oauth:...",
|
||||
clientId: "...",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "your_channel",
|
||||
// Temporary: allow everyone
|
||||
allowedRoles: ["all"]
|
||||
@ -397,7 +460,7 @@ clawdbot channels status --probe
|
||||
### Token issues
|
||||
|
||||
**"Failed to connect" or authentication errors:**
|
||||
- Verify token starts with `oauth:`
|
||||
- Verify `accessToken` is the OAuth access token value (typically starts with `oauth:` prefix)
|
||||
- Check token has `chat:read` and `chat:write` scopes
|
||||
- If using RefreshingAuthProvider, verify `clientSecret` and `refreshToken` are set
|
||||
|
||||
@ -422,14 +485,14 @@ Full configuration: [Configuration](/gateway/configuration)
|
||||
```typescript
|
||||
{
|
||||
username: string, // Bot username
|
||||
token: string, // OAuth token with chat:read and chat:write
|
||||
clientId: string, // Twitch Client ID
|
||||
accessToken: string, // OAuth access token with chat:read and chat:write
|
||||
clientId: string, // Twitch Client ID (from Token Generator site or your app)
|
||||
channel: string, // Channel to join
|
||||
enabled?: boolean, // Enable this account (default: true)
|
||||
clientSecret?: string, // For RefreshingAuthProvider
|
||||
refreshToken?: string, // For RefreshingAuthProvider
|
||||
expiresIn?: number, // Token expiry in seconds
|
||||
obtainmentTimestamp?: number, // Token obtained timestamp
|
||||
clientSecret?: string, // Optional: For automatic token refresh (from YOUR Twitch app)
|
||||
refreshToken?: string, // Optional: For automatic token refresh (from YOUR Twitch app)
|
||||
expiresIn?: number, // Token expiry in seconds (for refresh)
|
||||
obtainmentTimestamp?: number, // Token obtained timestamp (for refresh)
|
||||
allowFrom?: string[], // User ID allowlist
|
||||
allowedRoles?: TwitchRole[], // Role-based access control
|
||||
requireMention?: boolean // Require @mention (default: false)
|
||||
@ -448,13 +511,17 @@ Full configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
Provider options:
|
||||
- `channels.twitch.enabled`: enable/disable channel startup.
|
||||
- `channels.twitch.accounts.<accountName>.username`: bot username.
|
||||
- `channels.twitch.accounts.<accountName>.token`: OAuth token.
|
||||
- `channels.twitch.accounts.<accountName>.clientId`: Twitch Client ID.
|
||||
- `channels.twitch.username`: bot username (simplified single-account config).
|
||||
- `channels.twitch.accessToken`: OAuth access token (simplified single-account config).
|
||||
- `channels.twitch.clientId`: Twitch Client ID (simplified single-account config).
|
||||
- `channels.twitch.channel`: channel to join (simplified single-account config).
|
||||
- `channels.twitch.accounts.<accountName>.username`: bot username (multi-account config).
|
||||
- `channels.twitch.accounts.<accountName>.accessToken`: OAuth access token (multi-account config).
|
||||
- `channels.twitch.accounts.<accountName>.clientId`: Twitch Client ID (from Token Generator or your app).
|
||||
- `channels.twitch.accounts.<accountName>.channel`: channel to join.
|
||||
- `channels.twitch.accounts.<accountName>.enabled`: enable/disable account (default: true).
|
||||
- `channels.twitch.accounts.<accountName>.clientSecret`: for RefreshingAuthProvider.
|
||||
- `channels.twitch.accounts.<accountName>.refreshToken`: for RefreshingAuthProvider.
|
||||
- `channels.twitch.accounts.<accountName>.clientSecret`: optional, for automatic token refresh (must be from YOUR Twitch app).
|
||||
- `channels.twitch.accounts.<accountName>.refreshToken`: optional, for automatic token refresh (must be from YOUR Twitch app).
|
||||
- `channels.twitch.accounts.<accountName>.expiresIn`: token expiry in seconds.
|
||||
- `channels.twitch.accounts.<accountName>.obtainmentTimestamp`: token obtained timestamp.
|
||||
- `channels.twitch.accounts.<accountName>.allowFrom`: user ID allowlist.
|
||||
|
||||
@ -18,7 +18,23 @@ Onboarding: select Twitch and confirm the install prompt to fetch the plugin aut
|
||||
|
||||
## Config
|
||||
|
||||
Minimal config:
|
||||
Minimal config (simplified single-account):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
twitch: {
|
||||
enabled: true,
|
||||
username: "clawdbot",
|
||||
accessToken: "oauth:abc123...", // OAuth Access Token (add oauth: prefix)
|
||||
clientId: "xyz789...", // Client ID from Token Generator
|
||||
channel: "vevisk", // Channel to join
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Multi-account config (advanced):
|
||||
|
||||
```json5
|
||||
{
|
||||
@ -28,10 +44,16 @@ Minimal config:
|
||||
accounts: {
|
||||
default: {
|
||||
username: "clawdbot",
|
||||
token: "oauth:abc123...",
|
||||
clientId: "your_client_id_here",
|
||||
accessToken: "oauth:abc123...",
|
||||
clientId: "xyz789...",
|
||||
channel: "vevisk",
|
||||
},
|
||||
channel2: {
|
||||
username: "clawdbot",
|
||||
accessToken: "oauth:def456...",
|
||||
clientId: "uvw012...",
|
||||
channel: "secondchannel",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -40,10 +62,12 @@ Minimal config:
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create a Twitch application: [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
2. Generate OAuth token: [Twitch Token Generator](https://twitchtokengenerator.com/)
|
||||
- Select scopes: `chat:read` and `chat:write`
|
||||
3. Start the gateway
|
||||
1. Generate credentials: [Twitch Token Generator](https://twitchtokengenerator.com/)
|
||||
- Select **Bot Token**
|
||||
- Verify scopes `chat:read` and `chat:write` are selected
|
||||
- Copy the **Access Token** to `token` property
|
||||
- Copy the **Client ID** to `clientId` property
|
||||
2. Start the gateway
|
||||
|
||||
## Full documentation
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ const TwitchRoleSchema = z.enum(["moderator", "owner", "vip", "subscriber", "all
|
||||
const TwitchAccountSchema = z.object({
|
||||
/** Twitch username */
|
||||
username: z.string(),
|
||||
/** Twitch OAuth token (requires chat:read and chat:write scopes) */
|
||||
token: z.string(),
|
||||
/** Twitch OAuth access token (requires chat:read and chat:write scopes) */
|
||||
accessToken: z.string(),
|
||||
/** Twitch client ID (from Twitch Developer Portal or twitchtokengenerator.com) */
|
||||
clientId: z.string().optional(),
|
||||
/** Channel name to join (defaults to username) */
|
||||
@ -37,12 +37,48 @@ const TwitchAccountSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* Twitch plugin configuration schema
|
||||
* Simplified single-account configuration schema
|
||||
*
|
||||
* Use this for single-account setups. Properties are at the top level,
|
||||
* creating an implicit "default" account.
|
||||
*/
|
||||
export const TwitchConfigSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
/** Per-account configuration */
|
||||
accounts: z.record(z.string(), TwitchAccountSchema).optional(),
|
||||
});
|
||||
const simplifiedSchema = z.intersection(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
}),
|
||||
TwitchAccountSchema,
|
||||
);
|
||||
|
||||
/**
|
||||
* Multi-account configuration schema
|
||||
*
|
||||
* Use this for multi-account setups. Each key is an account ID (e.g., "default", "secondary").
|
||||
*/
|
||||
const multiAccountSchema = z.intersection(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
markdown: MarkdownConfigSchema.optional(),
|
||||
}),
|
||||
z
|
||||
.object({
|
||||
/** Per-account configuration (for multi-account setups) */
|
||||
accounts: z.record(z.string(), TwitchAccountSchema),
|
||||
})
|
||||
.refine((val) => Object.keys(val.accounts || {}).length > 0, {
|
||||
message: "accounts must contain at least one entry",
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Twitch plugin configuration schema
|
||||
*
|
||||
* Supports two mutually exclusive patterns:
|
||||
* 1. Simplified single-account: username, accessToken, clientId, channel at top level
|
||||
* 2. Multi-account: accounts object with named account configs
|
||||
*
|
||||
* The union ensures clear discrimination between the two modes.
|
||||
*/
|
||||
export const TwitchConfigSchema = z.union([simplifiedSchema, multiAccountSchema]);
|
||||
|
||||
@ -30,28 +30,55 @@ describe("parsePluginConfig", () => {
|
||||
});
|
||||
|
||||
describe("getAccountConfig", () => {
|
||||
const mockCoreConfig = {
|
||||
const mockMultiAccountConfig = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "oauth:test123",
|
||||
accessToken: "oauth:test123",
|
||||
},
|
||||
secondary: {
|
||||
username: "secondbot",
|
||||
accessToken: "oauth:secondary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("returns account config for valid account ID", () => {
|
||||
const result = getAccountConfig(mockCoreConfig, "default");
|
||||
const mockSimplifiedConfig = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("returns account config for valid account ID (multi-account)", () => {
|
||||
const result = getAccountConfig(mockMultiAccountConfig, "default");
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.username).toBe("testbot");
|
||||
});
|
||||
|
||||
it("returns account config for default account (simplified config)", () => {
|
||||
const result = getAccountConfig(mockSimplifiedConfig, "default");
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.username).toBe("testbot");
|
||||
});
|
||||
|
||||
it("returns non-default account from multi-account config", () => {
|
||||
const result = getAccountConfig(mockMultiAccountConfig, "secondary");
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.username).toBe("secondbot");
|
||||
});
|
||||
|
||||
it("returns null for non-existent account ID", () => {
|
||||
const result = getAccountConfig(mockCoreConfig, "nonexistent");
|
||||
const result = getAccountConfig(mockMultiAccountConfig, "nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
@ -8,6 +8,13 @@ export const DEFAULT_ACCOUNT_ID = "default";
|
||||
|
||||
/**
|
||||
* Get account config from core config
|
||||
*
|
||||
* Handles two patterns:
|
||||
* 1. Simplified single-account: base-level properties create implicit "default" account
|
||||
* 2. Multi-account: explicit accounts object
|
||||
*
|
||||
* For "default" account, base-level properties take precedence over accounts.default
|
||||
* For other accounts, only the accounts object is checked
|
||||
*/
|
||||
export function getAccountConfig(
|
||||
coreConfig: unknown,
|
||||
@ -22,6 +29,50 @@ export function getAccountConfig(
|
||||
const twitch = channels?.twitch as Record<string, unknown> | undefined;
|
||||
const accounts = twitch?.accounts as Record<string, unknown> | undefined;
|
||||
|
||||
// For default account, check base-level config first
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
const accountFromAccounts = accounts?.[DEFAULT_ACCOUNT_ID] as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
// Base-level properties that can form an implicit default account
|
||||
const baseLevel = {
|
||||
username: typeof twitch?.username === "string" ? twitch.username : undefined,
|
||||
accessToken: typeof twitch?.accessToken === "string" ? twitch.accessToken : undefined,
|
||||
clientId: typeof twitch?.clientId === "string" ? twitch.clientId : undefined,
|
||||
channel: typeof twitch?.channel === "string" ? twitch.channel : undefined,
|
||||
enabled: typeof twitch?.enabled === "boolean" ? twitch.enabled : undefined,
|
||||
allowFrom: Array.isArray(twitch?.allowFrom) ? twitch.allowFrom : undefined,
|
||||
allowedRoles: Array.isArray(twitch?.allowedRoles) ? twitch.allowedRoles : undefined,
|
||||
requireMention:
|
||||
typeof twitch?.requireMention === "boolean" ? twitch.requireMention : undefined,
|
||||
clientSecret: typeof twitch?.clientSecret === "string" ? twitch.clientSecret : undefined,
|
||||
refreshToken: typeof twitch?.refreshToken === "string" ? twitch.refreshToken : undefined,
|
||||
expiresIn: typeof twitch?.expiresIn === "number" ? twitch.expiresIn : undefined,
|
||||
obtainmentTimestamp:
|
||||
typeof twitch?.obtainmentTimestamp === "number" ? twitch.obtainmentTimestamp : undefined,
|
||||
};
|
||||
|
||||
// Merge: base-level takes precedence over accounts.default
|
||||
const merged = {
|
||||
...accountFromAccounts,
|
||||
...baseLevel,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
// Only return if we have at least username
|
||||
if (merged.username) {
|
||||
return merged as unknown as TwitchAccountConfig;
|
||||
}
|
||||
|
||||
// Fall through to accounts.default if no base-level username
|
||||
if (accountFromAccounts) {
|
||||
return accountFromAccounts as unknown as TwitchAccountConfig;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// For non-default accounts, only check accounts object
|
||||
if (!accounts || !accounts[accountId]) {
|
||||
return null;
|
||||
}
|
||||
@ -45,15 +96,31 @@ export function parsePluginConfig(value: unknown): TwitchPluginConfig {
|
||||
|
||||
/**
|
||||
* List all configured account IDs
|
||||
*
|
||||
* Includes both explicit accounts and implicit "default" from base-level config
|
||||
*/
|
||||
export function listAccountIds(cfg: ClawdbotConfig): string[] {
|
||||
const accounts = (cfg as Record<string, unknown>).channels as Record<string, unknown> | undefined;
|
||||
const twitch = accounts?.twitch as Record<string, unknown> | undefined;
|
||||
const accountMap = twitch?.accounts as Record<string, unknown> | undefined;
|
||||
|
||||
if (!accountMap) {
|
||||
return [];
|
||||
const ids: string[] = [];
|
||||
|
||||
// Add explicit accounts
|
||||
if (accountMap) {
|
||||
ids.push(...Object.keys(accountMap));
|
||||
}
|
||||
|
||||
return Object.keys(accountMap);
|
||||
// Add implicit "default" if base-level config exists and "default" not already present
|
||||
const hasBaseLevelConfig =
|
||||
twitch &&
|
||||
(typeof twitch.username === "string" ||
|
||||
typeof twitch.accessToken === "string" ||
|
||||
typeof twitch.channel === "string");
|
||||
|
||||
if (hasBaseLevelConfig && !ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
ids.push(DEFAULT_ACCOUNT_ID);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
@ -25,7 +25,7 @@ const mockPrompter: WizardPrompter = {
|
||||
|
||||
const mockAccount: TwitchAccountConfig = {
|
||||
username: "testbot",
|
||||
token: "oauth:test123",
|
||||
accessToken: "oauth:test123",
|
||||
clientId: "test-client-id",
|
||||
channel: "#testchannel",
|
||||
};
|
||||
@ -49,7 +49,7 @@ describe("onboarding helpers", () => {
|
||||
|
||||
expect(result).toBe("oauth:test123");
|
||||
expect(mockPromptConfirm).toHaveBeenCalledWith({
|
||||
message: "Token already configured. Keep it?",
|
||||
message: "Access token already configured. Keep it?",
|
||||
initialValue: true,
|
||||
});
|
||||
expect(mockPromptText).not.toHaveBeenCalled();
|
||||
|
||||
@ -26,7 +26,7 @@ function setTwitchAccount(
|
||||
const existing = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
const merged: TwitchAccountConfig = {
|
||||
username: account.username ?? existing?.username ?? "",
|
||||
token: account.token ?? existing?.token ?? "",
|
||||
accessToken: account.accessToken ?? existing?.accessToken ?? "",
|
||||
clientId: account.clientId ?? existing?.clientId ?? "",
|
||||
channel: account.channel ?? existing?.channel ?? "",
|
||||
enabled: account.enabled ?? existing?.enabled ?? true,
|
||||
@ -85,12 +85,12 @@ async function promptToken(
|
||||
account: TwitchAccountConfig | null,
|
||||
envToken: string | undefined,
|
||||
): Promise<string> {
|
||||
const existingToken = account?.token ?? "";
|
||||
const existingToken = account?.accessToken ?? "";
|
||||
|
||||
// If we have an existing token and no env var, ask if we should keep it
|
||||
if (existingToken && !envToken) {
|
||||
const keepToken = await prompter.confirm({
|
||||
message: "Token already configured. Keep it?",
|
||||
message: "Access token already configured. Keep it?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (keepToken) {
|
||||
@ -225,7 +225,7 @@ async function configureWithEnvToken(
|
||||
const cfgWithAccount = setTwitchAccount(cfg, {
|
||||
username,
|
||||
clientId,
|
||||
token: "", // Will use env var
|
||||
accessToken: "", // Will use env var
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
@ -275,7 +275,7 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
return setTwitchAccessControl(cfg as ClawdbotConfig, allowedRoles, false);
|
||||
},
|
||||
promptAllowFrom: async ({ cfg, prompter }) => {
|
||||
const account = getTwitchAccount(cfg as ClawdbotConfig);
|
||||
const account = getAccountConfig(cfg, DEFAULT_ACCOUNT_ID);
|
||||
const existingAllowFrom = account?.allowFrom ?? [];
|
||||
|
||||
const entry = await prompter.text({
|
||||
@ -319,7 +319,7 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
const envToken = process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN?.trim();
|
||||
|
||||
// Check if env var is set and config is empty
|
||||
if (envToken && !account?.token) {
|
||||
if (envToken && !account?.accessToken) {
|
||||
const envResult = await configureWithEnvToken(
|
||||
cfg,
|
||||
prompter,
|
||||
@ -342,7 +342,7 @@ export const twitchOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
|
||||
const cfgWithAccount = setTwitchAccount(cfg, {
|
||||
username,
|
||||
token,
|
||||
accessToken: token,
|
||||
clientId,
|
||||
channel: channelName,
|
||||
clientSecret,
|
||||
|
||||
@ -86,7 +86,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
// Return a default/empty account if not configured
|
||||
return {
|
||||
username: "",
|
||||
token: "",
|
||||
accessToken: "",
|
||||
clientId: "",
|
||||
enabled: false,
|
||||
} as TwitchAccountConfig;
|
||||
@ -112,7 +112,7 @@ export const twitchPlugin: ChannelPlugin<TwitchAccountConfig> = {
|
||||
return {
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
enabled: account?.enabled !== false,
|
||||
configured: account ? isAccountConfigured(account, account?.token) : false,
|
||||
configured: account ? isAccountConfigured(account, account?.accessToken) : false,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@ -50,7 +50,7 @@ describe("status", () => {
|
||||
expect(disabledIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect missing clientId when account configured", () => {
|
||||
it("should detect missing clientId when account configured (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
@ -63,13 +63,9 @@ describe("status", () => {
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "oauth:test123",
|
||||
// clientId missing
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123",
|
||||
// clientId missing
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -80,7 +76,7 @@ describe("status", () => {
|
||||
expect(clientIdIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should warn about oauth: prefix in token", () => {
|
||||
it("should warn about oauth: prefix in token (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
@ -93,13 +89,9 @@ describe("status", () => {
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "oauth:test123", // has prefix
|
||||
clientId: "test-id",
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123", // has prefix
|
||||
clientId: "test-id",
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -111,7 +103,7 @@ describe("status", () => {
|
||||
expect(prefixIssue?.kind).toBe("config");
|
||||
});
|
||||
|
||||
it("should detect clientSecret without refreshToken", () => {
|
||||
it("should detect clientSecret without refreshToken (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
@ -124,15 +116,11 @@ describe("status", () => {
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "oauth:test123",
|
||||
clientId: "test-id",
|
||||
clientSecret: "secret123",
|
||||
// refreshToken missing
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "oauth:test123",
|
||||
clientId: "test-id",
|
||||
clientSecret: "secret123",
|
||||
// refreshToken missing
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -143,7 +131,7 @@ describe("status", () => {
|
||||
expect(secretIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect empty allowFrom array", () => {
|
||||
it("should detect empty allowFrom array (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
@ -156,14 +144,10 @@ describe("status", () => {
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "test123",
|
||||
clientId: "test-id",
|
||||
allowFrom: [], // empty array
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "test123",
|
||||
clientId: "test-id",
|
||||
allowFrom: [], // empty array
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -174,7 +158,7 @@ describe("status", () => {
|
||||
expect(allowFromIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect allowedRoles 'all' with allowFrom conflict", () => {
|
||||
it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => {
|
||||
const snapshots: ChannelAccountSnapshot[] = [
|
||||
{
|
||||
accountId: "default",
|
||||
@ -187,15 +171,11 @@ describe("status", () => {
|
||||
const mockCfg = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "test123",
|
||||
clientId: "test-id",
|
||||
allowedRoles: ["all"],
|
||||
allowFrom: ["123456"], // conflict!
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "test123",
|
||||
clientId: "test-id",
|
||||
allowedRoles: ["all"],
|
||||
allowFrom: ["123456"], // conflict!
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -56,7 +56,7 @@ export function collectTwitchStatusIssues(
|
||||
accountId,
|
||||
kind: "config",
|
||||
message: "Twitch account is not properly configured",
|
||||
fix: "Add required fields: username, token, and clientId to your account configuration",
|
||||
fix: "Add required fields: username, accessToken, and clientId to your account configuration",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -72,7 +72,7 @@ export function collectTwitchStatusIssues(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (account && account.username && account.token && !account.clientId) {
|
||||
if (account && account.username && account.accessToken && !account.clientId) {
|
||||
issues.push({
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
@ -86,7 +86,7 @@ export function collectTwitchStatusIssues(
|
||||
? resolveTwitchToken(cfg as Parameters<typeof resolveTwitchToken>[0], { accountId })
|
||||
: { token: "", source: "none" };
|
||||
if (account && isAccountConfigured(account, tokenResolution.token)) {
|
||||
if (account.token?.startsWith("oauth:")) {
|
||||
if (account.accessToken?.startsWith("oauth:")) {
|
||||
issues.push({
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
|
||||
@ -13,23 +13,34 @@ import { resolveTwitchToken, type TwitchTokenSource } from "./token.js";
|
||||
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
|
||||
|
||||
describe("token", () => {
|
||||
const mockConfig = {
|
||||
// Multi-account config for testing non-default accounts
|
||||
const mockMultiAccountConfig = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "oauth:config-token",
|
||||
accessToken: "oauth:config-token",
|
||||
},
|
||||
other: {
|
||||
username: "otherbot",
|
||||
token: "oauth:other-token",
|
||||
accessToken: "oauth:other-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as ClawdbotConfig;
|
||||
|
||||
// Simplified single-account config
|
||||
const mockSimplifiedConfig = {
|
||||
channels: {
|
||||
twitch: {
|
||||
username: "testbot",
|
||||
accessToken: "oauth:config-token",
|
||||
},
|
||||
},
|
||||
} as unknown as ClawdbotConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
@ -40,42 +51,38 @@ describe("token", () => {
|
||||
});
|
||||
|
||||
describe("resolveTwitchToken", () => {
|
||||
it("should resolve token from config for default account", () => {
|
||||
const result = resolveTwitchToken(mockConfig, { accountId: "default" });
|
||||
it("should resolve token from simplified config for default account", () => {
|
||||
const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" });
|
||||
|
||||
expect(result.token).toBe("oauth:config-token");
|
||||
expect(result.source).toBe("config");
|
||||
});
|
||||
|
||||
it("should resolve token from config for non-default account", () => {
|
||||
const result = resolveTwitchToken(mockConfig, { accountId: "other" });
|
||||
it("should resolve token from config for non-default account (multi-account)", () => {
|
||||
const result = resolveTwitchToken(mockMultiAccountConfig, { accountId: "other" });
|
||||
|
||||
expect(result.token).toBe("oauth:other-token");
|
||||
expect(result.source).toBe("config");
|
||||
});
|
||||
|
||||
it("should prioritize config token over env var", () => {
|
||||
it("should prioritize config token over env var (simplified config)", () => {
|
||||
process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token";
|
||||
|
||||
const result = resolveTwitchToken(mockConfig, { accountId: "default" });
|
||||
const result = resolveTwitchToken(mockSimplifiedConfig, { accountId: "default" });
|
||||
|
||||
// Config token should be used even if env var exists
|
||||
expect(result.token).toBe("oauth:config-token");
|
||||
expect(result.source).toBe("config");
|
||||
});
|
||||
|
||||
it("should use env var when config token is empty", () => {
|
||||
it("should use env var when config token is empty (simplified config)", () => {
|
||||
process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token";
|
||||
|
||||
const configWithEmptyToken = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "",
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "",
|
||||
},
|
||||
},
|
||||
} as unknown as ClawdbotConfig;
|
||||
@ -86,16 +93,12 @@ describe("token", () => {
|
||||
expect(result.source).toBe("env");
|
||||
});
|
||||
|
||||
it("should return empty token when neither config nor env has token", () => {
|
||||
it("should return empty token when neither config nor env has token (simplified config)", () => {
|
||||
const configWithoutToken = {
|
||||
channels: {
|
||||
twitch: {
|
||||
accounts: {
|
||||
default: {
|
||||
username: "testbot",
|
||||
token: "",
|
||||
},
|
||||
},
|
||||
username: "testbot",
|
||||
accessToken: "",
|
||||
},
|
||||
},
|
||||
} as unknown as ClawdbotConfig;
|
||||
@ -106,7 +109,7 @@ describe("token", () => {
|
||||
expect(result.source).toBe("none");
|
||||
});
|
||||
|
||||
it("should not use env var for non-default accounts", () => {
|
||||
it("should not use env var for non-default accounts (multi-account)", () => {
|
||||
process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN = "oauth:env-token";
|
||||
|
||||
const configWithoutToken = {
|
||||
@ -115,7 +118,7 @@ describe("token", () => {
|
||||
accounts: {
|
||||
secondary: {
|
||||
username: "secondary",
|
||||
token: "",
|
||||
accessToken: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
/**
|
||||
* Twitch token resolution with environment variable support.
|
||||
* Twitch access token resolution with environment variable support.
|
||||
*
|
||||
* Supports reading Twitch OAuth tokens from config or environment variable.
|
||||
* Supports reading Twitch OAuth access tokens from config or environment variable.
|
||||
* The CLAWDBOT_TWITCH_ACCESS_TOKEN env var is only used for the default account.
|
||||
*
|
||||
* Token resolution priority:
|
||||
* 1. Account access token from merged config (accounts.{id} or base-level for default)
|
||||
* 2. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only)
|
||||
*/
|
||||
|
||||
import type { ClawdbotConfig } from "../../../src/config/config.js";
|
||||
@ -27,12 +31,14 @@ function normalizeTwitchToken(raw?: string | null): string | undefined {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Twitch token from config or environment variable.
|
||||
* Resolve Twitch access token from config or environment variable.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Account token: channels.twitch.accounts.{accountId}.token
|
||||
* 2. Base config token: channels.twitch.token (default account only)
|
||||
* 3. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only)
|
||||
* 1. Account access token (from merged config - base-level for default, or accounts.{accountId})
|
||||
* 2. Environment variable: CLAWDBOT_TWITCH_ACCESS_TOKEN (default account only)
|
||||
*
|
||||
* The getAccountConfig function handles merging base-level config with accounts.default,
|
||||
* so this logic works for both simplified and multi-account patterns.
|
||||
*
|
||||
* @param cfg - Clawdbot config
|
||||
* @param opts - Options including accountId and optional envToken override
|
||||
@ -43,26 +49,33 @@ export function resolveTwitchToken(
|
||||
opts: { accountId?: string | null; envToken?: string | null } = {},
|
||||
): TwitchTokenResolution {
|
||||
const accountId = normalizeAccountId(opts.accountId);
|
||||
|
||||
// Get merged account config (handles both simplified and multi-account patterns)
|
||||
const twitchCfg = cfg?.channels?.twitch;
|
||||
const accountCfg =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? twitchCfg?.accounts?.[accountId]
|
||||
: twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID];
|
||||
accountId === DEFAULT_ACCOUNT_ID
|
||||
? (twitchCfg?.accounts?.[DEFAULT_ACCOUNT_ID] as Record<string, unknown> | undefined)
|
||||
: (twitchCfg?.accounts?.[accountId as string] as Record<string, unknown> | undefined);
|
||||
|
||||
// 1. Account token (highest priority)
|
||||
const accountToken = normalizeTwitchToken(accountCfg?.token ?? undefined);
|
||||
if (accountToken) {
|
||||
return { token: accountToken, source: "config" };
|
||||
// For default account, also check base-level config
|
||||
let token: string | undefined;
|
||||
if (accountId === DEFAULT_ACCOUNT_ID) {
|
||||
// Base-level config takes precedence
|
||||
token = normalizeTwitchToken(
|
||||
(typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : undefined) ||
|
||||
(accountCfg?.accessToken as string | undefined),
|
||||
);
|
||||
} else {
|
||||
// Non-default accounts only use accounts object
|
||||
token = normalizeTwitchToken(accountCfg?.accessToken as string | undefined);
|
||||
}
|
||||
|
||||
// 2. Base config token (default account only)
|
||||
if (token) {
|
||||
return { token, source: "config" };
|
||||
}
|
||||
|
||||
// Environment variable (default account only)
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const configToken = allowEnv ? normalizeTwitchToken(twitchCfg?.token ?? undefined) : undefined;
|
||||
if (configToken) {
|
||||
return { token: configToken, source: "config" };
|
||||
}
|
||||
|
||||
// 3. Environment variable (default account only)
|
||||
const envToken = allowEnv
|
||||
? normalizeTwitchToken(opts.envToken ?? process.env.CLAWDBOT_TWITCH_ACCESS_TOKEN)
|
||||
: undefined;
|
||||
|
||||
@ -49,8 +49,8 @@ export interface TwitchPluginConfig {
|
||||
export interface TwitchAccountConfig {
|
||||
/** Twitch username */
|
||||
username: string;
|
||||
/** Twitch OAuth token (requires chat:read and chat:write scopes) */
|
||||
token: string;
|
||||
/** Twitch OAuth access token (requires chat:read and chat:write scopes) */
|
||||
accessToken: string;
|
||||
/** Twitch client ID (from Twitch Developer Portal or twitchtokengenerator.com) */
|
||||
clientId: string;
|
||||
/** Channel name to join (required) */
|
||||
|
||||
@ -68,11 +68,11 @@ export function normalizeToken(token: string): string {
|
||||
export function isAccountConfigured(
|
||||
account: {
|
||||
username?: string;
|
||||
token?: string;
|
||||
accessToken?: string;
|
||||
clientId?: string;
|
||||
},
|
||||
resolvedToken?: string | null,
|
||||
): boolean {
|
||||
const token = resolvedToken ?? account?.token;
|
||||
const token = resolvedToken ?? account?.accessToken;
|
||||
return Boolean(account?.username && token && account?.clientId);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user