pi-meta-oauth 0.7.0 → 0.7.2

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,32 @@
1
+ import { MUSE_USER_AGENT_ENV_VAR } from "./constants.ts";
2
+ import type { MetaEnv, MetaProviderModel } from "./types.ts";
3
+
4
+ /** Models whose max effort requires the captured Muse client fingerprint. */
5
+ export const MUSE_USER_AGENT_MODEL_IDS: ReadonlySet<string> = new Set([
6
+ "muse-spark-1.3-contributor",
7
+ ]);
8
+
9
+ /** Recognize the documented, explicit Muse fingerprint opt-in values. */
10
+ export function isMuseUserAgentEnabled(env: MetaEnv = process.env): boolean {
11
+ const configured = env[MUSE_USER_AGENT_ENV_VAR];
12
+ if (typeof configured !== "string") return false;
13
+ const value = configured.trim().toLowerCase();
14
+ return value === "1" || value === "true" || value === "yes";
15
+ }
16
+
17
+ /**
18
+ * Gate the literal max effort on known fingerprint-dependent models. A future
19
+ * server-advertised mapping to a different effort remains authoritative.
20
+ */
21
+ export function gateMuseMaxEffort(
22
+ model: MetaProviderModel,
23
+ env: MetaEnv = process.env,
24
+ ): MetaProviderModel {
25
+ const map = model.thinkingLevelMap;
26
+ if (!map || !MUSE_USER_AGENT_MODEL_IDS.has(model.id)) return model;
27
+ if (map.max != null && map.max !== "max") return model;
28
+
29
+ const max = isMuseUserAgentEnabled(env) ? "max" : null;
30
+ if (map.max === max) return model;
31
+ return { ...model, thinkingLevelMap: { ...map, max } };
32
+ }
@@ -0,0 +1,382 @@
1
+ import type {
2
+ OAuthCredentials,
3
+ OAuthLoginCallbacks,
4
+ } from "@earendil-works/pi-ai";
5
+ import {
6
+ API_KEY_MINT_URL,
7
+ API_KEY_REFRESH_INTERVAL_MS,
8
+ DEVICE_AUTHORIZATION_URL,
9
+ DEVICE_CODE_GRANT,
10
+ DEVICE_TOKEN_URL,
11
+ META_API_VERSION,
12
+ META_CLIENT_ID,
13
+ META_REQUEST_TIMEOUT_MS,
14
+ } from "./constants.ts";
15
+ import {
16
+ asRecord,
17
+ delay,
18
+ errorDetail,
19
+ postForm,
20
+ RequestTimeoutError,
21
+ responseBody,
22
+ withRequestTimeout,
23
+ } from "./http.ts";
24
+ import type { Fetch, Sleep } from "./types.ts";
25
+
26
+ type Clock = () => number;
27
+
28
+ interface DeviceAuthorization {
29
+ deviceCode: string;
30
+ userCode: string;
31
+ verificationUri: string;
32
+ intervalSeconds: number;
33
+ expiresInSeconds: number;
34
+ }
35
+
36
+ const DEFAULT_POLL_INTERVAL_SECONDS = 5;
37
+ const DEFAULT_DEVICE_EXPIRY_SECONDS = 900;
38
+ const POLL_SLOW_DOWN_SECONDS = 5;
39
+ /** Never poll faster than this, matching Pi's built-in device-code poller. */
40
+ const MIN_POLL_INTERVAL_SECONDS = 1;
41
+ /**
42
+ * Pi's interactive login hides a failure only when its message is exactly this
43
+ * string (as its built-in OAuth flows throw), so a user cancel stays silent.
44
+ */
45
+ const LOGIN_CANCELLED_MESSAGE = "Login cancelled";
46
+
47
+ function isNonBlankString(value: unknown): value is string {
48
+ return typeof value === "string" && value.trim().length > 0;
49
+ }
50
+
51
+ function positiveSeconds(value: unknown, fallback: number): number {
52
+ return typeof value === "number" &&
53
+ Number.isFinite(value) &&
54
+ value > 0 &&
55
+ Number.isFinite(value * 1000)
56
+ ? value
57
+ : fallback;
58
+ }
59
+
60
+ /**
61
+ * Pi writes server-supplied URLs to the terminal, the device URI as a clickable
62
+ * link. Mirror its built-in Meta flow: http(s) only, re-serialized through
63
+ * URL so control characters arrive percent-encoded.
64
+ */
65
+ function trustedHttpUrl(value: unknown): string | undefined {
66
+ if (!isNonBlankString(value)) return undefined;
67
+ try {
68
+ const url = new URL(value);
69
+ return url.protocol === "https:" || url.protocol === "http:"
70
+ ? url.href
71
+ : undefined;
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ function failure(message: string, body: Record<string, unknown>): Error {
78
+ const detail = errorDetail(body);
79
+ return new Error(`${message}${detail ? `: ${detail}` : ""}`);
80
+ }
81
+
82
+ /** Say which request timed out; every other failure passes through. */
83
+ function timeoutAs(message: string): (error: unknown) => never {
84
+ return (error) => {
85
+ if (error instanceof RequestTimeoutError) {
86
+ throw new Error(message, { cause: error });
87
+ }
88
+ throw error;
89
+ };
90
+ }
91
+
92
+ function checkLoginCancellation(signal?: AbortSignal): void {
93
+ if (signal?.aborted) throw new Error(LOGIN_CANCELLED_MESSAGE);
94
+ }
95
+
96
+ function waitForPoll(
97
+ milliseconds: number,
98
+ sleep: Sleep,
99
+ signal?: AbortSignal,
100
+ ): Promise<void> {
101
+ checkLoginCancellation(signal);
102
+ if (!signal) return sleep(milliseconds);
103
+
104
+ return new Promise((resolve, reject) => {
105
+ let timer: ReturnType<typeof setTimeout> | undefined;
106
+ const cleanup = () => {
107
+ signal.removeEventListener("abort", onAbort);
108
+ if (timer !== undefined) clearTimeout(timer);
109
+ };
110
+ const complete = () => {
111
+ cleanup();
112
+ resolve();
113
+ };
114
+ const fail = (error: unknown) => {
115
+ cleanup();
116
+ reject(error);
117
+ };
118
+ const onAbort = () => fail(new Error(LOGIN_CANCELLED_MESSAGE));
119
+ signal.addEventListener("abort", onAbort, { once: true });
120
+ if (signal.aborted) {
121
+ onAbort();
122
+ return;
123
+ }
124
+ // Own the default timer so cancellation also clears its pending work.
125
+ if (sleep === delay) {
126
+ timer = setTimeout(complete, milliseconds);
127
+ return;
128
+ }
129
+ try {
130
+ // Keep both handlers attached if an injected sleep finishes after abort.
131
+ sleep(milliseconds).then(complete, fail);
132
+ } catch (error) {
133
+ fail(error);
134
+ }
135
+ });
136
+ }
137
+
138
+ async function requestDeviceAuthorization(
139
+ fetchImpl: Fetch,
140
+ signal?: AbortSignal,
141
+ ): Promise<DeviceAuthorization> {
142
+ checkLoginCancellation(signal);
143
+ const { response, body } = await postForm(
144
+ DEVICE_AUTHORIZATION_URL,
145
+ { client_id: META_CLIENT_ID },
146
+ fetchImpl,
147
+ signal,
148
+ ).catch(timeoutAs("Meta login request timed out"));
149
+ checkLoginCancellation(signal);
150
+ if (!response.ok) {
151
+ throw failure(
152
+ `Meta login could not be started (HTTP ${response.status})`,
153
+ body,
154
+ );
155
+ }
156
+ const verificationUri =
157
+ trustedHttpUrl(body.verification_uri_complete) ??
158
+ trustedHttpUrl(body.verification_uri);
159
+ if (
160
+ !isNonBlankString(body.device_code) ||
161
+ !isNonBlankString(body.user_code) ||
162
+ verificationUri === undefined
163
+ ) {
164
+ throw new Error(
165
+ "Meta device authorization returned an incomplete response",
166
+ );
167
+ }
168
+
169
+ return {
170
+ deviceCode: body.device_code,
171
+ userCode: body.user_code,
172
+ verificationUri,
173
+ intervalSeconds: positiveSeconds(
174
+ body.interval,
175
+ DEFAULT_POLL_INTERVAL_SECONDS,
176
+ ),
177
+ expiresInSeconds: positiveSeconds(
178
+ body.expires_in,
179
+ DEFAULT_DEVICE_EXPIRY_SECONDS,
180
+ ),
181
+ };
182
+ }
183
+
184
+ async function pollIdentityToken(
185
+ device: DeviceAuthorization,
186
+ deadline: number,
187
+ fetchImpl: Fetch,
188
+ sleep: Sleep,
189
+ now: Clock,
190
+ signal?: AbortSignal,
191
+ ): Promise<string> {
192
+ let intervalSeconds = Math.max(
193
+ MIN_POLL_INTERVAL_SECONDS,
194
+ device.intervalSeconds,
195
+ );
196
+ while (true) {
197
+ checkLoginCancellation(signal);
198
+ const remainingMilliseconds = deadline - now();
199
+ if (remainingMilliseconds <= 0) break;
200
+ await waitForPoll(
201
+ Math.min(intervalSeconds * 1000, remainingMilliseconds),
202
+ sleep,
203
+ signal,
204
+ );
205
+ checkLoginCancellation(signal);
206
+ // A device code can expire while waiting, especially after slow_down.
207
+ const pollMilliseconds = deadline - now();
208
+ if (pollMilliseconds <= 0) break;
209
+
210
+ // Cap a stalled poll at the deadline, where its timeout means expiry.
211
+ const expiresFirst = pollMilliseconds <= META_REQUEST_TIMEOUT_MS;
212
+ const { response, body } = await postForm(
213
+ DEVICE_TOKEN_URL,
214
+ {
215
+ grant_type: DEVICE_CODE_GRANT,
216
+ device_code: device.deviceCode,
217
+ client_id: META_CLIENT_ID,
218
+ },
219
+ fetchImpl,
220
+ signal,
221
+ expiresFirst ? Math.max(1, pollMilliseconds) : META_REQUEST_TIMEOUT_MS,
222
+ ).catch(
223
+ timeoutAs(
224
+ expiresFirst
225
+ ? "Meta login request expired"
226
+ : "Meta login request timed out",
227
+ ),
228
+ );
229
+ checkLoginCancellation(signal);
230
+ if (response.ok && isNonBlankString(body.access_token)) {
231
+ return body.access_token;
232
+ }
233
+ switch (body.error) {
234
+ case "authorization_pending":
235
+ break;
236
+ case "slow_down":
237
+ // RFC 8628 §3.5 adds five seconds; a longer server-supplied interval wins.
238
+ intervalSeconds = Math.max(
239
+ intervalSeconds + POLL_SLOW_DOWN_SECONDS,
240
+ positiveSeconds(body.interval, 0),
241
+ );
242
+ break;
243
+ case "access_denied":
244
+ throw new Error("Meta login was denied");
245
+ case "expired_token":
246
+ throw new Error("Meta login request expired");
247
+ default:
248
+ throw failure(`Meta login failed (HTTP ${response.status})`, body);
249
+ }
250
+ }
251
+ throw new Error("Meta login request expired");
252
+ }
253
+
254
+ export async function mintMetaApiKey(
255
+ identityToken: string,
256
+ fetchImpl: Fetch = globalThis.fetch,
257
+ signal?: AbortSignal,
258
+ timeoutMs = META_REQUEST_TIMEOUT_MS,
259
+ ): Promise<string> {
260
+ signal?.throwIfAborted();
261
+ const { response, body } = await withRequestTimeout(
262
+ signal,
263
+ timeoutMs,
264
+ async (requestSignal) => {
265
+ const response = await fetchImpl(API_KEY_MINT_URL, {
266
+ method: "POST",
267
+ headers: {
268
+ Accept: "application/json",
269
+ Authorization: `Bearer ${identityToken}`,
270
+ "Content-Type": "application/json",
271
+ "x-api-version": META_API_VERSION,
272
+ },
273
+ body: "{}",
274
+ signal: requestSignal,
275
+ });
276
+ return { response, body: await responseBody(response) };
277
+ },
278
+ ).catch(timeoutAs("Meta API-key mint timed out"));
279
+ signal?.throwIfAborted();
280
+ if (!response.ok) {
281
+ const message =
282
+ response.status === 401 || response.status === 403
283
+ ? `Meta session expired (HTTP ${response.status}); run /login meta again`
284
+ : `Meta API-key mint failed (HTTP ${response.status})`;
285
+ throw failure(message, body);
286
+ }
287
+ if (!isNonBlankString(body.api_key)) {
288
+ const actionUrl = trustedHttpUrl(body.action_url);
289
+ const setup = actionUrl ? ` Complete setup at ${actionUrl}.` : "";
290
+ throw new Error(`Meta did not issue an API key.${setup}`);
291
+ }
292
+ return body.api_key;
293
+ }
294
+
295
+ export async function loginMeta(
296
+ callbacks: OAuthLoginCallbacks,
297
+ fetchImpl: Fetch = globalThis.fetch,
298
+ sleep: Sleep = delay,
299
+ now: Clock = Date.now,
300
+ ): Promise<OAuthCredentials> {
301
+ // Older Pi callbacks omit signal; its optional structural shape is compatible.
302
+ const compatibleCallbacks: OAuthLoginCallbacks & { signal?: AbortSignal } =
303
+ callbacks;
304
+ const { signal } = compatibleCallbacks;
305
+ try {
306
+ checkLoginCancellation(signal);
307
+ callbacks.onProgress?.("Starting Meta device authorization…");
308
+ const device = await requestDeviceAuthorization(fetchImpl, signal);
309
+ const deadline = now() + device.expiresInSeconds * 1000;
310
+ callbacks.onDeviceCode({
311
+ userCode: device.userCode,
312
+ verificationUri: device.verificationUri,
313
+ intervalSeconds: device.intervalSeconds,
314
+ expiresInSeconds: device.expiresInSeconds,
315
+ });
316
+ callbacks.onProgress?.("Waiting for Meta login approval…");
317
+ const identityToken = await pollIdentityToken(
318
+ device,
319
+ deadline,
320
+ fetchImpl,
321
+ sleep,
322
+ now,
323
+ signal,
324
+ );
325
+
326
+ checkLoginCancellation(signal);
327
+ callbacks.onProgress?.("Enabling Meta Model API access…");
328
+ const apiKey = await mintMetaApiKey(identityToken, fetchImpl, signal);
329
+ checkLoginCancellation(signal);
330
+ return {
331
+ refresh: identityToken,
332
+ access: apiKey,
333
+ expires: now() + API_KEY_REFRESH_INTERVAL_MS,
334
+ };
335
+ } catch (error) {
336
+ if (signal?.aborted) {
337
+ throw new Error(LOGIN_CANCELLED_MESSAGE, { cause: error });
338
+ }
339
+ throw error;
340
+ }
341
+ }
342
+
343
+ /** Pi 0.84+ also aborts refresh on its own timeout, which is not a cancel. */
344
+ function refreshAborted(signal: AbortSignal, options?: ErrorOptions): Error {
345
+ return new Error(
346
+ asRecord(signal.reason)?.name === "TimeoutError"
347
+ ? "Meta token refresh timed out"
348
+ : "Meta token refresh was cancelled",
349
+ options,
350
+ );
351
+ }
352
+
353
+ export async function refreshMetaToken(
354
+ credentials: OAuthCredentials,
355
+ fetchOrSignal: Fetch | AbortSignal = globalThis.fetch,
356
+ ): Promise<OAuthCredentials> {
357
+ if (!isNonBlankString(credentials.refresh)) {
358
+ throw new Error(
359
+ "Meta login is missing its identity token; run /login meta again",
360
+ );
361
+ }
362
+ // Pi 0.83 calls refreshToken(credentials); Pi 0.84 supplies an AbortSignal.
363
+ // The fetch injection in the same slot keeps the public helper testable.
364
+ const fetchImpl =
365
+ typeof fetchOrSignal === "function" ? fetchOrSignal : globalThis.fetch;
366
+ const signal =
367
+ typeof fetchOrSignal === "function" ? undefined : fetchOrSignal;
368
+ if (signal?.aborted) throw refreshAborted(signal);
369
+ let apiKey: string;
370
+ try {
371
+ apiKey = await mintMetaApiKey(credentials.refresh, fetchImpl, signal);
372
+ } catch (error) {
373
+ if (signal?.aborted) throw refreshAborted(signal, { cause: error });
374
+ throw error;
375
+ }
376
+ if (signal?.aborted) throw refreshAborted(signal);
377
+ return {
378
+ ...credentials,
379
+ access: apiKey,
380
+ expires: Date.now() + API_KEY_REFRESH_INTERVAL_MS,
381
+ };
382
+ }
@@ -0,0 +1,53 @@
1
+ // Pi's extension loader maps only pi-ai's root, compat, oauth, and providers
2
+ // entrypoints; installed extensions cannot resolve `pi-ai/api/*` subpaths.
3
+ import { openAIResponsesApi } from "@earendil-works/pi-ai/compat";
4
+ import {
5
+ META_API_BASE_URL,
6
+ META_ENV_VAR,
7
+ META_PROVIDER_ID,
8
+ } from "./constants.ts";
9
+ import { refreshMetaModels } from "./model-store.ts";
10
+ import { fallbackModels } from "./models.ts";
11
+ import { loginMeta, refreshMetaToken } from "./oauth.ts";
12
+ import {
13
+ applyMetaModelHeaders,
14
+ applyMetaResponsesCacheHints,
15
+ } from "./request-policy.ts";
16
+ import type { MetaProviderConfig } from "./types.ts";
17
+
18
+ const { streamSimple } = openAIResponsesApi();
19
+
20
+ export function createMetaProviderConfig(): MetaProviderConfig {
21
+ return {
22
+ name: "Meta Model API",
23
+ baseUrl: META_API_BASE_URL,
24
+ api: "openai-responses",
25
+ apiKey: `$${META_ENV_VAR}`,
26
+ streamSimple(model, context, options) {
27
+ // Pi's global hooks see the session selection, not the in-flight model.
28
+ // This adapter runs after auth/header resolution with the request's endpoint.
29
+ const requestModel = { ...model, api: "openai-responses" as const };
30
+ const headers = { ...model.headers, ...options?.headers };
31
+ applyMetaModelHeaders(headers, requestModel);
32
+ return streamSimple(requestModel, context, {
33
+ ...options,
34
+ headers,
35
+ onPayload: async (payload, sentModel) => {
36
+ const replacement = await options?.onPayload?.(payload, sentModel);
37
+ const body = replacement === undefined ? payload : replacement;
38
+ return model.provider === META_PROVIDER_ID
39
+ ? (applyMetaResponsesCacheHints(body) ?? body)
40
+ : body;
41
+ },
42
+ });
43
+ },
44
+ models: fallbackModels(),
45
+ refreshModels: refreshMetaModels,
46
+ oauth: {
47
+ name: "Meta Model API (browser login)",
48
+ login: loginMeta,
49
+ refreshToken: refreshMetaToken,
50
+ getApiKey: (credentials) => credentials.access,
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ META_PROMPT_CACHE_RETENTION,
3
+ META_PROVIDER_ID,
4
+ MUSE_USER_AGENT,
5
+ } from "./constants.ts";
6
+ import { asRecord } from "./http.ts";
7
+ import {
8
+ isMuseUserAgentEnabled,
9
+ MUSE_USER_AGENT_MODEL_IDS,
10
+ } from "./muse-policy.ts";
11
+ import type { MetaEnv } from "./types.ts";
12
+
13
+ type ProviderHeaders = Record<string, string | null>;
14
+
15
+ /**
16
+ * Match the direct HTTPS Meta /v1 endpoint, allowing URL-normalized equivalent
17
+ * hostnames, the default port, and trailing slashes. Other hosts, paths, schemes,
18
+ * and ports never receive the Muse client fingerprint.
19
+ */
20
+ export function isDirectMetaModelApiUrl(baseUrl: unknown): boolean {
21
+ if (typeof baseUrl !== "string" || !baseUrl) return false;
22
+ // URL.search/hash omit empty delimiters, which still break path appending.
23
+ if (baseUrl.includes("?") || baseUrl.includes("#")) return false;
24
+ try {
25
+ const parsed = new URL(baseUrl);
26
+ return (
27
+ parsed.protocol === "https:" &&
28
+ parsed.hostname.toLowerCase() === "api.meta.ai" &&
29
+ parsed.port === "" &&
30
+ parsed.username === "" &&
31
+ parsed.password === "" &&
32
+ parsed.pathname.replace(/\/+$/, "") === "/v1"
33
+ );
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Set the fingerprint only on direct endpoints without an explicit User-Agent.
41
+ * Header names are case-insensitive; null is an explicit suppression and wins.
42
+ * The caller owns provider, model, and opt-in checks; use applyMetaModelHeaders
43
+ * when handling provider events.
44
+ */
45
+ export function applyMetaUserAgentFingerprint(
46
+ headers: ProviderHeaders,
47
+ baseUrl: unknown,
48
+ ): boolean {
49
+ if (!isDirectMetaModelApiUrl(baseUrl) || !asRecord(headers)) return false;
50
+ if (
51
+ Object.keys(headers).some((name) => name.toLowerCase() === "user-agent")
52
+ ) {
53
+ return false;
54
+ }
55
+ headers["User-Agent"] = MUSE_USER_AGENT;
56
+ return true;
57
+ }
58
+
59
+ /** Apply every fingerprint guard in one place for the provider headers hook. */
60
+ export function applyMetaModelHeaders(
61
+ headers: ProviderHeaders,
62
+ model: unknown,
63
+ env: MetaEnv = process.env,
64
+ ): boolean {
65
+ const requestModel = asRecord(model);
66
+ if (
67
+ requestModel?.provider !== META_PROVIDER_ID ||
68
+ typeof requestModel.id !== "string" ||
69
+ !MUSE_USER_AGENT_MODEL_IDS.has(requestModel.id) ||
70
+ !isMuseUserAgentEnabled(env)
71
+ ) {
72
+ return false;
73
+ }
74
+ return applyMetaUserAgentFingerprint(headers, requestModel.baseUrl);
75
+ }
76
+
77
+ /** Set the Responses cache default and remove unsupported empty/none reasoning. */
78
+ export function applyMetaResponsesCacheHints(
79
+ payload: unknown,
80
+ ): Record<string, unknown> | undefined {
81
+ const body = asRecord(payload);
82
+ if (!body) return undefined;
83
+ if (body.prompt_cache_retention === undefined) {
84
+ body.prompt_cache_retention = META_PROMPT_CACHE_RETENTION;
85
+ }
86
+ const reasoning = asRecord(body.reasoning);
87
+ if (
88
+ reasoning &&
89
+ (reasoning.effort === "none" ||
90
+ reasoning.effort === undefined ||
91
+ reasoning.effort === null)
92
+ ) {
93
+ delete body.reasoning;
94
+ }
95
+ return body;
96
+ }
@@ -0,0 +1,33 @@
1
+ import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
2
+
3
+ type ProviderModel = NonNullable<ProviderConfig["models"]>[number];
4
+
5
+ /** Responses hints spanning the supported Pi versions, including newer flags. */
6
+ export interface MetaResponsesCompat {
7
+ supportsReasoningEffort?: boolean;
8
+ supportsToolSearch?: boolean;
9
+ supportsDeveloperRole?: boolean;
10
+ supportsMidConvoSystemMessages?: boolean;
11
+ supportsLongCacheRetention?: boolean;
12
+ supportsStrictMode?: boolean;
13
+ supportsOpenAIGrammarTools?: boolean;
14
+ supportsAdditionalTools?: boolean;
15
+ supportsExplicitPromptCacheMode?: boolean;
16
+ supportsMaxOutputTokens?: boolean;
17
+ sessionAffinityFormat?: "openai" | "openai-nosession" | "openrouter";
18
+ }
19
+
20
+ /** This provider never registers metadata for another transport API. */
21
+ export type MetaProviderModel = Omit<ProviderModel, "api" | "compat"> & {
22
+ api?: "openai-responses";
23
+ compat?: MetaResponsesCompat;
24
+ };
25
+ export type MetaProviderConfig = Omit<ProviderConfig, "models"> & {
26
+ models: MetaProviderModel[];
27
+ };
28
+ export type MetaEnv = Record<string, string | undefined>;
29
+ /** Depend on fetch's callable contract, without runtime-specific static helpers. */
30
+ export type Fetch = (
31
+ ...args: Parameters<typeof globalThis.fetch>
32
+ ) => ReturnType<typeof globalThis.fetch>;
33
+ export type Sleep = (milliseconds: number) => Promise<void>;