@opengeni/xai-subscription 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/oauth.ts ADDED
@@ -0,0 +1,335 @@
1
+ import {
2
+ OAUTH_MAX_RESPONSE_BYTES,
3
+ readResponseJsonBounded,
4
+ validateHttpUrl,
5
+ } from "@opengeni/network";
6
+
7
+ import { runBoundedXaiOperation } from "./bounded-operation";
8
+ import {
9
+ XAI_CLIENT_VERSION,
10
+ XAI_DEVICE_AUTHORIZATION_URL,
11
+ XAI_DEVICE_CODE_GRANT_TYPE,
12
+ XAI_OAUTH_CLIENT_ID,
13
+ XAI_OAUTH_OPERATION_TIMEOUT_MS,
14
+ XAI_OAUTH_SCOPES,
15
+ XAI_TOKEN_URL,
16
+ XAI_USERINFO_URL,
17
+ } from "./constants";
18
+ import {
19
+ XaiSubscriptionError,
20
+ XaiSubscriptionReloginRequired,
21
+ XaiSubscriptionTransientError,
22
+ } from "./errors";
23
+
24
+ export type XaiFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
25
+
26
+ export type XaiDeviceCode = {
27
+ deviceCode: string;
28
+ userCode: string;
29
+ verificationUri: string;
30
+ verificationUriComplete: string | null;
31
+ expiresInSeconds: number;
32
+ intervalSeconds: number;
33
+ };
34
+
35
+ export type XaiOAuthTokens = {
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ idToken: string | null;
39
+ tokenType: string | null;
40
+ scope: string | null;
41
+ expiresInSeconds: number;
42
+ };
43
+
44
+ export type XaiDevicePollResult =
45
+ | { status: "pending"; intervalSeconds: number }
46
+ | { status: "slow_down"; intervalSeconds: number }
47
+ | { status: "expired" }
48
+ | { status: "denied" }
49
+ | { status: "authorized"; tokens: XaiOAuthTokens };
50
+
51
+ export type XaiVerifiedIdentity = {
52
+ subject: string;
53
+ email: string | null;
54
+ emailVerified: boolean | null;
55
+ name: string | null;
56
+ };
57
+
58
+ type XaiOAuthOptions = {
59
+ fetch?: XaiFetch;
60
+ clientId?: string;
61
+ deviceAuthorizationUrl?: string;
62
+ tokenUrl?: string;
63
+ userinfoUrl?: string;
64
+ timeoutMs?: number;
65
+ };
66
+
67
+ function oauthHeaders(): Headers {
68
+ return new Headers({
69
+ accept: "application/json",
70
+ "content-type": "application/x-www-form-urlencoded",
71
+ "user-agent": `opengeni/${XAI_CLIENT_VERSION}`,
72
+ "x-grok-client-version": XAI_CLIENT_VERSION,
73
+ "x-grok-client-surface": "headless",
74
+ });
75
+ }
76
+
77
+ function positiveSeconds(value: unknown, fallback: number): number {
78
+ const number = Number(value);
79
+ return Number.isFinite(number) && number > 0 ? Math.ceil(number) : fallback;
80
+ }
81
+
82
+ function requireNonEmptyString(value: unknown, field: string): string {
83
+ if (typeof value !== "string" || value.length === 0) {
84
+ throw new XaiSubscriptionError("invalid_response", `xAI OAuth response is missing ${field}`);
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function nullableString(value: unknown): string | null {
90
+ return typeof value === "string" && value.length > 0 ? value : null;
91
+ }
92
+
93
+ function validateUserCode(userCode: string): void {
94
+ if (![...userCode].every((char) => /[A-Za-z0-9-]/.test(char))) {
95
+ throw new XaiSubscriptionError("invalid_response", "xAI returned an invalid user code");
96
+ }
97
+ }
98
+
99
+ function validateVerificationUri(uri: string): string {
100
+ try {
101
+ return validateHttpUrl(uri, {
102
+ allowLoopbackHttp: true,
103
+ label: "xAI device verification",
104
+ });
105
+ } catch {
106
+ throw new XaiSubscriptionError("invalid_response", "xAI returned an invalid verification URL");
107
+ }
108
+ }
109
+
110
+ async function boundedFetchJson(
111
+ label: string,
112
+ input: string,
113
+ init: RequestInit,
114
+ options: Pick<XaiOAuthOptions, "fetch" | "timeoutMs">,
115
+ ): Promise<{ response: Response; body: Record<string, unknown> }> {
116
+ const fetchImpl = options.fetch ?? fetch;
117
+ const timeoutMs = options.timeoutMs ?? XAI_OAUTH_OPERATION_TIMEOUT_MS;
118
+ const fetched = await runBoundedXaiOperation(async (signal) => {
119
+ const response = await fetchImpl(input, { ...init, signal });
120
+ const body = await readResponseJsonBounded<Record<string, unknown>>(
121
+ response,
122
+ OAUTH_MAX_RESPONSE_BYTES,
123
+ label,
124
+ { signal },
125
+ );
126
+ return { response, body };
127
+ }, timeoutMs);
128
+ if (!fetched.ok) {
129
+ throw new XaiSubscriptionTransientError(`xAI ${label} ${fetched.reason}`);
130
+ }
131
+ return fetched.value;
132
+ }
133
+
134
+ export async function requestXaiDeviceCode(options: XaiOAuthOptions = {}): Promise<XaiDeviceCode> {
135
+ const { response, body } = await boundedFetchJson(
136
+ "device code request",
137
+ options.deviceAuthorizationUrl ?? XAI_DEVICE_AUTHORIZATION_URL,
138
+ {
139
+ method: "POST",
140
+ headers: oauthHeaders(),
141
+ body: new URLSearchParams({
142
+ client_id: options.clientId ?? XAI_OAUTH_CLIENT_ID,
143
+ scope: XAI_OAUTH_SCOPES.join(" "),
144
+ referrer: "opengeni",
145
+ }).toString(),
146
+ },
147
+ options,
148
+ );
149
+ if (response.status === 404) {
150
+ throw new XaiSubscriptionError(
151
+ "not_enabled",
152
+ "SuperGrok device-code login is not enabled by xAI",
153
+ 404,
154
+ );
155
+ }
156
+ if (!response.ok) {
157
+ throw new XaiSubscriptionTransientError(
158
+ `xAI device code request failed (${response.status})`,
159
+ response.status,
160
+ );
161
+ }
162
+
163
+ const deviceCode = requireNonEmptyString(body.device_code, "device_code");
164
+ const userCode = requireNonEmptyString(body.user_code, "user_code");
165
+ const verificationUri = validateVerificationUri(
166
+ requireNonEmptyString(body.verification_uri, "verification_uri"),
167
+ );
168
+ const verificationUriComplete = nullableString(body.verification_uri_complete);
169
+ validateUserCode(userCode);
170
+
171
+ return {
172
+ deviceCode,
173
+ userCode,
174
+ verificationUri,
175
+ verificationUriComplete: verificationUriComplete
176
+ ? validateVerificationUri(verificationUriComplete)
177
+ : null,
178
+ expiresInSeconds: positiveSeconds(body.expires_in, 5 * 60),
179
+ intervalSeconds: Math.max(1, positiveSeconds(body.interval, 5)),
180
+ };
181
+ }
182
+
183
+ export async function pollXaiDeviceCode(
184
+ input: { deviceCode: string; intervalSeconds: number },
185
+ options: XaiOAuthOptions = {},
186
+ ): Promise<XaiDevicePollResult> {
187
+ const { response, body } = await boundedFetchJson(
188
+ "device token exchange",
189
+ options.tokenUrl ?? XAI_TOKEN_URL,
190
+ {
191
+ method: "POST",
192
+ headers: oauthHeaders(),
193
+ body: new URLSearchParams({
194
+ grant_type: XAI_DEVICE_CODE_GRANT_TYPE,
195
+ client_id: options.clientId ?? XAI_OAUTH_CLIENT_ID,
196
+ device_code: input.deviceCode,
197
+ }).toString(),
198
+ },
199
+ options,
200
+ );
201
+
202
+ if (response.ok) {
203
+ return { status: "authorized", tokens: parseTokenResponse(body, true) };
204
+ }
205
+ const code = nullableString(body.error);
206
+ if (code === "authorization_pending") {
207
+ return { status: "pending", intervalSeconds: Math.max(1, input.intervalSeconds) };
208
+ }
209
+ if (code === "slow_down") {
210
+ return { status: "slow_down", intervalSeconds: Math.max(1, input.intervalSeconds) + 5 };
211
+ }
212
+ if (code === "access_denied" || code === "authorization_denied") {
213
+ return { status: "denied" };
214
+ }
215
+ if (code === "expired_token") {
216
+ return { status: "expired" };
217
+ }
218
+ throw new XaiSubscriptionTransientError(
219
+ `xAI device token exchange failed (${response.status})`,
220
+ response.status,
221
+ );
222
+ }
223
+
224
+ export async function refreshXaiToken(
225
+ refreshToken: string,
226
+ options: XaiOAuthOptions = {},
227
+ ): Promise<XaiOAuthTokens> {
228
+ const { response, body } = await boundedFetchJson(
229
+ "token refresh",
230
+ options.tokenUrl ?? XAI_TOKEN_URL,
231
+ {
232
+ method: "POST",
233
+ headers: oauthHeaders(),
234
+ body: new URLSearchParams({
235
+ grant_type: "refresh_token",
236
+ refresh_token: refreshToken,
237
+ client_id: options.clientId ?? XAI_OAUTH_CLIENT_ID,
238
+ }).toString(),
239
+ },
240
+ options,
241
+ );
242
+ if (!response.ok) {
243
+ const code = nullableString(body.error);
244
+ if (
245
+ response.status === 401 ||
246
+ code === "invalid_grant" ||
247
+ code === "invalid_token" ||
248
+ code === "access_denied"
249
+ ) {
250
+ throw new XaiSubscriptionReloginRequired();
251
+ }
252
+ throw new XaiSubscriptionTransientError(
253
+ `xAI token refresh failed (${response.status})`,
254
+ response.status,
255
+ );
256
+ }
257
+ const parsed = parseTokenResponse(body, false);
258
+ return {
259
+ ...parsed,
260
+ refreshToken: parsed.refreshToken || refreshToken,
261
+ };
262
+ }
263
+
264
+ function parseTokenResponse(
265
+ body: Record<string, unknown>,
266
+ requireRefresh: boolean,
267
+ ): XaiOAuthTokens {
268
+ const accessToken = requireNonEmptyString(body.access_token, "access_token");
269
+ const refreshToken = nullableString(body.refresh_token);
270
+ if (requireRefresh && !refreshToken) {
271
+ throw new XaiSubscriptionError(
272
+ "invalid_response",
273
+ "xAI OAuth response is missing refresh_token",
274
+ );
275
+ }
276
+ return {
277
+ accessToken,
278
+ refreshToken: refreshToken ?? "",
279
+ idToken: nullableString(body.id_token),
280
+ tokenType: nullableString(body.token_type),
281
+ scope: nullableString(body.scope),
282
+ expiresInSeconds: positiveSeconds(body.expires_in, 60 * 60),
283
+ };
284
+ }
285
+
286
+ export async function fetchXaiVerifiedIdentity(
287
+ accessToken: string,
288
+ options: XaiOAuthOptions = {},
289
+ ): Promise<XaiVerifiedIdentity> {
290
+ const headers = new Headers({
291
+ accept: "application/json",
292
+ authorization: `Bearer ${accessToken}`,
293
+ "user-agent": `opengeni/${XAI_CLIENT_VERSION}`,
294
+ "x-grok-client-version": XAI_CLIENT_VERSION,
295
+ });
296
+ const { response, body } = await boundedFetchJson(
297
+ "userinfo request",
298
+ options.userinfoUrl ?? XAI_USERINFO_URL,
299
+ { method: "GET", headers },
300
+ options,
301
+ );
302
+ if (response.status === 401 || response.status === 403) {
303
+ throw new XaiSubscriptionReloginRequired();
304
+ }
305
+ if (!response.ok) {
306
+ throw new XaiSubscriptionTransientError(
307
+ `xAI userinfo request failed (${response.status})`,
308
+ response.status,
309
+ );
310
+ }
311
+ return {
312
+ subject: requireNonEmptyString(body.sub, "userinfo sub"),
313
+ email: nullableString(body.email),
314
+ emailVerified: typeof body.email_verified === "boolean" ? body.email_verified : null,
315
+ name: nullableString(body.name),
316
+ };
317
+ }
318
+
319
+ export function decodeXaiJwtPayload(jwt: string): Record<string, unknown> | null {
320
+ const payload = jwt.split(".")[1];
321
+ if (!payload) return null;
322
+ try {
323
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<
324
+ string,
325
+ unknown
326
+ >;
327
+ } catch {
328
+ return null;
329
+ }
330
+ }
331
+
332
+ export function xaiAccessTokenExpiry(accessToken: string): Date | null {
333
+ const payload = decodeXaiJwtPayload(accessToken);
334
+ return typeof payload?.exp === "number" ? new Date(payload.exp * 1_000) : null;
335
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1,86 @@
1
+ import { OAUTH_MAX_RESPONSE_BYTES, pinnedFetch, readResponseJsonBounded } from "@opengeni/network";
2
+
3
+ import { runBoundedXaiOperation } from "./bounded-operation";
4
+ import {
5
+ XAI_CLIENT_MODE,
6
+ XAI_CLIENT_VERSION,
7
+ XAI_SUBSCRIPTION_PROXY_BASE_URL,
8
+ XAI_TOKEN_AUTH_HEADER_VALUE,
9
+ } from "./constants";
10
+ import { XaiSubscriptionReloginRequired, XaiSubscriptionTransientError } from "./errors";
11
+ import type { XaiFetchLike } from "./fetch";
12
+ import type { XaiSubscriptionTokenSnapshot } from "./request-context";
13
+
14
+ export type XaiProxyAuthContext = {
15
+ clientVersion?: string;
16
+ getToken: () => Promise<XaiSubscriptionTokenSnapshot>;
17
+ refresh: () => Promise<XaiSubscriptionTokenSnapshot>;
18
+ };
19
+
20
+ const defaultProxyFetch: XaiFetchLike = async (input, init) =>
21
+ await pinnedFetch(
22
+ input,
23
+ init,
24
+ { environment: "production", integrationsAllowPrivateNetworkTargets: false },
25
+ { label: "xAI subscription proxy", requireHttpsOutsideLocalTest: true },
26
+ );
27
+
28
+ export async function fetchXaiProxyJson<T>(input: {
29
+ path: string;
30
+ context: XaiProxyAuthContext;
31
+ fetch?: XaiFetchLike;
32
+ timeoutMs?: number;
33
+ maxBytes?: number;
34
+ baseUrl?: string;
35
+ label: string;
36
+ }): Promise<T> {
37
+ const fetchImpl = input.fetch ?? defaultProxyFetch;
38
+ const timeoutMs = input.timeoutMs ?? 15_000;
39
+ const maxBytes = input.maxBytes ?? OAUTH_MAX_RESPONSE_BYTES;
40
+ const url = `${(input.baseUrl ?? XAI_SUBSCRIPTION_PROXY_BASE_URL).replace(/\/+$/, "")}/${input.path.replace(/^\/+/, "")}`;
41
+ const request = async (token: XaiSubscriptionTokenSnapshot, signal: AbortSignal) => {
42
+ const headers = xaiSubscriptionProxyHeaders(token, input.context.clientVersion);
43
+ return await fetchImpl(url, { method: "GET", redirect: "error", headers, signal });
44
+ };
45
+ const fetched = await runBoundedXaiOperation(async (signal) => {
46
+ let response = await request(await input.context.getToken(), signal);
47
+ if (response.status === 401) {
48
+ await response.body?.cancel().catch(() => undefined);
49
+ response = await request(await input.context.refresh(), signal);
50
+ }
51
+ if (response.status === 401 || response.status === 403) {
52
+ await response.body?.cancel().catch(() => undefined);
53
+ throw new XaiSubscriptionReloginRequired();
54
+ }
55
+ if (!response.ok) {
56
+ await response.body?.cancel().catch(() => undefined);
57
+ throw new XaiSubscriptionTransientError(
58
+ `xAI ${input.label} failed (${response.status})`,
59
+ response.status,
60
+ );
61
+ }
62
+ return await readResponseJsonBounded<T>(response, maxBytes, `xAI ${input.label}`, { signal });
63
+ }, timeoutMs);
64
+ if (!fetched.ok) {
65
+ throw new XaiSubscriptionTransientError(`xAI ${input.label} ${fetched.reason}`);
66
+ }
67
+ return fetched.value;
68
+ }
69
+
70
+ export function xaiSubscriptionProxyHeaders(
71
+ token: XaiSubscriptionTokenSnapshot,
72
+ clientVersion = XAI_CLIENT_VERSION,
73
+ ): Headers {
74
+ return new Headers({
75
+ accept: "application/json",
76
+ authorization: `Bearer ${token.accessToken}`,
77
+ "user-agent": `opengeni/${clientVersion}`,
78
+ "x-grok-client-version": clientVersion,
79
+ "x-grok-client-identifier": "opengeni",
80
+ "x-grok-client-mode": XAI_CLIENT_MODE,
81
+ "x-authenticateresponse": "authenticate-response",
82
+ "x-xai-token-auth": XAI_TOKEN_AUTH_HEADER_VALUE,
83
+ "x-userid": token.userId,
84
+ "x-grok-user-id": token.userId,
85
+ });
86
+ }
package/src/quota.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { XaiFetchLike } from "./fetch";
2
+ import { fetchXaiProxyJson, type XaiProxyAuthContext } from "./proxy";
3
+
4
+ export type XaiUsagePeriod = {
5
+ type: string | null;
6
+ start: Date | null;
7
+ end: Date | null;
8
+ };
9
+
10
+ export type XaiSubscriptionQuota = {
11
+ usedPercent: number | null;
12
+ period: XaiUsagePeriod | null;
13
+ prepaidBalanceCents: number | null;
14
+ onDemandCapCents: number | null;
15
+ onDemandUsedCents: number | null;
16
+ onDemandEnabled: boolean | null;
17
+ unifiedBilling: boolean | null;
18
+ subscriptionTier: string | null;
19
+ checkedAt: Date;
20
+ };
21
+
22
+ export async function fetchXaiSubscriptionQuota(input: {
23
+ context: XaiProxyAuthContext;
24
+ fetch?: XaiFetchLike;
25
+ timeoutMs?: number;
26
+ baseUrl?: string;
27
+ }): Promise<XaiSubscriptionQuota> {
28
+ const body = await fetchXaiProxyJson<Record<string, unknown>>({
29
+ path: "billing?format=credits",
30
+ context: input.context,
31
+ ...(input.fetch ? { fetch: input.fetch } : {}),
32
+ ...(input.timeoutMs ? { timeoutMs: input.timeoutMs } : {}),
33
+ ...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
34
+ label: "billing request",
35
+ });
36
+ const config = record(body.config);
37
+ const currentPeriod = record(config?.currentPeriod ?? config?.current_period);
38
+ const monthlyLimit = cents(config?.monthlyLimit ?? config?.monthly_limit);
39
+ const used = cents(config?.used);
40
+ const directPercent = finitePercent(config?.creditUsagePercent ?? config?.credit_usage_percent);
41
+ const derivedPercent =
42
+ monthlyLimit !== null && monthlyLimit > 0 && used !== null
43
+ ? Math.max(0, Math.min(100, (used / monthlyLimit) * 100))
44
+ : null;
45
+ const period = currentPeriod
46
+ ? {
47
+ type: stringOrNull(currentPeriod.type),
48
+ start: dateOrNull(currentPeriod.start),
49
+ end: dateOrNull(currentPeriod.end),
50
+ }
51
+ : legacyPeriod(config);
52
+ return {
53
+ usedPercent: directPercent ?? derivedPercent,
54
+ period,
55
+ prepaidBalanceCents: cents(config?.prepaidBalance ?? config?.prepaid_balance),
56
+ onDemandCapCents: cents(config?.onDemandCap ?? config?.on_demand_cap),
57
+ onDemandUsedCents: cents(config?.onDemandUsed ?? config?.on_demand_used),
58
+ onDemandEnabled: booleanOrNull(body.onDemandEnabled ?? body.on_demand_enabled),
59
+ unifiedBilling: booleanOrNull(config?.isUnifiedBillingUser ?? config?.is_unified_billing_user),
60
+ subscriptionTier: stringOrNull(body.subscriptionTier ?? body.subscription_tier),
61
+ checkedAt: new Date(),
62
+ };
63
+ }
64
+
65
+ function record(value: unknown): Record<string, unknown> | null {
66
+ return value && typeof value === "object" && !Array.isArray(value)
67
+ ? (value as Record<string, unknown>)
68
+ : null;
69
+ }
70
+
71
+ function cents(value: unknown): number | null {
72
+ if (typeof value === "number" && Number.isSafeInteger(value)) return value;
73
+ const object = record(value);
74
+ const candidate = object?.val;
75
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) ? candidate : null;
76
+ }
77
+
78
+ function finitePercent(value: unknown): number | null {
79
+ const candidate = Number(value);
80
+ return Number.isFinite(candidate) ? Math.max(0, Math.min(100, candidate)) : null;
81
+ }
82
+
83
+ function stringOrNull(value: unknown): string | null {
84
+ return typeof value === "string" && value.length > 0 ? value : null;
85
+ }
86
+
87
+ function booleanOrNull(value: unknown): boolean | null {
88
+ return typeof value === "boolean" ? value : null;
89
+ }
90
+
91
+ function dateOrNull(value: unknown): Date | null {
92
+ if (typeof value !== "string") return null;
93
+ const date = new Date(value);
94
+ return Number.isNaN(date.getTime()) ? null : date;
95
+ }
96
+
97
+ function legacyPeriod(config: Record<string, unknown> | null): XaiUsagePeriod | null {
98
+ const start = dateOrNull(config?.billingPeriodStart ?? config?.billing_period_start);
99
+ const end = dateOrNull(config?.billingPeriodEnd ?? config?.billing_period_end);
100
+ return start || end ? { type: null, start, end } : null;
101
+ }
@@ -0,0 +1,31 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ export type XaiSubscriptionTokenSnapshot = {
4
+ accessToken: string;
5
+ userId: string;
6
+ };
7
+
8
+ export type XaiHostedSearchOptions = {
9
+ webSearch?: boolean | Record<string, unknown>;
10
+ xSearch?: boolean | Record<string, unknown>;
11
+ };
12
+
13
+ export type XaiFinalContextUsage = {
14
+ inputTokens: number;
15
+ outputTokens: number;
16
+ totalTokens: number;
17
+ };
18
+
19
+ export type XaiSubscriptionRequestContext = {
20
+ clientVersion: string;
21
+ sessionId: string;
22
+ turnId: string;
23
+ getToken: () => Promise<XaiSubscriptionTokenSnapshot>;
24
+ refresh: () => Promise<XaiSubscriptionTokenSnapshot>;
25
+ resolveModel: (slug: string) => string;
26
+ hostedSearch?: XaiHostedSearchOptions;
27
+ onFinalContextUsage?: (usage: XaiFinalContextUsage) => void;
28
+ nextRequestId?: () => string;
29
+ };
30
+
31
+ export const xaiSubscriptionRequestStorage = new AsyncLocalStorage<XaiSubscriptionRequestContext>();