notte-sdk 0.0.1

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,323 @@
1
+ // src/proxy/patterns.ts
2
+ var DEFAULT_ALLOWED_PATTERNS = [
3
+ // Agents endpoints
4
+ /^agents$/,
5
+ // /agents
6
+ /^agents\/[^\/]+$/,
7
+ // /agents/{agent_id}
8
+ /^agents\/[^\/]+\/stop$/,
9
+ // /agents/{agent_id}/stop
10
+ /^agents\/[^\/]+\/workflow\/code$/,
11
+ // /agents/{agent_id}/workflow/code
12
+ /^agents\/start$/,
13
+ // /agents/start
14
+ // Anything endpoints
15
+ /^anything\/start$/,
16
+ // /anything/start
17
+ // Functions endpoints
18
+ /^functions$/,
19
+ // /functions
20
+ /^functions\/[^\/]+$/,
21
+ // /functions/{function_id}
22
+ /^functions\/[^\/]+\/fork$/,
23
+ // /functions/{function_id}/fork
24
+ /^functions\/[^\/]+\/runs$/,
25
+ // /functions/{function_id}/runs
26
+ /^functions\/[^\/]+\/runs\/[^\/]+$/,
27
+ // /functions/{function_id}/runs/{run_id}
28
+ /^functions\/[^\/]+\/runs\/start$/,
29
+ // /functions/{function_id}/runs/start
30
+ /^functions\/[^\/]+\/schedule$/,
31
+ // /functions/{function_id}/schedule
32
+ // Personas endpoints
33
+ /^personas$/,
34
+ // /personas
35
+ /^personas\/[^\/]+$/,
36
+ // /personas/{persona_id}
37
+ /^personas\/[^\/]+\/emails$/,
38
+ // /personas/{persona_id}/emails
39
+ /^personas\/[^\/]+\/sms$/,
40
+ // /personas/{persona_id}/sms
41
+ /^personas\/create$/,
42
+ // /personas/create
43
+ // Profiles endpoints
44
+ /^profiles$/,
45
+ // /profiles
46
+ /^profiles\/[^\/]+$/,
47
+ // /profiles/{profile_id}
48
+ /^profiles\/create$/,
49
+ // /profiles/create
50
+ // Prompts endpoints
51
+ /^prompts\/improve$/,
52
+ // /prompts/improve
53
+ /^prompts\/nudge$/,
54
+ // /prompts/nudge
55
+ // Root endpoints
56
+ /^health$/,
57
+ // /health
58
+ /^scrape$/,
59
+ // /scrape
60
+ /^scrape_from_html$/,
61
+ // /scrape_from_html
62
+ // Sessions endpoints
63
+ /^sessions$/,
64
+ // /sessions
65
+ /^sessions\/[^\/]+$/,
66
+ // /sessions/{session_id}
67
+ /^sessions\/[^\/]+\/cookies$/,
68
+ // /sessions/{session_id}/cookies
69
+ /^sessions\/[^\/]+\/debug$/,
70
+ // /sessions/{session_id}/debug
71
+ /^sessions\/[^\/]+\/network\/logs$/,
72
+ // /sessions/{session_id}/network/logs
73
+ /^sessions\/[^\/]+\/offset$/,
74
+ // /sessions/{session_id}/offset
75
+ /^sessions\/[^\/]+\/page\/execute$/,
76
+ // /sessions/{session_id}/page/execute
77
+ /^sessions\/[^\/]+\/page\/observe$/,
78
+ // /sessions/{session_id}/page/observe
79
+ /^sessions\/[^\/]+\/page\/scrape$/,
80
+ // /sessions/{session_id}/page/scrape
81
+ /^sessions\/[^\/]+\/page\/screenshot$/,
82
+ // /sessions/{session_id}/page/screenshot
83
+ /^sessions\/[^\/]+\/replay$/,
84
+ // /sessions/{session_id}/replay
85
+ /^sessions\/[^\/]+\/stop$/,
86
+ // /sessions/{session_id}/stop
87
+ /^sessions\/[^\/]+\/workflow\/code$/,
88
+ // /sessions/{session_id}/workflow/code
89
+ /^sessions\/start$/,
90
+ // /sessions/start
91
+ // Storage endpoints
92
+ /^storage\/[^\/]+\/downloads$/,
93
+ // /storage/{session_id}/downloads
94
+ /^storage\/[^\/]+\/downloads\/[^\/]+$/,
95
+ // /storage/{session_id}/downloads/{filename}
96
+ /^storage\/uploads$/,
97
+ // /storage/uploads
98
+ /^storage\/uploads\/[^\/]+$/,
99
+ // /storage/uploads/{filename}
100
+ // Usage endpoints
101
+ /^usage$/,
102
+ // /usage
103
+ /^usage\/logs$/,
104
+ // /usage/logs
105
+ // Vaults endpoints
106
+ /^vaults$/,
107
+ // /vaults
108
+ /^vaults\/[^\/]+$/,
109
+ // /vaults/{vault_id}
110
+ /^vaults\/[^\/]+\/card$/,
111
+ // /vaults/{vault_id}/card
112
+ /^vaults\/[^\/]+\/credentials$/,
113
+ // /vaults/{vault_id}/credentials
114
+ /^vaults\/create$/
115
+ // /vaults/create
116
+ ];
117
+ function validatePath(pathSegments, allowedPatterns) {
118
+ const path = pathSegments.join("/");
119
+ if (path.includes("..") || path.includes("//") || path.startsWith("/")) {
120
+ return {
121
+ isValid: false,
122
+ error: "Invalid path: directory traversal detected"
123
+ };
124
+ }
125
+ const isValidPattern = allowedPatterns.some((pattern) => pattern.test(path));
126
+ if (!isValidPattern) {
127
+ return {
128
+ isValid: false,
129
+ error: `Invalid path: ${path} does not match any allowed endpoint pattern`
130
+ };
131
+ }
132
+ return { isValid: true };
133
+ }
134
+
135
+ // src/proxy/types.ts
136
+ var NotteProxyAuthError = class extends Error {
137
+ constructor(message = "Unauthorized") {
138
+ super(message);
139
+ this.name = "NotteProxyAuthError";
140
+ }
141
+ };
142
+
143
+ // src/proxy/core.ts
144
+ var DEFAULT_API_URL = "https://api.notte.cc";
145
+ var DEFAULT_FORWARD_HEADERS = ["content-type", "accept"];
146
+ var METHODS_WITH_BODY = ["POST", "PUT", "PATCH"];
147
+ async function handleProxyRequest(request, pathSegments, config) {
148
+ try {
149
+ if (config.allowedPatterns !== false) {
150
+ const patterns = config.allowedPatterns || DEFAULT_ALLOWED_PATTERNS;
151
+ const pathValidation = validatePath(pathSegments, patterns);
152
+ if (!pathValidation.isValid) {
153
+ return new Response(
154
+ JSON.stringify({
155
+ error: "Invalid API endpoint",
156
+ details: pathValidation.error
157
+ }),
158
+ {
159
+ status: 400,
160
+ headers: { "Content-Type": "application/json" }
161
+ }
162
+ );
163
+ }
164
+ }
165
+ let apiKey = config.apiKey || process.env.NOTTE_API_KEY;
166
+ if (config.authenticate) {
167
+ const result = await config.authenticate(request);
168
+ if (typeof result === "string") {
169
+ apiKey = result;
170
+ }
171
+ }
172
+ if (!apiKey) {
173
+ return new Response(
174
+ JSON.stringify({ error: "No API key configured" }),
175
+ {
176
+ status: 401,
177
+ headers: { "Content-Type": "application/json" }
178
+ }
179
+ );
180
+ }
181
+ const apiUrl = (config.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
182
+ const path = pathSegments.join("/");
183
+ const incomingUrl = new URL(request.url);
184
+ const searchParams = incomingUrl.searchParams.toString();
185
+ const targetUrl = `${apiUrl}/${path}${searchParams ? `?${searchParams}` : ""}`;
186
+ const forwardHeaders = new Headers();
187
+ forwardHeaders.set("Authorization", `Bearer ${apiKey}`);
188
+ forwardHeaders.set(
189
+ "x-notte-request-origin",
190
+ config.requestOrigin || "sdk-proxy"
191
+ );
192
+ const headersToForward = config.forwardHeaders || DEFAULT_FORWARD_HEADERS;
193
+ for (const header of headersToForward) {
194
+ const value = request.headers.get(header);
195
+ if (value) {
196
+ forwardHeaders.set(header, value);
197
+ }
198
+ }
199
+ if (config.onBeforeRequest) {
200
+ await config.onBeforeRequest({
201
+ path,
202
+ method: request.method,
203
+ headers: forwardHeaders,
204
+ url: targetUrl,
205
+ incomingRequest: request
206
+ });
207
+ }
208
+ const requestOptions = {
209
+ method: request.method,
210
+ headers: forwardHeaders
211
+ };
212
+ if (METHODS_WITH_BODY.includes(request.method) && request.body) {
213
+ requestOptions.body = request.body;
214
+ requestOptions.duplex = "half";
215
+ }
216
+ let response;
217
+ try {
218
+ response = await fetch(targetUrl, requestOptions);
219
+ } catch (fetchError) {
220
+ const errorMessage = fetchError instanceof Error ? fetchError.message : "Unknown fetch error";
221
+ return new Response(
222
+ JSON.stringify({
223
+ error: "Failed to forward request to Notte API",
224
+ details: errorMessage
225
+ }),
226
+ {
227
+ status: 502,
228
+ headers: { "Content-Type": "application/json" }
229
+ }
230
+ );
231
+ }
232
+ const contentType = response.headers.get("content-type") || "";
233
+ const isBinaryResponse = contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/") || contentType.includes("octet-stream");
234
+ const isStreamingResponse = contentType.includes("text/event-stream");
235
+ if (isStreamingResponse && response.body) {
236
+ return new Response(response.body, {
237
+ status: response.status,
238
+ statusText: response.statusText,
239
+ headers: {
240
+ "Content-Type": contentType,
241
+ "Cache-Control": "no-cache",
242
+ Connection: "keep-alive"
243
+ }
244
+ });
245
+ }
246
+ if (response.body) {
247
+ return new Response(response.body, {
248
+ status: response.status,
249
+ statusText: response.statusText,
250
+ headers: {
251
+ "Content-Type": contentType
252
+ }
253
+ });
254
+ }
255
+ if (isBinaryResponse) {
256
+ const data2 = await response.arrayBuffer();
257
+ return new Response(data2, {
258
+ status: response.status,
259
+ statusText: response.statusText,
260
+ headers: { "Content-Type": contentType }
261
+ });
262
+ }
263
+ const data = await response.text();
264
+ return new Response(data, {
265
+ status: response.status,
266
+ statusText: response.statusText,
267
+ headers: { "Content-Type": contentType }
268
+ });
269
+ } catch (error) {
270
+ if (error instanceof NotteProxyAuthError) {
271
+ return new Response(
272
+ JSON.stringify({ error: error.message }),
273
+ {
274
+ status: 401,
275
+ headers: { "Content-Type": "application/json" }
276
+ }
277
+ );
278
+ }
279
+ throw error;
280
+ }
281
+ }
282
+
283
+ // src/proxy/next.ts
284
+ function createNotteProxy(config) {
285
+ const resolvedConfig = { ...config };
286
+ const makeHandler = () => {
287
+ return async (request, context) => {
288
+ try {
289
+ const { path } = await context.params;
290
+ return await handleProxyRequest(request, path, resolvedConfig);
291
+ } catch (error) {
292
+ if (error instanceof Error) {
293
+ return new Response(JSON.stringify({ error: error.message }), {
294
+ status: 500,
295
+ headers: { "Content-Type": "application/json" }
296
+ });
297
+ }
298
+ return new Response(
299
+ JSON.stringify({ error: "Internal server error" }),
300
+ {
301
+ status: 500,
302
+ headers: { "Content-Type": "application/json" }
303
+ }
304
+ );
305
+ }
306
+ };
307
+ };
308
+ return {
309
+ GET: makeHandler(),
310
+ POST: makeHandler(),
311
+ PUT: makeHandler(),
312
+ DELETE: makeHandler(),
313
+ PATCH: makeHandler()
314
+ };
315
+ }
316
+ export {
317
+ DEFAULT_ALLOWED_PATTERNS,
318
+ NotteProxyAuthError,
319
+ createNotteProxy,
320
+ handleProxyRequest,
321
+ validatePath
322
+ };
323
+ //# sourceMappingURL=next.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/proxy/patterns.ts","../../src/proxy/types.ts","../../src/proxy/core.ts","../../src/proxy/next.ts"],"sourcesContent":["/**\n * Auto-generated from sdk.gen.ts by scripts/generate-proxy-patterns.js\n * DO NOT EDIT MANUALLY — re-run `npm run generate` to update.\n *\n * Allowed API endpoint patterns to prevent SSRF attacks.\n * Every pattern corresponds to an endpoint in the Notte API OpenAPI spec.\n */\nexport const DEFAULT_ALLOWED_PATTERNS: RegExp[] = [\n // Agents endpoints\n /^agents$/, // /agents\n /^agents\\/[^\\/]+$/, // /agents/{agent_id}\n /^agents\\/[^\\/]+\\/stop$/, // /agents/{agent_id}/stop\n /^agents\\/[^\\/]+\\/workflow\\/code$/, // /agents/{agent_id}/workflow/code\n /^agents\\/start$/, // /agents/start\n\n // Anything endpoints\n /^anything\\/start$/, // /anything/start\n\n // Functions endpoints\n /^functions$/, // /functions\n /^functions\\/[^\\/]+$/, // /functions/{function_id}\n /^functions\\/[^\\/]+\\/fork$/, // /functions/{function_id}/fork\n /^functions\\/[^\\/]+\\/runs$/, // /functions/{function_id}/runs\n /^functions\\/[^\\/]+\\/runs\\/[^\\/]+$/, // /functions/{function_id}/runs/{run_id}\n /^functions\\/[^\\/]+\\/runs\\/start$/, // /functions/{function_id}/runs/start\n /^functions\\/[^\\/]+\\/schedule$/, // /functions/{function_id}/schedule\n\n // Personas endpoints\n /^personas$/, // /personas\n /^personas\\/[^\\/]+$/, // /personas/{persona_id}\n /^personas\\/[^\\/]+\\/emails$/, // /personas/{persona_id}/emails\n /^personas\\/[^\\/]+\\/sms$/, // /personas/{persona_id}/sms\n /^personas\\/create$/, // /personas/create\n\n // Profiles endpoints\n /^profiles$/, // /profiles\n /^profiles\\/[^\\/]+$/, // /profiles/{profile_id}\n /^profiles\\/create$/, // /profiles/create\n\n // Prompts endpoints\n /^prompts\\/improve$/, // /prompts/improve\n /^prompts\\/nudge$/, // /prompts/nudge\n\n // Root endpoints\n /^health$/, // /health\n /^scrape$/, // /scrape\n /^scrape_from_html$/, // /scrape_from_html\n\n // Sessions endpoints\n /^sessions$/, // /sessions\n /^sessions\\/[^\\/]+$/, // /sessions/{session_id}\n /^sessions\\/[^\\/]+\\/cookies$/, // /sessions/{session_id}/cookies\n /^sessions\\/[^\\/]+\\/debug$/, // /sessions/{session_id}/debug\n /^sessions\\/[^\\/]+\\/network\\/logs$/, // /sessions/{session_id}/network/logs\n /^sessions\\/[^\\/]+\\/offset$/, // /sessions/{session_id}/offset\n /^sessions\\/[^\\/]+\\/page\\/execute$/, // /sessions/{session_id}/page/execute\n /^sessions\\/[^\\/]+\\/page\\/observe$/, // /sessions/{session_id}/page/observe\n /^sessions\\/[^\\/]+\\/page\\/scrape$/, // /sessions/{session_id}/page/scrape\n /^sessions\\/[^\\/]+\\/page\\/screenshot$/, // /sessions/{session_id}/page/screenshot\n /^sessions\\/[^\\/]+\\/replay$/, // /sessions/{session_id}/replay\n /^sessions\\/[^\\/]+\\/stop$/, // /sessions/{session_id}/stop\n /^sessions\\/[^\\/]+\\/workflow\\/code$/, // /sessions/{session_id}/workflow/code\n /^sessions\\/start$/, // /sessions/start\n\n // Storage endpoints\n /^storage\\/[^\\/]+\\/downloads$/, // /storage/{session_id}/downloads\n /^storage\\/[^\\/]+\\/downloads\\/[^\\/]+$/, // /storage/{session_id}/downloads/{filename}\n /^storage\\/uploads$/, // /storage/uploads\n /^storage\\/uploads\\/[^\\/]+$/, // /storage/uploads/{filename}\n\n // Usage endpoints\n /^usage$/, // /usage\n /^usage\\/logs$/, // /usage/logs\n\n // Vaults endpoints\n /^vaults$/, // /vaults\n /^vaults\\/[^\\/]+$/, // /vaults/{vault_id}\n /^vaults\\/[^\\/]+\\/card$/, // /vaults/{vault_id}/card\n /^vaults\\/[^\\/]+\\/credentials$/, // /vaults/{vault_id}/credentials\n /^vaults\\/create$/, // /vaults/create\n];\n\nexport interface PathValidationResult {\n isValid: boolean;\n error?: string;\n}\n\n/**\n * Validates a request path against a list of allowed patterns.\n * Prevents directory traversal and SSRF attacks.\n */\nexport function validatePath(\n pathSegments: string[],\n allowedPatterns: RegExp[],\n): PathValidationResult {\n const path = pathSegments.join('/');\n\n // Check for directory traversal attempts\n if (path.includes('..') || path.includes('//') || path.startsWith('/')) {\n return {\n isValid: false,\n error: 'Invalid path: directory traversal detected',\n };\n }\n\n // Check against allowed patterns\n const isValidPattern = allowedPatterns.some((pattern) => pattern.test(path));\n if (!isValidPattern) {\n return {\n isValid: false,\n error: `Invalid path: ${path} does not match any allowed endpoint pattern`,\n };\n }\n\n return { isValid: true };\n}\n","/**\n * Error class for authentication/authorization failures in the proxy.\n * Throw this from `authenticate()` to return a 401 response.\n *\n * @example\n * ```typescript\n * authenticate: async (request) => {\n * const session = await getSession(request);\n * if (!session) throw new NotteProxyAuthError('Not logged in');\n * }\n * ```\n */\nexport class NotteProxyAuthError extends Error {\n constructor(message = 'Unauthorized') {\n super(message);\n this.name = 'NotteProxyAuthError';\n }\n}\n\n/**\n * Configuration for the Notte API proxy.\n *\n * The proxy forwards incoming HTTP requests to the Notte API,\n * injecting authentication headers server-side so the API key\n * is never exposed to the client.\n */\nexport interface NotteProxyConfig {\n /**\n * The Notte API key to use for all forwarded requests.\n * Falls back to the NOTTE_API_KEY environment variable when omitted.\n * Can be overridden per-request by returning a string from `authenticate`.\n */\n apiKey?: string;\n\n /**\n * Base URL of the Notte API.\n * @default 'https://api.notte.cc'\n */\n apiUrl?: string;\n\n /**\n * Optional async function to authenticate/authorize incoming requests.\n *\n * - Throw a `NotteProxyAuthError` to reject the request (returns 401).\n * - Return `void` to allow the request through using the default `apiKey`.\n * - Return a `string` to override the API key for this specific request\n * (useful for per-user API keys).\n */\n authenticate?: (request: Request) => Promise<void | string>;\n\n /**\n * Allowed API endpoint patterns for path validation (SSRF prevention).\n *\n * - `undefined` (default): uses the built-in allowlist derived from the OpenAPI spec.\n * - `RegExp[]`: custom list of allowed patterns.\n * - `false`: disables path validation entirely (not recommended for public-facing proxies).\n */\n allowedPatterns?: RegExp[] | false;\n\n /**\n * Value for the `x-notte-request-origin` header sent to the Notte API.\n * @default 'sdk-proxy'\n */\n requestOrigin?: string;\n\n /**\n * List of header names to forward from the incoming request to the Notte API.\n * @default ['content-type', 'accept']\n */\n forwardHeaders?: string[];\n\n /**\n * Optional hook called just before the request is forwarded to the Notte API.\n * Use this to modify outgoing headers, log requests, etc.\n */\n onBeforeRequest?: (ctx: {\n path: string;\n method: string;\n headers: Headers;\n url: string;\n incomingRequest: Request;\n }) => Promise<void> | void;\n}\n","import { validatePath, DEFAULT_ALLOWED_PATTERNS } from './patterns';\nimport type { NotteProxyConfig } from './types';\nimport { NotteProxyAuthError } from './types';\n\nconst DEFAULT_API_URL = 'https://api.notte.cc';\nconst DEFAULT_FORWARD_HEADERS = ['content-type', 'accept'];\nconst METHODS_WITH_BODY = ['POST', 'PUT', 'PATCH'];\n\n/**\n * Framework-agnostic proxy handler that forwards requests to the Notte API.\n *\n * Takes a standard Web API `Request`, injects authentication, and returns\n * a standard Web API `Response`. Works with any runtime that supports\n * the Web API standards (Node 18+, Deno, Bun, Cloudflare Workers).\n *\n * @param request - The incoming HTTP request (standard Web API Request)\n * @param pathSegments - The path segments after the proxy base path (e.g., ['sessions', 'start'])\n * @param config - Proxy configuration including API key, auth hooks, etc.\n * @returns A standard Web API Response\n */\nexport async function handleProxyRequest(\n request: Request,\n pathSegments: string[],\n config: NotteProxyConfig,\n): Promise<Response> {\n try {\n // Step 1: Path validation\n if (config.allowedPatterns !== false) {\n const patterns = config.allowedPatterns || DEFAULT_ALLOWED_PATTERNS;\n const pathValidation = validatePath(pathSegments, patterns);\n\n if (!pathValidation.isValid) {\n return new Response(\n JSON.stringify({\n error: 'Invalid API endpoint',\n details: pathValidation.error,\n }),\n {\n status: 400,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n }\n\n // Step 2: Authentication\n let apiKey = config.apiKey || process.env.NOTTE_API_KEY;\n\n if (config.authenticate) {\n const result = await config.authenticate(request);\n if (typeof result === 'string') {\n apiKey = result;\n }\n }\n\n if (!apiKey) {\n return new Response(\n JSON.stringify({ error: 'No API key configured' }),\n {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n\n // Step 3: Build target URL\n const apiUrl = (config.apiUrl || DEFAULT_API_URL).replace(/\\/$/, '');\n const path = pathSegments.join('/');\n const incomingUrl = new URL(request.url);\n const searchParams = incomingUrl.searchParams.toString();\n const targetUrl = `${apiUrl}/${path}${searchParams ? `?${searchParams}` : ''}`;\n\n // Step 4: Build headers\n const forwardHeaders = new Headers();\n forwardHeaders.set('Authorization', `Bearer ${apiKey}`);\n forwardHeaders.set(\n 'x-notte-request-origin',\n config.requestOrigin || 'sdk-proxy',\n );\n\n const headersToForward =\n config.forwardHeaders || DEFAULT_FORWARD_HEADERS;\n for (const header of headersToForward) {\n const value = request.headers.get(header);\n if (value) {\n forwardHeaders.set(header, value);\n }\n }\n\n // Step 5: onBeforeRequest hook\n if (config.onBeforeRequest) {\n await config.onBeforeRequest({\n path,\n method: request.method,\n headers: forwardHeaders,\n url: targetUrl,\n incomingRequest: request,\n });\n }\n\n // Step 6: Forward the request\n const requestOptions: RequestInit = {\n method: request.method,\n headers: forwardHeaders,\n };\n\n if (METHODS_WITH_BODY.includes(request.method) && request.body) {\n requestOptions.body = request.body;\n (requestOptions as any).duplex = 'half';\n }\n\n let response: globalThis.Response;\n try {\n response = await fetch(targetUrl, requestOptions);\n } catch (fetchError) {\n const errorMessage =\n fetchError instanceof Error\n ? fetchError.message\n : 'Unknown fetch error';\n return new Response(\n JSON.stringify({\n error: 'Failed to forward request to Notte API',\n details: errorMessage,\n }),\n {\n status: 502,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n\n // Step 7: Handle response\n const contentType = response.headers.get('content-type') || '';\n\n const isBinaryResponse =\n contentType.startsWith('image/') ||\n contentType.startsWith('video/') ||\n contentType.startsWith('audio/') ||\n contentType.includes('octet-stream');\n\n const isStreamingResponse = contentType.includes('text/event-stream');\n\n // For streaming responses, pass through the body with appropriate headers\n if (isStreamingResponse && response.body) {\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: {\n 'Content-Type': contentType,\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n },\n });\n }\n\n // For all other responses with a body stream, pass it through directly\n if (response.body) {\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: {\n 'Content-Type': contentType,\n },\n });\n }\n\n // Fallback for responses without a body stream: buffer appropriately\n if (isBinaryResponse) {\n const data = await response.arrayBuffer();\n return new Response(data, {\n status: response.status,\n statusText: response.statusText,\n headers: { 'Content-Type': contentType },\n });\n }\n\n // Fallback: buffer as text\n const data = await response.text();\n return new Response(data, {\n status: response.status,\n statusText: response.statusText,\n headers: { 'Content-Type': contentType },\n });\n } catch (error) {\n // NotteProxyAuthError from authenticate() → 401\n if (error instanceof NotteProxyAuthError) {\n return new Response(\n JSON.stringify({ error: error.message }),\n {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n\n // Re-throw unexpected errors so the adapter layer can handle them\n throw error;\n }\n}\n","import type { NotteProxyConfig } from './types';\nimport { handleProxyRequest } from './core';\n\n/**\n * Type for a Next.js App Router route handler.\n * Uses standard Web API Request/Response (Next.js accepts these natively),\n * so `next` is NOT a dependency.\n */\ntype NextRouteHandler = (\n request: Request,\n context: { params: Promise<{ path: string[] }> },\n) => Promise<Response>;\n\n/**\n * Creates Next.js App Router route handlers that proxy requests to the Notte API.\n *\n * This allows you to hide your Notte API key from client-side code by routing\n * requests through your own server.\n *\n * @example\n * ```typescript\n * // app/api/notte/[...path]/route.ts\n * import { createNotteProxy } from '@notte/sdk/next';\n *\n * export const { GET, POST, PUT, DELETE, PATCH } = createNotteProxy({\n * apiKey: process.env.NOTTE_API_KEY!,\n * });\n * ```\n *\n * @example With authentication\n * ```typescript\n * import { createNotteProxy, NotteProxyAuthError } from '@notte/sdk/next';\n *\n * export const { GET, POST, PUT, DELETE, PATCH } = createNotteProxy({\n * apiKey: process.env.NOTTE_API_KEY!,\n * authenticate: async (request) => {\n * const session = await getSession(request);\n * if (!session) throw new NotteProxyAuthError('Not logged in');\n * },\n * });\n * ```\n *\n * @example With per-user API keys\n * ```typescript\n * export const { GET, POST, PUT, DELETE, PATCH } = createNotteProxy({\n * apiKey: 'fallback-key', // pragma: allowlist secret\n * authenticate: async (request) => {\n * const user = await getUser(request);\n * if (!user) throw new NotteProxyAuthError();\n * return user.notteApiKey; // returned string overrides apiKey\n * },\n * });\n * ```\n */\nexport function createNotteProxy(config: NotteProxyConfig): {\n GET: NextRouteHandler;\n POST: NextRouteHandler;\n PUT: NextRouteHandler;\n DELETE: NextRouteHandler;\n PATCH: NextRouteHandler;\n} {\n const resolvedConfig = { ...config };\n\n const makeHandler = (): NextRouteHandler => {\n return async (request, context) => {\n try {\n const { path } = await context.params;\n return await handleProxyRequest(request, path, resolvedConfig);\n } catch (error) {\n if (error instanceof Error) {\n return new Response(JSON.stringify({ error: error.message }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n };\n };\n\n return {\n GET: makeHandler(),\n POST: makeHandler(),\n PUT: makeHandler(),\n DELETE: makeHandler(),\n PATCH: makeHandler(),\n };\n}\n\n// Re-export types and utilities for convenience\nexport type { NotteProxyConfig } from './types';\nexport { NotteProxyAuthError } from './types';\nexport { DEFAULT_ALLOWED_PATTERNS, validatePath } from './patterns';\nexport { handleProxyRequest } from './core';\n"],"mappings":"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","names":["data"]}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Error class for authentication/authorization failures in the proxy.
3
+ * Throw this from `authenticate()` to return a 401 response.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * authenticate: async (request) => {
8
+ * const session = await getSession(request);
9
+ * if (!session) throw new NotteProxyAuthError('Not logged in');
10
+ * }
11
+ * ```
12
+ */
13
+ declare class NotteProxyAuthError extends Error {
14
+ constructor(message?: string);
15
+ }
16
+ /**
17
+ * Configuration for the Notte API proxy.
18
+ *
19
+ * The proxy forwards incoming HTTP requests to the Notte API,
20
+ * injecting authentication headers server-side so the API key
21
+ * is never exposed to the client.
22
+ */
23
+ interface NotteProxyConfig {
24
+ /**
25
+ * The Notte API key to use for all forwarded requests.
26
+ * Falls back to the NOTTE_API_KEY environment variable when omitted.
27
+ * Can be overridden per-request by returning a string from `authenticate`.
28
+ */
29
+ apiKey?: string;
30
+ /**
31
+ * Base URL of the Notte API.
32
+ * @default 'https://api.notte.cc'
33
+ */
34
+ apiUrl?: string;
35
+ /**
36
+ * Optional async function to authenticate/authorize incoming requests.
37
+ *
38
+ * - Throw a `NotteProxyAuthError` to reject the request (returns 401).
39
+ * - Return `void` to allow the request through using the default `apiKey`.
40
+ * - Return a `string` to override the API key for this specific request
41
+ * (useful for per-user API keys).
42
+ */
43
+ authenticate?: (request: Request) => Promise<void | string>;
44
+ /**
45
+ * Allowed API endpoint patterns for path validation (SSRF prevention).
46
+ *
47
+ * - `undefined` (default): uses the built-in allowlist derived from the OpenAPI spec.
48
+ * - `RegExp[]`: custom list of allowed patterns.
49
+ * - `false`: disables path validation entirely (not recommended for public-facing proxies).
50
+ */
51
+ allowedPatterns?: RegExp[] | false;
52
+ /**
53
+ * Value for the `x-notte-request-origin` header sent to the Notte API.
54
+ * @default 'sdk-proxy'
55
+ */
56
+ requestOrigin?: string;
57
+ /**
58
+ * List of header names to forward from the incoming request to the Notte API.
59
+ * @default ['content-type', 'accept']
60
+ */
61
+ forwardHeaders?: string[];
62
+ /**
63
+ * Optional hook called just before the request is forwarded to the Notte API.
64
+ * Use this to modify outgoing headers, log requests, etc.
65
+ */
66
+ onBeforeRequest?: (ctx: {
67
+ path: string;
68
+ method: string;
69
+ headers: Headers;
70
+ url: string;
71
+ incomingRequest: Request;
72
+ }) => Promise<void> | void;
73
+ }
74
+
75
+ export { NotteProxyAuthError as N, type NotteProxyConfig as a };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Error class for authentication/authorization failures in the proxy.
3
+ * Throw this from `authenticate()` to return a 401 response.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * authenticate: async (request) => {
8
+ * const session = await getSession(request);
9
+ * if (!session) throw new NotteProxyAuthError('Not logged in');
10
+ * }
11
+ * ```
12
+ */
13
+ declare class NotteProxyAuthError extends Error {
14
+ constructor(message?: string);
15
+ }
16
+ /**
17
+ * Configuration for the Notte API proxy.
18
+ *
19
+ * The proxy forwards incoming HTTP requests to the Notte API,
20
+ * injecting authentication headers server-side so the API key
21
+ * is never exposed to the client.
22
+ */
23
+ interface NotteProxyConfig {
24
+ /**
25
+ * The Notte API key to use for all forwarded requests.
26
+ * Falls back to the NOTTE_API_KEY environment variable when omitted.
27
+ * Can be overridden per-request by returning a string from `authenticate`.
28
+ */
29
+ apiKey?: string;
30
+ /**
31
+ * Base URL of the Notte API.
32
+ * @default 'https://api.notte.cc'
33
+ */
34
+ apiUrl?: string;
35
+ /**
36
+ * Optional async function to authenticate/authorize incoming requests.
37
+ *
38
+ * - Throw a `NotteProxyAuthError` to reject the request (returns 401).
39
+ * - Return `void` to allow the request through using the default `apiKey`.
40
+ * - Return a `string` to override the API key for this specific request
41
+ * (useful for per-user API keys).
42
+ */
43
+ authenticate?: (request: Request) => Promise<void | string>;
44
+ /**
45
+ * Allowed API endpoint patterns for path validation (SSRF prevention).
46
+ *
47
+ * - `undefined` (default): uses the built-in allowlist derived from the OpenAPI spec.
48
+ * - `RegExp[]`: custom list of allowed patterns.
49
+ * - `false`: disables path validation entirely (not recommended for public-facing proxies).
50
+ */
51
+ allowedPatterns?: RegExp[] | false;
52
+ /**
53
+ * Value for the `x-notte-request-origin` header sent to the Notte API.
54
+ * @default 'sdk-proxy'
55
+ */
56
+ requestOrigin?: string;
57
+ /**
58
+ * List of header names to forward from the incoming request to the Notte API.
59
+ * @default ['content-type', 'accept']
60
+ */
61
+ forwardHeaders?: string[];
62
+ /**
63
+ * Optional hook called just before the request is forwarded to the Notte API.
64
+ * Use this to modify outgoing headers, log requests, etc.
65
+ */
66
+ onBeforeRequest?: (ctx: {
67
+ path: string;
68
+ method: string;
69
+ headers: Headers;
70
+ url: string;
71
+ incomingRequest: Request;
72
+ }) => Promise<void> | void;
73
+ }
74
+
75
+ export { NotteProxyAuthError as N, type NotteProxyConfig as a };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "notte-sdk",
3
+ "version": "0.0.1",
4
+ "description": "TypeScript SDK for Notte - Cloud-hosted browser sessions with LLM-powered web agents",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./next": {
15
+ "types": "./dist/proxy/next.d.ts",
16
+ "import": "./dist/proxy/next.mjs",
17
+ "require": "./dist/proxy/next.js"
18
+ },
19
+ "./proxy": {
20
+ "types": "./dist/proxy/core.d.ts",
21
+ "import": "./dist/proxy/core.mjs",
22
+ "require": "./dist/proxy/core.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "generate": "openapi-ts && node scripts/normalize-generated.js && node scripts/generate-proxy-patterns.js",
30
+ "build": "npm run generate && tsup",
31
+ "dev": "tsup --watch",
32
+ "prepublishOnly": "npm run build",
33
+ "test": "vitest",
34
+ "test:unit": "vitest --exclude='**/*.integration.test.ts'",
35
+ "test:integration": "VITEST_INTEGRATION=1 vitest --run",
36
+ "typecheck": "tsc --noEmit"
37
+ },
38
+ "keywords": [
39
+ "notte",
40
+ "api",
41
+ "sdk",
42
+ "typescript"
43
+ ],
44
+ "author": "Notte",
45
+ "license": "MIT",
46
+ "dependencies": {
47
+ "ws": "^8.18.0",
48
+ "zod": "^4.1.9"
49
+ },
50
+ "devDependencies": {
51
+ "@hey-api/openapi-ts": "^0.95.0",
52
+ "@types/node": "^20.14.0",
53
+ "@types/ws": "^8.5.10",
54
+ "dotenv": "^16.4.5",
55
+ "tsup": "^8.5.1",
56
+ "typescript": "^5.5.4",
57
+ "vitest": "^4.0.10"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "overrides": {
63
+ "glob": "^11.1.0",
64
+ "tar": "^7.5.8"
65
+ }
66
+ }