libfx 0.0.13 → 0.0.14-dev.1534.g6afa7c33dac5

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/fx-term.wasm CHANGED
Binary file
@@ -0,0 +1,145 @@
1
+ // `libfx/grok`: sign in with a Grok plan for a Grok agent. Two flows, both
2
+ // plain data between steps so a website can start in one request and finish in
3
+ // another:
4
+ //
5
+ // - Device code: the user enters a short code on xAI's page.
6
+ // - Paste the address: the user signs in on xAI, lands on a page at
7
+ // 127.0.0.1 that does not load, and pastes its address back.
8
+ import { grokAccountId, grokClientId, grokIssuer, normalizeGrokCredentials, refreshGrokCredentials } from "./grok.js";
9
+ import { hostFetch, pkceChallenge, randomSecret, readSignInAddress, signInError, wait } from "./oauth.js";
10
+
11
+ const scope = "openid profile email offline_access grok-cli:access api:access";
12
+ const deviceGrant = "urn:ietf:params:oauth:grant-type:device_code";
13
+ const slowDownMs = 5000;
14
+
15
+ const form = (fields) => ({
16
+ method: "POST",
17
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
18
+ body: new URLSearchParams(fields).toString(),
19
+ });
20
+
21
+ /** Credentials for a token response, with the account it belongs to. */
22
+ async function credentialsFor(tokens, { fetch, signal }) {
23
+ const accountId = await grokAccountId(tokens.access_token, { fetch, signal });
24
+ return normalizeGrokCredentials({
25
+ accessToken: tokens.access_token,
26
+ refreshToken: tokens.refresh_token,
27
+ accountId,
28
+ ...(Number.isFinite(tokens.expires_in) ? { expiresAt: Date.now() + tokens.expires_in * 1000 } : {}),
29
+ });
30
+ }
31
+
32
+ /** Starts a sign-in. Keep the result on the server: `verifier` is secret. */
33
+ export async function startSignIn() {
34
+ const verifier = randomSecret();
35
+ const state = randomSecret();
36
+ // xAI accepts any loopback port. Nothing listens there: the user copies the address.
37
+ const port = 1024 + (crypto.getRandomValues(new Uint16Array(1))[0] % (65536 - 1024));
38
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
39
+ const url = new URL(`${grokIssuer}/oauth2/authorize`);
40
+ for (const [key, value] of Object.entries({
41
+ response_type: "code",
42
+ client_id: grokClientId,
43
+ redirect_uri: redirectUri,
44
+ scope,
45
+ code_challenge: await pkceChallenge(verifier),
46
+ code_challenge_method: "S256",
47
+ state,
48
+ referrer: "fx",
49
+ })) url.searchParams.set(key, value);
50
+ return { url: url.href, state, verifier, redirectUri };
51
+ }
52
+
53
+ /** Finishes a sign-in from the address the user pasted. */
54
+ export async function finishSignIn(signIn, address, { fetch, signal } = {}) {
55
+ if (!signIn || typeof signIn.state !== "string" || typeof signIn.verifier !== "string" || typeof signIn.redirectUri !== "string") {
56
+ throw new TypeError("finishSignIn() needs the object startSignIn() returned");
57
+ }
58
+ const { code, state } = readSignInAddress(address, { prefix: "LIBFX_GROK", issuer: "xAI", redirectUri: signIn.redirectUri });
59
+ if (state !== signIn.state) {
60
+ throw signInError("LIBFX_GROK_SIGN_IN_STATE_MISMATCH", "That address is from a different sign-in. Start again and paste the address from this one.");
61
+ }
62
+ const response = await hostFetch(fetch)(`${grokIssuer}/oauth2/token`, {
63
+ ...form({ grant_type: "authorization_code", client_id: grokClientId, code, code_verifier: signIn.verifier, redirect_uri: signIn.redirectUri }),
64
+ signal,
65
+ });
66
+ if (response.status === 400 || response.status === 401) {
67
+ await response.body?.cancel().catch(() => {});
68
+ throw signInError("LIBFX_GROK_SIGN_IN_EXPIRED", "This sign-in expired or was already used. Start again.", { status: response.status });
69
+ }
70
+ if (!response.ok) {
71
+ await response.body?.cancel().catch(() => {});
72
+ throw signInError("LIBFX_GROK_SIGN_IN_FAILED", `xAI could not finish the sign-in (HTTP ${response.status}).`, { status: response.status });
73
+ }
74
+ return credentialsFor(await response.json(), { fetch, signal });
75
+ }
76
+
77
+ /** Starts a device code sign-in. Show `userCode` and `verificationUrl`. */
78
+ export async function startDeviceSignIn({ fetch, signal } = {}) {
79
+ const response = await hostFetch(fetch)(`${grokIssuer}/oauth2/device/code`, { ...form({ client_id: grokClientId, scope }), signal });
80
+ if (!response.ok) {
81
+ await response.body?.cancel().catch(() => {});
82
+ throw signInError("LIBFX_GROK_DEVICE_CODE_UNAVAILABLE", `xAI did not issue a device code (HTTP ${response.status}).`, { status: response.status });
83
+ }
84
+ const body = await response.json();
85
+ const expiresIn = Number(body.expires_in);
86
+ if (typeof body.user_code !== "string" || typeof body.device_code !== "string" || typeof body.verification_uri !== "string" || !Number.isFinite(expiresIn)) {
87
+ throw signInError("LIBFX_GROK_DEVICE_CODE_UNAVAILABLE", "xAI returned a device code response libfx does not understand.");
88
+ }
89
+ const interval = Number(body.interval);
90
+ return {
91
+ verificationUrl: typeof body.verification_uri_complete === "string" ? body.verification_uri_complete : body.verification_uri,
92
+ userCode: body.user_code,
93
+ deviceCode: body.device_code,
94
+ intervalMs: Math.max(Number.isFinite(interval) ? interval : 5, 1) * 1000,
95
+ expiresAt: Date.now() + expiresIn * 1000,
96
+ };
97
+ }
98
+
99
+ /** One poll: `{ credentials }`, or `{ pending: true, slowDown }` until the user enters the code. */
100
+ async function poll(device, { fetch, signal }) {
101
+ if (!device || typeof device.deviceCode !== "string" || typeof device.userCode !== "string") {
102
+ throw new TypeError("pollDeviceSignIn() needs the object startDeviceSignIn() returned");
103
+ }
104
+ if (Date.now() > device.expiresAt) {
105
+ throw signInError("LIBFX_GROK_DEVICE_CODE_EXPIRED", "The code expired before it was entered. Start again.");
106
+ }
107
+ const response = await hostFetch(fetch)(`${grokIssuer}/oauth2/token`, {
108
+ ...form({ grant_type: deviceGrant, device_code: device.deviceCode, client_id: grokClientId }),
109
+ signal,
110
+ });
111
+ if (response.ok) return { credentials: await credentialsFor(await response.json(), { fetch, signal }) };
112
+ const body = await response.json().catch(() => null);
113
+ switch (body?.error) {
114
+ case "authorization_pending": return { pending: true, slowDown: false };
115
+ case "slow_down": return { pending: true, slowDown: true };
116
+ case "expired_token":
117
+ throw signInError("LIBFX_GROK_DEVICE_CODE_EXPIRED", "The code expired before it was entered. Start again.");
118
+ case "access_denied":
119
+ throw signInError("LIBFX_GROK_SIGN_IN_DENIED", "The sign-in was declined on xAI's page.", { reason: body.error });
120
+ default:
121
+ throw signInError("LIBFX_GROK_SIGN_IN_FAILED", `xAI could not finish the sign-in (HTTP ${response.status}).`, { status: response.status });
122
+ }
123
+ }
124
+
125
+ /** Checks a device sign-in once: credentials when the user has entered the code, otherwise null. */
126
+ export async function pollDeviceSignIn(device, { fetch, signal } = {}) {
127
+ const result = await poll(device, { fetch, signal });
128
+ return result.credentials ?? null;
129
+ }
130
+
131
+ /** Waits until the user enters the code, polling at xAI's interval and slowing down when asked. */
132
+ export async function completeDeviceSignIn(device, { fetch, signal } = {}) {
133
+ let intervalMs = device?.intervalMs;
134
+ for (;;) {
135
+ const result = await poll(device, { fetch, signal });
136
+ if (result.credentials) return result.credentials;
137
+ if (result.slowDown) intervalMs += slowDownMs;
138
+ await wait(intervalMs, signal);
139
+ }
140
+ }
141
+
142
+ /** Refreshes credentials. Grok agents refresh on their own; this is for hosts. */
143
+ export function refreshCredentials(credentials, { fetch, signal } = {}) {
144
+ return refreshGrokCredentials(normalizeGrokCredentials(credentials), { fetch, signal });
145
+ }
package/grok.js ADDED
@@ -0,0 +1,108 @@
1
+ // Grok (xAI subscription) credentials for libfx agents: the Grok endpoints,
2
+ // how a Grok plan's credentials are checked and refreshed, and the headers
3
+ // that carry them. `subscription.js` adds them at the fetch edge.
4
+ import { normalizeCredentials } from "./subscription.js";
5
+
6
+ export const grokIssuer = "https://auth.x.ai";
7
+ export const grokClientId = "b1a00492-073a-47ea-816f-4c329264a828";
8
+
9
+ const proxyOrigin = "https://cli-chat-proxy.grok.com";
10
+ const responsesPath = "/v1/responses";
11
+ const modelsPath = "/v1/models";
12
+ const languageModelsEndpoint = "https://api.x.ai/v1/language-models";
13
+
14
+ function endpointOf(url) {
15
+ try {
16
+ const parsed = new URL(String(url));
17
+ return `${parsed.origin}${parsed.pathname}`;
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /** True only for the exact Grok endpoints the engine calls. */
24
+ export function isGrokRequest(url) {
25
+ const endpoint = endpointOf(url);
26
+ return endpoint === `${proxyOrigin}${responsesPath}` ||
27
+ endpoint === `${proxyOrigin}${modelsPath}` ||
28
+ endpoint === languageModelsEndpoint;
29
+ }
30
+
31
+ /**
32
+ * Validates credentials from the host. The account id defaults to the access
33
+ * token's `sub` claim when the token carries one; sign-in from `libfx/grok`
34
+ * always returns it. Returns a new object.
35
+ */
36
+ export function normalizeGrokCredentials(value) {
37
+ return normalizeCredentials(value, { label: "Grok", accountClaim: (claims) => claims?.sub });
38
+ }
39
+
40
+ function grokError(code, message, status) {
41
+ const error = new Error(message);
42
+ error.code = code;
43
+ if (status !== undefined) error.status = status;
44
+ return error;
45
+ }
46
+
47
+ /** The xAI account an access token belongs to: the `sub` of its userinfo. */
48
+ export async function grokAccountId(accessToken, { fetch, signal } = {}) {
49
+ const doFetch = fetch ?? globalThis.fetch?.bind(globalThis);
50
+ const response = await doFetch(`${grokIssuer}/oauth2/userinfo`, {
51
+ headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
52
+ signal,
53
+ });
54
+ if (!response.ok) {
55
+ await response.body?.cancel().catch(() => {});
56
+ throw grokError("LIBFX_GROK_ACCOUNT_UNAVAILABLE", `xAI did not identify the account (HTTP ${response.status})`, response.status);
57
+ }
58
+ const info = await response.json().catch(() => null);
59
+ if (typeof info?.sub !== "string" || info.sub.length === 0) {
60
+ throw grokError("LIBFX_GROK_ACCOUNT_UNAVAILABLE", "xAI's account response had no account id");
61
+ }
62
+ return info.sub;
63
+ }
64
+
65
+ /**
66
+ * Exchanges a refresh token at xAI, keeping the old refresh token when the
67
+ * response does not rotate it, and confirms the new token is for the same
68
+ * account.
69
+ */
70
+ export async function refreshGrokCredentials(credentials, { fetch, signal } = {}) {
71
+ if (credentials.refreshToken === undefined) throw new TypeError("Grok credentials have no refreshToken");
72
+ const doFetch = fetch ?? globalThis.fetch?.bind(globalThis);
73
+ const response = await doFetch(`${grokIssuer}/oauth2/token`, {
74
+ method: "POST",
75
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
76
+ body: new URLSearchParams({ grant_type: "refresh_token", client_id: grokClientId, refresh_token: credentials.refreshToken }).toString(),
77
+ signal,
78
+ });
79
+ if (!response.ok) {
80
+ await response.body?.cancel().catch(() => {});
81
+ throw grokError("LIBFX_GROK_REFRESH_FAILED", `Grok token refresh failed with HTTP ${response.status}`, response.status);
82
+ }
83
+ const tokens = await response.json();
84
+ const next = normalizeGrokCredentials({
85
+ accessToken: tokens.access_token,
86
+ refreshToken: tokens.refresh_token ?? credentials.refreshToken,
87
+ accountId: credentials.accountId,
88
+ ...(Number.isFinite(tokens.expires_in) ? { expiresAt: Date.now() + tokens.expires_in * 1000 } : {}),
89
+ });
90
+ if (await grokAccountId(next.accessToken, { fetch, signal }) !== credentials.accountId) {
91
+ throw grokError("LIBFX_GROK_ACCOUNT_CHANGED", "The refreshed Grok token belongs to a different account");
92
+ }
93
+ return next;
94
+ }
95
+
96
+ /** Grok on a Grok plan, for `createSubscriptionAuthorizer()`. */
97
+ export const grokSubscription = {
98
+ provider: "grok",
99
+ matches: isGrokRequest,
100
+ sessionId: (headers) => headers.get("x-grok-conv-id"),
101
+ normalize: normalizeGrokCredentials,
102
+ refresh: refreshGrokCredentials,
103
+ authorize(headers, endpoint, credentials) {
104
+ headers.set("authorization", `Bearer ${credentials.accessToken}`);
105
+ if (endpoint === `${proxyOrigin}${responsesPath}`) headers.set("x-grok-user-id", credentials.accountId);
106
+ if (endpoint === `${proxyOrigin}${modelsPath}`) headers.set("x-userid", credentials.accountId);
107
+ },
108
+ };
Binary file
Binary file
Binary file
Binary file
package/mcp.js CHANGED
@@ -67,6 +67,9 @@ export async function createMcpAdapter(client, options = {}) {
67
67
  name,
68
68
  description: tool.description || "MCP tool",
69
69
  inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
70
+ // The server's hints are not trusted: a call is never run twice on its
71
+ // own. A host that knows a tool is safe to repeat can set this to true.
72
+ idempotent: false,
70
73
  async execute(input, { signal }) {
71
74
  const result = await client.callTool({ name: tool.name, arguments: input }, undefined, { signal });
72
75
  const text = resultText(result);