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/LICENSE +21 -0
- package/README.md +501 -102
- package/dist/index.d.mts +1623 -229
- package/dist/index.d.ts +1623 -229
- package/dist/index.js +2784 -670
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2735 -676
- package/dist/index.mjs.map +1 -1
- package/dist/proxy/core.d.mts +1 -1
- package/dist/proxy/core.d.ts +1 -1
- package/dist/proxy/core.js +6 -4
- package/dist/proxy/core.js.map +1 -1
- package/dist/proxy/core.mjs +6 -4
- package/dist/proxy/core.mjs.map +1 -1
- package/dist/proxy/next.d.mts +12 -7
- package/dist/proxy/next.d.ts +12 -7
- package/dist/proxy/next.js +6 -4
- package/dist/proxy/next.js.map +1 -1
- package/dist/proxy/next.mjs +6 -4
- package/dist/proxy/next.mjs.map +1 -1
- package/dist/{types-DmqqdEMP.d.mts → types-C5V9BOux.d.mts} +3 -1
- package/dist/{types-DmqqdEMP.d.ts → types-C5V9BOux.d.ts} +3 -1
- package/package.json +29 -3
package/dist/index.mjs
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/version.ts
|
|
9
|
-
var SDK_VERSION = true ? "
|
|
2
|
+
var SDK_VERSION = true ? "1.9.0" : "dev";
|
|
10
3
|
var SDK_PACKAGE_NAME = true ? "notte-sdk" : "notte-sdk";
|
|
11
4
|
|
|
12
5
|
// src/version-check.ts
|
|
13
6
|
var DISABLE_ENV = "NOTTE_SDK_DISABLE_VERSION_CHECK";
|
|
14
7
|
var VERSION_CHECK_TIMEOUT_MS = 1500;
|
|
15
|
-
var
|
|
8
|
+
var versionCheckPerformed = false;
|
|
9
|
+
var cachedLatestVersion;
|
|
16
10
|
function startVersionCheck() {
|
|
17
|
-
if (
|
|
11
|
+
if (versionCheckPerformed || !shouldCheckVersion()) {
|
|
18
12
|
return;
|
|
19
13
|
}
|
|
20
|
-
|
|
14
|
+
versionCheckPerformed = true;
|
|
21
15
|
const timer = setTimeout(() => {
|
|
22
16
|
void checkForLatestVersion().catch(() => void 0);
|
|
23
17
|
}, 0);
|
|
24
18
|
timer.unref?.();
|
|
25
19
|
}
|
|
20
|
+
function isVersionOlder(current, latest) {
|
|
21
|
+
return isVersionGreater(latest, current);
|
|
22
|
+
}
|
|
23
|
+
function upgradeSuggestion(currentVersion = SDK_VERSION) {
|
|
24
|
+
if (!cachedLatestVersion || !isPublishedVersion(currentVersion)) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
return isVersionOlder(currentVersion, cachedLatestVersion) ? cachedLatestVersion : void 0;
|
|
28
|
+
}
|
|
29
|
+
function createUpgradeErrorMessage(errorContext, latestVersion, originalError, currentVersion = SDK_VERSION, packageName = SDK_PACKAGE_NAME) {
|
|
30
|
+
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}'`;
|
|
31
|
+
if (originalError) {
|
|
32
|
+
message += `. Original error: ${originalError}`;
|
|
33
|
+
}
|
|
34
|
+
return message;
|
|
35
|
+
}
|
|
26
36
|
async function checkForLatestVersion(options = {}) {
|
|
27
37
|
const currentVersion = options.currentVersion ?? SDK_VERSION;
|
|
28
38
|
const packageName = options.packageName ?? SDK_PACKAGE_NAME;
|
|
@@ -33,11 +43,15 @@ async function checkForLatestVersion(options = {}) {
|
|
|
33
43
|
return;
|
|
34
44
|
}
|
|
35
45
|
const latestVersion = await fetchLatestVersion(packageName, fetchFn);
|
|
36
|
-
if (!latestVersion
|
|
46
|
+
if (!latestVersion) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
cachedLatestVersion = latestVersion;
|
|
50
|
+
if (!isVersionGreater(latestVersion, currentVersion)) {
|
|
37
51
|
return;
|
|
38
52
|
}
|
|
39
53
|
warnFn(
|
|
40
|
-
|
|
54
|
+
`\u26A0\uFE0F You are using ${packageName} version ${currentVersion}, but version ${latestVersion} is available on npm. Run 'npm install ${packageName}@${latestVersion}' to avoid any interruptions.`
|
|
41
55
|
);
|
|
42
56
|
}
|
|
43
57
|
function shouldCheckVersion(currentVersion = SDK_VERSION, fetchFn = globalThis.fetch, env = getEnv()) {
|
|
@@ -53,7 +67,8 @@ async function fetchLatestVersion(packageName, fetchFn) {
|
|
|
53
67
|
try {
|
|
54
68
|
const response = await fetchFn(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, {
|
|
55
69
|
headers: {
|
|
56
|
-
accept: "application/json"
|
|
70
|
+
accept: "application/json",
|
|
71
|
+
"user-agent": `${packageName}/${SDK_VERSION} (https://github.com/nottelabs/notte)`
|
|
57
72
|
},
|
|
58
73
|
signal: controller?.signal
|
|
59
74
|
});
|
|
@@ -157,7 +172,7 @@ function createSseClient({
|
|
|
157
172
|
...options
|
|
158
173
|
}) {
|
|
159
174
|
let lastEventId;
|
|
160
|
-
const
|
|
175
|
+
const sleep2 = sseSleepFn ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
161
176
|
const createStream = async function* () {
|
|
162
177
|
let retryDelay = sseDefaultRetryDelay ?? 3e3;
|
|
163
178
|
let attempt = 0;
|
|
@@ -261,7 +276,7 @@ function createSseClient({
|
|
|
261
276
|
break;
|
|
262
277
|
}
|
|
263
278
|
const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4);
|
|
264
|
-
await
|
|
279
|
+
await sleep2(backoff);
|
|
265
280
|
}
|
|
266
281
|
}
|
|
267
282
|
};
|
|
@@ -658,9 +673,7 @@ var mergeHeaders = (...headers) => {
|
|
|
658
673
|
return mergedHeaders;
|
|
659
674
|
};
|
|
660
675
|
var Interceptors = class {
|
|
661
|
-
|
|
662
|
-
this.fns = [];
|
|
663
|
-
}
|
|
676
|
+
fns = [];
|
|
664
677
|
clear() {
|
|
665
678
|
this.fns = [];
|
|
666
679
|
}
|
|
@@ -916,7 +929,7 @@ var createClient = (config = {}) => {
|
|
|
916
929
|
};
|
|
917
930
|
|
|
918
931
|
// src/lib/client/client.gen.ts
|
|
919
|
-
var client = createClient(createConfig({ baseUrl: "https://
|
|
932
|
+
var client = createClient(createConfig({ baseUrl: "https://api.notte.cc" }));
|
|
920
933
|
|
|
921
934
|
// src/lib/client/sdk.gen.ts
|
|
922
935
|
var listSessions = (options) => (options?.client ?? client).get({
|
|
@@ -1476,15 +1489,6 @@ var deleteSecret = (options) => (options.client ?? client).delete({
|
|
|
1476
1489
|
url: "/secrets/{secret_id}",
|
|
1477
1490
|
...options
|
|
1478
1491
|
});
|
|
1479
|
-
var mintProxyCredentials = (options) => (options.client ?? client).post({
|
|
1480
|
-
security: [{ scheme: "bearer", type: "http" }],
|
|
1481
|
-
url: "/proxies/gateway/credentials",
|
|
1482
|
-
...options,
|
|
1483
|
-
headers: {
|
|
1484
|
-
"Content-Type": "application/json",
|
|
1485
|
-
...options.headers
|
|
1486
|
-
}
|
|
1487
|
-
});
|
|
1488
1492
|
var healthCheck = (options) => (options?.client ?? client).get({ url: "/health", ...options });
|
|
1489
1493
|
var readyCheck = (options) => (options?.client ?? client).get({ url: "/ready", ...options });
|
|
1490
1494
|
var scrapeWebpage = (options) => (options.client ?? client).post({
|
|
@@ -1515,6 +1519,582 @@ var scrapeFromHtml = (options) => (options.client ?? client).post({
|
|
|
1515
1519
|
}
|
|
1516
1520
|
});
|
|
1517
1521
|
|
|
1522
|
+
// src/session.ts
|
|
1523
|
+
import { existsSync } from "fs";
|
|
1524
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
1525
|
+
import { resolve as resolve2 } from "path";
|
|
1526
|
+
|
|
1527
|
+
// src/errors.ts
|
|
1528
|
+
var NotteError = class extends Error {
|
|
1529
|
+
constructor(message, options) {
|
|
1530
|
+
super(message, options);
|
|
1531
|
+
this.name = new.target.name;
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
function describeBody(error) {
|
|
1535
|
+
try {
|
|
1536
|
+
return JSON.stringify(error);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return String(error);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
var NotteAPIError = class _NotteAPIError extends NotteError {
|
|
1542
|
+
path;
|
|
1543
|
+
statusCode;
|
|
1544
|
+
error;
|
|
1545
|
+
response;
|
|
1546
|
+
constructor(path, statusCode, error, response) {
|
|
1547
|
+
super(`Request to \`${path}\` failed with status code ${statusCode}: ${describeBody(error)}`);
|
|
1548
|
+
this.path = path;
|
|
1549
|
+
this.statusCode = statusCode;
|
|
1550
|
+
this.error = error;
|
|
1551
|
+
this.response = response;
|
|
1552
|
+
}
|
|
1553
|
+
/** The API's own error message, when it sent one. */
|
|
1554
|
+
get apiMessage() {
|
|
1555
|
+
const message = this.error.message ?? this.error.detail;
|
|
1556
|
+
return typeof message === "string" ? message : void 0;
|
|
1557
|
+
}
|
|
1558
|
+
static fromBody(path, statusCode, body, response) {
|
|
1559
|
+
return new _NotteAPIError(path, statusCode, normalizeErrorBody(body), response);
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
var NotteAPIExecutionError = class extends NotteAPIError {
|
|
1563
|
+
constructor(path, statusCode, error, response) {
|
|
1564
|
+
super(path, statusCode, error, response);
|
|
1565
|
+
this.message = `Error on ${path}: ${describeBody(error)}`;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
var AuthenticationError = class extends NotteError {
|
|
1569
|
+
constructor(message) {
|
|
1570
|
+
super(`Authentication failed. ${message}`);
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
var InvalidRequestError = class extends NotteError {
|
|
1574
|
+
constructor(message) {
|
|
1575
|
+
super(`Invalid request. ${message}`);
|
|
1576
|
+
}
|
|
1577
|
+
};
|
|
1578
|
+
var NotteTimeoutError = class extends NotteError {
|
|
1579
|
+
};
|
|
1580
|
+
var FailedToRunCloudFunctionError = class extends NotteError {
|
|
1581
|
+
functionId;
|
|
1582
|
+
functionRunId;
|
|
1583
|
+
response;
|
|
1584
|
+
constructor(functionId, functionRunId, response) {
|
|
1585
|
+
super(
|
|
1586
|
+
`Function ${functionId} run ${functionRunId} failed: ${typeof response === "string" ? response : describeBody(response ?? {})}`
|
|
1587
|
+
);
|
|
1588
|
+
this.functionId = functionId;
|
|
1589
|
+
this.functionRunId = functionRunId;
|
|
1590
|
+
this.response = response;
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
var ScrapeFailedError = class extends NotteError {
|
|
1594
|
+
structuredError;
|
|
1595
|
+
constructor(error) {
|
|
1596
|
+
super(`Scrape failed: ${error ?? "unknown error"}`);
|
|
1597
|
+
this.structuredError = error;
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
var ActionExecutionError = class _ActionExecutionError extends NotteError {
|
|
1601
|
+
errorType;
|
|
1602
|
+
devMessage;
|
|
1603
|
+
userMessage;
|
|
1604
|
+
agentMessage;
|
|
1605
|
+
shouldRetryLater;
|
|
1606
|
+
shouldNotifyTeam;
|
|
1607
|
+
constructor(detail) {
|
|
1608
|
+
super(detail.dev_message || detail.user_message || detail.error_type);
|
|
1609
|
+
this.errorType = detail.error_type;
|
|
1610
|
+
this.devMessage = detail.dev_message;
|
|
1611
|
+
this.userMessage = detail.user_message;
|
|
1612
|
+
this.agentMessage = detail.agent_message;
|
|
1613
|
+
this.shouldRetryLater = detail.should_retry_later ?? false;
|
|
1614
|
+
this.shouldNotifyTeam = detail.should_notify_team ?? false;
|
|
1615
|
+
}
|
|
1616
|
+
static fromMessage(message, errorType = "NotteBaseError") {
|
|
1617
|
+
return new _ActionExecutionError({
|
|
1618
|
+
error_type: errorType,
|
|
1619
|
+
dev_message: message,
|
|
1620
|
+
user_message: message,
|
|
1621
|
+
agent_message: message
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
var EvaluateJsNoDataError = class extends NotteError {
|
|
1626
|
+
constructor() {
|
|
1627
|
+
super("evaluate_js returned no data. Upgrade the Notte API to a build that returns the evaluated value.");
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
var FetchResponseDecodeError = class extends NotteError {
|
|
1631
|
+
constructor(reason) {
|
|
1632
|
+
super(`Failed to decode the response returned by the page fetch: ${reason}`);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
function normalizeErrorBody(body) {
|
|
1636
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
1637
|
+
return body;
|
|
1638
|
+
}
|
|
1639
|
+
if (body === void 0 || body === null || body === "") {
|
|
1640
|
+
return {};
|
|
1641
|
+
}
|
|
1642
|
+
return { message: typeof body === "string" ? body : String(body) };
|
|
1643
|
+
}
|
|
1644
|
+
var DEFAULT_RETRY_MESSAGE = "An error occurred while executing the function. Try again later...";
|
|
1645
|
+
function retry(fn, options) {
|
|
1646
|
+
const {
|
|
1647
|
+
maxTries,
|
|
1648
|
+
delayMs = 5e3,
|
|
1649
|
+
errorMessage = DEFAULT_RETRY_MESSAGE,
|
|
1650
|
+
shouldRetry = () => true,
|
|
1651
|
+
onRetry = (error, attempt, total) => {
|
|
1652
|
+
console.warn(`Failed to execute ${fn.name || "function"}: ${String(error)} (attempt ${attempt}/${total})`);
|
|
1653
|
+
}
|
|
1654
|
+
} = options;
|
|
1655
|
+
if (!Number.isInteger(maxTries) || maxTries < 1) {
|
|
1656
|
+
throw new InvalidRequestError("maxTries must be a positive integer");
|
|
1657
|
+
}
|
|
1658
|
+
return async (...args) => {
|
|
1659
|
+
let lastError;
|
|
1660
|
+
for (let attempt = 1; attempt <= maxTries; attempt += 1) {
|
|
1661
|
+
try {
|
|
1662
|
+
return await fn(...args);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
lastError = error;
|
|
1665
|
+
if (attempt === maxTries || !shouldRetry(error, attempt)) {
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
onRetry(error, attempt, maxTries);
|
|
1669
|
+
await sleep(delayMs);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
throw new Error(errorMessage, { cause: lastError });
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
function sleep(ms) {
|
|
1676
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/actions.ts
|
|
1680
|
+
function builder(type) {
|
|
1681
|
+
return (...[input]) => ({ ...input ?? {}, type });
|
|
1682
|
+
}
|
|
1683
|
+
var goto = builder("goto");
|
|
1684
|
+
var gotoNewTab = builder("goto_new_tab");
|
|
1685
|
+
var closeTab = builder("close_tab");
|
|
1686
|
+
var switchTab = builder("switch_tab");
|
|
1687
|
+
var goBack = builder("go_back");
|
|
1688
|
+
var goForward = builder("go_forward");
|
|
1689
|
+
var reload = builder("reload");
|
|
1690
|
+
var wait = builder("wait");
|
|
1691
|
+
var pressKey = builder("press_key");
|
|
1692
|
+
var scrollUp = builder("scroll_up");
|
|
1693
|
+
var scrollDown = builder("scroll_down");
|
|
1694
|
+
var captchaSolve = builder("captcha_solve");
|
|
1695
|
+
var help = builder("help");
|
|
1696
|
+
var completion = builder("completion");
|
|
1697
|
+
var scrape = builder("scrape");
|
|
1698
|
+
var emailRead = builder("email_read");
|
|
1699
|
+
var emailVerificationRead = builder("email_verification_read");
|
|
1700
|
+
var smsRead = builder("sms_read");
|
|
1701
|
+
var evaluateJs = builder("evaluate_js");
|
|
1702
|
+
var formFill = builder("form_fill");
|
|
1703
|
+
var click = builder("click");
|
|
1704
|
+
var fill = builder("fill");
|
|
1705
|
+
var multiFactorFill = builder("multi_factor_fill");
|
|
1706
|
+
var fallbackFill = builder("fallback_fill");
|
|
1707
|
+
var check = builder("check");
|
|
1708
|
+
var selectDropdownOption = builder("select_dropdown_option");
|
|
1709
|
+
var uploadFile = builder("upload_file");
|
|
1710
|
+
var downloadFile = builder("download_file");
|
|
1711
|
+
var actions = {
|
|
1712
|
+
goto,
|
|
1713
|
+
gotoNewTab,
|
|
1714
|
+
closeTab,
|
|
1715
|
+
switchTab,
|
|
1716
|
+
goBack,
|
|
1717
|
+
goForward,
|
|
1718
|
+
reload,
|
|
1719
|
+
wait,
|
|
1720
|
+
pressKey,
|
|
1721
|
+
scrollUp,
|
|
1722
|
+
scrollDown,
|
|
1723
|
+
captchaSolve,
|
|
1724
|
+
help,
|
|
1725
|
+
completion,
|
|
1726
|
+
scrape,
|
|
1727
|
+
emailRead,
|
|
1728
|
+
emailVerificationRead,
|
|
1729
|
+
smsRead,
|
|
1730
|
+
evaluateJs,
|
|
1731
|
+
formFill,
|
|
1732
|
+
click,
|
|
1733
|
+
fill,
|
|
1734
|
+
multiFactorFill,
|
|
1735
|
+
fallbackFill,
|
|
1736
|
+
check,
|
|
1737
|
+
selectDropdownOption,
|
|
1738
|
+
uploadFile,
|
|
1739
|
+
downloadFile
|
|
1740
|
+
};
|
|
1741
|
+
function isCaptchaSolveAction(action) {
|
|
1742
|
+
return action.type === "captcha_solve";
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/page-fetch.ts
|
|
1746
|
+
var CONTENT_TYPE = "content-type";
|
|
1747
|
+
function hasHeader(headers, name) {
|
|
1748
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === name);
|
|
1749
|
+
}
|
|
1750
|
+
function urlencode(mapping) {
|
|
1751
|
+
const search = new URLSearchParams();
|
|
1752
|
+
for (const [key, value] of Object.entries(mapping)) {
|
|
1753
|
+
if (Array.isArray(value)) {
|
|
1754
|
+
for (const item of value) search.append(key, String(item));
|
|
1755
|
+
} else {
|
|
1756
|
+
search.append(key, String(value));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return search.toString();
|
|
1760
|
+
}
|
|
1761
|
+
function withParams(url, params) {
|
|
1762
|
+
const hashIndex = url.indexOf("#");
|
|
1763
|
+
const fragment = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
1764
|
+
const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
1765
|
+
const queryIndex = base.indexOf("?");
|
|
1766
|
+
const path = queryIndex >= 0 ? base.slice(0, queryIndex) : base;
|
|
1767
|
+
const existing = queryIndex >= 0 ? base.slice(queryIndex + 1) : "";
|
|
1768
|
+
let query = urlencode(params);
|
|
1769
|
+
if (existing) {
|
|
1770
|
+
query = `${existing}&${query}`;
|
|
1771
|
+
}
|
|
1772
|
+
return `${path}?${query}${fragment}`;
|
|
1773
|
+
}
|
|
1774
|
+
function buildFetchScript(url, options = {}) {
|
|
1775
|
+
const { method = "GET", headers, params, json, data, timeoutMs, allowInsecure = false } = options;
|
|
1776
|
+
const redirect = options.redirect ?? (allowInsecure ? "follow" : "manual");
|
|
1777
|
+
if (json !== void 0 && data !== void 0) {
|
|
1778
|
+
throw new InvalidRequestError("pass either json or data, not both");
|
|
1779
|
+
}
|
|
1780
|
+
if (timeoutMs !== void 0 && !(timeoutMs > 0)) {
|
|
1781
|
+
throw new InvalidRequestError("timeout must be positive");
|
|
1782
|
+
}
|
|
1783
|
+
let requestUrl = url;
|
|
1784
|
+
if (params && Object.keys(params).length > 0) {
|
|
1785
|
+
requestUrl = withParams(url, params);
|
|
1786
|
+
}
|
|
1787
|
+
const requestHeaders = { ...headers ?? {} };
|
|
1788
|
+
let body;
|
|
1789
|
+
if (json !== void 0) {
|
|
1790
|
+
body = JSON.stringify(json);
|
|
1791
|
+
if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
|
|
1792
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
1793
|
+
}
|
|
1794
|
+
} else if (data !== void 0 && typeof data !== "string") {
|
|
1795
|
+
body = urlencode(data);
|
|
1796
|
+
if (!hasHeader(requestHeaders, CONTENT_TYPE)) {
|
|
1797
|
+
requestHeaders["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1798
|
+
}
|
|
1799
|
+
} else if (data !== void 0) {
|
|
1800
|
+
body = data;
|
|
1801
|
+
}
|
|
1802
|
+
const init = {
|
|
1803
|
+
method: method.toUpperCase(),
|
|
1804
|
+
headers: requestHeaders,
|
|
1805
|
+
credentials: "include",
|
|
1806
|
+
redirect
|
|
1807
|
+
};
|
|
1808
|
+
if (body !== void 0) {
|
|
1809
|
+
if (init.method === "GET" || init.method === "HEAD") {
|
|
1810
|
+
throw new InvalidRequestError(`${String(init.method)} requests cannot have a body`);
|
|
1811
|
+
}
|
|
1812
|
+
init.body = body;
|
|
1813
|
+
}
|
|
1814
|
+
let abort = "";
|
|
1815
|
+
if (timeoutMs !== void 0) {
|
|
1816
|
+
abort = `const controller = new AbortController();setTimeout(() => controller.abort(), ${Math.trunc(timeoutMs)});init.signal = controller.signal;`;
|
|
1817
|
+
}
|
|
1818
|
+
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)});})()`;
|
|
1819
|
+
}
|
|
1820
|
+
var REASON_PHRASES = {
|
|
1821
|
+
100: "Continue",
|
|
1822
|
+
101: "Switching Protocols",
|
|
1823
|
+
102: "Processing",
|
|
1824
|
+
103: "Early Hints",
|
|
1825
|
+
200: "OK",
|
|
1826
|
+
201: "Created",
|
|
1827
|
+
202: "Accepted",
|
|
1828
|
+
203: "Non-Authoritative Information",
|
|
1829
|
+
204: "No Content",
|
|
1830
|
+
205: "Reset Content",
|
|
1831
|
+
206: "Partial Content",
|
|
1832
|
+
207: "Multi-Status",
|
|
1833
|
+
208: "Already Reported",
|
|
1834
|
+
226: "IM Used",
|
|
1835
|
+
300: "Multiple Choices",
|
|
1836
|
+
301: "Moved Permanently",
|
|
1837
|
+
302: "Found",
|
|
1838
|
+
303: "See Other",
|
|
1839
|
+
304: "Not Modified",
|
|
1840
|
+
305: "Use Proxy",
|
|
1841
|
+
307: "Temporary Redirect",
|
|
1842
|
+
308: "Permanent Redirect",
|
|
1843
|
+
400: "Bad Request",
|
|
1844
|
+
401: "Unauthorized",
|
|
1845
|
+
402: "Payment Required",
|
|
1846
|
+
403: "Forbidden",
|
|
1847
|
+
404: "Not Found",
|
|
1848
|
+
405: "Method Not Allowed",
|
|
1849
|
+
406: "Not Acceptable",
|
|
1850
|
+
407: "Proxy Authentication Required",
|
|
1851
|
+
408: "Request Timeout",
|
|
1852
|
+
409: "Conflict",
|
|
1853
|
+
410: "Gone",
|
|
1854
|
+
411: "Length Required",
|
|
1855
|
+
412: "Precondition Failed",
|
|
1856
|
+
413: "Request Entity Too Large",
|
|
1857
|
+
414: "Request-URI Too Long",
|
|
1858
|
+
415: "Unsupported Media Type",
|
|
1859
|
+
416: "Requested Range Not Satisfiable",
|
|
1860
|
+
417: "Expectation Failed",
|
|
1861
|
+
418: "I'm a Teapot",
|
|
1862
|
+
421: "Misdirected Request",
|
|
1863
|
+
422: "Unprocessable Entity",
|
|
1864
|
+
423: "Locked",
|
|
1865
|
+
424: "Failed Dependency",
|
|
1866
|
+
425: "Too Early",
|
|
1867
|
+
426: "Upgrade Required",
|
|
1868
|
+
428: "Precondition Required",
|
|
1869
|
+
429: "Too Many Requests",
|
|
1870
|
+
431: "Request Header Fields Too Large",
|
|
1871
|
+
451: "Unavailable For Legal Reasons",
|
|
1872
|
+
500: "Internal Server Error",
|
|
1873
|
+
501: "Not Implemented",
|
|
1874
|
+
502: "Bad Gateway",
|
|
1875
|
+
503: "Service Unavailable",
|
|
1876
|
+
504: "Gateway Timeout",
|
|
1877
|
+
505: "HTTP Version Not Supported",
|
|
1878
|
+
506: "Variant Also Negotiates",
|
|
1879
|
+
507: "Insufficient Storage",
|
|
1880
|
+
508: "Loop Detected",
|
|
1881
|
+
510: "Not Extended",
|
|
1882
|
+
511: "Network Authentication Required"
|
|
1883
|
+
};
|
|
1884
|
+
var PageFetchHTTPError = class extends NotteError {
|
|
1885
|
+
response;
|
|
1886
|
+
constructor(response) {
|
|
1887
|
+
const kind = response.status < 500 ? "Client Error" : "Server Error";
|
|
1888
|
+
super(`${response.status} ${kind}: ${response.statusText} for url: ${response.url}`);
|
|
1889
|
+
this.response = response;
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
function encodingFor(headers, content) {
|
|
1893
|
+
const contentType = Object.entries(headers).find(([key]) => key.toLowerCase() === CONTENT_TYPE)?.[1] ?? "";
|
|
1894
|
+
for (const param of contentType.split(";").slice(1)) {
|
|
1895
|
+
const [name, ...rest] = param.split("=");
|
|
1896
|
+
const value = rest.join("=");
|
|
1897
|
+
if (name?.trim().toLowerCase() === "charset" && value) {
|
|
1898
|
+
return value.trim().replace(/^["']|["']$/g, "");
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
try {
|
|
1902
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
1903
|
+
} catch {
|
|
1904
|
+
return void 0;
|
|
1905
|
+
}
|
|
1906
|
+
return "utf-8";
|
|
1907
|
+
}
|
|
1908
|
+
var PageFetchResponse = class {
|
|
1909
|
+
status;
|
|
1910
|
+
statusText;
|
|
1911
|
+
headers;
|
|
1912
|
+
/** The final URL after redirects. */
|
|
1913
|
+
url;
|
|
1914
|
+
/** The declared charset, `utf-8` when the body is valid utf-8, else `undefined`. */
|
|
1915
|
+
encoding;
|
|
1916
|
+
content;
|
|
1917
|
+
constructor(init) {
|
|
1918
|
+
this.status = init.status;
|
|
1919
|
+
this.statusText = REASON_PHRASES[init.status] ?? "";
|
|
1920
|
+
this.headers = new Headers(init.headers);
|
|
1921
|
+
this.url = init.url;
|
|
1922
|
+
this.content = init.content;
|
|
1923
|
+
this.encoding = encodingFor(init.headers, init.content);
|
|
1924
|
+
}
|
|
1925
|
+
get ok() {
|
|
1926
|
+
return this.status >= 200 && this.status < 300;
|
|
1927
|
+
}
|
|
1928
|
+
/** The raw body bytes. */
|
|
1929
|
+
bytes() {
|
|
1930
|
+
return this.content;
|
|
1931
|
+
}
|
|
1932
|
+
async arrayBuffer() {
|
|
1933
|
+
return this.content.slice().buffer;
|
|
1934
|
+
}
|
|
1935
|
+
async text() {
|
|
1936
|
+
let decoder;
|
|
1937
|
+
try {
|
|
1938
|
+
decoder = new TextDecoder(this.encoding ?? "utf-8");
|
|
1939
|
+
} catch {
|
|
1940
|
+
decoder = new TextDecoder("utf-8");
|
|
1941
|
+
}
|
|
1942
|
+
return decoder.decode(this.content);
|
|
1943
|
+
}
|
|
1944
|
+
async json() {
|
|
1945
|
+
return JSON.parse(await this.text());
|
|
1946
|
+
}
|
|
1947
|
+
/** Throw `PageFetchHTTPError` on a 4xx/5xx status, like `requests.Response.raise_for_status()`. */
|
|
1948
|
+
raiseForStatus() {
|
|
1949
|
+
if (this.status >= 400) {
|
|
1950
|
+
throw new PageFetchHTTPError(this);
|
|
1951
|
+
}
|
|
1952
|
+
return this;
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
var BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
1956
|
+
function decodeBase64(value) {
|
|
1957
|
+
if (!BASE64_PATTERN.test(value) || value.length % 4 !== 0) {
|
|
1958
|
+
throw new Error("body_b64 is not valid base64");
|
|
1959
|
+
}
|
|
1960
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
1961
|
+
}
|
|
1962
|
+
function responseFromEvaluated(raw) {
|
|
1963
|
+
let payload;
|
|
1964
|
+
try {
|
|
1965
|
+
payload = JSON.parse(raw);
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
|
|
1968
|
+
}
|
|
1969
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
1970
|
+
throw new FetchResponseDecodeError("envelope is not an object");
|
|
1971
|
+
}
|
|
1972
|
+
const envelope = payload;
|
|
1973
|
+
try {
|
|
1974
|
+
if (!("status" in envelope)) {
|
|
1975
|
+
throw new Error("'status'");
|
|
1976
|
+
}
|
|
1977
|
+
const status = Number(envelope.status);
|
|
1978
|
+
if (!Number.isInteger(status)) {
|
|
1979
|
+
throw new Error(`invalid status: ${String(envelope.status)}`);
|
|
1980
|
+
}
|
|
1981
|
+
const rawHeaders = envelope.headers ?? {};
|
|
1982
|
+
if (typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
|
|
1983
|
+
throw new Error("headers is not an object");
|
|
1984
|
+
}
|
|
1985
|
+
const headers = {};
|
|
1986
|
+
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
1987
|
+
headers[String(key)] = String(value);
|
|
1988
|
+
}
|
|
1989
|
+
const content = decodeBase64(String(envelope.body_b64 ?? ""));
|
|
1990
|
+
const url = String(envelope.url ?? "");
|
|
1991
|
+
return new PageFetchResponse({ status, headers, url, content });
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
throw new FetchResponseDecodeError(error instanceof Error ? error.message : String(error));
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/cookies.ts
|
|
1998
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1999
|
+
import { resolve } from "path";
|
|
2000
|
+
async function readCookiesFile(cookieFile) {
|
|
2001
|
+
const cookies = JSON.parse(await readFile(cookieFile, "utf-8"));
|
|
2002
|
+
if (!Array.isArray(cookies)) {
|
|
2003
|
+
throw new InvalidRequestError(`Cookie file ${cookieFile} must contain a JSON list of cookies`);
|
|
2004
|
+
}
|
|
2005
|
+
return cookies;
|
|
2006
|
+
}
|
|
2007
|
+
async function createOrAppendCookiesToFile(cookieFile, cookies) {
|
|
2008
|
+
console.info(`\u{1F36A} Automatically saving cookies to ${cookieFile}`);
|
|
2009
|
+
await withFileLock(cookieFile, async () => {
|
|
2010
|
+
let existing = [];
|
|
2011
|
+
try {
|
|
2012
|
+
existing = await readCookiesFile(cookieFile);
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
if (!isNotFound(error)) {
|
|
2015
|
+
throw error;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
existing.push(...cookies);
|
|
2019
|
+
await writeFile(cookieFile, JSON.stringify(existing), "utf-8");
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
var fileLocks = /* @__PURE__ */ new Map();
|
|
2023
|
+
async function withFileLock(path, fn) {
|
|
2024
|
+
const key = resolve(path);
|
|
2025
|
+
const previous = fileLocks.get(key) ?? Promise.resolve();
|
|
2026
|
+
let release;
|
|
2027
|
+
const current = new Promise((done) => {
|
|
2028
|
+
release = done;
|
|
2029
|
+
});
|
|
2030
|
+
const tail = previous.then(() => current);
|
|
2031
|
+
fileLocks.set(key, tail);
|
|
2032
|
+
await previous;
|
|
2033
|
+
try {
|
|
2034
|
+
return await fn();
|
|
2035
|
+
} finally {
|
|
2036
|
+
release();
|
|
2037
|
+
if (fileLocks.get(key) === tail) {
|
|
2038
|
+
fileLocks.delete(key);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function isNotFound(error) {
|
|
2043
|
+
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// src/scrape.ts
|
|
2047
|
+
function isZodSchema(value) {
|
|
2048
|
+
return typeof value === "object" && value !== null && typeof value.parse === "function";
|
|
2049
|
+
}
|
|
2050
|
+
function requiresSchema(options) {
|
|
2051
|
+
return Boolean(options.response_format || options.instructions);
|
|
2052
|
+
}
|
|
2053
|
+
async function buildScrapeBody(options) {
|
|
2054
|
+
const { raiseOnFailure: _raise, json_schema, response_format, ...rest } = options;
|
|
2055
|
+
const body = { ...rest };
|
|
2056
|
+
if (isZodSchema(response_format)) {
|
|
2057
|
+
if (json_schema) {
|
|
2058
|
+
body.response_format = json_schema;
|
|
2059
|
+
} else {
|
|
2060
|
+
const { z: z2 } = await import("zod");
|
|
2061
|
+
body.response_format = z2.toJSONSchema(response_format);
|
|
2062
|
+
}
|
|
2063
|
+
} else if (response_format) {
|
|
2064
|
+
body.response_format = response_format;
|
|
2065
|
+
}
|
|
2066
|
+
return body;
|
|
2067
|
+
}
|
|
2068
|
+
function processScrapeResponse(scrapeResponse, options) {
|
|
2069
|
+
if (!scrapeResponse) {
|
|
2070
|
+
throw new ScrapeFailedError("empty response");
|
|
2071
|
+
}
|
|
2072
|
+
const raiseOnFailure = options.raiseOnFailure ?? true;
|
|
2073
|
+
if (options.only_images) {
|
|
2074
|
+
return scrapeResponse.images ?? [];
|
|
2075
|
+
}
|
|
2076
|
+
if (requiresSchema(options)) {
|
|
2077
|
+
const structured = scrapeResponse.structured;
|
|
2078
|
+
if (!structured) {
|
|
2079
|
+
throw new ScrapeFailedError("the API returned no structured data. This should not happen, please report it.");
|
|
2080
|
+
}
|
|
2081
|
+
const success = structured.success ?? false;
|
|
2082
|
+
const schema = isZodSchema(options.response_format) ? options.response_format : void 0;
|
|
2083
|
+
if (!success) {
|
|
2084
|
+
if (raiseOnFailure) {
|
|
2085
|
+
throw new ScrapeFailedError(structured.error ?? void 0);
|
|
2086
|
+
}
|
|
2087
|
+
return { success, error: structured.error ?? null, data: null };
|
|
2088
|
+
}
|
|
2089
|
+
const data = schema && structured.data != null ? schema.parse(structured.data) : structured.data;
|
|
2090
|
+
if (raiseOnFailure) {
|
|
2091
|
+
return data;
|
|
2092
|
+
}
|
|
2093
|
+
return { success, error: structured.error ?? null, data };
|
|
2094
|
+
}
|
|
2095
|
+
return scrapeResponse.markdown;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
1518
2098
|
// src/utils.ts
|
|
1519
2099
|
import { spawn } from "child_process";
|
|
1520
2100
|
function formatError(error) {
|
|
@@ -1557,55 +2137,64 @@ function openBrowser(url) {
|
|
|
1557
2137
|
}
|
|
1558
2138
|
|
|
1559
2139
|
// src/session.ts
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
if (requiresSchema(options)) {
|
|
1566
|
-
const structured = scrapeResponse.structured;
|
|
1567
|
-
if (!structured) {
|
|
1568
|
-
throw new Error("Failed to scrape structured data. This should not happen. Please report this issue.");
|
|
1569
|
-
}
|
|
1570
|
-
if (options.response_format && typeof options.response_format.parse === "function") {
|
|
1571
|
-
if (structured.data) {
|
|
1572
|
-
try {
|
|
1573
|
-
const validatedData = options.response_format.parse(structured.data);
|
|
1574
|
-
return validatedData;
|
|
1575
|
-
} catch (error) {
|
|
1576
|
-
throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
return structured;
|
|
1580
|
-
}
|
|
1581
|
-
return structured;
|
|
1582
|
-
}
|
|
1583
|
-
if (options.response_format && typeof options.response_format.parse === "function") {
|
|
1584
|
-
try {
|
|
1585
|
-
const validatedData = options.response_format.parse(scrapeResponse);
|
|
1586
|
-
return validatedData;
|
|
1587
|
-
} catch (error) {
|
|
1588
|
-
throw new Error(`Schema validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
return scrapeResponse.markdown;
|
|
2140
|
+
var SESSION_START_TRIES = 3;
|
|
2141
|
+
var CLUSTER_OVERLOAD_RETRY_DELAY_MS = 3e4;
|
|
2142
|
+
var CAPTCHA_SOLVE_TIMEOUT_MS = 1e5;
|
|
2143
|
+
var CAPTCHA_SOLVE_TRIES = 3;
|
|
2144
|
+
function observeServerOwnedDialog(_dialog) {
|
|
1592
2145
|
}
|
|
1593
|
-
function
|
|
1594
|
-
|
|
2146
|
+
function installServerOwnedDialogPolicy(browser) {
|
|
2147
|
+
for (const context of browser.contexts()) {
|
|
2148
|
+
context.on("dialog", observeServerOwnedDialog);
|
|
2149
|
+
}
|
|
1595
2150
|
}
|
|
1596
2151
|
var Session = class {
|
|
2152
|
+
client;
|
|
2153
|
+
options;
|
|
2154
|
+
openViewer;
|
|
2155
|
+
legacyHeadless;
|
|
2156
|
+
defaultRaiseOnFailure;
|
|
2157
|
+
defaultPerceptionType;
|
|
2158
|
+
cookieFile;
|
|
2159
|
+
fileStorage;
|
|
2160
|
+
sessionId = null;
|
|
2161
|
+
isActive = false;
|
|
2162
|
+
response = null;
|
|
2163
|
+
playwrightBrowser = null;
|
|
2164
|
+
playwrightPage = null;
|
|
1597
2165
|
constructor(client2, options = {}) {
|
|
1598
|
-
this.sessionId = null;
|
|
1599
|
-
this.isActive = false;
|
|
1600
|
-
this.response = null;
|
|
1601
2166
|
this.client = client2;
|
|
1602
|
-
const {
|
|
1603
|
-
|
|
2167
|
+
const {
|
|
2168
|
+
open_viewer = false,
|
|
2169
|
+
headless,
|
|
2170
|
+
raiseOnFailure = true,
|
|
2171
|
+
perception_type = "fast",
|
|
2172
|
+
cookie_file,
|
|
2173
|
+
storage,
|
|
2174
|
+
...sessionOptions
|
|
2175
|
+
} = options;
|
|
2176
|
+
this.options = storage ? { ...sessionOptions, use_file_storage: true } : sessionOptions;
|
|
1604
2177
|
this.openViewer = open_viewer;
|
|
1605
2178
|
this.legacyHeadless = headless;
|
|
1606
|
-
|
|
2179
|
+
this.defaultRaiseOnFailure = raiseOnFailure;
|
|
2180
|
+
this.defaultPerceptionType = perception_type;
|
|
2181
|
+
this.cookieFile = cookie_file;
|
|
2182
|
+
this.fileStorage = storage;
|
|
2183
|
+
}
|
|
2184
|
+
// #######################################################################
|
|
2185
|
+
// ############################# Session #################################
|
|
2186
|
+
// #######################################################################
|
|
1607
2187
|
/**
|
|
1608
|
-
* Start the session
|
|
2188
|
+
* Start the session: creates a new session on the server.
|
|
2189
|
+
*
|
|
2190
|
+
* Retries up to `SESSION_START_TRIES` times on 5xx errors, waiting
|
|
2191
|
+
* `CLUSTER_OVERLOAD_RETRY_DELAY_MS` on HTTP 529 (cluster overload). 4xx
|
|
2192
|
+
* errors are never retried.
|
|
2193
|
+
*
|
|
2194
|
+
* Prefer `session.use(async session => { ... })`, which also stops the session.
|
|
2195
|
+
* @example
|
|
2196
|
+
* const session = client.Session();
|
|
2197
|
+
* await session.start();
|
|
1609
2198
|
*/
|
|
1610
2199
|
async start() {
|
|
1611
2200
|
if (this.legacyHeadless === false) {
|
|
@@ -1620,93 +2209,171 @@ var Session = class {
|
|
|
1620
2209
|
if (this.isActive) {
|
|
1621
2210
|
throw new Error("Session is already active");
|
|
1622
2211
|
}
|
|
1623
|
-
|
|
2212
|
+
const body = this.options;
|
|
2213
|
+
let tries = SESSION_START_TRIES;
|
|
2214
|
+
const origTries = tries;
|
|
2215
|
+
let sessionData;
|
|
2216
|
+
while (tries > 0) {
|
|
2217
|
+
tries -= 1;
|
|
2218
|
+
try {
|
|
2219
|
+
const response = await sessionStart({ client: this.client.getClient(), body, throwOnError: true });
|
|
2220
|
+
sessionData = response.data;
|
|
2221
|
+
break;
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
if (tries === 0 || !(error instanceof NotteAPIError) || error.statusCode < 500) {
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
const retryStr = `${origTries - tries}/${origTries - 1}`;
|
|
2227
|
+
if (error.statusCode === 529) {
|
|
2228
|
+
console.warn(
|
|
2229
|
+
`Failed to start session due to cluster overload, retrying in ${CLUSTER_OVERLOAD_RETRY_DELAY_MS / 1e3} seconds (${retryStr})...`
|
|
2230
|
+
);
|
|
2231
|
+
await sleep(CLUSTER_OVERLOAD_RETRY_DELAY_MS);
|
|
2232
|
+
} else {
|
|
2233
|
+
console.warn(`Failed to start session: retrying (${retryStr})`);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (!sessionData) {
|
|
2238
|
+
throw new Error("Failed to start session: empty response");
|
|
2239
|
+
}
|
|
2240
|
+
this.sessionId = sessionData.session_id;
|
|
2241
|
+
this.isActive = true;
|
|
2242
|
+
this.response = sessionData;
|
|
2243
|
+
this.fileStorage = this.fileStorage?.forSession(sessionData.session_id);
|
|
1624
2244
|
try {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
2245
|
+
if (this.openViewer) {
|
|
2246
|
+
this.openViewerInBrowser();
|
|
2247
|
+
}
|
|
2248
|
+
if (this.cookieFile !== void 0) {
|
|
2249
|
+
if (existsSync(this.cookieFile)) {
|
|
2250
|
+
console.info(`\u{1F36A} Automatically loading cookies from ${this.cookieFile}`);
|
|
2251
|
+
await this.setCookiesFromFile(this.cookieFile);
|
|
2252
|
+
} else {
|
|
2253
|
+
console.warn(`\u{1F36A} Cookie file ${this.cookieFile} not found, skipping cookie loading`);
|
|
2254
|
+
}
|
|
1631
2255
|
}
|
|
1632
|
-
const sessionData = response.data;
|
|
1633
|
-
this.sessionId = sessionData.session_id;
|
|
1634
|
-
this.isActive = true;
|
|
1635
|
-
this.response = sessionData;
|
|
1636
|
-
shouldOpenViewer = this.openViewer;
|
|
1637
2256
|
} catch (error) {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2257
|
+
try {
|
|
2258
|
+
await this.stop("error");
|
|
2259
|
+
} catch (stopError) {
|
|
2260
|
+
console.error(`[Session] Failed to stop ${sessionData.session_id} after a start failure: ${String(stopError)}`);
|
|
2261
|
+
}
|
|
2262
|
+
throw error;
|
|
1642
2263
|
}
|
|
1643
2264
|
}
|
|
1644
2265
|
/**
|
|
1645
|
-
* Stop the session
|
|
2266
|
+
* Stop the session and clean up resources (including any Playwright
|
|
2267
|
+
* connection opened with `page()`). When a `cookie_file` was given, the
|
|
2268
|
+
* session cookies are appended to it first. A session the API reports as
|
|
2269
|
+
* already stopped is tolerated with a warning.
|
|
2270
|
+
*
|
|
2271
|
+
* @example
|
|
2272
|
+
* await session.stop();
|
|
1646
2273
|
*/
|
|
1647
2274
|
async stop(closeReason = "manual") {
|
|
1648
2275
|
if (!this.isActive || !this.sessionId) {
|
|
1649
2276
|
return;
|
|
1650
2277
|
}
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2278
|
+
const sessionId = this.sessionId;
|
|
2279
|
+
await this.closePlaywright();
|
|
2280
|
+
if (this.cookieFile !== void 0) {
|
|
2281
|
+
try {
|
|
2282
|
+
const cookies = await this.getCookies();
|
|
2283
|
+
await createOrAppendCookiesToFile(this.cookieFile, cookies);
|
|
2284
|
+
} catch (error) {
|
|
2285
|
+
console.error(`\u{1F36A} Error saving cookies to ${this.cookieFile}: ${String(error)}`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
try {
|
|
2289
|
+
const response = await sessionStop({
|
|
2290
|
+
client: this.client.getClient(),
|
|
2291
|
+
path: { session_id: sessionId },
|
|
2292
|
+
query: { close_reason: closeReason },
|
|
2293
|
+
throwOnError: true
|
|
2294
|
+
});
|
|
2295
|
+
if (response.data.status !== "closed") {
|
|
2296
|
+
throw new Error(`[Session] ${sessionId} failed to stop`);
|
|
2297
|
+
}
|
|
2298
|
+
this.response = response.data;
|
|
2299
|
+
} catch (error) {
|
|
2300
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
2301
|
+
if (message.includes("already stopped") || message.includes("already closed")) {
|
|
2302
|
+
console.warn(`Session ${sessionId} was already stopped`);
|
|
2303
|
+
} else {
|
|
2304
|
+
throw error;
|
|
1658
2305
|
}
|
|
1659
|
-
});
|
|
1660
|
-
if (response?.error) {
|
|
1661
|
-
throw new Error(`Failed to stop session: ${formatError(response.error)}`);
|
|
1662
2306
|
}
|
|
1663
|
-
this.response = response.data;
|
|
1664
2307
|
this.isActive = false;
|
|
1665
2308
|
this.sessionId = null;
|
|
1666
2309
|
}
|
|
2310
|
+
async closePlaywright() {
|
|
2311
|
+
const browser = this.playwrightBrowser;
|
|
2312
|
+
this.playwrightBrowser = null;
|
|
2313
|
+
this.playwrightPage = null;
|
|
2314
|
+
if (browser) {
|
|
2315
|
+
try {
|
|
2316
|
+
await browser.close();
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
console.warn(`[Session] Failed to close the Playwright connection: ${String(error)}`);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
1667
2322
|
/**
|
|
1668
|
-
* Get session
|
|
2323
|
+
* Get the current status of the session. Like Python, this keeps working
|
|
2324
|
+
* after `stop()` (for example to read the recorded `steps`).
|
|
2325
|
+
*
|
|
2326
|
+
* @returns Current session metadata, including lifecycle status and recorded steps. Works after `stop()`.
|
|
2327
|
+
* @throws Error if this session has never been started.
|
|
2328
|
+
* @throws NotteAPIError if the status request fails.
|
|
2329
|
+
* @example
|
|
2330
|
+
* const status = await session.status();
|
|
2331
|
+
* console.log(status.status); // 'active'
|
|
1669
2332
|
*/
|
|
1670
2333
|
async status() {
|
|
1671
|
-
if (!this.sessionId) {
|
|
1672
|
-
throw new Error("Session not started");
|
|
1673
|
-
}
|
|
1674
2334
|
const response = await sessionStatus({
|
|
1675
2335
|
client: this.client.getClient(),
|
|
1676
|
-
path: {
|
|
1677
|
-
|
|
1678
|
-
}
|
|
2336
|
+
path: { session_id: this.lastSessionId() },
|
|
2337
|
+
throwOnError: true
|
|
1679
2338
|
});
|
|
1680
|
-
if (response?.error) {
|
|
1681
|
-
throw new Error(`Failed to get session status: ${formatError(response.error)}`);
|
|
1682
|
-
}
|
|
1683
2339
|
return response.data;
|
|
1684
2340
|
}
|
|
1685
|
-
/**
|
|
1686
|
-
* Get session ID
|
|
1687
|
-
*/
|
|
2341
|
+
/** Session ID while the session is active, `null` otherwise. */
|
|
1688
2342
|
getId() {
|
|
1689
2343
|
return this.sessionId;
|
|
1690
2344
|
}
|
|
1691
|
-
/**
|
|
1692
|
-
* Check if session is active
|
|
1693
|
-
*/
|
|
2345
|
+
/** Whether the session has been started and not stopped yet. */
|
|
1694
2346
|
isSessionActive() {
|
|
1695
2347
|
return this.isActive;
|
|
1696
2348
|
}
|
|
1697
|
-
/**
|
|
1698
|
-
* Get the last response from session operations
|
|
1699
|
-
*/
|
|
2349
|
+
/** The last `SessionResponse` received from the API (start, stop or status). */
|
|
1700
2350
|
getResponse() {
|
|
1701
2351
|
return this.response;
|
|
1702
2352
|
}
|
|
2353
|
+
/** The file storage attached to the session, when one was given. */
|
|
2354
|
+
get storage() {
|
|
2355
|
+
return this.fileStorage;
|
|
2356
|
+
}
|
|
2357
|
+
requireSessionId() {
|
|
2358
|
+
if (!this.sessionId) {
|
|
2359
|
+
throw new Error("Session not started");
|
|
2360
|
+
}
|
|
2361
|
+
return this.sessionId;
|
|
2362
|
+
}
|
|
2363
|
+
/** Session ID of the active session, or of the last one when it was stopped. */
|
|
2364
|
+
lastSessionId() {
|
|
2365
|
+
const sessionId = this.sessionId ?? this.response?.session_id;
|
|
2366
|
+
if (!sessionId) {
|
|
2367
|
+
throw new Error("Session not started");
|
|
2368
|
+
}
|
|
2369
|
+
return sessionId;
|
|
2370
|
+
}
|
|
1703
2371
|
/**
|
|
1704
2372
|
* Open the live viewer for this session in the local default browser.
|
|
2373
|
+
* @throws If the session has not started or no viewer URL is available.
|
|
1705
2374
|
*/
|
|
1706
2375
|
viewer() {
|
|
1707
|
-
|
|
1708
|
-
throw new Error("Session not started");
|
|
1709
|
-
}
|
|
2376
|
+
this.requireSessionId();
|
|
1710
2377
|
const viewerUrl = this.response?.viewer_url;
|
|
1711
2378
|
if (!viewerUrl) {
|
|
1712
2379
|
throw new Error("No viewer URL available for this session");
|
|
@@ -1722,154 +2389,381 @@ var Session = class {
|
|
|
1722
2389
|
openBrowser(viewerUrl);
|
|
1723
2390
|
}
|
|
1724
2391
|
/**
|
|
1725
|
-
*
|
|
2392
|
+
* Get presigned URLs for the session replay. Recordings are finalized after
|
|
2393
|
+
* the session is closed, so by default this polls on 404 until the replay is
|
|
2394
|
+
* ready. It keeps working after `stop()`.
|
|
2395
|
+
*
|
|
2396
|
+
* Throws an `Error` when the API reports the session is still active and a
|
|
2397
|
+
* `NotteTimeoutError` when the replay is not ready within `timeoutMs`.
|
|
2398
|
+
* @example
|
|
2399
|
+
* await session.stop();
|
|
2400
|
+
* const replay = await session.replay();
|
|
2401
|
+
* console.log(replay.mp4_url);
|
|
1726
2402
|
*/
|
|
1727
|
-
async
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
|
|
2403
|
+
async replay(options = {}) {
|
|
2404
|
+
const { wait: wait2 = true, timeoutMs = 24e4, pollIntervalMs = 5e3 } = options;
|
|
2405
|
+
const sessionId = this.lastSessionId();
|
|
2406
|
+
const request = async () => {
|
|
2407
|
+
const response = await sessionReplay({
|
|
2408
|
+
client: this.client.getClient(),
|
|
2409
|
+
path: { session_id: sessionId },
|
|
2410
|
+
throwOnError: true
|
|
2411
|
+
});
|
|
2412
|
+
if (!response.data) {
|
|
2413
|
+
throw new Error("Failed to get session replay: empty response");
|
|
1738
2414
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2415
|
+
return response.data;
|
|
2416
|
+
};
|
|
2417
|
+
if (!wait2) {
|
|
2418
|
+
return request();
|
|
2419
|
+
}
|
|
2420
|
+
const deadline = Date.now() + timeoutMs;
|
|
2421
|
+
for (; ; ) {
|
|
2422
|
+
try {
|
|
2423
|
+
return await request();
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
if (!(error instanceof NotteAPIError) || error.statusCode !== 404) {
|
|
2426
|
+
throw error;
|
|
2427
|
+
}
|
|
2428
|
+
if (this.isActive && (error.apiMessage ?? "").includes("still active")) {
|
|
2429
|
+
throw new Error(`Session ${sessionId} is still active \u2014 close the session first to generate the replay.`, {
|
|
2430
|
+
cause: error
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
if (Date.now() + pollIntervalMs > deadline) {
|
|
2434
|
+
throw new NotteTimeoutError(`Replay for session ${sessionId} not ready within ${timeoutMs}ms`, { cause: error });
|
|
2435
|
+
}
|
|
2436
|
+
await sleep(pollIntervalMs);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Download the MP4 replay to a local file, like `ReplayResponse.download()`
|
|
2442
|
+
* in Python. Resolves with the absolute path of the written file and throws
|
|
2443
|
+
* when the replay has no `mp4_url`.
|
|
2444
|
+
*
|
|
2445
|
+
* @example
|
|
2446
|
+
* await session.stop();
|
|
2447
|
+
* const file = await session.downloadReplay('session.mp4');
|
|
2448
|
+
*/
|
|
2449
|
+
async downloadReplay(path = "replay.mp4", options = {}) {
|
|
2450
|
+
const replay = await this.replay(options);
|
|
2451
|
+
if (!replay.mp4_url) {
|
|
2452
|
+
throw new Error("No mp4_url available for download");
|
|
2453
|
+
}
|
|
2454
|
+
const response = await fetch(replay.mp4_url);
|
|
2455
|
+
if (!response.ok) {
|
|
2456
|
+
throw new Error(`Failed to download replay: HTTP ${response.status}`);
|
|
2457
|
+
}
|
|
2458
|
+
const target = resolve2(path);
|
|
2459
|
+
await writeFile2(target, new Uint8Array(await response.arrayBuffer()));
|
|
2460
|
+
return target;
|
|
2461
|
+
}
|
|
2462
|
+
// #######################################################################
|
|
2463
|
+
// ############################# Cookies #################################
|
|
2464
|
+
// #######################################################################
|
|
2465
|
+
/**
|
|
2466
|
+
* Upload cookies to the session.
|
|
2467
|
+
*
|
|
2468
|
+
* @example
|
|
2469
|
+
* await session.setCookies([{ name: 'token', value: 'abc', domain: 'example.com', path: '/', httpOnly: false }]);
|
|
2470
|
+
*/
|
|
2471
|
+
async setCookies(cookies) {
|
|
2472
|
+
const response = await sessionCookiesSet({
|
|
2473
|
+
client: this.client.getClient(),
|
|
2474
|
+
path: { session_id: this.requireSessionId() },
|
|
2475
|
+
body: { cookies },
|
|
2476
|
+
throwOnError: true
|
|
2477
|
+
});
|
|
1743
2478
|
return response.data;
|
|
1744
2479
|
}
|
|
1745
2480
|
/**
|
|
1746
|
-
*
|
|
2481
|
+
* Upload cookies from a JSON file (a list of cookies, like `getCookies()` returns).
|
|
2482
|
+
*
|
|
2483
|
+
* @example
|
|
2484
|
+
* await session.setCookiesFromFile('cookies.json');
|
|
1747
2485
|
*/
|
|
1748
2486
|
async setCookiesFromFile(cookieFile) {
|
|
1749
|
-
|
|
1750
|
-
const cookieData = await fs.readFile(cookieFile, "utf-8");
|
|
1751
|
-
const cookies = JSON.parse(cookieData);
|
|
1752
|
-
return this.setCookies(cookies);
|
|
2487
|
+
return this.setCookies(await readCookiesFile(cookieFile));
|
|
1753
2488
|
}
|
|
1754
2489
|
/**
|
|
1755
|
-
* Get cookies
|
|
2490
|
+
* Get the cookies of the session.
|
|
2491
|
+
*
|
|
2492
|
+
* @example
|
|
2493
|
+
* const cookies = await session.getCookies();
|
|
2494
|
+
* await fs.writeFile('cookies.json', JSON.stringify(cookies));
|
|
1756
2495
|
*/
|
|
1757
2496
|
async getCookies() {
|
|
1758
|
-
if (!this.sessionId) {
|
|
1759
|
-
throw new Error("Session not started");
|
|
1760
|
-
}
|
|
1761
2497
|
const response = await sessionCookiesGet({
|
|
1762
2498
|
client: this.client.getClient(),
|
|
1763
|
-
path: {
|
|
1764
|
-
|
|
1765
|
-
}
|
|
2499
|
+
path: { session_id: this.requireSessionId() },
|
|
2500
|
+
throwOnError: true
|
|
1766
2501
|
});
|
|
1767
|
-
if (response?.error) {
|
|
1768
|
-
throw new Error(`Failed to get cookies: ${formatError(response.error)}`);
|
|
1769
|
-
}
|
|
1770
2502
|
return response.data.cookies;
|
|
1771
2503
|
}
|
|
2504
|
+
// #######################################################################
|
|
2505
|
+
// ############################# Debug / CDP #############################
|
|
2506
|
+
// #######################################################################
|
|
2507
|
+
/** Detailed debug information for the session (debug URL, websocket URLs, tabs). */
|
|
2508
|
+
async debugInfo() {
|
|
2509
|
+
const response = await sessionDebugInfo({
|
|
2510
|
+
client: this.client.getClient(),
|
|
2511
|
+
path: { session_id: this.requireSessionId() },
|
|
2512
|
+
throwOnError: true
|
|
2513
|
+
});
|
|
2514
|
+
return response.data;
|
|
2515
|
+
}
|
|
1772
2516
|
/**
|
|
1773
|
-
*
|
|
2517
|
+
* Debug information for one tab of the session. The API client has no
|
|
2518
|
+
* dedicated tab endpoint, so this reads the tab from `debugInfo().tabs`.
|
|
1774
2519
|
*/
|
|
1775
|
-
async
|
|
1776
|
-
|
|
2520
|
+
async debugTabInfo(tabIdx = 0) {
|
|
2521
|
+
const debug = await this.debugInfo();
|
|
2522
|
+
const tab = debug.tabs[tabIdx];
|
|
2523
|
+
if (!tab) {
|
|
2524
|
+
throw new Error(`Tab ${tabIdx} not found: the session has ${debug.tabs.length} tab(s)`);
|
|
2525
|
+
}
|
|
2526
|
+
return tab;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Get the Chrome DevTools Protocol WebSocket URL for the session.
|
|
2530
|
+
*
|
|
2531
|
+
* A `cdp_url` given at construction (another browser provider) is returned
|
|
2532
|
+
* untouched. Notte-served URLs, from the start response or the debug info,
|
|
2533
|
+
* carry the database preview branch when one is configured.
|
|
2534
|
+
*
|
|
2535
|
+
* @returns The CDP WebSocket URL for connecting a browser automation client.
|
|
2536
|
+
* @throws Error if no session ID or CDP URL is available.
|
|
2537
|
+
* @example
|
|
2538
|
+
* const browser = await chromium.connectOverCDP(await session.cdpUrl());
|
|
2539
|
+
*/
|
|
2540
|
+
async cdpUrl() {
|
|
2541
|
+
if (!this.response) {
|
|
1777
2542
|
throw new Error("Session not started");
|
|
1778
2543
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
path: {
|
|
1782
|
-
session_id: this.sessionId
|
|
1783
|
-
},
|
|
1784
|
-
body: action
|
|
1785
|
-
});
|
|
1786
|
-
if (response?.error) {
|
|
1787
|
-
throw new Error(`Failed to execute action: ${formatError(response.error)}`);
|
|
2544
|
+
if (this.options.cdp_url) {
|
|
2545
|
+
return this.options.cdp_url;
|
|
1788
2546
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
throw new Error(`Action execution failed: ${result.message}`);
|
|
2547
|
+
if (this.response.cdp_url) {
|
|
2548
|
+
return this.client.withDbPreview(this.response.cdp_url);
|
|
1792
2549
|
}
|
|
1793
|
-
|
|
2550
|
+
const debug = await this.debugInfo();
|
|
2551
|
+
return this.client.withDbPreview(debug.ws.cdp);
|
|
1794
2552
|
}
|
|
1795
2553
|
/**
|
|
1796
|
-
*
|
|
2554
|
+
* Get a Playwright page connected to the session via CDP. The connection is
|
|
2555
|
+
* established lazily on first access, cached, and closed by `stop()`.
|
|
2556
|
+
* Native dialogs are left to the Notte backend.
|
|
2557
|
+
*
|
|
2558
|
+
* Requires the optional `playwright-core` peer dependency
|
|
2559
|
+
* (`npm install playwright-core`).
|
|
2560
|
+
*
|
|
2561
|
+
* @example
|
|
2562
|
+
* const page = await session.page();
|
|
2563
|
+
* await page.goto('https://www.google.com');
|
|
2564
|
+
* await page.screenshot({ path: 'screenshot.png' });
|
|
1797
2565
|
*/
|
|
1798
|
-
async
|
|
1799
|
-
if (
|
|
1800
|
-
|
|
2566
|
+
async page() {
|
|
2567
|
+
if (this.playwrightPage) {
|
|
2568
|
+
return this.playwrightPage;
|
|
1801
2569
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2570
|
+
let playwright;
|
|
2571
|
+
try {
|
|
2572
|
+
playwright = await import("playwright-core");
|
|
2573
|
+
} catch (error) {
|
|
2574
|
+
throw new Error("Playwright not installed. Run `npm install playwright-core` to use `session.page()`.", {
|
|
2575
|
+
cause: error
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
try {
|
|
2579
|
+
if (!this.playwrightBrowser) {
|
|
2580
|
+
const cdpUrl = await this.cdpUrl();
|
|
2581
|
+
this.playwrightBrowser = await playwright.chromium.connectOverCDP(cdpUrl);
|
|
2582
|
+
installServerOwnedDialogPolicy(this.playwrightBrowser);
|
|
1809
2583
|
}
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2584
|
+
const page = this.playwrightBrowser.contexts()[0]?.pages()[0];
|
|
2585
|
+
if (!page) {
|
|
2586
|
+
throw new Error("the browser has no open page");
|
|
2587
|
+
}
|
|
2588
|
+
this.playwrightPage = page;
|
|
2589
|
+
return page;
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
throw new Error(`Failed to access the playwright page from CDP: ${error instanceof Error ? error.message : String(error)}`, {
|
|
2592
|
+
cause: error
|
|
2593
|
+
});
|
|
1813
2594
|
}
|
|
1814
|
-
return response.data;
|
|
1815
2595
|
}
|
|
1816
2596
|
/**
|
|
1817
|
-
* Get session
|
|
2597
|
+
* Get the workflow code generated from the session steps
|
|
2598
|
+
* (`GET /sessions/{id}/workflow/code`).
|
|
2599
|
+
*
|
|
2600
|
+
* @example
|
|
2601
|
+
* const { python_script } = await session.getScript();
|
|
1818
2602
|
*/
|
|
1819
|
-
async
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1823
|
-
const response = await sessionReplay({
|
|
2603
|
+
async getScript(options = {}) {
|
|
2604
|
+
const { as_workflow = true, infer_response_format } = options;
|
|
2605
|
+
const response = await getSessionScript({
|
|
1824
2606
|
client: this.client.getClient(),
|
|
1825
|
-
path: {
|
|
1826
|
-
|
|
1827
|
-
|
|
2607
|
+
path: { session_id: this.lastSessionId() },
|
|
2608
|
+
query: { as_workflow, ...infer_response_format === void 0 ? {} : { infer_response_format } },
|
|
2609
|
+
throwOnError: true
|
|
1828
2610
|
});
|
|
1829
|
-
if (response?.error) {
|
|
1830
|
-
throw new Error(`Failed to get session replay: ${formatError(response.error)}`);
|
|
1831
|
-
}
|
|
1832
2611
|
return response.data;
|
|
1833
2612
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
2613
|
+
// #######################################################################
|
|
2614
|
+
// ############################# Page ####################################
|
|
2615
|
+
// #######################################################################
|
|
2616
|
+
/**
|
|
2617
|
+
* Execute an action on the current page.
|
|
2618
|
+
*
|
|
2619
|
+
* When the action fails and `raiseOnFailure` is true (the default, see the
|
|
2620
|
+
* session option), the structured `exception_detail` returned by the API is
|
|
2621
|
+
* rehydrated into an `ActionExecutionError` whose `errorType` names the
|
|
2622
|
+
* server-side error class. `captcha_solve` actions get a 100 s request
|
|
2623
|
+
* timeout and are retried up to 3 times on HTTP 408.
|
|
2624
|
+
*
|
|
2625
|
+
* @param action - Action type, target, and action-specific arguments.
|
|
2626
|
+
* @param options - Override failure handling for this call; otherwise use the session setting.
|
|
2627
|
+
* @returns The execution response, including `success` and failure details when throwing is disabled.
|
|
2628
|
+
* @throws ActionExecutionError if the action fails and failure throwing is enabled.
|
|
2629
|
+
* @throws NotteAPIError if the API request fails.
|
|
2630
|
+
* @example
|
|
2631
|
+
* import { actions } from 'notte-sdk';
|
|
2632
|
+
*
|
|
2633
|
+
* await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
|
|
2634
|
+
* await session.execute(actions.fill({ selector: "input[name='email']", value: 'user@example.com' }));
|
|
2635
|
+
* const result = await session.execute({ type: 'click', id: 'B1' }, { raiseOnFailure: false });
|
|
2636
|
+
* if (!result.success) console.log(result.message);
|
|
2637
|
+
*/
|
|
2638
|
+
async execute(action, options = {}) {
|
|
2639
|
+
const sessionId = this.requireSessionId();
|
|
2640
|
+
const raiseOnFailure = typeof options === "boolean" ? options : options.raiseOnFailure ?? this.defaultRaiseOnFailure;
|
|
2641
|
+
const isCaptcha = isCaptchaSolveAction(action);
|
|
2642
|
+
let result;
|
|
2643
|
+
let lastError;
|
|
2644
|
+
for (let attempt = 0; attempt < CAPTCHA_SOLVE_TRIES; attempt += 1) {
|
|
2645
|
+
try {
|
|
2646
|
+
const response = await pageExecute({
|
|
2647
|
+
client: this.client.getClient(),
|
|
2648
|
+
path: { session_id: sessionId },
|
|
2649
|
+
body: action,
|
|
2650
|
+
headers: isCaptcha ? { [TIMEOUT_HEADER]: String(CAPTCHA_SOLVE_TIMEOUT_MS) } : void 0,
|
|
2651
|
+
throwOnError: true
|
|
2652
|
+
});
|
|
2653
|
+
result = response.data;
|
|
2654
|
+
break;
|
|
2655
|
+
} catch (error) {
|
|
2656
|
+
if (isCaptcha && error instanceof NotteAPIError && error.statusCode === 408) {
|
|
2657
|
+
lastError = error;
|
|
2658
|
+
console.warn("Solve captcha action timed out. This can happen for long and complex captchas. Retrying...");
|
|
2659
|
+
continue;
|
|
1855
2660
|
}
|
|
2661
|
+
throw error;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (!result) {
|
|
2665
|
+
throw lastError ?? new Error(`Failed to execute action '${action.type}'. This should not happen. Please report this issue.`);
|
|
2666
|
+
}
|
|
2667
|
+
if (raiseOnFailure && !result.success) {
|
|
2668
|
+
console.error(`\u{1F6A8} Execution failed with message: '${result.message}'`);
|
|
2669
|
+
if (result.exception_detail) {
|
|
2670
|
+
throw new ActionExecutionError(result.exception_detail);
|
|
1856
2671
|
}
|
|
2672
|
+
const fallbackMessage = String(result.message ?? "").trim() || `Failed to execute action: ${action.type}`;
|
|
2673
|
+
throw ActionExecutionError.fromMessage(fallbackMessage);
|
|
2674
|
+
}
|
|
2675
|
+
return result;
|
|
2676
|
+
}
|
|
2677
|
+
async evaluateJs(code, options = {}) {
|
|
2678
|
+
const raiseOnFailure = options.raiseOnFailure ?? true;
|
|
2679
|
+
const result = await this.execute({ type: "evaluate_js", code }, { raiseOnFailure });
|
|
2680
|
+
if (!raiseOnFailure) {
|
|
2681
|
+
return result;
|
|
2682
|
+
}
|
|
2683
|
+
if (!result.data) {
|
|
2684
|
+
throw new EvaluateJsNoDataError();
|
|
2685
|
+
}
|
|
2686
|
+
return result.data.markdown;
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Issue an HTTP request from the page the session is on and return the response.
|
|
2690
|
+
*
|
|
2691
|
+
* The request runs inside the browser through `fetch()`, so it carries the
|
|
2692
|
+
* page's cookies, the session's proxy and the browser's own network
|
|
2693
|
+
* fingerprint. A relative `url` resolves against the current page, which
|
|
2694
|
+
* also makes it same-origin; a cross-origin URL is subject to CORS exactly
|
|
2695
|
+
* as in a browser tab, so `goto` the target origin first. Redirects are
|
|
2696
|
+
* followed and the final URL is on `response.url`. A non-2xx status is
|
|
2697
|
+
* returned, not thrown; `response.raiseForStatus()` throws `PageFetchHTTPError`.
|
|
2698
|
+
*
|
|
2699
|
+
* `json` is serialised as the body with an `application/json` content type,
|
|
2700
|
+
* `data` as a form body when it is an object or verbatim when it is a string.
|
|
2701
|
+
*
|
|
2702
|
+
* @param url - Absolute URL or a path relative to the current page.
|
|
2703
|
+
* @param options - HTTP method, headers, and request body settings.
|
|
2704
|
+
* @returns A response with status, headers, and body readers. Non-2xx responses are not automatically thrown.
|
|
2705
|
+
* @throws ActionExecutionError if browser-side execution fails.
|
|
2706
|
+
* @example
|
|
2707
|
+
* await session.execute({ type: 'goto', url: 'https://en.wikipedia.org/wiki/Main_Page' });
|
|
2708
|
+
* const summary = await (await session.fetch('/api/rest_v1/page/summary/Main_Page')).json();
|
|
2709
|
+
*/
|
|
2710
|
+
async fetch(url, options = {}) {
|
|
2711
|
+
const script = buildFetchScript(url, options);
|
|
2712
|
+
return responseFromEvaluated(await this.evaluateJs(script));
|
|
2713
|
+
}
|
|
2714
|
+
/**
|
|
2715
|
+
* Observe the current page: the list of actions that can be taken, a
|
|
2716
|
+
* screenshot and page metadata.
|
|
2717
|
+
*
|
|
2718
|
+
* The `perception_type` defaults to the session option (`fast`). A bare
|
|
2719
|
+
* string argument is accepted as the perception type.
|
|
2720
|
+
*
|
|
2721
|
+
* @param options - Observation settings, including an optional URL, or a perception-mode string.
|
|
2722
|
+
* @returns Structured page analysis, available actions, and page metadata.
|
|
2723
|
+
* @throws NotteAPIError if observation fails.
|
|
2724
|
+
* @example
|
|
2725
|
+
* const obs = await session.observe();
|
|
2726
|
+
* console.log(obs.space.description);
|
|
2727
|
+
* const deep = await session.observe({ perception_type: 'deep', max_nb_actions: 50 });
|
|
2728
|
+
*/
|
|
2729
|
+
async observe(options = {}) {
|
|
2730
|
+
const body = typeof options === "string" ? { perception_type: options } : { ...options };
|
|
2731
|
+
if (body.perception_type == null) {
|
|
2732
|
+
body.perception_type = this.defaultPerceptionType;
|
|
1857
2733
|
}
|
|
2734
|
+
const response = await pageObserve({
|
|
2735
|
+
client: this.client.getClient(),
|
|
2736
|
+
path: { session_id: this.requireSessionId() },
|
|
2737
|
+
body,
|
|
2738
|
+
throwOnError: true
|
|
2739
|
+
});
|
|
2740
|
+
return response.data;
|
|
2741
|
+
}
|
|
2742
|
+
async scrape(options = {}) {
|
|
2743
|
+
const sessionId = this.requireSessionId();
|
|
2744
|
+
const body = await buildScrapeBody(options);
|
|
1858
2745
|
const response = await pageScrape({
|
|
1859
2746
|
client: this.client.getClient(),
|
|
1860
|
-
path: {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
body: apiOptions
|
|
2747
|
+
path: { session_id: sessionId },
|
|
2748
|
+
body,
|
|
2749
|
+
throwOnError: true
|
|
1864
2750
|
});
|
|
1865
|
-
if (response?.error) {
|
|
1866
|
-
throw new Error(`Failed to scrape page: ${JSON.stringify(response.error)}`);
|
|
1867
|
-
}
|
|
1868
2751
|
return processScrapeResponse(response.data, options);
|
|
1869
2752
|
}
|
|
2753
|
+
// #######################################################################
|
|
2754
|
+
// ############################# Lifecycle ###############################
|
|
2755
|
+
// #######################################################################
|
|
1870
2756
|
/**
|
|
1871
|
-
* Context manager pattern
|
|
1872
|
-
*
|
|
2757
|
+
* Context manager pattern: start the session, run the callback and stop the
|
|
2758
|
+
* session, with close reason `error` when the callback throws.
|
|
2759
|
+
*
|
|
2760
|
+
* @param callback - Async work to perform with the started session.
|
|
2761
|
+
* @returns The callback's return value after session cleanup has been attempted.
|
|
2762
|
+
* @throws Error if startup fails, the callback throws, or cleanup fails after a successful callback.
|
|
2763
|
+
* @example
|
|
2764
|
+
* await client.Session().use(async session => {
|
|
2765
|
+
* await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
|
|
2766
|
+
* });
|
|
1873
2767
|
*/
|
|
1874
2768
|
async use(callback) {
|
|
1875
2769
|
await this.start();
|
|
@@ -1878,6 +2772,7 @@ var Session = class {
|
|
|
1878
2772
|
return await callback(this);
|
|
1879
2773
|
} catch (error) {
|
|
1880
2774
|
callbackFailed = true;
|
|
2775
|
+
console.warn(`Session exiting because of exception: ${String(error)}`);
|
|
1881
2776
|
throw error;
|
|
1882
2777
|
} finally {
|
|
1883
2778
|
try {
|
|
@@ -1891,8 +2786,13 @@ var Session = class {
|
|
|
1891
2786
|
}
|
|
1892
2787
|
}
|
|
1893
2788
|
/**
|
|
1894
|
-
* Async iterator pattern for session lifecycle management
|
|
1895
|
-
*
|
|
2789
|
+
* Async iterator pattern for session lifecycle management.
|
|
2790
|
+
*
|
|
2791
|
+
* ```ts
|
|
2792
|
+
* for await (const session of client.Session()) {
|
|
2793
|
+
* await session.execute({ type: 'goto', url: 'https://www.notte.cc' });
|
|
2794
|
+
* }
|
|
2795
|
+
* ```
|
|
1896
2796
|
*/
|
|
1897
2797
|
async *[Symbol.asyncIterator]() {
|
|
1898
2798
|
await this.start();
|
|
@@ -1906,12 +2806,12 @@ var Session = class {
|
|
|
1906
2806
|
|
|
1907
2807
|
// src/agent.ts
|
|
1908
2808
|
import WebSocket from "ws";
|
|
1909
|
-
import { z
|
|
2809
|
+
import { z } from "zod";
|
|
1910
2810
|
function prepareAgentRequest(data) {
|
|
1911
2811
|
const apiData = { ...data };
|
|
1912
2812
|
const rf = data.response_format;
|
|
1913
2813
|
if (rf && typeof rf.parse === "function" && typeof rf.safeParse === "function") {
|
|
1914
|
-
apiData.response_format =
|
|
2814
|
+
apiData.response_format = z.toJSONSchema(rf);
|
|
1915
2815
|
return { apiData, zodSchema: rf };
|
|
1916
2816
|
}
|
|
1917
2817
|
return { apiData, zodSchema: null };
|
|
@@ -2017,9 +2917,12 @@ var defaultStepLogger = (stepData, stepCounter, agentId) => {
|
|
|
2017
2917
|
console.log("");
|
|
2018
2918
|
};
|
|
2019
2919
|
var Agent = class {
|
|
2920
|
+
client;
|
|
2921
|
+
request;
|
|
2922
|
+
response = null;
|
|
2923
|
+
websocket = null;
|
|
2924
|
+
existingAgent;
|
|
2020
2925
|
constructor(client2, options) {
|
|
2021
|
-
this.response = null;
|
|
2022
|
-
this.websocket = null;
|
|
2023
2926
|
this.client = client2;
|
|
2024
2927
|
if ("agent_id" in options && options.agent_id) {
|
|
2025
2928
|
if (options.session) {
|
|
@@ -2106,22 +3009,29 @@ var Agent = class {
|
|
|
2106
3009
|
if (!this.response) {
|
|
2107
3010
|
throw new Error("Agent not started");
|
|
2108
3011
|
}
|
|
3012
|
+
const deadline = Date.now() + 3e5;
|
|
2109
3013
|
try {
|
|
2110
|
-
const result = await this.watchLogs(log, updateHandler);
|
|
2111
|
-
if (result) {
|
|
3014
|
+
const result = await this.watchLogs(log, updateHandler, deadline);
|
|
3015
|
+
if (result && result.status !== "active") {
|
|
2112
3016
|
return result;
|
|
2113
3017
|
}
|
|
2114
|
-
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
2115
|
-
return await this.status();
|
|
2116
3018
|
} catch (error) {
|
|
2117
|
-
console.
|
|
2118
|
-
|
|
3019
|
+
console.warn(`[Agent] ${this.agentId} log stream unavailable; polling for completion.`);
|
|
3020
|
+
}
|
|
3021
|
+
while (Date.now() < deadline) {
|
|
3022
|
+
const result = await this.status(AbortSignal.timeout(Math.max(1, deadline - Date.now())));
|
|
3023
|
+
if (result.status !== "active") {
|
|
3024
|
+
updateHandler?.({ type: "completion", data: result, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3025
|
+
return result;
|
|
3026
|
+
}
|
|
3027
|
+
await new Promise((resolve3) => setTimeout(resolve3, 1e3));
|
|
2119
3028
|
}
|
|
3029
|
+
throw new Error(`Agent ${this.agentId} did not complete before the polling timeout; retrieve status() later.`);
|
|
2120
3030
|
}
|
|
2121
3031
|
/**
|
|
2122
3032
|
* Watch logs via WebSocket - mirrors Python watch_logs method
|
|
2123
3033
|
*/
|
|
2124
|
-
async watchLogs(log = true, updateHandler = null) {
|
|
3034
|
+
async watchLogs(log = true, updateHandler = null, deadline = Date.now() + 3e5) {
|
|
2125
3035
|
if (this.existingAgent) {
|
|
2126
3036
|
throw new Error("You cannot call watchLogs() on an agent instantiated from agent id");
|
|
2127
3037
|
}
|
|
@@ -2129,18 +3039,39 @@ var Agent = class {
|
|
|
2129
3039
|
throw new Error("Agent not started");
|
|
2130
3040
|
}
|
|
2131
3041
|
const config = this.client.getConfig();
|
|
2132
|
-
|
|
3042
|
+
if (config.baseUrl?.startsWith("/")) return null;
|
|
2133
3043
|
const agentId = this.response.agent_id;
|
|
2134
3044
|
const sessionId = this.response.session_id;
|
|
2135
|
-
const
|
|
2136
|
-
|
|
3045
|
+
const debug = await sessionDebugInfo({
|
|
3046
|
+
client: this.client.getClient(),
|
|
3047
|
+
path: { session_id: sessionId },
|
|
3048
|
+
throwOnError: true,
|
|
3049
|
+
signal: AbortSignal.timeout(Math.max(1, deadline - Date.now()))
|
|
3050
|
+
});
|
|
3051
|
+
const wsUrl = new URL(this.client.withDbPreview(debug.data.ws.logs));
|
|
3052
|
+
const token = wsUrl.searchParams.get("token");
|
|
3053
|
+
if (!token || token === config.apiKey || wsUrl.protocol !== "wss:") return null;
|
|
3054
|
+
wsUrl.pathname = `/agents/${encodeURIComponent(agentId)}/debug/logs`;
|
|
3055
|
+
wsUrl.searchParams.set("session_id", sessionId);
|
|
3056
|
+
return new Promise((resolve3, reject) => {
|
|
2137
3057
|
this.websocket = new WebSocket(wsUrl);
|
|
3058
|
+
const socket = this.websocket;
|
|
3059
|
+
const timeout = setTimeout(() => finish(null), Math.max(0, deadline - Date.now()));
|
|
3060
|
+
let settled = false;
|
|
3061
|
+
const finish = (result) => {
|
|
3062
|
+
if (settled) return;
|
|
3063
|
+
settled = true;
|
|
3064
|
+
clearTimeout(timeout);
|
|
3065
|
+
if (this.websocket === socket) this.websocket = null;
|
|
3066
|
+
if (socket.readyState !== WebSocket.CLOSED) socket.terminate();
|
|
3067
|
+
resolve3(result);
|
|
3068
|
+
};
|
|
2138
3069
|
let counter = 0;
|
|
2139
3070
|
this.websocket.on("open", () => {
|
|
2140
3071
|
});
|
|
2141
3072
|
this.websocket.on("error", (error) => {
|
|
2142
3073
|
console.error(`Connection error: ${agentId} ${error.message}`);
|
|
2143
|
-
|
|
3074
|
+
finish(null);
|
|
2144
3075
|
});
|
|
2145
3076
|
this.websocket.on("message", (data) => {
|
|
2146
3077
|
try {
|
|
@@ -2162,8 +3093,7 @@ var Agent = class {
|
|
|
2162
3093
|
} else if (!finalStatus.success) {
|
|
2163
3094
|
console.error(finalStatus.answer);
|
|
2164
3095
|
}
|
|
2165
|
-
|
|
2166
|
-
resolve(finalStatus);
|
|
3096
|
+
finish(finalStatus);
|
|
2167
3097
|
return;
|
|
2168
3098
|
}
|
|
2169
3099
|
if (log) {
|
|
@@ -2183,26 +3113,20 @@ var Agent = class {
|
|
|
2183
3113
|
}
|
|
2184
3114
|
});
|
|
2185
3115
|
this.websocket.on("close", () => {
|
|
2186
|
-
|
|
2187
|
-
resolve(null);
|
|
3116
|
+
finish(null);
|
|
2188
3117
|
});
|
|
2189
|
-
setTimeout(() => {
|
|
2190
|
-
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
2191
|
-
this.disconnectWebSocket();
|
|
2192
|
-
resolve(null);
|
|
2193
|
-
}
|
|
2194
|
-
}, 3e5);
|
|
2195
3118
|
});
|
|
2196
3119
|
}
|
|
2197
3120
|
/**
|
|
2198
3121
|
* Get agent status - mirrors Python status method
|
|
2199
3122
|
*/
|
|
2200
|
-
async status() {
|
|
3123
|
+
async status(signal) {
|
|
2201
3124
|
if (!this.response) {
|
|
2202
3125
|
throw new Error("Agent not started or not accessible");
|
|
2203
3126
|
}
|
|
2204
3127
|
const response = await agentStatus({
|
|
2205
3128
|
client: this.client.getClient(),
|
|
3129
|
+
signal,
|
|
2206
3130
|
path: {
|
|
2207
3131
|
agent_id: this.response.agent_id
|
|
2208
3132
|
}
|
|
@@ -2271,24 +3195,126 @@ var Agent = class {
|
|
|
2271
3195
|
return this.response.session_id;
|
|
2272
3196
|
}
|
|
2273
3197
|
/**
|
|
2274
|
-
*
|
|
3198
|
+
* Whether `start()`/`run()` was called on this instance (or it was built from an agent id).
|
|
3199
|
+
* This is a local check and does not contact the API.
|
|
2275
3200
|
*/
|
|
2276
|
-
|
|
3201
|
+
hasStarted() {
|
|
2277
3202
|
return this.response !== null;
|
|
2278
3203
|
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Whether the agent is still active on the server. Fetches the current status,
|
|
3206
|
+
* like calling `agent.status()` in Python; an agent that was never started is not active.
|
|
3207
|
+
*/
|
|
3208
|
+
async isActive() {
|
|
3209
|
+
if (!this.response) {
|
|
3210
|
+
return false;
|
|
3211
|
+
}
|
|
3212
|
+
const status = await this.status();
|
|
3213
|
+
return status.status === "active";
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* @deprecated Only reports whether this instance was started; it does not
|
|
3217
|
+
* contact the API. Use `hasStarted()` for that check or `isActive()` for the
|
|
3218
|
+
* live server status.
|
|
3219
|
+
*/
|
|
3220
|
+
isRunning() {
|
|
3221
|
+
return this.hasStarted();
|
|
3222
|
+
}
|
|
2279
3223
|
};
|
|
2280
3224
|
|
|
2281
3225
|
// src/vaults.ts
|
|
3226
|
+
import { getDomain } from "tldts";
|
|
3227
|
+
var CREDENTIAL_FIELDS = ["email", "username", "mfa_secret", "password"];
|
|
3228
|
+
function extractHostname(url) {
|
|
3229
|
+
const trimmed = url.trim();
|
|
3230
|
+
if (trimmed.length === 0) {
|
|
3231
|
+
return "";
|
|
3232
|
+
}
|
|
3233
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
|
3234
|
+
const candidates = hasScheme ? [trimmed] : [`https://${trimmed}`];
|
|
3235
|
+
for (const candidate of candidates) {
|
|
3236
|
+
try {
|
|
3237
|
+
const { hostname } = new URL(candidate);
|
|
3238
|
+
if (hostname.length > 0 && !hostname.startsWith(".")) {
|
|
3239
|
+
return hostname.toLowerCase();
|
|
3240
|
+
}
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
return "";
|
|
3245
|
+
}
|
|
3246
|
+
function getRootDomain(url) {
|
|
3247
|
+
const hostname = extractHostname(url);
|
|
3248
|
+
if (hostname.length === 0) {
|
|
3249
|
+
return "";
|
|
3250
|
+
}
|
|
3251
|
+
if (hostname.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
|
|
3252
|
+
return hostname;
|
|
3253
|
+
}
|
|
3254
|
+
const domain = getDomain(hostname, { allowPrivateDomains: false, detectIp: false });
|
|
3255
|
+
if (domain) {
|
|
3256
|
+
return domain.toLowerCase();
|
|
3257
|
+
}
|
|
3258
|
+
return hostname.toLowerCase();
|
|
3259
|
+
}
|
|
3260
|
+
function validateUrl(url) {
|
|
3261
|
+
const domain = getRootDomain(url);
|
|
3262
|
+
if (domain.length === 0) {
|
|
3263
|
+
throw new InvalidRequestError(`Invalid URL: ${url}. Please provide a valid URL with a domain name.`);
|
|
3264
|
+
}
|
|
3265
|
+
return domain;
|
|
3266
|
+
}
|
|
3267
|
+
function isValidMfaSecret(secret) {
|
|
3268
|
+
if (secret.length === 0) {
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
let padded = secret.toUpperCase();
|
|
3272
|
+
if (padded.length % 8 !== 0) {
|
|
3273
|
+
padded += "=".repeat(8 - padded.length % 8);
|
|
3274
|
+
}
|
|
3275
|
+
let end = padded.length;
|
|
3276
|
+
while (end > 0 && padded[end - 1] === "=") {
|
|
3277
|
+
end -= 1;
|
|
3278
|
+
}
|
|
3279
|
+
const stripped = padded.slice(0, end);
|
|
3280
|
+
const padding = padded.length - stripped.length;
|
|
3281
|
+
return /^[A-Z2-7]*$/.test(stripped) && [0, 1, 3, 4, 6].includes(padding);
|
|
3282
|
+
}
|
|
3283
|
+
function validateMfaSecret(secret) {
|
|
3284
|
+
if (!isValidMfaSecret(secret)) {
|
|
3285
|
+
throw new InvalidRequestError("Invalid MFA secret code: did you try to store an OTP instead of a secret?");
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function validateCredentials(credentials) {
|
|
3289
|
+
const hasUsername = credentials.username !== void 0 && credentials.username !== null;
|
|
3290
|
+
const hasEmail = credentials.email !== void 0 && credentials.email !== null;
|
|
3291
|
+
if (hasUsername && hasEmail) {
|
|
3292
|
+
throw new InvalidRequestError("Can only set either username or email");
|
|
3293
|
+
}
|
|
3294
|
+
if (!hasUsername && !hasEmail) {
|
|
3295
|
+
throw new InvalidRequestError("Need to have either username or email set");
|
|
3296
|
+
}
|
|
3297
|
+
if (credentials.mfa_secret !== void 0 && credentials.mfa_secret !== null) {
|
|
3298
|
+
validateMfaSecret(credentials.mfa_secret);
|
|
3299
|
+
}
|
|
3300
|
+
return credentials;
|
|
3301
|
+
}
|
|
2282
3302
|
var NotteVault = class {
|
|
3303
|
+
client;
|
|
3304
|
+
_vaultId = null;
|
|
3305
|
+
initPromise = null;
|
|
3306
|
+
createData = null;
|
|
2283
3307
|
constructor(client2, options = {}) {
|
|
2284
|
-
this._vaultId = null;
|
|
2285
|
-
this.initPromise = null;
|
|
2286
|
-
this.createData = null;
|
|
2287
3308
|
this.client = client2;
|
|
2288
|
-
if (options && "vault_id" in options && options.vault_id) {
|
|
3309
|
+
if (options && "vault_id" in options && options.vault_id !== void 0) {
|
|
3310
|
+
if (options.vault_id.length === 0) {
|
|
3311
|
+
throw new InvalidRequestError("Vault ID cannot be empty");
|
|
3312
|
+
}
|
|
2289
3313
|
this.initPromise = this.initExistingVault(options.vault_id);
|
|
3314
|
+
this.initPromise.catch(() => void 0);
|
|
2290
3315
|
} else {
|
|
2291
|
-
|
|
3316
|
+
const { vault_id: _ignored, ...createData } = options ?? {};
|
|
3317
|
+
this.createData = createData;
|
|
2292
3318
|
}
|
|
2293
3319
|
}
|
|
2294
3320
|
async initExistingVault(vaultId) {
|
|
@@ -2299,29 +3325,20 @@ var NotteVault = class {
|
|
|
2299
3325
|
async initNewVault(createData) {
|
|
2300
3326
|
const response = await vaultCreate({
|
|
2301
3327
|
client: this.client.getClient(),
|
|
2302
|
-
body: createData
|
|
3328
|
+
body: createData,
|
|
3329
|
+
throwOnError: true
|
|
2303
3330
|
});
|
|
2304
|
-
if (response?.error) {
|
|
2305
|
-
throw new Error(`Failed to create vault: ${formatError(response.error)}`);
|
|
2306
|
-
}
|
|
2307
3331
|
const vault = response.data;
|
|
2308
3332
|
console.warn(`[Vault] ${vault.vault_id} created since no vault id was provided. Please store this to retrieve it later.`);
|
|
2309
3333
|
this._vaultId = vault.vault_id;
|
|
2310
3334
|
return this._vaultId;
|
|
2311
3335
|
}
|
|
2312
3336
|
async verifyVaultExists(vaultId) {
|
|
2313
|
-
|
|
2314
|
-
throw new Error("Vault ID cannot be empty");
|
|
2315
|
-
}
|
|
2316
|
-
const response = await vaultCredentialsList({
|
|
3337
|
+
await vaultCredentialsList({
|
|
2317
3338
|
client: this.client.getClient(),
|
|
2318
|
-
path: {
|
|
2319
|
-
|
|
2320
|
-
}
|
|
3339
|
+
path: { vault_id: vaultId },
|
|
3340
|
+
throwOnError: true
|
|
2321
3341
|
});
|
|
2322
|
-
if (response?.error) {
|
|
2323
|
-
throw new Error(`Failed to verify vault exists: ${formatError(response.error)}`);
|
|
2324
|
-
}
|
|
2325
3342
|
}
|
|
2326
3343
|
async ensureInitialized() {
|
|
2327
3344
|
if (this._vaultId) {
|
|
@@ -2336,257 +3353,215 @@ var NotteVault = class {
|
|
|
2336
3353
|
this.initPromise = this.initNewVault(createData);
|
|
2337
3354
|
return await this.initPromise;
|
|
2338
3355
|
}
|
|
2339
|
-
throw new
|
|
3356
|
+
throw new InvalidRequestError("Vault not initialized");
|
|
2340
3357
|
}
|
|
3358
|
+
/** ID of the remote vault. Throws until the first API operation has created or verified it. */
|
|
2341
3359
|
get vaultId() {
|
|
2342
3360
|
if (!this._vaultId) {
|
|
2343
|
-
throw new
|
|
3361
|
+
throw new InvalidRequestError("Vault not initialized. Await a vault operation first.");
|
|
2344
3362
|
}
|
|
2345
3363
|
return this._vaultId;
|
|
2346
3364
|
}
|
|
2347
3365
|
/**
|
|
2348
|
-
* Start the vault -
|
|
3366
|
+
* Start the vault. No-op kept for parity with Python's `SyncResource` interface.
|
|
2349
3367
|
*/
|
|
2350
3368
|
start() {
|
|
2351
3369
|
}
|
|
2352
3370
|
/**
|
|
2353
|
-
* Stop the vault
|
|
3371
|
+
* Stop the vault: deletes it together with every credential it holds.
|
|
3372
|
+
*
|
|
3373
|
+
* ```ts
|
|
3374
|
+
* await vault.stop();
|
|
3375
|
+
* ```
|
|
2354
3376
|
*/
|
|
2355
3377
|
async stop() {
|
|
2356
3378
|
const vaultId = await this.ensureInitialized();
|
|
2357
|
-
console.
|
|
3379
|
+
console.info(`[Vault] ${vaultId} deleted. All credentials have been deleted.`);
|
|
2358
3380
|
await this.delete();
|
|
2359
3381
|
}
|
|
2360
3382
|
/**
|
|
2361
|
-
* Add or update credentials for a URL
|
|
3383
|
+
* Add or update the credentials stored for a website. The URL is reduced to
|
|
3384
|
+
* its root domain (`https://github.com/login` → `github.com`) before it is
|
|
3385
|
+
* sent, like Python's `AddCredentialsRequest`.
|
|
3386
|
+
*
|
|
3387
|
+
* ```ts
|
|
3388
|
+
* await vault.addCredentials('https://github.com/', {
|
|
3389
|
+
* email: 'me@example.org',
|
|
3390
|
+
* password: 'secret', // pragma: allowlist secret
|
|
3391
|
+
* mfa_secret: 'JBSWY3DPEHPK3PXP', // pragma: allowlist secret
|
|
3392
|
+
* });
|
|
3393
|
+
* ```
|
|
2362
3394
|
*/
|
|
2363
3395
|
async addCredentials(url, credentials) {
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
3396
|
+
const domain = validateUrl(url);
|
|
3397
|
+
validateCredentials(credentials);
|
|
2367
3398
|
const vaultId = await this.ensureInitialized();
|
|
2368
|
-
|
|
3399
|
+
await vaultCredentialsAdd({
|
|
2369
3400
|
client: this.client.getClient(),
|
|
2370
|
-
path: {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
body: { url, credentials }
|
|
3401
|
+
path: { vault_id: vaultId },
|
|
3402
|
+
body: { url: domain, credentials },
|
|
3403
|
+
throwOnError: true
|
|
2374
3404
|
});
|
|
2375
|
-
if (response?.error) {
|
|
2376
|
-
throw new Error(`Failed to add credentials: ${formatError(response.error)}`);
|
|
2377
|
-
}
|
|
2378
3405
|
}
|
|
2379
3406
|
/**
|
|
2380
|
-
* Get credentials for a
|
|
3407
|
+
* Get the credentials stored for a website. Rejects with a `NotteAPIError`
|
|
3408
|
+
* when the vault holds no credentials for that domain, like Python's
|
|
3409
|
+
* `get_credentials`.
|
|
3410
|
+
*
|
|
3411
|
+
* ```ts
|
|
3412
|
+
* const { email, password } = await vault.getCredentials('https://github.com/login');
|
|
3413
|
+
* ```
|
|
2381
3414
|
*/
|
|
2382
3415
|
async getCredentials(url) {
|
|
2383
|
-
|
|
2384
|
-
const vaultId = await this.ensureInitialized();
|
|
2385
|
-
const response = await vaultCredentialsGet({
|
|
2386
|
-
client: this.client.getClient(),
|
|
2387
|
-
path: {
|
|
2388
|
-
vault_id: vaultId
|
|
2389
|
-
},
|
|
2390
|
-
query: { url }
|
|
2391
|
-
});
|
|
2392
|
-
if (response?.error) {
|
|
2393
|
-
return null;
|
|
2394
|
-
}
|
|
2395
|
-
const credentialsResponse = response.data;
|
|
2396
|
-
return credentialsResponse.credentials;
|
|
2397
|
-
} catch (error) {
|
|
2398
|
-
return null;
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
/**
|
|
2402
|
-
* Delete credentials for a URL
|
|
2403
|
-
*/
|
|
2404
|
-
async deleteCredentials(url) {
|
|
3416
|
+
const domain = validateUrl(url);
|
|
2405
3417
|
const vaultId = await this.ensureInitialized();
|
|
2406
|
-
const response = await
|
|
3418
|
+
const response = await vaultCredentialsGet({
|
|
2407
3419
|
client: this.client.getClient(),
|
|
2408
|
-
path: {
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
query: { url }
|
|
3420
|
+
path: { vault_id: vaultId },
|
|
3421
|
+
query: { url: domain },
|
|
3422
|
+
throwOnError: true
|
|
2412
3423
|
});
|
|
2413
|
-
|
|
2414
|
-
throw new Error(`Failed to delete credentials: ${formatError(response.error)}`);
|
|
2415
|
-
}
|
|
3424
|
+
return response.data.credentials;
|
|
2416
3425
|
}
|
|
2417
3426
|
/**
|
|
2418
|
-
*
|
|
3427
|
+
* Check whether the vault holds credentials for a website, the counterpart
|
|
3428
|
+
* of `BaseVault.has_credential`. Never rejects for a missing credential.
|
|
3429
|
+
*
|
|
3430
|
+
* ```ts
|
|
3431
|
+
* if (!(await vault.hasCredential('https://github.com/'))) {
|
|
3432
|
+
* await vault.addCredentialsFromEnv('https://github.com/');
|
|
3433
|
+
* }
|
|
3434
|
+
* ```
|
|
2419
3435
|
*/
|
|
2420
|
-
async
|
|
2421
|
-
const
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
path: {
|
|
2425
|
-
vault_id: vaultId
|
|
2426
|
-
},
|
|
2427
|
-
body: { credit_card: creditCard }
|
|
2428
|
-
});
|
|
2429
|
-
if (response?.error) {
|
|
2430
|
-
throw new Error(`Failed to set credit card: ${formatError(response.error)}`);
|
|
2431
|
-
}
|
|
3436
|
+
async hasCredential(url) {
|
|
3437
|
+
const domain = getRootDomain(url);
|
|
3438
|
+
const credentials = await this.listCredentials();
|
|
3439
|
+
return credentials.some((credential) => credential.url === url || credential.url === domain);
|
|
2432
3440
|
}
|
|
2433
3441
|
/**
|
|
2434
|
-
*
|
|
3442
|
+
* Delete the credentials stored for a website.
|
|
3443
|
+
*
|
|
3444
|
+
* ```ts
|
|
3445
|
+
* await vault.deleteCredentials('https://github.com/');
|
|
3446
|
+
* ```
|
|
2435
3447
|
*/
|
|
2436
|
-
async
|
|
3448
|
+
async deleteCredentials(url) {
|
|
3449
|
+
const domain = validateUrl(url);
|
|
2437
3450
|
const vaultId = await this.ensureInitialized();
|
|
2438
|
-
|
|
3451
|
+
await vaultCredentialsDelete({
|
|
2439
3452
|
client: this.client.getClient(),
|
|
2440
|
-
path: {
|
|
2441
|
-
|
|
2442
|
-
|
|
3453
|
+
path: { vault_id: vaultId },
|
|
3454
|
+
query: { url: domain },
|
|
3455
|
+
throwOnError: true
|
|
2443
3456
|
});
|
|
2444
|
-
if (response?.error) {
|
|
2445
|
-
throw new Error(`Failed to get credit card: ${formatError(response.error)}`);
|
|
2446
|
-
}
|
|
2447
|
-
const creditCardResponse = response.data;
|
|
2448
|
-
return creditCardResponse.credit_card;
|
|
2449
3457
|
}
|
|
2450
3458
|
/**
|
|
2451
|
-
* List
|
|
3459
|
+
* List the websites the vault holds credentials for. Passwords are never returned.
|
|
3460
|
+
*
|
|
3461
|
+
* ```ts
|
|
3462
|
+
* const credentials = await vault.listCredentials();
|
|
3463
|
+
* console.log(credentials.map(c => c.url));
|
|
3464
|
+
* ```
|
|
2452
3465
|
*/
|
|
2453
3466
|
async listCredentials() {
|
|
2454
3467
|
const vaultId = await this.ensureInitialized();
|
|
2455
3468
|
const response = await vaultCredentialsList({
|
|
2456
3469
|
client: this.client.getClient(),
|
|
2457
|
-
path: {
|
|
2458
|
-
|
|
2459
|
-
}
|
|
2460
|
-
});
|
|
2461
|
-
if (response?.error) {
|
|
2462
|
-
throw new Error(`Failed to list credentials: ${formatError(response.error)}`);
|
|
2463
|
-
}
|
|
2464
|
-
const credentialsResponse = response.data;
|
|
2465
|
-
return credentialsResponse.credentials;
|
|
2466
|
-
}
|
|
2467
|
-
/**
|
|
2468
|
-
* Delete credit card information
|
|
2469
|
-
*/
|
|
2470
|
-
async deleteCreditCard() {
|
|
2471
|
-
const vaultId = await this.ensureInitialized();
|
|
2472
|
-
const response = await vaultCreditCardDelete({
|
|
2473
|
-
client: this.client.getClient(),
|
|
2474
|
-
path: {
|
|
2475
|
-
vault_id: vaultId
|
|
2476
|
-
}
|
|
3470
|
+
path: { vault_id: vaultId },
|
|
3471
|
+
throwOnError: true
|
|
2477
3472
|
});
|
|
2478
|
-
|
|
2479
|
-
throw new Error(`Failed to delete credit card: ${formatError(response.error)}`);
|
|
2480
|
-
}
|
|
3473
|
+
return response.data.credentials;
|
|
2481
3474
|
}
|
|
2482
3475
|
/**
|
|
2483
|
-
* Delete the
|
|
3476
|
+
* Delete the vault and every credential it holds.
|
|
2484
3477
|
*/
|
|
2485
3478
|
async delete() {
|
|
2486
3479
|
const vaultId = await this.ensureInitialized();
|
|
2487
|
-
|
|
3480
|
+
await vaultDelete({
|
|
2488
3481
|
client: this.client.getClient(),
|
|
2489
|
-
path: {
|
|
2490
|
-
|
|
2491
|
-
}
|
|
3482
|
+
path: { vault_id: vaultId },
|
|
3483
|
+
throwOnError: true
|
|
2492
3484
|
});
|
|
2493
|
-
if (response?.error) {
|
|
2494
|
-
throw new Error(`Failed to delete vault: ${formatError(response.error)}`);
|
|
2495
|
-
}
|
|
2496
3485
|
}
|
|
2497
3486
|
/**
|
|
2498
|
-
* Add credentials from environment variables
|
|
2499
|
-
*
|
|
3487
|
+
* Add credentials read from environment variables, the counterpart of
|
|
3488
|
+
* `BaseVault.add_credentials_from_env`. For `https://github.com/` the
|
|
3489
|
+
* variables are `GITHUB_COM_EMAIL`, `GITHUB_COM_USERNAME`,
|
|
3490
|
+
* `GITHUB_COM_MFA_SECRET` and `GITHUB_COM_PASSWORD`.
|
|
3491
|
+
*
|
|
3492
|
+
* ```ts
|
|
3493
|
+
* process.env.GITHUB_COM_EMAIL = 'me@example.org';
|
|
3494
|
+
* process.env.GITHUB_COM_PASSWORD = 'secret'; // pragma: allowlist secret
|
|
3495
|
+
* await vault.addCredentialsFromEnv('https://github.com/');
|
|
3496
|
+
* ```
|
|
2500
3497
|
*/
|
|
2501
3498
|
async addCredentialsFromEnv(url) {
|
|
2502
|
-
const
|
|
2503
|
-
const
|
|
2504
|
-
const
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
3499
|
+
const rootDomain = validateUrl(url);
|
|
3500
|
+
const prefix = rootDomain.replace(/\./g, "_").toUpperCase();
|
|
3501
|
+
const credentials = {};
|
|
3502
|
+
const variableNames = [];
|
|
3503
|
+
for (const field of CREDENTIAL_FIELDS) {
|
|
3504
|
+
const variable = `${prefix}_${field.toUpperCase()}`;
|
|
3505
|
+
variableNames.push(variable);
|
|
3506
|
+
const value = process.env[variable];
|
|
3507
|
+
if (value !== void 0) {
|
|
3508
|
+
credentials[field] = value;
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
if (Object.keys(credentials).length === 0) {
|
|
3512
|
+
throw new InvalidRequestError(
|
|
3513
|
+
`No credentials found in the environment for ${url}. Please set the following variables: ${variableNames.join(", ")}`
|
|
3514
|
+
);
|
|
2514
3515
|
}
|
|
2515
3516
|
await this.addCredentials(url, credentials);
|
|
2516
3517
|
}
|
|
2517
3518
|
/**
|
|
2518
|
-
*
|
|
3519
|
+
* Generate a secure random password, the counterpart of Python's
|
|
3520
|
+
* `generate_password`. The result always contains a lowercase letter, an
|
|
3521
|
+
* uppercase letter and a digit, plus a special character unless
|
|
3522
|
+
* `includeSpecialChars` is false.
|
|
3523
|
+
*
|
|
3524
|
+
* ```ts
|
|
3525
|
+
* const password = vault.generatePassword(24);
|
|
3526
|
+
* ```
|
|
2519
3527
|
*/
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
} catch {
|
|
2525
|
-
const match = url.match(/(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
|
|
2526
|
-
return match ? match[1] : url;
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
/**
|
|
2530
|
-
* Validate MFA secret format
|
|
2531
|
-
* Mirrors Python's MFA secret validation
|
|
2532
|
-
*/
|
|
2533
|
-
validateMfaSecret(mfaSecret) {
|
|
2534
|
-
if (/^\d+$/.test(mfaSecret)) {
|
|
2535
|
-
throw new Error("MFA secret cannot be all numbers. Please provide a valid base32 secret.");
|
|
2536
|
-
}
|
|
2537
|
-
if (mfaSecret.length < 16) {
|
|
2538
|
-
throw new Error("MFA secret must be at least 16 characters long");
|
|
2539
|
-
}
|
|
2540
|
-
}
|
|
2541
|
-
/**
|
|
2542
|
-
* Generate a secure random password
|
|
2543
|
-
* Mirrors Python's generate_password method
|
|
2544
|
-
*/
|
|
2545
|
-
generatePassword(length = 20, includeSpecialChars = true) {
|
|
2546
|
-
const minRequiredLength = includeSpecialChars ? 4 : 3;
|
|
2547
|
-
if (length < minRequiredLength) {
|
|
2548
|
-
throw new Error(`Password length must be at least ${minRequiredLength} characters`);
|
|
3528
|
+
generatePassword(length = 20, includeSpecialChars = true) {
|
|
3529
|
+
const minRequiredLength = includeSpecialChars ? 4 : 3;
|
|
3530
|
+
if (length < minRequiredLength) {
|
|
3531
|
+
throw new InvalidRequestError(`Password length must be at least ${minRequiredLength} characters`);
|
|
2549
3532
|
}
|
|
2550
3533
|
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
|
2551
3534
|
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
2552
3535
|
const digits = "0123456789";
|
|
2553
3536
|
const specialChars = "!@#$%^&*()_+-=[]|;:,.<>?";
|
|
2554
|
-
const
|
|
2555
|
-
const
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
const alphanumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
2564
|
-
passwordArray[i] = alphanumeric[Math.floor(Math.random() * alphanumeric.length)];
|
|
3537
|
+
const alphanumeric = uppercase + lowercase + digits;
|
|
3538
|
+
const allowedChars = includeSpecialChars ? `${alphanumeric}-_` : alphanumeric;
|
|
3539
|
+
const randomIndex = (size) => {
|
|
3540
|
+
const limit = Math.floor(4294967296 / size) * size;
|
|
3541
|
+
const buffer = new Uint32Array(1);
|
|
3542
|
+
for (; ; ) {
|
|
3543
|
+
const [value] = globalThis.crypto.getRandomValues(buffer);
|
|
3544
|
+
if (value < limit) {
|
|
3545
|
+
return value % size;
|
|
2565
3546
|
}
|
|
2566
3547
|
}
|
|
2567
|
-
}
|
|
3548
|
+
};
|
|
3549
|
+
const passwordArray = Array.from({ length }, () => allowedChars[randomIndex(allowedChars.length)]);
|
|
3550
|
+
const pick = (chars) => chars[randomIndex(chars.length)];
|
|
2568
3551
|
let maxIterations = 10;
|
|
2569
3552
|
while (maxIterations-- > 0) {
|
|
2570
3553
|
let allRequirementsMet = true;
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
const index = Math.floor(Math.random() * passwordArray.length);
|
|
2583
|
-
passwordArray[index] = digits[Math.floor(Math.random() * digits.length)];
|
|
2584
|
-
allRequirementsMet = false;
|
|
2585
|
-
}
|
|
2586
|
-
if (includeSpecialChars && !passwordArray.some((c) => specialChars.includes(c))) {
|
|
2587
|
-
const index = Math.floor(Math.random() * passwordArray.length);
|
|
2588
|
-
passwordArray[index] = specialChars[Math.floor(Math.random() * specialChars.length)];
|
|
2589
|
-
allRequirementsMet = false;
|
|
3554
|
+
const requirements = [
|
|
3555
|
+
[true, lowercase],
|
|
3556
|
+
[true, uppercase],
|
|
3557
|
+
[true, digits],
|
|
3558
|
+
[includeSpecialChars, specialChars]
|
|
3559
|
+
];
|
|
3560
|
+
for (const [required, chars] of requirements) {
|
|
3561
|
+
if (required && !passwordArray.some((c) => chars.includes(c))) {
|
|
3562
|
+
passwordArray[randomIndex(passwordArray.length)] = pick(chars);
|
|
3563
|
+
allRequirementsMet = false;
|
|
3564
|
+
}
|
|
2590
3565
|
}
|
|
2591
3566
|
if (allRequirementsMet) {
|
|
2592
3567
|
break;
|
|
@@ -2598,10 +3573,15 @@ var NotteVault = class {
|
|
|
2598
3573
|
|
|
2599
3574
|
// src/personas.ts
|
|
2600
3575
|
var NottePersona = class {
|
|
3576
|
+
client;
|
|
3577
|
+
_initRequest;
|
|
3578
|
+
response = null;
|
|
3579
|
+
initPromise = null;
|
|
3580
|
+
_vault;
|
|
3581
|
+
ownsPersona;
|
|
2601
3582
|
constructor(client2, options = {}) {
|
|
2602
|
-
this.response = null;
|
|
2603
|
-
this.initPromise = null;
|
|
2604
3583
|
this.client = client2;
|
|
3584
|
+
this.ownsPersona = !("persona_id" in options && options.persona_id);
|
|
2605
3585
|
if (options && "persona_id" in options && options.persona_id) {
|
|
2606
3586
|
this.initPromise = this.initExistingPersona(options.persona_id);
|
|
2607
3587
|
this._initRequest = {};
|
|
@@ -2702,9 +3682,7 @@ var NottePersona = class {
|
|
|
2702
3682
|
* Start the persona - ensures initialization
|
|
2703
3683
|
*/
|
|
2704
3684
|
async start() {
|
|
2705
|
-
|
|
2706
|
-
await this.create();
|
|
2707
|
-
}
|
|
3685
|
+
await this.ensureInitialized();
|
|
2708
3686
|
}
|
|
2709
3687
|
/**
|
|
2710
3688
|
* Stop the persona - deletes it
|
|
@@ -2730,14 +3708,7 @@ var NottePersona = class {
|
|
|
2730
3708
|
if (this.response !== null) {
|
|
2731
3709
|
throw new Error(`Persona ${this.personaId} already initialized`);
|
|
2732
3710
|
}
|
|
2733
|
-
|
|
2734
|
-
client: this.client.getClient(),
|
|
2735
|
-
body: this._initRequest
|
|
2736
|
-
});
|
|
2737
|
-
if (response?.error) {
|
|
2738
|
-
throw new Error(`Failed to create persona: ${formatError(response.error)}`);
|
|
2739
|
-
}
|
|
2740
|
-
this.response = response.data;
|
|
3711
|
+
await this.ensureInitialized();
|
|
2741
3712
|
}
|
|
2742
3713
|
/**
|
|
2743
3714
|
* Delete the persona from the notte console
|
|
@@ -2813,13 +3784,15 @@ var NottePersona = class {
|
|
|
2813
3784
|
return this.response;
|
|
2814
3785
|
}
|
|
2815
3786
|
/**
|
|
2816
|
-
*
|
|
3787
|
+
* Run a callback and automatically delete a persona created by this instance.
|
|
3788
|
+
* Personas loaded by an existing ID are retained after the callback finishes.
|
|
2817
3789
|
*/
|
|
2818
3790
|
async use(callback) {
|
|
3791
|
+
await this.ensureInitialized();
|
|
2819
3792
|
try {
|
|
2820
3793
|
return await callback(this);
|
|
2821
3794
|
} finally {
|
|
2822
|
-
if (this.
|
|
3795
|
+
if (this.ownsPersona) {
|
|
2823
3796
|
try {
|
|
2824
3797
|
await this.delete();
|
|
2825
3798
|
} catch (error) {
|
|
@@ -2831,7 +3804,8 @@ var NottePersona = class {
|
|
|
2831
3804
|
};
|
|
2832
3805
|
|
|
2833
3806
|
// src/functions.ts
|
|
2834
|
-
import { writeFile } from "fs/promises";
|
|
3807
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
3808
|
+
import { basename } from "path";
|
|
2835
3809
|
|
|
2836
3810
|
// src/encryption.ts
|
|
2837
3811
|
import { createDecipheriv, createHmac, pbkdf2Sync, timingSafeEqual } from "crypto";
|
|
@@ -2843,6 +3817,7 @@ var FERNET_IV_LENGTH = 16;
|
|
|
2843
3817
|
var FERNET_HMAC_LENGTH = 32;
|
|
2844
3818
|
var FERNET_HEADER_LENGTH = 1 + 8 + FERNET_IV_LENGTH;
|
|
2845
3819
|
var Encryption = class {
|
|
3820
|
+
rootKey;
|
|
2846
3821
|
constructor(rootKey) {
|
|
2847
3822
|
if (!rootKey) {
|
|
2848
3823
|
throw new Error("Root key cannot be empty");
|
|
@@ -2884,141 +3859,385 @@ function decryptFernet(token, key) {
|
|
|
2884
3859
|
|
|
2885
3860
|
// src/functions.ts
|
|
2886
3861
|
var DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
3862
|
+
var FUNCTION_RUN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3863
|
+
var FUNCTION_RUN_ENDPOINTS = {
|
|
3864
|
+
/** `POST` creates a run record without executing it (`CreateFunctionRunRequest`). */
|
|
3865
|
+
create: "/functions/{function_id}/runs/create",
|
|
3866
|
+
/** `POST` executes a previously created run (`StartFunctionRunRequest`). */
|
|
3867
|
+
start: "/functions/{function_id}/runs/{run_id}"
|
|
3868
|
+
};
|
|
3869
|
+
var RUN_API_KEY_HEADER = "x-notte-api-key";
|
|
3870
|
+
var FUNCTION_RUNTIMES = ["standard", "extended"];
|
|
3871
|
+
var FUNCTION_RUN_STATUSES = ["closed", "active", "failed"];
|
|
2887
3872
|
var NotteFunction = class {
|
|
3873
|
+
client;
|
|
3874
|
+
_functionId = null;
|
|
3875
|
+
initPromise = null;
|
|
3876
|
+
createData = null;
|
|
3877
|
+
decryptionKey;
|
|
2888
3878
|
constructor(client2, options) {
|
|
2889
3879
|
this.client = client2;
|
|
2890
|
-
this.functionId = options.function_id;
|
|
2891
3880
|
this.decryptionKey = options.decryption_key;
|
|
3881
|
+
if (options.function_id !== void 0) {
|
|
3882
|
+
if (options.function_id.length === 0) {
|
|
3883
|
+
throw new InvalidRequestError("function_id cannot be empty");
|
|
3884
|
+
}
|
|
3885
|
+
this._functionId = options.function_id;
|
|
3886
|
+
} else if (options.path !== void 0) {
|
|
3887
|
+
assertPythonPath(options.path);
|
|
3888
|
+
this.createData = options;
|
|
3889
|
+
} else {
|
|
3890
|
+
throw new InvalidRequestError("Either 'function_id' or 'path' must be provided");
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
/** The function ID. Throws before a `path`-constructed function has been uploaded. */
|
|
3894
|
+
get functionId() {
|
|
3895
|
+
if (this._functionId === null) {
|
|
3896
|
+
throw new InvalidRequestError(
|
|
3897
|
+
"Function not initialized. Await a function operation (for example `await fn.get()`) before reading its ID."
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
return this._functionId;
|
|
3901
|
+
}
|
|
3902
|
+
/** Alias of the `functionId` accessor. */
|
|
3903
|
+
getFunctionId() {
|
|
3904
|
+
return this.functionId;
|
|
3905
|
+
}
|
|
3906
|
+
async ensureInitialized() {
|
|
3907
|
+
if (this._functionId !== null) {
|
|
3908
|
+
return this._functionId;
|
|
3909
|
+
}
|
|
3910
|
+
if (this.initPromise === null) {
|
|
3911
|
+
const createData = this.createData;
|
|
3912
|
+
this.createData = null;
|
|
3913
|
+
if (createData === null) {
|
|
3914
|
+
throw new InvalidRequestError("Function not initialized");
|
|
3915
|
+
}
|
|
3916
|
+
this.initPromise = this.createFunction(createData);
|
|
3917
|
+
}
|
|
3918
|
+
return this.initPromise;
|
|
3919
|
+
}
|
|
3920
|
+
async createFunction(data) {
|
|
3921
|
+
const file = await readPythonFile(data.path);
|
|
3922
|
+
const response = await functionCreate({
|
|
3923
|
+
client: this.client.getClient(),
|
|
3924
|
+
throwOnError: true,
|
|
3925
|
+
body: { file, name: data.name, description: data.description, shared: data.shared }
|
|
3926
|
+
});
|
|
3927
|
+
this._functionId = response.data.function_id;
|
|
3928
|
+
console.info(`[Function] ${this._functionId} created successfully.`);
|
|
3929
|
+
return this._functionId;
|
|
2892
3930
|
}
|
|
2893
3931
|
/**
|
|
2894
|
-
* Get metadata
|
|
3932
|
+
* Get the function metadata together with its download URL, the
|
|
3933
|
+
* counterpart of `client.functions.get()` in Python.
|
|
3934
|
+
*
|
|
3935
|
+
* @param options - Optional version selector; omitted means the latest version.
|
|
3936
|
+
* @returns Function metadata, version history, and the download link returned by the API.
|
|
3937
|
+
* @throws NotteAPIError if the function cannot be retrieved.
|
|
3938
|
+
* @example
|
|
3939
|
+
* const { latest_version, versions } = await fn.get();
|
|
2895
3940
|
*/
|
|
2896
|
-
async
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3941
|
+
async get(options = {}) {
|
|
3942
|
+
const functionId = await this.ensureInitialized();
|
|
3943
|
+
const response = await functionDownloadUrl({
|
|
3944
|
+
client: this.client.getClient(),
|
|
3945
|
+
throwOnError: true,
|
|
3946
|
+
path: { function_id: functionId },
|
|
3947
|
+
query: options.version !== void 0 ? { version: options.version } : {}
|
|
3948
|
+
});
|
|
3949
|
+
return response.data;
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Upload a new version of the code, mirroring `function.update(path=...)`.
|
|
3953
|
+
*
|
|
3954
|
+
* @param options - Local Python file path, optional version, and code-access settings.
|
|
3955
|
+
* @returns Updated function metadata, including its latest version.
|
|
3956
|
+
* @throws InvalidRequestError if the path is missing or does not end in `.py`.
|
|
3957
|
+
* @throws NotteAPIError if the upload is rejected.
|
|
3958
|
+
* @example
|
|
3959
|
+
* const updated = await fn.update({ path: './scraper.py' });
|
|
3960
|
+
* console.log(updated.latest_version);
|
|
3961
|
+
*/
|
|
3962
|
+
async update(options) {
|
|
3963
|
+
if (options.path === void 0) {
|
|
3964
|
+
throw new InvalidRequestError("'path' must be provided");
|
|
2914
3965
|
}
|
|
3966
|
+
const functionId = await this.ensureInitialized();
|
|
3967
|
+
const file = await readPythonFile(options.path);
|
|
3968
|
+
const response = await functionUpdate({
|
|
3969
|
+
client: this.client.getClient(),
|
|
3970
|
+
throwOnError: true,
|
|
3971
|
+
path: { function_id: functionId },
|
|
3972
|
+
query: {
|
|
3973
|
+
restricted: options.restricted ?? true,
|
|
3974
|
+
...options.version !== void 0 ? { version: options.version } : {}
|
|
3975
|
+
},
|
|
3976
|
+
body: { file }
|
|
3977
|
+
});
|
|
3978
|
+
console.info(`[Function] ${functionId} updated successfully to version ${response.data.latest_version}.`);
|
|
3979
|
+
return response.data;
|
|
3980
|
+
}
|
|
3981
|
+
/**
|
|
3982
|
+
* Update the function metadata (name, description, default runtime, ...).
|
|
3983
|
+
*
|
|
3984
|
+
* @param options - Metadata fields to update, such as name, description, or default runtime.
|
|
3985
|
+
* @returns Updated function metadata.
|
|
3986
|
+
* @throws NotteAPIError if the metadata update is rejected.
|
|
3987
|
+
* @example
|
|
3988
|
+
* await fn.updateMetadata({ name: 'Price monitor', default_runtime: 'extended' });
|
|
3989
|
+
*/
|
|
3990
|
+
async updateMetadata(options) {
|
|
3991
|
+
const functionId = await this.ensureInitialized();
|
|
3992
|
+
const response = await functionMetadataUpdate({
|
|
3993
|
+
client: this.client.getClient(),
|
|
3994
|
+
throwOnError: true,
|
|
3995
|
+
path: { function_id: functionId },
|
|
3996
|
+
body: options
|
|
3997
|
+
});
|
|
3998
|
+
return response.data;
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Delete the function from the Notte console.
|
|
4002
|
+
*
|
|
4003
|
+
* @returns The API's function-deletion confirmation.
|
|
4004
|
+
* @throws NotteAPIError if the function cannot be deleted.
|
|
4005
|
+
* @example
|
|
4006
|
+
* await fn.delete();
|
|
4007
|
+
*/
|
|
4008
|
+
async delete() {
|
|
4009
|
+
const functionId = await this.ensureInitialized();
|
|
4010
|
+
const response = await functionDelete({
|
|
4011
|
+
client: this.client.getClient(),
|
|
4012
|
+
throwOnError: true,
|
|
4013
|
+
path: { function_id: functionId }
|
|
4014
|
+
});
|
|
4015
|
+
console.info(`[Function] ${functionId} deleted successfully.`);
|
|
4016
|
+
return response.data;
|
|
4017
|
+
}
|
|
4018
|
+
/**
|
|
4019
|
+
* Schedule the function to run on a cron expression.
|
|
4020
|
+
*
|
|
4021
|
+
* @param options - Cron expression and optional variables passed to each scheduled execution.
|
|
4022
|
+
* @returns The saved schedule configuration.
|
|
4023
|
+
* @throws NotteAPIError if the schedule cannot be saved.
|
|
4024
|
+
* @example
|
|
4025
|
+
* await fn.setSchedule({ cron: '0 9 * * *', variables: { url: 'https://example.com' } });
|
|
4026
|
+
*/
|
|
4027
|
+
async setSchedule(options) {
|
|
4028
|
+
const functionId = await this.ensureInitialized();
|
|
4029
|
+
const response = await functionScheduleSet({
|
|
4030
|
+
client: this.client.getClient(),
|
|
4031
|
+
throwOnError: true,
|
|
4032
|
+
path: { function_id: functionId },
|
|
4033
|
+
body: { cron: options.cron, variables: options.variables ?? null }
|
|
4034
|
+
});
|
|
4035
|
+
return response.data;
|
|
4036
|
+
}
|
|
4037
|
+
/**
|
|
4038
|
+
* Remove the function schedule.
|
|
4039
|
+
*
|
|
4040
|
+
* @returns The API's schedule-deletion confirmation.
|
|
4041
|
+
* @throws NotteAPIError if schedule deletion fails.
|
|
4042
|
+
* @example
|
|
4043
|
+
* await fn.deleteSchedule();
|
|
4044
|
+
*/
|
|
4045
|
+
async deleteSchedule() {
|
|
4046
|
+
const functionId = await this.ensureInitialized();
|
|
4047
|
+
const response = await functionScheduleDelete({
|
|
4048
|
+
client: this.client.getClient(),
|
|
4049
|
+
throwOnError: true,
|
|
4050
|
+
path: { function_id: functionId }
|
|
4051
|
+
});
|
|
4052
|
+
return response.data;
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Roll the function back to a previous version.
|
|
4056
|
+
*
|
|
4057
|
+
* @param options - Target version and optional code-access restriction, which defaults to true.
|
|
4058
|
+
* @returns Updated function metadata after rollback.
|
|
4059
|
+
* @throws NotteAPIError if the requested version cannot be restored.
|
|
4060
|
+
* @example
|
|
4061
|
+
* const { versions } = await fn.get();
|
|
4062
|
+
* await fn.rollback({ version: versions[0] });
|
|
4063
|
+
*/
|
|
4064
|
+
async rollback(options) {
|
|
4065
|
+
const functionId = await this.ensureInitialized();
|
|
4066
|
+
const response = await functionRollback({
|
|
4067
|
+
client: this.client.getClient(),
|
|
4068
|
+
throwOnError: true,
|
|
4069
|
+
path: { function_id: functionId },
|
|
4070
|
+
query: { restricted: options.restricted ?? true },
|
|
4071
|
+
body: { version: options.version }
|
|
4072
|
+
});
|
|
4073
|
+
return response.data;
|
|
4074
|
+
}
|
|
4075
|
+
/**
|
|
4076
|
+
* Get metadata for a specific function run.
|
|
4077
|
+
*
|
|
4078
|
+
* @param functionRunId - ID of a run belonging to this function.
|
|
4079
|
+
* @returns The run's current status, logs, and result metadata. This does not wait for completion.
|
|
4080
|
+
* @throws NotteAPIError if the run cannot be retrieved.
|
|
4081
|
+
* @example
|
|
4082
|
+
* const run = await fn.getRun(result.function_run_id);
|
|
4083
|
+
* console.log(run.status, run.logs);
|
|
4084
|
+
*/
|
|
4085
|
+
async getRun(functionRunId) {
|
|
4086
|
+
const functionId = await this.ensureInitialized();
|
|
4087
|
+
const response = await functionRunGetMetadata({
|
|
4088
|
+
client: this.client.getClient(),
|
|
4089
|
+
throwOnError: true,
|
|
4090
|
+
path: { function_id: functionId, run_id: functionRunId }
|
|
4091
|
+
});
|
|
4092
|
+
return response.data;
|
|
4093
|
+
}
|
|
4094
|
+
/**
|
|
4095
|
+
* Compatibility alias for getRun().
|
|
4096
|
+
* @deprecated Use getRun() instead. Retained for backwards compatibility.
|
|
4097
|
+
*/
|
|
4098
|
+
async retrieve(functionRunId) {
|
|
4099
|
+
return this.getRun(functionRunId);
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Create a cloud run record without starting execution, like
|
|
4103
|
+
* `function.create_run()` in Python. Use this only when you need the ID
|
|
4104
|
+
* before execution, then pass it to `run(variables, { functionRunId })`.
|
|
4105
|
+
* `run()` otherwise creates a new run on its own.
|
|
4106
|
+
*
|
|
4107
|
+
* @param options - Run-record settings; `local` defaults to false. Node execution remains cloud-only.
|
|
4108
|
+
* @returns A created run record and its ID. No function code has executed yet.
|
|
4109
|
+
* @throws NotteAPIError if run creation is rejected.
|
|
4110
|
+
* @example
|
|
4111
|
+
* const created = await fn.createRun();
|
|
4112
|
+
* const result = await fn.run({ url: 'https://example.com' }, { functionRunId: created.function_run_id });
|
|
4113
|
+
*/
|
|
4114
|
+
async createRun(options = {}) {
|
|
4115
|
+
const functionId = await this.ensureInitialized();
|
|
4116
|
+
return this.postRunEndpoint({
|
|
4117
|
+
url: FUNCTION_RUN_ENDPOINTS.create,
|
|
4118
|
+
path: { function_id: functionId },
|
|
4119
|
+
body: { local: options.local ?? false }
|
|
4120
|
+
});
|
|
2915
4121
|
}
|
|
2916
4122
|
/**
|
|
2917
4123
|
* List the runs of this function.
|
|
2918
4124
|
*
|
|
2919
|
-
*
|
|
2920
|
-
*
|
|
2921
|
-
*
|
|
2922
|
-
*
|
|
2923
|
-
*
|
|
2924
|
-
*
|
|
2925
|
-
*
|
|
2926
|
-
*
|
|
2927
|
-
*
|
|
2928
|
-
* this endpoint yet".
|
|
2929
|
-
*
|
|
2930
|
-
* Sending it explicitly pins this method to the documented behaviour against
|
|
2931
|
-
* whichever API version it is talking to, including deployments that predate
|
|
2932
|
-
* the server-side change. Callers who want only in-flight runs pass
|
|
2933
|
-
* `only_active: true`.
|
|
4125
|
+
* Includes completed runs by default. Set `only_active: true` to retrieve
|
|
4126
|
+
* only in-flight runs. Use the response pagination metadata to request
|
|
4127
|
+
* additional pages.
|
|
4128
|
+
*
|
|
4129
|
+
* @param options - Pagination and filtering options. `only_active` defaults to false, including completed runs.
|
|
4130
|
+
* @returns One page of run records and pagination metadata; additional pages require another call.
|
|
4131
|
+
* @throws NotteAPIError if the run history cannot be retrieved.
|
|
4132
|
+
* @example
|
|
4133
|
+
* const { items } = await fn.runs({ page_size: 20 });
|
|
2934
4134
|
*/
|
|
2935
4135
|
async runs(options = {}) {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
only_active: options.only_active ?? false
|
|
2945
|
-
}
|
|
2946
|
-
});
|
|
2947
|
-
if (response?.error) {
|
|
2948
|
-
throw new Error(`Failed to list function runs: ${formatError(response.error)}`);
|
|
2949
|
-
}
|
|
2950
|
-
return response.data;
|
|
2951
|
-
} catch (error) {
|
|
2952
|
-
if (error instanceof Error && error.message.startsWith("Failed to list function runs:")) {
|
|
2953
|
-
throw error;
|
|
2954
|
-
}
|
|
2955
|
-
throw new Error(`Failed to list function runs: ${error instanceof Error ? error.message : String(error)}`);
|
|
2956
|
-
}
|
|
4136
|
+
const functionId = await this.ensureInitialized();
|
|
4137
|
+
const response = await listFunctionRunsByFunctionId({
|
|
4138
|
+
client: this.client.getClient(),
|
|
4139
|
+
throwOnError: true,
|
|
4140
|
+
path: { function_id: functionId },
|
|
4141
|
+
query: { ...options, only_active: options.only_active ?? false }
|
|
4142
|
+
});
|
|
4143
|
+
return response.data;
|
|
2957
4144
|
}
|
|
2958
4145
|
/**
|
|
2959
|
-
*
|
|
4146
|
+
* Run the function on the cloud and wait for its result, streaming logs by
|
|
4147
|
+
* default like `function.run(**variables)` in Python.
|
|
4148
|
+
*
|
|
4149
|
+
* Pass `{ stream: false }` to receive the backend's JSON response without
|
|
4150
|
+
* live logs; execution still blocks until completion. Pass `functionRunId`
|
|
4151
|
+
* to execute a record returned by `createRun()`; otherwise a new run record
|
|
4152
|
+
* is created first, exactly like Python. A failed execution throws
|
|
4153
|
+
* `FailedToRunCloudFunctionError` unless `raiseOnFailure` is false, and the
|
|
4154
|
+
* wait is aborted with `NotteTimeoutError` after `timeoutMs`.
|
|
4155
|
+
*
|
|
4156
|
+
* @param variables - JSON-serializable keyword arguments passed to the Python function's `run` entry point.
|
|
4157
|
+
* @param options - Log streaming, failure handling, runtime, run ID, and timeout settings.
|
|
4158
|
+
* @returns The completed execution response. The function's return value is in `result`. Disabling streaming still waits for completion.
|
|
4159
|
+
* @throws FailedToRunCloudFunctionError if execution fails and `raiseOnFailure` is true.
|
|
4160
|
+
* @throws NotteTimeoutError if the execution request exceeds `timeoutMs`.
|
|
4161
|
+
* @throws InvalidRequestError if the run options are invalid.
|
|
4162
|
+
* @example
|
|
4163
|
+
* const result = await fn.run({ url: 'https://example.com' });
|
|
4164
|
+
* console.log(result.result);
|
|
4165
|
+
*
|
|
4166
|
+
* await fn.run({ url: 'https://example.com' }, { runtime: 'extended', onLog: line => logger.info(line) });
|
|
2960
4167
|
*/
|
|
2961
4168
|
async run(variables = {}, options = {}) {
|
|
2962
|
-
const
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
4169
|
+
const stream = options.stream ?? true;
|
|
4170
|
+
const timeoutMs = options.timeoutMs ?? FUNCTION_RUN_TIMEOUT_MS;
|
|
4171
|
+
if (!(timeoutMs > 0)) {
|
|
4172
|
+
throw new InvalidRequestError("timeoutMs must be a positive number");
|
|
4173
|
+
}
|
|
4174
|
+
if (options.runtime !== void 0 && !FUNCTION_RUNTIMES.includes(options.runtime)) {
|
|
4175
|
+
throw new InvalidRequestError(`runtime must be one of ${FUNCTION_RUNTIMES.join(", ")}, got '${String(options.runtime)}'`);
|
|
4176
|
+
}
|
|
4177
|
+
const functionId = await this.ensureInitialized();
|
|
4178
|
+
const functionRunId = options.functionRunId ?? (await this.createRun()).function_run_id;
|
|
4179
|
+
const body = {
|
|
4180
|
+
workflow_id: functionId,
|
|
4181
|
+
variables,
|
|
4182
|
+
stream,
|
|
4183
|
+
...options.runtime !== void 0 ? { runtime: options.runtime } : {}
|
|
4184
|
+
};
|
|
4185
|
+
const headers = { [TIMEOUT_HEADER]: "0" };
|
|
4186
|
+
const apiKey = this.client.getConfig().apiKey;
|
|
4187
|
+
if (apiKey) {
|
|
4188
|
+
headers[RUN_API_KEY_HEADER] = apiKey;
|
|
4189
|
+
}
|
|
4190
|
+
const endpointPath = FUNCTION_RUN_ENDPOINTS.start.replace("{function_id}", functionId).replace("{run_id}", functionRunId);
|
|
4191
|
+
const parseAs = stream ? "stream" : "json";
|
|
4192
|
+
const controller = new AbortController();
|
|
4193
|
+
let timedOut = false;
|
|
4194
|
+
const timer = Number.isFinite(timeoutMs) ? setTimeout(() => {
|
|
4195
|
+
timedOut = true;
|
|
4196
|
+
controller.abort(new DOMException(`Function run timed out after ${timeoutMs}ms`, "TimeoutError"));
|
|
4197
|
+
}, timeoutMs) : void 0;
|
|
4198
|
+
timer?.unref?.();
|
|
2966
4199
|
try {
|
|
2967
|
-
const
|
|
2968
|
-
|
|
2969
|
-
path: {
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
headers
|
|
2973
|
-
|
|
2974
|
-
"x-notte-request-origin": "sdk-node",
|
|
2975
|
-
"x-notte-sdk-version": SDK_VERSION
|
|
2976
|
-
},
|
|
2977
|
-
body: {
|
|
2978
|
-
workflow_id: this.functionId,
|
|
2979
|
-
variables,
|
|
2980
|
-
stream: false,
|
|
2981
|
-
...options.runtime ? { runtime: options.runtime } : {}
|
|
2982
|
-
}
|
|
4200
|
+
const raw = await this.postRunEndpoint({
|
|
4201
|
+
url: FUNCTION_RUN_ENDPOINTS.start,
|
|
4202
|
+
path: { function_id: functionId, run_id: functionRunId },
|
|
4203
|
+
body: { ...body, function_run_id: functionRunId },
|
|
4204
|
+
parseAs,
|
|
4205
|
+
headers,
|
|
4206
|
+
signal: controller.signal
|
|
2983
4207
|
});
|
|
2984
|
-
|
|
2985
|
-
|
|
4208
|
+
const result = stream ? await readFunctionStream(raw, options.onLog ?? console.log, endpointPath, controller.signal) : parseFunctionRunResult(raw, endpointPath);
|
|
4209
|
+
if (result.status === "failed" && (options.raiseOnFailure ?? true)) {
|
|
4210
|
+
throw new FailedToRunCloudFunctionError(functionId, result.function_run_id, result);
|
|
2986
4211
|
}
|
|
2987
|
-
return
|
|
4212
|
+
return result;
|
|
2988
4213
|
} catch (error) {
|
|
2989
|
-
if (
|
|
2990
|
-
throw error;
|
|
4214
|
+
if (timedOut) {
|
|
4215
|
+
throw new NotteTimeoutError(`Function ${functionId} run timed out after ${timeoutMs}ms`, { cause: error });
|
|
2991
4216
|
}
|
|
2992
|
-
throw
|
|
4217
|
+
throw error;
|
|
4218
|
+
} finally {
|
|
4219
|
+
clearTimeout(timer);
|
|
2993
4220
|
}
|
|
2994
4221
|
}
|
|
2995
4222
|
/**
|
|
2996
4223
|
* Get the download URL of the function code, decrypting it when the API
|
|
2997
4224
|
* returns an encrypted one (Notte managed functions).
|
|
4225
|
+
*
|
|
4226
|
+
* @param options - Optional version and decryption-key override.
|
|
4227
|
+
* @returns An HTTP(S) URL for downloading the function source.
|
|
4228
|
+
* @throws InvalidRequestError if an encrypted URL has no usable decryption key.
|
|
4229
|
+
* @throws NotteError if decryption does not produce an HTTP(S) URL.
|
|
4230
|
+
* @example
|
|
4231
|
+
* const url = await fn.getUrl({ version: 'v1' });
|
|
2998
4232
|
*/
|
|
2999
4233
|
async getUrl(options = {}) {
|
|
3000
|
-
|
|
3001
|
-
try {
|
|
3002
|
-
response = await functionDownloadUrl({
|
|
3003
|
-
client: this.client.getClient(),
|
|
3004
|
-
path: {
|
|
3005
|
-
function_id: this.functionId
|
|
3006
|
-
},
|
|
3007
|
-
query: options.version ? { version: options.version } : {}
|
|
3008
|
-
});
|
|
3009
|
-
} catch (error) {
|
|
3010
|
-
throw new Error(`Failed to get function download url: ${error instanceof Error ? error.message : String(error)}`);
|
|
3011
|
-
}
|
|
3012
|
-
if (response?.error) {
|
|
3013
|
-
throw new Error(`Failed to get function download url: ${formatError(response.error)}`);
|
|
3014
|
-
}
|
|
3015
|
-
const url = response.data.url;
|
|
4234
|
+
const { url } = await this.get(options);
|
|
3016
4235
|
if (isHttpUrl(url)) {
|
|
3017
4236
|
return url;
|
|
3018
4237
|
}
|
|
3019
4238
|
const decryptionKey = options.decryption_key ?? this.decryptionKey;
|
|
3020
|
-
if (
|
|
3021
|
-
throw new
|
|
4239
|
+
if (decryptionKey === void 0) {
|
|
4240
|
+
throw new InvalidRequestError(
|
|
3022
4241
|
"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>' })."
|
|
3023
4242
|
);
|
|
3024
4243
|
}
|
|
@@ -3026,147 +4245,837 @@ var NotteFunction = class {
|
|
|
3026
4245
|
try {
|
|
3027
4246
|
decrypted = new Encryption(decryptionKey).decrypt(url);
|
|
3028
4247
|
} catch (error) {
|
|
3029
|
-
throw new
|
|
4248
|
+
throw new InvalidRequestError(
|
|
4249
|
+
`Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`
|
|
4250
|
+
);
|
|
3030
4251
|
}
|
|
3031
4252
|
if (!isHttpUrl(decrypted)) {
|
|
3032
|
-
throw new
|
|
4253
|
+
throw new NotteError(
|
|
4254
|
+
"Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help."
|
|
4255
|
+
);
|
|
3033
4256
|
}
|
|
3034
4257
|
return decrypted;
|
|
3035
4258
|
}
|
|
3036
4259
|
/**
|
|
3037
4260
|
* Download the function code as a python file. Returns the code, and writes
|
|
3038
4261
|
* it to `path` when one is provided.
|
|
4262
|
+
*
|
|
4263
|
+
* @param options - Optional version, decryption key, and local `.py` destination path.
|
|
4264
|
+
* @returns The Python source text, including when a local destination is supplied.
|
|
4265
|
+
* @throws InvalidRequestError if the destination does not end in `.py` or the decryption key is invalid.
|
|
4266
|
+
* @throws NotteError if the source download fails. Local filesystem failures also propagate.
|
|
4267
|
+
* @example
|
|
4268
|
+
* const code = await fn.download({ path: './scraper.py' });
|
|
3039
4269
|
*/
|
|
3040
4270
|
async download(options = {}) {
|
|
3041
4271
|
const { path } = options;
|
|
3042
|
-
if (path !== void 0
|
|
3043
|
-
|
|
4272
|
+
if (path !== void 0) {
|
|
4273
|
+
assertPythonPath(path);
|
|
3044
4274
|
}
|
|
3045
4275
|
const url = await this.getUrl(options);
|
|
3046
4276
|
let response;
|
|
3047
4277
|
try {
|
|
3048
4278
|
response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
3049
4279
|
} catch (error) {
|
|
3050
|
-
throw new
|
|
4280
|
+
throw new NotteError(
|
|
4281
|
+
`Failed to download function code in ${DOWNLOAD_TIMEOUT_MS / 1e3} seconds: ${error instanceof Error ? error.message : String(error)}`,
|
|
4282
|
+
{ cause: error }
|
|
4283
|
+
);
|
|
3051
4284
|
}
|
|
3052
4285
|
if (!response.ok) {
|
|
3053
|
-
throw new
|
|
4286
|
+
throw new NotteError(`Failed to download function code: ${response.status} ${response.statusText}`);
|
|
3054
4287
|
}
|
|
3055
4288
|
const code = await response.text();
|
|
3056
4289
|
if (path !== void 0) {
|
|
3057
|
-
await
|
|
4290
|
+
await writeFile3(path, code, "utf8");
|
|
3058
4291
|
}
|
|
3059
4292
|
return code;
|
|
3060
4293
|
}
|
|
3061
4294
|
/**
|
|
3062
|
-
*
|
|
4295
|
+
* Call one of the run endpoints that the OpenAPI spec does not describe
|
|
4296
|
+
* (`FUNCTION_RUN_ENDPOINTS`). The generated client has no operation for
|
|
4297
|
+
* them, so this is the single place where they are hand-rolled; the request
|
|
4298
|
+
* and response shapes are transcribed from `notte_sdk.types`
|
|
4299
|
+
* (`CreateFunctionRunRequest` / `CreateFunctionRunResponse` and
|
|
4300
|
+
* `StartFunctionRunRequest` / `FunctionRunResponse`). Replace the callers
|
|
4301
|
+
* with generated operations once the spec exposes these paths.
|
|
3063
4302
|
*/
|
|
3064
|
-
|
|
3065
|
-
|
|
4303
|
+
async postRunEndpoint(request) {
|
|
4304
|
+
const response = await this.client.getClient().post({
|
|
4305
|
+
url: request.url,
|
|
4306
|
+
path: request.path,
|
|
4307
|
+
body: request.body,
|
|
4308
|
+
headers: { "Content-Type": "application/json", ...request.headers },
|
|
4309
|
+
parseAs: request.parseAs ?? "json",
|
|
4310
|
+
signal: request.signal,
|
|
4311
|
+
throwOnError: true
|
|
4312
|
+
});
|
|
4313
|
+
return response.data;
|
|
3066
4314
|
}
|
|
3067
4315
|
};
|
|
3068
4316
|
function isHttpUrl(value) {
|
|
3069
4317
|
return value.startsWith("https://") || value.startsWith("http://");
|
|
3070
4318
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
4319
|
+
function assertPythonPath(path) {
|
|
4320
|
+
if (!path.endsWith(".py")) {
|
|
4321
|
+
throw new InvalidRequestError(`Code file path must end with .py, got '${path}'`);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
async function readPythonFile(path) {
|
|
4325
|
+
assertPythonPath(path);
|
|
4326
|
+
let bytes;
|
|
4327
|
+
try {
|
|
4328
|
+
bytes = await readFile2(path);
|
|
4329
|
+
} catch (error) {
|
|
4330
|
+
throw new InvalidRequestError(
|
|
4331
|
+
`The file '${path}' could not be read: ${error instanceof Error ? error.message : String(error)}`
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
return new File([new Uint8Array(bytes)], basename(path), { type: "text/x-python" });
|
|
4335
|
+
}
|
|
4336
|
+
function unwrapRuntimeEnvelope(candidate, path) {
|
|
4337
|
+
const { statusCode, body } = candidate;
|
|
4338
|
+
if (typeof statusCode !== "number" || statusCode < 400 || !("body" in candidate)) {
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
let parsed = body;
|
|
4342
|
+
if (typeof body === "string") {
|
|
4343
|
+
try {
|
|
4344
|
+
parsed = JSON.parse(body);
|
|
4345
|
+
} catch {
|
|
4346
|
+
parsed = body;
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
throw NotteAPIError.fromBody(path, statusCode, parsed);
|
|
4350
|
+
}
|
|
4351
|
+
function parseFunctionRunResult(value, path) {
|
|
4352
|
+
if (typeof value !== "object" || value === null) {
|
|
4353
|
+
throw new NotteError("Invalid function result: expected an object");
|
|
4354
|
+
}
|
|
4355
|
+
const candidate = value;
|
|
4356
|
+
unwrapRuntimeEnvelope(candidate, path);
|
|
4357
|
+
const status = candidate.status;
|
|
4358
|
+
if (typeof candidate.function_run_id !== "string" || typeof candidate.function_id !== "string" || typeof status !== "string" || !FUNCTION_RUN_STATUSES.includes(status) || !("result" in candidate)) {
|
|
4359
|
+
throw new NotteError("Invalid function result: missing function_id, function_run_id, status or result");
|
|
4360
|
+
}
|
|
4361
|
+
return {
|
|
4362
|
+
function_id: candidate.function_id,
|
|
4363
|
+
function_run_id: candidate.function_run_id,
|
|
4364
|
+
session_id: typeof candidate.session_id === "string" ? candidate.session_id : null,
|
|
4365
|
+
result: candidate.result,
|
|
4366
|
+
status
|
|
4367
|
+
};
|
|
4368
|
+
}
|
|
4369
|
+
async function readFunctionStream(body, onLog, path, signal) {
|
|
4370
|
+
if (!body?.getReader) {
|
|
4371
|
+
throw new NotteError("Missing function response stream");
|
|
4372
|
+
}
|
|
4373
|
+
const reader = body.getReader();
|
|
4374
|
+
const onAbort = () => {
|
|
4375
|
+
reader.cancel(signal?.reason).catch(() => {
|
|
4376
|
+
});
|
|
4377
|
+
};
|
|
4378
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4379
|
+
const decoder = new TextDecoder();
|
|
4380
|
+
let buffer = "";
|
|
4381
|
+
let data = [];
|
|
4382
|
+
let result;
|
|
4383
|
+
let sawFrame = false;
|
|
4384
|
+
let raw = "";
|
|
4385
|
+
const dispatch = () => {
|
|
4386
|
+
if (!data.length) return;
|
|
4387
|
+
const payload = data.join("\n");
|
|
4388
|
+
data = [];
|
|
4389
|
+
let event;
|
|
4390
|
+
try {
|
|
4391
|
+
event = JSON.parse(payload);
|
|
4392
|
+
} catch (error) {
|
|
4393
|
+
throw new NotteError(`Malformed function stream event: ${payload}`, { cause: error });
|
|
4394
|
+
}
|
|
4395
|
+
if (event.type === "log") onLog(String(event.message ?? ""));
|
|
4396
|
+
if (event.type === "error") throw new NotteError(`Function stream error: ${String(event.message ?? "")}`);
|
|
4397
|
+
if (event.type === "result") {
|
|
4398
|
+
const value = typeof event.message === "string" ? JSON.parse(event.message) : event.message;
|
|
4399
|
+
result = parseFunctionRunResult(value, path);
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
const line = (value) => {
|
|
4403
|
+
if (value === "") dispatch();
|
|
4404
|
+
else if (value.startsWith("data:")) {
|
|
4405
|
+
sawFrame = true;
|
|
4406
|
+
data.push(value.slice(5).replace(/^ /, ""));
|
|
4407
|
+
}
|
|
4408
|
+
};
|
|
4409
|
+
try {
|
|
4410
|
+
while (true) {
|
|
4411
|
+
const { value, done } = await reader.read();
|
|
4412
|
+
const text = decoder.decode(value, { stream: !done });
|
|
4413
|
+
buffer += text;
|
|
4414
|
+
if (!sawFrame) raw += text;
|
|
4415
|
+
let end;
|
|
4416
|
+
while ((end = buffer.indexOf("\n")) !== -1) {
|
|
4417
|
+
line(buffer.slice(0, end).replace(/\r$/, ""));
|
|
4418
|
+
buffer = buffer.slice(end + 1);
|
|
4419
|
+
}
|
|
4420
|
+
if (done) break;
|
|
4421
|
+
}
|
|
4422
|
+
if (buffer) line(buffer.replace(/\r$/, ""));
|
|
4423
|
+
dispatch();
|
|
4424
|
+
if (!result && !sawFrame && raw.trim().length > 0) {
|
|
4425
|
+
let parsed;
|
|
4426
|
+
try {
|
|
4427
|
+
parsed = JSON.parse(raw);
|
|
4428
|
+
} catch {
|
|
4429
|
+
throw new NotteError(`Function stream ended without a result: ${raw.slice(0, 200)}`);
|
|
4430
|
+
}
|
|
4431
|
+
result = parseFunctionRunResult(parsed, path);
|
|
4432
|
+
}
|
|
4433
|
+
if (!result) throw new NotteError("Function stream ended without a result");
|
|
4434
|
+
return result;
|
|
4435
|
+
} finally {
|
|
4436
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4437
|
+
await reader.cancel().catch(() => {
|
|
4438
|
+
});
|
|
4439
|
+
reader.releaseLock();
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
3074
4442
|
|
|
3075
4443
|
// src/files.ts
|
|
4444
|
+
import { createWriteStream } from "fs";
|
|
4445
|
+
import { link, mkdir, readFile as readFile3, rename, rm, stat } from "fs/promises";
|
|
4446
|
+
import { basename as basename2, extname, join } from "path";
|
|
4447
|
+
import { randomBytes } from "crypto";
|
|
4448
|
+
import { Readable } from "stream";
|
|
4449
|
+
import { pipeline } from "stream/promises";
|
|
4450
|
+
var FileNotFoundError = class extends NotteError {
|
|
4451
|
+
};
|
|
4452
|
+
var FileExistsError = class extends NotteError {
|
|
4453
|
+
};
|
|
4454
|
+
var METADATA_PAGE_SIZE = 1e3;
|
|
4455
|
+
var MIME_TYPES_BY_EXTENSION = {
|
|
4456
|
+
".csv": "text/csv",
|
|
4457
|
+
".gif": "image/gif",
|
|
4458
|
+
".htm": "text/html",
|
|
4459
|
+
".html": "text/html",
|
|
4460
|
+
".jpeg": "image/jpeg",
|
|
4461
|
+
".jpg": "image/jpeg",
|
|
4462
|
+
".json": "application/json",
|
|
4463
|
+
".md": "text/markdown",
|
|
4464
|
+
".pdf": "application/pdf",
|
|
4465
|
+
".png": "image/png",
|
|
4466
|
+
".svg": "image/svg+xml",
|
|
4467
|
+
".txt": "text/plain",
|
|
4468
|
+
".webp": "image/webp",
|
|
4469
|
+
".xml": "application/xml",
|
|
4470
|
+
".zip": "application/zip"
|
|
4471
|
+
};
|
|
4472
|
+
function guessMimeType(filename) {
|
|
4473
|
+
return MIME_TYPES_BY_EXTENSION[extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
4474
|
+
}
|
|
4475
|
+
function sanitizeFilename(filename) {
|
|
4476
|
+
const segment = filename.split(/[\\/]+/).filter(Boolean).pop() ?? "";
|
|
4477
|
+
return segment.replace(/^[a-zA-Z]:/, "");
|
|
4478
|
+
}
|
|
4479
|
+
function toFile(file, filename) {
|
|
4480
|
+
if (file instanceof File && file.name === filename) {
|
|
4481
|
+
return file;
|
|
4482
|
+
}
|
|
4483
|
+
const type = file instanceof Blob && file.type ? file.type : guessMimeType(filename);
|
|
4484
|
+
return new File([file], filename, { type });
|
|
4485
|
+
}
|
|
3076
4486
|
var SessionFiles = class {
|
|
3077
4487
|
constructor(client2, sessionId) {
|
|
3078
4488
|
this.client = client2;
|
|
3079
4489
|
this.sessionId = sessionId;
|
|
3080
4490
|
}
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
4491
|
+
/**
|
|
4492
|
+
* Upload bytes as a session file. `filename` is what the browser sees when
|
|
4493
|
+
* the file is later attached to a page.
|
|
4494
|
+
*
|
|
4495
|
+
* @param file - Bytes to upload as a Blob, File, or buffer.
|
|
4496
|
+
* @param filename - Stored filename, defaulting to `file`.
|
|
4497
|
+
* @returns Metadata for the uploaded file, including its ID, name, and size.
|
|
4498
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4499
|
+
*/
|
|
3085
4500
|
async upload(file, filename = "file") {
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
body
|
|
4501
|
+
const response = await uploadSessionFile({
|
|
4502
|
+
client: this.client,
|
|
4503
|
+
path: { session_id: this.sessionId },
|
|
4504
|
+
body: { file: toFile(file, filename) },
|
|
4505
|
+
throwOnError: true
|
|
3092
4506
|
});
|
|
3093
|
-
|
|
3094
|
-
return result.data;
|
|
4507
|
+
return response.data;
|
|
3095
4508
|
}
|
|
4509
|
+
/** List the session's non-expired files, newest first.
|
|
4510
|
+
*
|
|
4511
|
+
* @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
|
|
4512
|
+
* @returns One page of non-expired files and the total count; pagination is not automatic.
|
|
4513
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4514
|
+
*/
|
|
3096
4515
|
async list(options = {}) {
|
|
3097
|
-
const
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
query: { limit: options.limit ?? 100, offset: options.offset ?? 0, source: options.source }
|
|
4516
|
+
const response = await listSessionFiles({
|
|
4517
|
+
client: this.client,
|
|
4518
|
+
path: { session_id: this.sessionId },
|
|
4519
|
+
query: { limit: options.limit ?? 100, offset: options.offset ?? 0, source: options.source },
|
|
4520
|
+
throwOnError: true
|
|
3101
4521
|
});
|
|
3102
|
-
|
|
3103
|
-
|
|
4522
|
+
return response.data;
|
|
4523
|
+
}
|
|
4524
|
+
/**
|
|
4525
|
+
* Find a file's metadata by ID by scanning the session listing. Rejects with
|
|
4526
|
+
* `FileNotFoundError` when the ID is unknown or expired.
|
|
4527
|
+
*
|
|
4528
|
+
* @param fileId - ID of a file belonging to this session.
|
|
4529
|
+
* @returns The matching file's metadata.
|
|
4530
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4531
|
+
* @throws FileNotFoundError if the file ID is unknown or expired.
|
|
4532
|
+
*/
|
|
4533
|
+
async metadata(fileId) {
|
|
4534
|
+
let offset = 0;
|
|
4535
|
+
while (true) {
|
|
4536
|
+
const page = await this.list({ limit: METADATA_PAGE_SIZE, offset });
|
|
4537
|
+
const match = page.files.find((file) => file.id === fileId);
|
|
4538
|
+
if (match) {
|
|
4539
|
+
return match;
|
|
4540
|
+
}
|
|
4541
|
+
offset += page.files.length;
|
|
4542
|
+
if (offset >= page.total || page.files.length === 0) {
|
|
4543
|
+
throw new FileNotFoundError(`File ${fileId} was not found in session ${this.sessionId}`);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
3104
4546
|
}
|
|
4547
|
+
/** Download a file's bytes as a `Blob`.
|
|
4548
|
+
*
|
|
4549
|
+
* @param fileId - ID of a file belonging to this session.
|
|
4550
|
+
* @returns The downloaded bytes as a Blob; no local file is written.
|
|
4551
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4552
|
+
*/
|
|
3105
4553
|
async download(fileId) {
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
parseAs: "blob"
|
|
4554
|
+
const response = await downloadSessionFile({
|
|
4555
|
+
client: this.client,
|
|
4556
|
+
path: { session_id: this.sessionId, file_id: fileId },
|
|
4557
|
+
parseAs: "blob",
|
|
4558
|
+
throwOnError: true
|
|
3110
4559
|
});
|
|
3111
|
-
|
|
3112
|
-
return result.data;
|
|
4560
|
+
return response.data;
|
|
3113
4561
|
}
|
|
4562
|
+
/** Stream a file's bytes. Used by `RemoteFileStorage.download()` to write to disk without buffering. */
|
|
4563
|
+
async stream(fileId) {
|
|
4564
|
+
const response = await downloadSessionFile({
|
|
4565
|
+
client: this.client,
|
|
4566
|
+
path: { session_id: this.sessionId, file_id: fileId },
|
|
4567
|
+
parseAs: "stream",
|
|
4568
|
+
throwOnError: true
|
|
4569
|
+
});
|
|
4570
|
+
const body = response.data;
|
|
4571
|
+
if (!body) {
|
|
4572
|
+
throw new NotteError(`Download of file ${fileId} returned no body`);
|
|
4573
|
+
}
|
|
4574
|
+
return body;
|
|
4575
|
+
}
|
|
4576
|
+
/** Delete a session file.
|
|
4577
|
+
*
|
|
4578
|
+
* @param fileId - ID of the session file to delete.
|
|
4579
|
+
* @returns Resolves when deletion succeeds.
|
|
4580
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4581
|
+
*/
|
|
3114
4582
|
async delete(fileId) {
|
|
3115
|
-
|
|
3116
|
-
|
|
4583
|
+
await deleteSessionFile({
|
|
4584
|
+
client: this.client,
|
|
4585
|
+
path: { session_id: this.sessionId, file_id: fileId },
|
|
4586
|
+
throwOnError: true
|
|
4587
|
+
});
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
var RemoteFileStorage = class _RemoteFileStorage {
|
|
4591
|
+
constructor(client2, sessionId) {
|
|
4592
|
+
this.client = client2;
|
|
4593
|
+
this._sessionId = sessionId ?? null;
|
|
4594
|
+
}
|
|
4595
|
+
_sessionId;
|
|
4596
|
+
/** Bind the storage to a session. Called by `Session.start()` when the storage was passed to the session. */
|
|
4597
|
+
setSessionId(sessionId) {
|
|
4598
|
+
this._sessionId = sessionId;
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Bind this storage once, cloning it when another session already owns it.
|
|
4602
|
+
*
|
|
4603
|
+
* ```ts
|
|
4604
|
+
* const first = storage.forSession('session-a'); // first === storage
|
|
4605
|
+
* const second = storage.forSession('session-b'); // a clone sharing the client
|
|
4606
|
+
* ```
|
|
4607
|
+
*/
|
|
4608
|
+
forSession(sessionId) {
|
|
4609
|
+
if (this._sessionId === null) {
|
|
4610
|
+
this._sessionId = sessionId;
|
|
4611
|
+
return this;
|
|
4612
|
+
}
|
|
4613
|
+
if (this._sessionId === sessionId) {
|
|
4614
|
+
return this;
|
|
4615
|
+
}
|
|
4616
|
+
return new _RemoteFileStorage(this.client, sessionId);
|
|
4617
|
+
}
|
|
4618
|
+
/** ID of the bound session. Throws `InvalidRequestError` while the storage is unbound. */
|
|
4619
|
+
get sessionId() {
|
|
4620
|
+
if (this._sessionId === null) {
|
|
4621
|
+
throw new InvalidRequestError("A session ID is required for every file operation");
|
|
4622
|
+
}
|
|
4623
|
+
return this._sessionId;
|
|
4624
|
+
}
|
|
4625
|
+
/** Whether the storage has been bound to a session yet. */
|
|
4626
|
+
get isBound() {
|
|
4627
|
+
return this._sessionId !== null;
|
|
4628
|
+
}
|
|
4629
|
+
files() {
|
|
4630
|
+
return new SessionFiles(this.client.getClient(), this.sessionId);
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Upload a file to the session. A string is read as a local path and its
|
|
4634
|
+
* basename becomes the uploaded filename unless `uploadFileName` is set.
|
|
4635
|
+
*
|
|
4636
|
+
* @param file - Local file path, Blob, File, or buffer to upload.
|
|
4637
|
+
* @param uploadFileName - Optional stored filename. Local paths default to their basename; File objects keep their name.
|
|
4638
|
+
* @returns Metadata for the uploaded file, including its ID, name, and size.
|
|
4639
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4640
|
+
* @throws InvalidRequestError if storage has not been bound to a session.
|
|
4641
|
+
* @throws InvalidRequestError if a supplied local path is not a file.
|
|
4642
|
+
* @example
|
|
4643
|
+
* await storage.upload('./data/resume.pdf');
|
|
4644
|
+
* await storage.upload(Buffer.from('hello'), 'hello.txt');
|
|
4645
|
+
*/
|
|
4646
|
+
async upload(file, uploadFileName) {
|
|
4647
|
+
const files = this.files();
|
|
4648
|
+
if (typeof file === "string") {
|
|
4649
|
+
const info = await stat(file).catch(() => null);
|
|
4650
|
+
if (!info?.isFile()) {
|
|
4651
|
+
throw new InvalidRequestError(`Cannot upload file ${file}: it is not a file`);
|
|
4652
|
+
}
|
|
4653
|
+
const bytes = await readFile3(file);
|
|
4654
|
+
return files.upload(bytes, uploadFileName ?? basename2(file));
|
|
4655
|
+
}
|
|
4656
|
+
const filename = uploadFileName ?? (file instanceof File ? file.name : "file");
|
|
4657
|
+
return files.upload(file, filename);
|
|
4658
|
+
}
|
|
4659
|
+
/**
|
|
4660
|
+
* List the session's files.
|
|
4661
|
+
*
|
|
4662
|
+
* @param options - Optional source filter and pagination. Defaults to all sources, limit 100, and offset 0.
|
|
4663
|
+
* @returns One page of non-expired files and the total count; pagination is not automatic.
|
|
4664
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4665
|
+
* @throws InvalidRequestError if storage has not been bound to a session.
|
|
4666
|
+
* @example
|
|
4667
|
+
* const downloads = await storage.list({ source: 'session_download', limit: 1000 });
|
|
4668
|
+
*/
|
|
4669
|
+
async list(options = {}) {
|
|
4670
|
+
return this.files().list(options);
|
|
4671
|
+
}
|
|
4672
|
+
/** Metadata of a file by ID. Rejects with `FileNotFoundError` when unknown.
|
|
4673
|
+
*
|
|
4674
|
+
* @param fileId - ID of a file belonging to this session.
|
|
4675
|
+
* @returns The matching file's metadata.
|
|
4676
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4677
|
+
* @throws FileNotFoundError if the file ID is unknown or expired.
|
|
4678
|
+
* @throws InvalidRequestError if storage has not been bound to a session.
|
|
4679
|
+
*/
|
|
4680
|
+
async metadata(fileId) {
|
|
4681
|
+
return this.files().metadata(fileId);
|
|
4682
|
+
}
|
|
4683
|
+
/**
|
|
4684
|
+
* Download a file to `<localDir>/<filename>` and return the local path. The
|
|
4685
|
+
* file is written atomically (temporary file + rename). Rejects with
|
|
4686
|
+
* `FileExistsError` when the destination exists unless `force` is set.
|
|
4687
|
+
*
|
|
4688
|
+
* @param fileId - ID of the file to download.
|
|
4689
|
+
* @param localDir - Destination directory, created if needed. Defaults to the current directory.
|
|
4690
|
+
* @param options - Set `force` to overwrite an existing destination; defaults to false.
|
|
4691
|
+
* @returns The local file path after the download has been written successfully.
|
|
4692
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4693
|
+
* @throws InvalidRequestError if storage has not been bound to a session.
|
|
4694
|
+
* @throws FileExistsError if the destination exists and `force` is false. Filesystem errors also propagate.
|
|
4695
|
+
* @example
|
|
4696
|
+
* const path = await storage.download(fileId, './downloads', { force: true });
|
|
4697
|
+
*/
|
|
4698
|
+
async download(fileId, localDir = ".", options = {}) {
|
|
4699
|
+
const files = this.files();
|
|
4700
|
+
const metadata = await files.metadata(fileId);
|
|
4701
|
+
const safeName = sanitizeFilename(metadata.filename);
|
|
4702
|
+
if (safeName === "" || safeName === "." || safeName === "..") {
|
|
4703
|
+
throw new NotteError(`Unsafe filename returned for file ${fileId}: ${JSON.stringify(metadata.filename)}`);
|
|
4704
|
+
}
|
|
4705
|
+
await mkdir(localDir, { recursive: true });
|
|
4706
|
+
const destination = join(localDir, safeName);
|
|
4707
|
+
if (!options.force) {
|
|
4708
|
+
const exists = await stat(destination).then(() => true, () => false);
|
|
4709
|
+
if (exists) {
|
|
4710
|
+
throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
const body = await files.stream(fileId);
|
|
4714
|
+
const temporary = join(localDir, `.${safeName}.${randomBytes(8).toString("hex")}.part`);
|
|
4715
|
+
try {
|
|
4716
|
+
await pipeline(
|
|
4717
|
+
Readable.fromWeb(body),
|
|
4718
|
+
createWriteStream(temporary, { flags: "wx" })
|
|
4719
|
+
);
|
|
4720
|
+
if (options.force) {
|
|
4721
|
+
await rename(temporary, destination);
|
|
4722
|
+
} else {
|
|
4723
|
+
try {
|
|
4724
|
+
await link(temporary, destination);
|
|
4725
|
+
} catch (error) {
|
|
4726
|
+
if (error.code === "EEXIST") {
|
|
4727
|
+
throw new FileExistsError(`${destination} already exists; pass force: true to overwrite it`);
|
|
4728
|
+
}
|
|
4729
|
+
throw error;
|
|
4730
|
+
}
|
|
4731
|
+
await rm(temporary, { force: true });
|
|
4732
|
+
}
|
|
4733
|
+
} catch (error) {
|
|
4734
|
+
await rm(temporary, { force: true });
|
|
4735
|
+
throw error;
|
|
4736
|
+
}
|
|
4737
|
+
return destination;
|
|
4738
|
+
}
|
|
4739
|
+
/** Download a file's bytes as a `Blob` without touching the local filesystem. */
|
|
4740
|
+
async downloadBlob(fileId) {
|
|
4741
|
+
return this.files().download(fileId);
|
|
4742
|
+
}
|
|
4743
|
+
/** Stream a file's bytes without buffering the whole file in memory. */
|
|
4744
|
+
async stream(fileId) {
|
|
4745
|
+
return this.files().stream(fileId);
|
|
4746
|
+
}
|
|
4747
|
+
/** Delete a session file.
|
|
4748
|
+
*
|
|
4749
|
+
* @param fileId - ID of the session file to delete.
|
|
4750
|
+
* @returns Resolves when deletion succeeds.
|
|
4751
|
+
* @throws NotteAPIError if the storage API request fails.
|
|
4752
|
+
* @throws InvalidRequestError if storage has not been bound to a session.
|
|
4753
|
+
*/
|
|
4754
|
+
async delete(fileId) {
|
|
4755
|
+
await this.files().delete(fileId);
|
|
4756
|
+
}
|
|
4757
|
+
};
|
|
4758
|
+
|
|
4759
|
+
// src/anything.ts
|
|
4760
|
+
var NotteAnything = class {
|
|
4761
|
+
client;
|
|
4762
|
+
constructor(client2) {
|
|
4763
|
+
this.client = client2;
|
|
4764
|
+
}
|
|
4765
|
+
/**
|
|
4766
|
+
* Start building a function for `task`. Building is slow (minutes) and
|
|
4767
|
+
* billable; prefer the marketplace (`client.functions.list()`) when a
|
|
4768
|
+
* ready-made function already fits.
|
|
4769
|
+
*
|
|
4770
|
+
* ```ts
|
|
4771
|
+
* const result = await client.anything.start({ task: 'get the weather in Paris' });
|
|
4772
|
+
* if (typeof result.function_id === 'string') {
|
|
4773
|
+
* const fn = client.NotteFunction({ function_id: result.function_id });
|
|
4774
|
+
* }
|
|
4775
|
+
* ```
|
|
4776
|
+
*
|
|
4777
|
+
* Rejects with a `NotteAPIError` when the API answers with a non-2xx status.
|
|
4778
|
+
*/
|
|
4779
|
+
async start(body) {
|
|
4780
|
+
const response = await anythingStart({
|
|
4781
|
+
client: this.client.getClient(),
|
|
4782
|
+
body,
|
|
4783
|
+
throwOnError: true
|
|
4784
|
+
});
|
|
4785
|
+
return response.data ?? {};
|
|
4786
|
+
}
|
|
4787
|
+
};
|
|
4788
|
+
|
|
4789
|
+
// src/secrets.ts
|
|
4790
|
+
var NotteSecrets = class {
|
|
4791
|
+
client;
|
|
4792
|
+
constructor(client2) {
|
|
4793
|
+
this.client = client2;
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
4796
|
+
* List secret metadata (never the values), optionally filtered by namespace.
|
|
4797
|
+
*
|
|
4798
|
+
* ```ts
|
|
4799
|
+
* const secrets = await client.secrets.list({ namespace: 'llm_provider' });
|
|
4800
|
+
* secrets.forEach(s => console.log(s.name, s.key_hint));
|
|
4801
|
+
* ```
|
|
4802
|
+
*/
|
|
4803
|
+
async list(options = {}) {
|
|
4804
|
+
const response = await listSecrets({ client: this.client.getClient(), query: options, throwOnError: true });
|
|
4805
|
+
return response.data?.items ?? [];
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Store (or overwrite) a secret and return its metadata.
|
|
4809
|
+
*
|
|
4810
|
+
* ```ts
|
|
4811
|
+
* const meta = await client.secrets.store({ namespace: 'function_env', name: 'API_TOKEN', value: 'xyz' });
|
|
4812
|
+
* console.log(meta.id, meta.key_hint);
|
|
4813
|
+
* ```
|
|
4814
|
+
*/
|
|
4815
|
+
async store(body) {
|
|
4816
|
+
const response = await storeSecret({ client: this.client.getClient(), body, throwOnError: true });
|
|
4817
|
+
return response.data;
|
|
4818
|
+
}
|
|
4819
|
+
/**
|
|
4820
|
+
* Read the value of a secret by name within a namespace.
|
|
4821
|
+
*
|
|
4822
|
+
* ```ts
|
|
4823
|
+
* const { value } = await client.secrets.get('API_TOKEN', 'function_env');
|
|
4824
|
+
* ```
|
|
4825
|
+
*/
|
|
4826
|
+
async get(name, namespace) {
|
|
4827
|
+
const response = await getSecret({
|
|
4828
|
+
client: this.client.getClient(),
|
|
4829
|
+
path: { name },
|
|
4830
|
+
query: { namespace },
|
|
4831
|
+
throwOnError: true
|
|
4832
|
+
});
|
|
4833
|
+
return response.data;
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Delete a secret by id (the `id` field of `SecretMetadata`).
|
|
4837
|
+
*
|
|
4838
|
+
* ```ts
|
|
4839
|
+
* const meta = await client.secrets.store({ namespace: 'function_env', name: 'TMP', value: 'x' });
|
|
4840
|
+
* await client.secrets.delete(meta.id);
|
|
4841
|
+
* ```
|
|
4842
|
+
*/
|
|
4843
|
+
async delete(secretId) {
|
|
4844
|
+
await deleteSecret({ client: this.client.getClient(), path: { secret_id: secretId }, throwOnError: true });
|
|
4845
|
+
}
|
|
4846
|
+
};
|
|
4847
|
+
|
|
4848
|
+
// src/usage.ts
|
|
4849
|
+
var NotteUsage = class {
|
|
4850
|
+
client;
|
|
4851
|
+
constructor(client2) {
|
|
4852
|
+
this.client = client2;
|
|
4853
|
+
}
|
|
4854
|
+
/**
|
|
4855
|
+
* Usage summary for a monthly period (current month by default).
|
|
4856
|
+
*
|
|
4857
|
+
* ```ts
|
|
4858
|
+
* const usage = await client.usage.get({ period: 'May 2025' });
|
|
4859
|
+
* console.log(`${usage.session_count} sessions, $${usage.total_cost}`);
|
|
4860
|
+
* ```
|
|
4861
|
+
*/
|
|
4862
|
+
async get(options = {}) {
|
|
4863
|
+
const response = await getUsage({ client: this.client.getClient(), query: options, throwOnError: true });
|
|
4864
|
+
return response.data;
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* Paginated per-request usage logs, optionally filtered by endpoint.
|
|
4868
|
+
*
|
|
4869
|
+
* ```ts
|
|
4870
|
+
* const page = await client.usage.logs({ endpoint: 'sessions.start', page: 1, page_size: 50 });
|
|
4871
|
+
* page.items.forEach(log => console.log(log.created_at, log.endpoint, log.duration_ms));
|
|
4872
|
+
* ```
|
|
4873
|
+
*/
|
|
4874
|
+
async logs(options = {}) {
|
|
4875
|
+
const response = await getUsageLogs({ client: this.client.getClient(), query: options, throwOnError: true });
|
|
4876
|
+
return response.data;
|
|
3117
4877
|
}
|
|
3118
4878
|
};
|
|
3119
4879
|
|
|
3120
4880
|
// src/client.ts
|
|
3121
|
-
var
|
|
4881
|
+
var DEFAULT_NOTTE_API_URL = "https://api.notte.cc";
|
|
4882
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
4883
|
+
var REQUEST_ORIGIN = "sdk-node";
|
|
4884
|
+
var TIMEOUT_HEADER = "x-notte-timeout-ms";
|
|
4885
|
+
var DB_PREVIEW_ENV = "NOTTE_DB_PREVIEW_BRANCH";
|
|
4886
|
+
var DB_PREVIEW_HEADER = "x-db-preview";
|
|
4887
|
+
var EXECUTION_ERROR_CLASS = "NotteApiExecutionError";
|
|
4888
|
+
function isRelativeProxyUrl(baseUrl) {
|
|
4889
|
+
return /^\/(?!\/)/.test(baseUrl);
|
|
4890
|
+
}
|
|
4891
|
+
var NotteClient3 = class {
|
|
4892
|
+
config;
|
|
4893
|
+
client = createClient();
|
|
3122
4894
|
constructor(config = {}) {
|
|
3123
4895
|
const apiKey = config.apiKey || process.env.NOTTE_API_KEY;
|
|
3124
|
-
const
|
|
4896
|
+
const baseUrl = config.baseUrl || process.env.NOTTE_API_URL || DEFAULT_NOTTE_API_URL;
|
|
4897
|
+
const dbPreview = config.dbPreview || process.env[DB_PREVIEW_ENV] || void 0;
|
|
4898
|
+
const timeoutMs = config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
4899
|
+
if (!(timeoutMs >= 0)) {
|
|
4900
|
+
throw new InvalidRequestError("timeoutMs must be a non-negative number");
|
|
4901
|
+
}
|
|
4902
|
+
const isProxyMode = isRelativeProxyUrl(baseUrl);
|
|
4903
|
+
if (!isProxyMode) {
|
|
4904
|
+
const url = new URL(baseUrl);
|
|
4905
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
4906
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
4907
|
+
throw new InvalidRequestError(
|
|
4908
|
+
"API base URL must use HTTPS (HTTP is only allowed on loopback for local development)"
|
|
4909
|
+
);
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
3125
4912
|
if (!apiKey && !isProxyMode) {
|
|
3126
|
-
throw new
|
|
4913
|
+
throw new AuthenticationError(
|
|
4914
|
+
"NOTTE_API_KEY needs to be provided. Pass config.apiKey or set the NOTTE_API_KEY environment variable."
|
|
4915
|
+
);
|
|
3127
4916
|
}
|
|
3128
|
-
this.config = {
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
client.setConfig({
|
|
4917
|
+
this.config = { baseUrl, apiKey, timeoutMs, dbPreview, verbose: config.verbose ?? false };
|
|
4918
|
+
if (baseUrl !== DEFAULT_NOTTE_API_URL && !isProxyMode) {
|
|
4919
|
+
console.warn(`NOTTE_API_URL is set to: ${baseUrl}`);
|
|
4920
|
+
}
|
|
4921
|
+
this.client.setConfig({
|
|
3134
4922
|
baseUrl: this.config.baseUrl,
|
|
3135
|
-
redirect: "manual"
|
|
4923
|
+
redirect: "manual",
|
|
4924
|
+
throwOnError: true
|
|
3136
4925
|
});
|
|
3137
|
-
client.interceptors.request.use((request) => {
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
4926
|
+
this.client.interceptors.request.use((request) => {
|
|
4927
|
+
const headers = new Headers(request.headers);
|
|
4928
|
+
if (this.config.apiKey) headers.set("Authorization", `Bearer ${this.config.apiKey}`);
|
|
4929
|
+
headers.set("x-notte-request-origin", REQUEST_ORIGIN);
|
|
4930
|
+
headers.set("x-notte-sdk-version", SDK_VERSION);
|
|
4931
|
+
if (this.config.dbPreview) headers.set(DB_PREVIEW_HEADER, this.config.dbPreview);
|
|
4932
|
+
const override = headers.get(TIMEOUT_HEADER);
|
|
4933
|
+
headers.delete(TIMEOUT_HEADER);
|
|
4934
|
+
const timeoutMs2 = override !== null ? Number(override) : this.config.timeoutMs;
|
|
4935
|
+
if (this.config.verbose) {
|
|
4936
|
+
console.info(`Making \`${request.method}\` request to \`${request.url}\``);
|
|
4937
|
+
}
|
|
4938
|
+
if (!(timeoutMs2 > 0)) {
|
|
4939
|
+
return new Request(request, { headers });
|
|
4940
|
+
}
|
|
4941
|
+
const deadline = withTimeout(request.signal, timeoutMs2);
|
|
4942
|
+
const timed = new Request(request, { headers, signal: deadline.signal });
|
|
4943
|
+
pendingDeadlines.set(timed, deadline.release);
|
|
4944
|
+
return timed;
|
|
3142
4945
|
});
|
|
3143
|
-
client.interceptors.response.use(async (response, request, opts) => {
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
4946
|
+
this.client.interceptors.response.use(async (response, request, opts) => {
|
|
4947
|
+
const initialUrl = new URL(request.url);
|
|
4948
|
+
let currentUrl = initialUrl;
|
|
4949
|
+
const headers = new Headers(request.headers);
|
|
4950
|
+
const apiUrl = new URL(this.config.baseUrl, initialUrl);
|
|
4951
|
+
const executionPrefix = `${apiUrl.pathname.replace(/\/$/, "")}/functions/`;
|
|
4952
|
+
const executionPath = initialUrl.pathname.slice(executionPrefix.length);
|
|
4953
|
+
const runtimeHandoff = request.method === "POST" && initialUrl.origin === apiUrl.origin && initialUrl.pathname.startsWith(executionPrefix) && /^[^/]+\/runs\/[^/]+$/.test(executionPath) && !executionPath.endsWith("/create");
|
|
4954
|
+
for (let hop = 0; (response.status === 307 || response.status === 308) && response.headers.get("location"); hop++) {
|
|
4955
|
+
if (hop >= 10) throw new InvalidRequestError("Too many API redirects");
|
|
4956
|
+
const location = new URL(response.headers.get("location"), currentUrl);
|
|
4957
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(location.hostname);
|
|
4958
|
+
if (location.protocol !== "https:" && !(location.protocol === "http:" && loopback)) {
|
|
4959
|
+
throw new InvalidRequestError(`Refusing to follow a redirect to an insecure URL: ${location.origin}`);
|
|
4960
|
+
}
|
|
4961
|
+
if (location.username || location.password) {
|
|
4962
|
+
throw new InvalidRequestError("Refusing to follow a redirect containing URL credentials");
|
|
4963
|
+
}
|
|
4964
|
+
if (location.origin !== currentUrl.origin) {
|
|
3148
4965
|
headers.delete("Authorization");
|
|
4966
|
+
const localHandoff = location.protocol === "http:" && initialUrl.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(initialUrl.hostname) && loopback;
|
|
4967
|
+
if (!(hop === 0 && runtimeHandoff && (location.protocol === "https:" || localHandoff))) {
|
|
4968
|
+
headers.delete("x-notte-api-key");
|
|
4969
|
+
}
|
|
3149
4970
|
}
|
|
4971
|
+
if (hop > 0) headers.delete("x-notte-api-key");
|
|
4972
|
+
await response.body?.cancel();
|
|
3150
4973
|
const headerRecord = {};
|
|
3151
4974
|
headers.forEach((v, k) => {
|
|
3152
4975
|
headerRecord[k] = v;
|
|
3153
4976
|
});
|
|
3154
|
-
|
|
4977
|
+
response = await (opts.fetch ?? fetch)(location.toString(), {
|
|
3155
4978
|
method: request.method,
|
|
3156
4979
|
headers: headerRecord,
|
|
3157
|
-
body: opts.serializedBody ?? void 0
|
|
4980
|
+
body: opts.serializedBody ?? void 0,
|
|
4981
|
+
signal: request.signal,
|
|
4982
|
+
redirect: "manual"
|
|
3158
4983
|
});
|
|
3159
|
-
|
|
4984
|
+
currentUrl = location;
|
|
3160
4985
|
}
|
|
3161
4986
|
const ct = response.headers.get("content-type") ?? "";
|
|
3162
|
-
if (ct.split(";")[0]?.trim() === "text/plain") {
|
|
4987
|
+
if (opts.parseAs !== "stream" && ct.split(";")[0]?.trim() === "text/plain") {
|
|
3163
4988
|
const body = await response.arrayBuffer();
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
4989
|
+
const headers2 = new Headers(response.headers);
|
|
4990
|
+
headers2.set("content-type", "application/json");
|
|
4991
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: headers2 });
|
|
3167
4992
|
}
|
|
3168
4993
|
return response;
|
|
3169
4994
|
});
|
|
4995
|
+
this.client.interceptors.response.use(async (response, request, opts) => {
|
|
4996
|
+
try {
|
|
4997
|
+
const parseAs = opts.parseAs === "auto" ? getParseAs(response.headers.get("content-type")) : opts.parseAs;
|
|
4998
|
+
if (parseAs !== "stream" && response.body) {
|
|
4999
|
+
const body = await response.arrayBuffer();
|
|
5000
|
+
const buffered = new Response(body, {
|
|
5001
|
+
status: response.status,
|
|
5002
|
+
statusText: response.statusText,
|
|
5003
|
+
headers: response.headers
|
|
5004
|
+
});
|
|
5005
|
+
Object.defineProperty(buffered, "url", { value: response.url });
|
|
5006
|
+
return buffered;
|
|
5007
|
+
}
|
|
5008
|
+
return response;
|
|
5009
|
+
} finally {
|
|
5010
|
+
releaseDeadline(request);
|
|
5011
|
+
}
|
|
5012
|
+
});
|
|
5013
|
+
this.client.interceptors.error.use((error, response, request) => {
|
|
5014
|
+
releaseDeadline(request);
|
|
5015
|
+
return this.toTypedError(error, response, request);
|
|
5016
|
+
});
|
|
5017
|
+
startVersionCheck();
|
|
5018
|
+
}
|
|
5019
|
+
/**
|
|
5020
|
+
* Map the raw value the generated client rejects with into a `NotteError`.
|
|
5021
|
+
* HTTP failures become `NotteAPIError` (or `NotteAPIExecutionError` when the
|
|
5022
|
+
* API flags them), timeouts become `NotteTimeoutError`, anything else is
|
|
5023
|
+
* returned untouched.
|
|
5024
|
+
*/
|
|
5025
|
+
toTypedError(error, response, request) {
|
|
5026
|
+
if (error instanceof NotteAPIError) {
|
|
5027
|
+
return error;
|
|
5028
|
+
}
|
|
5029
|
+
if (request?.signal.aborted) {
|
|
5030
|
+
error = request.signal.reason ?? error;
|
|
5031
|
+
}
|
|
5032
|
+
if (isTimeoutAbort(error)) {
|
|
5033
|
+
const target = request ? ` to \`${requestPath(request)}\`` : "";
|
|
5034
|
+
return new NotteTimeoutError(`Request${target} ${error.message}`, { cause: error });
|
|
5035
|
+
}
|
|
5036
|
+
if (request?.signal.aborted) return error;
|
|
5037
|
+
if (response) {
|
|
5038
|
+
const path = requestPath(request, response);
|
|
5039
|
+
const body = normalizeErrorBody(error);
|
|
5040
|
+
if (response.status === 422) {
|
|
5041
|
+
const latest = upgradeSuggestion();
|
|
5042
|
+
const message = typeof body.message === "string" ? body.message : "";
|
|
5043
|
+
const looksLikeSchemaMismatch = message.includes("extra_forbidden") || message.includes("Extra inputs are not permitted") || message.includes("'type':");
|
|
5044
|
+
if (latest && looksLikeSchemaMismatch) {
|
|
5045
|
+
return new NotteAPIError(
|
|
5046
|
+
path,
|
|
5047
|
+
response.status,
|
|
5048
|
+
{ ...body, message: createUpgradeErrorMessage("API returned 422 validation error", latest, message) },
|
|
5049
|
+
response
|
|
5050
|
+
);
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
if (response.headers.get("x-error-class") === EXECUTION_ERROR_CLASS) {
|
|
5054
|
+
return new NotteAPIExecutionError(path, response.status, body, response);
|
|
5055
|
+
}
|
|
5056
|
+
return new NotteAPIError(path, response.status, body, response);
|
|
5057
|
+
}
|
|
5058
|
+
return error;
|
|
5059
|
+
}
|
|
5060
|
+
/**
|
|
5061
|
+
* Add the database preview branch to a websocket URL served by the Notte API.
|
|
5062
|
+
* The websocket handshake reads the branch from the query string, so without
|
|
5063
|
+
* it a preview-branch session is looked up in the default database.
|
|
5064
|
+
*/
|
|
5065
|
+
withDbPreview(url) {
|
|
5066
|
+
if (!this.config.dbPreview) {
|
|
5067
|
+
return url;
|
|
5068
|
+
}
|
|
5069
|
+
const parsed = new URL(url);
|
|
5070
|
+
parsed.searchParams.set("db_preview", this.config.dbPreview);
|
|
5071
|
+
return parsed.toString();
|
|
5072
|
+
}
|
|
5073
|
+
/**
|
|
5074
|
+
* Health check the Notte API. Resolves when the API answers 200 and rejects
|
|
5075
|
+
* with a `NotteAPIError` (or a network error) otherwise.
|
|
5076
|
+
*/
|
|
5077
|
+
async healthCheck() {
|
|
5078
|
+
await healthCheck({ client: this.client });
|
|
3170
5079
|
}
|
|
3171
5080
|
/**
|
|
3172
5081
|
* Create a new session
|
|
@@ -3178,6 +5087,20 @@ var NotteClient = class {
|
|
|
3178
5087
|
Files(sessionId) {
|
|
3179
5088
|
return new SessionFiles(this.getClient(), sessionId);
|
|
3180
5089
|
}
|
|
5090
|
+
/**
|
|
5091
|
+
* File storage to attach to a session, the counterpart of `client.FileStorage()`.
|
|
5092
|
+
*
|
|
5093
|
+
* ```ts
|
|
5094
|
+
* const storage = client.FileStorage();
|
|
5095
|
+
* await client.Session({ storage }).use(async session => {
|
|
5096
|
+
* await storage.upload('./invoice.pdf');
|
|
5097
|
+
* const files = await storage.list({ source: 'session_download' });
|
|
5098
|
+
* });
|
|
5099
|
+
* ```
|
|
5100
|
+
*/
|
|
5101
|
+
FileStorage(sessionId) {
|
|
5102
|
+
return new RemoteFileStorage(this, sessionId);
|
|
5103
|
+
}
|
|
3181
5104
|
/**
|
|
3182
5105
|
* Create a new agent
|
|
3183
5106
|
*/
|
|
@@ -3202,6 +5125,66 @@ var NotteClient = class {
|
|
|
3202
5125
|
NotteFunction(options) {
|
|
3203
5126
|
return new NotteFunction(this, options);
|
|
3204
5127
|
}
|
|
5128
|
+
/** Alias of `NotteFunction`, matching `client.Function` in Python. */
|
|
5129
|
+
Function(options) {
|
|
5130
|
+
return this.NotteFunction(options);
|
|
5131
|
+
}
|
|
5132
|
+
/** Sessions listing, the counterpart of `client.sessions.list()`. */
|
|
5133
|
+
get sessions() {
|
|
5134
|
+
return {
|
|
5135
|
+
list: async (options = {}) => {
|
|
5136
|
+
const response = await listSessions({ client: this.getClient(), query: options });
|
|
5137
|
+
return response.data?.items ?? [];
|
|
5138
|
+
}
|
|
5139
|
+
};
|
|
5140
|
+
}
|
|
5141
|
+
/** Agents listing, the counterpart of `client.agents.list()`. */
|
|
5142
|
+
get agents() {
|
|
5143
|
+
return {
|
|
5144
|
+
list: async (options = {}) => {
|
|
5145
|
+
const response = await listAgents({ client: this.getClient(), query: options });
|
|
5146
|
+
return response.data?.items ?? [];
|
|
5147
|
+
}
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
/** Vaults listing, the counterpart of `client.vaults.list()`. */
|
|
5151
|
+
get vaults() {
|
|
5152
|
+
return {
|
|
5153
|
+
list: async (options = {}) => {
|
|
5154
|
+
const response = await listVaults({ client: this.getClient(), query: options });
|
|
5155
|
+
return response.data?.items ?? [];
|
|
5156
|
+
}
|
|
5157
|
+
};
|
|
5158
|
+
}
|
|
5159
|
+
/** Functions listing, the counterpart of `client.functions.list()`. */
|
|
5160
|
+
get functions() {
|
|
5161
|
+
return {
|
|
5162
|
+
list: async (options = {}) => {
|
|
5163
|
+
const response = await listFunctions({ client: this.getClient(), query: options });
|
|
5164
|
+
return response.data?.items ?? [];
|
|
5165
|
+
}
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
async search(query, options = {}) {
|
|
5169
|
+
const response = await searchWeb({
|
|
5170
|
+
client: this.getClient(),
|
|
5171
|
+
body: { ...options, q: query },
|
|
5172
|
+
throwOnError: true
|
|
5173
|
+
});
|
|
5174
|
+
return response.data;
|
|
5175
|
+
}
|
|
5176
|
+
/** Anything API (`POST /anything/start`). */
|
|
5177
|
+
get anything() {
|
|
5178
|
+
return new NotteAnything(this);
|
|
5179
|
+
}
|
|
5180
|
+
/** Workspace secrets (`/secrets`). */
|
|
5181
|
+
get secrets() {
|
|
5182
|
+
return new NotteSecrets(this);
|
|
5183
|
+
}
|
|
5184
|
+
/** Usage and billing (`/usage`, `/usage/logs`). */
|
|
5185
|
+
get usage() {
|
|
5186
|
+
return new NotteUsage(this);
|
|
5187
|
+
}
|
|
3205
5188
|
/**
|
|
3206
5189
|
* Personas client for listing and managing personas
|
|
3207
5190
|
*/
|
|
@@ -3212,9 +5195,6 @@ var NotteClient = class {
|
|
|
3212
5195
|
client: this.getClient(),
|
|
3213
5196
|
query: options || {}
|
|
3214
5197
|
});
|
|
3215
|
-
if (response?.error) {
|
|
3216
|
-
throw new Error(`Failed to list personas: ${JSON.stringify(response.error)}`);
|
|
3217
|
-
}
|
|
3218
5198
|
return response.data?.items || [];
|
|
3219
5199
|
}
|
|
3220
5200
|
};
|
|
@@ -3223,49 +5203,80 @@ var NotteClient = class {
|
|
|
3223
5203
|
* Get the configured client instance
|
|
3224
5204
|
*/
|
|
3225
5205
|
getClient() {
|
|
3226
|
-
return client;
|
|
5206
|
+
return this.client;
|
|
3227
5207
|
}
|
|
3228
5208
|
async scrape(url, options = {}) {
|
|
3229
|
-
const
|
|
3230
|
-
if (options.response_format && typeof options.response_format.parse === "function") {
|
|
3231
|
-
if (options.json_schema) {
|
|
3232
|
-
apiOptions.response_format = options.json_schema;
|
|
3233
|
-
} else {
|
|
3234
|
-
apiOptions.response_format = z3.toJSONSchema(options.response_format);
|
|
3235
|
-
}
|
|
3236
|
-
if (!apiOptions.instructions) {
|
|
3237
|
-
const schemaStr = JSON.stringify(apiOptions.response_format);
|
|
3238
|
-
if (schemaStr.includes("plans") || schemaStr.includes("plan")) {
|
|
3239
|
-
apiOptions.instructions = "Extract pricing plans from the page";
|
|
3240
|
-
} else if (schemaStr.includes("products") || schemaStr.includes("product")) {
|
|
3241
|
-
apiOptions.instructions = "Extract product information from the page";
|
|
3242
|
-
} else if (schemaStr.includes("articles") || schemaStr.includes("article")) {
|
|
3243
|
-
apiOptions.instructions = "Extract article information from the page";
|
|
3244
|
-
} else {
|
|
3245
|
-
apiOptions.instructions = "Extract structured data from the page based on the provided schema";
|
|
3246
|
-
}
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
5209
|
+
const body = await buildScrapeBody(options);
|
|
3249
5210
|
const response = await scrapeWebpage({
|
|
3250
5211
|
client: this.getClient(),
|
|
3251
|
-
body: {
|
|
3252
|
-
url,
|
|
3253
|
-
...apiOptions
|
|
3254
|
-
}
|
|
5212
|
+
body: { url, ...body }
|
|
3255
5213
|
});
|
|
3256
|
-
|
|
3257
|
-
throw new Error(`Failed to scrape webpage: ${JSON.stringify(response.error)}`);
|
|
3258
|
-
}
|
|
3259
|
-
const scrapeResponse = response.data;
|
|
3260
|
-
return processScrapeResponse(scrapeResponse, options);
|
|
5214
|
+
return processScrapeResponse(response.data, options);
|
|
3261
5215
|
}
|
|
3262
5216
|
/**
|
|
3263
5217
|
* Get the client configuration
|
|
3264
5218
|
*/
|
|
3265
5219
|
getConfig() {
|
|
3266
|
-
return this.config;
|
|
5220
|
+
return { ...this.config };
|
|
3267
5221
|
}
|
|
3268
5222
|
};
|
|
5223
|
+
function requestPath(request, response) {
|
|
5224
|
+
const url = request?.url ?? response?.url;
|
|
5225
|
+
if (!url) {
|
|
5226
|
+
return "unknown";
|
|
5227
|
+
}
|
|
5228
|
+
try {
|
|
5229
|
+
return new URL(url).pathname;
|
|
5230
|
+
} catch {
|
|
5231
|
+
return url;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
function isTimeoutAbort(error) {
|
|
5235
|
+
return typeof error === "object" && error !== null && error.name === "TimeoutError";
|
|
5236
|
+
}
|
|
5237
|
+
function timeoutReason(timeoutMs) {
|
|
5238
|
+
return new DOMException(`timed out after ${timeoutMs}ms`, "TimeoutError");
|
|
5239
|
+
}
|
|
5240
|
+
var pendingDeadlines = /* @__PURE__ */ new WeakMap();
|
|
5241
|
+
function releaseDeadline(request) {
|
|
5242
|
+
if (!request) return;
|
|
5243
|
+
const release = pendingDeadlines.get(request);
|
|
5244
|
+
if (release) {
|
|
5245
|
+
pendingDeadlines.delete(request);
|
|
5246
|
+
release();
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
function withTimeout(signal, timeoutMs) {
|
|
5250
|
+
const controller = new AbortController();
|
|
5251
|
+
const timer = setTimeout(() => controller.abort(timeoutReason(timeoutMs)), timeoutMs);
|
|
5252
|
+
timer.unref?.();
|
|
5253
|
+
let detach = () => void 0;
|
|
5254
|
+
let composite;
|
|
5255
|
+
const signalAny = AbortSignal.any;
|
|
5256
|
+
if (typeof signalAny === "function") {
|
|
5257
|
+
composite = signalAny.call(AbortSignal, signal ? [signal, controller.signal] : [controller.signal]);
|
|
5258
|
+
} else {
|
|
5259
|
+
composite = controller.signal;
|
|
5260
|
+
if (signal) {
|
|
5261
|
+
if (signal.aborted) {
|
|
5262
|
+
controller.abort(signal.reason);
|
|
5263
|
+
} else {
|
|
5264
|
+
const forward = () => controller.abort(signal.reason);
|
|
5265
|
+
signal.addEventListener("abort", forward, { once: true });
|
|
5266
|
+
detach = () => signal.removeEventListener("abort", forward);
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5270
|
+
let released = false;
|
|
5271
|
+
const release = () => {
|
|
5272
|
+
if (released) return;
|
|
5273
|
+
released = true;
|
|
5274
|
+
clearTimeout(timer);
|
|
5275
|
+
detach();
|
|
5276
|
+
};
|
|
5277
|
+
composite.addEventListener("abort", release, { once: true });
|
|
5278
|
+
return { signal: composite, release };
|
|
5279
|
+
}
|
|
3269
5280
|
|
|
3270
5281
|
// src/proxy/types.ts
|
|
3271
5282
|
var NotteProxyAuthError = class extends Error {
|
|
@@ -3276,33 +5287,65 @@ var NotteProxyAuthError = class extends Error {
|
|
|
3276
5287
|
};
|
|
3277
5288
|
|
|
3278
5289
|
// src/index.ts
|
|
3279
|
-
startVersionCheck();
|
|
3280
5290
|
var createClient2 = (config) => {
|
|
5291
|
+
const client2 = createClient({ baseUrl: config?.baseUrl || DEFAULT_NOTTE_API_URL });
|
|
3281
5292
|
if (config?.baseUrl) {
|
|
3282
|
-
|
|
5293
|
+
client2.setConfig({ baseUrl: config.baseUrl });
|
|
3283
5294
|
}
|
|
3284
5295
|
if (config?.token) {
|
|
3285
|
-
|
|
5296
|
+
client2.interceptors.request.use((request) => {
|
|
3286
5297
|
request.headers.set("Authorization", `Bearer ${config.token}`);
|
|
3287
5298
|
return request;
|
|
3288
5299
|
});
|
|
3289
5300
|
}
|
|
3290
|
-
return
|
|
5301
|
+
return client2;
|
|
3291
5302
|
};
|
|
3292
5303
|
export {
|
|
5304
|
+
ActionExecutionError,
|
|
3293
5305
|
Agent,
|
|
5306
|
+
AuthenticationError,
|
|
5307
|
+
CAPTCHA_SOLVE_TIMEOUT_MS,
|
|
5308
|
+
CLUSTER_OVERLOAD_RETRY_DELAY_MS,
|
|
5309
|
+
CREDENTIAL_FIELDS,
|
|
5310
|
+
DEFAULT_NOTTE_API_URL,
|
|
5311
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
3294
5312
|
Encryption,
|
|
3295
|
-
|
|
5313
|
+
EvaluateJsNoDataError,
|
|
5314
|
+
FUNCTION_RUN_ENDPOINTS,
|
|
5315
|
+
FUNCTION_RUN_TIMEOUT_MS,
|
|
5316
|
+
FailedToRunCloudFunctionError,
|
|
5317
|
+
FetchResponseDecodeError,
|
|
5318
|
+
FileExistsError,
|
|
5319
|
+
FileNotFoundError,
|
|
5320
|
+
InvalidRequestError,
|
|
5321
|
+
NotteAPIError,
|
|
5322
|
+
NotteAPIExecutionError,
|
|
5323
|
+
NotteAnything,
|
|
5324
|
+
NotteClient3 as NotteClient,
|
|
5325
|
+
NotteError,
|
|
3296
5326
|
NotteFunction,
|
|
3297
5327
|
NottePersona,
|
|
3298
5328
|
NotteProxyAuthError,
|
|
5329
|
+
NotteSecrets,
|
|
5330
|
+
NotteTimeoutError,
|
|
5331
|
+
NotteUsage,
|
|
3299
5332
|
NotteVault,
|
|
5333
|
+
PageFetchHTTPError,
|
|
5334
|
+
PageFetchResponse,
|
|
5335
|
+
RUN_API_KEY_HEADER,
|
|
5336
|
+
RemoteFileStorage,
|
|
5337
|
+
SESSION_START_TRIES,
|
|
5338
|
+
ScrapeFailedError,
|
|
3300
5339
|
Session,
|
|
3301
5340
|
SessionFiles,
|
|
5341
|
+
TIMEOUT_HEADER,
|
|
5342
|
+
actions,
|
|
3302
5343
|
agentStart,
|
|
3303
5344
|
agentStatus,
|
|
3304
5345
|
agentStop,
|
|
3305
5346
|
anythingStart,
|
|
5347
|
+
buildFetchScript,
|
|
5348
|
+
checkForLatestVersion,
|
|
3306
5349
|
client,
|
|
3307
5350
|
connectLinkContext,
|
|
3308
5351
|
connectLinkMailboxConnect,
|
|
@@ -3311,7 +5354,8 @@ export {
|
|
|
3311
5354
|
connectLinkStatus,
|
|
3312
5355
|
connectLinkSubmit,
|
|
3313
5356
|
createClient2 as createClient,
|
|
3314
|
-
|
|
5357
|
+
createOrAppendCookiesToFile,
|
|
5358
|
+
NotteClient3 as default,
|
|
3315
5359
|
deleteSecret,
|
|
3316
5360
|
deleteSessionFile,
|
|
3317
5361
|
downloadSessionFile,
|
|
@@ -3329,6 +5373,7 @@ export {
|
|
|
3329
5373
|
functionScheduleDelete,
|
|
3330
5374
|
functionScheduleSet,
|
|
3331
5375
|
functionUpdate,
|
|
5376
|
+
getRootDomain,
|
|
3332
5377
|
getScript,
|
|
3333
5378
|
getSecret,
|
|
3334
5379
|
getSessionScript,
|
|
@@ -3336,6 +5381,9 @@ export {
|
|
|
3336
5381
|
getUsageLogs,
|
|
3337
5382
|
healthCheck,
|
|
3338
5383
|
improvePrompt,
|
|
5384
|
+
installServerOwnedDialogPolicy,
|
|
5385
|
+
isCaptchaSolveAction,
|
|
5386
|
+
isValidMfaSecret,
|
|
3339
5387
|
listAgents,
|
|
3340
5388
|
listFunctionRunsByFunctionId,
|
|
3341
5389
|
listFunctions,
|
|
@@ -3351,18 +5399,21 @@ export {
|
|
|
3351
5399
|
mailboxSync,
|
|
3352
5400
|
mailboxUpdate,
|
|
3353
5401
|
managedAuthConnectLinkCreate,
|
|
3354
|
-
mintProxyCredentials,
|
|
3355
5402
|
nudgePrompt,
|
|
5403
|
+
observeServerOwnedDialog,
|
|
3356
5404
|
pageExecute,
|
|
3357
5405
|
pageObserve,
|
|
3358
5406
|
pageScrape,
|
|
3359
5407
|
pageScreenshot,
|
|
5408
|
+
parseAgentStatusMessage,
|
|
3360
5409
|
personaCreate,
|
|
3361
5410
|
personaDelete,
|
|
3362
5411
|
personaEmailsList,
|
|
3363
5412
|
personaGet,
|
|
3364
5413
|
personaSmsList,
|
|
3365
5414
|
personaUpdate,
|
|
5415
|
+
prepareAgentRequest,
|
|
5416
|
+
processScrapeResponse,
|
|
3366
5417
|
profileCookiesGet,
|
|
3367
5418
|
profileCookiesSet,
|
|
3368
5419
|
profileCreate,
|
|
@@ -3370,7 +5421,11 @@ export {
|
|
|
3370
5421
|
profileDuplicate,
|
|
3371
5422
|
profileGet,
|
|
3372
5423
|
profileList,
|
|
5424
|
+
readCookiesFile,
|
|
3373
5425
|
readyCheck,
|
|
5426
|
+
responseFromEvaluated,
|
|
5427
|
+
retry,
|
|
5428
|
+
sanitizeFilename,
|
|
3374
5429
|
scrapeFromHtml,
|
|
3375
5430
|
scrapeWebpage,
|
|
3376
5431
|
searchWeb,
|
|
@@ -3383,8 +5438,12 @@ export {
|
|
|
3383
5438
|
sessionStart,
|
|
3384
5439
|
sessionStatus,
|
|
3385
5440
|
sessionStop,
|
|
5441
|
+
startVersionCheck,
|
|
3386
5442
|
storeSecret,
|
|
3387
5443
|
uploadSessionFile,
|
|
5444
|
+
validateCredentials,
|
|
5445
|
+
validateMfaSecret,
|
|
5446
|
+
validateUrl,
|
|
3388
5447
|
vaultCreate,
|
|
3389
5448
|
vaultCredentialsAdd,
|
|
3390
5449
|
vaultCredentialsDelete,
|