zitejs 0.10.0-start.10 → 0.10.0-start.13

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.
@@ -187,3 +187,33 @@ function redirectParamOf(href) {
187
187
  (0, vitest_1.expect)(params.get('usageToken')).toBeNull();
188
188
  });
189
189
  });
190
+ // AUTH-03: what the platform's auth routes receive from the client, on the app's own origin.
191
+ (0, vitest_1.describe)("the client's calls", () => {
192
+ const calls = () => {
193
+ const sent = [];
194
+ vitest_1.vi.stubGlobal('fetch', vitest_1.vi.fn(async (input, init) => {
195
+ const request = new Request(new URL(String(input instanceof Request ? input.url : input), 'https://app.zite.so'), init);
196
+ sent.push({
197
+ path: new URL(request.url).pathname,
198
+ method: request.method,
199
+ body: request.method === 'POST' ? await request.json().catch(() => null) : null,
200
+ });
201
+ return Response.json({});
202
+ }));
203
+ return sent;
204
+ };
205
+ (0, vitest_1.it)('sign in by link, sign out and edit the profile on /api/auth', async () => {
206
+ const sent = calls();
207
+ // The client takes its fetch when the module loads.
208
+ vitest_1.vi.resetModules();
209
+ const { signIn, signOut, updateProfile } = await Promise.resolve().then(() => __importStar(require('./index.js')));
210
+ await signIn.magicLink({ email: 'ada@x.co' });
211
+ await signOut();
212
+ await updateProfile({ firstName: 'Ada' });
213
+ (0, vitest_1.expect)(sent).toEqual([
214
+ { path: '/api/auth/sign-in/magic-link', method: 'POST', body: vitest_1.expect.objectContaining({ email: 'ada@x.co' }) },
215
+ { path: '/api/auth/sign-out', method: 'POST', body: vitest_1.expect.anything() },
216
+ { path: '/api/auth/update-user', method: 'POST', body: { firstName: 'Ada' } },
217
+ ]);
218
+ });
219
+ });
@@ -17,15 +17,17 @@ const renderStartServerEntry = (target = "cloudflare") => target === "cloudflare
17
17
  import handler from '@tanstack/react-start/server-entry';
18
18
  import { createZiteWorker } from 'zitejs/start/worker';
19
19
  import { getRouter } from '../src/router';
20
+ import { startInstance } from './start';
20
21
 
21
- export default createZiteWorker({ handler, getRouter });
22
+ export default createZiteWorker({ handler, getRouter, startInstance });
22
23
  `
23
24
  : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
24
25
  import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
25
26
  import { createZiteFetch } from 'zitejs/start/server';
26
27
  import { getRouter } from '../src/router';
28
+ import { startInstance } from './start';
27
29
 
28
- export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter }) });
30
+ export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter, startInstance }) });
29
31
  `;
30
32
  exports.renderStartServerEntry = renderStartServerEntry;
31
33
  /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
@@ -152,3 +152,33 @@ describe('loginWithRedirect', () => {
152
152
  expect(params.get('usageToken')).toBeNull();
153
153
  });
154
154
  });
155
+ // AUTH-03: what the platform's auth routes receive from the client, on the app's own origin.
156
+ describe("the client's calls", () => {
157
+ const calls = () => {
158
+ const sent = [];
159
+ vi.stubGlobal('fetch', vi.fn(async (input, init) => {
160
+ const request = new Request(new URL(String(input instanceof Request ? input.url : input), 'https://app.zite.so'), init);
161
+ sent.push({
162
+ path: new URL(request.url).pathname,
163
+ method: request.method,
164
+ body: request.method === 'POST' ? await request.json().catch(() => null) : null,
165
+ });
166
+ return Response.json({});
167
+ }));
168
+ return sent;
169
+ };
170
+ it('sign in by link, sign out and edit the profile on /api/auth', async () => {
171
+ const sent = calls();
172
+ // The client takes its fetch when the module loads.
173
+ vi.resetModules();
174
+ const { signIn, signOut, updateProfile } = await import('./index.js');
175
+ await signIn.magicLink({ email: 'ada@x.co' });
176
+ await signOut();
177
+ await updateProfile({ firstName: 'Ada' });
178
+ expect(sent).toEqual([
179
+ { path: '/api/auth/sign-in/magic-link', method: 'POST', body: expect.objectContaining({ email: 'ada@x.co' }) },
180
+ { path: '/api/auth/sign-out', method: 'POST', body: expect.anything() },
181
+ { path: '/api/auth/update-user', method: 'POST', body: { firstName: 'Ada' } },
182
+ ]);
183
+ });
184
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * What a call on an org's own AI key used, read from the provider's answer as
3
+ * classic's runtime reads it. For analytics only: the org pays the provider.
4
+ * OpenAI and Anthropic; the host meters Zite's own key itself.
5
+ */
6
+ export type AiProvider = "openai" | "anthropic";
7
+ export type AiUsageRecord = {
8
+ provider: AiProvider;
9
+ model?: string;
10
+ inputTokens?: number;
11
+ outputTokens?: number;
12
+ cachedReadInputTokens?: number;
13
+ cacheCreationInputTokens?: number;
14
+ isStreaming?: true;
15
+ };
16
+ export declare const AI_USAGE_HOSTS: Record<string, AiProvider>;
17
+ /** As classic: an OpenAI chat stream reports its usage only when asked to. The body to send instead, if any. */
18
+ export declare const withStreamUsage: (url: URL, method: string, body: unknown) => string | undefined;
19
+ /** The response to hand the app, and its usage once read; a stream is split so the app reads one copy as it arrives. */
20
+ export declare const meterAiResponse: (provider: AiProvider, response: Response) => {
21
+ response: Response;
22
+ usage: Promise<AiUsageRecord | undefined>;
23
+ };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * What a call on an org's own AI key used, read from the provider's answer as
3
+ * classic's runtime reads it. For analytics only: the org pays the provider.
4
+ * OpenAI and Anthropic; the host meters Zite's own key itself.
5
+ */
6
+ export const AI_USAGE_HOSTS = {
7
+ "api.openai.com": "openai",
8
+ "api.anthropic.com": "anthropic",
9
+ };
10
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11
+ const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
12
+ /** As classic: an OpenAI chat stream reports its usage only when asked to. The body to send instead, if any. */
13
+ export const withStreamUsage = (url, method, body) => {
14
+ if (method !== "POST" || typeof body !== "string" || !url.pathname.endsWith("/chat/completions"))
15
+ return undefined;
16
+ try {
17
+ const json = JSON.parse(body);
18
+ if (!isRecord(json) || json.stream !== true)
19
+ return undefined;
20
+ const options = isRecord(json.stream_options) ? json.stream_options : {};
21
+ if (options.include_usage === true)
22
+ return undefined;
23
+ return JSON.stringify({ ...json, stream_options: { ...options, include_usage: true } });
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ };
29
+ // OpenAI counts cached tokens inside its input; classic takes them apart.
30
+ const tokensFromJson = (provider, body) => {
31
+ const tokens = {};
32
+ if (!isRecord(body))
33
+ return tokens;
34
+ if (provider === "openai") {
35
+ const source = isRecord(body.response) ? body.response : body;
36
+ if (typeof source.model === "string")
37
+ tokens.model = source.model;
38
+ const usage = isRecord(source.usage) ? source.usage : undefined;
39
+ if (!usage)
40
+ return tokens;
41
+ const details = (name) => (isRecord(usage[name]) ? usage[name] : {});
42
+ const cached = details("prompt_tokens_details").cached_tokens ?? details("input_tokens_details").cached_tokens;
43
+ const input = usage.prompt_tokens ?? usage.input_tokens;
44
+ const output = usage.completion_tokens ?? usage.output_tokens;
45
+ if (isNumber(input))
46
+ tokens.inputTokens = input - (isNumber(cached) ? cached : 0);
47
+ if (isNumber(output))
48
+ tokens.outputTokens = output;
49
+ if (isNumber(cached))
50
+ tokens.cachedReadInputTokens = cached;
51
+ return tokens;
52
+ }
53
+ // A stream splits these: message_start has the input, message_delta the running output.
54
+ const message = isRecord(body.message) ? body.message : body;
55
+ if (typeof message.model === "string")
56
+ tokens.model = message.model;
57
+ const usage = isRecord(message.usage) ? message.usage : isRecord(body.usage) ? body.usage : undefined;
58
+ if (!usage)
59
+ return tokens;
60
+ if (isNumber(usage.input_tokens))
61
+ tokens.inputTokens = usage.input_tokens;
62
+ if (isNumber(usage.output_tokens))
63
+ tokens.outputTokens = usage.output_tokens;
64
+ if (isNumber(usage.cache_read_input_tokens))
65
+ tokens.cachedReadInputTokens = usage.cache_read_input_tokens;
66
+ if (isNumber(usage.cache_creation_input_tokens))
67
+ tokens.cacheCreationInputTokens = usage.cache_creation_input_tokens;
68
+ return tokens;
69
+ };
70
+ // Later readings win: Anthropic's output is cumulative, OpenAI's usage comes last.
71
+ const merge = (into, next) => ({
72
+ model: next.model ?? into.model,
73
+ inputTokens: next.inputTokens ?? into.inputTokens,
74
+ outputTokens: next.outputTokens ?? into.outputTokens,
75
+ cachedReadInputTokens: next.cachedReadInputTokens ?? into.cachedReadInputTokens,
76
+ cacheCreationInputTokens: next.cacheCreationInputTokens ?? into.cacheCreationInputTokens,
77
+ });
78
+ const MAX_PENDING_LINE = 1024 * 1024;
79
+ const tokensFromStream = async (provider, stream) => {
80
+ const reader = stream.getReader();
81
+ const decoder = new TextDecoder();
82
+ let pending = "";
83
+ let tokens = {};
84
+ const readLine = (line) => {
85
+ if (!line.startsWith("data:"))
86
+ return;
87
+ const payload = line.slice(5).trim();
88
+ if (!payload || payload === "[DONE]")
89
+ return;
90
+ try {
91
+ tokens = merge(tokens, tokensFromJson(provider, JSON.parse(payload)));
92
+ }
93
+ catch {
94
+ // Not JSON; nothing to count.
95
+ }
96
+ };
97
+ try {
98
+ for (;;) {
99
+ const { done, value } = await reader.read();
100
+ if (done)
101
+ break;
102
+ pending += decoder.decode(value, { stream: true });
103
+ const lines = pending.split("\n");
104
+ pending = lines.pop() ?? "";
105
+ if (pending.length > MAX_PENDING_LINE)
106
+ pending = "";
107
+ lines.forEach(readLine);
108
+ }
109
+ readLine(pending);
110
+ }
111
+ catch {
112
+ // What was read before the stream broke still counts.
113
+ }
114
+ return tokens;
115
+ };
116
+ const counted = (tokens) => tokens.inputTokens !== undefined || tokens.outputTokens !== undefined;
117
+ const usageOf = (provider, tokens, streaming) => counted(tokens)
118
+ ? {
119
+ provider,
120
+ ...Object.fromEntries(Object.entries(tokens).filter(([, value]) => value !== undefined)),
121
+ ...(streaming && { isStreaming: true }),
122
+ }
123
+ : undefined;
124
+ /** The response to hand the app, and its usage once read; a stream is split so the app reads one copy as it arrives. */
125
+ export const meterAiResponse = (provider, response) => {
126
+ const type = response.headers.get("content-type") ?? "";
127
+ if (!response.ok || !response.body)
128
+ return { response, usage: Promise.resolve(undefined) };
129
+ if (type.includes("text/event-stream")) {
130
+ const [forApp, forUs] = response.body.tee();
131
+ return {
132
+ response: new Response(forApp, response),
133
+ usage: tokensFromStream(provider, forUs).then((tokens) => usageOf(provider, tokens, true)),
134
+ };
135
+ }
136
+ if (!type.includes("json"))
137
+ return { response, usage: Promise.resolve(undefined) };
138
+ return {
139
+ response,
140
+ usage: response
141
+ .clone()
142
+ .json()
143
+ .then((json) => usageOf(provider, tokensFromJson(provider, json), false))
144
+ .catch(() => undefined),
145
+ };
146
+ };
@@ -2,10 +2,7 @@ import { type AirtableSettings } from "./dispatch.js";
2
2
  import type { PlatformSession, ZiteRequestContext } from "../types.js";
3
3
  /** The build's Airtable integration `integrationId`, or undefined when it isn't one, or the host can't say. */
4
4
  export declare const airtableSettingsOf: (session: PlatformSession, integrationId: string) => Promise<AirtableSettings | undefined>;
5
- /**
6
- * One generated Airtable table call, run here with the connection's placeholder
7
- * token; egress swaps the real one in for Airtable alone, and refreshes it.
8
- */
5
+ /** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
9
6
  export declare function runAirtableCall(host: ZiteRequestContext, session: PlatformSession, integrationId: string, settings: AirtableSettings, methodName: string, params: unknown, access: "signedIn" | "anyone" | "webhook" | "workflow"): Promise<unknown>;
10
7
  /** For tests: forget what this isolate learned. */
11
8
  export declare const resetAirtableSettings: () => void;
@@ -1,8 +1,8 @@
1
1
  import { executeAirtableCall } from "./dispatch.js";
2
+ import { INTEGRATIONS_PLATFORM_ID } from "../credentials.js";
2
3
  import { ZiteError, isZiteErrorCode } from "../errors.js";
4
+ import { rememberConnection } from "../outbound.js";
3
5
  const AIRTABLE_API = "https://api.airtable.com";
4
- // The platform id `integrations.get` is answered under (`INTEGRATIONS_SDK_INTEGRATION_ID`).
5
- const CONNECTIONS = "__connections__";
6
6
  // A build's integrations don't change, so per isolate: only answers, never a pending call.
7
7
  const settingsHeld = new Map();
8
8
  let unsupported = false;
@@ -29,21 +29,20 @@ export const airtableSettingsOf = async (session, integrationId) => {
29
29
  throw error;
30
30
  }
31
31
  };
32
- /**
33
- * One generated Airtable table call, run here with the connection's placeholder
34
- * token; egress swaps the real one in for Airtable alone, and refreshes it.
35
- */
32
+ /** One generated Airtable table call, run here with the connection's token, which the outbound fetch refreshes on a 401. */
36
33
  export async function runAirtableCall(host, session, integrationId, settings, methodName, params, access) {
37
34
  // As classic: a demo reaches no connected account.
38
35
  if (host.mode === "demo")
39
36
  throw new ZiteError("FORBIDDEN", "This action is not available in demo mode");
40
- const connection = await session.call({
41
- integrationId: CONNECTIONS,
42
- className: "Connections",
43
- methodName: "get",
44
- params: { name: integrationId },
45
- access,
46
- });
37
+ const connection = host.credentials
38
+ ? await host.credentials.get(integrationId)
39
+ : await session.call({
40
+ integrationId: INTEGRATIONS_PLATFORM_ID,
41
+ className: "Connections",
42
+ methodName: "get",
43
+ params: { name: integrationId },
44
+ access,
45
+ });
47
46
  if ("error" in connection) {
48
47
  const { code, message, userFacingMessage } = connection.error;
49
48
  throw new ZiteError({ code: isZiteErrorCode(code) ? code : "INTERNAL_ERROR", message, userFacingMessage });
@@ -52,6 +51,7 @@ export async function runAirtableCall(host, session, integrationId, settings, me
52
51
  if (typeof token !== "string") {
53
52
  throw new ZiteError("INTERNAL_ERROR", `The ${integrationId} connection has no token`);
54
53
  }
54
+ rememberConnection(host, integrationId, connection.value);
55
55
  host.airtableReads ??= new Map();
56
56
  return executeAirtableCall({
57
57
  settings,
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Where `integrations.get` reads a connection. In Zite's cloud the host
3
+ * answers it on the request's session; an app run elsewhere names its own
4
+ * source in its server entry: memory or the environment here, a shared cache
5
+ * once one exists.
6
+ */
7
+ import type { PlatformCallInput, PlatformCallResult, PlatformSession } from "./types.js";
8
+ export type CredentialSource = {
9
+ get(name: string): Promise<PlatformCallResult>;
10
+ /** After a provider answered 401: the connection refreshed, where the source can. */
11
+ refresh?(name: string): Promise<PlatformCallResult>;
12
+ };
13
+ export declare const INTEGRATIONS_PLATFORM_ID = "__connections__";
14
+ /** The host's, on the request's session. */
15
+ export declare const sessionCredentials: (session: PlatformSession, access: PlatformCallInput["access"]) => CredentialSource;
16
+ /** Connections held in memory, by name, as `integrations.get` answers them; `refresh` fetches one anew. */
17
+ export declare const memoryCredentials: (connections: Record<string, unknown>, refresh?: (name: string) => Promise<unknown>) => CredentialSource;
18
+ /** Connections from `ZITE_CONNECTIONS`, JSON by name, for an app run outside Zite. */
19
+ export declare const envCredentials: (env?: Record<string, string | undefined>) => CredentialSource;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Where `integrations.get` reads a connection. In Zite's cloud the host
3
+ * answers it on the request's session; an app run elsewhere names its own
4
+ * source in its server entry: memory or the environment here, a shared cache
5
+ * once one exists.
6
+ */
7
+ export const INTEGRATIONS_PLATFORM_ID = "__connections__";
8
+ /** The host's, on the request's session. */
9
+ export const sessionCredentials = (session, access) => ({
10
+ get: (name) => session.call({ integrationId: INTEGRATIONS_PLATFORM_ID, className: "Connections", methodName: "get", params: { name }, access }),
11
+ ...(session.refreshConnection && { refresh: (name) => session.refreshConnection(name) }),
12
+ });
13
+ const notFound = (name) => ({
14
+ error: { code: "BAD_REQUEST", message: `Integration ${name} not found` },
15
+ });
16
+ /** Connections held in memory, by name, as `integrations.get` answers them; `refresh` fetches one anew. */
17
+ export const memoryCredentials = (connections, refresh) => ({
18
+ get: async (name) => (Object.hasOwn(connections, name) ? { value: connections[name] } : notFound(name)),
19
+ ...(refresh && {
20
+ refresh: async (name) => {
21
+ const fresh = await refresh(name);
22
+ if (fresh === undefined)
23
+ return notFound(name);
24
+ connections[name] = fresh;
25
+ return { value: fresh };
26
+ },
27
+ }),
28
+ });
29
+ /** Connections from `ZITE_CONNECTIONS`, JSON by name, for an app run outside Zite. */
30
+ export const envCredentials = (env = process.env) => memoryCredentials(JSON.parse(env.ZITE_CONNECTIONS ?? "{}"));
@@ -12,15 +12,17 @@ export const renderStartServerEntry = (target = "cloudflare") => target === "clo
12
12
  import handler from '@tanstack/react-start/server-entry';
13
13
  import { createZiteWorker } from 'zitejs/start/worker';
14
14
  import { getRouter } from '../src/router';
15
+ import { startInstance } from './start';
15
16
 
16
- export default createZiteWorker({ handler, getRouter });
17
+ export default createZiteWorker({ handler, getRouter, startInstance });
17
18
  `
18
19
  : `// Generated by zitejs for TanStack Start apps. The platform owns this file.
19
20
  import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
20
21
  import { createZiteFetch } from 'zitejs/start/server';
21
22
  import { getRouter } from '../src/router';
23
+ import { startInstance } from './start';
22
24
 
23
- export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter }) });
25
+ export default createServerEntry({ fetch: createZiteFetch({ handler, getRouter, startInstance }) });
24
26
  `;
25
27
  /** The start instance: the platform's middleware, then the app's own `src/start.ts` when it has one. */
26
28
  export const renderStartInstance = (hasAppStart) => hasAppStart
@@ -6,6 +6,7 @@ import type { AnyFunctionMiddleware, AnyRequestMiddleware, StartInstance } from
6
6
  * adapters first, then whatever the app's own `src/start.ts` sets.
7
7
  */
8
8
  export declare function createZiteStart<const TAdapters extends ReadonlyArray<AnySerializationAdapter> = [], TDefaultSsr extends SSROption = SSROption, const TRequest extends ReadonlyArray<AnyRequestMiddleware> = [], const TFunction extends ReadonlyArray<AnyFunctionMiddleware> = []>(app?: StartInstance<TAdapters, TDefaultSsr, TRequest, TFunction>): StartInstance<readonly [import("@tanstack/react-router").SerializationAdapter<Error, {
9
+ stack?: string | undefined;
9
10
  code: "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMITED" | "INTERNAL_ERROR" | "INVALID_INPUT";
10
11
  message: string;
11
12
  }, never>, ...TAdapters], TDefaultSsr, readonly [import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, undefined>, ...TRequest], readonly [import("@tanstack/react-start").FunctionMiddlewareAfterClient<{}, unknown, undefined, undefined, undefined>, import("@tanstack/react-start").FunctionMiddlewareAfterServer<{}, unknown, undefined, undefined, undefined, undefined, undefined>, ...TFunction]>;
@@ -5,6 +5,7 @@ import { createSerializationAdapter, redirect } from "@tanstack/react-router";
5
5
  import { toClientError, ZiteError } from "./errors.js";
6
6
  import { isSignInRequired, loginHref } from "./index.js";
7
7
  import { errorOf, preview } from "./runLog.js";
8
+ import { currentHost } from "./host.js";
8
9
  import { getHost, inScope } from "./scope.js";
9
10
  // TanStack drops its default CSRF check once an app has a start instance.
10
11
  const csrf = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" });
@@ -50,16 +51,24 @@ const runCapture = createMiddleware({ type: "function" }).server(async ({ next,
50
51
  });
51
52
  /**
52
53
  * Every error a server function or a page's loader sends the browser arrives as a ZiteError, with its code and the
53
- * message classic would show. Anything else on it, a stack or an SDK's request, stays on the server.
54
+ * message classic would show. Anything else on it, an SDK's request, stays on the server; its stack goes only to the
55
+ * editor's preview, for "Fix it for me".
54
56
  */
55
57
  const ziteErrorAdapter = createSerializationAdapter({
56
58
  key: "zite-error",
57
59
  test: (value) => value instanceof Error,
58
60
  toSerializable: (error) => {
59
61
  const { code, message } = toClientError(error);
60
- return { code, message };
62
+ // `currentHost`, not `getHost`: this adapter ships to the browser, which mustn't bundle the request scope.
63
+ const stack = currentHost()?.mode === "preview" ? error.stack : undefined;
64
+ return { code, message, ...(stack && { stack }) };
65
+ },
66
+ fromSerializable: ({ code, message, stack }) => {
67
+ const error = new ZiteError(code, message);
68
+ if (stack)
69
+ error.stack = stack;
70
+ return error;
61
71
  },
62
- fromSerializable: ({ code, message }) => new ZiteError(code, message),
63
72
  });
64
73
  /**
65
74
  * The start instance every Start app builds with: the platform's middleware and
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Every fetch an app makes on the server, wrapped as classic's runtime wraps
3
+ * it: logged, every secret it knows redacted, with the connection whose token
4
+ * it carries and what an org's own AI key used; a 401 on a connection's token
5
+ * refreshed and sent once more; and a call on Zite's own AI key sent to the
6
+ * host over the app's platform binding.
7
+ */
8
+ import type { ZiteRequestContext } from "./types.js";
9
+ /** An `integrations.get` answer, whose values name the calls that carry them. */
10
+ export declare const rememberConnection: (host: ZiteRequestContext, name: string, connection: unknown) => void;
11
+ export declare const createOutboundFetch: (nativeFetch: typeof fetch) => typeof fetch;