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.
@@ -0,0 +1,251 @@
1
+ // Subscription credentials for libfx agents: Codex on a ChatGPT plan, Grok on
2
+ // a Grok plan. The engine sends these providers' requests without credentials;
3
+ // this module adds them at the fetch edge, from the host's `credentials`
4
+ // callback, refreshing near expiry and once after a rejection. Credentials
5
+ // never enter the engine or a session's saved records, and are masked in the
6
+ // body of a failed response.
7
+ //
8
+ // A provider spec supplies what differs: which requests it authorizes, how its
9
+ // credentials are checked and refreshed, and which headers carry them.
10
+
11
+ const refreshSkewMs = 5 * 60 * 1000;
12
+ const maxRememberedCredentials = 256;
13
+ const maxTokenBytes = 64 * 1024;
14
+ const maxErrorBodyBytes = 64 * 1024;
15
+ const encoder = new TextEncoder();
16
+
17
+ /** A non-empty, bounded credential field, or undefined when optional and absent. */
18
+ export function tokenString(value, name, required, label) {
19
+ if (value === undefined && !required) return undefined;
20
+ if (typeof value !== "string" || value.length === 0) {
21
+ throw new TypeError(`${label} credentials ${name} must be a non-empty string`);
22
+ }
23
+ if (encoder.encode(value).length > maxTokenBytes) {
24
+ throw new RangeError(`${label} credentials ${name} exceeds ${maxTokenBytes} bytes`);
25
+ }
26
+ return value;
27
+ }
28
+
29
+ /** The claims of a JWT, or null when it is not one. Does not verify it. */
30
+ export function jwtClaims(token) {
31
+ const part = typeof token === "string" ? token.split(".")[1] : undefined;
32
+ if (!part) return null;
33
+ try {
34
+ const base64 = part.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(part.length / 4) * 4, "=");
35
+ const binary = atob(base64);
36
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
37
+ const claims = JSON.parse(new TextDecoder().decode(bytes));
38
+ return claims && typeof claims === "object" ? claims : null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Validates credentials from the host. `accountId` and `expiresAt` default to
46
+ * the access token's claims, `accountClaim(claims)` picking the account.
47
+ * Returns a new object.
48
+ */
49
+ export function normalizeCredentials(value, { label, accountClaim }) {
50
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
51
+ throw new TypeError("credentials() must return { accessToken, refreshToken?, accountId?, expiresAt? }");
52
+ }
53
+ const accessToken = tokenString(value.accessToken, "accessToken", true, label);
54
+ const refreshToken = tokenString(value.refreshToken, "refreshToken", false, label);
55
+ const claims = jwtClaims(accessToken);
56
+ const accountId = value.accountId === undefined ? accountClaim(claims) : tokenString(value.accountId, "accountId", true, label);
57
+ if (typeof accountId !== "string" || accountId.length === 0) {
58
+ throw new TypeError(`${label} credentials need an accountId; it was not in the access token`);
59
+ }
60
+ let expiresAt = value.expiresAt;
61
+ if (expiresAt === undefined && Number.isFinite(claims?.exp)) expiresAt = claims.exp * 1000;
62
+ if (expiresAt !== undefined && !Number.isFinite(expiresAt)) {
63
+ throw new TypeError(`${label} credentials expiresAt must be milliseconds since the epoch`);
64
+ }
65
+ return { accessToken, ...(refreshToken === undefined ? {} : { refreshToken }), accountId, ...(expiresAt === undefined ? {} : { expiresAt }) };
66
+ }
67
+
68
+ /**
69
+ * A failed response whose body cannot repeat the credentials its request
70
+ * carried. Some servers quote the bearer token back in an error, and the
71
+ * engine shows and saves error details, so this edge, the only code that knows
72
+ * the token, masks it the way fx masks error details from its own providers.
73
+ * JSON is normalized first, so an escaped copy cannot survive.
74
+ */
75
+ async function withoutCredentials(response, credentials) {
76
+ if (response.status < 400) return response;
77
+ const raw = await boundedText(response, maxErrorBodyBytes);
78
+ let body = raw ?? "Provider error details exceeded the local limit";
79
+ if (raw !== null && /^\s*[[{"]/.test(raw)) {
80
+ try {
81
+ body = JSON.stringify(JSON.parse(raw));
82
+ } catch {
83
+ body = "Provider error details could not be decoded";
84
+ }
85
+ }
86
+ for (const secret of [credentials.accessToken, credentials.refreshToken]) {
87
+ if (!secret) continue;
88
+ for (const form of new Set([secret, JSON.stringify(secret).slice(1, -1)])) body = body.split(form).join("*".repeat(form.length));
89
+ }
90
+ const headers = new Headers(response.headers);
91
+ headers.delete("content-length");
92
+ headers.delete("content-encoding");
93
+ return new Response(body, { status: response.status, statusText: response.statusText, headers });
94
+ }
95
+
96
+ /** The body as text, or null once it exceeds `limit` bytes. */
97
+ async function boundedText(response, limit) {
98
+ if (!response.body) return "";
99
+ const reader = response.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ let size = 0;
102
+ let text = "";
103
+ for (;;) {
104
+ const { done, value } = await reader.read();
105
+ if (done) return text + decoder.decode();
106
+ size += value.byteLength;
107
+ if (size > limit) {
108
+ await reader.cancel().catch(() => {});
109
+ return null;
110
+ }
111
+ text += decoder.decode(value, { stream: true });
112
+ }
113
+ }
114
+
115
+ /** True when the credentials can and should be refreshed before use. */
116
+ export function needsRefresh(credentials, nowMs) {
117
+ return credentials.refreshToken !== undefined &&
118
+ credentials.expiresAt !== undefined &&
119
+ credentials.expiresAt - refreshSkewMs <= nowMs;
120
+ }
121
+
122
+ /** The engine option that carries an agent's `createCredentialMemory()`. */
123
+ export const credentialMemory = Symbol.for("libfx.credentialMemory");
124
+
125
+ /**
126
+ * Refresh state for `createSubscriptionAuthorizer()`. An agent gives the same
127
+ * memory to every session's engine, so its sessions refresh an account once.
128
+ */
129
+ export function createCredentialMemory() {
130
+ return { descendants: new Map(), refreshing: new Map(), saved: new WeakMap() };
131
+ }
132
+
133
+ /**
134
+ * The fetch edge for subscription requests. `credentials({ provider, sessionId })`
135
+ * supplies credentials; `onCredentials(next, { provider, sessionId })` receives
136
+ * refreshed ones once for each session that uses them, so the host can save them.
137
+ * `refreshLock({ provider, sessionId }, refresh)` runs `refresh` while the host
138
+ * holds a lock on the account, for a host with several servers.
139
+ *
140
+ * Each spec: `{ provider, matches(endpoint), sessionId(headers), normalize(value),
141
+ * refresh(credentials, { fetch, signal }), authorize(headers, endpoint, credentials) }`.
142
+ */
143
+ export function createSubscriptionAuthorizer(specs, { credentials, onCredentials, refreshLock, fetch, now = Date.now, memory = createCredentialMemory() }) {
144
+ if (typeof credentials !== "function") throw new TypeError("a subscription provider needs a credentials() function");
145
+ if (onCredentials !== undefined && typeof onCredentials !== "function") {
146
+ throw new TypeError("onCredentials must be a function");
147
+ }
148
+ if (refreshLock !== undefined && typeof refreshLock !== "function") {
149
+ throw new TypeError("refreshLock must be a function");
150
+ }
151
+ // Each access token the host supplied maps to the newest credentials refreshed
152
+ // from it, so a host that has not saved a rotation yet never spends the old
153
+ // refresh token twice. Engines that share `memory` share this.
154
+ const { descendants, refreshing, saved } = memory;
155
+ const keyOf = (spec, token) => `${spec.provider}\u0000${token}`;
156
+
157
+ const remember = (spec, supplied, next) => {
158
+ const key = keyOf(spec, supplied.accessToken);
159
+ descendants.delete(key);
160
+ descendants.set(key, next);
161
+ if (descendants.size > maxRememberedCredentials) descendants.delete(descendants.keys().next().value);
162
+ };
163
+
164
+ // A host that keeps credentials per session must also save a rotation that
165
+ // another session's request made.
166
+ const savedBy = (next) => {
167
+ let sessions = saved.get(next);
168
+ if (!sessions) saved.set(next, (sessions = new Set()));
169
+ return sessions;
170
+ };
171
+ const save = async (spec, next, sessionId) => {
172
+ if (!onCredentials) return;
173
+ const sessions = savedBy(next);
174
+ if (sessions.has(sessionId)) return;
175
+ sessions.add(sessionId);
176
+ await onCredentials(next, { provider: spec.provider, sessionId });
177
+ };
178
+
179
+ const redeem = async (spec, supplied, current, sessionId, signal) => {
180
+ if (refreshLock) {
181
+ // Another server may have refreshed and saved them while this one waited.
182
+ const latest = spec.normalize(await credentials({ provider: spec.provider, sessionId }));
183
+ if (latest.accessToken !== current.accessToken && latest.accessToken !== supplied.accessToken && !needsRefresh(latest, now())) {
184
+ remember(spec, supplied, latest);
185
+ savedBy(latest).add(sessionId);
186
+ return latest;
187
+ }
188
+ }
189
+ const next = await spec.refresh(current, { fetch, signal });
190
+ remember(spec, supplied, next);
191
+ await save(spec, next, sessionId);
192
+ return next;
193
+ };
194
+
195
+ // With `refreshLock`, the host holds a lock on the account while the refresh
196
+ // runs, so its servers refresh one at a time.
197
+ const locked = async (spec, supplied, current, sessionId, signal) => {
198
+ let next;
199
+ await refreshLock({ provider: spec.provider, sessionId }, async () => {
200
+ next = await redeem(spec, supplied, current, sessionId, signal);
201
+ });
202
+ if (next === undefined) throw new Error("refreshLock must run and await the refresh it is given");
203
+ return next;
204
+ };
205
+
206
+ const refresh = async (spec, supplied, current, sessionId, signal) => {
207
+ const key = keyOf(spec, current.refreshToken);
208
+ let task = refreshing.get(key);
209
+ if (!task) {
210
+ task = (refreshLock ? locked : redeem)(spec, supplied, current, sessionId, signal).finally(() => refreshing.delete(key));
211
+ refreshing.set(key, task);
212
+ }
213
+ const next = await task;
214
+ if (sessionId !== undefined) await save(spec, next, sessionId);
215
+ return next;
216
+ };
217
+
218
+ const current = async (spec, sessionId, signal) => {
219
+ const supplied = spec.normalize(await credentials({ provider: spec.provider, sessionId }));
220
+ const best = descendants.get(keyOf(spec, supplied.accessToken)) ?? supplied;
221
+ const used = needsRefresh(best, now()) ? await refresh(spec, supplied, best, sessionId, signal) : best;
222
+ if (used !== supplied && sessionId !== undefined) await save(spec, used, sessionId);
223
+ return { supplied, used };
224
+ };
225
+
226
+ const withCredentials = (spec, endpoint, init, used) => {
227
+ const headers = new Headers(init.headers);
228
+ spec.authorize(headers, endpoint, used);
229
+ return { ...init, headers };
230
+ };
231
+
232
+ return {
233
+ /** The spec that authorizes requests to `endpoint` (origin and path), if any. */
234
+ specFor: (endpoint) => specs.find((spec) => spec.matches(endpoint)),
235
+ /** Sends one request with credentials, refreshing once after a 401. */
236
+ async fetch(spec, endpoint, hostFetch, input, init = {}) {
237
+ const sessionId = spec.sessionId(new Headers(init.headers)) || undefined;
238
+ const { used } = await current(spec, sessionId, init.signal);
239
+ const response = await hostFetch(input, withCredentials(spec, endpoint, init, used));
240
+ if (response.status !== 401 || used.refreshToken === undefined) return withoutCredentials(response, used);
241
+ await response.body?.cancel().catch(() => {});
242
+ // Another request may have replaced these credentials while this one was
243
+ // out. Only a rejection of the newest ones spends a refresh token.
244
+ const latest = await current(spec, sessionId, init.signal);
245
+ const next = latest.used.accessToken !== used.accessToken
246
+ ? latest.used
247
+ : await refresh(spec, latest.supplied, used, sessionId, init.signal);
248
+ return withoutCredentials(await hostFetch(input, withCredentials(spec, endpoint, init, next)), next);
249
+ },
250
+ };
251
+ }
@@ -0,0 +1,139 @@
1
+ // `libfx` in browsers and other non-Node hosts, and `libfx/browser` (browser.js).
2
+ import type {
3
+ FxAgent,
4
+ FxAgentOptions,
5
+ FxBrowserBackendOptions,
6
+ FxEngine,
7
+ FxEngineOptions,
8
+ FxTerminal,
9
+ FxTerminalOptions,
10
+ } from "./libfx.cjs";
11
+
12
+ export {
13
+ createMemoryPersistence,
14
+ encodeXtermKeyEvent,
15
+ FxFencedError,
16
+ FxJournalVersionError,
17
+ fxSdkApiVersion,
18
+ listModels,
19
+ memory,
20
+ supportsJspi,
21
+ xtermAdapter,
22
+ } from "./libfx.cjs";
23
+
24
+ export type {
25
+ Durability,
26
+ FxAccepted,
27
+ FxAgent,
28
+ FxAgentEvent,
29
+ FxAgentHostTool,
30
+ FxAgentKeyedTool,
31
+ FxAgentOptions,
32
+ FxAgentTool,
33
+ FxAgentTools,
34
+ FxApiKeyCredentials,
35
+ FxBrowserBackendOptions,
36
+ FxBytes,
37
+ FxCheckpointMeta,
38
+ FxCodexProvider,
39
+ FxCustomProvider,
40
+ FxCustomProviderDefinition,
41
+ FxCustomProviderModel,
42
+ FxEffort,
43
+ FxEngine,
44
+ FxEngineEvent,
45
+ FxEngineOptions,
46
+ FxEnginePromptOptions,
47
+ FxEngineStorage,
48
+ FxEngineTurn,
49
+ FxEngineTurnResult,
50
+ FxErrorSummary,
51
+ FxFetch,
52
+ FxFollowUp,
53
+ FxGatewayProvider,
54
+ FxGrokProvider,
55
+ FxHostTool,
56
+ FxIdleResult,
57
+ FxImageBlock,
58
+ FxJsonSchema,
59
+ FxKeyedTool,
60
+ FxKeyEventLike,
61
+ FxListModelsOptions,
62
+ FxModelCatalogEntry,
63
+ FxModelChoice,
64
+ FxModelOptions,
65
+ FxOnAmbiguous,
66
+ FxPromptBlock,
67
+ FxPromptInput,
68
+ FxPromptOptions,
69
+ FxProviderCredentials,
70
+ FxProviderTool,
71
+ FxResizedImage,
72
+ FxResourceBlock,
73
+ FxResumeOptions,
74
+ FxResumeTurn,
75
+ FxSession,
76
+ FxSessionEvent,
77
+ FxSessionImageBlock,
78
+ FxSessionOptions,
79
+ FxSessionPromptInput,
80
+ FxSessionTurnEvent,
81
+ FxSharedOptions,
82
+ FxSteer,
83
+ FxSteerOptions,
84
+ FxSteered,
85
+ FxSteeringInput,
86
+ FxStopReason,
87
+ FxStreamLine,
88
+ FxTerminal,
89
+ FxTerminalAdapter,
90
+ FxTerminalEvent,
91
+ FxTerminalOptions,
92
+ FxTextBlock,
93
+ FxTool,
94
+ FxToolContext,
95
+ FxToolImage,
96
+ FxToolResult,
97
+ FxTools,
98
+ FxTurn,
99
+ FxTurnEvent,
100
+ FxTurnResult,
101
+ FxTurnView,
102
+ FxUsage,
103
+ FxWasmModule,
104
+ FxWasmSource,
105
+ FxWorkspace,
106
+ FxWorkspaceExecResult,
107
+ FxXtermLike,
108
+ JournalRecord,
109
+ JsonObject,
110
+ JsonValue,
111
+ MemoryOptions,
112
+ MemoryPersistence,
113
+ Persistence,
114
+ PersistenceLoadResult,
115
+ } from "./libfx.cjs";
116
+
117
+ /** Options for `createFxAgent()` in browsers. */
118
+ export type FxBrowserAgentOptions = FxAgentOptions & FxBrowserBackendOptions;
119
+
120
+ /** Options for `createFxEngine()` in browsers. */
121
+ export type FxBrowserEngineOptions = FxEngineOptions & FxBrowserBackendOptions;
122
+
123
+ /** Options for `createFxTerminal()` in browsers. */
124
+ export type FxBrowserTerminalOptions = FxTerminalOptions & FxBrowserBackendOptions;
125
+
126
+ /**
127
+ * An agent whose sessions live in this page unless `durability` says
128
+ * otherwise. It returns at once and does no I/O until a session runs a turn.
129
+ */
130
+ export declare function createFxAgent(options?: FxBrowserAgentOptions): FxAgent;
131
+
132
+ /** One fx session on WebAssembly, with no durability of its own. `apiKey` is required. */
133
+ export declare function createFxEngine(options: FxBrowserEngineOptions): Promise<FxEngine>;
134
+
135
+ /** The interactive terminal harness. Connect it to xterm.js with `xtermAdapter()`. */
136
+ export declare function createFxTerminal(options: FxBrowserTerminalOptions): Promise<FxTerminal>;
137
+
138
+ /** The version of the libfx agent API. */
139
+ export declare const libfxApiVersion: 3;
@@ -0,0 +1,58 @@
1
+ // `libfx/chatgpt` (chatgpt-sign-in.js): sign in with ChatGPT for a Codex agent.
2
+ import type { FxProviderCredentials, FxFetch } from "./libfx.cjs";
3
+
4
+ /** Options every network step accepts. */
5
+ export interface ChatgptRequestOptions {
6
+ /** Replaces the global `fetch` for OpenAI's sign-in endpoints. */
7
+ fetch?: FxFetch;
8
+ signal?: AbortSignal;
9
+ }
10
+
11
+ /** A sign-in in progress. Keep it on the server: `verifier` is secret. */
12
+ export interface ChatgptSignIn {
13
+ /** Where the user signs in. Afterwards OpenAI opens an address at localhost:1455 that does not load; the user pastes it back. */
14
+ readonly url: string;
15
+ readonly state: string;
16
+ readonly verifier: string;
17
+ }
18
+
19
+ /** A device code sign-in in progress. Plain data, so a later request can poll it. */
20
+ export interface ChatgptDeviceSignIn {
21
+ /** Where the user enters `userCode`. */
22
+ readonly verificationUrl: string;
23
+ readonly userCode: string;
24
+ readonly deviceAuthId: string;
25
+ /** How often to poll, in milliseconds. */
26
+ readonly intervalMs: number;
27
+ /** Milliseconds since the epoch. */
28
+ readonly expiresAt: number;
29
+ }
30
+
31
+ /** The `code` of an error from this module. */
32
+ export type ChatgptSignInErrorCode =
33
+ | "LIBFX_CHATGPT_SIGN_IN_INVALID_ADDRESS"
34
+ | "LIBFX_CHATGPT_SIGN_IN_STATE_MISMATCH"
35
+ | "LIBFX_CHATGPT_SIGN_IN_DENIED"
36
+ | "LIBFX_CHATGPT_SIGN_IN_EXPIRED"
37
+ | "LIBFX_CHATGPT_SIGN_IN_FAILED"
38
+ | "LIBFX_CHATGPT_DEVICE_CODE_UNAVAILABLE"
39
+ | "LIBFX_CHATGPT_DEVICE_CODE_EXPIRED"
40
+ | "LIBFX_CHATGPT_REFRESH_FAILED";
41
+
42
+ /** Starts a sign-in where the user pastes back the address OpenAI opens. */
43
+ export declare function startSignIn(): Promise<ChatgptSignIn>;
44
+
45
+ /** Finishes a sign-in from the pasted address, its query, or either wrapped in quotes or backticks. */
46
+ export declare function finishSignIn(signIn: ChatgptSignIn, address: string, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
47
+
48
+ /** Starts a device code sign-in. The account or workspace must allow device code sign-in. */
49
+ export declare function startDeviceSignIn(options?: ChatgptRequestOptions): Promise<ChatgptDeviceSignIn>;
50
+
51
+ /** Checks once: credentials after the user entered the code, otherwise null. */
52
+ export declare function pollDeviceSignIn(device: ChatgptDeviceSignIn, options?: ChatgptRequestOptions): Promise<FxProviderCredentials | null>;
53
+
54
+ /** Waits until the user enters the code, polling at OpenAI's interval. */
55
+ export declare function completeDeviceSignIn(device: ChatgptDeviceSignIn, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
56
+
57
+ /** Refreshes credentials. Codex agents refresh on their own; this is for hosts. */
58
+ export declare function refreshCredentials(credentials: FxProviderCredentials, options?: ChatgptRequestOptions): Promise<FxProviderCredentials>;
@@ -0,0 +1,18 @@
1
+ // `libfx/durable-local` (durable/local.mjs): sessions in files on this
2
+ // machine, through the Workflow local World libfx bundles.
3
+ import type { Durability } from "./libfx.cjs";
4
+
5
+ export type { Durability } from "./libfx.cjs";
6
+
7
+ /** Options for `local()`. */
8
+ export interface LocalOptions {
9
+ /** Where the sessions live. */
10
+ dir?: string;
11
+ /** Each delivery runs as if its function stopped after this long, as on Vercel. No limit by default. */
12
+ maxDurationMs?: number;
13
+ /** How long before the deadline each delivery stops. Default 30000. */
14
+ reserveMs?: number;
15
+ }
16
+
17
+ /** Keeps sessions in files on this machine. A session held by a process that died is free at once. */
18
+ export declare function local(options?: LocalOptions): Durability;
@@ -0,0 +1,22 @@
1
+ // `libfx/durable-vercel` (durable/vercel.mjs): sessions in Vercel's World,
2
+ // the store and queue behind Vercel Workflow, which libfx bundles.
3
+ import type { Durability } from "./libfx.cjs";
4
+
5
+ export type { Durability } from "./libfx.cjs";
6
+
7
+ /** Options for `vercel()`. */
8
+ export interface VercelOptions {
9
+ /** How long before the function's deadline each delivery stops. Default 30000. */
10
+ reserveMs?: number;
11
+ /**
12
+ * Renew a lease of this length in the session's log while a turn runs, every third of it, so a function that stopped
13
+ * frees its session this long after its last renewal. Without it, nothing is renewed: a function holds its session
14
+ * for as long as Vercel Queues keeps the turn's message for it, and a function that died frees its session when the
15
+ * queue delivers that message again, about a second after its process exits, or once the queue's hold on it runs
16
+ * out. At least 1000.
17
+ */
18
+ leaseMs?: number;
19
+ }
20
+
21
+ /** Keeps sessions in Vercel's World. A function holds its session while Vercel Queues keeps its message for it, and the next delivery of that message continues a turn whose function died. */
22
+ export declare function vercel(options?: VercelOptions): Durability;
@@ -0,0 +1,89 @@
1
+ // `libfx/durable-world` (durable/world.mjs): sessions kept in a World the app
2
+ // supplies, such as Workflow's Postgres World.
3
+ import type { Durability } from "./libfx.cjs";
4
+
5
+ export type { Durability } from "./libfx.cjs";
6
+
7
+ /**
8
+ * What libfx uses of a World: its events, streams, and queue. Workflow's
9
+ * Worlds fit. Methods are typed loosely so any conforming World is accepted.
10
+ */
11
+ export interface World {
12
+ readonly specVersion?: number;
13
+ readonly events: {
14
+ create(runId: string, data: unknown, params?: unknown): unknown;
15
+ list(params: unknown): unknown;
16
+ };
17
+ readonly streams: {
18
+ write(runId: string, name: string, chunk: string): unknown;
19
+ writeMulti?(runId: string, name: string, chunks: string[]): unknown;
20
+ get(runId: string, name: string, startIndex?: number): unknown;
21
+ getInfo(runId: string, name: string): unknown;
22
+ };
23
+ queue(queueName: string, message: unknown, options?: unknown): unknown;
24
+ createQueueHandler(prefix: string, handler: (message: unknown, meta: any) => Promise<unknown>): (request: Request) => Promise<Response>;
25
+ /** A World with `registerHandler` delivers to the agent in its process; any other delivers over HTTP to `agent.wakeHandler()`. */
26
+ registerHandler?(prefix: string, handler: (request: Request) => Promise<Response>): unknown;
27
+ getRuntimeDeadline?(): unknown;
28
+ getDeploymentId?(): unknown;
29
+ createRunId?(options?: Readonly<Record<string, unknown>>): string;
30
+ start?(): unknown;
31
+ close?(): unknown;
32
+ }
33
+
34
+ /** A lease as `alive()` sees it: the fields `holderInfo()` returned when it was taken, beside libfx's own. */
35
+ export type WorldLease = Readonly<Record<string, unknown>>;
36
+
37
+ /** A World, or a function that creates one. libfx starts and closes a World it creates; a World passed in stays the app's. */
38
+ export type WorldSource = World | (() => World | Promise<World>);
39
+
40
+ /** What `world()` cannot learn from the World itself. */
41
+ export type WorldOptions = {
42
+ /** The durability's name in errors. Default `"world"`. */
43
+ name?: string;
44
+ /** The queue keeps a message until a delivery acknowledges it, across crashes. When false, `prompt()` stores the prompt first. Default false. */
45
+ queueDurable?: boolean;
46
+ /** How often a waiting worker reads the session again. Default 1000. */
47
+ pollMs?: number;
48
+ /**
49
+ * The queue keeps a message from every other delivery while one handles it, and delivers it again once that
50
+ * delivery's process is gone, as Vercel Queues does. Without liveness, a worker then holds its session for as long as
51
+ * its delivery runs, up to its deadline, and renews nothing: the next delivery of the same message takes the session
52
+ * over. The World must pass each handler call the queue's `messageId` and delivery `attempt`. A worker with no
53
+ * deadline renews its lease all the same. Default false.
54
+ */
55
+ queueLeases?: boolean;
56
+ /**
57
+ * With `queueLeases`: a prompt runs in the request that took it when no worker holds the session, and the queue keeps
58
+ * the prompt as a backstop it delivers only after that request's deadline. The request's lease names the backstop, so
59
+ * if the request died, the backstop takes the session over. Default true with `queueLeases`.
60
+ */
61
+ runInRequest?: boolean;
62
+ /** Keeps the request alive until `work`, a turn it runs after its response, settles, such as Vercel's `waitUntil` or Next's `after()`. */
63
+ waitUntil?(work: Promise<void>): void;
64
+ /** Without liveness, how long a lease lasts after its worker took or last renewed it; renewed every third of that. At least 1000. Default 15000. */
65
+ leaseMs?: number;
66
+ /** Each delivery stops `reserveMs` before `maxDurationMs`, or before the World's `getRuntimeDeadline()`. No limit by default. */
67
+ maxDurationMs?: number;
68
+ /** Default 30000. */
69
+ reserveMs?: number;
70
+ /** What a worker puts in each lease it takes, for `alive()` to read. */
71
+ holderInfo?(): Record<string, unknown>;
72
+ /** The AI Gateway credential when the agent has no `apiKey`. */
73
+ gatewayKey?(): string | Promise<string>;
74
+ } & (
75
+ | {
76
+ /** `alive(lease)` answers whether a lease's holder still runs, so a crashed worker's session frees at once. */
77
+ livenessKnown: true;
78
+ /** True when the holder runs, false when it does not, and null when unknown. */
79
+ alive(lease: WorldLease): boolean | null;
80
+ }
81
+ | {
82
+ /** Without liveness, a worker renews its lease while it runs, and the session of one that died frees `leaseMs` after its last renewal. */
83
+ livenessKnown?: false;
84
+ alive?(lease: WorldLease): boolean | null;
85
+ }
86
+ );
87
+
88
+ /** Keeps sessions in a World the app supplies: its store, streams, and queue. */
89
+ export declare function world(source: WorldSource, options?: WorldOptions): Durability;
@@ -0,0 +1,61 @@
1
+ // `libfx/grok` (grok-sign-in.js): sign in with a Grok plan for a Grok agent.
2
+ import type { FxProviderCredentials, FxFetch } from "./libfx.cjs";
3
+
4
+ /** Options every network step accepts. */
5
+ export interface GrokRequestOptions {
6
+ /** Replaces the global `fetch` for xAI's sign-in endpoints. */
7
+ fetch?: FxFetch;
8
+ signal?: AbortSignal;
9
+ }
10
+
11
+ /** A sign-in in progress. Keep it on the server: `verifier` is secret. */
12
+ export interface GrokSignIn {
13
+ /** Where the user signs in. Afterwards xAI opens an address at `redirectUri` that does not load; the user pastes it back. */
14
+ readonly url: string;
15
+ readonly state: string;
16
+ readonly verifier: string;
17
+ readonly redirectUri: string;
18
+ }
19
+
20
+ /** A device code sign-in in progress. Plain data, so a later request can poll it. */
21
+ export interface GrokDeviceSignIn {
22
+ /** Where the user enters `userCode`. */
23
+ readonly verificationUrl: string;
24
+ readonly userCode: string;
25
+ readonly deviceCode: string;
26
+ /** How often to poll, in milliseconds. */
27
+ readonly intervalMs: number;
28
+ /** Milliseconds since the epoch. */
29
+ readonly expiresAt: number;
30
+ }
31
+
32
+ /** The `code` of an error from this module. */
33
+ export type GrokSignInErrorCode =
34
+ | "LIBFX_GROK_SIGN_IN_INVALID_ADDRESS"
35
+ | "LIBFX_GROK_SIGN_IN_STATE_MISMATCH"
36
+ | "LIBFX_GROK_SIGN_IN_DENIED"
37
+ | "LIBFX_GROK_SIGN_IN_EXPIRED"
38
+ | "LIBFX_GROK_SIGN_IN_FAILED"
39
+ | "LIBFX_GROK_DEVICE_CODE_UNAVAILABLE"
40
+ | "LIBFX_GROK_DEVICE_CODE_EXPIRED"
41
+ | "LIBFX_GROK_ACCOUNT_UNAVAILABLE"
42
+ | "LIBFX_GROK_ACCOUNT_CHANGED"
43
+ | "LIBFX_GROK_REFRESH_FAILED";
44
+
45
+ /** Starts a sign-in where the user pastes back the address xAI opens. */
46
+ export declare function startSignIn(): Promise<GrokSignIn>;
47
+
48
+ /** Finishes a sign-in from the pasted address, its query, or either wrapped in quotes or backticks. */
49
+ export declare function finishSignIn(signIn: GrokSignIn, address: string, options?: GrokRequestOptions): Promise<FxProviderCredentials>;
50
+
51
+ /** Starts a device code sign-in. */
52
+ export declare function startDeviceSignIn(options?: GrokRequestOptions): Promise<GrokDeviceSignIn>;
53
+
54
+ /** Checks once: credentials after the user entered the code, otherwise null. */
55
+ export declare function pollDeviceSignIn(device: GrokDeviceSignIn, options?: GrokRequestOptions): Promise<FxProviderCredentials | null>;
56
+
57
+ /** Waits until the user enters the code, polling at xAI's interval. */
58
+ export declare function completeDeviceSignIn(device: GrokDeviceSignIn, options?: GrokRequestOptions): Promise<FxProviderCredentials>;
59
+
60
+ /** Refreshes credentials. Grok agents refresh on their own; this is for hosts. */
61
+ export declare function refreshCredentials(credentials: FxProviderCredentials, options?: GrokRequestOptions): Promise<FxProviderCredentials>;