libfx 0.0.13 → 0.0.14-dev.1561.g1a070af32cdf
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/README.md +454 -486
- package/browser.js +18 -4
- package/chatgpt-sign-in.js +141 -0
- package/chatgpt.js +69 -0
- package/custom-provider.js +123 -0
- package/durable/local.mjs +190 -0
- package/durable/vercel.mjs +268 -0
- package/durable/world.mjs +1 -0
- package/durable.js +2752 -0
- package/fetch-cleanup.js +12 -0
- package/fx-core.wasm +0 -0
- package/fx-harness.js +220 -0
- package/fx-sdk.js +1302 -116
- package/fx-term.wasm +0 -0
- package/grok-sign-in.js +145 -0
- package/grok.js +108 -0
- package/libfx.darwin-arm64.node +0 -0
- package/libfx.darwin-x64.node +0 -0
- package/libfx.linux-arm64.node +0 -0
- package/libfx.linux-x64.node +0 -0
- package/mcp.js +14 -2
- package/node.cjs +4508 -213
- package/node.js +195 -47
- package/oauth.js +64 -0
- package/package.json +66 -13
- package/subscription.js +253 -0
- package/tool-observation.js +185 -0
- package/types/browser.d.ts +146 -0
- package/types/chatgpt.d.ts +58 -0
- package/types/durable-local.d.ts +18 -0
- package/types/durable-vercel.d.ts +22 -0
- package/types/durable-world.d.ts +95 -0
- package/types/grok.d.ts +61 -0
- package/types/libfx.d.cts +1061 -0
- package/types/mcp.d.ts +46 -0
- package/types/node.d.cts +165 -0
- package/types/node.d.ts +3 -0
- package/types/skills-node.d.ts +25 -0
- package/types/skills.d.ts +28 -0
- package/types/wasm.d.ts +106 -0
package/fx-term.wasm
CHANGED
|
Binary file
|
package/grok-sign-in.js
ADDED
|
@@ -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
|
+
};
|
package/libfx.darwin-arm64.node
CHANGED
|
Binary file
|
package/libfx.darwin-x64.node
CHANGED
|
Binary file
|
package/libfx.linux-arm64.node
CHANGED
|
Binary file
|
package/libfx.linux-x64.node
CHANGED
|
Binary file
|
package/mcp.js
CHANGED
|
@@ -33,6 +33,11 @@ export async function createMcpAdapter(client, options = {}) {
|
|
|
33
33
|
throw new TypeError("MCP client must provide listTools() and callTool()");
|
|
34
34
|
}
|
|
35
35
|
const prefix = options.prefix ?? "";
|
|
36
|
+
if (options.propagateCorrelation !== undefined && typeof options.propagateCorrelation !== "boolean") throw new TypeError("MCP propagateCorrelation must be a boolean");
|
|
37
|
+
if (options.serverName !== undefined && (typeof options.serverName !== "string" || options.serverName.length === 0 || options.serverName.length > 256)) throw new TypeError("MCP serverName must be a bounded name");
|
|
38
|
+
let reportedName;
|
|
39
|
+
try { reportedName = client.getServerVersion?.()?.name; } catch {}
|
|
40
|
+
const serverName = options.serverName ?? (typeof reportedName === "string" && reportedName.length > 0 && reportedName.length <= 256 ? reportedName : null);
|
|
36
41
|
if (typeof prefix !== "string" || !/^[A-Za-z0-9_-]*$/.test(prefix)) {
|
|
37
42
|
throw new TypeError("MCP prefix must contain only letters, digits, underscore, or hyphen");
|
|
38
43
|
}
|
|
@@ -67,8 +72,15 @@ export async function createMcpAdapter(client, options = {}) {
|
|
|
67
72
|
name,
|
|
68
73
|
description: tool.description || "MCP tool",
|
|
69
74
|
inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
// The server's hints are not trusted: a call is never run twice on its
|
|
76
|
+
// own. A host that knows a tool is safe to repeat can set this to true.
|
|
77
|
+
idempotent: false,
|
|
78
|
+
async execute(input, { signal, observation }) {
|
|
79
|
+
const correlationId = options.propagateCorrelation === true ? crypto.randomUUID() : undefined;
|
|
80
|
+
// Correlation is not progress reporting: progressToken requests remote
|
|
81
|
+
// notifications and some clients replace it with their JSON-RPC ID.
|
|
82
|
+
const send = () => client.callTool({ name: tool.name, arguments: input, ...(correlationId === undefined ? {} : { _meta: { "libfx/correlationId": correlationId } }) }, undefined, { signal });
|
|
83
|
+
const result = observation ? await observation.mcp({ server: serverName, tool: tool.name, correlationId }, send) : await send();
|
|
72
84
|
const text = resultText(result);
|
|
73
85
|
const images = (Array.isArray(result?.content) ? result.content : []).flatMap((item) => item?.type === "image" ? [item] : item?.type === "resource" && item.resource?.mimeType?.startsWith("image/") && typeof item.resource?.blob === "string" ? [{ type: "image", mimeType: item.resource.mimeType, data: item.resource.blob }] : []).map((item) => ({
|
|
74
86
|
type: "image", mimeType: item.mimeType, data: item.data,
|