notte-sdk 0.0.22 → 1.9.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.
package/dist/index.js CHANGED
@@ -30,19 +30,51 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ActionExecutionError: () => ActionExecutionError,
33
34
  Agent: () => Agent,
35
+ AuthenticationError: () => AuthenticationError,
36
+ CAPTCHA_SOLVE_TIMEOUT_MS: () => CAPTCHA_SOLVE_TIMEOUT_MS,
37
+ CLUSTER_OVERLOAD_RETRY_DELAY_MS: () => CLUSTER_OVERLOAD_RETRY_DELAY_MS,
38
+ CREDENTIAL_FIELDS: () => CREDENTIAL_FIELDS,
39
+ DEFAULT_NOTTE_API_URL: () => DEFAULT_NOTTE_API_URL,
40
+ DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
34
41
  Encryption: () => Encryption,
35
- NotteClient: () => NotteClient,
42
+ EvaluateJsNoDataError: () => EvaluateJsNoDataError,
43
+ FUNCTION_RUN_ENDPOINTS: () => FUNCTION_RUN_ENDPOINTS,
44
+ FUNCTION_RUN_TIMEOUT_MS: () => FUNCTION_RUN_TIMEOUT_MS,
45
+ FailedToRunCloudFunctionError: () => FailedToRunCloudFunctionError,
46
+ FetchResponseDecodeError: () => FetchResponseDecodeError,
47
+ FileExistsError: () => FileExistsError,
48
+ FileNotFoundError: () => FileNotFoundError,
49
+ InvalidRequestError: () => InvalidRequestError,
50
+ NotteAPIError: () => NotteAPIError,
51
+ NotteAPIExecutionError: () => NotteAPIExecutionError,
52
+ NotteAnything: () => NotteAnything,
53
+ NotteClient: () => NotteClient3,
54
+ NotteError: () => NotteError,
36
55
  NotteFunction: () => NotteFunction,
37
56
  NottePersona: () => NottePersona,
38
57
  NotteProxyAuthError: () => NotteProxyAuthError,
58
+ NotteSecrets: () => NotteSecrets,
59
+ NotteTimeoutError: () => NotteTimeoutError,
60
+ NotteUsage: () => NotteUsage,
39
61
  NotteVault: () => NotteVault,
62
+ PageFetchHTTPError: () => PageFetchHTTPError,
63
+ PageFetchResponse: () => PageFetchResponse,
64
+ RUN_API_KEY_HEADER: () => RUN_API_KEY_HEADER,
65
+ RemoteFileStorage: () => RemoteFileStorage,
66
+ SESSION_START_TRIES: () => SESSION_START_TRIES,
67
+ ScrapeFailedError: () => ScrapeFailedError,
40
68
  Session: () => Session,
41
69
  SessionFiles: () => SessionFiles,
70
+ TIMEOUT_HEADER: () => TIMEOUT_HEADER,
71
+ actions: () => actions,
42
72
  agentStart: () => agentStart,
43
73
  agentStatus: () => agentStatus,
44
74
  agentStop: () => agentStop,
45
75
  anythingStart: () => anythingStart,
76
+ buildFetchScript: () => buildFetchScript,
77
+ checkForLatestVersion: () => checkForLatestVersion,
46
78
  client: () => client,
47
79
  connectLinkContext: () => connectLinkContext,
48
80
  connectLinkMailboxConnect: () => connectLinkMailboxConnect,
@@ -51,7 +83,8 @@ __export(index_exports, {
51
83
  connectLinkStatus: () => connectLinkStatus,
52
84
  connectLinkSubmit: () => connectLinkSubmit,
53
85
  createClient: () => createClient2,
54
- default: () => NotteClient,
86
+ createOrAppendCookiesToFile: () => createOrAppendCookiesToFile,
87
+ default: () => NotteClient3,
55
88
  deleteSecret: () => deleteSecret,
56
89
  deleteSessionFile: () => deleteSessionFile,
57
90
  downloadSessionFile: () => downloadSessionFile,
@@ -69,6 +102,7 @@ __export(index_exports, {
69
102
  functionScheduleDelete: () => functionScheduleDelete,
70
103
  functionScheduleSet: () => functionScheduleSet,
71
104
  functionUpdate: () => functionUpdate,
105
+ getRootDomain: () => getRootDomain,
72
106
  getScript: () => getScript,
73
107
  getSecret: () => getSecret,
74
108
  getSessionScript: () => getSessionScript,
@@ -76,6 +110,9 @@ __export(index_exports, {
76
110
  getUsageLogs: () => getUsageLogs,
77
111
  healthCheck: () => healthCheck,
78
112
  improvePrompt: () => improvePrompt,
113
+ installServerOwnedDialogPolicy: () => installServerOwnedDialogPolicy,
114
+ isCaptchaSolveAction: () => isCaptchaSolveAction,
115
+ isValidMfaSecret: () => isValidMfaSecret,
79
116
  listAgents: () => listAgents,
80
117
  listFunctionRunsByFunctionId: () => listFunctionRunsByFunctionId,
81
118
  listFunctions: () => listFunctions,
@@ -91,18 +128,21 @@ __export(index_exports, {
91
128
  mailboxSync: () => mailboxSync,
92
129
  mailboxUpdate: () => mailboxUpdate,
93
130
  managedAuthConnectLinkCreate: () => managedAuthConnectLinkCreate,
94
- mintProxyCredentials: () => mintProxyCredentials,
95
131
  nudgePrompt: () => nudgePrompt,
132
+ observeServerOwnedDialog: () => observeServerOwnedDialog,
96
133
  pageExecute: () => pageExecute,
97
134
  pageObserve: () => pageObserve,
98
135
  pageScrape: () => pageScrape,
99
136
  pageScreenshot: () => pageScreenshot,
137
+ parseAgentStatusMessage: () => parseAgentStatusMessage,
100
138
  personaCreate: () => personaCreate,
101
139
  personaDelete: () => personaDelete,
102
140
  personaEmailsList: () => personaEmailsList,
103
141
  personaGet: () => personaGet,
104
142
  personaSmsList: () => personaSmsList,
105
143
  personaUpdate: () => personaUpdate,
144
+ prepareAgentRequest: () => prepareAgentRequest,
145
+ processScrapeResponse: () => processScrapeResponse,
106
146
  profileCookiesGet: () => profileCookiesGet,
107
147
  profileCookiesSet: () => profileCookiesSet,
108
148
  profileCreate: () => profileCreate,
@@ -110,7 +150,11 @@ __export(index_exports, {
110
150
  profileDuplicate: () => profileDuplicate,
111
151
  profileGet: () => profileGet,
112
152
  profileList: () => profileList,
153
+ readCookiesFile: () => readCookiesFile,
113
154
  readyCheck: () => readyCheck,
155
+ responseFromEvaluated: () => responseFromEvaluated,
156
+ retry: () => retry,
157
+ sanitizeFilename: () => sanitizeFilename,
114
158
  scrapeFromHtml: () => scrapeFromHtml,
115
159
  scrapeWebpage: () => scrapeWebpage,
116
160
  searchWeb: () => searchWeb,
@@ -123,8 +167,12 @@ __export(index_exports, {
123
167
  sessionStart: () => sessionStart,
124
168
  sessionStatus: () => sessionStatus,
125
169
  sessionStop: () => sessionStop,
170
+ startVersionCheck: () => startVersionCheck,
126
171
  storeSecret: () => storeSecret,
127
172
  uploadSessionFile: () => uploadSessionFile,
173
+ validateCredentials: () => validateCredentials,
174
+ validateMfaSecret: () => validateMfaSecret,
175
+ validateUrl: () => validateUrl,
128
176
  vaultCreate: () => vaultCreate,
129
177
  vaultCredentialsAdd: () => vaultCredentialsAdd,
130
178
  vaultCredentialsDelete: () => vaultCredentialsDelete,
@@ -139,23 +187,40 @@ __export(index_exports, {
139
187
  module.exports = __toCommonJS(index_exports);
140
188
 
141
189
  // src/version.ts
142
- var SDK_VERSION = true ? "0.0.22" : "dev";
190
+ var SDK_VERSION = true ? "1.9.0" : "dev";
143
191
  var SDK_PACKAGE_NAME = true ? "notte-sdk" : "notte-sdk";
144
192
 
145
193
  // src/version-check.ts
146
194
  var DISABLE_ENV = "NOTTE_SDK_DISABLE_VERSION_CHECK";
147
195
  var VERSION_CHECK_TIMEOUT_MS = 1500;
148
- var hasStartedVersionCheck = false;
196
+ var versionCheckPerformed = false;
197
+ var cachedLatestVersion;
149
198
  function startVersionCheck() {
150
- if (hasStartedVersionCheck || !shouldCheckVersion()) {
199
+ if (versionCheckPerformed || !shouldCheckVersion()) {
151
200
  return;
152
201
  }
153
- hasStartedVersionCheck = true;
202
+ versionCheckPerformed = true;
154
203
  const timer = setTimeout(() => {
155
204
  void checkForLatestVersion().catch(() => void 0);
156
205
  }, 0);
157
206
  timer.unref?.();
158
207
  }
208
+ function isVersionOlder(current, latest) {
209
+ return isVersionGreater(latest, current);
210
+ }
211
+ function upgradeSuggestion(currentVersion = SDK_VERSION) {
212
+ if (!cachedLatestVersion || !isPublishedVersion(currentVersion)) {
213
+ return void 0;
214
+ }
215
+ return isVersionOlder(currentVersion, cachedLatestVersion) ? cachedLatestVersion : void 0;
216
+ }
217
+ function createUpgradeErrorMessage(errorContext, latestVersion, originalError, currentVersion = SDK_VERSION, packageName = SDK_PACKAGE_NAME) {
218
+ let message = `${errorContext}. This might be due to API schema changes. Current SDK version: ${currentVersion}, Latest available: ${latestVersion}. Either you made a mistake in the request arguments, or you should upgrade to the latest ${packageName} version by running: 'npm install ${packageName}@${latestVersion}'`;
219
+ if (originalError) {
220
+ message += `. Original error: ${originalError}`;
221
+ }
222
+ return message;
223
+ }
159
224
  async function checkForLatestVersion(options = {}) {
160
225
  const currentVersion = options.currentVersion ?? SDK_VERSION;
161
226
  const packageName = options.packageName ?? SDK_PACKAGE_NAME;
@@ -166,11 +231,15 @@ async function checkForLatestVersion(options = {}) {
166
231
  return;
167
232
  }
168
233
  const latestVersion = await fetchLatestVersion(packageName, fetchFn);
169
- if (!latestVersion || !isVersionGreater(latestVersion, currentVersion)) {
234
+ if (!latestVersion) {
235
+ return;
236
+ }
237
+ cachedLatestVersion = latestVersion;
238
+ if (!isVersionGreater(latestVersion, currentVersion)) {
170
239
  return;
171
240
  }
172
241
  warnFn(
173
- `[${packageName}] A newer version is available: ${currentVersion} -> ${latestVersion}. Update with: npm install ${packageName}@latest`
242
+ `\u26A0\uFE0F You are using ${packageName} version ${currentVersion}, but version ${latestVersion} is available on npm. Run 'npm install ${packageName}@${latestVersion}' to avoid any interruptions.`
174
243
  );
175
244
  }
176
245
  function shouldCheckVersion(currentVersion = SDK_VERSION, fetchFn = globalThis.fetch, env = getEnv()) {
@@ -186,7 +255,8 @@ async function fetchLatestVersion(packageName, fetchFn) {
186
255
  try {
187
256
  const response = await fetchFn(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
188
257
  headers: {
189
- accept: "application/json"
258
+ accept: "application/json",
259
+ "user-agent": `${packageName}/${SDK_VERSION} (https://github.com/nottelabs/notte)`
190
260
  },
191
261
  signal: controller?.signal
192
262
  });
@@ -290,7 +360,7 @@ function createSseClient({
290
360
  ...options
291
361
  }) {
292
362
  let lastEventId;
293
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
363
+ const sleep2 = sseSleepFn ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
294
364
  const createStream = async function* () {
295
365
  let retryDelay = sseDefaultRetryDelay ?? 3e3;
296
366
  let attempt = 0;
@@ -394,7 +464,7 @@ function createSseClient({
394
464
  break;
395
465
  }
396
466
  const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
397
- await sleep(backoff);
467
+ await sleep2(backoff);
398
468
  }
399
469
  }
400
470
  };
@@ -791,9 +861,7 @@ var mergeHeaders = (...headers) => {
791
861
  return mergedHeaders;
792
862
  };
793
863
  var Interceptors = class {
794
- constructor() {
795
- this.fns = [];
796
- }
864
+ fns = [];
797
865
  clear() {
798
866
  this.fns = [];
799
867
  }
@@ -1049,7 +1117,7 @@ var createClient = (config = {}) => {
1049
1117
  };
1050
1118
 
1051
1119
  // src/lib/client/client.gen.ts
1052
- var client = createClient(createConfig({ baseUrl: "https://us-staging.notte.cc" }));
1120
+ var client = createClient(createConfig({ baseUrl: "https://api.notte.cc" }));
1053
1121
 
1054
1122
  // src/lib/client/sdk.gen.ts
1055
1123
  var listSessions = (options) => (options?.client ?? client).get({
@@ -1609,15 +1677,6 @@ var deleteSecret = (options) => (options.client ?? client).delete({
1609
1677
  url: "/secrets/{secret_id}",
1610
1678
  ...options
1611
1679
  });
1612
- var mintProxyCredentials = (options) => (options.client ?? client).post({
1613
- security: [{ scheme: "bearer", type: "http" }],
1614
- url: "/proxies/gateway/credentials",
1615
- ...options,
1616
- headers: {
1617
- "Content-Type": "application/json",
1618
- ...options.headers
1619
- }
1620
- });
1621
1680
  var healthCheck = (options) => (options?.client ?? client).get({ url: "/health", ...options });
1622
1681
  var readyCheck = (options) => (options?.client ?? client).get({ url: "/ready", ...options });
1623
1682
  var scrapeWebpage = (options) => (options.client ?? client).post({
@@ -1648,6 +1707,582 @@ var scrapeFromHtml = (options) => (options.client ?? client).post({
1648
1707
  }
1649
1708
  });
1650
1709
 
1710
+ // src/session.ts
1711
+ var import_node_fs = require("fs");
1712
+ var import_promises2 = require("fs/promises");
1713
+ var import_node_path2 = require("path");
1714
+
1715
+ // src/errors.ts
1716
+ var NotteError = class extends Error {
1717
+ constructor(message, options) {
1718
+ super(message, options);
1719
+ this.name = new.target.name;
1720
+ }
1721
+ };
1722
+ function describeBody(error) {
1723
+ try {
1724
+ return JSON.stringify(error);
1725
+ } catch {
1726
+ return String(error);
1727
+ }
1728
+ }
1729
+ var NotteAPIError = class _NotteAPIError extends NotteError {
1730
+ path;
1731
+ statusCode;
1732
+ error;
1733
+ response;
1734
+ constructor(path, statusCode, error, response) {
1735
+ super(`Request to \`${path}\` failed with status code ${statusCode}: ${describeBody(error)}`);
1736
+ this.path = path;
1737
+ this.statusCode = statusCode;
1738
+ this.error = error;
1739
+ this.response = response;
1740
+ }
1741
+ /** The API's own error message, when it sent one. */
1742
+ get apiMessage() {
1743
+ const message = this.error.message ?? this.error.detail;
1744
+ return typeof message === "string" ? message : void 0;
1745
+ }
1746
+ static fromBody(path, statusCode, body, response) {
1747
+ return new _NotteAPIError(path, statusCode, normalizeErrorBody(body), response);
1748
+ }
1749
+ };
1750
+ var NotteAPIExecutionError = class extends NotteAPIError {
1751
+ constructor(path, statusCode, error, response) {
1752
+ super(path, statusCode, error, response);
1753
+ this.message = `Error on ${path}: ${describeBody(error)}`;
1754
+ }
1755
+ };
1756
+ var AuthenticationError = class extends NotteError {
1757
+ constructor(message) {
1758
+ super(`Authentication failed. ${message}`);
1759
+ }
1760
+ };
1761
+ var InvalidRequestError = class extends NotteError {
1762
+ constructor(message) {
1763
+ super(`Invalid request. ${message}`);
1764
+ }
1765
+ };
1766
+ var NotteTimeoutError = class extends NotteError {
1767
+ };
1768
+ var FailedToRunCloudFunctionError = class extends NotteError {
1769
+ functionId;
1770
+ functionRunId;
1771
+ response;
1772
+ constructor(functionId, functionRunId, response) {
1773
+ super(
1774
+ `Function ${functionId} run ${functionRunId} failed: ${typeof response === "string" ? response : describeBody(response ?? {})}`
1775
+ );
1776
+ this.functionId = functionId;
1777
+ this.functionRunId = functionRunId;
1778
+ this.response = response;
1779
+ }
1780
+ };
1781
+ var ScrapeFailedError = class extends NotteError {
1782
+ structuredError;
1783
+ constructor(error) {
1784
+ super(`Scrape failed: ${error ?? "unknown error"}`);
1785
+ this.structuredError = error;
1786
+ }
1787
+ };
1788
+ var ActionExecutionError = class _ActionExecutionError extends NotteError {
1789
+ errorType;
1790
+ devMessage;
1791
+ userMessage;
1792
+ agentMessage;
1793
+ shouldRetryLater;
1794
+ shouldNotifyTeam;
1795
+ constructor(detail) {
1796
+ super(detail.dev_message || detail.user_message || detail.error_type);
1797
+ this.errorType = detail.error_type;
1798
+ this.devMessage = detail.dev_message;
1799
+ this.userMessage = detail.user_message;
1800
+ this.agentMessage = detail.agent_message;
1801
+ this.shouldRetryLater = detail.should_retry_later ?? false;
1802
+ this.shouldNotifyTeam = detail.should_notify_team ?? false;
1803
+ }
1804
+ static fromMessage(message, errorType = "NotteBaseError") {
1805
+ return new _ActionExecutionError({
1806
+ error_type: errorType,
1807
+ dev_message: message,
1808
+ user_message: message,
1809
+ agent_message: message
1810
+ });
1811
+ }
1812
+ };
1813
+ var EvaluateJsNoDataError = class extends NotteError {
1814
+ constructor() {
1815
+ super("evaluate_js returned no data. Upgrade the Notte API to a build that returns the evaluated value.");
1816
+ }
1817
+ };
1818
+ var FetchResponseDecodeError = class extends NotteError {
1819
+ constructor(reason) {
1820
+ super(`Failed to decode the response returned by the page fetch: ${reason}`);
1821
+ }
1822
+ };
1823
+ function normalizeErrorBody(body) {
1824
+ if (body && typeof body === "object" && !Array.isArray(body)) {
1825
+ return body;
1826
+ }
1827
+ if (body === void 0 || body === null || body === "") {
1828
+ return {};
1829
+ }
1830
+ return { message: typeof body === "string" ? body : String(body) };
1831
+ }
1832
+ var DEFAULT_RETRY_MESSAGE = "An error occurred while executing the function. Try again later...";
1833
+ function retry(fn, options) {
1834
+ const {
1835
+ maxTries,
1836
+ delayMs = 5e3,
1837
+ errorMessage = DEFAULT_RETRY_MESSAGE,
1838
+ shouldRetry = () => true,
1839
+ onRetry = (error, attempt, total) => {
1840
+ console.warn(`Failed to execute ${fn.name || "function"}: ${String(error)} (attempt ${attempt}/${total})`);
1841
+ }
1842
+ } = options;
1843
+ if (!Number.isInteger(maxTries) || maxTries < 1) {
1844
+ throw new InvalidRequestError("maxTries must be a positive integer");
1845
+ }
1846
+ return async (...args) => {
1847
+ let lastError;
1848
+ for (let attempt = 1; attempt <= maxTries; attempt += 1) {
1849
+ try {
1850
+ return await fn(...args);
1851
+ } catch (error) {
1852
+ lastError = error;
1853
+ if (attempt === maxTries || !shouldRetry(error, attempt)) {
1854
+ break;
1855
+ }
1856
+ onRetry(error, attempt, maxTries);
1857
+ await sleep(delayMs);
1858
+ }
1859
+ }
1860
+ throw new Error(errorMessage, { cause: lastError });
1861
+ };
1862
+ }
1863
+ function sleep(ms) {
1864
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1865
+ }
1866
+
1867
+ // src/actions.ts
1868
+ function builder(type) {
1869
+ return (...[input]) => ({ ...input ?? {}, type });
1870
+ }
1871
+ var goto = builder("goto");
1872
+ var gotoNewTab = builder("goto_new_tab");
1873
+ var closeTab = builder("close_tab");
1874
+ var switchTab = builder("switch_tab");
1875
+ var goBack = builder("go_back");
1876
+ var goForward = builder("go_forward");
1877
+ var reload = builder("reload");
1878
+ var wait = builder("wait");
1879
+ var pressKey = builder("press_key");
1880
+ var scrollUp = builder("scroll_up");
1881
+ var scrollDown = builder("scroll_down");
1882
+ var captchaSolve = builder("captcha_solve");
1883
+ var help = builder("help");
1884
+ var completion = builder("completion");
1885
+ var scrape = builder("scrape");
1886
+ var emailRead = builder("email_read");
1887
+ var emailVerificationRead = builder("email_verification_read");
1888
+ var smsRead = builder("sms_read");
1889
+ var evaluateJs = builder("evaluate_js");
1890
+ var formFill = builder("form_fill");
1891
+ var click = builder("click");
1892
+ var fill = builder("fill");
1893
+ var multiFactorFill = builder("multi_factor_fill");
1894
+ var fallbackFill = builder("fallback_fill");
1895
+ var check = builder("check");
1896
+ var selectDropdownOption = builder("select_dropdown_option");
1897
+ var uploadFile = builder("upload_file");
1898
+ var downloadFile = builder("download_file");
1899
+ var actions = {
1900
+ goto,
1901
+ gotoNewTab,
1902
+ closeTab,
1903
+ switchTab,
1904
+ goBack,
1905
+ goForward,
1906
+ reload,
1907
+ wait,
1908
+ pressKey,
1909
+ scrollUp,
1910
+ scrollDown,
1911
+ captchaSolve,
1912
+ help,
1913
+ completion,
1914
+ scrape,
1915
+ emailRead,
1916
+ emailVerificationRead,
1917
+ smsRead,
1918
+ evaluateJs,
1919
+ formFill,
1920
+ click,
1921
+ fill,
1922
+ multiFactorFill,
1923
+ fallbackFill,
1924
+ check,
1925
+ selectDropdownOption,
1926
+ uploadFile,
1927
+ downloadFile
1928
+ };
1929
+ function isCaptchaSolveAction(action) {
1930
+ return action.type === "captcha_solve";
1931
+ }
1932
+
1933
+ // src/page-fetch.ts
1934
+ var CONTENT_TYPE = "content-type";
1935
+ function hasHeader(headers, name) {
1936
+ return Object.keys(headers).some((key) => key.toLowerCase() === name);
1937
+ }
1938
+ function urlencode(mapping) {
1939
+ const search = new URLSearchParams();
1940
+ for (const [key, value] of Object.entries(mapping)) {
1941
+ if (Array.isArray(value)) {
1942
+ for (const item of value) search.append(key, String(item));
1943
+ } else {
1944
+ search.append(key, String(value));
1945
+ }
1946
+ }
1947
+ return search.toString();
1948
+ }
1949
+ function withParams(url, params) {
1950
+ const hashIndex = url.indexOf("#");
1951
+ const fragment = hashIndex >= 0 ? url.slice(hashIndex) : "";
1952
+ const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
1953
+ const queryIndex = base.indexOf("?");
1954
+ const path = queryIndex >= 0 ? base.slice(0, queryIndex) : base;
1955
+ const existing = queryIndex >= 0 ? base.slice(queryIndex + 1) : "";
1956
+ let query = urlencode(params);
1957
+ if (existing) {
1958
+ query = `${existing}&${query}`;
1959
+ }
1960
+ return `${path}?${query}${fragment}`;
1961
+ }
1962
+ function buildFetchScript(url, options = {}) {
1963
+ const { method = "GET", headers, params, json, data, timeoutMs, allowInsecure = false } = options;
1964
+ const redirect = options.redirect ?? (allowInsecure ? "follow" : "manual");
1965
+ if (json !== void 0 && data !== void 0) {
1966
+ throw new InvalidRequestError("pass either json or data, not both");
1967
+ }
1968
+ if (timeoutMs !== void 0 && !(timeoutMs > 0)) {
1969
+ throw new InvalidRequestError("timeout must be positive");
1970
+ }
1971
+ let requestUrl = url;
1972
+ if (params && Object.keys(params).length > 0) {
1973
+ requestUrl = withParams(url, params);
1974
+ }
1975
+ const requestHeaders = { ...headers ?? {} };
1976
+ let body;
1977
+ if (json !== void 0) {
1978
+ body = JSON.stringify(json);
1979
+ if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
1980
+ requestHeaders["Content-Type"] = "application/json";
1981
+ }
1982
+ } else if (data !== void 0 && typeof data !== "string") {
1983
+ body = urlencode(data);
1984
+ if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
1985
+ requestHeaders["Content-Type"] = "application/x-www-form-urlencoded";
1986
+ }
1987
+ } else if (data !== void 0) {
1988
+ body = data;
1989
+ }
1990
+ const init = {
1991
+ method: method.toUpperCase(),
1992
+ headers: requestHeaders,
1993
+ credentials: "include",
1994
+ redirect
1995
+ };
1996
+ if (body !== void 0) {
1997
+ if (init.method === "GET" || init.method === "HEAD") {
1998
+ throw new InvalidRequestError(`${String(init.method)} requests cannot have a body`);
1999
+ }
2000
+ init.body = body;
2001
+ }
2002
+ let abort = "";
2003
+ if (timeoutMs !== void 0) {
2004
+ abort = `const controller = new AbortController();setTimeout(() => controller.abort(), ${Math.trunc(timeoutMs)});init.signal = controller.signal;`;
2005
+ }
2006
+ return `(async () => {const init = ${JSON.stringify(init)};${abort}const target = new URL(${JSON.stringify(requestUrl)}, location.href);const allowInsecure = ${allowInsecure ? "true" : "false"};if (target.protocol !== 'https:' && !allowInsecure) { throw new Error('Refusing to fetch ' + target.origin + ' over plaintext; pass allowInsecure: true to override');}const response = await fetch(target.toString(), init);if (response.type === 'opaqueredirect') { throw new Error('The request to ' + target.origin + ' was redirected; redirects are not followed because the page cookies could be sent in cleartext. Fetch the final URL directly or pass allowInsecure: true');}if (!allowInsecure && new URL(response.url).protocol !== 'https:') { throw new Error('Refusing to read a response served over plaintext from ' + new URL(response.url).origin + '; pass allowInsecure: true to override');}const bytes = new Uint8Array(await response.arrayBuffer());let binary = '';for (let i = 0; i < bytes.length; i += 0x8000) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000));}const headers = {};response.headers.forEach((value, key) => { headers[key] = value; });return JSON.stringify({status: response.status, url: response.url, headers: headers, body_b64: btoa(binary)});})()`;
2007
+ }
2008
+ var REASON_PHRASES = {
2009
+ 100: "Continue",
2010
+ 101: "Switching Protocols",
2011
+ 102: "Processing",
2012
+ 103: "Early Hints",
2013
+ 200: "OK",
2014
+ 201: "Created",
2015
+ 202: "Accepted",
2016
+ 203: "Non-Authoritative Information",
2017
+ 204: "No Content",
2018
+ 205: "Reset Content",
2019
+ 206: "Partial Content",
2020
+ 207: "Multi-Status",
2021
+ 208: "Already Reported",
2022
+ 226: "IM Used",
2023
+ 300: "Multiple Choices",
2024
+ 301: "Moved Permanently",
2025
+ 302: "Found",
2026
+ 303: "See Other",
2027
+ 304: "Not Modified",
2028
+ 305: "Use Proxy",
2029
+ 307: "Temporary Redirect",
2030
+ 308: "Permanent Redirect",
2031
+ 400: "Bad Request",
2032
+ 401: "Unauthorized",
2033
+ 402: "Payment Required",
2034
+ 403: "Forbidden",
2035
+ 404: "Not Found",
2036
+ 405: "Method Not Allowed",
2037
+ 406: "Not Acceptable",
2038
+ 407: "Proxy Authentication Required",
2039
+ 408: "Request Timeout",
2040
+ 409: "Conflict",
2041
+ 410: "Gone",
2042
+ 411: "Length Required",
2043
+ 412: "Precondition Failed",
2044
+ 413: "Request Entity Too Large",
2045
+ 414: "Request-URI Too Long",
2046
+ 415: "Unsupported Media Type",
2047
+ 416: "Requested Range Not Satisfiable",
2048
+ 417: "Expectation Failed",
2049
+ 418: "I'm a Teapot",
2050
+ 421: "Misdirected Request",
2051
+ 422: "Unprocessable Entity",
2052
+ 423: "Locked",
2053
+ 424: "Failed Dependency",
2054
+ 425: "Too Early",
2055
+ 426: "Upgrade Required",
2056
+ 428: "Precondition Required",
2057
+ 429: "Too Many Requests",
2058
+ 431: "Request Header Fields Too Large",
2059
+ 451: "Unavailable For Legal Reasons",
2060
+ 500: "Internal Server Error",
2061
+ 501: "Not Implemented",
2062
+ 502: "Bad Gateway",
2063
+ 503: "Service Unavailable",
2064
+ 504: "Gateway Timeout",
2065
+ 505: "HTTP Version Not Supported",
2066
+ 506: "Variant Also Negotiates",
2067
+ 507: "Insufficient Storage",
2068
+ 508: "Loop Detected",
2069
+ 510: "Not Extended",
2070
+ 511: "Network Authentication Required"
2071
+ };
2072
+ var PageFetchHTTPError = class extends NotteError {
2073
+ response;
2074
+ constructor(response) {
2075
+ const kind = response.status < 500 ? "Client Error" : "Server Error";
2076
+ super(`${response.status} ${kind}: ${response.statusText} for url: ${response.url}`);
2077
+ this.response = response;
2078
+ }
2079
+ };
2080
+ function encodingFor(headers, content) {
2081
+ const contentType = Object.entries(headers).find(([key]) => key.toLowerCase() === CONTENT_TYPE)?.[1] ?? "";
2082
+ for (const param of contentType.split(";").slice(1)) {
2083
+ const [name, ...rest] = param.split("=");
2084
+ const value = rest.join("=");
2085
+ if (name?.trim().toLowerCase() === "charset" && value) {
2086
+ return value.trim().replace(/^["']|["']$/g, "");
2087
+ }
2088
+ }
2089
+ try {
2090
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
2091
+ } catch {
2092
+ return void 0;
2093
+ }
2094
+ return "utf-8";
2095
+ }
2096
+ var PageFetchResponse = class {
2097
+ status;
2098
+ statusText;
2099
+ headers;
2100
+ /** The final URL after redirects. */
2101
+ url;
2102
+ /** The declared charset, `utf-8` when the body is valid utf-8, else `undefined`. */
2103
+ encoding;
2104
+ content;
2105
+ constructor(init) {
2106
+ this.status = init.status;
2107
+ this.statusText = REASON_PHRASES[init.status] ?? "";
2108
+ this.headers = new Headers(init.headers);
2109
+ this.url = init.url;
2110
+ this.content = init.content;
2111
+ this.encoding = encodingFor(init.headers, init.content);
2112
+ }
2113
+ get ok() {
2114
+ return this.status >= 200 && this.status < 300;
2115
+ }
2116
+ /** The raw body bytes. */
2117
+ bytes() {
2118
+ return this.content;
2119
+ }
2120
+ async arrayBuffer() {
2121
+ return this.content.slice().buffer;
2122
+ }
2123
+ async text() {
2124
+ let decoder;
2125
+ try {
2126
+ decoder = new TextDecoder(this.encoding ?? "utf-8");
2127
+ } catch {
2128
+ decoder = new TextDecoder("utf-8");
2129
+ }
2130
+ return decoder.decode(this.content);
2131
+ }
2132
+ async json() {
2133
+ return JSON.parse(await this.text());
2134
+ }
2135
+ /** Throw `PageFetchHTTPError` on a 4xx/5xx status, like `requests.Response.raise_for_status()`. */
2136
+ raiseForStatus() {
2137
+ if (this.status >= 400) {
2138
+ throw new PageFetchHTTPError(this);
2139
+ }
2140
+ return this;
2141
+ }
2142
+ };
2143
+ var BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
2144
+ function decodeBase64(value) {
2145
+ if (!BASE64_PATTERN.test(value) || value.length % 4 !== 0) {
2146
+ throw new Error("body_b64 is not valid base64");
2147
+ }
2148
+ return new Uint8Array(Buffer.from(value, "base64"));
2149
+ }
2150
+ function responseFromEvaluated(raw) {
2151
+ let payload;
2152
+ try {
2153
+ payload = JSON.parse(raw);
2154
+ } catch (error) {
2155
+ throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
2156
+ }
2157
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
2158
+ throw new FetchResponseDecodeError("envelope is not an object");
2159
+ }
2160
+ const envelope = payload;
2161
+ try {
2162
+ if (!("status" in envelope)) {
2163
+ throw new Error("'status'");
2164
+ }
2165
+ const status = Number(envelope.status);
2166
+ if (!Number.isInteger(status)) {
2167
+ throw new Error(`invalid status: ${String(envelope.status)}`);
2168
+ }
2169
+ const rawHeaders = envelope.headers ?? {};
2170
+ if (typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
2171
+ throw new Error("headers is not an object");
2172
+ }
2173
+ const headers = {};
2174
+ for (const [key, value] of Object.entries(rawHeaders)) {
2175
+ headers[String(key)] = String(value);
2176
+ }
2177
+ const content = decodeBase64(String(envelope.body_b64 ?? ""));
2178
+ const url = String(envelope.url ?? "");
2179
+ return new PageFetchResponse({ status, headers, url, content });
2180
+ } catch (error) {
2181
+ throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
2182
+ }
2183
+ }
2184
+
2185
+ // src/cookies.ts
2186
+ var import_promises = require("fs/promises");
2187
+ var import_node_path = require("path");
2188
+ async function readCookiesFile(cookieFile) {
2189
+ const cookies = JSON.parse(await (0, import_promises.readFile)(cookieFile, "utf-8"));
2190
+ if (!Array.isArray(cookies)) {
2191
+ throw new InvalidRequestError(`Cookie file ${cookieFile} must contain a JSON list of cookies`);
2192
+ }
2193
+ return cookies;
2194
+ }
2195
+ async function createOrAppendCookiesToFile(cookieFile, cookies) {
2196
+ console.info(`\u{1F36A} Automatically saving cookies to ${cookieFile}`);
2197
+ await withFileLock(cookieFile, async () => {
2198
+ let existing = [];
2199
+ try {
2200
+ existing = await readCookiesFile(cookieFile);
2201
+ } catch (error) {
2202
+ if (!isNotFound(error)) {
2203
+ throw error;
2204
+ }
2205
+ }
2206
+ existing.push(...cookies);
2207
+ await (0, import_promises.writeFile)(cookieFile, JSON.stringify(existing), "utf-8");
2208
+ });
2209
+ }
2210
+ var fileLocks = /* @__PURE__ */ new Map();
2211
+ async function withFileLock(path, fn) {
2212
+ const key = (0, import_node_path.resolve)(path);
2213
+ const previous = fileLocks.get(key) ?? Promise.resolve();
2214
+ let release;
2215
+ const current = new Promise((done) => {
2216
+ release = done;
2217
+ });
2218
+ const tail = previous.then(() => current);
2219
+ fileLocks.set(key, tail);
2220
+ await previous;
2221
+ try {
2222
+ return await fn();
2223
+ } finally {
2224
+ release();
2225
+ if (fileLocks.get(key) === tail) {
2226
+ fileLocks.delete(key);
2227
+ }
2228
+ }
2229
+ }
2230
+ function isNotFound(error) {
2231
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
2232
+ }
2233
+
2234
+ // src/scrape.ts
2235
+ function isZodSchema(value) {
2236
+ return typeof value === "object" && value !== null && typeof value.parse === "function";
2237
+ }
2238
+ function requiresSchema(options) {
2239
+ return Boolean(options.response_format || options.instructions);
2240
+ }
2241
+ async function buildScrapeBody(options) {
2242
+ const { raiseOnFailure: _raise, json_schema, response_format, ...rest } = options;
2243
+ const body = { ...rest };
2244
+ if (isZodSchema(response_format)) {
2245
+ if (json_schema) {
2246
+ body.response_format = json_schema;
2247
+ } else {
2248
+ const { z: z2 } = await import("zod");
2249
+ body.response_format = z2.toJSONSchema(response_format);
2250
+ }
2251
+ } else if (response_format) {
2252
+ body.response_format = response_format;
2253
+ }
2254
+ return body;
2255
+ }
2256
+ function processScrapeResponse(scrapeResponse, options) {
2257
+ if (!scrapeResponse) {
2258
+ throw new ScrapeFailedError("empty response");
2259
+ }
2260
+ const raiseOnFailure = options.raiseOnFailure ?? true;
2261
+ if (options.only_images) {
2262
+ return scrapeResponse.images ?? [];
2263
+ }
2264
+ if (requiresSchema(options)) {
2265
+ const structured = scrapeResponse.structured;
2266
+ if (!structured) {
2267
+ throw new ScrapeFailedError("the API returned no structured data. This should not happen, please report it.");
2268
+ }
2269
+ const success = structured.success ?? false;
2270
+ const schema = isZodSchema(options.response_format) ? options.response_format : void 0;
2271
+ if (!success) {
2272
+ if (raiseOnFailure) {
2273
+ throw new ScrapeFailedError(structured.error ?? void 0);
2274
+ }
2275
+ return { success, error: structured.error ?? null, data: null };
2276
+ }
2277
+ const data = schema && structured.data != null ? schema.parse(structured.data) : structured.data;
2278
+ if (raiseOnFailure) {
2279
+ return data;
2280
+ }
2281
+ return { success, error: structured.error ?? null, data };
2282
+ }
2283
+ return scrapeResponse.markdown;
2284
+ }
2285
+
1651
2286
  // src/utils.ts
1652
2287
  var import_node_child_process = require("child_process");
1653
2288
  function formatError(error) {
@@ -1690,55 +2325,64 @@ function openBrowser(url) {
1690
2325
  }
1691
2326
 
1692
2327
  // src/session.ts
1693
- var import_zod = require("zod");
1694
- function processScrapeResponse(scrapeResponse, options) {
1695
- if (options.only_images && scrapeResponse.images) {
1696
- return scrapeResponse.images;
1697
- }
1698
- if (requiresSchema(options)) {
1699
- const structured = scrapeResponse.structured;
1700
- if (!structured) {
1701
- throw new Error("Failed to scrape structured data. This should not happen. Please report this issue.");
1702
- }
1703
- if (options.response_format && typeof options.response_format.parse === "function") {
1704
- if (structured.data) {
1705
- try {
1706
- const validatedData = options.response_format.parse(structured.data);
1707
- return validatedData;
1708
- } catch (error) {
1709
- throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
1710
- }
1711
- }
1712
- return structured;
1713
- }
1714
- return structured;
1715
- }
1716
- if (options.response_format && typeof options.response_format.parse === "function") {
1717
- try {
1718
- const validatedData = options.response_format.parse(scrapeResponse);
1719
- return validatedData;
1720
- } catch (error) {
1721
- throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
1722
- }
1723
- }
1724
- return scrapeResponse.markdown;
2328
+ var SESSION_START_TRIES = 3;
2329
+ var CLUSTER_OVERLOAD_RETRY_DELAY_MS = 3e4;
2330
+ var CAPTCHA_SOLVE_TIMEOUT_MS = 1e5;
2331
+ var CAPTCHA_SOLVE_TRIES = 3;
2332
+ function observeServerOwnedDialog(_dialog) {
1725
2333
  }
1726
- function requiresSchema(options) {
1727
- return !!(options.response_format || options.instructions);
2334
+ function installServerOwnedDialogPolicy(browser) {
2335
+ for (const context of browser.contexts()) {
2336
+ context.on("dialog", observeServerOwnedDialog);
2337
+ }
1728
2338
  }
1729
2339
  var Session = class {
2340
+ client;
2341
+ options;
2342
+ openViewer;
2343
+ legacyHeadless;
2344
+ defaultRaiseOnFailure;
2345
+ defaultPerceptionType;
2346
+ cookieFile;
2347
+ fileStorage;
2348
+ sessionId = null;
2349
+ isActive = false;
2350
+ response = null;
2351
+ playwrightBrowser = null;
2352
+ playwrightPage = null;
1730
2353
  constructor(client2, options = {}) {
1731
- this.sessionId = null;
1732
- this.isActive = false;
1733
- this.response = null;
1734
2354
  this.client = client2;
1735
- const { open_viewer = false, headless, ...sessionOptions } = options;
1736
- this.options = sessionOptions;
2355
+ const {
2356
+ open_viewer = false,
2357
+ headless,
2358
+ raiseOnFailure = true,
2359
+ perception_type = "fast",
2360
+ cookie_file,
2361
+ storage,
2362
+ ...sessionOptions
2363
+ } = options;
2364
+ this.options = storage ? { ...sessionOptions, use_file_storage: true } : sessionOptions;
1737
2365
  this.openViewer = open_viewer;
1738
2366
  this.legacyHeadless = headless;
1739
- }
2367
+ this.defaultRaiseOnFailure = raiseOnFailure;
2368
+ this.defaultPerceptionType = perception_type;
2369
+ this.cookieFile = cookie_file;
2370
+ this.fileStorage = storage;
2371
+ }
2372
+ // #######################################################################
2373
+ // ############################# Session #################################
2374
+ // #######################################################################
1740
2375
  /**
1741
- * Start the session - creates a new session on the server
2376
+ * Start the session: creates a new session on the server.
2377
+ *
2378
+ * Retries up to `SESSION_START_TRIES` times on 5xx errors, waiting
2379
+ * `CLUSTER_OVERLOAD_RETRY_DELAY_MS` on HTTP 529 (cluster overload). 4xx
2380
+ * errors are never retried.
2381
+ *
2382
+ * Prefer `session.use(async session => { ... })`, which also stops the session.
2383
+ * @example
2384
+ * const session = client.Session();
2385
+ * await session.start();
1742
2386
  */
1743
2387
  async start() {
1744
2388
  if (this.legacyHeadless === false) {
@@ -1753,93 +2397,171 @@ var Session = class {
1753
2397
  if (this.isActive) {
1754
2398
  throw new Error("Session is already active");
1755
2399
  }
1756
- let shouldOpenViewer = false;
2400
+ const body = this.options;
2401
+ let tries = SESSION_START_TRIES;
2402
+ const origTries = tries;
2403
+ let sessionData;
2404
+ while (tries > 0) {
2405
+ tries -= 1;
2406
+ try {
2407
+ const response = await sessionStart({ client: this.client.getClient(), body, throwOnError: true });
2408
+ sessionData = response.data;
2409
+ break;
2410
+ } catch (error) {
2411
+ if (tries === 0 || !(error instanceof NotteAPIError) || error.statusCode < 500) {
2412
+ throw error;
2413
+ }
2414
+ const retryStr = `${origTries - tries}/${origTries - 1}`;
2415
+ if (error.statusCode === 529) {
2416
+ console.warn(
2417
+ `Failed to start session due to cluster overload, retrying in ${CLUSTER_OVERLOAD_RETRY_DELAY_MS / 1e3} seconds (${retryStr})...`
2418
+ );
2419
+ await sleep(CLUSTER_OVERLOAD_RETRY_DELAY_MS);
2420
+ } else {
2421
+ console.warn(`Failed to start session: retrying (${retryStr})`);
2422
+ }
2423
+ }
2424
+ }
2425
+ if (!sessionData) {
2426
+ throw new Error("Failed to start session: empty response");
2427
+ }
2428
+ this.sessionId = sessionData.session_id;
2429
+ this.isActive = true;
2430
+ this.response = sessionData;
2431
+ this.fileStorage = this.fileStorage?.forSession(sessionData.session_id);
1757
2432
  try {
1758
- const response = await sessionStart({
1759
- client: this.client.getClient(),
1760
- body: this.options
1761
- });
1762
- if (response?.error) {
1763
- throw new Error(`Failed to create session: ${formatError(response.error)}`);
2433
+ if (this.openViewer) {
2434
+ this.openViewerInBrowser();
2435
+ }
2436
+ if (this.cookieFile !== void 0) {
2437
+ if ((0, import_node_fs.existsSync)(this.cookieFile)) {
2438
+ console.info(`\u{1F36A} Automatically loading cookies from ${this.cookieFile}`);
2439
+ await this.setCookiesFromFile(this.cookieFile);
2440
+ } else {
2441
+ console.warn(`\u{1F36A} Cookie file ${this.cookieFile} not found, skipping cookie loading`);
2442
+ }
1764
2443
  }
1765
- const sessionData = response.data;
1766
- this.sessionId = sessionData.session_id;
1767
- this.isActive = true;
1768
- this.response = sessionData;
1769
- shouldOpenViewer = this.openViewer;
1770
2444
  } catch (error) {
1771
- throw new Error(`Failed to start session: ${error instanceof Error ? error.message : String(error)}`);
1772
- }
1773
- if (shouldOpenViewer) {
1774
- this.openViewerInBrowser();
2445
+ try {
2446
+ await this.stop("error");
2447
+ } catch (stopError) {
2448
+ console.error(`[Session] Failed to stop ${sessionData.session_id} after a start failure: ${String(stopError)}`);
2449
+ }
2450
+ throw error;
1775
2451
  }
1776
2452
  }
1777
2453
  /**
1778
- * Stop the session - terminates the session on the server
2454
+ * Stop the session and clean up resources (including any Playwright
2455
+ * connection opened with `page()`). When a `cookie_file` was given, the
2456
+ * session cookies are appended to it first. A session the API reports as
2457
+ * already stopped is tolerated with a warning.
2458
+ *
2459
+ * @example
2460
+ * await session.stop();
1779
2461
  */
1780
2462
  async stop(closeReason = "manual") {
1781
2463
  if (!this.isActive || !this.sessionId) {
1782
2464
  return;
1783
2465
  }
1784
- const response = await sessionStop({
1785
- client: this.client.getClient(),
1786
- path: {
1787
- session_id: this.sessionId
1788
- },
1789
- query: {
1790
- close_reason: closeReason
2466
+ const sessionId = this.sessionId;
2467
+ await this.closePlaywright();
2468
+ if (this.cookieFile !== void 0) {
2469
+ try {
2470
+ const cookies = await this.getCookies();
2471
+ await createOrAppendCookiesToFile(this.cookieFile, cookies);
2472
+ } catch (error) {
2473
+ console.error(`\u{1F36A} Error saving cookies to ${this.cookieFile}: ${String(error)}`);
2474
+ }
2475
+ }
2476
+ try {
2477
+ const response = await sessionStop({
2478
+ client: this.client.getClient(),
2479
+ path: { session_id: sessionId },
2480
+ query: { close_reason: closeReason },
2481
+ throwOnError: true
2482
+ });
2483
+ if (response.data.status !== "closed") {
2484
+ throw new Error(`[Session] ${sessionId} failed to stop`);
2485
+ }
2486
+ this.response = response.data;
2487
+ } catch (error) {
2488
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
2489
+ if (message.includes("already stopped") || message.includes("already closed")) {
2490
+ console.warn(`Session ${sessionId} was already stopped`);
2491
+ } else {
2492
+ throw error;
1791
2493
  }
1792
- });
1793
- if (response?.error) {
1794
- throw new Error(`Failed to stop session: ${formatError(response.error)}`);
1795
2494
  }
1796
- this.response = response.data;
1797
2495
  this.isActive = false;
1798
2496
  this.sessionId = null;
1799
2497
  }
2498
+ async closePlaywright() {
2499
+ const browser = this.playwrightBrowser;
2500
+ this.playwrightBrowser = null;
2501
+ this.playwrightPage = null;
2502
+ if (browser) {
2503
+ try {
2504
+ await browser.close();
2505
+ } catch (error) {
2506
+ console.warn(`[Session] Failed to close the Playwright connection: ${String(error)}`);
2507
+ }
2508
+ }
2509
+ }
1800
2510
  /**
1801
- * Get session status
2511
+ * Get the current status of the session. Like Python, this keeps working
2512
+ * after `stop()` (for example to read the recorded `steps`).
2513
+ *
2514
+ * @returns Current session metadata, including lifecycle status and recorded steps. Works after `stop()`.
2515
+ * @throws Error if this session has never been started.
2516
+ * @throws NotteAPIError if the status request fails.
2517
+ * @example
2518
+ * const status = await session.status();
2519
+ * console.log(status.status); // 'active'
1802
2520
  */
1803
2521
  async status() {
1804
- if (!this.sessionId) {
1805
- throw new Error("Session not started");
1806
- }
1807
2522
  const response = await sessionStatus({
1808
2523
  client: this.client.getClient(),
1809
- path: {
1810
- session_id: this.sessionId
1811
- }
2524
+ path: { session_id: this.lastSessionId() },
2525
+ throwOnError: true
1812
2526
  });
1813
- if (response?.error) {
1814
- throw new Error(`Failed to get session status: ${formatError(response.error)}`);
1815
- }
1816
2527
  return response.data;
1817
2528
  }
1818
- /**
1819
- * Get session ID
1820
- */
2529
+ /** Session ID while the session is active, `null` otherwise. */
1821
2530
  getId() {
1822
2531
  return this.sessionId;
1823
2532
  }
1824
- /**
1825
- * Check if session is active
1826
- */
2533
+ /** Whether the session has been started and not stopped yet. */
1827
2534
  isSessionActive() {
1828
2535
  return this.isActive;
1829
2536
  }
1830
- /**
1831
- * Get the last response from session operations
1832
- */
2537
+ /** The last `SessionResponse` received from the API (start, stop or status). */
1833
2538
  getResponse() {
1834
2539
  return this.response;
1835
2540
  }
2541
+ /** The file storage attached to the session, when one was given. */
2542
+ get storage() {
2543
+ return this.fileStorage;
2544
+ }
2545
+ requireSessionId() {
2546
+ if (!this.sessionId) {
2547
+ throw new Error("Session not started");
2548
+ }
2549
+ return this.sessionId;
2550
+ }
2551
+ /** Session ID of the active session, or of the last one when it was stopped. */
2552
+ lastSessionId() {
2553
+ const sessionId = this.sessionId ?? this.response?.session_id;
2554
+ if (!sessionId) {
2555
+ throw new Error("Session not started");
2556
+ }
2557
+ return sessionId;
2558
+ }
1836
2559
  /**
1837
2560
  * Open the live viewer for this session in the local default browser.
2561
+ * @throws If the session has not started or no viewer URL is available.
1838
2562
  */
1839
2563
  viewer() {
1840
- if (!this.sessionId) {
1841
- throw new Error("Session not started");
1842
- }
2564
+ this.requireSessionId();
1843
2565
  const viewerUrl = this.response?.viewer_url;
1844
2566
  if (!viewerUrl) {
1845
2567
  throw new Error("No viewer URL available for this session");
@@ -1855,154 +2577,381 @@ var Session = class {
1855
2577
  openBrowser(viewerUrl);
1856
2578
  }
1857
2579
  /**
1858
- * Set cookies for the session
2580
+ * Get presigned URLs for the session replay. Recordings are finalized after
2581
+ * the session is closed, so by default this polls on 404 until the replay is
2582
+ * ready. It keeps working after `stop()`.
2583
+ *
2584
+ * Throws an `Error` when the API reports the session is still active and a
2585
+ * `NotteTimeoutError` when the replay is not ready within `timeoutMs`.
2586
+ * @example
2587
+ * await session.stop();
2588
+ * const replay = await session.replay();
2589
+ * console.log(replay.mp4_url);
1859
2590
  */
1860
- async setCookies(cookies) {
1861
- if (!this.sessionId) {
1862
- throw new Error("Session not started");
1863
- }
1864
- const response = await sessionCookiesSet({
1865
- client: this.client.getClient(),
1866
- path: {
1867
- session_id: this.sessionId
1868
- },
1869
- body: {
1870
- cookies
2591
+ async replay(options = {}) {
2592
+ const { wait: wait2 = true, timeoutMs = 24e4, pollIntervalMs = 5e3 } = options;
2593
+ const sessionId = this.lastSessionId();
2594
+ const request = async () => {
2595
+ const response = await sessionReplay({
2596
+ client: this.client.getClient(),
2597
+ path: { session_id: sessionId },
2598
+ throwOnError: true
2599
+ });
2600
+ if (!response.data) {
2601
+ throw new Error("Failed to get session replay: empty response");
1871
2602
  }
1872
- });
1873
- if (response?.error) {
1874
- throw new Error(`Failed to set cookies: ${formatError(response.error)}`);
1875
- }
2603
+ return response.data;
2604
+ };
2605
+ if (!wait2) {
2606
+ return request();
2607
+ }
2608
+ const deadline = Date.now() + timeoutMs;
2609
+ for (; ; ) {
2610
+ try {
2611
+ return await request();
2612
+ } catch (error) {
2613
+ if (!(error instanceof NotteAPIError) || error.statusCode !== 404) {
2614
+ throw error;
2615
+ }
2616
+ if (this.isActive && (error.apiMessage ?? "").includes("still active")) {
2617
+ throw new Error(`Session ${sessionId} is still active \u2014 close the session first to generate the replay.`, {
2618
+ cause: error
2619
+ });
2620
+ }
2621
+ if (Date.now() + pollIntervalMs > deadline) {
2622
+ throw new NotteTimeoutError(`Replay for session ${sessionId} not ready within ${timeoutMs}ms`, { cause: error });
2623
+ }
2624
+ await sleep(pollIntervalMs);
2625
+ }
2626
+ }
2627
+ }
2628
+ /**
2629
+ * Download the MP4 replay to a local file, like `ReplayResponse.download()`
2630
+ * in Python. Resolves with the absolute path of the written file and throws
2631
+ * when the replay has no `mp4_url`.
2632
+ *
2633
+ * @example
2634
+ * await session.stop();
2635
+ * const file = await session.downloadReplay('session.mp4');
2636
+ */
2637
+ async downloadReplay(path = "replay.mp4", options = {}) {
2638
+ const replay = await this.replay(options);
2639
+ if (!replay.mp4_url) {
2640
+ throw new Error("No mp4_url available for download");
2641
+ }
2642
+ const response = await fetch(replay.mp4_url);
2643
+ if (!response.ok) {
2644
+ throw new Error(`Failed to download replay: HTTP ${response.status}`);
2645
+ }
2646
+ const target = (0, import_node_path2.resolve)(path);
2647
+ await (0, import_promises2.writeFile)(target, new Uint8Array(await response.arrayBuffer()));
2648
+ return target;
2649
+ }
2650
+ // #######################################################################
2651
+ // ############################# Cookies #################################
2652
+ // #######################################################################
2653
+ /**
2654
+ * Upload cookies to the session.
2655
+ *
2656
+ * @example
2657
+ * await session.setCookies([{ name: 'token', value: 'abc', domain: 'example.com', path: '/', httpOnly: false }]);
2658
+ */
2659
+ async setCookies(cookies) {
2660
+ const response = await sessionCookiesSet({
2661
+ client: this.client.getClient(),
2662
+ path: { session_id: this.requireSessionId() },
2663
+ body: { cookies },
2664
+ throwOnError: true
2665
+ });
1876
2666
  return response.data;
1877
2667
  }
1878
2668
  /**
1879
- * Set cookies from a file
2669
+ * Upload cookies from a JSON file (a list of cookies, like `getCookies()` returns).
2670
+ *
2671
+ * @example
2672
+ * await session.setCookiesFromFile('cookies.json');
1880
2673
  */
1881
2674
  async setCookiesFromFile(cookieFile) {
1882
- const fs = await import("fs/promises");
1883
- const cookieData = await fs.readFile(cookieFile, "utf-8");
1884
- const cookies = JSON.parse(cookieData);
1885
- return this.setCookies(cookies);
2675
+ return this.setCookies(await readCookiesFile(cookieFile));
1886
2676
  }
1887
2677
  /**
1888
- * Get cookies from the session
2678
+ * Get the cookies of the session.
2679
+ *
2680
+ * @example
2681
+ * const cookies = await session.getCookies();
2682
+ * await fs.writeFile('cookies.json', JSON.stringify(cookies));
1889
2683
  */
1890
2684
  async getCookies() {
1891
- if (!this.sessionId) {
1892
- throw new Error("Session not started");
1893
- }
1894
2685
  const response = await sessionCookiesGet({
1895
2686
  client: this.client.getClient(),
1896
- path: {
1897
- session_id: this.sessionId
1898
- }
2687
+ path: { session_id: this.requireSessionId() },
2688
+ throwOnError: true
1899
2689
  });
1900
- if (response?.error) {
1901
- throw new Error(`Failed to get cookies: ${formatError(response.error)}`);
1902
- }
1903
2690
  return response.data.cookies;
1904
2691
  }
2692
+ // #######################################################################
2693
+ // ############################# Debug / CDP #############################
2694
+ // #######################################################################
2695
+ /** Detailed debug information for the session (debug URL, websocket URLs, tabs). */
2696
+ async debugInfo() {
2697
+ const response = await sessionDebugInfo({
2698
+ client: this.client.getClient(),
2699
+ path: { session_id: this.requireSessionId() },
2700
+ throwOnError: true
2701
+ });
2702
+ return response.data;
2703
+ }
1905
2704
  /**
1906
- * Execute an action on the page
2705
+ * Debug information for one tab of the session. The API client has no
2706
+ * dedicated tab endpoint, so this reads the tab from `debugInfo().tabs`.
1907
2707
  */
1908
- async execute(action, raiseOnFailure = true) {
1909
- if (!this.sessionId) {
2708
+ async debugTabInfo(tabIdx = 0) {
2709
+ const debug = await this.debugInfo();
2710
+ const tab = debug.tabs[tabIdx];
2711
+ if (!tab) {
2712
+ throw new Error(`Tab ${tabIdx} not found: the session has ${debug.tabs.length} tab(s)`);
2713
+ }
2714
+ return tab;
2715
+ }
2716
+ /**
2717
+ * Get the Chrome DevTools Protocol WebSocket URL for the session.
2718
+ *
2719
+ * A `cdp_url` given at construction (another browser provider) is returned
2720
+ * untouched. Notte-served URLs, from the start response or the debug info,
2721
+ * carry the database preview branch when one is configured.
2722
+ *
2723
+ * @returns The CDP WebSocket URL for connecting a browser automation client.
2724
+ * @throws Error if no session ID or CDP URL is available.
2725
+ * @example
2726
+ * const browser = await chromium.connectOverCDP(await session.cdpUrl());
2727
+ */
2728
+ async cdpUrl() {
2729
+ if (!this.response) {
1910
2730
  throw new Error("Session not started");
1911
2731
  }
1912
- const response = await pageExecute({
1913
- client: this.client.getClient(),
1914
- path: {
1915
- session_id: this.sessionId
1916
- },
1917
- body: action
1918
- });
1919
- if (response?.error) {
1920
- throw new Error(`Failed to execute action: ${formatError(response.error)}`);
2732
+ if (this.options.cdp_url) {
2733
+ return this.options.cdp_url;
1921
2734
  }
1922
- const result = response.data;
1923
- if (raiseOnFailure && !result.success) {
1924
- throw new Error(`Action execution failed: ${result.message}`);
2735
+ if (this.response.cdp_url) {
2736
+ return this.client.withDbPreview(this.response.cdp_url);
1925
2737
  }
1926
- return result;
2738
+ const debug = await this.debugInfo();
2739
+ return this.client.withDbPreview(debug.ws.cdp);
1927
2740
  }
1928
2741
  /**
1929
- * Observe the current page state
2742
+ * Get a Playwright page connected to the session via CDP. The connection is
2743
+ * established lazily on first access, cached, and closed by `stop()`.
2744
+ * Native dialogs are left to the Notte backend.
2745
+ *
2746
+ * Requires the optional `playwright-core` peer dependency
2747
+ * (`npm install playwright-core`).
2748
+ *
2749
+ * @example
2750
+ * const page = await session.page();
2751
+ * await page.goto('https://www.google.com');
2752
+ * await page.screenshot({ path: 'screenshot.png' });
1930
2753
  */
1931
- async observe(perceptionType = "fast") {
1932
- if (!this.sessionId) {
1933
- throw new Error("Session not started");
2754
+ async page() {
2755
+ if (this.playwrightPage) {
2756
+ return this.playwrightPage;
1934
2757
  }
1935
- const response = await pageObserve({
1936
- client: this.client.getClient(),
1937
- path: {
1938
- session_id: this.sessionId
1939
- },
1940
- body: {
1941
- perception_type: perceptionType
2758
+ let playwright;
2759
+ try {
2760
+ playwright = await import("playwright-core");
2761
+ } catch (error) {
2762
+ throw new Error("Playwright not installed. Run `npm install playwright-core` to use `session.page()`.", {
2763
+ cause: error
2764
+ });
2765
+ }
2766
+ try {
2767
+ if (!this.playwrightBrowser) {
2768
+ const cdpUrl = await this.cdpUrl();
2769
+ this.playwrightBrowser = await playwright.chromium.connectOverCDP(cdpUrl);
2770
+ installServerOwnedDialogPolicy(this.playwrightBrowser);
1942
2771
  }
1943
- });
1944
- if (response?.error) {
1945
- throw new Error(`Failed to observe page: ${formatError(response.error)}`);
2772
+ const page = this.playwrightBrowser.contexts()[0]?.pages()[0];
2773
+ if (!page) {
2774
+ throw new Error("the browser has no open page");
2775
+ }
2776
+ this.playwrightPage = page;
2777
+ return page;
2778
+ } catch (error) {
2779
+ throw new Error(`Failed to access the playwright page from CDP: ${error instanceof Error ? error.message : String(error)}`, {
2780
+ cause: error
2781
+ });
1946
2782
  }
1947
- return response.data;
1948
2783
  }
1949
2784
  /**
1950
- * Get session replay data
2785
+ * Get the workflow code generated from the session steps
2786
+ * (`GET /sessions/{id}/workflow/code`).
2787
+ *
2788
+ * @example
2789
+ * const { python_script } = await session.getScript();
1951
2790
  */
1952
- async replay() {
1953
- if (!this.sessionId) {
1954
- throw new Error("Session not started");
1955
- }
1956
- const response = await sessionReplay({
2791
+ async getScript(options = {}) {
2792
+ const { as_workflow = true, infer_response_format } = options;
2793
+ const response = await getSessionScript({
1957
2794
  client: this.client.getClient(),
1958
- path: {
1959
- session_id: this.sessionId
1960
- }
2795
+ path: { session_id: this.lastSessionId() },
2796
+ query: { as_workflow, ...infer_response_format === void 0 ? {} : { infer_response_format } },
2797
+ throwOnError: true
1961
2798
  });
1962
- if (response?.error) {
1963
- throw new Error(`Failed to get session replay: ${formatError(response.error)}`);
1964
- }
1965
2799
  return response.data;
1966
2800
  }
1967
- async scrape(options = {}) {
1968
- if (!this.sessionId) {
1969
- throw new Error("Session not started");
1970
- }
1971
- const apiOptions = { ...options };
1972
- if (options.response_format && typeof options.response_format.parse === "function") {
1973
- if (options.json_schema) {
1974
- apiOptions.response_format = options.json_schema;
1975
- } else {
1976
- apiOptions.response_format = import_zod.z.toJSONSchema(options.response_format);
1977
- }
1978
- if (!apiOptions.instructions) {
1979
- const schemaStr = JSON.stringify(apiOptions.response_format);
1980
- if (schemaStr.includes("plans") || schemaStr.includes("plan")) {
1981
- apiOptions.instructions = "Extract pricing plans from the page";
1982
- } else if (schemaStr.includes("products") || schemaStr.includes("product")) {
1983
- apiOptions.instructions = "Extract product information from the page";
1984
- } else if (schemaStr.includes("articles") || schemaStr.includes("article")) {
1985
- apiOptions.instructions = "Extract article information from the page";
1986
- } else {
1987
- apiOptions.instructions = "Extract structured data from the page based on the provided schema";
2801
+ // #######################################################################
2802
+ // ############################# Page ####################################
2803
+ // #######################################################################
2804
+ /**
2805
+ * Execute an action on the current page.
2806
+ *
2807
+ * When the action fails and `raiseOnFailure` is true (the default, see the
2808
+ * session option), the structured `exception_detail` returned by the API is
2809
+ * rehydrated into an `ActionExecutionError` whose `errorType` names the
2810
+ * server-side error class. `captcha_solve` actions get a 100 s request
2811
+ * timeout and are retried up to 3 times on HTTP 408.
2812
+ *
2813
+ * @param action - Action type, target, and action-specific arguments.
2814
+ * @param options - Override failure handling for this call; otherwise use the session setting.
2815
+ * @returns The execution response, including `success` and failure details when throwing is disabled.
2816
+ * @throws ActionExecutionError if the action fails and failure throwing is enabled.
2817
+ * @throws NotteAPIError if the API request fails.
2818
+ * @example
2819
+ * import { actions } from 'notte-sdk';
2820
+ *
2821
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2822
+ * await session.execute(actions.fill({ selector: "input[name='email']", value: 'user@example.com' }));
2823
+ * const result = await session.execute({ type: 'click', id: 'B1' }, { raiseOnFailure: false });
2824
+ * if (!result.success) console.log(result.message);
2825
+ */
2826
+ async execute(action, options = {}) {
2827
+ const sessionId = this.requireSessionId();
2828
+ const raiseOnFailure = typeof options === "boolean" ? options : options.raiseOnFailure ?? this.defaultRaiseOnFailure;
2829
+ const isCaptcha = isCaptchaSolveAction(action);
2830
+ let result;
2831
+ let lastError;
2832
+ for (let attempt = 0; attempt < CAPTCHA_SOLVE_TRIES; attempt += 1) {
2833
+ try {
2834
+ const response = await pageExecute({
2835
+ client: this.client.getClient(),
2836
+ path: { session_id: sessionId },
2837
+ body: action,
2838
+ headers: isCaptcha ? { [TIMEOUT_HEADER]: String(CAPTCHA_SOLVE_TIMEOUT_MS) } : void 0,
2839
+ throwOnError: true
2840
+ });
2841
+ result = response.data;
2842
+ break;
2843
+ } catch (error) {
2844
+ if (isCaptcha && error instanceof NotteAPIError && error.statusCode === 408) {
2845
+ lastError = error;
2846
+ console.warn("Solve captcha action timed out. This can happen for long and complex captchas. Retrying...");
2847
+ continue;
1988
2848
  }
2849
+ throw error;
2850
+ }
2851
+ }
2852
+ if (!result) {
2853
+ throw lastError ?? new Error(`Failed to execute action '${action.type}'. This should not happen. Please report this issue.`);
2854
+ }
2855
+ if (raiseOnFailure && !result.success) {
2856
+ console.error(`\u{1F6A8} Execution failed with message: '${result.message}'`);
2857
+ if (result.exception_detail) {
2858
+ throw new ActionExecutionError(result.exception_detail);
1989
2859
  }
2860
+ const fallbackMessage = String(result.message ?? "").trim() || `Failed to execute action: ${action.type}`;
2861
+ throw ActionExecutionError.fromMessage(fallbackMessage);
2862
+ }
2863
+ return result;
2864
+ }
2865
+ async evaluateJs(code, options = {}) {
2866
+ const raiseOnFailure = options.raiseOnFailure ?? true;
2867
+ const result = await this.execute({ type: "evaluate_js", code }, { raiseOnFailure });
2868
+ if (!raiseOnFailure) {
2869
+ return result;
2870
+ }
2871
+ if (!result.data) {
2872
+ throw new EvaluateJsNoDataError();
2873
+ }
2874
+ return result.data.markdown;
2875
+ }
2876
+ /**
2877
+ * Issue an HTTP request from the page the session is on and return the response.
2878
+ *
2879
+ * The request runs inside the browser through `fetch()`, so it carries the
2880
+ * page's cookies, the session's proxy and the browser's own network
2881
+ * fingerprint. A relative `url` resolves against the current page, which
2882
+ * also makes it same-origin; a cross-origin URL is subject to CORS exactly
2883
+ * as in a browser tab, so `goto` the target origin first. Redirects are
2884
+ * followed and the final URL is on `response.url`. A non-2xx status is
2885
+ * returned, not thrown; `response.raiseForStatus()` throws `PageFetchHTTPError`.
2886
+ *
2887
+ * `json` is serialised as the body with an `application/json` content type,
2888
+ * `data` as a form body when it is an object or verbatim when it is a string.
2889
+ *
2890
+ * @param url - Absolute URL or a path relative to the current page.
2891
+ * @param options - HTTP method, headers, and request body settings.
2892
+ * @returns A response with status, headers, and body readers. Non-2xx responses are not automatically thrown.
2893
+ * @throws ActionExecutionError if browser-side execution fails.
2894
+ * @example
2895
+ * await session.execute({ type: 'goto', url: 'https://en.wikipedia.org/wiki/Main_Page' });
2896
+ * const summary = await (await session.fetch('/api/rest_v1/page/summary/Main_Page')).json();
2897
+ */
2898
+ async fetch(url, options = {}) {
2899
+ const script = buildFetchScript(url, options);
2900
+ return responseFromEvaluated(await this.evaluateJs(script));
2901
+ }
2902
+ /**
2903
+ * Observe the current page: the list of actions that can be taken, a
2904
+ * screenshot and page metadata.
2905
+ *
2906
+ * The `perception_type` defaults to the session option (`fast`). A bare
2907
+ * string argument is accepted as the perception type.
2908
+ *
2909
+ * @param options - Observation settings, including an optional URL, or a perception-mode string.
2910
+ * @returns Structured page analysis, available actions, and page metadata.
2911
+ * @throws NotteAPIError if observation fails.
2912
+ * @example
2913
+ * const obs = await session.observe();
2914
+ * console.log(obs.space.description);
2915
+ * const deep = await session.observe({ perception_type: 'deep', max_nb_actions: 50 });
2916
+ */
2917
+ async observe(options = {}) {
2918
+ const body = typeof options === "string" ? { perception_type: options } : { ...options };
2919
+ if (body.perception_type == null) {
2920
+ body.perception_type = this.defaultPerceptionType;
1990
2921
  }
2922
+ const response = await pageObserve({
2923
+ client: this.client.getClient(),
2924
+ path: { session_id: this.requireSessionId() },
2925
+ body,
2926
+ throwOnError: true
2927
+ });
2928
+ return response.data;
2929
+ }
2930
+ async scrape(options = {}) {
2931
+ const sessionId = this.requireSessionId();
2932
+ const body = await buildScrapeBody(options);
1991
2933
  const response = await pageScrape({
1992
2934
  client: this.client.getClient(),
1993
- path: {
1994
- session_id: this.sessionId
1995
- },
1996
- body: apiOptions
2935
+ path: { session_id: sessionId },
2936
+ body,
2937
+ throwOnError: true
1997
2938
  });
1998
- if (response?.error) {
1999
- throw new Error(`Failed to scrape page: ${JSON.stringify(response.error)}`);
2000
- }
2001
2939
  return processScrapeResponse(response.data, options);
2002
2940
  }
2941
+ // #######################################################################
2942
+ // ############################# Lifecycle ###############################
2943
+ // #######################################################################
2003
2944
  /**
2004
- * Context manager pattern - automatically start and stop session
2005
- * Usage: await session.use(async (session) => { ... })
2945
+ * Context manager pattern: start the session, run the callback and stop the
2946
+ * session, with close reason `error` when the callback throws.
2947
+ *
2948
+ * @param callback - Async work to perform with the started session.
2949
+ * @returns The callback's return value after session cleanup has been attempted.
2950
+ * @throws Error if startup fails, the callback throws, or cleanup fails after a successful callback.
2951
+ * @example
2952
+ * await client.Session().use(async session => {
2953
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2954
+ * });
2006
2955
  */
2007
2956
  async use(callback) {
2008
2957
  await this.start();
@@ -2011,6 +2960,7 @@ var Session = class {
2011
2960
  return await callback(this);
2012
2961
  } catch (error) {
2013
2962
  callbackFailed = true;
2963
+ console.warn(`Session exiting because of exception: ${String(error)}`);
2014
2964
  throw error;
2015
2965
  } finally {
2016
2966
  try {
@@ -2024,8 +2974,13 @@ var Session = class {
2024
2974
  }
2025
2975
  }
2026
2976
  /**
2027
- * Async iterator pattern for session lifecycle management
2028
- * Usage: for await (const session of sessionInstance) { ... }
2977
+ * Async iterator pattern for session lifecycle management.
2978
+ *
2979
+ * ```ts
2980
+ * for await (const session of client.Session()) {
2981
+ * await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
2982
+ * }
2983
+ * ```
2029
2984
  */
2030
2985
  async *[Symbol.asyncIterator]() {
2031
2986
  await this.start();
@@ -2039,12 +2994,12 @@ var Session = class {
2039
2994
 
2040
2995
  // src/agent.ts
2041
2996
  var import_ws = __toESM(require("ws"));
2042
- var import_zod2 = require("zod");
2997
+ var import_zod = require("zod");
2043
2998
  function prepareAgentRequest(data) {
2044
2999
  const apiData = { ...data };
2045
3000
  const rf = data.response_format;
2046
3001
  if (rf && typeof rf.parse === "function" && typeof rf.safeParse === "function") {
2047
- apiData.response_format = import_zod2.z.toJSONSchema(rf);
3002
+ apiData.response_format = import_zod.z.toJSONSchema(rf);
2048
3003
  return { apiData, zodSchema: rf };
2049
3004
  }
2050
3005
  return { apiData, zodSchema: null };
@@ -2150,9 +3105,12 @@ var defaultStepLogger = (stepData, stepCounter, agentId) => {
2150
3105
  console.log("");
2151
3106
  };
2152
3107
  var Agent = class {
3108
+ client;
3109
+ request;
3110
+ response = null;
3111
+ websocket = null;
3112
+ existingAgent;
2153
3113
  constructor(client2, options) {
2154
- this.response = null;
2155
- this.websocket = null;
2156
3114
  this.client = client2;
2157
3115
  if ("agent_id" in options && options.agent_id) {
2158
3116
  if (options.session) {
@@ -2239,22 +3197,29 @@ var Agent = class {
2239
3197
  if (!this.response) {
2240
3198
  throw new Error("Agent not started");
2241
3199
  }
3200
+ const deadline = Date.now() + 3e5;
2242
3201
  try {
2243
- const result = await this.watchLogs(log, updateHandler);
2244
- if (result) {
3202
+ const result = await this.watchLogs(log, updateHandler, deadline);
3203
+ if (result && result.status !== "active") {
2245
3204
  return result;
2246
3205
  }
2247
- await new Promise((resolve) => setTimeout(resolve, 3e3));
2248
- return await this.status();
2249
3206
  } catch (error) {
2250
- console.error(`[Agent] ${this.agentId} failed to complete in time. Try running agent.status() after a few seconds.`);
2251
- return await this.status();
3207
+ console.warn(`[Agent] ${this.agentId} log stream unavailable; polling for completion.`);
3208
+ }
3209
+ while (Date.now() < deadline) {
3210
+ const result = await this.status(AbortSignal.timeout(Math.max(1, deadline - Date.now())));
3211
+ if (result.status !== "active") {
3212
+ updateHandler?.({ type: "completion", data: result, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3213
+ return result;
3214
+ }
3215
+ await new Promise((resolve3) => setTimeout(resolve3, 1e3));
2252
3216
  }
3217
+ throw new Error(`Agent ${this.agentId} did not complete before the polling timeout; retrieve status() later.`);
2253
3218
  }
2254
3219
  /**
2255
3220
  * Watch logs via WebSocket - mirrors Python watch_logs method
2256
3221
  */
2257
- async watchLogs(log = true, updateHandler = null) {
3222
+ async watchLogs(log = true, updateHandler = null, deadline = Date.now() + 3e5) {
2258
3223
  if (this.existingAgent) {
2259
3224
  throw new Error("You cannot call watchLogs() on an agent instantiated from agent id");
2260
3225
  }
@@ -2262,18 +3227,39 @@ var Agent = class {
2262
3227
  throw new Error("Agent not started");
2263
3228
  }
2264
3229
  const config = this.client.getConfig();
2265
- const token = config.apiKey;
3230
+ if (config.baseUrl?.startsWith("/")) return null;
2266
3231
  const agentId = this.response.agent_id;
2267
3232
  const sessionId = this.response.session_id;
2268
- const wsUrl = config.baseUrl?.replace("https://", "wss://").replace("http://", "ws://") + `/agents/${agentId}/debug/logs?token=${token}&session_id=${sessionId}`;
2269
- return new Promise((resolve, reject) => {
3233
+ const debug = await sessionDebugInfo({
3234
+ client: this.client.getClient(),
3235
+ path: { session_id: sessionId },
3236
+ throwOnError: true,
3237
+ signal: AbortSignal.timeout(Math.max(1, deadline - Date.now()))
3238
+ });
3239
+ const wsUrl = new URL(this.client.withDbPreview(debug.data.ws.logs));
3240
+ const token = wsUrl.searchParams.get("token");
3241
+ if (!token || token === config.apiKey || wsUrl.protocol !== "wss:") return null;
3242
+ wsUrl.pathname = `/agents/${encodeURIComponent(agentId)}/debug/logs`;
3243
+ wsUrl.searchParams.set("session_id", sessionId);
3244
+ return new Promise((resolve3, reject) => {
2270
3245
  this.websocket = new import_ws.default(wsUrl);
3246
+ const socket = this.websocket;
3247
+ const timeout = setTimeout(() => finish(null), Math.max(0, deadline - Date.now()));
3248
+ let settled = false;
3249
+ const finish = (result) => {
3250
+ if (settled) return;
3251
+ settled = true;
3252
+ clearTimeout(timeout);
3253
+ if (this.websocket === socket) this.websocket = null;
3254
+ if (socket.readyState !== import_ws.default.CLOSED) socket.terminate();
3255
+ resolve3(result);
3256
+ };
2271
3257
  let counter = 0;
2272
3258
  this.websocket.on("open", () => {
2273
3259
  });
2274
3260
  this.websocket.on("error", (error) => {
2275
3261
  console.error(`Connection error: ${agentId} ${error.message}`);
2276
- resolve(null);
3262
+ finish(null);
2277
3263
  });
2278
3264
  this.websocket.on("message", (data) => {
2279
3265
  try {
@@ -2295,8 +3281,7 @@ var Agent = class {
2295
3281
  } else if (!finalStatus.success) {
2296
3282
  console.error(finalStatus.answer);
2297
3283
  }
2298
- this.disconnectWebSocket();
2299
- resolve(finalStatus);
3284
+ finish(finalStatus);
2300
3285
  return;
2301
3286
  }
2302
3287
  if (log) {
@@ -2316,26 +3301,20 @@ var Agent = class {
2316
3301
  }
2317
3302
  });
2318
3303
  this.websocket.on("close", () => {
2319
- this.websocket = null;
2320
- resolve(null);
3304
+ finish(null);
2321
3305
  });
2322
- setTimeout(() => {
2323
- if (this.websocket && this.websocket.readyState === import_ws.default.OPEN) {
2324
- this.disconnectWebSocket();
2325
- resolve(null);
2326
- }
2327
- }, 3e5);
2328
3306
  });
2329
3307
  }
2330
3308
  /**
2331
3309
  * Get agent status - mirrors Python status method
2332
3310
  */
2333
- async status() {
3311
+ async status(signal) {
2334
3312
  if (!this.response) {
2335
3313
  throw new Error("Agent not started or not accessible");
2336
3314
  }
2337
3315
  const response = await agentStatus({
2338
3316
  client: this.client.getClient(),
3317
+ signal,
2339
3318
  path: {
2340
3319
  agent_id: this.response.agent_id
2341
3320
  }
@@ -2404,24 +3383,126 @@ var Agent = class {
2404
3383
  return this.response.session_id;
2405
3384
  }
2406
3385
  /**
2407
- * Check if agent is running - convenience method
3386
+ * Whether `start()`/`run()` was called on this instance (or it was built from an agent id).
3387
+ * This is a local check and does not contact the API.
2408
3388
  */
2409
- isRunning() {
3389
+ hasStarted() {
2410
3390
  return this.response !== null;
2411
3391
  }
3392
+ /**
3393
+ * Whether the agent is still active on the server. Fetches the current status,
3394
+ * like calling `agent.status()` in Python; an agent that was never started is not active.
3395
+ */
3396
+ async isActive() {
3397
+ if (!this.response) {
3398
+ return false;
3399
+ }
3400
+ const status = await this.status();
3401
+ return status.status === "active";
3402
+ }
3403
+ /**
3404
+ * @deprecated Only reports whether this instance was started; it does not
3405
+ * contact the API. Use `hasStarted()` for that check or `isActive()` for the
3406
+ * live server status.
3407
+ */
3408
+ isRunning() {
3409
+ return this.hasStarted();
3410
+ }
2412
3411
  };
2413
3412
 
2414
3413
  // src/vaults.ts
3414
+ var import_tldts = require("tldts");
3415
+ var CREDENTIAL_FIELDS = ["email", "username", "mfa_secret", "password"];
3416
+ function extractHostname(url) {
3417
+ const trimmed = url.trim();
3418
+ if (trimmed.length === 0) {
3419
+ return "";
3420
+ }
3421
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
3422
+ const candidates = hasScheme ? [trimmed] : [`https://${trimmed}`];
3423
+ for (const candidate of candidates) {
3424
+ try {
3425
+ const { hostname } = new URL(candidate);
3426
+ if (hostname.length > 0 && !hostname.startsWith(".")) {
3427
+ return hostname.toLowerCase();
3428
+ }
3429
+ } catch {
3430
+ }
3431
+ }
3432
+ return "";
3433
+ }
3434
+ function getRootDomain(url) {
3435
+ const hostname = extractHostname(url);
3436
+ if (hostname.length === 0) {
3437
+ return "";
3438
+ }
3439
+ if (hostname.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
3440
+ return hostname;
3441
+ }
3442
+ const domain = (0, import_tldts.getDomain)(hostname, { allowPrivateDomains: false, detectIp: false });
3443
+ if (domain) {
3444
+ return domain.toLowerCase();
3445
+ }
3446
+ return hostname.toLowerCase();
3447
+ }
3448
+ function validateUrl(url) {
3449
+ const domain = getRootDomain(url);
3450
+ if (domain.length === 0) {
3451
+ throw new InvalidRequestError(`Invalid URL: ${url}. Please provide a valid URL with a domain name.`);
3452
+ }
3453
+ return domain;
3454
+ }
3455
+ function isValidMfaSecret(secret) {
3456
+ if (secret.length === 0) {
3457
+ return false;
3458
+ }
3459
+ let padded = secret.toUpperCase();
3460
+ if (padded.length % 8 !== 0) {
3461
+ padded += "=".repeat(8 - padded.length % 8);
3462
+ }
3463
+ let end = padded.length;
3464
+ while (end > 0 && padded[end - 1] === "=") {
3465
+ end -= 1;
3466
+ }
3467
+ const stripped = padded.slice(0, end);
3468
+ const padding = padded.length - stripped.length;
3469
+ return /^[A-Z2-7]*$/.test(stripped) && [0, 1, 3, 4, 6].includes(padding);
3470
+ }
3471
+ function validateMfaSecret(secret) {
3472
+ if (!isValidMfaSecret(secret)) {
3473
+ throw new InvalidRequestError("Invalid MFA secret code: did you try to store an OTP instead of a secret?");
3474
+ }
3475
+ }
3476
+ function validateCredentials(credentials) {
3477
+ const hasUsername = credentials.username !== void 0 && credentials.username !== null;
3478
+ const hasEmail = credentials.email !== void 0 && credentials.email !== null;
3479
+ if (hasUsername && hasEmail) {
3480
+ throw new InvalidRequestError("Can only set either username or email");
3481
+ }
3482
+ if (!hasUsername && !hasEmail) {
3483
+ throw new InvalidRequestError("Need to have either username or email set");
3484
+ }
3485
+ if (credentials.mfa_secret !== void 0 && credentials.mfa_secret !== null) {
3486
+ validateMfaSecret(credentials.mfa_secret);
3487
+ }
3488
+ return credentials;
3489
+ }
2415
3490
  var NotteVault = class {
3491
+ client;
3492
+ _vaultId = null;
3493
+ initPromise = null;
3494
+ createData = null;
2416
3495
  constructor(client2, options = {}) {
2417
- this._vaultId = null;
2418
- this.initPromise = null;
2419
- this.createData = null;
2420
3496
  this.client = client2;
2421
- if (options && "vault_id" in options && options.vault_id) {
3497
+ if (options && "vault_id" in options && options.vault_id !== void 0) {
3498
+ if (options.vault_id.length === 0) {
3499
+ throw new InvalidRequestError("Vault ID cannot be empty");
3500
+ }
2422
3501
  this.initPromise = this.initExistingVault(options.vault_id);
3502
+ this.initPromise.catch(() => void 0);
2423
3503
  } else {
2424
- this.createData = options || {};
3504
+ const { vault_id: _ignored, ...createData } = options ?? {};
3505
+ this.createData = createData;
2425
3506
  }
2426
3507
  }
2427
3508
  async initExistingVault(vaultId) {
@@ -2432,29 +3513,20 @@ var NotteVault = class {
2432
3513
  async initNewVault(createData) {
2433
3514
  const response = await vaultCreate({
2434
3515
  client: this.client.getClient(),
2435
- body: createData
3516
+ body: createData,
3517
+ throwOnError: true
2436
3518
  });
2437
- if (response?.error) {
2438
- throw new Error(`Failed to create vault: ${formatError(response.error)}`);
2439
- }
2440
3519
  const vault = response.data;
2441
3520
  console.warn(`[Vault] ${vault.vault_id} created since no vault id was provided. Please store this to retrieve it later.`);
2442
3521
  this._vaultId = vault.vault_id;
2443
3522
  return this._vaultId;
2444
3523
  }
2445
3524
  async verifyVaultExists(vaultId) {
2446
- if (vaultId.length === 0) {
2447
- throw new Error("Vault ID cannot be empty");
2448
- }
2449
- const response = await vaultCredentialsList({
3525
+ await vaultCredentialsList({
2450
3526
  client: this.client.getClient(),
2451
- path: {
2452
- vault_id: vaultId
2453
- }
3527
+ path: { vault_id: vaultId },
3528
+ throwOnError: true
2454
3529
  });
2455
- if (response?.error) {
2456
- throw new Error(`Failed to verify vault exists: ${formatError(response.error)}`);
2457
- }
2458
3530
  }
2459
3531
  async ensureInitialized() {
2460
3532
  if (this._vaultId) {
@@ -2469,257 +3541,215 @@ var NotteVault = class {
2469
3541
  this.initPromise = this.initNewVault(createData);
2470
3542
  return await this.initPromise;
2471
3543
  }
2472
- throw new Error("Vault not initialized");
3544
+ throw new InvalidRequestError("Vault not initialized");
2473
3545
  }
3546
+ /** ID of the remote vault. Throws until the first API operation has created or verified it. */
2474
3547
  get vaultId() {
2475
3548
  if (!this._vaultId) {
2476
- throw new Error("Vault not initialized. Use await on vault operations first.");
3549
+ throw new InvalidRequestError("Vault not initialized. Await a vault operation first.");
2477
3550
  }
2478
3551
  return this._vaultId;
2479
3552
  }
2480
3553
  /**
2481
- * Start the vault - no-op for compatibility with Python interface
3554
+ * Start the vault. No-op kept for parity with Python's `SyncResource` interface.
2482
3555
  */
2483
3556
  start() {
2484
3557
  }
2485
3558
  /**
2486
- * Stop the vault - deletes it and all credentials
3559
+ * Stop the vault: deletes it together with every credential it holds.
3560
+ *
3561
+ * ```ts
3562
+ * await vault.stop();
3563
+ * ```
2487
3564
  */
2488
3565
  async stop() {
2489
3566
  const vaultId = await this.ensureInitialized();
2490
- console.log(`[Vault] ${vaultId} deleted. All credentials have been deleted.`);
3567
+ console.info(`[Vault] ${vaultId} deleted. All credentials have been deleted.`);
2491
3568
  await this.delete();
2492
3569
  }
2493
3570
  /**
2494
- * Add or update credentials for a URL
3571
+ * Add or update the credentials stored for a website. The URL is reduced to
3572
+ * its root domain (`https://github.com/login` → `github.com`) before it is
3573
+ * sent, like Python's `AddCredentialsRequest`.
3574
+ *
3575
+ * ```ts
3576
+ * await vault.addCredentials('https://github.com/', {
3577
+ * email: 'me@example.org',
3578
+ * password: 'secret', // pragma: allowlist secret
3579
+ * mfa_secret: 'JBSWY3DPEHPK3PXP', // pragma: allowlist secret
3580
+ * });
3581
+ * ```
2495
3582
  */
2496
3583
  async addCredentials(url, credentials) {
2497
- if (credentials.mfa_secret) {
2498
- this.validateMfaSecret(credentials.mfa_secret);
2499
- }
3584
+ const domain = validateUrl(url);
3585
+ validateCredentials(credentials);
2500
3586
  const vaultId = await this.ensureInitialized();
2501
- const response = await vaultCredentialsAdd({
3587
+ await vaultCredentialsAdd({
2502
3588
  client: this.client.getClient(),
2503
- path: {
2504
- vault_id: vaultId
2505
- },
2506
- body: { url, credentials }
3589
+ path: { vault_id: vaultId },
3590
+ body: { url: domain, credentials },
3591
+ throwOnError: true
2507
3592
  });
2508
- if (response?.error) {
2509
- throw new Error(`Failed to add credentials: ${formatError(response.error)}`);
2510
- }
2511
3593
  }
2512
3594
  /**
2513
- * Get credentials for a URL
3595
+ * Get the credentials stored for a website. Rejects with a `NotteAPIError`
3596
+ * when the vault holds no credentials for that domain, like Python's
3597
+ * `get_credentials`.
3598
+ *
3599
+ * ```ts
3600
+ * const { email, password } = await vault.getCredentials('https://github.com/login');
3601
+ * ```
2514
3602
  */
2515
3603
  async getCredentials(url) {
2516
- try {
2517
- const vaultId = await this.ensureInitialized();
2518
- const response = await vaultCredentialsGet({
2519
- client: this.client.getClient(),
2520
- path: {
2521
- vault_id: vaultId
2522
- },
2523
- query: { url }
2524
- });
2525
- if (response?.error) {
2526
- return null;
2527
- }
2528
- const credentialsResponse = response.data;
2529
- return credentialsResponse.credentials;
2530
- } catch (error) {
2531
- return null;
2532
- }
2533
- }
2534
- /**
2535
- * Delete credentials for a URL
2536
- */
2537
- async deleteCredentials(url) {
3604
+ const domain = validateUrl(url);
2538
3605
  const vaultId = await this.ensureInitialized();
2539
- const response = await vaultCredentialsDelete({
3606
+ const response = await vaultCredentialsGet({
2540
3607
  client: this.client.getClient(),
2541
- path: {
2542
- vault_id: vaultId
2543
- },
2544
- query: { url }
3608
+ path: { vault_id: vaultId },
3609
+ query: { url: domain },
3610
+ throwOnError: true
2545
3611
  });
2546
- if (response?.error) {
2547
- throw new Error(`Failed to delete credentials: ${formatError(response.error)}`);
2548
- }
3612
+ return response.data.credentials;
2549
3613
  }
2550
3614
  /**
2551
- * Set credit card information
3615
+ * Check whether the vault holds credentials for a website, the counterpart
3616
+ * of `BaseVault.has_credential`. Never rejects for a missing credential.
3617
+ *
3618
+ * ```ts
3619
+ * if (!(await vault.hasCredential('https://github.com/'))) {
3620
+ * await vault.addCredentialsFromEnv('https://github.com/');
3621
+ * }
3622
+ * ```
2552
3623
  */
2553
- async setCreditCard(creditCard) {
2554
- const vaultId = await this.ensureInitialized();
2555
- const response = await vaultCreditCardSet({
2556
- client: this.client.getClient(),
2557
- path: {
2558
- vault_id: vaultId
2559
- },
2560
- body: { credit_card: creditCard }
2561
- });
2562
- if (response?.error) {
2563
- throw new Error(`Failed to set credit card: ${formatError(response.error)}`);
2564
- }
3624
+ async hasCredential(url) {
3625
+ const domain = getRootDomain(url);
3626
+ const credentials = await this.listCredentials();
3627
+ return credentials.some((credential) => credential.url === url || credential.url === domain);
2565
3628
  }
2566
3629
  /**
2567
- * Get credit card information
3630
+ * Delete the credentials stored for a website.
3631
+ *
3632
+ * ```ts
3633
+ * await vault.deleteCredentials('https://github.com/');
3634
+ * ```
2568
3635
  */
2569
- async getCreditCard() {
3636
+ async deleteCredentials(url) {
3637
+ const domain = validateUrl(url);
2570
3638
  const vaultId = await this.ensureInitialized();
2571
- const response = await vaultCreditCardGet({
3639
+ await vaultCredentialsDelete({
2572
3640
  client: this.client.getClient(),
2573
- path: {
2574
- vault_id: vaultId
2575
- }
3641
+ path: { vault_id: vaultId },
3642
+ query: { url: domain },
3643
+ throwOnError: true
2576
3644
  });
2577
- if (response?.error) {
2578
- throw new Error(`Failed to get credit card: ${formatError(response.error)}`);
2579
- }
2580
- const creditCardResponse = response.data;
2581
- return creditCardResponse.credit_card;
2582
3645
  }
2583
3646
  /**
2584
- * List all credentials in the vault
3647
+ * List the websites the vault holds credentials for. Passwords are never returned.
3648
+ *
3649
+ * ```ts
3650
+ * const credentials = await vault.listCredentials();
3651
+ * console.log(credentials.map(c => c.url));
3652
+ * ```
2585
3653
  */
2586
3654
  async listCredentials() {
2587
3655
  const vaultId = await this.ensureInitialized();
2588
3656
  const response = await vaultCredentialsList({
2589
3657
  client: this.client.getClient(),
2590
- path: {
2591
- vault_id: vaultId
2592
- }
2593
- });
2594
- if (response?.error) {
2595
- throw new Error(`Failed to list credentials: ${formatError(response.error)}`);
2596
- }
2597
- const credentialsResponse = response.data;
2598
- return credentialsResponse.credentials;
2599
- }
2600
- /**
2601
- * Delete credit card information
2602
- */
2603
- async deleteCreditCard() {
2604
- const vaultId = await this.ensureInitialized();
2605
- const response = await vaultCreditCardDelete({
2606
- client: this.client.getClient(),
2607
- path: {
2608
- vault_id: vaultId
2609
- }
3658
+ path: { vault_id: vaultId },
3659
+ throwOnError: true
2610
3660
  });
2611
- if (response?.error) {
2612
- throw new Error(`Failed to delete credit card: ${formatError(response.error)}`);
2613
- }
3661
+ return response.data.credentials;
2614
3662
  }
2615
3663
  /**
2616
- * Delete the entire vault
3664
+ * Delete the vault and every credential it holds.
2617
3665
  */
2618
3666
  async delete() {
2619
3667
  const vaultId = await this.ensureInitialized();
2620
- const response = await vaultDelete({
3668
+ await vaultDelete({
2621
3669
  client: this.client.getClient(),
2622
- path: {
2623
- vault_id: vaultId
2624
- }
3670
+ path: { vault_id: vaultId },
3671
+ throwOnError: true
2625
3672
  });
2626
- if (response?.error) {
2627
- throw new Error(`Failed to delete vault: ${formatError(response.error)}`);
2628
- }
2629
3673
  }
2630
3674
  /**
2631
- * Add credentials from environment variables
2632
- * Mirrors Python's add_credentials_from_env method
3675
+ * Add credentials read from environment variables, the counterpart of
3676
+ * `BaseVault.add_credentials_from_env`. For `https://github.com/` the
3677
+ * variables are `GITHUB_COM_EMAIL`, `GITHUB_COM_USERNAME`,
3678
+ * `GITHUB_COM_MFA_SECRET` and `GITHUB_COM_PASSWORD`.
3679
+ *
3680
+ * ```ts
3681
+ * process.env.GITHUB_COM_EMAIL = 'me@example.org';
3682
+ * process.env.GITHUB_COM_PASSWORD = 'secret'; // pragma: allowlist secret
3683
+ * await vault.addCredentialsFromEnv('https://github.com/');
3684
+ * ```
2633
3685
  */
2634
3686
  async addCredentialsFromEnv(url) {
2635
- const domain = this.extractDomainFromUrl(url);
2636
- const envPrefix = domain.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
2637
- const email = process.env[`${envPrefix}_EMAIL`] || process.env[`${envPrefix}_USERNAME`];
2638
- const password = process.env[`${envPrefix}_PASSWORD`];
2639
- if (!email || !password) {
2640
- throw new Error(`Environment variables ${envPrefix}_EMAIL/USERNAME and ${envPrefix}_PASSWORD are required`);
2641
- }
2642
- const credentials = { email, password };
2643
- const mfaSecret = process.env[`${envPrefix}_MFA_SECRET`];
2644
- if (mfaSecret) {
2645
- this.validateMfaSecret(mfaSecret);
2646
- credentials.mfa_secret = mfaSecret;
3687
+ const rootDomain = validateUrl(url);
3688
+ const prefix = rootDomain.replace(/\./g, "_").toUpperCase();
3689
+ const credentials = {};
3690
+ const variableNames = [];
3691
+ for (const field of CREDENTIAL_FIELDS) {
3692
+ const variable = `${prefix}_${field.toUpperCase()}`;
3693
+ variableNames.push(variable);
3694
+ const value = process.env[variable];
3695
+ if (value !== void 0) {
3696
+ credentials[field] = value;
3697
+ }
3698
+ }
3699
+ if (Object.keys(credentials).length === 0) {
3700
+ throw new InvalidRequestError(
3701
+ `No credentials found in the environment for ${url}. Please set the following variables: ${variableNames.join(", ")}`
3702
+ );
2647
3703
  }
2648
3704
  await this.addCredentials(url, credentials);
2649
3705
  }
2650
3706
  /**
2651
- * Extract domain from URL for environment variable naming
3707
+ * Generate a secure random password, the counterpart of Python's
3708
+ * `generate_password`. The result always contains a lowercase letter, an
3709
+ * uppercase letter and a digit, plus a special character unless
3710
+ * `includeSpecialChars` is false.
3711
+ *
3712
+ * ```ts
3713
+ * const password = vault.generatePassword(24);
3714
+ * ```
2652
3715
  */
2653
- extractDomainFromUrl(url) {
2654
- try {
2655
- const urlObj = new URL(url);
2656
- return urlObj.hostname.replace(/^www\./, "");
2657
- } catch {
2658
- const match = url.match(/(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
2659
- return match ? match[1] : url;
2660
- }
2661
- }
2662
- /**
2663
- * Validate MFA secret format
2664
- * Mirrors Python's MFA secret validation
2665
- */
2666
- validateMfaSecret(mfaSecret) {
2667
- if (/^\d+$/.test(mfaSecret)) {
2668
- throw new Error("MFA secret cannot be all numbers. Please provide a valid base32 secret.");
2669
- }
2670
- if (mfaSecret.length < 16) {
2671
- throw new Error("MFA secret must be at least 16 characters long");
2672
- }
2673
- }
2674
- /**
2675
- * Generate a secure random password
2676
- * Mirrors Python's generate_password method
2677
- */
2678
- generatePassword(length = 20, includeSpecialChars = true) {
2679
- const minRequiredLength = includeSpecialChars ? 4 : 3;
2680
- if (length < minRequiredLength) {
2681
- throw new Error(`Password length must be at least ${minRequiredLength} characters`);
3716
+ generatePassword(length = 20, includeSpecialChars = true) {
3717
+ const minRequiredLength = includeSpecialChars ? 4 : 3;
3718
+ if (length < minRequiredLength) {
3719
+ throw new InvalidRequestError(`Password length must be at least ${minRequiredLength} characters`);
2682
3720
  }
2683
3721
  const lowercase = "abcdefghijklmnopqrstuvwxyz";
2684
3722
  const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
2685
3723
  const digits = "0123456789";
2686
3724
  const specialChars = "!@#$%^&*()_+-=[]|;:,.<>?";
2687
- const crypto = globalThis.crypto || require("crypto");
2688
- const array = new Uint8Array(length);
2689
- crypto.getRandomValues(array);
2690
- const allowedChars = includeSpecialChars ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2691
- let password = Array.from(array, (byte) => allowedChars[byte % allowedChars.length]).join("").slice(0, length);
2692
- const passwordArray = password.split("");
2693
- if (!includeSpecialChars) {
2694
- for (let i = 0; i < passwordArray.length; i++) {
2695
- if (specialChars.includes(passwordArray[i])) {
2696
- const alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2697
- passwordArray[i] = alphanumeric[Math.floor(Math.random() * alphanumeric.length)];
3725
+ const alphanumeric = uppercase + lowercase + digits;
3726
+ const allowedChars = includeSpecialChars ? `${alphanumeric}-_` : alphanumeric;
3727
+ const randomIndex = (size) => {
3728
+ const limit = Math.floor(4294967296 / size) * size;
3729
+ const buffer = new Uint32Array(1);
3730
+ for (; ; ) {
3731
+ const [value] = globalThis.crypto.getRandomValues(buffer);
3732
+ if (value < limit) {
3733
+ return value % size;
2698
3734
  }
2699
3735
  }
2700
- }
3736
+ };
3737
+ const passwordArray = Array.from({ length }, () => allowedChars[randomIndex(allowedChars.length)]);
3738
+ const pick = (chars) => chars[randomIndex(chars.length)];
2701
3739
  let maxIterations = 10;
2702
3740
  while (maxIterations-- > 0) {
2703
3741
  let allRequirementsMet = true;
2704
- if (!passwordArray.some((c) => lowercase.includes(c))) {
2705
- const index = Math.floor(Math.random() * passwordArray.length);
2706
- passwordArray[index] = lowercase[Math.floor(Math.random() * lowercase.length)];
2707
- allRequirementsMet = false;
2708
- }
2709
- if (!passwordArray.some((c) => uppercase.includes(c))) {
2710
- const index = Math.floor(Math.random() * passwordArray.length);
2711
- passwordArray[index] = uppercase[Math.floor(Math.random() * uppercase.length)];
2712
- allRequirementsMet = false;
2713
- }
2714
- if (!passwordArray.some((c) => digits.includes(c))) {
2715
- const index = Math.floor(Math.random() * passwordArray.length);
2716
- passwordArray[index] = digits[Math.floor(Math.random() * digits.length)];
2717
- allRequirementsMet = false;
2718
- }
2719
- if (includeSpecialChars && !passwordArray.some((c) => specialChars.includes(c))) {
2720
- const index = Math.floor(Math.random() * passwordArray.length);
2721
- passwordArray[index] = specialChars[Math.floor(Math.random() * specialChars.length)];
2722
- allRequirementsMet = false;
3742
+ const requirements = [
3743
+ [true, lowercase],
3744
+ [true, uppercase],
3745
+ [true, digits],
3746
+ [includeSpecialChars, specialChars]
3747
+ ];
3748
+ for (const [required, chars] of requirements) {
3749
+ if (required && !passwordArray.some((c) => chars.includes(c))) {
3750
+ passwordArray[randomIndex(passwordArray.length)] = pick(chars);
3751
+ allRequirementsMet = false;
3752
+ }
2723
3753
  }
2724
3754
  if (allRequirementsMet) {
2725
3755
  break;
@@ -2731,10 +3761,15 @@ var NotteVault = class {
2731
3761
 
2732
3762
  // src/personas.ts
2733
3763
  var NottePersona = class {
3764
+ client;
3765
+ _initRequest;
3766
+ response = null;
3767
+ initPromise = null;
3768
+ _vault;
3769
+ ownsPersona;
2734
3770
  constructor(client2, options = {}) {
2735
- this.response = null;
2736
- this.initPromise = null;
2737
3771
  this.client = client2;
3772
+ this.ownsPersona = !("persona_id" in options && options.persona_id);
2738
3773
  if (options && "persona_id" in options && options.persona_id) {
2739
3774
  this.initPromise = this.initExistingPersona(options.persona_id);
2740
3775
  this._initRequest = {};
@@ -2835,9 +3870,7 @@ var NottePersona = class {
2835
3870
  * Start the persona - ensures initialization
2836
3871
  */
2837
3872
  async start() {
2838
- if (this.response === null) {
2839
- await this.create();
2840
- }
3873
+ await this.ensureInitialized();
2841
3874
  }
2842
3875
  /**
2843
3876
  * Stop the persona - deletes it
@@ -2863,14 +3896,7 @@ var NottePersona = class {
2863
3896
  if (this.response !== null) {
2864
3897
  throw new Error(`Persona ${this.personaId} already initialized`);
2865
3898
  }
2866
- const response = await personaCreate({
2867
- client: this.client.getClient(),
2868
- body: this._initRequest
2869
- });
2870
- if (response?.error) {
2871
- throw new Error(`Failed to create persona: ${formatError(response.error)}`);
2872
- }
2873
- this.response = response.data;
3899
+ await this.ensureInitialized();
2874
3900
  }
2875
3901
  /**
2876
3902
  * Delete the persona from the notte console
@@ -2946,13 +3972,15 @@ var NottePersona = class {
2946
3972
  return this.response;
2947
3973
  }
2948
3974
  /**
2949
- * Context manager for automatic cleanup
3975
+ * Run a callback and automatically delete a persona created by this instance.
3976
+ * Personas loaded by an existing ID are retained after the callback finishes.
2950
3977
  */
2951
3978
  async use(callback) {
3979
+ await this.ensureInitialized();
2952
3980
  try {
2953
3981
  return await callback(this);
2954
3982
  } finally {
2955
- if (this._initRequest && !("persona_id" in this._initRequest)) {
3983
+ if (this.ownsPersona) {
2956
3984
  try {
2957
3985
  await this.delete();
2958
3986
  } catch (error) {
@@ -2964,7 +3992,8 @@ var NottePersona = class {
2964
3992
  };
2965
3993
 
2966
3994
  // src/functions.ts
2967
- var import_promises = require("fs/promises");
3995
+ var import_promises3 = require("fs/promises");
3996
+ var import_node_path3 = require("path");
2968
3997
 
2969
3998
  // src/encryption.ts
2970
3999
  var import_node_crypto = require("crypto");
@@ -2976,6 +4005,7 @@ var FERNET_IV_LENGTH = 16;
2976
4005
  var FERNET_HMAC_LENGTH = 32;
2977
4006
  var FERNET_HEADER_LENGTH = 1 + 8 + FERNET_IV_LENGTH;
2978
4007
  var Encryption = class {
4008
+ rootKey;
2979
4009
  constructor(rootKey) {
2980
4010
  if (!rootKey) {
2981
4011
  throw new Error("Root key cannot be empty");
@@ -3017,141 +4047,385 @@ function decryptFernet(token, key) {
3017
4047
 
3018
4048
  // src/functions.ts
3019
4049
  var DOWNLOAD_TIMEOUT_MS = 3e4;
4050
+ var FUNCTION_RUN_TIMEOUT_MS = 5 * 60 * 1e3;
4051
+ var FUNCTION_RUN_ENDPOINTS = {
4052
+ /** `POST` creates a run record without executing it (`CreateFunctionRunRequest`). */
4053
+ create: "/functions/{function_id}/runs/create",
4054
+ /** `POST` executes a previously created run (`StartFunctionRunRequest`). */
4055
+ start: "/functions/{function_id}/runs/{run_id}"
4056
+ };
4057
+ var RUN_API_KEY_HEADER = "x-notte-api-key";
4058
+ var FUNCTION_RUNTIMES = ["standard", "extended"];
4059
+ var FUNCTION_RUN_STATUSES = ["closed", "active", "failed"];
3020
4060
  var NotteFunction = class {
4061
+ client;
4062
+ _functionId = null;
4063
+ initPromise = null;
4064
+ createData = null;
4065
+ decryptionKey;
3021
4066
  constructor(client2, options) {
3022
4067
  this.client = client2;
3023
- this.functionId = options.function_id;
3024
4068
  this.decryptionKey = options.decryption_key;
4069
+ if (options.function_id !== void 0) {
4070
+ if (options.function_id.length === 0) {
4071
+ throw new InvalidRequestError("function_id cannot be empty");
4072
+ }
4073
+ this._functionId = options.function_id;
4074
+ } else if (options.path !== void 0) {
4075
+ assertPythonPath(options.path);
4076
+ this.createData = options;
4077
+ } else {
4078
+ throw new InvalidRequestError("Either 'function_id' or 'path' must be provided");
4079
+ }
4080
+ }
4081
+ /** The function ID. Throws before a `path`-constructed function has been uploaded. */
4082
+ get functionId() {
4083
+ if (this._functionId === null) {
4084
+ throw new InvalidRequestError(
4085
+ "Function not initialized. Await a function operation (for example `await fn.get()`) before reading its ID."
4086
+ );
4087
+ }
4088
+ return this._functionId;
4089
+ }
4090
+ /** Alias of the `functionId` accessor. */
4091
+ getFunctionId() {
4092
+ return this.functionId;
4093
+ }
4094
+ async ensureInitialized() {
4095
+ if (this._functionId !== null) {
4096
+ return this._functionId;
4097
+ }
4098
+ if (this.initPromise === null) {
4099
+ const createData = this.createData;
4100
+ this.createData = null;
4101
+ if (createData === null) {
4102
+ throw new InvalidRequestError("Function not initialized");
4103
+ }
4104
+ this.initPromise = this.createFunction(createData);
4105
+ }
4106
+ return this.initPromise;
4107
+ }
4108
+ async createFunction(data) {
4109
+ const file = await readPythonFile(data.path);
4110
+ const response = await functionCreate({
4111
+ client: this.client.getClient(),
4112
+ throwOnError: true,
4113
+ body: { file, name: data.name, description: data.description, shared: data.shared }
4114
+ });
4115
+ this._functionId = response.data.function_id;
4116
+ console.info(`[Function] ${this._functionId} created successfully.`);
4117
+ return this._functionId;
3025
4118
  }
3026
4119
  /**
3027
- * Get metadata for a specific function run
4120
+ * Get the function metadata together with its download URL, the
4121
+ * counterpart of `client.functions.get()` in Python.
4122
+ *
4123
+ * @param options - Optional version selector; omitted means the latest version.
4124
+ * @returns Function metadata, version history, and the download link returned by the API.
4125
+ * @throws NotteAPIError if the function cannot be retrieved.
4126
+ * @example
4127
+ * const { latest_version, versions } = await fn.get();
3028
4128
  */
3029
- async retrieve(functionRunId) {
3030
- try {
3031
- const response = await functionRunGetMetadata({
3032
- client: this.client.getClient(),
3033
- path: {
3034
- function_id: this.functionId,
3035
- run_id: functionRunId
3036
- }
3037
- });
3038
- if (response?.error) {
3039
- throw new Error(`Failed to get function run metadata: ${formatError(response.error)}`);
3040
- }
3041
- return response.data;
3042
- } catch (error) {
3043
- if (error instanceof Error && error.message.startsWith("Failed to get function run metadata:")) {
3044
- throw error;
3045
- }
3046
- throw new Error(`Failed to get function run metadata: ${error instanceof Error ? error.message : String(error)}`);
4129
+ async get(options = {}) {
4130
+ const functionId = await this.ensureInitialized();
4131
+ const response = await functionDownloadUrl({
4132
+ client: this.client.getClient(),
4133
+ throwOnError: true,
4134
+ path: { function_id: functionId },
4135
+ query: options.version !== void 0 ? { version: options.version } : {}
4136
+ });
4137
+ return response.data;
4138
+ }
4139
+ /**
4140
+ * Upload a new version of the code, mirroring `function.update(path=...)`.
4141
+ *
4142
+ * @param options - Local Python file path, optional version, and code-access settings.
4143
+ * @returns Updated function metadata, including its latest version.
4144
+ * @throws InvalidRequestError if the path is missing or does not end in `.py`.
4145
+ * @throws NotteAPIError if the upload is rejected.
4146
+ * @example
4147
+ * const updated = await fn.update({ path: './scraper.py' });
4148
+ * console.log(updated.latest_version);
4149
+ */
4150
+ async update(options) {
4151
+ if (options.path === void 0) {
4152
+ throw new InvalidRequestError("'path' must be provided");
3047
4153
  }
4154
+ const functionId = await this.ensureInitialized();
4155
+ const file = await readPythonFile(options.path);
4156
+ const response = await functionUpdate({
4157
+ client: this.client.getClient(),
4158
+ throwOnError: true,
4159
+ path: { function_id: functionId },
4160
+ query: {
4161
+ restricted: options.restricted ?? true,
4162
+ ...options.version !== void 0 ? { version: options.version } : {}
4163
+ },
4164
+ body: { file }
4165
+ });
4166
+ console.info(`[Function] ${functionId} updated successfully to version ${response.data.latest_version}.`);
4167
+ return response.data;
4168
+ }
4169
+ /**
4170
+ * Update the function metadata (name, description, default runtime, ...).
4171
+ *
4172
+ * @param options - Metadata fields to update, such as name, description, or default runtime.
4173
+ * @returns Updated function metadata.
4174
+ * @throws NotteAPIError if the metadata update is rejected.
4175
+ * @example
4176
+ * await fn.updateMetadata({ name: 'Price monitor', default_runtime: 'extended' });
4177
+ */
4178
+ async updateMetadata(options) {
4179
+ const functionId = await this.ensureInitialized();
4180
+ const response = await functionMetadataUpdate({
4181
+ client: this.client.getClient(),
4182
+ throwOnError: true,
4183
+ path: { function_id: functionId },
4184
+ body: options
4185
+ });
4186
+ return response.data;
4187
+ }
4188
+ /**
4189
+ * Delete the function from the Notte console.
4190
+ *
4191
+ * @returns The API's function-deletion confirmation.
4192
+ * @throws NotteAPIError if the function cannot be deleted.
4193
+ * @example
4194
+ * await fn.delete();
4195
+ */
4196
+ async delete() {
4197
+ const functionId = await this.ensureInitialized();
4198
+ const response = await functionDelete({
4199
+ client: this.client.getClient(),
4200
+ throwOnError: true,
4201
+ path: { function_id: functionId }
4202
+ });
4203
+ console.info(`[Function] ${functionId} deleted successfully.`);
4204
+ return response.data;
4205
+ }
4206
+ /**
4207
+ * Schedule the function to run on a cron expression.
4208
+ *
4209
+ * @param options - Cron expression and optional variables passed to each scheduled execution.
4210
+ * @returns The saved schedule configuration.
4211
+ * @throws NotteAPIError if the schedule cannot be saved.
4212
+ * @example
4213
+ * await fn.setSchedule({ cron: '0 9 * * *', variables: { url: 'https://example.com' } });
4214
+ */
4215
+ async setSchedule(options) {
4216
+ const functionId = await this.ensureInitialized();
4217
+ const response = await functionScheduleSet({
4218
+ client: this.client.getClient(),
4219
+ throwOnError: true,
4220
+ path: { function_id: functionId },
4221
+ body: { cron: options.cron, variables: options.variables ?? null }
4222
+ });
4223
+ return response.data;
4224
+ }
4225
+ /**
4226
+ * Remove the function schedule.
4227
+ *
4228
+ * @returns The API's schedule-deletion confirmation.
4229
+ * @throws NotteAPIError if schedule deletion fails.
4230
+ * @example
4231
+ * await fn.deleteSchedule();
4232
+ */
4233
+ async deleteSchedule() {
4234
+ const functionId = await this.ensureInitialized();
4235
+ const response = await functionScheduleDelete({
4236
+ client: this.client.getClient(),
4237
+ throwOnError: true,
4238
+ path: { function_id: functionId }
4239
+ });
4240
+ return response.data;
4241
+ }
4242
+ /**
4243
+ * Roll the function back to a previous version.
4244
+ *
4245
+ * @param options - Target version and optional code-access restriction, which defaults to true.
4246
+ * @returns Updated function metadata after rollback.
4247
+ * @throws NotteAPIError if the requested version cannot be restored.
4248
+ * @example
4249
+ * const { versions } = await fn.get();
4250
+ * await fn.rollback({ version: versions[0] });
4251
+ */
4252
+ async rollback(options) {
4253
+ const functionId = await this.ensureInitialized();
4254
+ const response = await functionRollback({
4255
+ client: this.client.getClient(),
4256
+ throwOnError: true,
4257
+ path: { function_id: functionId },
4258
+ query: { restricted: options.restricted ?? true },
4259
+ body: { version: options.version }
4260
+ });
4261
+ return response.data;
4262
+ }
4263
+ /**
4264
+ * Get metadata for a specific function run.
4265
+ *
4266
+ * @param functionRunId - ID of a run belonging to this function.
4267
+ * @returns The run's current status, logs, and result metadata. This does not wait for completion.
4268
+ * @throws NotteAPIError if the run cannot be retrieved.
4269
+ * @example
4270
+ * const run = await fn.getRun(result.function_run_id);
4271
+ * console.log(run.status, run.logs);
4272
+ */
4273
+ async getRun(functionRunId) {
4274
+ const functionId = await this.ensureInitialized();
4275
+ const response = await functionRunGetMetadata({
4276
+ client: this.client.getClient(),
4277
+ throwOnError: true,
4278
+ path: { function_id: functionId, run_id: functionRunId }
4279
+ });
4280
+ return response.data;
4281
+ }
4282
+ /**
4283
+ * Compatibility alias for getRun().
4284
+ * @deprecated Use getRun() instead. Retained for backwards compatibility.
4285
+ */
4286
+ async retrieve(functionRunId) {
4287
+ return this.getRun(functionRunId);
4288
+ }
4289
+ /**
4290
+ * Create a cloud run record without starting execution, like
4291
+ * `function.create_run()` in Python. Use this only when you need the ID
4292
+ * before execution, then pass it to `run(variables, { functionRunId })`.
4293
+ * `run()` otherwise creates a new run on its own.
4294
+ *
4295
+ * @param options - Run-record settings; `local` defaults to false. Node execution remains cloud-only.
4296
+ * @returns A created run record and its ID. No function code has executed yet.
4297
+ * @throws NotteAPIError if run creation is rejected.
4298
+ * @example
4299
+ * const created = await fn.createRun();
4300
+ * const result = await fn.run({ url: 'https://example.com' }, { functionRunId: created.function_run_id });
4301
+ */
4302
+ async createRun(options = {}) {
4303
+ const functionId = await this.ensureInitialized();
4304
+ return this.postRunEndpoint({
4305
+ url: FUNCTION_RUN_ENDPOINTS.create,
4306
+ path: { function_id: functionId },
4307
+ body: { local: options.local ?? false }
4308
+ });
3048
4309
  }
3049
4310
  /**
3050
4311
  * List the runs of this function.
3051
4312
  *
3052
- * `only_active` is sent as `false` unless the caller says otherwise, and that
3053
- * is deliberate rather than redundant. `GET /functions/{function_id}/runs`
3054
- * now defaults it to `false` server-side as well, but it used to default to
3055
- * `true`: the request model was a bare subclass of the session one, where
3056
- * "only active" reads sensibly as "list my running sessions" and, for "list
3057
- * this function's runs", was a trap. Hitting the endpoint with no query
3058
- * parameters returned only the runs executing right now - an empty list for
3059
- * any function that had finished. That cost us a bug in the anything-api
3060
- * console, where a function with 15 recorded runs displayed "Nothing has run
3061
- * this endpoint yet".
3062
- *
3063
- * Sending it explicitly pins this method to the documented behaviour against
3064
- * whichever API version it is talking to, including deployments that predate
3065
- * the server-side change. Callers who want only in-flight runs pass
3066
- * `only_active: true`.
4313
+ * Includes completed runs by default. Set `only_active: true` to retrieve
4314
+ * only in-flight runs. Use the response pagination metadata to request
4315
+ * additional pages.
4316
+ *
4317
+ * @param options - Pagination and filtering options. `only_active` defaults to false, including completed runs.
4318
+ * @returns One page of run records and pagination metadata; additional pages require another call.
4319
+ * @throws NotteAPIError if the run history cannot be retrieved.
4320
+ * @example
4321
+ * const { items } = await fn.runs({ page_size: 20 });
3067
4322
  */
3068
4323
  async runs(options = {}) {
3069
- try {
3070
- const response = await listFunctionRunsByFunctionId({
3071
- client: this.client.getClient(),
3072
- path: {
3073
- function_id: this.functionId
3074
- },
3075
- query: {
3076
- ...options,
3077
- only_active: options.only_active ?? false
3078
- }
3079
- });
3080
- if (response?.error) {
3081
- throw new Error(`Failed to list function runs: ${formatError(response.error)}`);
3082
- }
3083
- return response.data;
3084
- } catch (error) {
3085
- if (error instanceof Error && error.message.startsWith("Failed to list function runs:")) {
3086
- throw error;
3087
- }
3088
- throw new Error(`Failed to list function runs: ${error instanceof Error ? error.message : String(error)}`);
3089
- }
4324
+ const functionId = await this.ensureInitialized();
4325
+ const response = await listFunctionRunsByFunctionId({
4326
+ client: this.client.getClient(),
4327
+ throwOnError: true,
4328
+ path: { function_id: functionId },
4329
+ query: { ...options, only_active: options.only_active ?? false }
4330
+ });
4331
+ return response.data;
3090
4332
  }
3091
4333
  /**
3092
- * Start a function run
4334
+ * Run the function on the cloud and wait for its result, streaming logs by
4335
+ * default like `function.run(**variables)` in Python.
4336
+ *
4337
+ * Pass `{ stream: false }` to receive the backend's JSON response without
4338
+ * live logs; execution still blocks until completion. Pass `functionRunId`
4339
+ * to execute a record returned by `createRun()`; otherwise a new run record
4340
+ * is created first, exactly like Python. A failed execution throws
4341
+ * `FailedToRunCloudFunctionError` unless `raiseOnFailure` is false, and the
4342
+ * wait is aborted with `NotteTimeoutError` after `timeoutMs`.
4343
+ *
4344
+ * @param variables - JSON-serializable keyword arguments passed to the Python function's `run` entry point.
4345
+ * @param options - Log streaming, failure handling, runtime, run ID, and timeout settings.
4346
+ * @returns The completed execution response. The function's return value is in `result`. Disabling streaming still waits for completion.
4347
+ * @throws FailedToRunCloudFunctionError if execution fails and `raiseOnFailure` is true.
4348
+ * @throws NotteTimeoutError if the execution request exceeds `timeoutMs`.
4349
+ * @throws InvalidRequestError if the run options are invalid.
4350
+ * @example
4351
+ * const result = await fn.run({ url: 'https://example.com' });
4352
+ * console.log(result.result);
4353
+ *
4354
+ * await fn.run({ url: 'https://example.com' }, { runtime: 'extended', onLog: line => logger.info(line) });
3093
4355
  */
3094
4356
  async run(variables = {}, options = {}) {
3095
- const apiKey = this.client.getConfig().apiKey || "";
3096
- if (!apiKey) {
3097
- throw new Error("API key is required. Provide it via config.apiKey or set the NOTTE_API_KEY environment variable.");
3098
- }
4357
+ const stream = options.stream ?? true;
4358
+ const timeoutMs = options.timeoutMs ?? FUNCTION_RUN_TIMEOUT_MS;
4359
+ if (!(timeoutMs > 0)) {
4360
+ throw new InvalidRequestError("timeoutMs must be a positive number");
4361
+ }
4362
+ if (options.runtime !== void 0 && !FUNCTION_RUNTIMES.includes(options.runtime)) {
4363
+ throw new InvalidRequestError(`runtime must be one of ${FUNCTION_RUNTIMES.join(", ")}, got '${String(options.runtime)}'`);
4364
+ }
4365
+ const functionId = await this.ensureInitialized();
4366
+ const functionRunId = options.functionRunId ?? (await this.createRun()).function_run_id;
4367
+ const body = {
4368
+ workflow_id: functionId,
4369
+ variables,
4370
+ stream,
4371
+ ...options.runtime !== void 0 ? { runtime: options.runtime } : {}
4372
+ };
4373
+ const headers = { [TIMEOUT_HEADER]: "0" };
4374
+ const apiKey = this.client.getConfig().apiKey;
4375
+ if (apiKey) {
4376
+ headers[RUN_API_KEY_HEADER] = apiKey;
4377
+ }
4378
+ const endpointPath = FUNCTION_RUN_ENDPOINTS.start.replace("{function_id}", functionId).replace("{run_id}", functionRunId);
4379
+ const parseAs = stream ? "stream" : "json";
4380
+ const controller = new AbortController();
4381
+ let timedOut = false;
4382
+ const timer = Number.isFinite(timeoutMs) ? setTimeout(() => {
4383
+ timedOut = true;
4384
+ controller.abort(new DOMException(`Function run timed out after ${timeoutMs}ms`, "TimeoutError"));
4385
+ }, timeoutMs) : void 0;
4386
+ timer?.unref?.();
3099
4387
  try {
3100
- const response = await functionRunStart({
3101
- client: this.client.getClient(),
3102
- path: {
3103
- function_id: this.functionId
3104
- },
3105
- headers: {
3106
- "x-notte-api-key": apiKey,
3107
- "x-notte-request-origin": "sdk-node",
3108
- "x-notte-sdk-version": SDK_VERSION
3109
- },
3110
- body: {
3111
- workflow_id: this.functionId,
3112
- variables,
3113
- stream: false,
3114
- ...options.runtime ? { runtime: options.runtime } : {}
3115
- }
4388
+ const raw = await this.postRunEndpoint({
4389
+ url: FUNCTION_RUN_ENDPOINTS.start,
4390
+ path: { function_id: functionId, run_id: functionRunId },
4391
+ body: { ...body, function_run_id: functionRunId },
4392
+ parseAs,
4393
+ headers,
4394
+ signal: controller.signal
3116
4395
  });
3117
- if (response?.error) {
3118
- throw new Error(`Failed to start function run: ${formatError(response.error)}`);
4396
+ const result = stream ? await readFunctionStream(raw, options.onLog ?? console.log, endpointPath, controller.signal) : parseFunctionRunResult(raw, endpointPath);
4397
+ if (result.status === "failed" && (options.raiseOnFailure ?? true)) {
4398
+ throw new FailedToRunCloudFunctionError(functionId, result.function_run_id, result);
3119
4399
  }
3120
- return response.data;
4400
+ return result;
3121
4401
  } catch (error) {
3122
- if (error instanceof Error && error.message.startsWith("Failed to start function run:")) {
3123
- throw error;
4402
+ if (timedOut) {
4403
+ throw new NotteTimeoutError(`Function ${functionId} run timed out after ${timeoutMs}ms`, { cause: error });
3124
4404
  }
3125
- throw new Error(`Failed to run function: ${error instanceof Error ? error.message : String(error)}`);
4405
+ throw error;
4406
+ } finally {
4407
+ clearTimeout(timer);
3126
4408
  }
3127
4409
  }
3128
4410
  /**
3129
4411
  * Get the download URL of the function code, decrypting it when the API
3130
4412
  * returns an encrypted one (Notte managed functions).
4413
+ *
4414
+ * @param options - Optional version and decryption-key override.
4415
+ * @returns An HTTP(S) URL for downloading the function source.
4416
+ * @throws InvalidRequestError if an encrypted URL has no usable decryption key.
4417
+ * @throws NotteError if decryption does not produce an HTTP(S) URL.
4418
+ * @example
4419
+ * const url = await fn.getUrl({ version: 'v1' });
3131
4420
  */
3132
4421
  async getUrl(options = {}) {
3133
- let response;
3134
- try {
3135
- response = await functionDownloadUrl({
3136
- client: this.client.getClient(),
3137
- path: {
3138
- function_id: this.functionId
3139
- },
3140
- query: options.version ? { version: options.version } : {}
3141
- });
3142
- } catch (error) {
3143
- throw new Error(`Failed to get function download url: ${error instanceof Error ? error.message : String(error)}`);
3144
- }
3145
- if (response?.error) {
3146
- throw new Error(`Failed to get function download url: ${formatError(response.error)}`);
3147
- }
3148
- const url = response.data.url;
4422
+ const { url } = await this.get(options);
3149
4423
  if (isHttpUrl(url)) {
3150
4424
  return url;
3151
4425
  }
3152
4426
  const decryptionKey = options.decryption_key ?? this.decryptionKey;
3153
- if (!decryptionKey) {
3154
- throw new Error(
4427
+ if (decryptionKey === void 0) {
4428
+ throw new InvalidRequestError(
3155
4429
  "Decryption key is required to decrypt the function download url. Set it when creating the function, i.e. client.NotteFunction({ function_id: '<your-function-id>', decryption_key: '<your-key>' })."
3156
4430
  );
3157
4431
  }
@@ -3159,147 +4433,837 @@ var NotteFunction = class {
3159
4433
  try {
3160
4434
  decrypted = new Encryption(decryptionKey).decrypt(url);
3161
4435
  } catch (error) {
3162
- throw new Error(`Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`);
4436
+ throw new InvalidRequestError(
4437
+ `Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`
4438
+ );
3163
4439
  }
3164
4440
  if (!isHttpUrl(decrypted)) {
3165
- throw new Error("Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help.");
4441
+ throw new NotteError(
4442
+ "Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help."
4443
+ );
3166
4444
  }
3167
4445
  return decrypted;
3168
4446
  }
3169
4447
  /**
3170
4448
  * Download the function code as a python file. Returns the code, and writes
3171
4449
  * it to `path` when one is provided.
4450
+ *
4451
+ * @param options - Optional version, decryption key, and local `.py` destination path.
4452
+ * @returns The Python source text, including when a local destination is supplied.
4453
+ * @throws InvalidRequestError if the destination does not end in `.py` or the decryption key is invalid.
4454
+ * @throws NotteError if the source download fails. Local filesystem failures also propagate.
4455
+ * @example
4456
+ * const code = await fn.download({ path: './scraper.py' });
3172
4457
  */
3173
4458
  async download(options = {}) {
3174
4459
  const { path } = options;
3175
- if (path !== void 0 && !path.endsWith(".py")) {
3176
- throw new Error(`Code file path must end with .py, got '${path}'`);
4460
+ if (path !== void 0) {
4461
+ assertPythonPath(path);
3177
4462
  }
3178
4463
  const url = await this.getUrl(options);
3179
4464
  let response;
3180
4465
  try {
3181
4466
  response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
3182
4467
  } catch (error) {
3183
- throw new Error(`Failed to download function code: ${error instanceof Error ? error.message : String(error)}`);
4468
+ throw new NotteError(
4469
+ `Failed to download function code in ${DOWNLOAD_TIMEOUT_MS / 1e3} seconds: ${error instanceof Error ? error.message : String(error)}`,
4470
+ { cause: error }
4471
+ );
3184
4472
  }
3185
4473
  if (!response.ok) {
3186
- throw new Error(`Failed to download function code: ${response.status} ${response.statusText}`);
4474
+ throw new NotteError(`Failed to download function code: ${response.status} ${response.statusText}`);
3187
4475
  }
3188
4476
  const code = await response.text();
3189
4477
  if (path !== void 0) {
3190
- await (0, import_promises.writeFile)(path, code, "utf8");
4478
+ await (0, import_promises3.writeFile)(path, code, "utf8");
3191
4479
  }
3192
4480
  return code;
3193
4481
  }
3194
4482
  /**
3195
- * Get the function ID
4483
+ * Call one of the run endpoints that the OpenAPI spec does not describe
4484
+ * (`FUNCTION_RUN_ENDPOINTS`). The generated client has no operation for
4485
+ * them, so this is the single place where they are hand-rolled; the request
4486
+ * and response shapes are transcribed from `notte_sdk.types`
4487
+ * (`CreateFunctionRunRequest` / `CreateFunctionRunResponse` and
4488
+ * `StartFunctionRunRequest` / `FunctionRunResponse`). Replace the callers
4489
+ * with generated operations once the spec exposes these paths.
3196
4490
  */
3197
- getFunctionId() {
3198
- return this.functionId;
4491
+ async postRunEndpoint(request) {
4492
+ const response = await this.client.getClient().post({
4493
+ url: request.url,
4494
+ path: request.path,
4495
+ body: request.body,
4496
+ headers: { "Content-Type": "application/json", ...request.headers },
4497
+ parseAs: request.parseAs ?? "json",
4498
+ signal: request.signal,
4499
+ throwOnError: true
4500
+ });
4501
+ return response.data;
3199
4502
  }
3200
4503
  };
3201
4504
  function isHttpUrl(value) {
3202
4505
  return value.startsWith("https://") || value.startsWith("http://");
3203
4506
  }
3204
-
3205
- // src/client.ts
3206
- var import_zod3 = require("zod");
4507
+ function assertPythonPath(path) {
4508
+ if (!path.endsWith(".py")) {
4509
+ throw new InvalidRequestError(`Code file path must end with .py, got '${path}'`);
4510
+ }
4511
+ }
4512
+ async function readPythonFile(path) {
4513
+ assertPythonPath(path);
4514
+ let bytes;
4515
+ try {
4516
+ bytes = await (0, import_promises3.readFile)(path);
4517
+ } catch (error) {
4518
+ throw new InvalidRequestError(
4519
+ `The file '${path}' could not be read: ${error instanceof Error ? error.message : String(error)}`
4520
+ );
4521
+ }
4522
+ return new File([new Uint8Array(bytes)], (0, import_node_path3.basename)(path), { type: "text/x-python" });
4523
+ }
4524
+ function unwrapRuntimeEnvelope(candidate, path) {
4525
+ const { statusCode, body } = candidate;
4526
+ if (typeof statusCode !== "number" || statusCode < 400 || !("body" in candidate)) {
4527
+ return;
4528
+ }
4529
+ let parsed = body;
4530
+ if (typeof body === "string") {
4531
+ try {
4532
+ parsed = JSON.parse(body);
4533
+ } catch {
4534
+ parsed = body;
4535
+ }
4536
+ }
4537
+ throw NotteAPIError.fromBody(path, statusCode, parsed);
4538
+ }
4539
+ function parseFunctionRunResult(value, path) {
4540
+ if (typeof value !== "object" || value === null) {
4541
+ throw new NotteError("Invalid function result: expected an object");
4542
+ }
4543
+ const candidate = value;
4544
+ unwrapRuntimeEnvelope(candidate, path);
4545
+ const status = candidate.status;
4546
+ if (typeof candidate.function_run_id !== "string" || typeof candidate.function_id !== "string" || typeof status !== "string" || !FUNCTION_RUN_STATUSES.includes(status) || !("result" in candidate)) {
4547
+ throw new NotteError("Invalid function result: missing function_id, function_run_id, status or result");
4548
+ }
4549
+ return {
4550
+ function_id: candidate.function_id,
4551
+ function_run_id: candidate.function_run_id,
4552
+ session_id: typeof candidate.session_id === "string" ? candidate.session_id : null,
4553
+ result: candidate.result,
4554
+ status
4555
+ };
4556
+ }
4557
+ async function readFunctionStream(body, onLog, path, signal) {
4558
+ if (!body?.getReader) {
4559
+ throw new NotteError("Missing function response stream");
4560
+ }
4561
+ const reader = body.getReader();
4562
+ const onAbort = () => {
4563
+ reader.cancel(signal?.reason).catch(() => {
4564
+ });
4565
+ };
4566
+ signal?.addEventListener("abort", onAbort, { once: true });
4567
+ const decoder = new TextDecoder();
4568
+ let buffer = "";
4569
+ let data = [];
4570
+ let result;
4571
+ let sawFrame = false;
4572
+ let raw = "";
4573
+ const dispatch = () => {
4574
+ if (!data.length) return;
4575
+ const payload = data.join("\n");
4576
+ data = [];
4577
+ let event;
4578
+ try {
4579
+ event = JSON.parse(payload);
4580
+ } catch (error) {
4581
+ throw new NotteError(`Malformed function stream event: ${payload}`, { cause: error });
4582
+ }
4583
+ if (event.type === "log") onLog(String(event.message ?? ""));
4584
+ if (event.type === "error") throw new NotteError(`Function stream error: ${String(event.message ?? "")}`);
4585
+ if (event.type === "result") {
4586
+ const value = typeof event.message === "string" ? JSON.parse(event.message) : event.message;
4587
+ result = parseFunctionRunResult(value, path);
4588
+ }
4589
+ };
4590
+ const line = (value) => {
4591
+ if (value === "") dispatch();
4592
+ else if (value.startsWith("data:")) {
4593
+ sawFrame = true;
4594
+ data.push(value.slice(5).replace(/^ /, ""));
4595
+ }
4596
+ };
4597
+ try {
4598
+ while (true) {
4599
+ const { value, done } = await reader.read();
4600
+ const text = decoder.decode(value, { stream: !done });
4601
+ buffer += text;
4602
+ if (!sawFrame) raw += text;
4603
+ let end;
4604
+ while ((end = buffer.indexOf("\n")) !== -1) {
4605
+ line(buffer.slice(0, end).replace(/\r$/, ""));
4606
+ buffer = buffer.slice(end + 1);
4607
+ }
4608
+ if (done) break;
4609
+ }
4610
+ if (buffer) line(buffer.replace(/\r$/, ""));
4611
+ dispatch();
4612
+ if (!result && !sawFrame && raw.trim().length > 0) {
4613
+ let parsed;
4614
+ try {
4615
+ parsed = JSON.parse(raw);
4616
+ } catch {
4617
+ throw new NotteError(`Function stream ended without a result: ${raw.slice(0, 200)}`);
4618
+ }
4619
+ result = parseFunctionRunResult(parsed, path);
4620
+ }
4621
+ if (!result) throw new NotteError("Function stream ended without a result");
4622
+ return result;
4623
+ } finally {
4624
+ signal?.removeEventListener("abort", onAbort);
4625
+ await reader.cancel().catch(() => {
4626
+ });
4627
+ reader.releaseLock();
4628
+ }
4629
+ }
3207
4630
 
3208
4631
  // src/files.ts
4632
+ var import_node_fs2 = require("fs");
4633
+ var import_promises4 = require("fs/promises");
4634
+ var import_node_path4 = require("path");
4635
+ var import_node_crypto2 = require("crypto");
4636
+ var import_node_stream = require("stream");
4637
+ var import_promises5 = require("stream/promises");
4638
+ var FileNotFoundError = class extends NotteError {
4639
+ };
4640
+ var FileExistsError = class extends NotteError {
4641
+ };
4642
+ var METADATA_PAGE_SIZE = 1e3;
4643
+ var MIME_TYPES_BY_EXTENSION = {
4644
+ ".csv": "text/csv",
4645
+ ".gif": "image/gif",
4646
+ ".htm": "text/html",
4647
+ ".html": "text/html",
4648
+ ".jpeg": "image/jpeg",
4649
+ ".jpg": "image/jpeg",
4650
+ ".json": "application/json",
4651
+ ".md": "text/markdown",
4652
+ ".pdf": "application/pdf",
4653
+ ".png": "image/png",
4654
+ ".svg": "image/svg+xml",
4655
+ ".txt": "text/plain",
4656
+ ".webp": "image/webp",
4657
+ ".xml": "application/xml",
4658
+ ".zip": "application/zip"
4659
+ };
4660
+ function guessMimeType(filename) {
4661
+ return MIME_TYPES_BY_EXTENSION[(0, import_node_path4.extname)(filename).toLowerCase()] ?? "application/octet-stream";
4662
+ }
4663
+ function sanitizeFilename(filename) {
4664
+ const segment = filename.split(/[\\/]+/).filter(Boolean).pop() ?? "";
4665
+ return segment.replace(/^[a-zA-Z]:/, "");
4666
+ }
4667
+ function toFile(file, filename) {
4668
+ if (file instanceof File && file.name === filename) {
4669
+ return file;
4670
+ }
4671
+ const type = file instanceof Blob && file.type ? file.type : guessMimeType(filename);
4672
+ return new File([file], filename, { type });
4673
+ }
3209
4674
  var SessionFiles = class {
3210
4675
  constructor(client2, sessionId) {
3211
4676
  this.client = client2;
3212
4677
  this.sessionId = sessionId;
3213
4678
  }
3214
- path(fileId) {
3215
- const root = `/sessions/${encodeURIComponent(this.sessionId)}/files`;
3216
- return fileId ? `${root}/${encodeURIComponent(fileId)}` : root;
3217
- }
4679
+ /**
4680
+ * Upload bytes as a session file. `filename` is what the browser sees when
4681
+ * the file is later attached to a page.
4682
+ *
4683
+ * @param file - Bytes to upload as a Blob, File, or buffer.
4684
+ * @param filename - Stored filename, defaulting to `file`.
4685
+ * @returns Metadata for the uploaded file, including its ID, name, and size.
4686
+ * @throws NotteAPIError if the storage API request fails.
4687
+ */
3218
4688
  async upload(file, filename = "file") {
3219
- const body = new FormData();
3220
- body.append("file", file, filename);
3221
- const result = await this.client.request({
3222
- method: "POST",
3223
- url: this.path(),
3224
- body
4689
+ const response = await uploadSessionFile({
4690
+ client: this.client,
4691
+ path: { session_id: this.sessionId },
4692
+ body: { file: toFile(file, filename) },
4693
+ throwOnError: true
3225
4694
  });
3226
- if (result.error || !result.data) throw new Error("Failed to upload session file");
3227
- return result.data;
4695
+ return response.data;
3228
4696
  }
4697
+ /** List the session's non-expired files, newest first.
4698
+ *
4699
+ * @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
4700
+ * @returns One page of non-expired files and the total count; pagination is not automatic.
4701
+ * @throws NotteAPIError if the storage API request fails.
4702
+ */
3229
4703
  async list(options = {}) {
3230
- const result = await this.client.request({
3231
- method: "GET",
3232
- url: this.path(),
3233
- query: { limit: options.limit ?? 100, offset: options.offset ?? 0, source: options.source }
4704
+ const response = await listSessionFiles({
4705
+ client: this.client,
4706
+ path: { session_id: this.sessionId },
4707
+ query: { limit: options.limit ?? 100, offset: options.offset ?? 0, source: options.source },
4708
+ throwOnError: true
3234
4709
  });
3235
- if (result.error || !result.data) throw new Error("Failed to list session files");
3236
- return result.data;
4710
+ return response.data;
3237
4711
  }
4712
+ /**
4713
+ * Find a file's metadata by ID by scanning the session listing. Rejects with
4714
+ * `FileNotFoundError` when the ID is unknown or expired.
4715
+ *
4716
+ * @param fileId - ID of a file belonging to this session.
4717
+ * @returns The matching file's metadata.
4718
+ * @throws NotteAPIError if the storage API request fails.
4719
+ * @throws FileNotFoundError if the file ID is unknown or expired.
4720
+ */
4721
+ async metadata(fileId) {
4722
+ let offset = 0;
4723
+ while (true) {
4724
+ const page = await this.list({ limit: METADATA_PAGE_SIZE, offset });
4725
+ const match = page.files.find((file) => file.id === fileId);
4726
+ if (match) {
4727
+ return match;
4728
+ }
4729
+ offset += page.files.length;
4730
+ if (offset >= page.total || page.files.length === 0) {
4731
+ throw new FileNotFoundError(`File ${fileId} was not found in session ${this.sessionId}`);
4732
+ }
4733
+ }
4734
+ }
4735
+ /** Download a file's bytes as a `Blob`.
4736
+ *
4737
+ * @param fileId - ID of a file belonging to this session.
4738
+ * @returns The downloaded bytes as a Blob; no local file is written.
4739
+ * @throws NotteAPIError if the storage API request fails.
4740
+ */
3238
4741
  async download(fileId) {
3239
- const result = await this.client.request({
3240
- method: "GET",
3241
- url: this.path(fileId),
3242
- parseAs: "blob"
4742
+ const response = await downloadSessionFile({
4743
+ client: this.client,
4744
+ path: { session_id: this.sessionId, file_id: fileId },
4745
+ parseAs: "blob",
4746
+ throwOnError: true
4747
+ });
4748
+ return response.data;
4749
+ }
4750
+ /** Stream a file's bytes. Used by `RemoteFileStorage.download()` to write to disk without buffering. */
4751
+ async stream(fileId) {
4752
+ const response = await downloadSessionFile({
4753
+ client: this.client,
4754
+ path: { session_id: this.sessionId, file_id: fileId },
4755
+ parseAs: "stream",
4756
+ throwOnError: true
4757
+ });
4758
+ const body = response.data;
4759
+ if (!body) {
4760
+ throw new NotteError(`Download of file ${fileId} returned no body`);
4761
+ }
4762
+ return body;
4763
+ }
4764
+ /** Delete a session file.
4765
+ *
4766
+ * @param fileId - ID of the session file to delete.
4767
+ * @returns Resolves when deletion succeeds.
4768
+ * @throws NotteAPIError if the storage API request fails.
4769
+ */
4770
+ async delete(fileId) {
4771
+ await deleteSessionFile({
4772
+ client: this.client,
4773
+ path: { session_id: this.sessionId, file_id: fileId },
4774
+ throwOnError: true
3243
4775
  });
3244
- if (result.error || !result.data) throw new Error(`Failed to download session file ${fileId}`);
3245
- return result.data;
3246
4776
  }
4777
+ };
4778
+ var RemoteFileStorage = class _RemoteFileStorage {
4779
+ constructor(client2, sessionId) {
4780
+ this.client = client2;
4781
+ this._sessionId = sessionId ?? null;
4782
+ }
4783
+ _sessionId;
4784
+ /** Bind the storage to a session. Called by `Session.start()` when the storage was passed to the session. */
4785
+ setSessionId(sessionId) {
4786
+ this._sessionId = sessionId;
4787
+ }
4788
+ /**
4789
+ * Bind this storage once, cloning it when another session already owns it.
4790
+ *
4791
+ * ```ts
4792
+ * const first = storage.forSession('session-a'); // first === storage
4793
+ * const second = storage.forSession('session-b'); // a clone sharing the client
4794
+ * ```
4795
+ */
4796
+ forSession(sessionId) {
4797
+ if (this._sessionId === null) {
4798
+ this._sessionId = sessionId;
4799
+ return this;
4800
+ }
4801
+ if (this._sessionId === sessionId) {
4802
+ return this;
4803
+ }
4804
+ return new _RemoteFileStorage(this.client, sessionId);
4805
+ }
4806
+ /** ID of the bound session. Throws `InvalidRequestError` while the storage is unbound. */
4807
+ get sessionId() {
4808
+ if (this._sessionId === null) {
4809
+ throw new InvalidRequestError("A session ID is required for every file operation");
4810
+ }
4811
+ return this._sessionId;
4812
+ }
4813
+ /** Whether the storage has been bound to a session yet. */
4814
+ get isBound() {
4815
+ return this._sessionId !== null;
4816
+ }
4817
+ files() {
4818
+ return new SessionFiles(this.client.getClient(), this.sessionId);
4819
+ }
4820
+ /**
4821
+ * Upload a file to the session. A string is read as a local path and its
4822
+ * basename becomes the uploaded filename unless `uploadFileName` is set.
4823
+ *
4824
+ * @param file - Local file path, Blob, File, or buffer to upload.
4825
+ * @param uploadFileName - Optional stored filename. Local paths default to their basename; File objects keep their name.
4826
+ * @returns Metadata for the uploaded file, including its ID, name, and size.
4827
+ * @throws NotteAPIError if the storage API request fails.
4828
+ * @throws InvalidRequestError if storage has not been bound to a session.
4829
+ * @throws InvalidRequestError if a supplied local path is not a file.
4830
+ * @example
4831
+ * await storage.upload('./data/resume.pdf');
4832
+ * await storage.upload(Buffer.from('hello'), 'hello.txt');
4833
+ */
4834
+ async upload(file, uploadFileName) {
4835
+ const files = this.files();
4836
+ if (typeof file === "string") {
4837
+ const info = await (0, import_promises4.stat)(file).catch(() => null);
4838
+ if (!info?.isFile()) {
4839
+ throw new InvalidRequestError(`Cannot upload file ${file}: it is not a file`);
4840
+ }
4841
+ const bytes = await (0, import_promises4.readFile)(file);
4842
+ return files.upload(bytes, uploadFileName ?? (0, import_node_path4.basename)(file));
4843
+ }
4844
+ const filename = uploadFileName ?? (file instanceof File ? file.name : "file");
4845
+ return files.upload(file, filename);
4846
+ }
4847
+ /**
4848
+ * List the session's files.
4849
+ *
4850
+ * @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
4851
+ * @returns One page of non-expired files and the total count; pagination is not automatic.
4852
+ * @throws NotteAPIError if the storage API request fails.
4853
+ * @throws InvalidRequestError if storage has not been bound to a session.
4854
+ * @example
4855
+ * const downloads = await storage.list({ source: 'session_download', limit: 1000 });
4856
+ */
4857
+ async list(options = {}) {
4858
+ return this.files().list(options);
4859
+ }
4860
+ /** Metadata of a file by ID. Rejects with `FileNotFoundError` when unknown.
4861
+ *
4862
+ * @param fileId - ID of a file belonging to this session.
4863
+ * @returns The matching file's metadata.
4864
+ * @throws NotteAPIError if the storage API request fails.
4865
+ * @throws FileNotFoundError if the file ID is unknown or expired.
4866
+ * @throws InvalidRequestError if storage has not been bound to a session.
4867
+ */
4868
+ async metadata(fileId) {
4869
+ return this.files().metadata(fileId);
4870
+ }
4871
+ /**
4872
+ * Download a file to `<localDir>/<filename>` and return the local path. The
4873
+ * file is written atomically (temporary file + rename). Rejects with
4874
+ * `FileExistsError` when the destination exists unless `force` is set.
4875
+ *
4876
+ * @param fileId - ID of the file to download.
4877
+ * @param localDir - Destination directory, created if needed. Defaults to the current directory.
4878
+ * @param options - Set `force` to overwrite an existing destination; defaults to false.
4879
+ * @returns The local file path after the download has been written successfully.
4880
+ * @throws NotteAPIError if the storage API request fails.
4881
+ * @throws InvalidRequestError if storage has not been bound to a session.
4882
+ * @throws FileExistsError if the destination exists and `force` is false. Filesystem errors also propagate.
4883
+ * @example
4884
+ * const path = await storage.download(fileId, './downloads', { force: true });
4885
+ */
4886
+ async download(fileId, localDir = ".", options = {}) {
4887
+ const files = this.files();
4888
+ const metadata = await files.metadata(fileId);
4889
+ const safeName = sanitizeFilename(metadata.filename);
4890
+ if (safeName === "" || safeName === "." || safeName === "..") {
4891
+ throw new NotteError(`Unsafe filename returned for file ${fileId}: ${JSON.stringify(metadata.filename)}`);
4892
+ }
4893
+ await (0, import_promises4.mkdir)(localDir, { recursive: true });
4894
+ const destination = (0, import_node_path4.join)(localDir, safeName);
4895
+ if (!options.force) {
4896
+ const exists = await (0, import_promises4.stat)(destination).then(() => true, () => false);
4897
+ if (exists) {
4898
+ throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
4899
+ }
4900
+ }
4901
+ const body = await files.stream(fileId);
4902
+ const temporary = (0, import_node_path4.join)(localDir, `.${safeName}.${(0, import_node_crypto2.randomBytes)(8).toString("hex")}.part`);
4903
+ try {
4904
+ await (0, import_promises5.pipeline)(
4905
+ import_node_stream.Readable.fromWeb(body),
4906
+ (0, import_node_fs2.createWriteStream)(temporary, { flags: "wx" })
4907
+ );
4908
+ if (options.force) {
4909
+ await (0, import_promises4.rename)(temporary, destination);
4910
+ } else {
4911
+ try {
4912
+ await (0, import_promises4.link)(temporary, destination);
4913
+ } catch (error) {
4914
+ if (error.code === "EEXIST") {
4915
+ throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
4916
+ }
4917
+ throw error;
4918
+ }
4919
+ await (0, import_promises4.rm)(temporary, { force: true });
4920
+ }
4921
+ } catch (error) {
4922
+ await (0, import_promises4.rm)(temporary, { force: true });
4923
+ throw error;
4924
+ }
4925
+ return destination;
4926
+ }
4927
+ /** Download a file's bytes as a `Blob` without touching the local filesystem. */
4928
+ async downloadBlob(fileId) {
4929
+ return this.files().download(fileId);
4930
+ }
4931
+ /** Stream a file's bytes without buffering the whole file in memory. */
4932
+ async stream(fileId) {
4933
+ return this.files().stream(fileId);
4934
+ }
4935
+ /** Delete a session file.
4936
+ *
4937
+ * @param fileId - ID of the session file to delete.
4938
+ * @returns Resolves when deletion succeeds.
4939
+ * @throws NotteAPIError if the storage API request fails.
4940
+ * @throws InvalidRequestError if storage has not been bound to a session.
4941
+ */
3247
4942
  async delete(fileId) {
3248
- const result = await this.client.request({ method: "DELETE", url: this.path(fileId) });
3249
- if (result.error) throw new Error(`Failed to delete session file ${fileId}`);
4943
+ await this.files().delete(fileId);
4944
+ }
4945
+ };
4946
+
4947
+ // src/anything.ts
4948
+ var NotteAnything = class {
4949
+ client;
4950
+ constructor(client2) {
4951
+ this.client = client2;
4952
+ }
4953
+ /**
4954
+ * Start building a function for `task`. Building is slow (minutes) and
4955
+ * billable; prefer the marketplace (`client.functions.list()`) when a
4956
+ * ready-made function already fits.
4957
+ *
4958
+ * ```ts
4959
+ * const result = await client.anything.start({ task: 'get the weather in Paris' });
4960
+ * if (typeof result.function_id === 'string') {
4961
+ * const fn = client.NotteFunction({ function_id: result.function_id });
4962
+ * }
4963
+ * ```
4964
+ *
4965
+ * Rejects with a `NotteAPIError` when the API answers with a non-2xx status.
4966
+ */
4967
+ async start(body) {
4968
+ const response = await anythingStart({
4969
+ client: this.client.getClient(),
4970
+ body,
4971
+ throwOnError: true
4972
+ });
4973
+ return response.data ?? {};
4974
+ }
4975
+ };
4976
+
4977
+ // src/secrets.ts
4978
+ var NotteSecrets = class {
4979
+ client;
4980
+ constructor(client2) {
4981
+ this.client = client2;
4982
+ }
4983
+ /**
4984
+ * List secret metadata (never the values), optionally filtered by namespace.
4985
+ *
4986
+ * ```ts
4987
+ * const secrets = await client.secrets.list({ namespace: 'llm_provider' });
4988
+ * secrets.forEach(s => console.log(s.name, s.key_hint));
4989
+ * ```
4990
+ */
4991
+ async list(options = {}) {
4992
+ const response = await listSecrets({ client: this.client.getClient(), query: options, throwOnError: true });
4993
+ return response.data?.items ?? [];
4994
+ }
4995
+ /**
4996
+ * Store (or overwrite) a secret and return its metadata.
4997
+ *
4998
+ * ```ts
4999
+ * const meta = await client.secrets.store({ namespace: 'function_env', name: 'API_TOKEN', value: 'xyz' });
5000
+ * console.log(meta.id, meta.key_hint);
5001
+ * ```
5002
+ */
5003
+ async store(body) {
5004
+ const response = await storeSecret({ client: this.client.getClient(), body, throwOnError: true });
5005
+ return response.data;
5006
+ }
5007
+ /**
5008
+ * Read the value of a secret by name within a namespace.
5009
+ *
5010
+ * ```ts
5011
+ * const { value } = await client.secrets.get('API_TOKEN', 'function_env');
5012
+ * ```
5013
+ */
5014
+ async get(name, namespace) {
5015
+ const response = await getSecret({
5016
+ client: this.client.getClient(),
5017
+ path: { name },
5018
+ query: { namespace },
5019
+ throwOnError: true
5020
+ });
5021
+ return response.data;
5022
+ }
5023
+ /**
5024
+ * Delete a secret by id (the `id` field of `SecretMetadata`).
5025
+ *
5026
+ * ```ts
5027
+ * const meta = await client.secrets.store({ namespace: 'function_env', name: 'TMP', value: 'x' });
5028
+ * await client.secrets.delete(meta.id);
5029
+ * ```
5030
+ */
5031
+ async delete(secretId) {
5032
+ await deleteSecret({ client: this.client.getClient(), path: { secret_id: secretId }, throwOnError: true });
5033
+ }
5034
+ };
5035
+
5036
+ // src/usage.ts
5037
+ var NotteUsage = class {
5038
+ client;
5039
+ constructor(client2) {
5040
+ this.client = client2;
5041
+ }
5042
+ /**
5043
+ * Usage summary for a monthly period (current month by default).
5044
+ *
5045
+ * ```ts
5046
+ * const usage = await client.usage.get({ period: 'May 2025' });
5047
+ * console.log(`${usage.session_count} sessions, $${usage.total_cost}`);
5048
+ * ```
5049
+ */
5050
+ async get(options = {}) {
5051
+ const response = await getUsage({ client: this.client.getClient(), query: options, throwOnError: true });
5052
+ return response.data;
5053
+ }
5054
+ /**
5055
+ * Paginated per-request usage logs, optionally filtered by endpoint.
5056
+ *
5057
+ * ```ts
5058
+ * const page = await client.usage.logs({ endpoint: 'sessions.start', page: 1, page_size: 50 });
5059
+ * page.items.forEach(log => console.log(log.created_at, log.endpoint, log.duration_ms));
5060
+ * ```
5061
+ */
5062
+ async logs(options = {}) {
5063
+ const response = await getUsageLogs({ client: this.client.getClient(), query: options, throwOnError: true });
5064
+ return response.data;
3250
5065
  }
3251
5066
  };
3252
5067
 
3253
5068
  // src/client.ts
3254
- var NotteClient = class {
5069
+ var DEFAULT_NOTTE_API_URL = "https://api.notte.cc";
5070
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
5071
+ var REQUEST_ORIGIN = "sdk-node";
5072
+ var TIMEOUT_HEADER = "x-notte-timeout-ms";
5073
+ var DB_PREVIEW_ENV = "NOTTE_DB_PREVIEW_BRANCH";
5074
+ var DB_PREVIEW_HEADER = "x-db-preview";
5075
+ var EXECUTION_ERROR_CLASS = "NotteApiExecutionError";
5076
+ function isRelativeProxyUrl(baseUrl) {
5077
+ return /^\/(?!\/)/.test(baseUrl);
5078
+ }
5079
+ var NotteClient3 = class {
5080
+ config;
5081
+ client = createClient();
3255
5082
  constructor(config = {}) {
3256
5083
  const apiKey = config.apiKey || process.env.NOTTE_API_KEY;
3257
- const isProxyMode = config.baseUrl && !config.baseUrl.startsWith("https://");
5084
+ const baseUrl = config.baseUrl || process.env.NOTTE_API_URL || DEFAULT_NOTTE_API_URL;
5085
+ const dbPreview = config.dbPreview || process.env[DB_PREVIEW_ENV] || void 0;
5086
+ const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
5087
+ if (!(timeoutMs >= 0)) {
5088
+ throw new InvalidRequestError("timeoutMs must be a non-negative number");
5089
+ }
5090
+ const isProxyMode = isRelativeProxyUrl(baseUrl);
5091
+ if (!isProxyMode) {
5092
+ const url = new URL(baseUrl);
5093
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
5094
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
5095
+ throw new InvalidRequestError(
5096
+ "API base URL must use HTTPS (HTTP is only allowed on loopback for local development)"
5097
+ );
5098
+ }
5099
+ }
3258
5100
  if (!apiKey && !isProxyMode) {
3259
- throw new Error("API key is required. Provide it via config.apiKey or set the NOTTE_API_KEY environment variable.");
5101
+ throw new AuthenticationError(
5102
+ "NOTTE_API_KEY needs to be provided. Pass config.apiKey or set the NOTTE_API_KEY environment variable."
5103
+ );
3260
5104
  }
3261
- this.config = {
3262
- baseUrl: config.baseUrl || process.env.NOTTE_API_URL || "https://api.notte.cc",
3263
- apiKey,
3264
- ...config
3265
- };
3266
- client.setConfig({
5105
+ this.config = { baseUrl, apiKey, timeoutMs, dbPreview, verbose: config.verbose ?? false };
5106
+ if (baseUrl !== DEFAULT_NOTTE_API_URL && !isProxyMode) {
5107
+ console.warn(`NOTTE_API_URL is set to: ${baseUrl}`);
5108
+ }
5109
+ this.client.setConfig({
3267
5110
  baseUrl: this.config.baseUrl,
3268
- redirect: "manual"
5111
+ redirect: "manual",
5112
+ throwOnError: true
3269
5113
  });
3270
- client.interceptors.request.use((request) => {
3271
- request.headers.set("Authorization", `Bearer ${this.config.apiKey}`);
3272
- request.headers.set("x-notte-request-origin", "sdk-node");
3273
- request.headers.set("x-notte-sdk-version", SDK_VERSION);
3274
- return request;
5114
+ this.client.interceptors.request.use((request) => {
5115
+ const headers = new Headers(request.headers);
5116
+ if (this.config.apiKey) headers.set("Authorization", `Bearer ${this.config.apiKey}`);
5117
+ headers.set("x-notte-request-origin", REQUEST_ORIGIN);
5118
+ headers.set("x-notte-sdk-version", SDK_VERSION);
5119
+ if (this.config.dbPreview) headers.set(DB_PREVIEW_HEADER, this.config.dbPreview);
5120
+ const override = headers.get(TIMEOUT_HEADER);
5121
+ headers.delete(TIMEOUT_HEADER);
5122
+ const timeoutMs2 = override !== null ? Number(override) : this.config.timeoutMs;
5123
+ if (this.config.verbose) {
5124
+ console.info(`Making \`${request.method}\` request to \`${request.url}\``);
5125
+ }
5126
+ if (!(timeoutMs2 > 0)) {
5127
+ return new Request(request, { headers });
5128
+ }
5129
+ const deadline = withTimeout(request.signal, timeoutMs2);
5130
+ const timed = new Request(request, { headers, signal: deadline.signal });
5131
+ pendingDeadlines.set(timed, deadline.release);
5132
+ return timed;
3275
5133
  });
3276
- client.interceptors.response.use(async (response, request, opts) => {
3277
- if ((response.status === 307 || response.status === 308) && response.headers.get("location")) {
3278
- const location = response.headers.get("location");
3279
- const headers = new Headers(request.headers);
3280
- if (new URL(location).origin !== new URL(request.url).origin) {
5134
+ this.client.interceptors.response.use(async (response, request, opts) => {
5135
+ const initialUrl = new URL(request.url);
5136
+ let currentUrl = initialUrl;
5137
+ const headers = new Headers(request.headers);
5138
+ const apiUrl = new URL(this.config.baseUrl, initialUrl);
5139
+ const executionPrefix = `${apiUrl.pathname.replace(/\/$/, "")}/functions/`;
5140
+ const executionPath = initialUrl.pathname.slice(executionPrefix.length);
5141
+ const runtimeHandoff = request.method === "POST" && initialUrl.origin === apiUrl.origin && initialUrl.pathname.startsWith(executionPrefix) && /^[^/]+\/runs\/[^/]+$/.test(executionPath) && !executionPath.endsWith("/create");
5142
+ for (let hop = 0; (response.status === 307 || response.status === 308) && response.headers.get("location"); hop++) {
5143
+ if (hop >= 10) throw new InvalidRequestError("Too many API redirects");
5144
+ const location = new URL(response.headers.get("location"), currentUrl);
5145
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
5146
+ if (location.protocol !== "https:" && !(location.protocol === "http:" && loopback)) {
5147
+ throw new InvalidRequestError(`Refusing to follow a redirect to an insecure URL: ${location.origin}`);
5148
+ }
5149
+ if (location.username || location.password) {
5150
+ throw new InvalidRequestError("Refusing to follow a redirect containing URL credentials");
5151
+ }
5152
+ if (location.origin !== currentUrl.origin) {
3281
5153
  headers.delete("Authorization");
5154
+ const localHandoff = location.protocol === "http:" && initialUrl.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(initialUrl.hostname) && loopback;
5155
+ if (!(hop === 0 && runtimeHandoff && (location.protocol === "https:" || localHandoff))) {
5156
+ headers.delete("x-notte-api-key");
5157
+ }
3282
5158
  }
5159
+ if (hop > 0) headers.delete("x-notte-api-key");
5160
+ await response.body?.cancel();
3283
5161
  const headerRecord = {};
3284
5162
  headers.forEach((v, k) => {
3285
5163
  headerRecord[k] = v;
3286
5164
  });
3287
- const redirected = await fetch(location, {
5165
+ response = await (opts.fetch ?? fetch)(location.toString(), {
3288
5166
  method: request.method,
3289
5167
  headers: headerRecord,
3290
- body: opts.serializedBody ?? void 0
5168
+ body: opts.serializedBody ?? void 0,
5169
+ signal: request.signal,
5170
+ redirect: "manual"
3291
5171
  });
3292
- return redirected;
5172
+ currentUrl = location;
3293
5173
  }
3294
5174
  const ct = response.headers.get("content-type") ?? "";
3295
- if (ct.split(";")[0]?.trim() === "text/plain") {
5175
+ if (opts.parseAs !== "stream" && ct.split(";")[0]?.trim() === "text/plain") {
3296
5176
  const body = await response.arrayBuffer();
3297
- const headers = new Headers(response.headers);
3298
- headers.set("content-type", "application/json");
3299
- return new Response(body, { status: response.status, statusText: response.statusText, headers });
5177
+ const headers2 = new Headers(response.headers);
5178
+ headers2.set("content-type", "application/json");
5179
+ return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
3300
5180
  }
3301
5181
  return response;
3302
5182
  });
5183
+ this.client.interceptors.response.use(async (response, request, opts) => {
5184
+ try {
5185
+ const parseAs = opts.parseAs === "auto" ? getParseAs(response.headers.get("content-type")) : opts.parseAs;
5186
+ if (parseAs !== "stream" && response.body) {
5187
+ const body = await response.arrayBuffer();
5188
+ const buffered = new Response(body, {
5189
+ status: response.status,
5190
+ statusText: response.statusText,
5191
+ headers: response.headers
5192
+ });
5193
+ Object.defineProperty(buffered, "url", { value: response.url });
5194
+ return buffered;
5195
+ }
5196
+ return response;
5197
+ } finally {
5198
+ releaseDeadline(request);
5199
+ }
5200
+ });
5201
+ this.client.interceptors.error.use((error, response, request) => {
5202
+ releaseDeadline(request);
5203
+ return this.toTypedError(error, response, request);
5204
+ });
5205
+ startVersionCheck();
5206
+ }
5207
+ /**
5208
+ * Map the raw value the generated client rejects with into a `NotteError`.
5209
+ * HTTP failures become `NotteAPIError` (or `NotteAPIExecutionError` when the
5210
+ * API flags them), timeouts become `NotteTimeoutError`, anything else is
5211
+ * returned untouched.
5212
+ */
5213
+ toTypedError(error, response, request) {
5214
+ if (error instanceof NotteAPIError) {
5215
+ return error;
5216
+ }
5217
+ if (request?.signal.aborted) {
5218
+ error = request.signal.reason ?? error;
5219
+ }
5220
+ if (isTimeoutAbort(error)) {
5221
+ const target = request ? ` to \`${requestPath(request)}\`` : "";
5222
+ return new NotteTimeoutError(`Request${target} ${error.message}`, { cause: error });
5223
+ }
5224
+ if (request?.signal.aborted) return error;
5225
+ if (response) {
5226
+ const path = requestPath(request, response);
5227
+ const body = normalizeErrorBody(error);
5228
+ if (response.status === 422) {
5229
+ const latest = upgradeSuggestion();
5230
+ const message = typeof body.message === "string" ? body.message : "";
5231
+ const looksLikeSchemaMismatch = message.includes("extra_forbidden") || message.includes("Extra inputs are not permitted") || message.includes("'type':");
5232
+ if (latest && looksLikeSchemaMismatch) {
5233
+ return new NotteAPIError(
5234
+ path,
5235
+ response.status,
5236
+ { ...body, message: createUpgradeErrorMessage("API returned 422 validation error", latest, message) },
5237
+ response
5238
+ );
5239
+ }
5240
+ }
5241
+ if (response.headers.get("x-error-class") === EXECUTION_ERROR_CLASS) {
5242
+ return new NotteAPIExecutionError(path, response.status, body, response);
5243
+ }
5244
+ return new NotteAPIError(path, response.status, body, response);
5245
+ }
5246
+ return error;
5247
+ }
5248
+ /**
5249
+ * Add the database preview branch to a websocket URL served by the Notte API.
5250
+ * The websocket handshake reads the branch from the query string, so without
5251
+ * it a preview-branch session is looked up in the default database.
5252
+ */
5253
+ withDbPreview(url) {
5254
+ if (!this.config.dbPreview) {
5255
+ return url;
5256
+ }
5257
+ const parsed = new URL(url);
5258
+ parsed.searchParams.set("db_preview", this.config.dbPreview);
5259
+ return parsed.toString();
5260
+ }
5261
+ /**
5262
+ * Health check the Notte API. Resolves when the API answers 200 and rejects
5263
+ * with a `NotteAPIError` (or a network error) otherwise.
5264
+ */
5265
+ async healthCheck() {
5266
+ await healthCheck({ client: this.client });
3303
5267
  }
3304
5268
  /**
3305
5269
  * Create a new session
@@ -3311,6 +5275,20 @@ var NotteClient = class {
3311
5275
  Files(sessionId) {
3312
5276
  return new SessionFiles(this.getClient(), sessionId);
3313
5277
  }
5278
+ /**
5279
+ * File storage to attach to a session, the counterpart of `client.FileStorage()`.
5280
+ *
5281
+ * ```ts
5282
+ * const storage = client.FileStorage();
5283
+ * await client.Session({ storage }).use(async session => {
5284
+ * await storage.upload('./invoice.pdf');
5285
+ * const files = await storage.list({ source: 'session_download' });
5286
+ * });
5287
+ * ```
5288
+ */
5289
+ FileStorage(sessionId) {
5290
+ return new RemoteFileStorage(this, sessionId);
5291
+ }
3314
5292
  /**
3315
5293
  * Create a new agent
3316
5294
  */
@@ -3335,6 +5313,66 @@ var NotteClient = class {
3335
5313
  NotteFunction(options) {
3336
5314
  return new NotteFunction(this, options);
3337
5315
  }
5316
+ /** Alias of `NotteFunction`, matching `client.Function` in Python. */
5317
+ Function(options) {
5318
+ return this.NotteFunction(options);
5319
+ }
5320
+ /** Sessions listing, the counterpart of `client.sessions.list()`. */
5321
+ get sessions() {
5322
+ return {
5323
+ list: async (options = {}) => {
5324
+ const response = await listSessions({ client: this.getClient(), query: options });
5325
+ return response.data?.items ?? [];
5326
+ }
5327
+ };
5328
+ }
5329
+ /** Agents listing, the counterpart of `client.agents.list()`. */
5330
+ get agents() {
5331
+ return {
5332
+ list: async (options = {}) => {
5333
+ const response = await listAgents({ client: this.getClient(), query: options });
5334
+ return response.data?.items ?? [];
5335
+ }
5336
+ };
5337
+ }
5338
+ /** Vaults listing, the counterpart of `client.vaults.list()`. */
5339
+ get vaults() {
5340
+ return {
5341
+ list: async (options = {}) => {
5342
+ const response = await listVaults({ client: this.getClient(), query: options });
5343
+ return response.data?.items ?? [];
5344
+ }
5345
+ };
5346
+ }
5347
+ /** Functions listing, the counterpart of `client.functions.list()`. */
5348
+ get functions() {
5349
+ return {
5350
+ list: async (options = {}) => {
5351
+ const response = await listFunctions({ client: this.getClient(), query: options });
5352
+ return response.data?.items ?? [];
5353
+ }
5354
+ };
5355
+ }
5356
+ async search(query, options = {}) {
5357
+ const response = await searchWeb({
5358
+ client: this.getClient(),
5359
+ body: { ...options, q: query },
5360
+ throwOnError: true
5361
+ });
5362
+ return response.data;
5363
+ }
5364
+ /** Anything API (`POST /anything/start`). */
5365
+ get anything() {
5366
+ return new NotteAnything(this);
5367
+ }
5368
+ /** Workspace secrets (`/secrets`). */
5369
+ get secrets() {
5370
+ return new NotteSecrets(this);
5371
+ }
5372
+ /** Usage and billing (`/usage`, `/usage/logs`). */
5373
+ get usage() {
5374
+ return new NotteUsage(this);
5375
+ }
3338
5376
  /**
3339
5377
  * Personas client for listing and managing personas
3340
5378
  */
@@ -3345,9 +5383,6 @@ var NotteClient = class {
3345
5383
  client: this.getClient(),
3346
5384
  query: options || {}
3347
5385
  });
3348
- if (response?.error) {
3349
- throw new Error(`Failed to list personas: ${JSON.stringify(response.error)}`);
3350
- }
3351
5386
  return response.data?.items || [];
3352
5387
  }
3353
5388
  };
@@ -3356,49 +5391,80 @@ var NotteClient = class {
3356
5391
  * Get the configured client instance
3357
5392
  */
3358
5393
  getClient() {
3359
- return client;
5394
+ return this.client;
3360
5395
  }
3361
5396
  async scrape(url, options = {}) {
3362
- const apiOptions = { ...options };
3363
- if (options.response_format && typeof options.response_format.parse === "function") {
3364
- if (options.json_schema) {
3365
- apiOptions.response_format = options.json_schema;
3366
- } else {
3367
- apiOptions.response_format = import_zod3.z.toJSONSchema(options.response_format);
3368
- }
3369
- if (!apiOptions.instructions) {
3370
- const schemaStr = JSON.stringify(apiOptions.response_format);
3371
- if (schemaStr.includes("plans") || schemaStr.includes("plan")) {
3372
- apiOptions.instructions = "Extract pricing plans from the page";
3373
- } else if (schemaStr.includes("products") || schemaStr.includes("product")) {
3374
- apiOptions.instructions = "Extract product information from the page";
3375
- } else if (schemaStr.includes("articles") || schemaStr.includes("article")) {
3376
- apiOptions.instructions = "Extract article information from the page";
3377
- } else {
3378
- apiOptions.instructions = "Extract structured data from the page based on the provided schema";
3379
- }
3380
- }
3381
- }
5397
+ const body = await buildScrapeBody(options);
3382
5398
  const response = await scrapeWebpage({
3383
5399
  client: this.getClient(),
3384
- body: {
3385
- url,
3386
- ...apiOptions
3387
- }
5400
+ body: { url, ...body }
3388
5401
  });
3389
- if (response?.error) {
3390
- throw new Error(`Failed to scrape webpage: ${JSON.stringify(response.error)}`);
3391
- }
3392
- const scrapeResponse = response.data;
3393
- return processScrapeResponse(scrapeResponse, options);
5402
+ return processScrapeResponse(response.data, options);
3394
5403
  }
3395
5404
  /**
3396
5405
  * Get the client configuration
3397
5406
  */
3398
5407
  getConfig() {
3399
- return this.config;
5408
+ return { ...this.config };
3400
5409
  }
3401
5410
  };
5411
+ function requestPath(request, response) {
5412
+ const url = request?.url ?? response?.url;
5413
+ if (!url) {
5414
+ return "unknown";
5415
+ }
5416
+ try {
5417
+ return new URL(url).pathname;
5418
+ } catch {
5419
+ return url;
5420
+ }
5421
+ }
5422
+ function isTimeoutAbort(error) {
5423
+ return typeof error === "object" && error !== null && error.name === "TimeoutError";
5424
+ }
5425
+ function timeoutReason(timeoutMs) {
5426
+ return new DOMException(`timed out after ${timeoutMs}ms`, "TimeoutError");
5427
+ }
5428
+ var pendingDeadlines = /* @__PURE__ */ new WeakMap();
5429
+ function releaseDeadline(request) {
5430
+ if (!request) return;
5431
+ const release = pendingDeadlines.get(request);
5432
+ if (release) {
5433
+ pendingDeadlines.delete(request);
5434
+ release();
5435
+ }
5436
+ }
5437
+ function withTimeout(signal, timeoutMs) {
5438
+ const controller = new AbortController();
5439
+ const timer = setTimeout(() => controller.abort(timeoutReason(timeoutMs)), timeoutMs);
5440
+ timer.unref?.();
5441
+ let detach = () => void 0;
5442
+ let composite;
5443
+ const signalAny = AbortSignal.any;
5444
+ if (typeof signalAny === "function") {
5445
+ composite = signalAny.call(AbortSignal, signal ? [signal, controller.signal] : [controller.signal]);
5446
+ } else {
5447
+ composite = controller.signal;
5448
+ if (signal) {
5449
+ if (signal.aborted) {
5450
+ controller.abort(signal.reason);
5451
+ } else {
5452
+ const forward = () => controller.abort(signal.reason);
5453
+ signal.addEventListener("abort", forward, { once: true });
5454
+ detach = () => signal.removeEventListener("abort", forward);
5455
+ }
5456
+ }
5457
+ }
5458
+ let released = false;
5459
+ const release = () => {
5460
+ if (released) return;
5461
+ released = true;
5462
+ clearTimeout(timer);
5463
+ detach();
5464
+ };
5465
+ composite.addEventListener("abort", release, { once: true });
5466
+ return { signal: composite, release };
5467
+ }
3402
5468
 
3403
5469
  // src/proxy/types.ts
3404
5470
  var NotteProxyAuthError = class extends Error {
@@ -3409,34 +5475,66 @@ var NotteProxyAuthError = class extends Error {
3409
5475
  };
3410
5476
 
3411
5477
  // src/index.ts
3412
- startVersionCheck();
3413
5478
  var createClient2 = (config) => {
5479
+ const client2 = createClient({ baseUrl: config?.baseUrl || DEFAULT_NOTTE_API_URL });
3414
5480
  if (config?.baseUrl) {
3415
- client.setConfig({ baseUrl: config.baseUrl });
5481
+ client2.setConfig({ baseUrl: config.baseUrl });
3416
5482
  }
3417
5483
  if (config?.token) {
3418
- client.interceptors.request.use((request) => {
5484
+ client2.interceptors.request.use((request) => {
3419
5485
  request.headers.set("Authorization", `Bearer ${config.token}`);
3420
5486
  return request;
3421
5487
  });
3422
5488
  }
3423
- return client;
5489
+ return client2;
3424
5490
  };
3425
5491
  // Annotate the CommonJS export names for ESM import in node:
3426
5492
  0 && (module.exports = {
5493
+ ActionExecutionError,
3427
5494
  Agent,
5495
+ AuthenticationError,
5496
+ CAPTCHA_SOLVE_TIMEOUT_MS,
5497
+ CLUSTER_OVERLOAD_RETRY_DELAY_MS,
5498
+ CREDENTIAL_FIELDS,
5499
+ DEFAULT_NOTTE_API_URL,
5500
+ DEFAULT_REQUEST_TIMEOUT_MS,
3428
5501
  Encryption,
5502
+ EvaluateJsNoDataError,
5503
+ FUNCTION_RUN_ENDPOINTS,
5504
+ FUNCTION_RUN_TIMEOUT_MS,
5505
+ FailedToRunCloudFunctionError,
5506
+ FetchResponseDecodeError,
5507
+ FileExistsError,
5508
+ FileNotFoundError,
5509
+ InvalidRequestError,
5510
+ NotteAPIError,
5511
+ NotteAPIExecutionError,
5512
+ NotteAnything,
3429
5513
  NotteClient,
5514
+ NotteError,
3430
5515
  NotteFunction,
3431
5516
  NottePersona,
3432
5517
  NotteProxyAuthError,
5518
+ NotteSecrets,
5519
+ NotteTimeoutError,
5520
+ NotteUsage,
3433
5521
  NotteVault,
5522
+ PageFetchHTTPError,
5523
+ PageFetchResponse,
5524
+ RUN_API_KEY_HEADER,
5525
+ RemoteFileStorage,
5526
+ SESSION_START_TRIES,
5527
+ ScrapeFailedError,
3434
5528
  Session,
3435
5529
  SessionFiles,
5530
+ TIMEOUT_HEADER,
5531
+ actions,
3436
5532
  agentStart,
3437
5533
  agentStatus,
3438
5534
  agentStop,
3439
5535
  anythingStart,
5536
+ buildFetchScript,
5537
+ checkForLatestVersion,
3440
5538
  client,
3441
5539
  connectLinkContext,
3442
5540
  connectLinkMailboxConnect,
@@ -3445,6 +5543,7 @@ var createClient2 = (config) => {
3445
5543
  connectLinkStatus,
3446
5544
  connectLinkSubmit,
3447
5545
  createClient,
5546
+ createOrAppendCookiesToFile,
3448
5547
  deleteSecret,
3449
5548
  deleteSessionFile,
3450
5549
  downloadSessionFile,
@@ -3462,6 +5561,7 @@ var createClient2 = (config) => {
3462
5561
  functionScheduleDelete,
3463
5562
  functionScheduleSet,
3464
5563
  functionUpdate,
5564
+ getRootDomain,
3465
5565
  getScript,
3466
5566
  getSecret,
3467
5567
  getSessionScript,
@@ -3469,6 +5569,9 @@ var createClient2 = (config) => {
3469
5569
  getUsageLogs,
3470
5570
  healthCheck,
3471
5571
  improvePrompt,
5572
+ installServerOwnedDialogPolicy,
5573
+ isCaptchaSolveAction,
5574
+ isValidMfaSecret,
3472
5575
  listAgents,
3473
5576
  listFunctionRunsByFunctionId,
3474
5577
  listFunctions,
@@ -3484,18 +5587,21 @@ var createClient2 = (config) => {
3484
5587
  mailboxSync,
3485
5588
  mailboxUpdate,
3486
5589
  managedAuthConnectLinkCreate,
3487
- mintProxyCredentials,
3488
5590
  nudgePrompt,
5591
+ observeServerOwnedDialog,
3489
5592
  pageExecute,
3490
5593
  pageObserve,
3491
5594
  pageScrape,
3492
5595
  pageScreenshot,
5596
+ parseAgentStatusMessage,
3493
5597
  personaCreate,
3494
5598
  personaDelete,
3495
5599
  personaEmailsList,
3496
5600
  personaGet,
3497
5601
  personaSmsList,
3498
5602
  personaUpdate,
5603
+ prepareAgentRequest,
5604
+ processScrapeResponse,
3499
5605
  profileCookiesGet,
3500
5606
  profileCookiesSet,
3501
5607
  profileCreate,
@@ -3503,7 +5609,11 @@ var createClient2 = (config) => {
3503
5609
  profileDuplicate,
3504
5610
  profileGet,
3505
5611
  profileList,
5612
+ readCookiesFile,
3506
5613
  readyCheck,
5614
+ responseFromEvaluated,
5615
+ retry,
5616
+ sanitizeFilename,
3507
5617
  scrapeFromHtml,
3508
5618
  scrapeWebpage,
3509
5619
  searchWeb,
@@ -3516,8 +5626,12 @@ var createClient2 = (config) => {
3516
5626
  sessionStart,
3517
5627
  sessionStatus,
3518
5628
  sessionStop,
5629
+ startVersionCheck,
3519
5630
  storeSecret,
3520
5631
  uploadSessionFile,
5632
+ validateCredentials,
5633
+ validateMfaSecret,
5634
+ validateUrl,
3521
5635
  vaultCreate,
3522
5636
  vaultCredentialsAdd,
3523
5637
  vaultCredentialsDelete,