streamops-twitch-connector 0.1.4 → 0.2.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/README.md CHANGED
@@ -76,6 +76,23 @@ await twitch.connectEventSub();
76
76
 
77
77
  - No dependency on NestJS or Express.
78
78
  - OAuth tokens are persisted through a caller-provided token store.
79
+ - Tokens are validated when first used and then hourly, as required by
80
+ Twitch. Helix `401` responses trigger a single refresh-and-retry attempt,
81
+ following Twitch's recommendation to handle invalid tokens reactively.
82
+ See Twitch's [token validation](https://dev.twitch.tv/docs/authentication/validate-tokens/)
83
+ and [refresh](https://dev.twitch.tv/docs/authentication/refresh-tokens/)
84
+ guidance.
85
+ - `twitch.tokens.refresh()` is available to request a refresh explicitly.
86
+ Listen to `twitch.tokens.onAuthError()` for background validation failures;
87
+ a `reauthentication_required` error means the user must authorize again.
88
+ The existing `startLogin()` / `completeLogin()` flow can be run in-process;
89
+ restarting the app is not required.
90
+ - Refresh tokens may rotate, so token stores must persist every `save()` call.
91
+ - Twitch documents silent refresh for Authorization Code Grant user tokens.
92
+ The built-in Device Code flow may therefore require interactive login again
93
+ when Twitch rejects its refresh token; the library reports this explicitly.
94
+ - A `clientSecret` may be provided for confidential applications only. Never
95
+ ship it in a desktop or other public client.
79
96
  - Helix requests automatically include the Client ID and access token.
80
97
  - Chat messages are received through EventSub and sent through Helix.
81
98
  - EventSub subscriptions use the active WebSocket session ID.
@@ -1 +1 @@
1
- {"version":3,"file":"twitch-api-client.d.ts","sourceRoot":"","sources":["../../src/api/twitch-api-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAKxD,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAIvB,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAJZ,MAAM,EAAE,IAAI,CAC3B,qBAAqB,EACrB,UAAU,GAAG,OAAO,CACrB,EACgB,YAAY,EAAE,YAAY;IAG7C,OAAO,KAAK,OAAO,GAElB;IAEK,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,CAAC,CAAC;IAqCb,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAOhD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;CAKpC"}
1
+ {"version":3,"file":"twitch-api-client.d.ts","sourceRoot":"","sources":["../../src/api/twitch-api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAKxD,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAIvB,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAJZ,MAAM,EAAE,IAAI,CAC3B,qBAAqB,EACrB,UAAU,GAAG,OAAO,CACrB,EACgB,YAAY,EAAE,YAAY;IAG7C,OAAO,KAAK,OAAO,GAElB;IAEK,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,CAAC,CAAC;IA+Cb,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAOhD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;CAKpC"}
@@ -11,18 +11,26 @@ export class TwitchApiClient {
11
11
  return this.config.fetch ?? fetch;
12
12
  }
13
13
  async request(path, init = {}) {
14
- const accessToken = await this.tokenManager.getAccessToken();
14
+ let accessToken = await this.tokenManager.getAccessToken();
15
15
  const url = new URL(path.replace(/^\/+/, ''), `${HELIX_BASE_URL}/`);
16
- const headers = new Headers(init.headers);
17
- headers.set('Client-Id', this.config.clientId);
18
- headers.set('Authorization', `Bearer ${accessToken}`);
19
- if (init.body && !headers.has('Content-Type')) {
20
- headers.set('Content-Type', 'application/json');
16
+ const send = (token) => {
17
+ const headers = new Headers(init.headers);
18
+ headers.set('Client-Id', this.config.clientId);
19
+ headers.set('Authorization', `Bearer ${token}`);
20
+ if (init.body && !headers.has('Content-Type')) {
21
+ headers.set('Content-Type', 'application/json');
22
+ }
23
+ return this.fetcher(url, {
24
+ ...init,
25
+ headers,
26
+ });
27
+ };
28
+ let response = await send(accessToken);
29
+ if (response.status === 401 && isReplayableBody(init.body)) {
30
+ accessToken =
31
+ await this.tokenManager.refreshAfterUnauthorized(accessToken);
32
+ response = await send(accessToken);
21
33
  }
22
- const response = await this.fetcher(url, {
23
- ...init,
24
- headers,
25
- });
26
34
  if (!response.ok) {
27
35
  const responseText = await response.text().catch(() => '');
28
36
  throw new TwitchError(`Twitch Helix request failed (${response.status})${responseText ? `: ${responseText}` : ''}`, response.status);
@@ -47,4 +55,9 @@ export class TwitchApiClient {
47
55
  });
48
56
  }
49
57
  }
58
+ function isReplayableBody(body) {
59
+ return (body == null ||
60
+ typeof body !== 'object' ||
61
+ !('getReader' in body));
62
+ }
50
63
  //# sourceMappingURL=twitch-api-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"twitch-api-client.js","sourceRoot":"","sources":["../../src/api/twitch-api-client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAExD,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,MAAM,OAAO,eAAe;IAEP;IAIA;IALnB,YACmB,MAGhB,EACgB,YAA0B;QAJ1B,WAAM,GAAN,MAAM,CAGtB;QACgB,iBAAY,GAAZ,YAAY,CAAc;IAC1C,CAAC;IAEJ,IAAY,OAAO;QACjB,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAO,CACX,IAAY,EACZ,OAAoB,EAAE;QAEtB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QAC7D,MAAM,GAAG,GAAG,IAAI,GAAG,CACjB,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EACxB,GAAG,cAAc,GAAG,CACrB,CAAC;QAEF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1C,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,WAAW,EAAE,CAAC,CAAC;QAEtD,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;YAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;YACvC,GAAG,IAAI;YACP,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC3D,MAAM,IAAI,WAAW,CACnB,gCAAgC,QAAQ,CAAC,MAAM,IAC7C,YAAY,CAAC,CAAC,CAAC,KAAK,YAAY,EAAE,CAAC,CAAC,CAAC,EACvC,EAAE,EACF,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO,SAAc,CAAC;QACxB,CAAC;QAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IACtC,CAAC;IAED,GAAG,CAAI,IAAY;QACjB,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,CAAI,IAAY,EAAE,IAAa;QACjC,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAI,IAAY;QACpB,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,EAAE;YAC3B,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;IACL,CAAC;CACF"}
1
+ {"version":3,"file":"twitch-api-client.js","sourceRoot":"","sources":["../../src/api/twitch-api-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAExD,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD,MAAM,OAAO,eAAe;IAEP;IAIA;IALnB,YACmB,MAGhB,EACgB,YAA0B;QAJ1B,WAAM,GAAN,MAAM,CAGtB;QACgB,iBAAY,GAAZ,YAAY,CAAc;IAC1C,CAAC;IAEJ,IAAY,OAAO;QACjB,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAO,CACX,IAAY,EACZ,OAAoB,EAAE;QAEtB,IAAI,WAAW,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QAC3D,MAAM,GAAG,GAAG,IAAI,GAAG,CACjB,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EACxB,GAAG,cAAc,GAAG,CACrB,CAAC;QAEF,MAAM,IAAI,GAAG,CAAC,KAAa,EAAqB,EAAE;YAChD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1C,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC/C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,EAAE,CAAC,CAAC;YAEhD,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;gBAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YAClD,CAAC;YAED,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;gBACvB,GAAG,IAAI;gBACP,OAAO;aACR,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,CAAC;QAEvC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3D,WAAW;gBACT,MAAM,IAAI,CAAC,YAAY,CAAC,wBAAwB,CAAC,WAAW,CAAC,CAAC;YAChE,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,CAAC;QACrC,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC3D,MAAM,IAAI,WAAW,CACnB,gCAAgC,QAAQ,CAAC,MAAM,IAC7C,YAAY,CAAC,CAAC,CAAC,KAAK,YAAY,EAAE,CAAC,CAAC,CAAC,EACvC,EAAE,EACF,QAAQ,CAAC,MAAM,CAChB,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO,SAAc,CAAC;QACxB,CAAC;QAED,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAM,CAAC;IACtC,CAAC;IAED,GAAG,CAAI,IAAY;QACjB,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,CAAI,IAAY,EAAE,IAAa;QACjC,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,EAAE;YAC3B,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAI,IAAY;QACpB,OAAO,IAAI,CAAC,OAAO,CAAI,IAAI,EAAE;YAC3B,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;IACL,CAAC;CACF;AAED,SAAS,gBAAgB,CAAC,IAAiC;IACzD,OAAO,CACL,IAAI,IAAI,IAAI;QACZ,OAAO,IAAI,KAAK,QAAQ;QACxB,CAAC,CAAC,WAAW,IAAI,IAAI,CAAC,CACvB,CAAC;AACJ,CAAC"}
@@ -1,15 +1,17 @@
1
1
  import type { TwitchTokenStore } from '../auth/token-store.js';
2
2
  export interface TwitchConnectorConfig {
3
3
  clientId: string;
4
+ /** Only provide this for a confidential Twitch application. Never embed it in a desktop client. */
5
+ clientSecret?: string;
4
6
  scopes: string[];
5
7
  tokenStore: TwitchTokenStore;
6
8
  fetch?: typeof fetch;
7
9
  }
8
10
  export interface TwitchValidatedToken {
9
11
  client_id: string;
10
- login: string;
12
+ login: string | null;
11
13
  scopes: string[];
12
- user_id: string;
14
+ user_id: string | null;
13
15
  expires_in: number;
14
16
  }
15
17
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB"}
@@ -6,14 +6,36 @@ export declare class TokenManager {
6
6
  private readonly tokenStore;
7
7
  private readonly deviceCodeFlow;
8
8
  private refreshPromise;
9
- constructor(config: Pick<TwitchConnectorConfig, 'clientId' | 'scopes' | 'fetch'>, tokenStore: TwitchTokenStore, deviceCodeFlow: DeviceCodeFlow);
9
+ private validationPromise;
10
+ private validationTimer;
11
+ private lastValidatedAt;
12
+ private lastAuthError;
13
+ private validationMonitoring;
14
+ private readonly authErrorHandlers;
15
+ constructor(config: Pick<TwitchConnectorConfig, 'clientId' | 'clientSecret' | 'scopes' | 'fetch'>, tokenStore: TwitchTokenStore, deviceCodeFlow: DeviceCodeFlow);
10
16
  private get fetcher();
17
+ /** Loads persisted token metadata without attempting an expiry-based refresh. */
11
18
  getTokens(): Promise<TwitchTokens>;
12
19
  getAccessToken(): Promise<string>;
13
20
  getValidatedToken(): Promise<TwitchValidatedToken>;
21
+ /** Force a refresh without requiring the consuming application to restart. */
22
+ refresh(): Promise<TwitchTokens>;
23
+ /**
24
+ * Refresh after Helix rejects a token. Concurrent requests reuse a refresh
25
+ * already performed by another request when the store has the newer token.
26
+ */
27
+ refreshAfterUnauthorized(rejectedAccessToken: string): Promise<string>;
14
28
  startLogin(): Promise<import("./device-code-flow.js").DeviceCodeLogin>;
15
29
  completeLogin(signal?: AbortSignal): Promise<TwitchTokens>;
16
30
  logout(): Promise<void>;
31
+ onAuthError(handler: (error: Error) => void): () => void;
32
+ get authError(): Error | null;
33
+ dispose(): void;
34
+ private validateIfDue;
35
+ private validateCurrentTokens;
36
+ private validate;
37
+ private scheduleValidation;
38
+ private stopValidationTimer;
17
39
  private refreshTokens;
18
40
  private performRefresh;
19
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAavE,qBAAa,YAAY;IAIrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAIvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc;IARjC,OAAO,CAAC,cAAc,CAAsC;gBAGzC,MAAM,EAAE,IAAI,CAC3B,qBAAqB,EACrB,UAAU,GAAG,QAAQ,GAAG,OAAO,CAChC,EACgB,UAAU,EAAE,gBAAgB,EAC5B,cAAc,EAAE,cAAc;IAGjD,OAAO,KAAK,OAAO,GAElB;IAEK,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAclC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAIjC,iBAAiB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAmBlD,UAAU;IAIV,aAAa,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAc1D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,OAAO,CAAC,aAAa;YAYP,cAAc;CA4C7B"}
1
+ {"version":3,"file":"token-manager.d.ts","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAevE,qBAAa,YAAY;IAUrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAIvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAdjC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,iBAAiB,CAA8C;IACvE,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,oBAAoB,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqC;gBAGpD,MAAM,EAAE,IAAI,CAC3B,qBAAqB,EACrB,UAAU,GAAG,cAAc,GAAG,QAAQ,GAAG,OAAO,CACjD,EACgB,UAAU,EAAE,gBAAgB,EAC5B,cAAc,EAAE,cAAc;IAGjD,OAAO,KAAK,OAAO,GAElB;IAED,iFAAiF;IAC3E,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAUlC,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAKjC,iBAAiB,IAAI,OAAO,CAAC,oBAAoB,CAAC;IAIxD,8EAA8E;IACxE,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;IAUtC;;;OAGG;IACG,wBAAwB,CAC5B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,CAAC;IAeZ,UAAU;IAIV,aAAa,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAgB1D,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ7B,WAAW,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKxD,IAAI,SAAS,IAAI,KAAK,GAAG,IAAI,CAE5B;IAED,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,aAAa;YAqBP,qBAAqB;IA8CnC,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,aAAa;YAYP,cAAc;CAqD7B"}
@@ -1,11 +1,18 @@
1
1
  import { TwitchError } from '../errors/twitch-error.js';
2
2
  const TWITCH_VALIDATE_URL = 'https://id.twitch.tv/oauth2/validate';
3
3
  const TWITCH_TOKEN_URL = 'https://id.twitch.tv/oauth2/token';
4
+ const TOKEN_VALIDATION_INTERVAL_MS = 60 * 60 * 1000;
4
5
  export class TokenManager {
5
6
  config;
6
7
  tokenStore;
7
8
  deviceCodeFlow;
8
9
  refreshPromise = null;
10
+ validationPromise = null;
11
+ validationTimer = null;
12
+ lastValidatedAt = 0;
13
+ lastAuthError = null;
14
+ validationMonitoring = true;
15
+ authErrorHandlers = new Set();
9
16
  constructor(config, tokenStore, deviceCodeFlow) {
10
17
  this.config = config;
11
18
  this.tokenStore = tokenStore;
@@ -14,30 +21,42 @@ export class TokenManager {
14
21
  get fetcher() {
15
22
  return this.config.fetch ?? fetch;
16
23
  }
24
+ /** Loads persisted token metadata without attempting an expiry-based refresh. */
17
25
  async getTokens() {
18
26
  const tokens = await this.tokenStore.load();
19
27
  if (!tokens) {
20
28
  throw new TwitchError('Not authenticated with Twitch.');
21
29
  }
22
- if (tokens.expiresAt > Date.now() + 60_000) {
23
- return tokens;
24
- }
25
- return this.refreshTokens(tokens);
30
+ return tokens;
26
31
  }
27
32
  async getAccessToken() {
33
+ await this.validateIfDue();
28
34
  return (await this.getTokens()).accessToken;
29
35
  }
30
36
  async getValidatedToken() {
31
- const tokens = await this.getTokens();
32
- const response = await this.fetcher(TWITCH_VALIDATE_URL, {
33
- headers: {
34
- Authorization: `OAuth ${tokens.accessToken}`,
35
- },
36
- });
37
- if (!response.ok) {
38
- throw new TwitchError('Twitch token validation failed.', response.status);
37
+ return this.validateCurrentTokens();
38
+ }
39
+ /** Force a refresh without requiring the consuming application to restart. */
40
+ async refresh() {
41
+ const tokens = await this.tokenStore.load();
42
+ if (!tokens) {
43
+ throw new TwitchError('Not authenticated with Twitch.');
44
+ }
45
+ return this.refreshTokens(tokens);
46
+ }
47
+ /**
48
+ * Refresh after Helix rejects a token. Concurrent requests reuse a refresh
49
+ * already performed by another request when the store has the newer token.
50
+ */
51
+ async refreshAfterUnauthorized(rejectedAccessToken) {
52
+ const tokens = await this.tokenStore.load();
53
+ if (!tokens) {
54
+ throw new TwitchError('Not authenticated with Twitch.');
39
55
  }
40
- return (await response.json());
56
+ const refreshed = tokens.accessToken === rejectedAccessToken
57
+ ? await this.refreshTokens(tokens)
58
+ : tokens;
59
+ return refreshed.accessToken;
41
60
  }
42
61
  async startLogin() {
43
62
  return this.deviceCodeFlow.startLogin();
@@ -51,11 +70,102 @@ export class TokenManager {
51
70
  scopes: response.scope,
52
71
  };
53
72
  await this.tokenStore.save(tokens);
73
+ this.lastValidatedAt = 0;
74
+ this.validationMonitoring = true;
54
75
  return tokens;
55
76
  }
56
77
  async logout() {
78
+ this.validationMonitoring = false;
79
+ this.stopValidationTimer();
80
+ this.lastValidatedAt = 0;
81
+ this.lastAuthError = null;
57
82
  await this.tokenStore.clear();
58
83
  }
84
+ onAuthError(handler) {
85
+ this.authErrorHandlers.add(handler);
86
+ return () => this.authErrorHandlers.delete(handler);
87
+ }
88
+ get authError() {
89
+ return this.lastAuthError;
90
+ }
91
+ dispose() {
92
+ this.validationMonitoring = false;
93
+ this.stopValidationTimer();
94
+ }
95
+ validateIfDue() {
96
+ if (this.lastValidatedAt > 0 &&
97
+ Date.now() - this.lastValidatedAt < TOKEN_VALIDATION_INTERVAL_MS) {
98
+ return Promise.resolve();
99
+ }
100
+ if (this.validationPromise) {
101
+ return this.validationPromise.then(() => undefined);
102
+ }
103
+ this.validationPromise = this.validateCurrentTokens().finally(() => {
104
+ this.validationPromise = null;
105
+ });
106
+ return this.validationPromise.then(() => undefined);
107
+ }
108
+ async validateCurrentTokens() {
109
+ const tokens = await this.getTokens();
110
+ let response = await this.validate(tokens.accessToken);
111
+ if (response.status === 401) {
112
+ const refreshed = await this.refreshTokens(tokens);
113
+ response = await this.validate(refreshed.accessToken);
114
+ }
115
+ if (!response.ok) {
116
+ throw new TwitchError('Twitch token validation failed.', response.status, response.status === 401 ? 'reauthentication_required' : undefined);
117
+ }
118
+ const validated = (await response.json());
119
+ if (validated.client_id !== this.config.clientId ||
120
+ !Number.isFinite(validated.expires_in) ||
121
+ validated.expires_in <= 0) {
122
+ throw new TwitchError('Twitch token validation returned unexpected token details.', response.status, 'invalid_validated_token');
123
+ }
124
+ const currentTokens = await this.tokenStore.load();
125
+ if (currentTokens?.accessToken === tokens.accessToken) {
126
+ await this.tokenStore.save({
127
+ ...currentTokens,
128
+ expiresAt: Date.now() + validated.expires_in * 1000,
129
+ });
130
+ }
131
+ this.lastValidatedAt = Date.now();
132
+ this.lastAuthError = null;
133
+ this.scheduleValidation();
134
+ return validated;
135
+ }
136
+ validate(accessToken) {
137
+ return this.fetcher(TWITCH_VALIDATE_URL, {
138
+ headers: {
139
+ Authorization: `OAuth ${accessToken}`,
140
+ },
141
+ });
142
+ }
143
+ scheduleValidation() {
144
+ if (!this.validationMonitoring) {
145
+ return;
146
+ }
147
+ this.stopValidationTimer();
148
+ this.validationTimer = setTimeout(() => {
149
+ this.validationTimer = null;
150
+ void this.validateCurrentTokens()
151
+ .catch((error) => {
152
+ const authError = error instanceof Error ? error : new Error(String(error));
153
+ this.lastAuthError = authError;
154
+ for (const handler of this.authErrorHandlers) {
155
+ handler(authError);
156
+ }
157
+ })
158
+ .finally(() => this.scheduleValidation());
159
+ }, TOKEN_VALIDATION_INTERVAL_MS);
160
+ const timer = this.validationTimer;
161
+ timer.unref?.();
162
+ }
163
+ stopValidationTimer() {
164
+ if (this.validationTimer) {
165
+ clearTimeout(this.validationTimer);
166
+ this.validationTimer = null;
167
+ }
168
+ }
59
169
  refreshTokens(current) {
60
170
  if (this.refreshPromise) {
61
171
  return this.refreshPromise;
@@ -71,6 +181,9 @@ export class TokenManager {
71
181
  refresh_token: current.refreshToken,
72
182
  client_id: this.config.clientId,
73
183
  });
184
+ if (this.config.clientSecret) {
185
+ body.set('client_secret', this.config.clientSecret);
186
+ }
74
187
  const response = await this.fetcher(TWITCH_TOKEN_URL, {
75
188
  method: 'POST',
76
189
  headers: {
@@ -79,21 +192,25 @@ export class TokenManager {
79
192
  body,
80
193
  });
81
194
  if (!response.ok) {
82
- throw new TwitchError('Twitch token refresh failed. You may need to log in again.', response.status);
195
+ throw new TwitchError('Twitch token refresh failed. You may need to log in again.', response.status, response.status === 400 || response.status === 401
196
+ ? 'reauthentication_required'
197
+ : 'token_refresh_failed');
83
198
  }
84
199
  const result = (await response.json());
85
200
  if (!result.access_token ||
86
201
  !result.refresh_token ||
87
- !Number.isFinite(result.expires_in)) {
202
+ !Number.isFinite(result.expires_in) ||
203
+ result.expires_in <= 0) {
88
204
  throw new TwitchError('Twitch returned an invalid refresh response.');
89
205
  }
90
206
  const tokens = {
91
207
  accessToken: result.access_token,
92
208
  refreshToken: result.refresh_token,
93
209
  expiresAt: Date.now() + result.expires_in * 1000,
94
- scopes: result.scope ?? current.scopes,
210
+ scopes: Array.isArray(result.scope) ? result.scope : current.scopes,
95
211
  };
96
212
  await this.tokenStore.save(tokens);
213
+ this.lastValidatedAt = 0;
97
214
  return tokens;
98
215
  }
99
216
  }
@@ -1 +1 @@
1
- {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../../src/auth/token-manager.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"twitch-client.d.ts","sourceRoot":"","sources":["../../src/twitch/twitch-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,qBAAa,YAAY;IACvB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;gBAElC,MAAM,EAAE,qBAAqB;IA+BnC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CAG1C"}
1
+ {"version":3,"file":"twitch-client.d.ts","sourceRoot":"","sources":["../../src/twitch/twitch-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,qBAAa,YAAY;IACvB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,qBAAqB,CAAC;gBAElC,MAAM,EAAE,qBAAqB;IA+BnC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAKhC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;CAG1C"}
@@ -32,6 +32,7 @@ export class TwitchClient {
32
32
  });
33
33
  }
34
34
  async connectEventSub() {
35
+ await this.tokens.getAccessToken();
35
36
  await this.eventSub.connect();
36
37
  }
37
38
  async disconnectEventSub() {
@@ -1 +1 @@
1
- {"version":3,"file":"twitch-client.js","sourceRoot":"","sources":["../../src/twitch/twitch-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,MAAM,OAAO,YAAY;IACd,MAAM,CAAe;IACrB,GAAG,CAAkB;IACrB,IAAI,CAAmB;IACvB,QAAQ,CAAiB;IACzB,aAAa,CAAwB;IAE9C,YAAY,MAA6B;QACvC,MAAM,UAAU,GAAqB,MAAM,CAAC,UAAU,CAAC;QAEvD,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC;YACxC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,KAAK;SACpB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,GAAG,IAAI,YAAY,CAC5B,MAAM,EACN,UAAU,EACV,cAAc,CACf,CAAC;QAEF,IAAI,CAAC,GAAG,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzD,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC3D,IAAI,IAAI,KAAK,sBAAsB,EAAE,CAAC;gBACpC,MAAM,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC;YAED,4DAA4D;YAC5D,mCAAmC;YACnC,KAAK,QAAQ,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACnC,CAAC;CACF"}
1
+ {"version":3,"file":"twitch-client.js","sourceRoot":"","sources":["../../src/twitch/twitch-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E,MAAM,OAAO,YAAY;IACd,MAAM,CAAe;IACrB,GAAG,CAAkB;IACrB,IAAI,CAAmB;IACvB,QAAQ,CAAiB;IACzB,aAAa,CAAwB;IAE9C,YAAY,MAA6B;QACvC,MAAM,UAAU,GAAqB,MAAM,CAAC,UAAU,CAAC;QAEvD,MAAM,cAAc,GAAG,IAAI,cAAc,CAAC;YACxC,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,KAAK,EAAE,MAAM,CAAC,KAAK;SACpB,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,GAAG,IAAI,YAAY,CAC5B,MAAM,EACN,UAAU,EACV,cAAc,CACf,CAAC;QAEF,IAAI,CAAC,GAAG,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACpD,IAAI,CAAC,IAAI,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,IAAI,CAAC,aAAa,GAAG,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzD,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;YAC3D,IAAI,IAAI,KAAK,sBAAsB,EAAE,CAAC;gBACpC,MAAM,IAAI,CAAC,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;YAC/C,CAAC;YAED,4DAA4D;YAC5D,mCAAmC;YACnC,KAAK,QAAQ,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QACnC,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;IACnC,CAAC;CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamops-twitch-connector",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-independent Twitch OAuth, EventSub, and chat connector",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,8 +27,5 @@
27
27
  "@types/node": "^24.0.0",
28
28
  "typescript": "^6.0.2",
29
29
  "vitest": "^4.1.2"
30
- },
31
- "dependencies": {
32
- "streamops-twitch-connector": "file:streamops-twitch-connector-0.1.2.tgz"
33
30
  }
34
31
  }