Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 13x 13x 13x 13x 13x 13x 13x 13x 12x 9x 13x 13x 13x 13x 13x 4x 2x 13x 1x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 5x 3x 5x 5x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 1x 1x 6x 3x 3x 3x 6x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 5x 5x 5x 5x 1x 1x 1x 5x 5x 5x 5x 5x 5x 5x 5x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 2x 2x 3x | import { randomBytes, timingSafeEqual } from "node:crypto"
import { createServer } from "node:http"
import type { TwitchCredentials } from "@workspace/env/twitch"
import { BOT_SCOPES, TwitchApiService } from "../services/TwitchApiService"
import { createGrant, GrantStore } from "./grantStore"
type OperatorConfig = TwitchCredentials & { TWITCH_BOT_REDIRECT_URI: string }
export type CallbackResult =
| { status: "code"; code: string }
| { status: "denied" }
| { status: "invalid" }
export function authorizationUrl(
config: OperatorConfig,
state: string
): string {
const url = new URL("https://id.twitch.tv/oauth2/authorize")
url.search = new URLSearchParams({
client_id: config.TWITCH_CLIENT_ID,
redirect_uri: config.TWITCH_BOT_REDIRECT_URI,
response_type: "code",
scope: BOT_SCOPES.join(" "),
state,
force_verify: "true",
}).toString()
return url.href
}
export function parseAuthorizationCallback(
url: URL,
expectedState: string
): CallbackResult {
const state = url.searchParams.get("state") ?? ""
const actual = Buffer.from(state)
const expected = Buffer.from(expectedState)
if (
url.pathname !== "/callback" ||
actual.length !== expected.length ||
!timingSafeEqual(actual, expected)
)
return { status: "invalid" }
if (url.searchParams.has("error")) return { status: "denied" }
const code = url.searchParams.get("code")
return code && code.length <= 2048
? { status: "code", code }
: { status: "invalid" }
}
export async function waitForBotCode(
redirectUri: string,
state: string,
showUrl: () => void,
timeoutMs = 300000
): Promise<string> {
const redirect = new URL(redirectUri)
return new Promise((resolve, reject) => {
const finish = (result: { code: string } | { error: Error }) => {
clearTimeout(timer)
server.close()
if ("code" in result) resolve(result.code)
else reject(result.error)
}
const server = createServer((request, response) => {
response.setHeader("Cache-Control", "no-store")
response.setHeader("Content-Type", "text/plain; charset=utf-8")
response.setHeader("Referrer-Policy", "no-referrer")
let result: CallbackResult = { status: "invalid" }
try {
result = parseAuthorizationCallback(
new URL(String(request.url), redirect),
state
)
} catch {
/* Reject malformed callback targets. */
}
if (request.method !== "GET" || result.status === "invalid") {
response.writeHead(403).end("Invalid authorization callback.")
return
}
if (result.status === "denied") {
response
.writeHead(400)
.end("Authorization was denied. Close this window.")
finish({ error: new Error("Bot authorization denied.") })
return
}
response
.writeHead(200)
.end(
"Authorization received. Check the local operator command for the result, then close this window."
)
finish({ code: result.code })
})
server.headersTimeout = 5000
server.requestTimeout = 10000
server.on("error", () =>
finish({
error: new Error("Cannot bind the operator callback listener."),
})
)
const timer = setTimeout(
() => finish({ error: new Error("Bot authorization timed out.") }),
timeoutMs
)
server.listen(Number(redirect.port), "localhost", showUrl)
})
}
export async function authorizeBot(
config: OperatorConfig,
dependencies: {
api: TwitchApiService
store: GrantStore
showUrl: (url: string) => void
waitForCode?: typeof waitForBotCode
}
): Promise<void> {
const state = randomBytes(32).toString("hex")
const url = authorizationUrl(config, state)
const code = await (dependencies.waitForCode ?? waitForBotCode)(
config.TWITCH_BOT_REDIRECT_URI,
state,
() => dependencies.showUrl(url)
)
const token = await dependencies.api.exchangeBotCode(
code,
config.TWITCH_BOT_REDIRECT_URI
)
await dependencies.api.validateBotToken(token.access_token)
await dependencies.store.locked(() =>
dependencies.store.write(createGrant(token, config), false)
)
}
|