15 Commits
Author SHA1 Message Date
sugoidogo a27f8804e4 fix type error part 2
docker.yml / build (push) Successful in 46s
2026-08-02 04:18:24 -07:00
sugoidogo 2788275d25 fix type error
docker.yml / build (push) Failing after 39s
2026-08-02 04:15:36 -07:00
sugoidogo 2265fd39e0 always ensure userId is present on tokens
docker.yml / build (push) Failing after 35s
2026-08-02 04:11:11 -07:00
sugoidogo 83f3217565 keep refresh token
docker.yml / build (push) Successful in 46s
2026-08-02 03:53:56 -07:00
sugoidogo a704849061 uphold userid promise
docker.yml / build (push) Successful in 48s
2026-08-02 03:49:31 -07:00
sugoidogo 2c89510400 fix type checking errors
docker.yml / build (push) Successful in 46s
2026-08-01 19:04:05 -07:00
sugoidogo 041bebe4c5 revised auth provider
docker.yml / build (push) Failing after 47s
2026-08-01 18:40:01 -07:00
sugoidogo f50403f381 fix type errors 2026-08-01 18:37:29 -07:00
sugoidogo 6c138c93a4 Update README.md 2025-12-20 07:27:35 +00:00
sugoidogo 6590dcdac7 add tba to exports
/ build (push) Successful in 1m23s
2025-12-17 02:16:38 -08:00
sugoidogo ace36340a8 add setProxyOrigin to tba 2025-12-17 02:16:31 -08:00
sugoidogo 4918696c5a add TwitchAuth to exports 2025-12-17 02:11:18 -08:00
sugoidogo 4bf079571b update readme 2025-12-17 00:34:31 -08:00
sugoidogo f032477067 publish to gitea npm registry
/ build (push) Successful in 2m25s
2025-12-17 00:25:08 -08:00
sugoidogo ada811dda2 revert default fetch handler
/ build (push) Successful in 2m35s
2025-12-16 02:43:00 -08:00
10 changed files with 173 additions and 46 deletions
+8 -24
View File
@@ -2,40 +2,24 @@ on:
push:
tags: [ '*' ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v6
with:
with:
node-version: latest
- name: Install Worker Dependencies
run: npm ci
- run: npm run deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- run: npm pack
- run: mv twitch-cloud-ebs*tgz twitch-cloud-ebs.tgz
- run: git log $(git describe --tags --abbrev=0 HEAD~1)..HEAD --oneline > changelog.txt
- uses: softprops/action-gh-release@v2
with:
make_latest: true
body_path: changelog.txt
files: '*.tgz'
- run: npm version from-git --allow-same-version --no-git-tag-version
- run: npm config set registry https://gitea.sugoidogo.com/api/packages/sugoidogo/npm/
- run: npm config set -- '//gitea.sugoidogo.com/api/packages/sugoidogo/npm/:_authToken' "$PACKAGES_KEY"
env:
PACKAGES_KEY: ${{ secrets.PACKAGES_KEY }}
- run: npm publish
+9 -11
View File
@@ -5,30 +5,28 @@ This is a server-side component providing cloud storage and [OAuth Code Grant Fl
### Cloudflare Workers
[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https%3A%2F%2Fgithub.com%2Fsugoidogo%2Ftwitch-cloud-ebs)
Previously, there was a "deploy to cloudflare" button here, but it seems to only work with GitHub repos, so you'll have to clone and deploy this yourself.
### Docker
This worker requires that the KV store is pre-populated with client-id keys and client-secret values, but doesn't provide any mechanism for this to work with selflare. As such, the docker container may not be usable until this is resolved. PRs are welcome.
This worker is also availible as a docker container at [ghcr.io/sugoidogo/twitch-cloud-ebs](https://github.com/sugoidogo/twitch-clips-consent-api/pkgs/container/twitch-clips-consent-api).
It listens on HTTP port 8080 and requires mounts for `/worker/cache`, `/worker/kv`, `/worker/d1`, and `/worker/r2`.
The r2 and kv mounts are the only ones actually used, but the selflare runtime requires all of them regardless.
Previously, this repo produced oci images, but they were inclomlete and more work is needed before they are ready.
## Twurple Module
https://gitea.sugoidogo.com/sugoidogo/-/packages/npm/@sugoidogo%2Ftwitch-cloud-ebs/
This API provides two Twurple modules, one for authorization and one for storage, allowing for easy integration if you're already using the Twurple javascript library.
the WebStorage module can be used to cache fetched resources from anywhere, but when used as demonstrated below, allows your client to maintain access to previously stored resources from cloud storage via the browser's CacheStorage API.
<details><summary>javascript</summary>
```javascript
import SugoiAuthProvider from 'https://ebs.domain.com/SugoiAuthProvider.js'
import WebStorage from 'https://ebs.domain.com/WebStorage.js'
import { ApiClient } from 'https://cdn.jsdelivr.net/npm/@twurple/api@7/+esm';
import { SugoiAuthProvider, WebStorage } from '@sugoidogo/twitch-cloud-ebs'
import { ApiClient } from '@twurple/ebs';
const authProvicder=new SugoiAuthProvider('your-client-id')
const webStorage=new WebStorage(authProvider)
const ebs_host='https://your.ebs.hostname'
const authProvicder=new SugoiAuthProvider('your-client-id',ebs_host)
const webStorage=new WebStorage(authProvider,undefined,ebs_host)
const apiClient = new ApiClient({ authProvider });
const config=await webStorage.fetch('config.json').then(response=>response.json())
+65 -2
View File
@@ -1,14 +1,15 @@
{
"name": "twitch-cloud-ebs",
"name": "@sugoidogo/twitch-cloud-ebs",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "twitch-cloud-ebs",
"name": "@sugoidogo/twitch-cloud-ebs",
"version": "0.0.0",
"devDependencies": {
"@sugoidogo/selflare": "^1.1.3",
"@twurple/api": "^7.4.0",
"@twurple/auth": "^7.4.0",
"fetch-retry": "^6.0.0",
"typescript": "^5.9.3",
@@ -57,6 +58,17 @@
"node": ">=12"
}
},
"node_modules/@d-fischer/cache-decorators": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@d-fischer/cache-decorators/-/cache-decorators-4.0.1.tgz",
"integrity": "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@d-fischer/shared-utils": "^3.6.3",
"tslib": "^2.6.2"
}
},
"node_modules/@d-fischer/cross-fetch": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/@d-fischer/cross-fetch/-/cross-fetch-5.0.5.tgz",
@@ -102,6 +114,21 @@
"url": "https://github.com/sponsors/d-fischer"
}
},
"node_modules/@d-fischer/rate-limiter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@d-fischer/rate-limiter/-/rate-limiter-1.1.0.tgz",
"integrity": "sha512-O5HgACwApyCZhp4JTEBEtbv/W3eAwEkrARFvgWnEsDmXgCMWjIHwohWoHre5BW6IYXFSHBGsuZB/EvNL3942kQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@d-fischer/logger": "^4.2.3",
"@d-fischer/shared-utils": "^3.6.3",
"tslib": "^2.6.2"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
}
},
"node_modules/@d-fischer/shared-utils": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/@d-fischer/shared-utils/-/shared-utils-3.6.4.tgz",
@@ -1741,6 +1768,32 @@
"stacktracey": "^2.1.8"
}
},
"node_modules/@twurple/api": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@twurple/api/-/api-7.4.0.tgz",
"integrity": "sha512-RlXLs4ZvS8n0+iIk7YyVDwrjhlwpn+N+h7fX5Q61HoxlmzoCShmnnFo03abYw9i8Cc3deGpbQATOSVmigXM4qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@d-fischer/cache-decorators": "^4.0.0",
"@d-fischer/cross-fetch": "^5.0.1",
"@d-fischer/detect-node": "^3.0.1",
"@d-fischer/logger": "^4.2.1",
"@d-fischer/rate-limiter": "^1.1.0",
"@d-fischer/shared-utils": "^3.6.1",
"@d-fischer/typed-event-emitter": "^3.3.1",
"@twurple/api-call": "7.4.0",
"@twurple/common": "7.4.0",
"retry": "^0.13.1",
"tslib": "^2.0.3"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"@twurple/auth": "7.4.0"
}
},
"node_modules/@twurple/api-call": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@twurple/api-call/-/api-call-7.4.0.tgz",
@@ -2393,6 +2446,16 @@
"node": ">=0.10.0"
}
},
"node_modules/retry": {
"version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/rollup-plugin-inject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
+2 -1
View File
@@ -1,5 +1,5 @@
{
"name": "twitch-cloud-ebs",
"name": "@sugoidogo/twitch-cloud-ebs",
"version": "0.0.0",
"files": [
"static",
@@ -13,6 +13,7 @@
},
"devDependencies": {
"@sugoidogo/selflare": "^1.1.3",
"@twurple/api": "^7.4.0",
"@twurple/auth": "^7.4.0",
"fetch-retry": "^6.0.0",
"typescript": "^5.9.3",
+75
View File
@@ -0,0 +1,75 @@
import type { AccessTokenMaybeWithUserId, AccessTokenWithUserId, AuthProvider } from "@twurple/auth";
import type { UserIdResolvable } from "@twurple/api"
import * as TwitchAuth from "./TwitchAuth";
async function ensureUserId(token: TwitchAuth.TwitchToken | null): Promise<TwitchAuth.TwitchToken | null> {
if (!token) return null
if (!token.user_id) {
const refresh_token = token.refresh_token
token = await TwitchAuth.validateToken(token.access_token)
token.refresh_token = refresh_token
}
return token
}
async function getTwurpleProxy(token: TwitchAuth.TwitchToken): Promise<AccessTokenWithUserId> {
token=await ensureUserId(token)
return new Proxy(token, {
get(target, name, receiver) {
return target[name.toString().replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`) as keyof TwitchAuth.TwitchToken]
}
}) as unknown as AccessTokenWithUserId
}
export class SugoiAuthProvider implements AuthProvider {
#token: TwitchAuth.TwitchToken | Promise<TwitchAuth.TwitchToken> | Promise<TwitchAuth.TwitchToken | null> | null = null;
clientId: string;
constructor(clientId:string,proxyOrigin?:string) {
this.clientId = clientId;
if (proxyOrigin) TwitchAuth.setProxyOrigin(proxyOrigin);
this.#token = TwitchAuth.getUserTokenPassive(clientId).then(ensureUserId)
}
#authorize(scopes: string[]) {
const promise = TwitchAuth.requestAuthCode(this.clientId, ...scopes)
this.#token = promise
return promise
}
/**
* Checks cached token for given scopes, and redirects to authorization page on failure.
* You should only call this from your configured redirect uri.
*
* https://dev.twitch.tv/docs/authentication/getting-tokens-oauth/#get-the-user-to-authorize-your-app
*/
async getAuthorization(...scopes: string[]): Promise<void> {
const token = await this.#token
if(!token) return this.#authorize(scopes)
for (const scope of scopes) {
if (!token.scope!.includes(scope)) {
return this.#authorize(scopes)
}
}
}
getCurrentScopesForUser(user: UserIdResolvable):string[] {
if (!this.#token || this.#token instanceof Promise) throw new Error("unauthorized")
return this.#token.scope || []
}
async getAccessTokenForUser(user: UserIdResolvable, ...scopeSets: (string[] | undefined)[]): Promise<AccessTokenWithUserId | null> {
const token = await this.#token
if (!token) return null
return getTwurpleProxy(token)
}
async getAnyAccessToken(user?: UserIdResolvable): Promise<AccessTokenMaybeWithUserId> {
const token = await this.#token
if (!token) throw new Error("unauthorized")
return getTwurpleProxy(token)
}
async refreshAccessTokenForUser(user: UserIdResolvable): Promise<AccessTokenWithUserId> {
const token=await this.#token
if (!token) throw new Error("unauthorized")
if (!token.refresh_token) throw new Error("missing refresh token")
this.#token = await TwitchAuth.refreshToken(this.clientId, token.refresh_token).then(ensureUserId)
return getTwurpleProxy(this.#token!)
}
}
export default SugoiAuthProvider
+6 -6
View File
@@ -86,7 +86,7 @@ export function requestAccessToken(client_id: string, ...scopes: Array<string>)
* @param {Array<string>|string} scopes
* @returns {Promise<AuthCode>}
*/
export function requestAuthCode(client_id: string, ...scopes: Array<string>): Promise<any> {
export function requestAuthCode(client_id: string, ...scopes: Array<string>): Promise<never> {
console.debug('requesting authorization code')
const url = new URL('https://id.twitch.tv/oauth2/authorize')
url.searchParams.append('response_type', 'code')
@@ -178,7 +178,7 @@ export function refreshToken(client_id: string, refresh_token: string): Promise<
* @param {string} client_id
* @returns {TwitchToken}
*/
export async function getUserToken(client_id: string, ...scopes): Promise<TwitchToken> {
export async function getUserToken(client_id: string, ...scopes:string[]): Promise<TwitchToken> {
const token=await getUserTokenPassive(client_id, ...scopes)
if(!token){
return requestAuthCode(client_id, ...scopes)
@@ -193,19 +193,19 @@ export async function getUserToken(client_id: string, ...scopes): Promise<Twitch
* @param {string} client_id
* @returns {TwitchToken}
*/
export async function getUserTokenPassive(client_id: string, ...scopes): Promise<TwitchToken>{
export async function getUserTokenPassive(client_id: string, ...scopes:string[]): Promise<TwitchToken|null>{
console.debug('scopes requested:',...scopes)
const params = new URLSearchParams(location.search + '&' + location.hash.substring(1))
if (params.has('code')) {
const code = params.get('code')
const code = params.get('code')!
history.replaceState(null, '', redirect_uri)
return exchangeCode(client_id, code)
}
if (params.has('refresh_token')) {
return refreshToken(client_id, params.get('refresh_token'))
return refreshToken(client_id, params.get('refresh_token')!)
}
if (params.has('access_token')) {
return validateToken(params.get('access_token'))
return validateToken(params.get('access_token')!)
}
if (params.has('error')) {
const error_message = params.get('error') + ': ' + params.get('error_description')
+1 -1
View File
@@ -20,7 +20,7 @@ export default class WebStorage {
* @param {import('@twurple/auth').AuthProvider} auth_provider used to add the authentication header to requests for web storage
* @param {fetch} fetch defaults to `globalThis.fetch`, allows you to further customize fetch behavior via chaining, for example with `fetch-retry`
*/
constructor(auth_provider: import('@twurple/auth').AuthProvider, fetch: typeof globalThis.fetch = globalThis.fetch, origin?: string) {
constructor(auth_provider: import('@twurple/auth').AuthProvider, fetch: typeof globalThis.fetch = (input, init) => globalThis.fetch(input, init), origin?: string) {
this.#fetch = fetch
this.#auth_provider = auth_provider
auth_provider.getAccessTokenForUser(undefined)
+4 -1
View File
@@ -1,2 +1,5 @@
export { default as SugoiAuthProvider } from "./SugoiAuthProvider"
export { default as WebStorage } from "./WebStorage"
export { default as SugoiAuthProvider2 } from "./SugoiAuthProvider2"
export { default as WebStorage } from "./WebStorage"
export * as TwitchAuth from "./TwitchAuth"
export * as tba from "./tba"
+2
View File
@@ -69,4 +69,6 @@ export async function get_tokens(client_id: string,scope='',redirect_uri=locatio
return token
}
export function setProxyOrigin(proxy_origin:string) { TwitchAuth.setProxyOrigin(proxy_origin) }
export default get_tokens
+1
View File
@@ -3,6 +3,7 @@
"target": "es2020",
"module": "es2020",
"moduleResolution": "node",
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"emitDeclarationOnly": true,
"declaration": true,