chore: generate
This commit is contained in:
parent
b32debb8a3
commit
6602341c0d
2 changed files with 137 additions and 49 deletions
|
|
@ -118,7 +118,10 @@ function authHeaders() {
|
||||||
// to make trust decisions, so unsigned decode is safe. Returns false for
|
// to make trust decisions, so unsigned decode is safe. Returns false for
|
||||||
// opaque tokens (no JWT shape), which conservatively skips the proactive
|
// opaque tokens (no JWT shape), which conservatively skips the proactive
|
||||||
// refresh and lets the 401-on-call path drive the refresh instead.
|
// refresh and lets the 401-on-call path drive the refresh instead.
|
||||||
export function accessTokenIsExpiring(token: string | undefined, skewMs: number = ACCESS_TOKEN_REFRESH_SKEW_MS): boolean {
|
export function accessTokenIsExpiring(
|
||||||
|
token: string | undefined,
|
||||||
|
skewMs: number = ACCESS_TOKEN_REFRESH_SKEW_MS,
|
||||||
|
): boolean {
|
||||||
if (!token || typeof token !== "string") return false
|
if (!token || typeof token !== "string") return false
|
||||||
const parts = token.split(".")
|
const parts = token.split(".")
|
||||||
if (parts.length < 2) return false
|
if (parts.length < 2) return false
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test } from "bun:test"
|
||||||
import { accessTokenIsExpiring, buildAuthorizeUrl, escapeHtml, pollDeviceCodeToken, requestDeviceCode, XaiAuthPlugin } from "../../src/plugin/xai"
|
import {
|
||||||
|
accessTokenIsExpiring,
|
||||||
|
buildAuthorizeUrl,
|
||||||
|
escapeHtml,
|
||||||
|
pollDeviceCodeToken,
|
||||||
|
requestDeviceCode,
|
||||||
|
XaiAuthPlugin,
|
||||||
|
} from "../../src/plugin/xai"
|
||||||
import { OAUTH_DUMMY_KEY } from "../../src/auth"
|
import { OAUTH_DUMMY_KEY } from "../../src/auth"
|
||||||
|
|
||||||
function makeJwt(payload: object): string {
|
function makeJwt(payload: object): string {
|
||||||
|
|
@ -113,7 +120,9 @@ describe("plugin.xai", () => {
|
||||||
test("returns no options unless stored auth is OAuth and exposes methods in order", async () => {
|
test("returns no options unless stored auth is OAuth and exposes methods in order", async () => {
|
||||||
const hooks = await XaiAuthPlugin({} as any)
|
const hooks = await XaiAuthPlugin({} as any)
|
||||||
expect(await hooks.auth!.loader!(async () => ({ type: "api", key: "sk-test" }), {} as any)).toEqual({})
|
expect(await hooks.auth!.loader!(async () => ({ type: "api", key: "sk-test" }), {} as any)).toEqual({})
|
||||||
expect(await hooks.auth!.loader!(async () => ({ type: "wellknown", key: "k", token: "t" }) as any, {} as any)).toEqual({})
|
expect(
|
||||||
|
await hooks.auth!.loader!(async () => ({ type: "wellknown", key: "k", token: "t" }) as any, {} as any),
|
||||||
|
).toEqual({})
|
||||||
expect(hooks.auth!.methods.map((m) => [m.type, m.label])).toEqual([
|
expect(hooks.auth!.methods.map((m) => [m.type, m.label])).toEqual([
|
||||||
["oauth", "xAI Grok OAuth (SuperGrok Subscription)"],
|
["oauth", "xAI Grok OAuth (SuperGrok Subscription)"],
|
||||||
["oauth", "xAI Grok OAuth (Headless / Remote / VPS)"],
|
["oauth", "xAI Grok OAuth (Headless / Remote / VPS)"],
|
||||||
|
|
@ -152,12 +161,17 @@ describe("plugin.xai", () => {
|
||||||
captured.push(request.headers)
|
captured.push(request.headers)
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const opts = await (await XaiAuthPlugin(input)).auth!.loader!(
|
const opts = await (
|
||||||
|
await XaiAuthPlugin(input)
|
||||||
|
).auth!.loader!(
|
||||||
async () => ({ type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }),
|
async () => ({ type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }),
|
||||||
{} as any,
|
{} as any,
|
||||||
)
|
)
|
||||||
|
|
||||||
const objHeaders: Record<string, string> = { Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "x-trace": "plain-object" }
|
const objHeaders: Record<string, string> = {
|
||||||
|
Authorization: `Bearer ${OAUTH_DUMMY_KEY}`,
|
||||||
|
"x-trace": "plain-object",
|
||||||
|
}
|
||||||
await opts.fetch!(new URL("/chat/completions", server.url), { headers: objHeaders })
|
await opts.fetch!(new URL("/chat/completions", server.url), { headers: objHeaders })
|
||||||
expect(objHeaders).toEqual({ Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "x-trace": "plain-object" })
|
expect(objHeaders).toEqual({ Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "x-trace": "plain-object" })
|
||||||
|
|
||||||
|
|
@ -170,7 +184,11 @@ describe("plugin.xai", () => {
|
||||||
await opts.fetch!(new URL("/chat/completions", server.url), { headers: headersInstance })
|
await opts.fetch!(new URL("/chat/completions", server.url), { headers: headersInstance })
|
||||||
expect(headersInstance.get("x-trace")).toBe("headers-instance")
|
expect(headersInstance.get("x-trace")).toBe("headers-instance")
|
||||||
|
|
||||||
expect(captured.map((headers) => headers.get("x-trace"))).toEqual(["plain-object", "tuple-array", "headers-instance"])
|
expect(captured.map((headers) => headers.get("x-trace"))).toEqual([
|
||||||
|
"plain-object",
|
||||||
|
"tuple-array",
|
||||||
|
"headers-instance",
|
||||||
|
])
|
||||||
for (const headers of captured) {
|
for (const headers of captured) {
|
||||||
expect(headers.get("authorization")).toBe("Bearer tok")
|
expect(headers.get("authorization")).toBe("Bearer tok")
|
||||||
expect(headers.get("user-agent")).toMatch(/^opencode\//)
|
expect(headers.get("user-agent")).toMatch(/^opencode\//)
|
||||||
|
|
@ -184,14 +202,20 @@ describe("plugin.xai", () => {
|
||||||
captured.push(request.headers)
|
captured.push(request.headers)
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const opts = await (await XaiAuthPlugin(input)).auth!.loader!(
|
const opts = await (
|
||||||
|
await XaiAuthPlugin(input)
|
||||||
|
).auth!.loader!(
|
||||||
async () => ({ type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }),
|
async () => ({ type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }),
|
||||||
{} as any,
|
{} as any,
|
||||||
)
|
)
|
||||||
|
|
||||||
await opts.fetch!(
|
await opts.fetch!(
|
||||||
new Request(new URL("/chat/completions", server.url), {
|
new Request(new URL("/chat/completions", server.url), {
|
||||||
headers: { Authorization: `Bearer ${OAUTH_DUMMY_KEY}`, "content-type": "application/json", "x-trace": "request" },
|
headers: {
|
||||||
|
Authorization: `Bearer ${OAUTH_DUMMY_KEY}`,
|
||||||
|
"content-type": "application/json",
|
||||||
|
"x-trace": "request",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{ headers: { "x-trace": "init", "x-extra": "yes" } },
|
{ headers: { "x-trace": "init", "x-extra": "yes" } },
|
||||||
)
|
)
|
||||||
|
|
@ -210,7 +234,9 @@ describe("plugin.xai", () => {
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
let firstCall = true
|
let firstCall = true
|
||||||
const opts = await (await XaiAuthPlugin(input)).auth!.loader!(async () => {
|
const opts = await (
|
||||||
|
await XaiAuthPlugin(input)
|
||||||
|
).auth!.loader!(async () => {
|
||||||
if (firstCall) {
|
if (firstCall) {
|
||||||
firstCall = false
|
firstCall = false
|
||||||
return { type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }
|
return { type: "oauth", access: "tok", refresh: "rt", expires: Date.now() + 3600_000 }
|
||||||
|
|
@ -239,10 +265,9 @@ describe("plugin.xai", () => {
|
||||||
apiRequests.push(request.headers)
|
apiRequests.push(request.headers)
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const opts = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const opts = await (
|
||||||
async () => ({ type: "oauth" as const, access: "old", refresh: "rt-old", expires: 0 }),
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
{} as any,
|
).auth!.loader!(async () => ({ type: "oauth" as const, access: "old", refresh: "rt-old", expires: 0 }), {} as any)
|
||||||
)
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
opts.fetch!(new URL("/chat/completions", server.url), { headers: {} }),
|
opts.fetch!(new URL("/chat/completions", server.url), { headers: {} }),
|
||||||
|
|
@ -250,7 +275,10 @@ describe("plugin.xai", () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(tokenRequests).toBe(1)
|
expect(tokenRequests).toBe(1)
|
||||||
expect(apiRequests.map((headers) => headers.get("authorization"))).toEqual(["Bearer new-access", "Bearer new-access"])
|
expect(apiRequests.map((headers) => headers.get("authorization"))).toEqual([
|
||||||
|
"Bearer new-access",
|
||||||
|
"Bearer new-access",
|
||||||
|
])
|
||||||
expect(setCalls).toHaveLength(1)
|
expect(setCalls).toHaveLength(1)
|
||||||
expect((setCalls[0].body as any).refresh).toBe("rt-new")
|
expect((setCalls[0].body as any).refresh).toBe("rt-new")
|
||||||
})
|
})
|
||||||
|
|
@ -264,14 +292,24 @@ describe("plugin.xai", () => {
|
||||||
const refreshToken = new URLSearchParams(await request.text()).get("refresh_token")!
|
const refreshToken = new URLSearchParams(await request.text()).get("refresh_token")!
|
||||||
tokenRequests.push(refreshToken)
|
tokenRequests.push(refreshToken)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||||
return Response.json({ access_token: `access-${refreshToken}`, refresh_token: `next-${refreshToken}`, expires_in: 3600 })
|
return Response.json({
|
||||||
|
access_token: `access-${refreshToken}`,
|
||||||
|
refresh_token: `next-${refreshToken}`,
|
||||||
|
expires_in: 3600,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
apiRequests.push(request.headers.get("authorization")!)
|
apiRequests.push(request.headers.get("authorization")!)
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const hooks = await XaiAuthPlugin(input, serverOptions(server))
|
const hooks = await XaiAuthPlugin(input, serverOptions(server))
|
||||||
const first = await hooks.auth!.loader!(async () => ({ type: "oauth", access: "old-a", refresh: "rt-a", expires: 0 }), {} as any)
|
const first = await hooks.auth!.loader!(
|
||||||
const second = await hooks.auth!.loader!(async () => ({ type: "oauth", access: "old-b", refresh: "rt-b", expires: 0 }), {} as any)
|
async () => ({ type: "oauth", access: "old-a", refresh: "rt-a", expires: 0 }),
|
||||||
|
{} as any,
|
||||||
|
)
|
||||||
|
const second = await hooks.auth!.loader!(
|
||||||
|
async () => ({ type: "oauth", access: "old-b", refresh: "rt-b", expires: 0 }),
|
||||||
|
{} as any,
|
||||||
|
)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
first.fetch!(new URL("/chat/completions", server.url), { headers: {} }),
|
first.fetch!(new URL("/chat/completions", server.url), { headers: {} }),
|
||||||
|
|
@ -289,17 +327,22 @@ describe("plugin.xai", () => {
|
||||||
if (url.pathname === "/oauth2/token") {
|
if (url.pathname === "/oauth2/token") {
|
||||||
tokenRequests++
|
tokenRequests++
|
||||||
if (tokenRequests === 2) return new Response("temporarily unavailable", { status: 503 })
|
if (tokenRequests === 2) return new Response("temporarily unavailable", { status: 503 })
|
||||||
return Response.json({ access_token: `new-${tokenRequests}`, refresh_token: `rt-${tokenRequests}`, expires_in: 3600 })
|
return Response.json({
|
||||||
|
access_token: `new-${tokenRequests}`,
|
||||||
|
refresh_token: `rt-${tokenRequests}`,
|
||||||
|
expires_in: 3600,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const opts = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const opts = await (
|
||||||
async () => ({ type: "oauth", access: "old", refresh: "rt-old", expires: 0 }),
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
{} as any,
|
).auth!.loader!(async () => ({ type: "oauth", access: "old", refresh: "rt-old", expires: 0 }), {} as any)
|
||||||
)
|
|
||||||
|
|
||||||
await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
await expect(opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })).rejects.toThrow(/xAI token refresh failed \(503\)/)
|
await expect(opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })).rejects.toThrow(
|
||||||
|
/xAI token refresh failed \(503\)/,
|
||||||
|
)
|
||||||
await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
expect(tokenRequests).toBe(3)
|
expect(tokenRequests).toBe(3)
|
||||||
})
|
})
|
||||||
|
|
@ -312,10 +355,9 @@ describe("plugin.xai", () => {
|
||||||
captured.push(request.headers)
|
captured.push(request.headers)
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const opts = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const opts = await (
|
||||||
async () => ({ type: "oauth", access: "old", refresh: "rt-old", expires: 0 }),
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
{} as any,
|
).auth!.loader!(async () => ({ type: "oauth", access: "old", refresh: "rt-old", expires: 0 }), {} as any)
|
||||||
)
|
|
||||||
|
|
||||||
const resp = await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
const resp = await opts.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
expect(resp.status).toBe(200)
|
expect(resp.status).toBe(200)
|
||||||
|
|
@ -333,7 +375,9 @@ describe("plugin.xai", () => {
|
||||||
}
|
}
|
||||||
return new Response("{}", { status: 200 })
|
return new Response("{}", { status: 200 })
|
||||||
})
|
})
|
||||||
const fresh = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const fresh = await (
|
||||||
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
|
).auth!.loader!(
|
||||||
async () => ({
|
async () => ({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
access: makeJwt({ exp: Math.floor(Date.now() / 1000) + 24 * 3600 }),
|
access: makeJwt({ exp: Math.floor(Date.now() / 1000) + 24 * 3600 }),
|
||||||
|
|
@ -345,7 +389,9 @@ describe("plugin.xai", () => {
|
||||||
await fresh.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
await fresh.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
expect(tokenRequests).toBe(0)
|
expect(tokenRequests).toBe(0)
|
||||||
|
|
||||||
const jwtExpiring = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const jwtExpiring = await (
|
||||||
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
|
).auth!.loader!(
|
||||||
async () => ({
|
async () => ({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
access: makeJwt({ exp: Math.floor((Date.now() + 30_000) / 1000) }),
|
access: makeJwt({ exp: Math.floor((Date.now() + 30_000) / 1000) }),
|
||||||
|
|
@ -354,10 +400,9 @@ describe("plugin.xai", () => {
|
||||||
}),
|
}),
|
||||||
{} as any,
|
{} as any,
|
||||||
)
|
)
|
||||||
const missingExpires = await (await XaiAuthPlugin(input, serverOptions(server))).auth!.loader!(
|
const missingExpires = await (
|
||||||
async () => ({ type: "oauth", access: "opaque-token", refresh: "rt", expires: 0 }),
|
await XaiAuthPlugin(input, serverOptions(server))
|
||||||
{} as any,
|
).auth!.loader!(async () => ({ type: "oauth", access: "opaque-token", refresh: "rt", expires: 0 }), {} as any)
|
||||||
)
|
|
||||||
await jwtExpiring.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
await jwtExpiring.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
await missingExpires.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
await missingExpires.fetch!(new URL("/chat/completions", server.url), { headers: {} })
|
||||||
expect(tokenRequests).toBe(2)
|
expect(tokenRequests).toBe(2)
|
||||||
|
|
@ -366,10 +411,9 @@ describe("plugin.xai", () => {
|
||||||
|
|
||||||
test("network failure during refresh surfaces the underlying fetch error", async () => {
|
test("network failure during refresh surfaces the underlying fetch error", async () => {
|
||||||
const { input } = makeInput()
|
const { input } = makeInput()
|
||||||
const opts = await (await XaiAuthPlugin(input, { tokenUrl: "http://127.0.0.1:9/oauth2/token" })).auth!.loader!(
|
const opts = await (
|
||||||
async () => ({ type: "oauth", access: "old", refresh: "rt", expires: 0 }),
|
await XaiAuthPlugin(input, { tokenUrl: "http://127.0.0.1:9/oauth2/token" })
|
||||||
{} as any,
|
).auth!.loader!(async () => ({ type: "oauth", access: "old", refresh: "rt", expires: 0 }), {} as any)
|
||||||
)
|
|
||||||
|
|
||||||
await expect(opts.fetch!("https://api.x.ai/v1/chat/completions", { headers: {} })).rejects.toThrow()
|
await expect(opts.fetch!("https://api.x.ai/v1/chat/completions", { headers: {} })).rejects.toThrow()
|
||||||
})
|
})
|
||||||
|
|
@ -394,7 +438,10 @@ describe("plugin.xai", () => {
|
||||||
return new Response("unexpected request", { status: 500 })
|
return new Response("unexpected request", { status: 500 })
|
||||||
})
|
})
|
||||||
const hooks = await XaiAuthPlugin({} as any, serverOptions(server))
|
const hooks = await XaiAuthPlugin({} as any, serverOptions(server))
|
||||||
const headless = hooks.auth!.methods.find((m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)")!
|
const headless = hooks.auth!.methods.find(
|
||||||
|
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||||
|
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||||
|
)!
|
||||||
const result = await headless.authorize!()
|
const result = await headless.authorize!()
|
||||||
|
|
||||||
expect(result.method).toBe("auto")
|
expect(result.method).toBe("auto")
|
||||||
|
|
@ -407,12 +454,17 @@ describe("plugin.xai", () => {
|
||||||
test("authorize falls back to verification_uri when verification_uri_complete is absent", async () => {
|
test("authorize falls back to verification_uri when verification_uri_complete is absent", async () => {
|
||||||
using server = makeServer((_, url) => {
|
using server = makeServer((_, url) => {
|
||||||
if (url.pathname === "/oauth2/device/code") {
|
if (url.pathname === "/oauth2/device/code") {
|
||||||
return Response.json({ device_code: "DEVICE-2", user_code: "WXYZ-9876", verification_uri: "https://x.ai/device" })
|
return Response.json({
|
||||||
|
device_code: "DEVICE-2",
|
||||||
|
user_code: "WXYZ-9876",
|
||||||
|
verification_uri: "https://x.ai/device",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return new Response("unexpected request", { status: 500 })
|
return new Response("unexpected request", { status: 500 })
|
||||||
})
|
})
|
||||||
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
||||||
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||||
|
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||||
)!
|
)!
|
||||||
expect((await headless.authorize!()).url).toBe("https://x.ai/device")
|
expect((await headless.authorize!()).url).toBe("https://x.ai/device")
|
||||||
})
|
})
|
||||||
|
|
@ -436,8 +488,12 @@ describe("plugin.xai", () => {
|
||||||
expect(parsed.get("scope")).toContain("offline_access")
|
expect(parsed.get("scope")).toContain("offline_access")
|
||||||
expect(parsed.get("scope")).toContain("grok-cli:access")
|
expect(parsed.get("scope")).toContain("grok-cli:access")
|
||||||
expect(parsed.get("scope")).toContain("api:access")
|
expect(parsed.get("scope")).toContain("api:access")
|
||||||
await expect(requestDeviceCode({ deviceAuthorizationUrl: new URL("/error", server.url).toString() })).rejects.toThrow(/429.*rate limited/)
|
await expect(
|
||||||
await expect(requestDeviceCode({ deviceAuthorizationUrl: new URL("/missing", server.url).toString() })).rejects.toThrow(/missing device_code/)
|
requestDeviceCode({ deviceAuthorizationUrl: new URL("/error", server.url).toString() }),
|
||||||
|
).rejects.toThrow(/429.*rate limited/)
|
||||||
|
await expect(
|
||||||
|
requestDeviceCode({ deviceAuthorizationUrl: new URL("/missing", server.url).toString() }),
|
||||||
|
).rejects.toThrow(/missing device_code/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("pollDeviceCodeToken resolves on success and posts the device-code grant", async () => {
|
test("pollDeviceCodeToken resolves on success and posts the device-code grant", async () => {
|
||||||
|
|
@ -487,7 +543,13 @@ describe("plugin.xai", () => {
|
||||||
using server = makeServer(() => Response.json(body, { status: 500 }))
|
using server = makeServer(() => Response.json(body, { status: 500 }))
|
||||||
await expect(
|
await expect(
|
||||||
pollDeviceCodeToken(
|
pollDeviceCodeToken(
|
||||||
{ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: 1, expires_in: 600 },
|
{
|
||||||
|
device_code: "DC",
|
||||||
|
user_code: "UC",
|
||||||
|
verification_uri: "https://x.ai/device",
|
||||||
|
interval: 1,
|
||||||
|
expires_in: 600,
|
||||||
|
},
|
||||||
{ sleep: async () => {}, tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
{ sleep: async () => {}, tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
||||||
),
|
),
|
||||||
).rejects.toThrow(error)
|
).rejects.toThrow(error)
|
||||||
|
|
@ -498,7 +560,11 @@ describe("plugin.xai", () => {
|
||||||
await expect(
|
await expect(
|
||||||
pollDeviceCodeToken(
|
pollDeviceCodeToken(
|
||||||
{ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: 1, expires_in: 1 },
|
{ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: 1, expires_in: 1 },
|
||||||
{ sleep: async () => {}, now: () => 1_000_000 + tick++ * 600, tokenUrl: new URL("/oauth2/token", pending.url).toString() },
|
{
|
||||||
|
sleep: async () => {},
|
||||||
|
now: () => 1_000_000 + tick++ * 600,
|
||||||
|
tokenUrl: new URL("/oauth2/token", pending.url).toString(),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
).rejects.toThrow(/timed out/)
|
).rejects.toThrow(/timed out/)
|
||||||
})
|
})
|
||||||
|
|
@ -514,7 +580,13 @@ describe("plugin.xai", () => {
|
||||||
})
|
})
|
||||||
const sleeps: number[] = []
|
const sleeps: number[] = []
|
||||||
await pollDeviceCodeToken(
|
await pollDeviceCodeToken(
|
||||||
{ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: bad as number, expires_in: 600 },
|
{
|
||||||
|
device_code: "DC",
|
||||||
|
user_code: "UC",
|
||||||
|
verification_uri: "https://x.ai/device",
|
||||||
|
interval: bad as number,
|
||||||
|
expires_in: 600,
|
||||||
|
},
|
||||||
{ sleep: async (ms) => void sleeps.push(ms), tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
{ sleep: async (ms) => void sleeps.push(ms), tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
||||||
)
|
)
|
||||||
expect(sleeps[0]).toBe(8_000)
|
expect(sleeps[0]).toBe(8_000)
|
||||||
|
|
@ -525,7 +597,13 @@ describe("plugin.xai", () => {
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await pollDeviceCodeToken(
|
await pollDeviceCodeToken(
|
||||||
{ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: 1, expires_in: bad as number },
|
{
|
||||||
|
device_code: "DC",
|
||||||
|
user_code: "UC",
|
||||||
|
verification_uri: "https://x.ai/device",
|
||||||
|
interval: 1,
|
||||||
|
expires_in: bad as number,
|
||||||
|
},
|
||||||
{ sleep: async () => {}, tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
{ sleep: async () => {}, tokenUrl: new URL("/oauth2/token", server.url).toString() },
|
||||||
)
|
)
|
||||||
).access_token,
|
).access_token,
|
||||||
|
|
@ -536,14 +614,21 @@ describe("plugin.xai", () => {
|
||||||
test("device-code authorize callback returns failed when polling errors", async () => {
|
test("device-code authorize callback returns failed when polling errors", async () => {
|
||||||
using server = makeServer((_, url) => {
|
using server = makeServer((_, url) => {
|
||||||
if (url.pathname === "/oauth2/device/code") {
|
if (url.pathname === "/oauth2/device/code") {
|
||||||
return Response.json({ device_code: "DC", user_code: "UC", verification_uri: "https://x.ai/device", interval: 0, expires_in: 600 })
|
return Response.json({
|
||||||
|
device_code: "DC",
|
||||||
|
user_code: "UC",
|
||||||
|
verification_uri: "https://x.ai/device",
|
||||||
|
interval: 0,
|
||||||
|
expires_in: 600,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return Response.json({ error: "access_denied" }, { status: 400 })
|
return Response.json({ error: "access_denied" }, { status: 400 })
|
||||||
})
|
})
|
||||||
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
const headless = (await XaiAuthPlugin({} as any, serverOptions(server))).auth!.methods.find(
|
||||||
(m): m is Extract<typeof m, { type: "oauth" }> => m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
(m): m is Extract<typeof m, { type: "oauth" }> =>
|
||||||
|
m.type === "oauth" && m.label === "xAI Grok OAuth (Headless / Remote / VPS)",
|
||||||
)!
|
)!
|
||||||
expect(await (await headless.authorize!() as any).callback()).toEqual({ type: "failed" })
|
expect(await ((await headless.authorize!()) as any).callback()).toEqual({ type: "failed" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue