@inferencesh/sdk 0.20.1 → 0.22.0

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/agent/actions.js +1 -4
  3. package/dist/agent/api.d.ts +37 -6
  4. package/dist/agent/api.js +37 -3
  5. package/dist/agent/reducer.js +23 -7
  6. package/dist/agent/types.d.ts +30 -5
  7. package/dist/api/agents.d.ts +9 -3
  8. package/dist/api/agents.js +87 -65
  9. package/dist/api/api-keys.d.ts +2 -5
  10. package/dist/api/apps.d.ts +9 -2
  11. package/dist/api/apps.js +9 -1
  12. package/dist/api/chats.d.ts +13 -3
  13. package/dist/api/chats.js +13 -2
  14. package/dist/api/engines.d.ts +4 -2
  15. package/dist/api/engines.js +3 -2
  16. package/dist/api/flow-runs.d.ts +7 -2
  17. package/dist/api/flow-runs.js +7 -2
  18. package/dist/api/flows.d.ts +13 -2
  19. package/dist/api/flows.js +15 -1
  20. package/dist/api/knowledge.d.ts +7 -4
  21. package/dist/api/knowledge.js +6 -4
  22. package/dist/api/mcp-servers.d.ts +2 -1
  23. package/dist/api/projects.d.ts +5 -3
  24. package/dist/api/projects.js +3 -2
  25. package/dist/api/secrets.d.ts +2 -2
  26. package/dist/api/tasks.js +39 -60
  27. package/dist/api/teams.d.ts +4 -2
  28. package/dist/api/teams.js +3 -2
  29. package/dist/api/write-bodies.d.ts +110 -0
  30. package/dist/api/write-bodies.js +1 -0
  31. package/dist/http/client.d.ts +68 -7
  32. package/dist/http/client.js +138 -97
  33. package/dist/http/index.d.ts +2 -2
  34. package/dist/http/loopback.d.ts +8 -0
  35. package/dist/http/loopback.js +18 -0
  36. package/dist/http/stream-until-terminal.d.ts +55 -0
  37. package/dist/http/stream-until-terminal.js +88 -0
  38. package/dist/http/stream.d.ts +1 -0
  39. package/dist/http/stream.js +7 -3
  40. package/dist/http/streamable.d.ts +25 -5
  41. package/dist/http/streamable.js +28 -40
  42. package/dist/index.d.ts +4 -2
  43. package/dist/index.js +1 -0
  44. package/dist/proxy/endpoints.json +156 -0
  45. package/dist/proxy/express.d.ts +5 -6
  46. package/dist/proxy/express.js +18 -2
  47. package/dist/proxy/hono.d.ts +5 -3
  48. package/dist/proxy/hono.js +4 -3
  49. package/dist/proxy/index.d.ts +165 -6
  50. package/dist/proxy/index.js +409 -25
  51. package/dist/proxy/nextjs.d.ts +29 -17
  52. package/dist/proxy/nextjs.js +72 -62
  53. package/dist/proxy/remix.d.ts +5 -3
  54. package/dist/proxy/remix.js +4 -3
  55. package/dist/proxy/svelte.d.ts +5 -4
  56. package/dist/proxy/svelte.js +4 -4
  57. package/dist/types.d.ts +591 -271
  58. package/dist/types.js +117 -252
  59. package/dist/version.d.ts +1 -0
  60. package/dist/version.js +7 -0
  61. package/package.json +2 -1
@@ -4,13 +4,26 @@
4
4
  * Protects API keys by proxying requests from frontend apps through your server.
5
5
  * Supports Next.js (App & Page Router), Express, Hono, Remix, and SvelteKit.
6
6
  *
7
+ * The proxy forwards only what a frontend needs to run things (app runs,
8
+ * agent chats, and reading or streaming their task or chat; see
9
+ * PROXY_ENDPOINTS) and refuses every other API call with 403.
10
+ *
7
11
  * @example Next.js App Router
8
12
  * ```typescript
9
13
  * // app/api/inference/proxy/route.ts
10
14
  * import { createHandler } from "@inferencesh/sdk/proxy/nextjs";
11
- * export const { GET, POST, PUT } = createHandler();
15
+ * export const { GET, POST } = createHandler({
16
+ * allowedEndpoints: ["my-team/support-agent"],
17
+ * isAuthenticated: async (req) => Boolean(req.cookies.get("session")),
18
+ * });
12
19
  * ```
13
20
  */
21
+ // With the .js extension: dist/proxy/index.js is the one proxy entry that
22
+ // loads in plain Node ESM (no bundler), and Node does not add extensions.
23
+ import { isHttpsOrLoopback } from "../http/loopback.js";
24
+ // The calls the proxy forwards, generated by go/api from its route table
25
+ // (`make proxy-endpoints`, copied here by `make types`). Do not edit it here.
26
+ import manifest from "./endpoints.json" with { type: "json" };
14
27
  // ============================================================================
15
28
  // Constants
16
29
  // ============================================================================
@@ -20,11 +33,330 @@ export const INF_TARGET_HEADER = "x-inf-target-url";
20
33
  export const INF_TARGET_PARAM = "__inf_target";
21
34
  /** Default proxy route path */
22
35
  export const PROXY_PATH = "/api/inference/proxy";
23
- /** Valid inference.sh domain pattern */
24
- const VALID_DOMAIN_PATTERN = /(\.|^)inference\.sh$/;
36
+ /** The API the proxy forwards to unless `apiUrl` or `apiBaseUrl` says otherwise. */
37
+ export const DEFAULT_API_URL = "https://api.inference.sh";
38
+ /**
39
+ * Default `maxUploadBytes`: 100 MiB, well under the API's own per-file limit
40
+ * (storage.max_upload_bytes, 5 GiB by default).
41
+ */
42
+ export const DEFAULT_MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
25
43
  /** Headers stripped from response (fetch auto-decompresses) */
26
44
  const STRIP_RESPONSE_HEADERS = ["content-length", "content-encoding"];
27
45
  // ============================================================================
46
+ // Endpoint policy
47
+ // ============================================================================
48
+ /**
49
+ * What the proxy forwards with the site's API key.
50
+ *
51
+ * A frontend needs the proxy to run things: run an app, start or continue an
52
+ * agent chat, and read or stream the resulting task or chat. PROXY_ENDPOINTS
53
+ * lists exactly the calls the SDK's own client makes for that (`run`,
54
+ * `agent().sendMessage`, the agent chat provider) and nothing else, so
55
+ * secrets, API keys, billing, account, file listings and app/agent management
56
+ * are never reachable through it.
57
+ *
58
+ * `allowedEndpoints` then limits which apps and agents may be run. It is
59
+ * checked where a request names one: a new run or chat, a chat handed to
60
+ * another agent, an agent's info. A request about an existing task or chat
61
+ * names it by id, and the proxy cannot tell which app or agent that id
62
+ * belongs to: the id holder may use every id call below on it.
63
+ *
64
+ * Tool approvals are not forwarded. The API takes an approval (approve,
65
+ * reject, always allow, resolve an interrupt, answer a question awaiting a
66
+ * person) only from the account holder's own sign-in, never from an API key,
67
+ * and the proxy signs with a key. Agents behind a proxy should not use
68
+ * approval-gated tools.
69
+ */
70
+ /**
71
+ * Ids of tasks, chats, messages, tool calls and the like, in the shape the
72
+ * API mints them: a lowercase 26-character ULID (Crockford base32, no i, l,
73
+ * o or u). Literal routes beside an id (`/tasks/stream`, `/chats/list`, ...)
74
+ * never match.
75
+ */
76
+ const ID = "[0-9a-hjkmnp-tv-z]{26}";
77
+ /** One segment of an agent ref in a path (namespace, name, name@version). */
78
+ const SEGMENT = "[A-Za-z0-9._@:-]+";
79
+ /** API routes under /agents/{id}/ that are not an agent's namespace/name. */
80
+ const AGENT_SUBROUTES = "(?!(?:card|versions|rules)$)";
81
+ /**
82
+ * The proxy's own checks, by manifest route. A route here that the manifest
83
+ * does not list is never forwarded (a test keeps the two in step).
84
+ */
85
+ export const PROXY_ENDPOINT_RULES = {
86
+ // Apps: only the app, its input and how to follow the run: no workers,
87
+ // infra, session, webhook, schedule, setup or metadata from a visitor.
88
+ "POST /apps/run": {
89
+ ref: { body: "app", refuse: ["app_id", "version_id"] },
90
+ onlyFields: ["app", "app_id", "version_id", "function", "input", "stream", "wait"],
91
+ },
92
+ "POST /files": { onlyFields: ["files"], validate: checkUpload },
93
+ // Agents: the agent a chat starts with or is handed to.
94
+ "POST /agents/run": { ref: { body: "agent", refuse: ["agent_config"] } },
95
+ "POST /chats": { ref: { body: "agent", refuse: [] } },
96
+ "POST /chats/{id}/agent": { ref: { body: "agent", refuse: [] } },
97
+ "GET /agents/{namespace}/{name}": { ref: { path: true } },
98
+ };
99
+ /** A route with its placeholders unnamed ("POST /chats/{}/messages"), to compare routes. */
100
+ const routeKey = (route) => route.replace(/\{\w+\}/g, "{}");
101
+ const RULES_BY_KEY = new Map(Object.entries(PROXY_ENDPOINT_RULES).map(([route, rules]) => [routeKey(route), rules]));
102
+ /** The regexp for a manifest path: an agent ref's segments, ids everywhere else. */
103
+ function pathPattern(template) {
104
+ const source = template
105
+ .split(/(\{\w+\})/)
106
+ .map((part) => {
107
+ if (part === "{namespace}")
108
+ return SEGMENT;
109
+ if (part === "{name}")
110
+ return `${AGENT_SUBROUTES}${SEGMENT}`;
111
+ if (/^\{\w+\}$/.test(part))
112
+ return ID;
113
+ return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
114
+ })
115
+ .join("");
116
+ return new RegExp(`^${source}$`);
117
+ }
118
+ function fromManifest(e) {
119
+ if (e.method !== "GET" && e.method !== "POST")
120
+ throw new Error(`proxy manifest: unsupported method ${e.method} ${e.path}`);
121
+ const route = `${e.method} ${e.path}`;
122
+ return {
123
+ method: e.method,
124
+ route,
125
+ path: pathPattern(e.path),
126
+ purpose: e.purpose,
127
+ scopes: e.scopes,
128
+ ...RULES_BY_KEY.get(routeKey(route)),
129
+ };
130
+ }
131
+ /** The API's proxy manifest as shipped in this package. */
132
+ export const PROXY_MANIFEST = manifest;
133
+ /**
134
+ * The calls the proxy forwards: exactly the manifest's, with the proxy's
135
+ * request checks. Everything else is refused with 403.
136
+ */
137
+ export const PROXY_ENDPOINTS = PROXY_MANIFEST.endpoints.map(fromManifest);
138
+ /** PROXY_ENDPOINTS and what pickProxyEndpoint derived from them: the only entries `endpoints` takes. */
139
+ const SANCTIONED = new WeakSet(PROXY_ENDPOINTS);
140
+ /** The PROXY_ENDPOINTS entry for a request, or undefined when the proxy does not forward it. */
141
+ export function matchProxyEndpoint(method, pathname, endpoints = PROXY_ENDPOINTS) {
142
+ const m = method.toUpperCase();
143
+ return endpoints.find((e) => e.method === m && e.path.test(pathname));
144
+ }
145
+ /**
146
+ * The PROXY_ENDPOINTS entry for `route` ("METHOD /path", with `{id}`,
147
+ * `{namespace}` and `{name}` placeholders, e.g. "POST /chats/{id}/messages"),
148
+ * optionally narrowed: `onlyFields` must be a subset of the entry's own,
149
+ * and `validate` runs after the entry's own check. For the `endpoints`
150
+ * option. Throws when the proxy does not forward the route.
151
+ */
152
+ export function pickProxyEndpoint(route, narrow = {}) {
153
+ const [method = "", template = ""] = route.split(" ");
154
+ const key = routeKey(`${method.toUpperCase()} ${template}`);
155
+ const base = PROXY_ENDPOINTS.find((e) => routeKey(e.route) === key);
156
+ if (!base)
157
+ throw new Error(`pickProxyEndpoint: "${route}" is not in PROXY_ENDPOINTS`);
158
+ const { onlyFields, validate } = narrow;
159
+ const wider = onlyFields?.find((field) => base.onlyFields && !base.onlyFields.includes(field));
160
+ if (wider !== undefined)
161
+ throw new Error(`pickProxyEndpoint: "${route}" does not take "${wider}"`);
162
+ const picked = {
163
+ ...base,
164
+ onlyFields: onlyFields ?? base.onlyFields,
165
+ validate: !validate ? base.validate : (body, limits) => base.validate?.(body, limits) ?? validate(body, limits),
166
+ };
167
+ SANCTIONED.add(picked);
168
+ return picked;
169
+ }
170
+ /** File fields `POST /files` takes (models.PartialFile). */
171
+ const UPLOAD_FILE_FIELDS = ["uri", "path", "content_type", "size", "filename"];
172
+ /**
173
+ * `POST /files` through the proxy: one file, its declared size within
174
+ * maxUploadBytes, no category (uploads land in the default "files" category).
175
+ */
176
+ function checkUpload(body, limits) {
177
+ for (const files of fieldValues(body, "files")) {
178
+ if (!Array.isArray(files) || files.length !== 1) {
179
+ return { status: 403, error: "the inference.sh proxy uploads one file per request" };
180
+ }
181
+ const file = files[0];
182
+ if (file === null || typeof file !== "object" || Array.isArray(file)) {
183
+ return { status: 400, error: "files[0] must be a JSON object" };
184
+ }
185
+ const fields = file;
186
+ const extra = Object.keys(fields).find((key) => !UPLOAD_FILE_FIELDS.includes(foldKey(key)));
187
+ if (extra !== undefined) {
188
+ return { status: 403, error: `"files[0].${extra}" cannot be set through the inference.sh proxy` };
189
+ }
190
+ const sizes = fieldValues(fields, "size");
191
+ if (sizes.length === 0)
192
+ return { status: 400, error: "files[0].size is required" };
193
+ for (const size of sizes) {
194
+ if (typeof size !== "number" || !Number.isInteger(size) || size <= 0) {
195
+ return { status: 400, error: "files[0].size must be a positive integer" };
196
+ }
197
+ if (size > limits.maxUploadBytes) {
198
+ return { status: 413, error: `file is larger than this proxy's upload limit (${limits.maxUploadBytes} bytes)` };
199
+ }
200
+ }
201
+ }
202
+ return undefined;
203
+ }
204
+ /**
205
+ * `namespace/name` of an app or agent ref, parsed the way the API parses it
206
+ * (`[type/]namespace/name[@version][:function]`), or undefined when the ref
207
+ * names no namespace (an id, or a bare name).
208
+ */
209
+ function refName(ref) {
210
+ let full = ref;
211
+ const colon = ref.lastIndexOf(":");
212
+ const at = ref.lastIndexOf("@");
213
+ if (colon !== -1 && (at === -1 || colon > at))
214
+ full = ref.slice(0, colon);
215
+ const versionAt = full.lastIndexOf("@");
216
+ const fullName = versionAt === -1 ? full : full.slice(0, versionAt);
217
+ const parts = fullName.split("/");
218
+ if (parts.length < 2)
219
+ return undefined;
220
+ let namespace = parts[0];
221
+ let name = parts.slice(1).join("/");
222
+ if (parts.length >= 3 && ["knowledge", "skill", "app", "agent"].includes(parts[0])) {
223
+ namespace = parts[1];
224
+ name = parts.slice(2).join("/");
225
+ }
226
+ if (!namespace || !name)
227
+ return undefined;
228
+ return `${namespace}/${name}`;
229
+ }
230
+ /**
231
+ * Compile allowedEndpoints. Globs: `*` matches within one path segment, so
232
+ * "ana/*" is every app and agent of the namespace "ana", at any version.
233
+ * With `exact`, each entry is a whole ref.
234
+ */
235
+ function compileEndpointPatterns(patterns, exact) {
236
+ return patterns.map((pattern) => {
237
+ if (exact) {
238
+ if (typeof pattern !== "string" || pattern.includes("*") || refName(pattern) === undefined) {
239
+ throw new Error(`allowedEndpoints: "${pattern}" is not an app or agent ref (e.g. "ana/helper@v2")`);
240
+ }
241
+ return (ref) => ref === pattern;
242
+ }
243
+ if (typeof pattern !== "string" || refName(pattern) !== pattern) {
244
+ throw new Error(`allowedEndpoints: "${pattern}" is not a namespace/name pattern (e.g. "ana/helper" or "ana/*")`);
245
+ }
246
+ const source = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*");
247
+ const glob = new RegExp(`^${source}$`);
248
+ return (ref) => {
249
+ const name = refName(ref);
250
+ return name !== undefined && glob.test(name);
251
+ };
252
+ });
253
+ }
254
+ /**
255
+ * allowedEndpoints compiled once per options array (adapters pass the same
256
+ * array on every request), with a bad entry's error kept so every request
257
+ * answers it.
258
+ */
259
+ const compiledPatterns = new WeakMap();
260
+ function endpointPatterns(patterns, exact) {
261
+ if (!patterns || patterns.length === 0)
262
+ return [];
263
+ let byMode = compiledPatterns.get(patterns);
264
+ if (!byMode)
265
+ compiledPatterns.set(patterns, (byMode = new Map()));
266
+ let compiled = byMode.get(exact);
267
+ if (!compiled) {
268
+ try {
269
+ compiled = compileEndpointPatterns(patterns, exact);
270
+ }
271
+ catch (e) {
272
+ compiled = e;
273
+ }
274
+ byMode.set(exact, compiled);
275
+ }
276
+ return compiled;
277
+ }
278
+ /**
279
+ * Go's encoding/json matches object keys to struct fields case-insensitively
280
+ * (with Unicode folding: "ſ" is "s", "K" is "k"), so the API reads "Agent" or
281
+ * "AGENT" as "agent". Every key that folds to a field is that field.
282
+ */
283
+ function foldKey(key) {
284
+ return key.toUpperCase().toLowerCase();
285
+ }
286
+ function fieldValues(body, field) {
287
+ return Object.keys(body).filter((key) => foldKey(key) === field).map((key) => body[key]);
288
+ }
289
+ /**
290
+ * Check a request against the endpoint list and allowedEndpoints. Returns a
291
+ * refusal, or undefined when the request may be forwarded.
292
+ *
293
+ * @param patterns compiled allowedEndpoints; empty means every app and agent.
294
+ * @param endpoints the calls forwarded (PROXY_ENDPOINTS or a subset).
295
+ */
296
+ function checkProxyRequest(method, pathname, body, patterns, limits, endpoints) {
297
+ const m = method.toUpperCase();
298
+ const match = matchProxyEndpoint(m, pathname, endpoints);
299
+ if (!match) {
300
+ return {
301
+ status: 403,
302
+ error: `${m} ${pathname} is not available through the inference.sh proxy: it forwards app runs and agent chats only`,
303
+ };
304
+ }
305
+ const checkRef = match.ref !== undefined && patterns.length > 0;
306
+ if (!checkRef && !match.onlyFields && !match.validate)
307
+ return undefined;
308
+ if (match.ref && "path" in match.ref) {
309
+ const ref = pathname.slice("/agents/".length);
310
+ return allowedRef(ref, patterns) ? undefined : notAllowed(`agent "${ref}"`);
311
+ }
312
+ let json;
313
+ try {
314
+ json = body ? JSON.parse(body) : undefined;
315
+ }
316
+ catch {
317
+ return { status: 400, error: "request body must be JSON" };
318
+ }
319
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
320
+ return { status: 400, error: "request body must be a JSON object" };
321
+ }
322
+ const fields = json;
323
+ if (match.onlyFields) {
324
+ const extra = Object.keys(fields).find((key) => !match.onlyFields.includes(foldKey(key)));
325
+ if (extra !== undefined) {
326
+ return { status: 403, error: `"${extra}" cannot be set through the inference.sh proxy` };
327
+ }
328
+ }
329
+ if (match.validate) {
330
+ const refusal = match.validate(fields, limits);
331
+ if (refusal)
332
+ return refusal;
333
+ }
334
+ if (checkRef && match.ref && "body" in match.ref) {
335
+ for (const field of match.ref.refuse) {
336
+ if (fieldValues(fields, field).some((v) => v !== null && v !== undefined && v !== "")) {
337
+ return {
338
+ status: 403,
339
+ error: `"${field}" is not accepted when allowedEndpoints is set: name the app or agent by its namespace/name ref`,
340
+ };
341
+ }
342
+ }
343
+ const refs = fieldValues(fields, match.ref.body);
344
+ if (refs.length === 0) {
345
+ return { status: 403, error: `"${match.ref.body}" is required when allowedEndpoints is set` };
346
+ }
347
+ const refused = refs.find((ref) => !allowedRef(ref, patterns));
348
+ if (refused !== undefined)
349
+ return notAllowed(`${match.ref.body} ${JSON.stringify(refused)}`);
350
+ }
351
+ return undefined;
352
+ }
353
+ function allowedRef(ref, patterns) {
354
+ return typeof ref === "string" && patterns.some((matches) => matches(ref));
355
+ }
356
+ function notAllowed(what) {
357
+ return { status: 403, error: `${what} is not in this proxy's allowedEndpoints` };
358
+ }
359
+ // ============================================================================
28
360
  // Utilities
29
361
  // ============================================================================
30
362
  /** Get first value from header (may be array) */
@@ -37,14 +369,20 @@ function firstValue(value) {
37
369
  function envApiKey() {
38
370
  return process.env.INFERENCE_API_KEY;
39
371
  }
40
- /** Check if domain is allowed */
41
- function isAllowedDomain(host, extraDomains) {
42
- if (VALID_DOMAIN_PATTERN.test(host))
372
+ /** Whether a pattern matches the whole host (no partial or suffix match). */
373
+ function matchesWholeHost(pattern, host) {
374
+ const whole = new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, ""));
375
+ const m = whole.exec(host);
376
+ return m !== null && m.index === 0 && m[0] === host;
377
+ }
378
+ /**
379
+ * The target must be the configured API's origin or a host the site listed
380
+ * in allowedDomains: the API key goes nowhere else.
381
+ */
382
+ function isAllowedTarget(target, apiOrigin, extraDomains) {
383
+ if (target.origin === apiOrigin)
43
384
  return true;
44
- if (extraDomains) {
45
- return extraDomains.some((pattern) => pattern.test(host));
46
- }
47
- return false;
385
+ return (extraDomains ?? []).some((pattern) => matchesWholeHost(pattern, target.host));
48
386
  }
49
387
  // ============================================================================
50
388
  // Core Handler
@@ -60,6 +398,26 @@ function isAllowedDomain(host, extraDomains) {
60
398
  * @returns Promise resolving to the framework's response type
61
399
  */
62
400
  export async function processProxyRequest(adapter, options) {
401
+ // 0. Who may use the key: isAuthenticated when given, else
402
+ // allowUnauthorizedRequests (default true).
403
+ if (options?.isAuthenticated) {
404
+ if (!(await options.isAuthenticated(adapter.request))) {
405
+ return adapter.error(401, { error: "Unauthorized" });
406
+ }
407
+ }
408
+ else if (options?.allowUnauthorizedRequests === false) {
409
+ return adapter.error(401, {
410
+ error: "Unauthorized: this proxy sets allowUnauthorizedRequests: false without isAuthenticated",
411
+ });
412
+ }
413
+ const patterns = endpointPatterns(options?.allowedEndpoints, options?.exactRefs === true);
414
+ if (patterns instanceof Error) {
415
+ return adapter.error(500, { error: patterns.message });
416
+ }
417
+ const endpoints = options?.endpoints ?? PROXY_ENDPOINTS;
418
+ if (!endpoints.every((e) => SANCTIONED.has(e))) {
419
+ return adapter.error(500, { error: "endpoints: entries must come from PROXY_ENDPOINTS or pickProxyEndpoint" });
420
+ }
63
421
  // 1. Extract target URL (header first, query param fallback for SSE)
64
422
  let targetUrl = firstValue(adapter.header(INF_TARGET_HEADER));
65
423
  if (!targetUrl && adapter.query) {
@@ -73,28 +431,53 @@ export async function processProxyRequest(adapter, options) {
73
431
  error: `Missing ${INF_TARGET_HEADER} header or ${INF_TARGET_PARAM} query param`,
74
432
  });
75
433
  }
76
- // 1b. Rewrite base URL if INFERENCE_API_BASE_URL is set
77
- const overrideBase = options?.apiBaseUrl || process.env.INFERENCE_API_BASE_URL;
78
- if (overrideBase) {
79
- const parsed = new URL(targetUrl);
80
- const override = new URL(overrideBase);
81
- parsed.protocol = override.protocol;
82
- parsed.host = override.host;
83
- targetUrl = parsed.toString();
84
- }
85
- // 2. Validate target domain
86
- let host;
434
+ // 1b. Parse, and rewrite the base URL if apiBaseUrl / INFERENCE_API_BASE_URL is set
435
+ let target;
436
+ let apiOrigin;
87
437
  try {
88
- host = new URL(targetUrl).host;
438
+ target = new URL(targetUrl);
439
+ const overrideBase = options?.apiBaseUrl || process.env.INFERENCE_API_BASE_URL;
440
+ if (overrideBase) {
441
+ const override = new URL(overrideBase);
442
+ target.protocol = override.protocol;
443
+ target.host = override.host;
444
+ apiOrigin = override.origin;
445
+ }
446
+ else {
447
+ apiOrigin = new URL(options?.apiUrl || DEFAULT_API_URL).origin;
448
+ }
89
449
  }
90
450
  catch {
91
451
  return adapter.error(400, { error: "Invalid target URL" });
92
452
  }
93
- if (!isAllowedDomain(host, options?.allowedDomains)) {
453
+ if (target.username || target.password) {
454
+ return adapter.error(400, { error: "Target URL must not carry credentials" });
455
+ }
456
+ targetUrl = target.toString();
457
+ // 2. Validate the target: https only (the API key rides on this request),
458
+ // and the configured API's origin or an explicitly allowed host.
459
+ if (!isHttpsOrLoopback(target)) {
94
460
  return adapter.error(412, {
95
- error: `Target must be an inference.sh domain, got: ${host}`,
461
+ error: `Target must use https, got: ${target.protocol}`,
96
462
  });
97
463
  }
464
+ if (!isAllowedTarget(target, apiOrigin, options?.allowedDomains)) {
465
+ return adapter.error(412, {
466
+ error: `Target must be the API at ${apiOrigin}, got: ${target.host}`,
467
+ });
468
+ }
469
+ const maxUploadBytes = options?.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;
470
+ if (!(Number.isInteger(maxUploadBytes) && maxUploadBytes > 0)) {
471
+ return adapter.error(500, { error: "maxUploadBytes must be a positive integer" });
472
+ }
473
+ // 2b. Only the calls a frontend needs to run apps and agents, and only
474
+ // the allowed apps and agents. The body is read once, here, and the same
475
+ // string is what goes upstream.
476
+ const body = adapter.method.toUpperCase() === "GET" ? undefined : await adapter.body();
477
+ const refusal = checkProxyRequest(adapter.method, target.pathname, body, patterns, { maxUploadBytes }, endpoints);
478
+ if (refusal) {
479
+ return adapter.error(refusal.status, { error: refusal.error });
480
+ }
98
481
  // 3. Resolve API key
99
482
  const apiKey = options?.apiKey
100
483
  ?? (adapter.apiKey ? await adapter.apiKey() : undefined)
@@ -138,7 +521,8 @@ export async function processProxyRequest(adapter, options) {
138
521
  "user-agent": userAgent || proxyId,
139
522
  "x-inf-proxy": proxyId,
140
523
  },
141
- body: adapter.method.toUpperCase() === "GET" ? undefined : await adapter.body(),
524
+ body,
525
+ signal: adapter.signal,
142
526
  });
143
527
  // 7. Forward response headers (strip compression headers since fetch decompresses)
144
528
  response.headers.forEach((value, key) => {
@@ -5,17 +5,19 @@
5
5
  *
6
6
  * @example App Router (app/api/inference/proxy/route.ts)
7
7
  * ```typescript
8
- * import { handlers } from "@inferencesh/sdk/proxy/nextjs";
9
- * export const { GET, POST, PUT } = handlers;
8
+ * import { createHandler } from "@inferencesh/sdk/proxy/nextjs";
9
+ * export const { GET, POST } = createHandler({ allowedEndpoints: ["my-team/*"] });
10
10
  * ```
11
11
  *
12
12
  * @example Page Router (pages/api/inference/proxy.ts)
13
13
  * ```typescript
14
- * export { pageHandler as default } from "@inferencesh/sdk/proxy/nextjs";
14
+ * import { createPageHandler } from "@inferencesh/sdk/proxy/nextjs";
15
+ * export default createPageHandler({ allowedEndpoints: ["my-team/*"] });
15
16
  * ```
16
17
  */
17
18
  import { type NextRequest } from "next/server";
18
- import type { NextApiHandler } from "next/types";
19
+ import type { NextApiHandler, NextApiRequest } from "next/types";
20
+ import { type ProxyOptions } from "./index";
19
21
  /** Default proxy route path */
20
22
  export declare const PROXY_ROUTE = "/api/inference/proxy";
21
23
  /**
@@ -27,37 +29,47 @@ export declare const PROXY_ROUTE = "/api/inference/proxy";
27
29
  * @example
28
30
  * ```typescript
29
31
  * // pages/api/inference/proxy.ts
30
- * export { pageHandler as default } from "@inferencesh/sdk/proxy/nextjs";
32
+ * import { createPageHandler } from "@inferencesh/sdk/proxy/nextjs";
33
+ * export default createPageHandler({ allowedEndpoints: ["my-team/*"] });
31
34
  * ```
32
35
  */
36
+ export declare function createPageHandler(options?: ProxyOptions<NextApiRequest>): NextApiHandler;
37
+ /** Page Router handler with the default options. */
33
38
  export declare const pageHandler: NextApiHandler;
34
39
  /**
35
- * Create an App Router handler for the Inference.sh proxy.
40
+ * Create App Router route handlers for the Inference.sh proxy.
36
41
  * Supports full streaming passthrough for SSE responses.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // app/api/inference/proxy/route.ts
46
+ * import { createHandler } from "@inferencesh/sdk/proxy/nextjs";
47
+ * export const { GET, POST } = createHandler({ allowedEndpoints: ["my-team/*"] });
48
+ * ```
37
49
  */
38
- declare function appHandler(request: NextRequest): Promise<Response>;
50
+ export declare function createHandler(options?: ProxyOptions<NextRequest>): {
51
+ GET: (request: NextRequest) => Promise<Response>;
52
+ POST: (request: NextRequest) => Promise<Response>;
53
+ };
39
54
  /**
40
- * App Router route handlers.
55
+ * App Router route handlers with the default options.
41
56
  *
42
57
  * @example
43
58
  * ```typescript
44
59
  * // app/api/inference/proxy/route.ts
45
60
  * import { handlers } from "@inferencesh/sdk/proxy/nextjs";
46
- * export const { GET, POST, PUT } = handlers;
61
+ * export const { GET, POST } = handlers;
47
62
  * ```
48
63
  */
49
64
  export declare const handlers: {
50
- GET: typeof appHandler;
51
- POST: typeof appHandler;
52
- PUT: typeof appHandler;
65
+ GET: (request: NextRequest) => Promise<Response>;
66
+ POST: (request: NextRequest) => Promise<Response>;
53
67
  };
54
68
  /** @deprecated Use pageHandler */
55
69
  export declare const handler: NextApiHandler;
56
70
  /** @deprecated Use handlers */
57
71
  export declare const route: {
58
- handler: typeof appHandler;
59
- GET: typeof appHandler;
60
- POST: typeof appHandler;
61
- PUT: typeof appHandler;
72
+ GET: (request: NextRequest) => Promise<Response>;
73
+ POST: (request: NextRequest) => Promise<Response>;
74
+ handler: (request: NextRequest) => Promise<Response>;
62
75
  };
63
- export {};