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