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/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Agent: () => Agent,
34
+ Encryption: () => Encryption,
34
35
  NotteClient: () => NotteClient,
35
36
  NotteFunction: () => NotteFunction,
36
37
  NottePersona: () => NottePersona,
@@ -55,6 +56,7 @@ __export(index_exports, {
55
56
  functionDelete: () => functionDelete,
56
57
  functionDownloadUrl: () => functionDownloadUrl,
57
58
  functionFork: () => functionFork,
59
+ functionMetadataUpdate: () => functionMetadataUpdate,
58
60
  functionRunGetMetadata: () => functionRunGetMetadata,
59
61
  functionRunStart: () => functionRunStart,
60
62
  functionRunStop: () => functionRunStop,
@@ -128,7 +130,7 @@ __export(index_exports, {
128
130
  module.exports = __toCommonJS(index_exports);
129
131
 
130
132
  // src/version.ts
131
- var SDK_VERSION = true ? "0.0.19" : "dev";
133
+ var SDK_VERSION = true ? "0.0.20" : "dev";
132
134
  var SDK_PACKAGE_NAME = true ? "notte-sdk" : "notte-sdk";
133
135
 
134
136
  // src/version-check.ts
@@ -1190,6 +1192,15 @@ var functionDownloadUrl = (options) => (options.client ?? client).get({
1190
1192
  url: "/functions/{function_id}",
1191
1193
  ...options
1192
1194
  });
1195
+ var functionMetadataUpdate = (options) => (options.client ?? client).patch({
1196
+ security: [{ scheme: "bearer", type: "http" }],
1197
+ url: "/functions/{function_id}",
1198
+ ...options,
1199
+ headers: {
1200
+ "Content-Type": "application/json",
1201
+ ...options.headers
1202
+ }
1203
+ });
1193
1204
  var functionUpdate = (options) => (options.client ?? client).post({
1194
1205
  ...formDataBodySerializer,
1195
1206
  security: [{ scheme: "bearer", type: "http" }],
@@ -1668,14 +1679,24 @@ var Session = class {
1668
1679
  this.isActive = false;
1669
1680
  this.response = null;
1670
1681
  this.client = client2;
1671
- const { open_viewer = false, ...sessionOptions } = options;
1682
+ const { open_viewer = false, headless, ...sessionOptions } = options;
1672
1683
  this.options = sessionOptions;
1673
1684
  this.openViewer = open_viewer;
1685
+ this.legacyHeadless = headless;
1674
1686
  }
1675
1687
  /**
1676
1688
  * Start the session - creates a new session on the server
1677
1689
  */
1678
1690
  async start() {
1691
+ if (this.legacyHeadless === false) {
1692
+ throw new Error(
1693
+ "Remote `headless: false` is no longer supported. `headless: true` sessions still include session replays and access to the live viewer."
1694
+ );
1695
+ }
1696
+ if (this.legacyHeadless === true) {
1697
+ console.warn("`headless: true` is deprecated and ignored; remote sessions always use the supported browser launch mode.");
1698
+ this.legacyHeadless = void 0;
1699
+ }
1679
1700
  if (this.isActive) {
1680
1701
  throw new Error("Session is already active");
1681
1702
  }
@@ -1703,7 +1724,7 @@ var Session = class {
1703
1724
  /**
1704
1725
  * Stop the session - terminates the session on the server
1705
1726
  */
1706
- async stop() {
1727
+ async stop(closeReason = "manual") {
1707
1728
  if (!this.isActive || !this.sessionId) {
1708
1729
  return;
1709
1730
  }
@@ -1711,6 +1732,9 @@ var Session = class {
1711
1732
  client: this.client.getClient(),
1712
1733
  path: {
1713
1734
  session_id: this.sessionId
1735
+ },
1736
+ query: {
1737
+ close_reason: closeReason
1714
1738
  }
1715
1739
  });
1716
1740
  if (response?.error) {
@@ -1929,10 +1953,21 @@ var Session = class {
1929
1953
  */
1930
1954
  async use(callback) {
1931
1955
  await this.start();
1956
+ let callbackFailed = false;
1932
1957
  try {
1933
1958
  return await callback(this);
1959
+ } catch (error) {
1960
+ callbackFailed = true;
1961
+ throw error;
1934
1962
  } finally {
1935
- await this.stop();
1963
+ try {
1964
+ await this.stop(callbackFailed ? "error" : "manual");
1965
+ } catch (stopError) {
1966
+ if (!callbackFailed) {
1967
+ throw stopError;
1968
+ }
1969
+ console.error("[Session] Failed to stop session after callback error:", stopError);
1970
+ }
1936
1971
  }
1937
1972
  }
1938
1973
  /**
@@ -2864,6 +2899,59 @@ var NottePersona = class {
2864
2899
  };
2865
2900
 
2866
2901
  // src/functions.ts
2902
+ var import_promises = require("fs/promises");
2903
+
2904
+ // src/encryption.ts
2905
+ var import_node_crypto = require("crypto");
2906
+ var SALT_LENGTH = 16;
2907
+ var PBKDF2_ITERATIONS = 1e5;
2908
+ var PBKDF2_KEY_LENGTH = 32;
2909
+ var FERNET_VERSION = 128;
2910
+ var FERNET_IV_LENGTH = 16;
2911
+ var FERNET_HMAC_LENGTH = 32;
2912
+ var FERNET_HEADER_LENGTH = 1 + 8 + FERNET_IV_LENGTH;
2913
+ var Encryption = class {
2914
+ constructor(rootKey) {
2915
+ if (!rootKey) {
2916
+ throw new Error("Root key cannot be empty");
2917
+ }
2918
+ this.rootKey = rootKey;
2919
+ }
2920
+ decrypt(token) {
2921
+ const combined = Buffer.from(token, "base64url");
2922
+ if (combined.length <= SALT_LENGTH) {
2923
+ throw new Error("Invalid encrypted token: too short to contain a salt");
2924
+ }
2925
+ const salt = combined.subarray(0, SALT_LENGTH);
2926
+ const fernetToken = combined.subarray(SALT_LENGTH).toString("utf8");
2927
+ const key = (0, import_node_crypto.pbkdf2Sync)(this.rootKey, salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH, "sha256");
2928
+ return decryptFernet(fernetToken, key);
2929
+ }
2930
+ };
2931
+ function decryptFernet(token, key) {
2932
+ const signingKey = key.subarray(0, 16);
2933
+ const encryptionKey = key.subarray(16);
2934
+ const data = Buffer.from(token, "base64url");
2935
+ if (data.length < FERNET_HEADER_LENGTH + FERNET_HMAC_LENGTH) {
2936
+ throw new Error("Invalid Fernet token: too short");
2937
+ }
2938
+ if (data[0] !== FERNET_VERSION) {
2939
+ throw new Error(`Invalid Fernet token: unsupported version 0x${data[0].toString(16)}`);
2940
+ }
2941
+ const signed = data.subarray(0, data.length - FERNET_HMAC_LENGTH);
2942
+ const signature = data.subarray(data.length - FERNET_HMAC_LENGTH);
2943
+ const expected = (0, import_node_crypto.createHmac)("sha256", signingKey).update(signed).digest();
2944
+ if (!(0, import_node_crypto.timingSafeEqual)(signature, expected)) {
2945
+ throw new Error("Invalid Fernet token: signature verification failed");
2946
+ }
2947
+ const iv = signed.subarray(9, FERNET_HEADER_LENGTH);
2948
+ const ciphertext = signed.subarray(FERNET_HEADER_LENGTH);
2949
+ const decipher = (0, import_node_crypto.createDecipheriv)("aes-128-cbc", encryptionKey, iv);
2950
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
2951
+ }
2952
+
2953
+ // src/functions.ts
2954
+ var DOWNLOAD_TIMEOUT_MS = 3e4;
2867
2955
  var NotteFunction = class {
2868
2956
  constructor(client2, options) {
2869
2957
  this.client = client2;
@@ -2929,6 +3017,72 @@ var NotteFunction = class {
2929
3017
  throw new Error(`Failed to run function: ${error instanceof Error ? error.message : String(error)}`);
2930
3018
  }
2931
3019
  }
3020
+ /**
3021
+ * Get the download URL of the function code, decrypting it when the API
3022
+ * returns an encrypted one (Notte managed functions).
3023
+ */
3024
+ async getUrl(options = {}) {
3025
+ let response;
3026
+ try {
3027
+ response = await functionDownloadUrl({
3028
+ client: this.client.getClient(),
3029
+ path: {
3030
+ function_id: this.functionId
3031
+ },
3032
+ query: options.version ? { version: options.version } : {}
3033
+ });
3034
+ } catch (error) {
3035
+ throw new Error(`Failed to get function download url: ${error instanceof Error ? error.message : String(error)}`);
3036
+ }
3037
+ if (response?.error) {
3038
+ throw new Error(`Failed to get function download url: ${formatError(response.error)}`);
3039
+ }
3040
+ const url = response.data.url;
3041
+ if (isHttpUrl(url)) {
3042
+ return url;
3043
+ }
3044
+ const decryptionKey = options.decryption_key ?? this.decryptionKey;
3045
+ if (!decryptionKey) {
3046
+ throw new Error(
3047
+ "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>' })."
3048
+ );
3049
+ }
3050
+ let decrypted;
3051
+ try {
3052
+ decrypted = new Encryption(decryptionKey).decrypt(url);
3053
+ } catch (error) {
3054
+ 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.`);
3055
+ }
3056
+ if (!isHttpUrl(decrypted)) {
3057
+ 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.");
3058
+ }
3059
+ return decrypted;
3060
+ }
3061
+ /**
3062
+ * Download the function code as a python file. Returns the code, and writes
3063
+ * it to `path` when one is provided.
3064
+ */
3065
+ async download(options = {}) {
3066
+ const { path } = options;
3067
+ if (path !== void 0 && !path.endsWith(".py")) {
3068
+ throw new Error(`Code file path must end with .py, got '${path}'`);
3069
+ }
3070
+ const url = await this.getUrl(options);
3071
+ let response;
3072
+ try {
3073
+ response = await fetch(url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
3074
+ } catch (error) {
3075
+ throw new Error(`Failed to download function code: ${error instanceof Error ? error.message : String(error)}`);
3076
+ }
3077
+ if (!response.ok) {
3078
+ throw new Error(`Failed to download function code: ${response.status} ${response.statusText}`);
3079
+ }
3080
+ const code = await response.text();
3081
+ if (path !== void 0) {
3082
+ await (0, import_promises.writeFile)(path, code, "utf8");
3083
+ }
3084
+ return code;
3085
+ }
2932
3086
  /**
2933
3087
  * Get the function ID
2934
3088
  */
@@ -2936,6 +3090,9 @@ var NotteFunction = class {
2936
3090
  return this.functionId;
2937
3091
  }
2938
3092
  };
3093
+ function isHttpUrl(value) {
3094
+ return value.startsWith("https://") || value.startsWith("http://");
3095
+ }
2939
3096
 
2940
3097
  // src/client.ts
2941
3098
  var import_zod3 = require("zod");
@@ -3109,6 +3266,7 @@ var createClient2 = (config) => {
3109
3266
  // Annotate the CommonJS export names for ESM import in node:
3110
3267
  0 && (module.exports = {
3111
3268
  Agent,
3269
+ Encryption,
3112
3270
  NotteClient,
3113
3271
  NotteFunction,
3114
3272
  NottePersona,
@@ -3132,6 +3290,7 @@ var createClient2 = (config) => {
3132
3290
  functionDelete,
3133
3291
  functionDownloadUrl,
3134
3292
  functionFork,
3293
+ functionMetadataUpdate,
3135
3294
  functionRunGetMetadata,
3136
3295
  functionRunStart,
3137
3296
  functionRunStop,