{"_id":"@blorkfield/twitch-integration","_rev":"9-1967ce8fa5f2b6f435e6eac7d02817bf","name":"@blorkfield/twitch-integration","dist-tags":{"latest":"0.6.1"},"versions":{"0.1.0":{"name":"@blorkfield/twitch-integration","version":"0.1.0","_id":"@blorkfield/twitch-integration@0.1.0","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"dist":{"shasum":"d76513d4c0534eaed3f02f4e9324b9923795a3ea","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.1.0.tgz","fileCount":8,"integrity":"sha512-0BWsL7/IyqqI7AAO6HStK2rApnZpEbM0DRbiR5edaBHJ7u+G3vbCEj2MODdKJhUbUq7EgCcTECiVy4GPzYANIg==","signatures":[{"sig":"MEUCIGwatFX5rZ4Z4L5gwqGoVzRVj5FTXci/nUInwVRIq3loAiEAwRiXqQnh3CZB58Fwyz0sdqrzpR6YcKj9E4xuPxnbKXg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":114287},"main":"./dist/index.cjs","type":"module","_from":"file:blorkfield-twitch-integration-0.1.0.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"scripts":{"dev":"pnpm build && pnpm -C testbed dev","build":"tsup","changeset":"changeset","typecheck":"tsc --noEmit"},"_npmUser":{"name":"blorkfield_user","email":"meier_steven@yahoo.com"},"_resolved":"/tmp/8d7259262b9e459ea5312a53f8654f9a/blorkfield-twitch-integration-0.1.0.tgz","_integrity":"sha512-0BWsL7/IyqqI7AAO6HStK2rApnZpEbM0DRbiR5edaBHJ7u+G3vbCEj2MODdKJhUbUq7EgCcTECiVy4GPzYANIg==","_npmVersion":"11.10.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"25.6.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.1.0_1771987151195_0.4422322372910157","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@blorkfield/twitch-integration","version":"0.2.1","_id":"@blorkfield/twitch-integration@0.2.1","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"c53fc2fa5c2ff192b1ead83372685ea2d8a89e79","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.2.1.tgz","fileCount":8,"integrity":"sha512-5pM2Sk+T2w/EB1VBq+ctwLfX01CDfEV1NX9S3EvrizeCTjl8NhsqK3BtnfonaUKpmeYbTzW5s/66rXKnGhAIbw==","signatures":[{"sig":"MEUCIQDCHzjlqEMmhe6zurfuUd5L3gbsTmbunaHo5k6XvZ5iDAIgHYeust4uOZAdIyoM6gBAzjM0yILmChUZQoJbixNNDyI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":114561},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d6629a6bec65445fc118d7f140e743f7d90d667e","scripts":{"dev":"pnpm build && pnpm -C testbed dev","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true","publish":"pnpm build && pnpm publish --access public","changeset":"changeset","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.10.1","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.2.1_1771987718872_0.2767850356104269","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@blorkfield/twitch-integration","version":"0.2.2","_id":"@blorkfield/twitch-integration@0.2.2","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"eac3eb0875ebb3bbbccb2b9f8964e0b55cc1a22e","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.2.2.tgz","fileCount":8,"integrity":"sha512-br6gKyphLp3VbW7A93EW8lKom8iWEQD40gY0OWaMJVdsN0E8gXLZi54LM3cl/BhEfRNebRqix+8yHkwYSyovWA==","signatures":[{"sig":"MEUCIQC5LDKdkAJDlb4mkXP7A61qxFfU1jg6yWdD51/JaVgZSAIgPMNlRyozr06/PrdyFAqjByZhk+bJratINqlYy6FMUnQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.2.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":114500},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"1e209f13096e34233475f613b09a061ebc97a760","scripts":{"dev":"pnpm build && pnpm -C testbed dev","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true","changeset":"changeset","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.10.1","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.2.2_1771988023372_0.8738497131052951","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@blorkfield/twitch-integration","version":"0.3.0","_id":"@blorkfield/twitch-integration@0.3.0","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"3db57d6ee4c241321c20fd65ea5c6b297b64f428","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.3.0.tgz","fileCount":8,"integrity":"sha512-B515DQrvj6hW4Mm40FCzkb9kVdlgLlh3nngVwXEC6GV2anKNdX+VJNZ2UKhecnwg17VlDIuDvktW5VvNT3ngng==","signatures":[{"sig":"MEQCIATgDN8ALzi0OyJQpjgutwmhkJ7adUWZiYRpf5R3KwomAiBXUFbUU6WTSb/ZVwl/iDIA3X7ptix0u4+axw7wy28LLA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":135495},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d65ba8d569e22c2e3b00197bd12449632fdf8506","scripts":{"dev":"pnpm build && pnpm -C testbed dev","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true","changeset":"changeset","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.11.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.3.0_1772069721888_0.6620191866921548","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@blorkfield/twitch-integration","version":"0.4.0","_id":"@blorkfield/twitch-integration@0.4.0","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"1f95499a7e2b48ea724d1d1bf8528502d9eae4da","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.4.0.tgz","fileCount":8,"integrity":"sha512-ExAdwkHgmuRAPj2+Uh6RKmdBS1gPq/rytZpdbuQRuXHpeNy4Lz0altFRY17ucJHWO9P98MIFpMwdZS96CVFnQA==","signatures":[{"sig":"MEUCICM7dSZ/9AaqlQbuq9bGbdxF14gWnc5gLMiRJJoIQvvSAiEAqayAMl2QQKrJdty3MIbyxfAZrcljMEQG/lROQ5x4dFE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":249082},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"b6abe33aafcfd36aec7878d4822d45dcb006f6ab","scripts":{"dev":"pnpm build && pnpm -C testbed dev","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true","changeset":"changeset","typecheck":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.11.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.4.0_1772147486769_0.7940622740099788","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@blorkfield/twitch-integration","version":"0.5.0","_id":"@blorkfield/twitch-integration@0.5.0","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"221c84c1b63cc1fb4e89b0b15bac80389a220cdf","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.5.0.tgz","fileCount":8,"integrity":"sha512-sX1n6W+b2r/a8/tv0WU2MEPYul+vWOfPfq62+jIEhYAuHG9A6TzC8WhWxms+t8zNhBFIhmj8ZDFB/K4nIaYA/Q==","signatures":[{"sig":"MEYCIQCUhNZ2oCuww5DOsVYrT85hQBWkEPoDQRz9vBnllI5RnAIhANuuHNr66XsJzBq6ltOr6SQMZZFRhJMZOKTuEIkUdWH6","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":250227},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"cc4c412166ab89b501d426c6c7618aef752f6a83","scripts":{"dev":"pnpm build && node scripts/dev.js","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true; [ -d testbed-sim ] && pnpm -C testbed-sim install || true","changeset":"changeset","typecheck":"tsc --noEmit","dev:connect":"pnpm build && pnpm -C testbed dev","dev:simulation":"pnpm build && pnpm -C testbed-sim dev"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.11.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.5.0_1772423291671_0.8623213038292588","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@blorkfield/twitch-integration","version":"0.5.1","_id":"@blorkfield/twitch-integration@0.5.1","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"37b893aeba4693a2375c875571dab98a8e3c9689","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.5.1.tgz","fileCount":16,"integrity":"sha512-WlK9xvHwxUCW9uVE3PsYHx42N/lz6ip27UeFe9qIHC1f6YpBAZ8dAYzoV4jgWHoz7RG877RhswW5ps39OF8aTw==","signatures":[{"sig":"MEUCIQDGorl/97l+V0HIOZx4STFLh7hHOApZ+kxqQf2vt73TMwIgNnX9MnhSRBH4tNfseVgmZFeb+rWECT1jxfJPO1UPY74=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.5.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":442409},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./simulation":{"import":{"types":"./dist/simulation/index.d.ts","default":"./dist/simulation/index.js"},"require":{"types":"./dist/simulation/index.d.cts","default":"./dist/simulation/index.cjs"}}},"gitHead":"1224c43aa36cba5cd775fe59dd16430b35e06845","scripts":{"dev":"pnpm build && node scripts/dev.js","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true; [ -d testbed-sim ] && pnpm -C testbed-sim install || true","changeset":"changeset","typecheck":"tsc --noEmit","dev:connect":"pnpm build && pnpm -C testbed dev","dev:simulation":"pnpm build && pnpm -C testbed-sim dev"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.11.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.5.1_1772462643511_0.2931061738891647","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@blorkfield/twitch-integration","version":"0.6.0","_id":"@blorkfield/twitch-integration@0.6.0","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"homepage":"https://github.com/Blorkfield/twitch-integration#readme","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"dist":{"shasum":"97c97e3b9fcd6a1d892cb2387e173f993a759acb","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.6.0.tgz","fileCount":16,"integrity":"sha512-ZKO6LNPxOJpbhVRlHhTxUphEWvLmImhpzdAvPW8jekrUvbgn6VdypmmRPC9RM73uX69fdbMxmT0a+aiDIxEhNg==","signatures":[{"sig":"MEYCIQCLtrRZbmNxvlYMr8DlN1zoknIi47Ml7dAxNs8EKr+EGAIhAP+WQI9i6HvHsTon5U9bQ4HIiWljlRczHVCjvPxZXCHu","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":456452},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./simulation":{"import":{"types":"./dist/simulation/index.d.ts","default":"./dist/simulation/index.js"},"require":{"types":"./dist/simulation/index.d.cts","default":"./dist/simulation/index.cjs"}}},"gitHead":"a5a0737897f44279bb0ad31bc83416275c78cbd8","scripts":{"dev":"pnpm build && node scripts/dev.js","build":"tsup","prepare":"[ -d testbed ] && pnpm -C testbed install || true; [ -d testbed-sim ] && pnpm -C testbed-sim install || true","changeset":"changeset","typecheck":"tsc --noEmit","dev:connect":"pnpm build && pnpm -C testbed dev","dev:simulation":"pnpm build && pnpm -C testbed-sim dev"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"repository":{"url":"git+https://github.com/Blorkfield/twitch-integration.git","type":"git"},"_npmVersion":"11.11.0","description":"Twitch EventSub WebSocket client with normalized chat message stream","directories":{},"_nodeVersion":"22.22.0","dependencies":{"eventemitter3":"^5.0.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.27.0","devDependencies":{"ws":"^8.18.0","tsup":"^8.0.0","@types/ws":"^8.5.10","typescript":"^5.4.0","@types/node":"^20.0.0","@changesets/cli":"^2.27.0"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/twitch-integration_0.6.0_1772507099627_0.3927288219262166","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@blorkfield/twitch-integration","version":"0.6.1","description":"Twitch EventSub WebSocket client with normalized chat message stream","repository":{"type":"git","url":"git+https://github.com/Blorkfield/twitch-integration.git"},"packageManager":"pnpm@10.27.0","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./simulation":{"import":{"types":"./dist/simulation/index.d.ts","default":"./dist/simulation/index.js"},"require":{"types":"./dist/simulation/index.d.cts","default":"./dist/simulation/index.cjs"}}},"scripts":{"dev":"pnpm build && node scripts/dev.js","dev:connect":"pnpm build && pnpm -C testbed dev","dev:simulation":"pnpm build && pnpm -C testbed-sim dev","build":"tsup","typecheck":"tsc --noEmit","changeset":"changeset","prepare":"[ -d testbed ] && pnpm -C testbed install || true; [ -d testbed-sim ] && pnpm -C testbed-sim install || true"},"dependencies":{"eventemitter3":"^5.0.1"},"peerDependencies":{"ws":"^8.0.0"},"peerDependenciesMeta":{"ws":{"optional":true}},"devDependencies":{"@changesets/cli":"^2.27.0","@types/node":"^20.0.0","@types/ws":"^8.5.10","tsup":"^8.0.0","typescript":"^5.4.0","ws":"^8.18.0"},"publishConfig":{"access":"public"},"gitHead":"24d8bff32af1190ab9db4e6bab7f31d5c706e9d0","_id":"@blorkfield/twitch-integration@0.6.1","bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"homepage":"https://github.com/Blorkfield/twitch-integration#readme","_nodeVersion":"22.22.0","_npmVersion":"11.11.0","dist":{"integrity":"sha512-2tNnUU9m+q5Jn+npBoX/Gv1QXUj+XIHkONsfeabIq8GC8TPP9zXUG9ARgqb3jtv4pTUzHVeWgjLe3msKttYYCw==","shasum":"d5802a84348d493687d113f4ba000d58a35e6575","tarball":"https://registry.npmjs.org/@blorkfield/twitch-integration/-/twitch-integration-0.6.1.tgz","fileCount":16,"unpackedSize":461566,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@blorkfield%2ftwitch-integration@0.6.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDJMR7jErJhd1msJTrb0AbeKk6zHvwv/EjWsC1o7JdFkAIgWxNpCvDDBkaP/qQ7FYOTCXjxVKQb0zScdoa6Nk+Dukw="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ba3234f8-3d3b-4f69-935a-6aad6f12ded5"}},"directories":{},"maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/twitch-integration_0.6.1_1772508610417_0.7455362228688411"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-25T02:39:11.127Z","modified":"2026-03-03T03:30:11.420Z","0.1.0":"2026-02-25T02:39:11.341Z","0.2.1":"2026-02-25T02:48:39.048Z","0.2.2":"2026-02-25T02:53:43.562Z","0.3.0":"2026-02-26T01:35:22.058Z","0.4.0":"2026-02-26T23:11:26.959Z","0.5.0":"2026-03-02T03:48:11.820Z","0.5.1":"2026-03-02T14:44:03.674Z","0.6.0":"2026-03-03T03:04:59.774Z","0.6.1":"2026-03-03T03:30:10.766Z"},"bugs":{"url":"https://github.com/Blorkfield/twitch-integration/issues"},"homepage":"https://github.com/Blorkfield/twitch-integration#readme","repository":{"type":"git","url":"git+https://github.com/Blorkfield/twitch-integration.git"},"description":"Twitch EventSub WebSocket client with normalized chat message stream","maintainers":[{"name":"blorkfield_user","email":"meier_steven@yahoo.com"}],"readme":"# @blorkfield/twitch-integration\n\nManages a Twitch EventSub WebSocket connection and exposes a typed event stream for all major channel and stream events: chat messages, follows, subscriptions, raids, cheers, channel points, hype trains, polls, predictions, shoutouts, stream status, ad breaks, and channel updates.\n\n---\n\n## Installation\n\n```bash\npnpm add @blorkfield/twitch-integration\n# ws is required in Node.js environments\npnpm add ws\n```\n\n---\n\n## Prerequisites: credentials\n\nYou need four things before constructing `TwitchClient`:\n\n| Option | What it is | How to get it |\n|---|---|---|\n| `clientId` | Your Twitch app's client ID | [Twitch Developer Console](https://dev.twitch.tv/console/apps) → your app |\n| `accessToken` | A **user** access token for the account | OAuth flow — **not** an app access token |\n| `userId` | Twitch numeric user ID of the account that owns the token | Call `GET /helix/users` with the token |\n| `channelId` | Twitch numeric user ID of the broadcaster whose channel you're monitoring | Call `GET /helix/users?login=channelname` |\n\nThe library does not handle OAuth. Obtain the token yourself and pass it in. Use `onTokenRefresh` to persist refreshed tokens.\n\n> **Why a user token?** Twitch's EventSub WebSocket transport does not accept app access tokens — this is a hard Twitch protocol requirement.\n\n---\n\n## Getting an access token\n\nNo tools needed.\n\n### Step 1 — Add the redirect URL to your Twitch app\n\n**This must be done before the flow will work.** Go to [dev.twitch.tv/console/apps](https://dev.twitch.tv/console/apps), open your app, and add exactly this to the **OAuth Redirect URLs** list:\n\n```\nhttps://localhost\n```\n\nSave the app. If this value isn't there, Twitch will reject the authorization and you'll get a redirect_uri mismatch error.\n\n### Step 2 — Open the authorization URL\n\nPaste this into your browser after substituting your `clientId`:\n\n```\nhttps://id.twitch.tv/oauth2/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://localhost&response_type=token&scope=user:read:chat+moderator:read:followers+channel:read:subscriptions+bits:read+channel:read:hype_train+channel:read:polls+channel:read:predictions+channel:read:redemptions+channel:read:ads+moderator:read:shoutouts\n```\n\n### Step 3 — Grab the token\n\n1. Authorize in the browser\n2. You get redirected to `https://localhost` (which fails to load — that's expected)\n3. Your token is in the URL bar: `https://localhost/#access_token=YOUR_TOKEN&...`\n4. Copy everything after `access_token=` up to the first `&`\n\nThat's your `accessToken`. You only need scopes for the subscription flags you actually enable — remove any you don't need from the scope list before opening the URL.\n\n---\n\n## OAuth scopes\n\nEach subscription type requires the corresponding scope(s) on your access token. Only enable subscriptions for scopes your token actually has — missing scopes cause individual subscription failures which are emitted as `error` events.\n\n| `subscriptions` flag | EventSub types registered | Required scope |\n|---|---|---|\n| `chat` | `channel.chat.message` | `user:read:chat` |\n| `follow` | `channel.follow` | `moderator:read:followers` |\n| `subscribe` | `channel.subscribe`, `channel.subscription.message`, `channel.subscription.gift`, `channel.subscription.end` | `channel:read:subscriptions` |\n| `cheer` | `channel.cheer` | `bits:read` |\n| `raid` | `channel.raid` | *(none)* |\n| `streamStatus` | `stream.online`, `stream.offline` | *(none)* |\n| `channelUpdate` | `channel.update` | *(none)* |\n| `hypeTrain` | `channel.hype_train.begin`, `channel.hype_train.progress`, `channel.hype_train.end` | `channel:read:hype_train` |\n| `polls` | `channel.poll.begin`, `channel.poll.progress`, `channel.poll.end` | `channel:read:polls` |\n| `predictions` | `channel.prediction.begin`, `channel.prediction.progress`, `channel.prediction.lock`, `channel.prediction.end` | `channel:read:predictions` |\n| `channelPoints` | `channel.channel_points_custom_reward_redemption.add` | `channel:read:redemptions` |\n| `adBreak` | `channel.ad_break.begin` | `channel:read:ads` |\n| `shoutouts` | `channel.shoutout.create`, `channel.shoutout.receive` | `moderator:read:shoutouts` |\n\n---\n\n## APIs called at runtime\n\n### On `connect()`\n\n1. Opens a WebSocket to `wss://eventsub.wss.twitch.tv/ws`\n2. Receives `session_welcome` from Twitch containing a `session_id`\n3. `POST https://api.twitch.tv/helix/eventsub/subscriptions` — one call per enabled subscription, registered in parallel using your `clientId` + `accessToken`\n\n### On `preloadEmotes()` / `refreshEmotes()`\n\nFour parallel fetches, all unauthenticated:\n\n| Source | Endpoint |\n|---|---|\n| BTTV global | `GET https://api.betterttv.net/3/cached/emotes/global` |\n| BTTV channel | `GET https://api.betterttv.net/3/cached/users/twitch/{channelId}` |\n| 7TV global | `GET https://7tv.io/v3/emote-sets/global` |\n| 7TV channel | `GET https://7tv.io/v3/users/twitch/{channelId}` |\n\n### On `preloadBadges()`\n\nTwo parallel calls using your credentials:\n\n- `GET https://api.twitch.tv/helix/chat/badges/global`\n- `GET https://api.twitch.tv/helix/chat/badges?broadcaster_id={channelId}`\n\n### On user lookup\n\n- `GET https://api.twitch.tv/helix/users` (batched, cached for 5 minutes)\n\n---\n\n## Usage\n\n```typescript\nimport { TwitchClient } from '@blorkfield/twitch-integration'\n\nconst client = new TwitchClient({\n  channelId: '123456789',\n  userId: '987654321',\n  clientId: 'abc123...',\n  accessToken: 'oauth:...',\n  subscriptions: {\n    chat: true,\n    follow: true,\n    subscribe: true,\n    cheer: true,\n    raid: true,\n    streamStatus: true,\n    channelUpdate: true,\n    hypeTrain: true,\n    polls: true,\n    predictions: true,\n    channelPoints: true,\n    adBreak: true,\n    shoutouts: true,\n  },\n  onTokenRefresh: (token) => saveTokenSomewhere(token),\n})\n\nawait client.preloadEmotes()   // optional: fetch BTTV + 7TV emote maps\nawait client.connect()         // resolves once connected; subscriptions registered in parallel\n\n// --- Chat ---\nclient.on('message', (msg) => {\n  console.log(msg.user.displayName, msg.text)\n  console.log(msg.emotes)    // all resolved emotes in this message\n  console.log(msg.fragments) // text/emote/cheermote/mention breakdown\n})\n\n// --- Follows ---\nclient.on('follow', async (e) => {\n  console.log(`${e.user.displayName} followed!`)\n  // fetch profile picture on demand:\n  const pfp = await client.getProfilePictureUrl(e.user.id)\n})\n\n// --- Subscriptions ---\nclient.on('subscribe', (e) => {\n  console.log(`${e.user.displayName} subscribed at tier ${e.tier} (gift: ${e.isGift})`)\n})\n\nclient.on('subscriptionMessage', (e) => {\n  // resub with a shared message\n  console.log(`${e.user.displayName} resubbed for ${e.cumulativeMonths} months: ${e.message.text}`)\n})\n\nclient.on('subscriptionGift', (e) => {\n  const who = e.isAnonymous ? 'anonymous' : e.gifter!.displayName\n  console.log(`${who} gifted ${e.total} tier ${e.tier} subs`)\n})\n\nclient.on('subscriptionEnd', (e) => {\n  console.log(`${e.user.displayName}'s sub ended`)\n})\n\n// --- Cheers ---\nclient.on('cheer', (e) => {\n  const who = e.isAnonymous ? 'anonymous' : e.user!.displayName\n  console.log(`${who} cheered ${e.bits} bits: ${e.message}`)\n})\n\n// --- Raids ---\nclient.on('raid', (e) => {\n  console.log(`${e.fromBroadcaster.displayName} raided with ${e.viewerCount} viewers`)\n})\n\n// --- Stream status ---\nclient.on('streamOnline', (e) => {\n  console.log(`Stream went live at ${e.startedAt}`)\n})\n\nclient.on('streamOffline', () => {\n  console.log('Stream ended')\n})\n\n// --- Channel update ---\nclient.on('channelUpdate', (e) => {\n  console.log(`Title: ${e.title} | Category: ${e.categoryName}`)\n})\n\n// --- Hype Train ---\nclient.on('hypeTrain.begin', (e) => {\n  console.log(`Hype Train started! Level ${e.level} — ${e.total} / ${e.goal}`)\n})\n\nclient.on('hypeTrain.progress', (e) => {\n  console.log(`Hype Train level ${e.level} — ${e.progress} / ${e.goal}`)\n})\n\nclient.on('hypeTrain.end', (e) => {\n  console.log(`Hype Train ended at level ${e.level} with ${e.total} total`)\n})\n\n// --- Polls ---\nclient.on('poll.begin', (e) => {\n  console.log(`Poll: ${e.title}`, e.choices.map(c => c.title))\n})\n\nclient.on('poll.progress', (e) => {\n  console.log(e.choices.map(c => `${c.title}: ${c.votes}`))\n})\n\nclient.on('poll.end', (e) => {\n  const winner = [...e.choices].sort((a, b) => b.votes - a.votes)[0]\n  console.log(`Poll ended — winner: ${winner.title} with ${winner.votes} votes`)\n})\n\n// --- Predictions ---\nclient.on('prediction.begin', (e) => {\n  console.log(`Prediction: ${e.title}`, e.outcomes.map(o => o.title))\n})\n\nclient.on('prediction.progress', (e) => {\n  console.log(e.outcomes.map(o => `${o.title}: ${o.channelPoints} pts`))\n})\n\nclient.on('prediction.lock', (e) => {\n  console.log(`Prediction locked: ${e.title}`)\n})\n\nclient.on('prediction.end', (e) => {\n  const winner = e.outcomes.find(o => o.id === e.winningOutcomeId)\n  console.log(`Prediction resolved — winner: ${winner?.title}`)\n})\n\n// --- Channel Points ---\nclient.on('channelPoints', (e) => {\n  console.log(`${e.user.displayName} redeemed \"${e.reward.title}\"`, e.userInput ?? '')\n})\n\n// --- Ad Breaks ---\nclient.on('adBreak', (e) => {\n  console.log(`Ad break started — ${e.durationSeconds}s (auto: ${e.isAutomatic})`)\n})\n\n// --- Shoutouts ---\nclient.on('shoutout.create', (e) => {\n  console.log(`Gave shoutout to ${e.toBroadcaster.displayName}`)\n})\n\nclient.on('shoutout.receive', (e) => {\n  console.log(`Received shoutout from ${e.fromBroadcaster.displayName}`)\n})\n\n// --- Connection events ---\nclient.on('subscription_error', (type, err) => {\n  // an individual subscription failed — check your token has the required scope\n  console.error(`Failed to subscribe to ${type}:`, err.message)\n})\n\nclient.on('auth_error', () => {\n  // token is invalid or expired — re-authenticate\n})\n\nclient.on('revoked', (reason) => {\n  // Twitch revoked a subscription — do not auto-reconnect\n  console.error('Subscription revoked:', reason)\n})\n\nclient.on('disconnected', (code, reason) => {\n  // library auto-reconnects on unexpected disconnects (non-1000 codes)\n})\n\n// Later:\nclient.disconnect()\n```\n\n---\n\n## Simulation mode\n\n`TwitchSimulator` lets you drive the full `TwitchClient` pipeline without credentials — no Twitch account, no WebSocket connection, no Helix API calls. Useful for UI development, automated tests, demos, and CI.\n\nThe simulator works by passing mock `WebSocket` and `fetch` implementations directly into `TwitchClient` via its `transport` option. No globals are patched — your app's real `fetch` and `WebSocket` are untouched, so other libraries in the same page or process are completely unaffected.\n\n```bash\npnpm add @blorkfield/twitch-integration\n```\n\n```typescript\nimport { TwitchSimulator } from '@blorkfield/twitch-integration/simulation'\n\nconst simulator = new TwitchSimulator()\nconst client = simulator.client\n\nclient.on('message', (msg) => console.log(msg.user.displayName, msg.text))\nclient.on('follow', (e) => console.log(`${e.user.displayName} followed!`))\nclient.on('raid', (e) => console.log(`Raid: ${e.viewerCount} viewers`))\n\nawait simulator.connect()\n\n// Fire individual events\nsimulator.fireChat('hello chat!')\nsimulator.fireFollow()\nsimulator.fireSubscribe()\nsimulator.fireResub(12, 'Been here a year!')\nsimulator.fireGiftSub(5)\nsimulator.fireCheer(500)\nsimulator.fireRaid(250)\n\n// Fire with a specific user\nimport { USER_POOL } from '@blorkfield/twitch-integration/simulation'\nsimulator.fireChat('hi!', USER_POOL[0])\n\n// Generic fire — useful when action type is dynamic\nsimulator.fire('cheer', undefined, { bits: 100, message: 'Cheer100' })\n```\n\n### Scenario runner\n\nFires events automatically over time:\n\n```typescript\nsimulator.run({\n  duration: 30,        // seconds\n  rate: 3,             // events per second\n  actions: ['chat', 'follow', 'subscribe'],\n  users: 'random',     // or pass SimUser[] to restrict to specific users\n  onFire: (user, action) => console.log(action, user.displayName),\n  onComplete: () => console.log('done'),\n})\n\n// Stop early\nsimulator.stop()\nconsole.log(simulator.running) // false\n```\n\n### Custom users\n\n```typescript\nimport { TwitchSimulator } from '@blorkfield/twitch-integration/simulation'\nimport type { SimUser } from '@blorkfield/twitch-integration/simulation'\n\nconst users: SimUser[] = [\n  { id: '100', login: 'alice', displayName: 'Alice', color: '#FF4444' },\n  { id: '101', login: 'bob',   displayName: 'Bob',   color: '#4488FF' },\n]\n\nconst simulator = new TwitchSimulator({ users })\n```\n\n### `SimulationOptions`\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `channelId` | `string` | `'mock_channel'` | Simulated broadcaster ID |\n| `channelLogin` | `string` | `'mock_streamer'` | Simulated broadcaster login |\n| `channelName` | `string` | `'MockStreamer'` | Simulated broadcaster display name |\n| `users` | `SimUser[]` | 8 built-in users | User pool for random selection |\n\n### `ScenarioOptions`\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `duration` | `number` | — | Run length in seconds |\n| `rate` | `number` | — | Events per second |\n| `actions` | `ActionType[]` | all actions | Which event types to fire |\n| `users` | `'random' \\| SimUser[]` | `'random'` | User source for each tick |\n| `onFire` | `(user, action) => void` | — | Called each time an event fires |\n| `onComplete` | `() => void` | — | Called when the scenario ends |\n\n### Available `ActionType` values\n\n`'chat'` `'follow'` `'subscribe'` `'resub'` `'giftsub'` `'cheer'` `'raid'`\n\n---\n\n## Event reference\n\n### Connection events\n\n| Event | Arguments | When |\n|---|---|---|\n| `connected` | — | All subscription POSTs attempted; WebSocket ready |\n| `disconnected` | `code: number, reason: string` | WebSocket closed |\n| `revoked` | `reason: string` | Twitch revoked a subscription |\n| `auth_error` | — | 401 response on any subscription POST |\n| `subscription_error` | `type: string, err: Error` | Individual subscription POST failed; `type` is the Twitch EventSub type string (e.g. `'channel.follow'`) |\n| `error` | `err: Error` | Parse errors and other unexpected errors |\n\n### Channel events\n\n| Event | Arguments | When |\n|---|---|---|\n| `message` | `msg: NormalizedMessage` | Chat message received |\n| `follow` | `event: FollowEvent` | Someone followed the channel |\n| `subscribe` | `event: SubscribeEvent` | New subscription (not a resub) |\n| `subscriptionMessage` | `event: SubscriptionMessageEvent` | Resub with a shared message |\n| `subscriptionGift` | `event: SubscriptionGiftEvent` | Gifted subscription(s) |\n| `subscriptionEnd` | `event: SubscriptionEndEvent` | Subscription ended |\n| `cheer` | `event: CheerEvent` | Bits cheered |\n| `raid` | `event: RaidEvent` | Incoming raid |\n| `streamOnline` | `event: StreamOnlineEvent` | Stream went live |\n| `streamOffline` | `event: StreamOfflineEvent` | Stream went offline |\n| `channelUpdate` | `event: ChannelUpdateEvent` | Title, category, or language changed |\n| `hypeTrain.begin` | `event: HypeTrainBeginEvent` | Hype Train started |\n| `hypeTrain.progress` | `event: HypeTrainProgressEvent` | Hype Train level progress |\n| `hypeTrain.end` | `event: HypeTrainEndEvent` | Hype Train ended |\n| `poll.begin` | `event: PollBeginEvent` | Poll created |\n| `poll.progress` | `event: PollProgressEvent` | Poll votes updated |\n| `poll.end` | `event: PollEndEvent` | Poll ended |\n| `prediction.begin` | `event: PredictionBeginEvent` | Prediction created |\n| `prediction.progress` | `event: PredictionProgressEvent` | Prediction bets updated |\n| `prediction.lock` | `event: PredictionLockEvent` | Prediction locked (no more bets) |\n| `prediction.end` | `event: PredictionEndEvent` | Prediction resolved or cancelled |\n| `channelPoints` | `event: ChannelPointsEvent` | Channel point reward redeemed |\n| `adBreak` | `event: AdBreakEvent` | Ad break started |\n| `shoutout.create` | `event: ShoutoutCreateEvent` | Shoutout sent to another channel |\n| `shoutout.receive` | `event: ShoutoutReceiveEvent` | Received a shoutout from another channel |\n\n---\n\n## Event payload shapes\n\nAll event payloads include minimal user references (`EventUser`) with `id`, `login`, and `displayName`. Use `client.getUser(id)` or `client.getProfilePictureUrl(id)` to fetch the full user info including profile picture.\n\n```typescript\ninterface EventUser {\n  id: string\n  login: string\n  displayName: string\n}\n```\n\n### Follow\n\n```typescript\ninterface FollowEvent {\n  user: EventUser\n  followedAt: string        // ISO 8601\n}\n```\n\n### Subscribe\n\n```typescript\ninterface SubscribeEvent {\n  user: EventUser\n  tier: '1000' | '2000' | '3000'\n  isGift: boolean\n}\n```\n\n### Subscription message (resub)\n\n```typescript\ninterface SubscriptionMessageEvent {\n  user: EventUser\n  tier: '1000' | '2000' | '3000'\n  cumulativeMonths: number\n  streakMonths: number | null   // null if user chose not to share streak\n  durationMonths: number\n  message: {\n    text: string\n    emotes: Array<{ begin: number; end: number; id: string }>\n  }\n}\n```\n\n### Subscription gift\n\n```typescript\ninterface SubscriptionGiftEvent {\n  gifter: EventUser | null      // null if anonymous\n  isAnonymous: boolean\n  tier: '1000' | '2000' | '3000'\n  total: number                 // subs gifted in this batch\n  cumulativeTotal: number | null\n}\n```\n\n### Subscription end\n\n```typescript\ninterface SubscriptionEndEvent {\n  user: EventUser\n  tier: '1000' | '2000' | '3000'\n  isGift: boolean\n}\n```\n\n### Cheer\n\n```typescript\ninterface CheerEvent {\n  user: EventUser | null        // null if anonymous\n  isAnonymous: boolean\n  bits: number\n  message: string\n}\n```\n\n### Raid\n\n```typescript\ninterface RaidEvent {\n  fromBroadcaster: EventUser\n  viewerCount: number\n}\n```\n\n### Stream online\n\n```typescript\ninterface StreamOnlineEvent {\n  id: string\n  type: string                  // 'live'\n  startedAt: string\n}\n```\n\n### Stream offline\n\n```typescript\ninterface StreamOfflineEvent {}\n```\n\n### Channel update\n\n```typescript\ninterface ChannelUpdateEvent {\n  title: string\n  language: string\n  categoryId: string\n  categoryName: string\n  contentClassificationLabels: string[]\n}\n```\n\n### Hype Train\n\n```typescript\ninterface HypeTrainContribution {\n  user: EventUser\n  type: 'bits' | 'subscription'\n  total: number\n}\n\ninterface HypeTrainBeginEvent {\n  id: string\n  total: number\n  progress: number\n  goal: number\n  topContributions: HypeTrainContribution[]\n  lastContribution: HypeTrainContribution\n  level: number\n  startedAt: string\n  expiresAt: string\n}\n\ninterface HypeTrainProgressEvent {\n  id: string\n  level: number\n  total: number\n  progress: number\n  goal: number\n  topContributions: HypeTrainContribution[]\n  lastContribution: HypeTrainContribution\n  startedAt: string\n  expiresAt: string\n}\n\ninterface HypeTrainEndEvent {\n  id: string\n  level: number\n  total: number\n  topContributions: HypeTrainContribution[]\n  endedAt: string\n  cooldownEndsAt: string\n}\n```\n\n### Poll\n\n```typescript\ninterface PollChoice {\n  id: string\n  title: string\n  bitsVotes: number\n  channelPointsVotes: number\n  votes: number\n}\n\ninterface PollBeginEvent {\n  id: string\n  title: string\n  choices: PollChoice[]\n  bitsVoting: { isEnabled: boolean; amountPerVote: number }\n  channelPointsVoting: { isEnabled: boolean; amountPerVote: number }\n  startedAt: string\n  endsAt: string\n}\n\n// PollProgressEvent has the same shape as PollBeginEvent (votes update in place)\n\ninterface PollEndEvent {\n  id: string\n  title: string\n  choices: PollChoice[]\n  status: 'completed' | 'archived' | 'terminated'\n  startedAt: string\n  endedAt: string\n}\n```\n\n### Prediction\n\n```typescript\ninterface PredictionPredictor {\n  user: EventUser\n  channelPointsWon: number | null\n  channelPointsUsed: number\n}\n\ninterface PredictionOutcome {\n  id: string\n  title: string\n  color: 'blue' | 'pink'\n  users: number\n  channelPoints: number\n  topPredictors: PredictionPredictor[]\n}\n\ninterface PredictionBeginEvent {\n  id: string\n  title: string\n  outcomes: PredictionOutcome[]\n  startedAt: string\n  locksAt: string\n}\n\n// PredictionProgressEvent has the same shape as PredictionBeginEvent\n\ninterface PredictionLockEvent {\n  id: string\n  title: string\n  outcomes: PredictionOutcome[]\n  startedAt: string\n  lockedAt: string\n}\n\ninterface PredictionEndEvent {\n  id: string\n  title: string\n  outcomes: PredictionOutcome[]\n  winningOutcomeId: string | null\n  status: 'resolved' | 'canceled'\n  startedAt: string\n  endedAt: string\n}\n```\n\n### Channel Points redemption\n\n```typescript\ninterface ChannelPointsEvent {\n  id: string\n  user: EventUser\n  reward: {\n    id: string\n    title: string\n    cost: number\n    prompt: string\n  }\n  userInput: string | null\n  status: 'unfulfilled' | 'fulfilled' | 'canceled'\n  redeemedAt: string\n}\n```\n\n### Ad break\n\n```typescript\ninterface AdBreakEvent {\n  durationSeconds: number\n  startedAt: string\n  isAutomatic: boolean\n}\n```\n\n### Shoutout\n\n```typescript\ninterface ShoutoutCreateEvent {\n  toBroadcaster: EventUser\n  viewerCount: number\n  startedAt: string\n}\n\ninterface ShoutoutReceiveEvent {\n  fromBroadcaster: EventUser\n  viewerCount: number\n  startedAt: string\n}\n```\n\n---\n\n## NormalizedMessage shape\n\n```typescript\ninterface NormalizedMessage {\n  id: string\n  text: string                   // full raw message text\n  user: ChatUser\n  fragments: MessageFragment[]   // per-token breakdown\n  emotes: ResolvedEmote[]        // deduplicated list of all emotes in message\n  timestamp: string              // RFC3339\n  cheer?: { bits: number }\n  reply?: {\n    parentMessageId: string\n    parentUserLogin: string\n    parentUserDisplayName: string\n  }\n  channelPointsRewardId?: string\n}\n\ntype MessageFragment =\n  | { type: 'text'; text: string }\n  | { type: 'emote'; text: string; emote: ResolvedEmote }\n  | { type: 'cheermote'; text: string; bits: number; tier: number }\n  | { type: 'mention'; text: string; userId: string; userLogin: string }\n\ninterface ResolvedEmote {\n  id: string\n  name: string\n  source: 'twitch' | 'bttv' | '7tv'\n  animated: boolean\n  imageUrl1x: string\n  imageUrl2x?: string\n  imageUrl3x?: string\n}\n```\n\n### Emote resolution\n\nThird-party emote name collisions are resolved in priority order:\n\n1. 7TV channel\n2. BTTV channel\n3. 7TV global\n4. BTTV global\n5. Twitch (authoritative for native emotes — resolved from fragment data, not name lookup)\n\n---\n\n## User lookup\n\n`TwitchClient` exposes a cached user lookup backed by the Helix API. Use this to fetch profile pictures for users in events.\n\n```typescript\n// Full user info\nconst user: UserInfo | null = await client.getUser('123456789')\n// { id, login, displayName, profileImageUrl, broadcasterType, description, createdAt }\n\n// Batch lookup\nconst users: Map<string, UserInfo | null> = await client.getUsers(['123456789', '987654321'])\n\n// Profile picture shorthand (same cache, same TTL)\nconst url: string | null = await client.getProfilePictureUrl('123456789')\nconst urls: Map<string, string | null> = await client.getProfilePictureUrls(['123456789', '987654321'])\n```\n\nResults are cached for 5 minutes. `broadcasterType` is `'partner'`, `'affiliate'`, or `''`.\n\n### Example: profile picture in a follow event\n\n```typescript\nclient.on('follow', async (e) => {\n  const pfp = await client.getProfilePictureUrl(e.user.id)\n  showFollowAlert({ name: e.user.displayName, avatar: pfp })\n})\n```\n\n---\n\n## Badges\n\n### Preloading\n\n```typescript\nawait client.preloadBadges()\n```\n\nFetches global and channel badge sets from Helix in two parallel calls.\n\n### Auto-resolution on messages\n\nIf badges are preloaded, each `Badge` in `msg.user.badges` will have a `resolved` field populated automatically:\n\n```typescript\nclient.on('message', (msg) => {\n  for (const badge of msg.user.badges) {\n    console.log(badge.setId, badge.id)       // e.g. 'subscriber', '6'\n    console.log(badge.resolved?.title)        // e.g. 'Subscriber'\n    console.log(badge.resolved?.imageUrl2x)   // CDN image URL\n  }\n})\n```\n\n### Manual resolution\n\n```typescript\nconst badge: ResolvedBadge | undefined = client.resolveBadge('subscriber', '6')\n// { title, imageUrl1x, imageUrl2x, imageUrl4x }\n```\n\n---\n\n## Lifecycle notes\n\n- **Reconnect:** handled automatically on unexpected disconnects with a 2s backoff\n- **`session_reconnect`:** library connects to the new URL, waits for `session_welcome`, then closes the old connection — subscriptions carry over automatically, no re-POST\n- **Keepalive:** Twitch sends keepalives; if one doesn't arrive within `keepalive_timeout_seconds + 0.5s`, the library reconnects\n- **Subscription failures:** individual subscription POST failures emit `error` events but do not disconnect or prevent `connected` from firing — the connection stays open for whichever subscriptions succeeded\n- **Max 3 active WebSocket connections** per Twitch user account — `disconnect()` cleanly closes before reconnecting elsewhere\n\n---\n\n## Development\n\nTwo browser testbeds are included.\n\n### Connect testbed (default)\n\nConnects to a real Twitch channel using your own credentials.\n\n```bash\npnpm dev               # build + open connect testbed on http://localhost:5175\npnpm dev connect       # same\n```\n\n### Simulation testbed\n\nNo credentials needed. Uses the package's own `TwitchSimulator` API to drive the full `TwitchClient` pipeline with fake events.\n\n```bash\npnpm dev simulation    # build + open simulation testbed on http://localhost:5176\n```\n\nTwo panels (Controls + Chat Simulation):\n\n- **Controls** — select users, configure a timed run (duration, rate, action types, random or specific users), or manually fire individual events\n- **Chat Simulation** — scrolling chat log showing the normalized events as they flow through the client\n\n### Docker\n\nThe Docker image serves the connect testbed on port 5175.\n\n```bash\ndocker compose up --build\n# → http://localhost:5175\n```\n\n---\n\n## Build\n\n```bash\npnpm build      # tsup → dist/ (ESM + CJS + .d.ts)\npnpm typecheck  # tsc --noEmit\n```\n","readmeFilename":"README.md"}