notte-sdk 0.0.19 → 0.0.20
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/README.md +28 -0
- package/dist/index.d.mts +508 -34
- package/dist/index.d.ts +508 -34
- package/dist/index.js +163 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/version.ts
|
|
9
|
-
var SDK_VERSION = true ? "0.0.
|
|
9
|
+
var SDK_VERSION = true ? "0.0.20" : "dev";
|
|
10
10
|
var SDK_PACKAGE_NAME = true ? "notte-sdk" : "notte-sdk";
|
|
11
11
|
|
|
12
12
|
// src/version-check.ts
|
|
@@ -1068,6 +1068,15 @@ var functionDownloadUrl = (options) => (options.client ?? client).get({
|
|
|
1068
1068
|
url: "/functions/{function_id}",
|
|
1069
1069
|
...options
|
|
1070
1070
|
});
|
|
1071
|
+
var functionMetadataUpdate = (options) => (options.client ?? client).patch({
|
|
1072
|
+
security: [{ scheme: "bearer", type: "http" }],
|
|
1073
|
+
url: "/functions/{function_id}",
|
|
1074
|
+
...options,
|
|
1075
|
+
headers: {
|
|
1076
|
+
"Content-Type": "application/json",
|
|
1077
|
+
...options.headers
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1071
1080
|
var functionUpdate = (options) => (options.client ?? client).post({
|
|
1072
1081
|
...formDataBodySerializer,
|
|
1073
1082
|
security: [{ scheme: "bearer", type: "http" }],
|
|
@@ -1546,14 +1555,24 @@ var Session = class {
|
|
|
1546
1555
|
this.isActive = false;
|
|
1547
1556
|
this.response = null;
|
|
1548
1557
|
this.client = client2;
|
|
1549
|
-
const { open_viewer = false, ...sessionOptions } = options;
|
|
1558
|
+
const { open_viewer = false, headless, ...sessionOptions } = options;
|
|
1550
1559
|
this.options = sessionOptions;
|
|
1551
1560
|
this.openViewer = open_viewer;
|
|
1561
|
+
this.legacyHeadless = headless;
|
|
1552
1562
|
}
|
|
1553
1563
|
/**
|
|
1554
1564
|
* Start the session - creates a new session on the server
|
|
1555
1565
|
*/
|
|
1556
1566
|
async start() {
|
|
1567
|
+
if (this.legacyHeadless === false) {
|
|
1568
|
+
throw new Error(
|
|
1569
|
+
"Remote `headless: false` is no longer supported. `headless: true` sessions still include session replays and access to the live viewer."
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
if (this.legacyHeadless === true) {
|
|
1573
|
+
console.warn("`headless: true` is deprecated and ignored; remote sessions always use the supported browser launch mode.");
|
|
1574
|
+
this.legacyHeadless = void 0;
|
|
1575
|
+
}
|
|
1557
1576
|
if (this.isActive) {
|
|
1558
1577
|
throw new Error("Session is already active");
|
|
1559
1578
|
}
|
|
@@ -1581,7 +1600,7 @@ var Session = class {
|
|
|
1581
1600
|
/**
|
|
1582
1601
|
* Stop the session - terminates the session on the server
|
|
1583
1602
|
*/
|
|
1584
|
-
async stop() {
|
|
1603
|
+
async stop(closeReason = "manual") {
|
|
1585
1604
|
if (!this.isActive || !this.sessionId) {
|
|
1586
1605
|
return;
|
|
1587
1606
|
}
|
|
@@ -1589,6 +1608,9 @@ var Session = class {
|
|
|
1589
1608
|
client: this.client.getClient(),
|
|
1590
1609
|
path: {
|
|
1591
1610
|
session_id: this.sessionId
|
|
1611
|
+
},
|
|
1612
|
+
query: {
|
|
1613
|
+
close_reason: closeReason
|
|
1592
1614
|
}
|
|
1593
1615
|
});
|
|
1594
1616
|
if (response?.error) {
|
|
@@ -1807,10 +1829,21 @@ var Session = class {
|
|
|
1807
1829
|
*/
|
|
1808
1830
|
async use(callback) {
|
|
1809
1831
|
await this.start();
|
|
1832
|
+
let callbackFailed = false;
|
|
1810
1833
|
try {
|
|
1811
1834
|
return await callback(this);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
callbackFailed = true;
|
|
1837
|
+
throw error;
|
|
1812
1838
|
} finally {
|
|
1813
|
-
|
|
1839
|
+
try {
|
|
1840
|
+
await this.stop(callbackFailed ? "error" : "manual");
|
|
1841
|
+
} catch (stopError) {
|
|
1842
|
+
if (!callbackFailed) {
|
|
1843
|
+
throw stopError;
|
|
1844
|
+
}
|
|
1845
|
+
console.error("[Session] Failed to stop session after callback error:", stopError);
|
|
1846
|
+
}
|
|
1814
1847
|
}
|
|
1815
1848
|
}
|
|
1816
1849
|
/**
|
|
@@ -2742,6 +2775,59 @@ var NottePersona = class {
|
|
|
2742
2775
|
};
|
|
2743
2776
|
|
|
2744
2777
|
// src/functions.ts
|
|
2778
|
+
import { writeFile } from "fs/promises";
|
|
2779
|
+
|
|
2780
|
+
// src/encryption.ts
|
|
2781
|
+
import { createDecipheriv, createHmac, pbkdf2Sync, timingSafeEqual } from "crypto";
|
|
2782
|
+
var SALT_LENGTH = 16;
|
|
2783
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
2784
|
+
var PBKDF2_KEY_LENGTH = 32;
|
|
2785
|
+
var FERNET_VERSION = 128;
|
|
2786
|
+
var FERNET_IV_LENGTH = 16;
|
|
2787
|
+
var FERNET_HMAC_LENGTH = 32;
|
|
2788
|
+
var FERNET_HEADER_LENGTH = 1 + 8 + FERNET_IV_LENGTH;
|
|
2789
|
+
var Encryption = class {
|
|
2790
|
+
constructor(rootKey) {
|
|
2791
|
+
if (!rootKey) {
|
|
2792
|
+
throw new Error("Root key cannot be empty");
|
|
2793
|
+
}
|
|
2794
|
+
this.rootKey = rootKey;
|
|
2795
|
+
}
|
|
2796
|
+
decrypt(token) {
|
|
2797
|
+
const combined = Buffer.from(token, "base64url");
|
|
2798
|
+
if (combined.length <= SALT_LENGTH) {
|
|
2799
|
+
throw new Error("Invalid encrypted token: too short to contain a salt");
|
|
2800
|
+
}
|
|
2801
|
+
const salt = combined.subarray(0, SALT_LENGTH);
|
|
2802
|
+
const fernetToken = combined.subarray(SALT_LENGTH).toString("utf8");
|
|
2803
|
+
const key = pbkdf2Sync(this.rootKey, salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH, "sha256");
|
|
2804
|
+
return decryptFernet(fernetToken, key);
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
function decryptFernet(token, key) {
|
|
2808
|
+
const signingKey = key.subarray(0, 16);
|
|
2809
|
+
const encryptionKey = key.subarray(16);
|
|
2810
|
+
const data = Buffer.from(token, "base64url");
|
|
2811
|
+
if (data.length < FERNET_HEADER_LENGTH + FERNET_HMAC_LENGTH) {
|
|
2812
|
+
throw new Error("Invalid Fernet token: too short");
|
|
2813
|
+
}
|
|
2814
|
+
if (data[0] !== FERNET_VERSION) {
|
|
2815
|
+
throw new Error(`Invalid Fernet token: unsupported version 0x${data[0].toString(16)}`);
|
|
2816
|
+
}
|
|
2817
|
+
const signed = data.subarray(0, data.length - FERNET_HMAC_LENGTH);
|
|
2818
|
+
const signature = data.subarray(data.length - FERNET_HMAC_LENGTH);
|
|
2819
|
+
const expected = createHmac("sha256", signingKey).update(signed).digest();
|
|
2820
|
+
if (!timingSafeEqual(signature, expected)) {
|
|
2821
|
+
throw new Error("Invalid Fernet token: signature verification failed");
|
|
2822
|
+
}
|
|
2823
|
+
const iv = signed.subarray(9, FERNET_HEADER_LENGTH);
|
|
2824
|
+
const ciphertext = signed.subarray(FERNET_HEADER_LENGTH);
|
|
2825
|
+
const decipher = createDecipheriv("aes-128-cbc", encryptionKey, iv);
|
|
2826
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/functions.ts
|
|
2830
|
+
var DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
2745
2831
|
var NotteFunction = class {
|
|
2746
2832
|
constructor(client2, options) {
|
|
2747
2833
|
this.client = client2;
|
|
@@ -2807,6 +2893,72 @@ var NotteFunction = class {
|
|
|
2807
2893
|
throw new Error(`Failed to run function: ${error instanceof Error ? error.message : String(error)}`);
|
|
2808
2894
|
}
|
|
2809
2895
|
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Get the download URL of the function code, decrypting it when the API
|
|
2898
|
+
* returns an encrypted one (Notte managed functions).
|
|
2899
|
+
*/
|
|
2900
|
+
async getUrl(options = {}) {
|
|
2901
|
+
let response;
|
|
2902
|
+
try {
|
|
2903
|
+
response = await functionDownloadUrl({
|
|
2904
|
+
client: this.client.getClient(),
|
|
2905
|
+
path: {
|
|
2906
|
+
function_id: this.functionId
|
|
2907
|
+
},
|
|
2908
|
+
query: options.version ? { version: options.version } : {}
|
|
2909
|
+
});
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
throw new Error(`Failed to get function download url: ${error instanceof Error ? error.message : String(error)}`);
|
|
2912
|
+
}
|
|
2913
|
+
if (response?.error) {
|
|
2914
|
+
throw new Error(`Failed to get function download url: ${formatError(response.error)}`);
|
|
2915
|
+
}
|
|
2916
|
+
const url = response.data.url;
|
|
2917
|
+
if (isHttpUrl(url)) {
|
|
2918
|
+
return url;
|
|
2919
|
+
}
|
|
2920
|
+
const decryptionKey = options.decryption_key ?? this.decryptionKey;
|
|
2921
|
+
if (!decryptionKey) {
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
"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>' })."
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
let decrypted;
|
|
2927
|
+
try {
|
|
2928
|
+
decrypted = new Encryption(decryptionKey).decrypt(url);
|
|
2929
|
+
} catch (error) {
|
|
2930
|
+
throw new Error(`Failed to decrypt the function download url: ${error instanceof Error ? error.message : String(error)}. Contact support@notte.cc if you need help.`);
|
|
2931
|
+
}
|
|
2932
|
+
if (!isHttpUrl(decrypted)) {
|
|
2933
|
+
throw new Error("Failed to decrypt the function download url: the decrypted value is not a URL. Contact support@notte.cc if you need help.");
|
|
2934
|
+
}
|
|
2935
|
+
return decrypted;
|
|
2936
|
+
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Download the function code as a python file. Returns the code, and writes
|
|
2939
|
+
* it to `path` when one is provided.
|
|
2940
|
+
*/
|
|
2941
|
+
async download(options = {}) {
|
|
2942
|
+
const { path } = options;
|
|
2943
|
+
if (path !== void 0 && !path.endsWith(".py")) {
|
|
2944
|
+
throw new Error(`Code file path must end with .py, got '${path}'`);
|
|
2945
|
+
}
|
|
2946
|
+
const url = await this.getUrl(options);
|
|
2947
|
+
let response;
|
|
2948
|
+
try {
|
|
2949
|
+
response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
2950
|
+
} catch (error) {
|
|
2951
|
+
throw new Error(`Failed to download function code: ${error instanceof Error ? error.message : String(error)}`);
|
|
2952
|
+
}
|
|
2953
|
+
if (!response.ok) {
|
|
2954
|
+
throw new Error(`Failed to download function code: ${response.status} ${response.statusText}`);
|
|
2955
|
+
}
|
|
2956
|
+
const code = await response.text();
|
|
2957
|
+
if (path !== void 0) {
|
|
2958
|
+
await writeFile(path, code, "utf8");
|
|
2959
|
+
}
|
|
2960
|
+
return code;
|
|
2961
|
+
}
|
|
2810
2962
|
/**
|
|
2811
2963
|
* Get the function ID
|
|
2812
2964
|
*/
|
|
@@ -2814,6 +2966,9 @@ var NotteFunction = class {
|
|
|
2814
2966
|
return this.functionId;
|
|
2815
2967
|
}
|
|
2816
2968
|
};
|
|
2969
|
+
function isHttpUrl(value) {
|
|
2970
|
+
return value.startsWith("https://") || value.startsWith("http://");
|
|
2971
|
+
}
|
|
2817
2972
|
|
|
2818
2973
|
// src/client.ts
|
|
2819
2974
|
import { z as z3 } from "zod";
|
|
@@ -2986,6 +3141,7 @@ var createClient2 = (config) => {
|
|
|
2986
3141
|
};
|
|
2987
3142
|
export {
|
|
2988
3143
|
Agent,
|
|
3144
|
+
Encryption,
|
|
2989
3145
|
NotteClient,
|
|
2990
3146
|
NotteFunction,
|
|
2991
3147
|
NottePersona,
|
|
@@ -3010,6 +3166,7 @@ export {
|
|
|
3010
3166
|
functionDelete,
|
|
3011
3167
|
functionDownloadUrl,
|
|
3012
3168
|
functionFork,
|
|
3169
|
+
functionMetadataUpdate,
|
|
3013
3170
|
functionRunGetMetadata,
|
|
3014
3171
|
functionRunStart,
|
|
3015
3172
|
functionRunStop,
|