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.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.19" : "dev";
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
- await this.stop();
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,