From c9f8517252596faa951383b30f2ab062e6905161 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 13 Oct 2026 07:31:53 +0000 Subject: [PATCH 1/3] chore(deps): add @deepnote/cloud-sync and align bundled @deepnote packages Add @deepnote/cloud-sync 0.1.0 and @deepnote/cloud 0.2.1 as direct dependencies and move blocks, convert, database-integrations and runtime-core to the versions cloud-sync pins, so the desktop bundle resolves one copy of each. @deepnote/cli stays at 0.8.0 and keeps its own nested copies. Raise the runtime-core ws override to 8.21.3 and complete the default ServerInfo in the runtime-core test mock (exited, stderrTail, childPids). Closes #560 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_017pqrotDqxwXxXYVEG5Z7wh --- package-lock.json | 1411 ++++++++++++++++++++----- package.json | 12 +- src/test/mocks/deepnoteRuntimeCore.ts | 8 +- 3 files changed, 1150 insertions(+), 281 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4f504d120..8a64c805a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "license": "MIT", "dependencies": { "@c4312/evt": "^0.1.1", - "@deepnote/blocks": "^4.6.0", + "@deepnote/blocks": "^4.8.1", "@deepnote/cli": "0.8.0", - "@deepnote/convert": "^4.0.0", - "@deepnote/database-integrations": "^1.5.0", - "@deepnote/runtime-core": "^0.5.0", + "@deepnote/cloud": "0.2.1", + "@deepnote/cloud-sync": "0.1.0", + "@deepnote/convert": "^4.0.3", + "@deepnote/database-integrations": "^1.7.1", + "@deepnote/runtime-core": "^0.7.0", "@deepnote/sql-language-server": "^3.0.0", "@enonic/fnv-plus": "^1.3.0", "@jupyter-widgets/base": "^6.0.8", @@ -409,85 +411,129 @@ "dev": true, "license": "MIT" }, + "node_modules/@ai-sdk/anthropic": { + "version": "4.0.79", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-4.0.79.tgz", + "integrity": "sha512-bBAmgc8IcwJDz7RkyIHaGNrN6k84oqZu6K2aYcdekYwab8PNcxvUgp/LG1S/tSU3SBoS+blFYuMjTQrIg8WHRQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@ai-sdk/gateway": { - "version": "3.0.139", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.139.tgz", - "integrity": "sha512-RFpxyh5j9g7ZMfKxhiwCcF7bGU872o3JvoiIqZbHOM4qkR4RCzeKJF+k+zHomcJYGeUabEbeMXTKNASzz4Toxw==", + "version": "4.0.111", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.111.tgz", + "integrity": "sha512-+Uq8GgAsEkq3rv1AoYyqpCeHSOXG5ZP7KnsquM8yyIXkn1oBeZbFZvTtNE6gC9Eg1VK34KwImv8NrtB6tfm2wA==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59", "@vercel/oidc": "3.2.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "node_modules/@ai-sdk/mcp": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.55.tgz", - "integrity": "sha512-G2QSVjEP1Q9kclhdlfw5m/tV5YH0h+b45n2ANx5GGpCTeA1D0enp68773EWJMflGREhybsUXNmkaC+vLbLEzKg==", + "version": "2.0.74", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-2.0.74.tgz", + "integrity": "sha512-GrvM5rkNltQRsjDESieTFBSSNxTqk87gl4fbvU6eQ/osI4Mu7QhumCd6qLpVdqcgKleNlNs0u68HHVEe5hgnRg==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", - "pkce-challenge": "^5.0.0" + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.1" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "node_modules/@ai-sdk/openai": { - "version": "3.0.77", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.77.tgz", - "integrity": "sha512-DK0sGy/9j6KhgSrdhSKplTo3qf5frzGjLNYo9DVloH37L6DT42AxR/26UVCCoTEjR+WdsA9svm+5cT3Aye9NTg==", + "version": "4.0.92", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-4.0.92.tgz", + "integrity": "sha512-py1DdK0t1bvUOkRCJDU5saql95Kyed13Bo41GYVBKkbff2Lh5pHGAeNZBkOsVSQXr+Ah1qJdR/ZYnisoxvoP6Q==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33" + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" }, "engines": { - "node": ">=18" + "node": ">=22" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/openai-compatible": { + "version": "3.0.68", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.68.tgz", + "integrity": "sha512-xnBmn8wJvlRX4BtB2nJTzSoYHHD7OpZwUigwH0oKDzg5HfYx+rNFqOKUMQDUuOClhMyDRMn29q/rzp0M41vgQw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" + }, + "engines": { + "node": ">=22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "node_modules/@ai-sdk/provider": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.12.tgz", - "integrity": "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q==", + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.27.tgz", + "integrity": "sha512-TtWwm6SxFiPomlMIbTQa+yhJte+w4/oCsW+wJrJzMC3MqHpAY9e5dJF1Y4HxyKt3W2Csac0KiI76Ckio11TFYw==", "license": "Apache-2.0", "dependencies": { "json-schema": "^0.4.0" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "node_modules/@ai-sdk/provider-utils": { - "version": "4.0.33", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.33.tgz", - "integrity": "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA==", + "version": "5.0.59", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.59.tgz", + "integrity": "sha512-eaXBTkguxBFzS+KvxGRsbdkX9A7+R1g/C4jWx9mup0oSs3Cx5Mb427/byGpD2XL8SnMYT9O8LmXBmrgo+N3TJw==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/provider": "3.0.12", + "@ai-sdk/provider": "4.0.27", "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.8" + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@ai-sdk/provider-utils/node_modules/undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -2223,13 +2269,13 @@ } }, "node_modules/@deepnote/blocks": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", - "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.8.1.tgz", + "integrity": "sha512-RWNTvZ4bar31IQ/tSqF2zUwJUCd+KJneZeyHrcixv559t6+WVQweziJUlw2etguTkOxhn7Zmj+MO+/OJkVYk/Q==", "license": "Apache-2.0", "dependencies": { - "ts-dedent": "^2.2.0", - "yaml": "^2.8.3", + "ts-dedent": "^2.3.0", + "yaml": "^2.9.0", "zod": "3.25.76" }, "engines": { @@ -2277,6 +2323,183 @@ "deepnote": "dist/bin.js" } }, + "node_modules/@deepnote/cli/node_modules/@ai-sdk/gateway": { + "version": "3.0.212", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.212.tgz", + "integrity": "sha512-iTJSim/6LuSxiG3RF0FWU7bEQ8dF6EplcWci5Km4Vc8SUcR8vxFuC7T/pfCutW00pI/3WYsHm8RCQ5141/Xykw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/cli/node_modules/@ai-sdk/mcp": { + "version": "1.0.93", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.93.tgz", + "integrity": "sha512-d+PYwDtcpQ23lRu08LijaNN1KzuJ4nTloHjPl43xdqs10uUmLNxTmyOq7oLSBkdnUgSEzv8y2vuAyBUlNfZGpg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/cli/node_modules/@ai-sdk/openai": { + "version": "3.0.126", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.126.tgz", + "integrity": "sha512-IHOe6MQgrC22WDUA+rQcfK/H3rUmtgHFRYh/BsnpHzXPyVQyvhymKh5r7fgxQsJ8vVyp+esPpQ7/4fr8hr6Iuw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/cli/node_modules/@ai-sdk/provider": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.18.tgz", + "integrity": "sha512-IpefF5ssZVOZCD4Wu7zxt6PwZoGT2HLVjxpJ10Bjx6EAmRfLYJPjyrfxb0RYDlwvXnDb9nkQ7Do/oJVDuRFzew==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@deepnote/cli/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.57", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.57.tgz", + "integrity": "sha512-89a7sPvZqXsP3PigcAQLWntu/duIVEcN/RyChAB+j6p3C7NwW767sVttvL8r0+BetTg63uAXQoYHCZZueQKBSw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + }, + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/cli/node_modules/@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "license": "Apache-2.0", + "dependencies": { + "ts-dedent": "^2.2.0", + "yaml": "^2.8.3", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/cli/node_modules/@deepnote/cloud": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", + "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/database-integrations": "1.6.0", + "fflate": "^0.8.2", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/cli/node_modules/@deepnote/convert": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", + "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/blocks": "4.7.0", + "chalk": "^5.6.2", + "cleye": "^2.0.0", + "ora": "^9.0.0", + "yaml": "^2.8.3" + }, + "bin": { + "deepnote-convert": "dist/bin.js" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/cli/node_modules/@deepnote/database-integrations": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", + "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/blocks": "4.7.0", + "yaml": "^2.8.3", + "zod": "3.25.76" + } + }, + "node_modules/@deepnote/cli/node_modules/@deepnote/runtime-core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", + "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/mcp": "^1.0.25", + "@ai-sdk/openai": "^3.0.0", + "@deepnote/blocks": "4.7.0", + "@jupyterlab/nbformat": "^4.3.2", + "@jupyterlab/services": "^7.3.2", + "ai": "^6.0.0", + "tcp-port-used": "^1.0.2", + "ws": "^8.20.1", + "zod": "3.25.76" + } + }, + "node_modules/@deepnote/cli/node_modules/ai": { + "version": "6.0.304", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.304.tgz", + "integrity": "sha512-xhKVjs+pEGLVZ8mIbkv1UTThsNYnlr7lQRkroKFZDPYqhlPyUHGE+ICQsbAX4wyP0ELjej5/Bhd3a2owIf+UhQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.212", + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, "node_modules/@deepnote/cli/node_modules/ansi-regex": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", @@ -2374,6 +2597,27 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@deepnote/cli/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@deepnote/cli/node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -2384,13 +2628,29 @@ } }, "node_modules/@deepnote/cloud": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.2.1.tgz", + "integrity": "sha512-o01TEAQ2LxA4/ulWK+9onsqLdd6d9EwLIlD3oMeSLAqH2b+eYSmnbsLWgnFQ5fUpYTpPtinlVlui5pu/LbF3lA==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/database-integrations": "1.7.1", + "fflate": "^0.8.3", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/cloud-sync": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", - "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "resolved": "https://registry.npmjs.org/@deepnote/cloud-sync/-/cloud-sync-0.1.0.tgz", + "integrity": "sha512-GrN8a8xpnEeM2d8E7y5g4FVWhvZWMhw3JEEta8HW+rHjALrWAPmLp88CvQD1JgSvRn0sL4v6r36Yt8MVDP92AQ==", "license": "Apache-2.0", "dependencies": { - "@deepnote/database-integrations": "1.6.0", - "fflate": "^0.8.2", + "@deepnote/blocks": "4.8.1", + "@deepnote/cloud": "0.2.1", + "@deepnote/database-integrations": "1.7.1", "zod": "3.25.76" }, "engines": { @@ -2398,6 +2658,15 @@ "pnpm": ">=10.17.1" } }, + "node_modules/@deepnote/cloud-sync/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@deepnote/cloud/node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -2408,16 +2677,16 @@ } }, "node_modules/@deepnote/convert": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", - "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.3.tgz", + "integrity": "sha512-uEZxxrUAL4vNrlHdwSV7hOUgdcTMFHJqv1xWmrb8e2QtTpxL1YTACvAkT0ptRxpQN0Km6vagzLMqkNuiJRo21A==", "license": "Apache-2.0", "dependencies": { - "@deepnote/blocks": "4.7.0", + "@deepnote/blocks": "4.8.1", "chalk": "^5.6.2", - "cleye": "^2.0.0", - "ora": "^9.0.0", - "yaml": "^2.8.3" + "cleye": "^2.6.0", + "ora": "^9.4.1", + "yaml": "^2.9.0" }, "bin": { "deepnote-convert": "dist/bin.js" @@ -2428,13 +2697,13 @@ } }, "node_modules/@deepnote/database-integrations": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", - "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.7.1.tgz", + "integrity": "sha512-GbW+wibPjAd2/1fMOu4PlPdjfCu+aOoUWY6m6aHwCXmexutCoPN5H0U7RcxJB2qiafkEMUmtDwdSvxTqkbvFkA==", "license": "Apache-2.0", "dependencies": { - "@deepnote/blocks": "4.7.0", - "yaml": "^2.8.3", + "@deepnote/blocks": "4.8.1", + "yaml": "^2.9.0", "zod": "3.25.76" } }, @@ -2464,6 +2733,213 @@ "pnpm": ">=10.17.1" } }, + "node_modules/@deepnote/local-runner/node_modules/@ai-sdk/gateway": { + "version": "3.0.212", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.212.tgz", + "integrity": "sha512-iTJSim/6LuSxiG3RF0FWU7bEQ8dF6EplcWci5Km4Vc8SUcR8vxFuC7T/pfCutW00pI/3WYsHm8RCQ5141/Xykw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@vercel/oidc": "3.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@ai-sdk/mcp": { + "version": "1.0.93", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.93.tgz", + "integrity": "sha512-d+PYwDtcpQ23lRu08LijaNN1KzuJ4nTloHjPl43xdqs10uUmLNxTmyOq7oLSBkdnUgSEzv8y2vuAyBUlNfZGpg==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@ai-sdk/openai": { + "version": "3.0.126", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.126.tgz", + "integrity": "sha512-IHOe6MQgrC22WDUA+rQcfK/H3rUmtgHFRYh/BsnpHzXPyVQyvhymKh5r7fgxQsJ8vVyp+esPpQ7/4fr8hr6Iuw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@ai-sdk/provider": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.18.tgz", + "integrity": "sha512-IpefF5ssZVOZCD4Wu7zxt6PwZoGT2HLVjxpJ10Bjx6EAmRfLYJPjyrfxb0RYDlwvXnDb9nkQ7Do/oJVDuRFzew==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@ai-sdk/provider-utils": { + "version": "4.0.57", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.57.tgz", + "integrity": "sha512-89a7sPvZqXsP3PigcAQLWntu/duIVEcN/RyChAB+j6p3C7NwW767sVttvL8r0+BetTg63uAXQoYHCZZueQKBSw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.18", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + }, + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "license": "Apache-2.0", + "dependencies": { + "ts-dedent": "^2.2.0", + "yaml": "^2.8.3", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@deepnote/cloud": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", + "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/database-integrations": "1.6.0", + "fflate": "^0.8.2", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@deepnote/convert": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", + "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/blocks": "4.7.0", + "chalk": "^5.6.2", + "cleye": "^2.0.0", + "ora": "^9.0.0", + "yaml": "^2.8.3" + }, + "bin": { + "deepnote-convert": "dist/bin.js" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@deepnote/database-integrations": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", + "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "license": "Apache-2.0", + "dependencies": { + "@deepnote/blocks": "4.7.0", + "yaml": "^2.8.3", + "zod": "3.25.76" + } + }, + "node_modules/@deepnote/local-runner/node_modules/@deepnote/runtime-core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", + "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/mcp": "^1.0.25", + "@ai-sdk/openai": "^3.0.0", + "@deepnote/blocks": "4.7.0", + "@jupyterlab/nbformat": "^4.3.2", + "@jupyterlab/services": "^7.3.2", + "ai": "^6.0.0", + "tcp-port-used": "^1.0.2", + "ws": "^8.20.1", + "zod": "3.25.76" + } + }, + "node_modules/@deepnote/local-runner/node_modules/ai": { + "version": "6.0.304", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.304.tgz", + "integrity": "sha512-xhKVjs+pEGLVZ8mIbkv1UTThsNYnlr7lQRkroKFZDPYqhlPyUHGE+ICQsbAX4wyP0ELjej5/Bhd3a2owIf+UhQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.212", + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@opentelemetry/api": "^1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@deepnote/local-runner/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@deepnote/local-runner/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@deepnote/reactivity": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@deepnote/reactivity/-/reactivity-1.2.2.tgz", @@ -2474,6 +2950,21 @@ "zod": "3.25.76" } }, + "node_modules/@deepnote/reactivity/node_modules/@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "license": "Apache-2.0", + "dependencies": { + "ts-dedent": "^2.2.0", + "yaml": "^2.8.3", + "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0", + "pnpm": ">=10.17.1" + } + }, "node_modules/@deepnote/reactivity/node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -2484,26 +2975,32 @@ } }, "node_modules/@deepnote/runtime-core": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", - "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.7.0.tgz", + "integrity": "sha512-/3kPiDBNUaIsbuh3DWT0SI+8z7UPtaN9PRxEz21Qn8pQBIJD/UzMQDX+RSsXCGZTdSgi7EbrzzVFKr7Kv8/z2Q==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/mcp": "^1.0.25", - "@ai-sdk/openai": "^3.0.0", - "@deepnote/blocks": "4.7.0", - "@jupyterlab/nbformat": "^4.3.2", - "@jupyterlab/services": "^7.3.2", - "ai": "^6.0.0", - "tcp-port-used": "^1.0.2", - "ws": "^8.20.1", + "@ai-sdk/anthropic": "^4.0.76", + "@ai-sdk/mcp": "^2.0.71", + "@ai-sdk/openai": "^4.0.89", + "@ai-sdk/openai-compatible": "^3.0.66", + "@deepnote/blocks": "4.8.1", + "@jupyterlab/nbformat": "^4.6.3", + "@jupyterlab/services": "^7.6.3", + "@lumino/signaling": "^2.1.6", + "ai": "^7.0.133", + "tcp-port-used": "^1.0.3", + "ws": "^8.21.3", "zod": "3.25.76" + }, + "engines": { + "node": ">=22.14.0" } }, "node_modules/@deepnote/runtime-core/node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -5076,9 +5573,9 @@ } }, "node_modules/@jupyterlab/coreutils": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.5.1.tgz", - "integrity": "sha512-E95Zkvd9cTAnaT01iXkOAT2r7i6CYaFEs2e8NNlEnaQYgTB0xhUXRkYeK4tQ2Lc7uImyy3HKWfkkwH+iyc9x0w==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.6.4.tgz", + "integrity": "sha512-s787EqSgqtwTwhn/oQYOF/uveX4A/A1QlkXw/WD9iYNTfiIfwYtXkVo4uL2VopHWp+17Ikg4OqIbkqOfd/eeMQ==", "license": "BSD-3-Clause", "dependencies": { "@lumino/coreutils": "^2.2.2", @@ -5908,9 +6405,9 @@ "license": "MIT" }, "node_modules/@jupyterlab/nbformat": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.5.1.tgz", - "integrity": "sha512-aVPqZ7ao/RIwfPHNtTWy38J99zSi1/NZWOEfcnsb3Ap6p6HiCPI2LCKHk4SvBLIDzaFD68DesCA+ofac0P0bWw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.6.4.tgz", + "integrity": "sha512-p0oIcD2Ok4nh6qAxehVNjNjzzlKgkCxF4cNId1mVd0xf/glh+xXLJLhBkel8hI5yqVY07AUuu1izZge2XF6Rcg==", "license": "BSD-3-Clause", "dependencies": { "@lumino/coreutils": "^2.2.2" @@ -6033,16 +6530,16 @@ } }, "node_modules/@jupyterlab/services": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.5.1.tgz", - "integrity": "sha512-AwDp5mJHWxhj5E7Nqb49zA+4gMfuEx2pySs7k0ASNpPOpWD7eb5UtzZTAtlhQcY0SBRPWUGhMMMT/J56IgsEFg==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.6.4.tgz", + "integrity": "sha512-4phhiMy1vVpSOOtHyWKiBWzIZQ5rEbImgtKnqhR3cWUbZ2TW0Pwow9VFvkwSO0PMdoTcUn+KMIimJimuJdMZiQ==", "license": "BSD-3-Clause", "dependencies": { - "@jupyter/ydoc": "^3.1.0", - "@jupyterlab/coreutils": "^6.5.1", - "@jupyterlab/nbformat": "^4.5.1", - "@jupyterlab/settingregistry": "^4.5.1", - "@jupyterlab/statedb": "^4.5.1", + "@jupyter/ydoc": "^4.0.0", + "@jupyterlab/coreutils": "^6.6.4", + "@jupyterlab/nbformat": "^4.6.4", + "@jupyterlab/settingregistry": "^4.6.4", + "@jupyterlab/statedb": "^4.6.4", "@lumino/coreutils": "^2.2.2", "@lumino/disposable": "^2.1.5", "@lumino/polling": "^2.1.5", @@ -6051,6 +6548,20 @@ "ws": "^8.11.0" } }, + "node_modules/@jupyterlab/services/node_modules/@jupyter/ydoc": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-4.1.1.tgz", + "integrity": "sha512-fO6WMy2j+Hu+/SjfLycCQZq2OIPBPJtdgrY02mgaHUAsGJ+THQ9JmVgm/Lbai4x1+npXT663FJwVm7z0zJd3Pw==", + "license": "BSD-3-Clause", + "dependencies": { + "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", + "@lumino/coreutils": "^1.11.0 || ^2.0.0", + "@lumino/disposable": "^1.10.0 || ^2.0.0", + "@lumino/signaling": "^1.10.0 || ^2.0.0", + "y-protocols": "^1.0.5", + "yjs": "^13.5.40" + } + }, "node_modules/@jupyterlab/services/node_modules/ws": { "version": "7.5.11", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", @@ -6073,13 +6584,13 @@ } }, "node_modules/@jupyterlab/settingregistry": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.5.1.tgz", - "integrity": "sha512-3yJ2LUFMI32xV+sRmLAO4ooXUZPbHFkmivTn7dlJqAKO1XCgJJ3OtcEsXefIszTrjA+yj0Th15NL6vSGFgSTAQ==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.6.4.tgz", + "integrity": "sha512-g9p7GsRE41lakfRxPKy+jQCD4w4Vvnxk1IGo0YBevcQ4IHsQruknKUXrjX9SHMi87Ncj4JmsuujHWXrdBRknbg==", "license": "BSD-3-Clause", "dependencies": { - "@jupyterlab/nbformat": "^4.5.1", - "@jupyterlab/statedb": "^4.5.1", + "@jupyterlab/nbformat": "^4.6.4", + "@jupyterlab/statedb": "^4.6.4", "@lumino/commands": "^2.3.3", "@lumino/coreutils": "^2.2.2", "@lumino/disposable": "^2.1.5", @@ -6118,9 +6629,9 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/@jupyterlab/statedb": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.5.1.tgz", - "integrity": "sha512-hyvts9YWOB5UeMWAJ8JJCtLs2mNLvverQUC1lSCENmmIP89lAql/HGE9IJJXb6z+GS8eP9hZVaeC3LheFU51rw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.6.4.tgz", + "integrity": "sha512-BBXdGR3ykM5YEbA4B+MW7e31xEUVd3RCKRRZcQZ93BDMEzeukPVcbhDnB3sSk3mRC/KW131jAbdQmqH9SnHweg==", "license": "BSD-3-Clause", "dependencies": { "@lumino/commands": "^2.3.3", @@ -7311,6 +7822,15 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@oxlint-tsgolint/darwin-arm64": { "version": "7.0.2001", "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", @@ -11104,6 +11624,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@workflow/serde": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0.tgz", + "integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==", + "license": "Apache-2.0" + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -11243,32 +11769,22 @@ } }, "node_modules/ai": { - "version": "6.0.214", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.214.tgz", - "integrity": "sha512-9MlePEXT5pXtQv4fXqmiR0RG3DZU4Dbv+kU9ktEJC2COi2RH2WvI2GiyG9MuCqgPII6f1w+5kB5fNIiArqPzaQ==", + "version": "7.0.138", + "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.138.tgz", + "integrity": "sha512-rB2grfrM9se2GY/au4e5Eq0xadPM4+a+ip3EZ0ofedIPU8/GETWGjXO6gbzQDA+7nMaZnaUVaX2f+ByAj/LF8w==", "license": "Apache-2.0", "dependencies": { - "@ai-sdk/gateway": "3.0.139", - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", - "@opentelemetry/api": "^1.9.0" + "@ai-sdk/gateway": "4.0.111", + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" }, "engines": { - "node": ">=18" + "node": ">=22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, - "node_modules/ai/node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -13896,18 +14412,28 @@ } }, "node_modules/cleye": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cleye/-/cleye-2.0.1.tgz", - "integrity": "sha512-c7dha4cG1fF3TZOkzhRyhEvFxHbdVL5AuQ2+xEBB7dR0Y/mhgmuzIdG60+wRUIPyRCPhF17CDbwP+LZ/83PnOg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cleye/-/cleye-2.7.0.tgz", + "integrity": "sha512-vSxO418kPRu+oNh6dit+ZMUQf0fQYifZs/2cP85RQqbMtnlJ0tshkxyiMZlKOhMntct4hgi1GC1DXUS5T/zoxA==", "license": "MIT", "dependencies": { + "ansis": "4.2.0", "terminal-columns": "^2.0.0", - "type-flag": "^4.0.1" + "type-flag": "^4.5.2" }, "funding": { "url": "https://github.com/privatenumber/cleye?sponsor=1" } }, + "node_modules/cleye/node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -17791,9 +18317,9 @@ } }, "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -20547,11 +21073,12 @@ } }, "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/ipaddr.js": { @@ -21239,7 +21766,8 @@ "node_modules/is-url": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" }, "node_modules/is-valid-glob": { "version": "1.0.0", @@ -21358,13 +21886,14 @@ } }, "node_modules/is2": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", - "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" }, "engines": { "node": ">=v0.10.0" @@ -27312,9 +27841,9 @@ } }, "node_modules/ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", "license": "MIT", "dependencies": { "chalk": "^5.6.2", @@ -27323,9 +27852,8 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" }, "engines": { "node": ">=20" @@ -31834,9 +32362,9 @@ } }, "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", "license": "MIT", "engines": { "node": ">=18" @@ -32647,12 +33175,13 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "node_modules/tcp-port-used": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", - "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.3.tgz", + "integrity": "sha512-4CEQ3qRJYo+mtEbJ+OoQu3dF4TDkwaO3RDVC4UzP5cpAOIUWwuwPjD7sdxDFFqsMUjsXVVYBMlg/boAaloThMA==", + "license": "MIT", "dependencies": { "debug": "4.3.1", - "is2": "^2.0.6" + "is2": "2.0.1" } }, "node_modules/tcp-port-used/node_modules/debug": { @@ -33168,9 +33697,9 @@ } }, "node_modules/ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", "license": "MIT", "engines": { "node": ">=6.10" @@ -33321,9 +33850,9 @@ } }, "node_modules/type-flag": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/type-flag/-/type-flag-4.0.2.tgz", - "integrity": "sha512-diALGSg9wBpvowixHbIeCJtXNxUKJHUNR8oE67K9zuPkpBe5FMmg+dpevDWmEwGQDmn/RJD3m7szs6Ssdz5A4w==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/type-flag/-/type-flag-4.5.2.tgz", + "integrity": "sha512-v8O8xoLq3W6eKKYZvlyvk8LI46ryYkfnKrjGVdJzO+1LsxUu1ztvVAKFzQpphKQgS0ucinnbAiEGUbvibQmt/A==", "license": "MIT", "funding": { "url": "https://github.com/privatenumber/type-flag?sponsor=1" @@ -33612,7 +34141,6 @@ "version": "6.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18.17" @@ -36134,51 +36662,79 @@ "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", "dev": true }, + "@ai-sdk/anthropic": { + "version": "4.0.79", + "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-4.0.79.tgz", + "integrity": "sha512-bBAmgc8IcwJDz7RkyIHaGNrN6k84oqZu6K2aYcdekYwab8PNcxvUgp/LG1S/tSU3SBoS+blFYuMjTQrIg8WHRQ==", + "requires": { + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" + } + }, "@ai-sdk/gateway": { - "version": "3.0.139", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.139.tgz", - "integrity": "sha512-RFpxyh5j9g7ZMfKxhiwCcF7bGU872o3JvoiIqZbHOM4qkR4RCzeKJF+k+zHomcJYGeUabEbeMXTKNASzz4Toxw==", + "version": "4.0.111", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-4.0.111.tgz", + "integrity": "sha512-+Uq8GgAsEkq3rv1AoYyqpCeHSOXG5ZP7KnsquM8yyIXkn1oBeZbFZvTtNE6gC9Eg1VK34KwImv8NrtB6tfm2wA==", "requires": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59", "@vercel/oidc": "3.2.0" } }, "@ai-sdk/mcp": { - "version": "1.0.55", - "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.55.tgz", - "integrity": "sha512-G2QSVjEP1Q9kclhdlfw5m/tV5YH0h+b45n2ANx5GGpCTeA1D0enp68773EWJMflGREhybsUXNmkaC+vLbLEzKg==", + "version": "2.0.74", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-2.0.74.tgz", + "integrity": "sha512-GrvM5rkNltQRsjDESieTFBSSNxTqk87gl4fbvU6eQ/osI4Mu7QhumCd6qLpVdqcgKleNlNs0u68HHVEe5hgnRg==", "requires": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", - "pkce-challenge": "^5.0.0" + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.1" } }, "@ai-sdk/openai": { - "version": "3.0.77", - "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.77.tgz", - "integrity": "sha512-DK0sGy/9j6KhgSrdhSKplTo3qf5frzGjLNYo9DVloH37L6DT42AxR/26UVCCoTEjR+WdsA9svm+5cT3Aye9NTg==", + "version": "4.0.92", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-4.0.92.tgz", + "integrity": "sha512-py1DdK0t1bvUOkRCJDU5saql95Kyed13Bo41GYVBKkbff2Lh5pHGAeNZBkOsVSQXr+Ah1qJdR/ZYnisoxvoP6Q==", + "requires": { + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" + } + }, + "@ai-sdk/openai-compatible": { + "version": "3.0.68", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-3.0.68.tgz", + "integrity": "sha512-xnBmn8wJvlRX4BtB2nJTzSoYHHD7OpZwUigwH0oKDzg5HfYx+rNFqOKUMQDUuOClhMyDRMn29q/rzp0M41vgQw==", "requires": { - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33" + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" } }, "@ai-sdk/provider": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.12.tgz", - "integrity": "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q==", + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-4.0.27.tgz", + "integrity": "sha512-TtWwm6SxFiPomlMIbTQa+yhJte+w4/oCsW+wJrJzMC3MqHpAY9e5dJF1Y4HxyKt3W2Csac0KiI76Ckio11TFYw==", "requires": { "json-schema": "^0.4.0" } }, "@ai-sdk/provider-utils": { - "version": "4.0.33", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.33.tgz", - "integrity": "sha512-nJ0bAfegMAIJtrzMJtbzer1cS3nb7c7DsyU1S4nrPm7ZU0Mn6SBBZv5IGZZGTbpWTJwqKTSPeZJTXalbAxt1BA==", + "version": "5.0.59", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-5.0.59.tgz", + "integrity": "sha512-eaXBTkguxBFzS+KvxGRsbdkX9A7+R1g/C4jWx9mup0oSs3Cx5Mb427/byGpD2XL8SnMYT9O8LmXBmrgo+N3TJw==", "requires": { - "@ai-sdk/provider": "3.0.12", + "@ai-sdk/provider": "4.0.27", "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.8" + "@workflow/serde": "4.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^7.29.0" + }, + "dependencies": { + "undici": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==" + } } }, "@alloc/quick-lru": { @@ -37506,11 +38062,11 @@ "dev": true }, "@deepnote/blocks": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", - "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.8.1.tgz", + "integrity": "sha512-RWNTvZ4bar31IQ/tSqF2zUwJUCd+KJneZeyHrcixv559t6+WVQweziJUlw2etguTkOxhn7Zmj+MO+/OJkVYk/Q==", "requires": { - "ts-dedent": "^2.2.0", + "ts-dedent": "^2.3.0", "yaml": "2.8.3", "zod": "3.25.76" }, @@ -37549,6 +38105,124 @@ "zod": "3.25.76" }, "dependencies": { + "@ai-sdk/gateway": { + "version": "3.0.212", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.212.tgz", + "integrity": "sha512-iTJSim/6LuSxiG3RF0FWU7bEQ8dF6EplcWci5Km4Vc8SUcR8vxFuC7T/pfCutW00pI/3WYsHm8RCQ5141/Xykw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@vercel/oidc": "3.2.0" + } + }, + "@ai-sdk/mcp": { + "version": "1.0.93", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.93.tgz", + "integrity": "sha512-d+PYwDtcpQ23lRu08LijaNN1KzuJ4nTloHjPl43xdqs10uUmLNxTmyOq7oLSBkdnUgSEzv8y2vuAyBUlNfZGpg==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.0" + } + }, + "@ai-sdk/openai": { + "version": "3.0.126", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.126.tgz", + "integrity": "sha512-IHOe6MQgrC22WDUA+rQcfK/H3rUmtgHFRYh/BsnpHzXPyVQyvhymKh5r7fgxQsJ8vVyp+esPpQ7/4fr8hr6Iuw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57" + } + }, + "@ai-sdk/provider": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.18.tgz", + "integrity": "sha512-IpefF5ssZVOZCD4Wu7zxt6PwZoGT2HLVjxpJ10Bjx6EAmRfLYJPjyrfxb0RYDlwvXnDb9nkQ7Do/oJVDuRFzew==", + "requires": { + "json-schema": "^0.4.0" + } + }, + "@ai-sdk/provider-utils": { + "version": "4.0.57", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.57.tgz", + "integrity": "sha512-89a7sPvZqXsP3PigcAQLWntu/duIVEcN/RyChAB+j6p3C7NwW767sVttvL8r0+BetTg63uAXQoYHCZZueQKBSw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + } + }, + "@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "requires": { + "ts-dedent": "^2.2.0", + "yaml": "2.8.3", + "zod": "3.25.76" + } + }, + "@deepnote/cloud": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", + "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "requires": { + "@deepnote/database-integrations": "1.6.0", + "fflate": "^0.8.2", + "zod": "3.25.76" + } + }, + "@deepnote/convert": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", + "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "requires": { + "@deepnote/blocks": "4.7.0", + "chalk": "^5.6.2", + "cleye": "^2.0.0", + "ora": "^9.0.0", + "yaml": "2.8.3" + } + }, + "@deepnote/database-integrations": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", + "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "requires": { + "@deepnote/blocks": "4.7.0", + "yaml": "2.8.3", + "zod": "3.25.76" + } + }, + "@deepnote/runtime-core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", + "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", + "requires": { + "@ai-sdk/mcp": "^1.0.25", + "@ai-sdk/openai": "^3.0.0", + "@deepnote/blocks": "4.7.0", + "@jupyterlab/nbformat": "^4.3.2", + "@jupyterlab/services": "^7.3.2", + "ai": "^6.0.0", + "tcp-port-used": "^1.0.2", + "ws": "8.21.3", + "zod": "3.25.76" + } + }, + "ai": { + "version": "6.0.304", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.304.tgz", + "integrity": "sha512-xhKVjs+pEGLVZ8mIbkv1UTThsNYnlr7lQRkroKFZDPYqhlPyUHGE+ICQsbAX4wyP0ELjej5/Bhd3a2owIf+UhQ==", + "requires": { + "@ai-sdk/gateway": "3.0.212", + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@opentelemetry/api": "^1.9.0" + } + }, "ansi-regex": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", @@ -37602,6 +38276,12 @@ "strip-ansi": "^7.1.0" } }, + "ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "requires": {} + }, "zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -37610,12 +38290,30 @@ } }, "@deepnote/cloud": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.2.1.tgz", + "integrity": "sha512-o01TEAQ2LxA4/ulWK+9onsqLdd6d9EwLIlD3oMeSLAqH2b+eYSmnbsLWgnFQ5fUpYTpPtinlVlui5pu/LbF3lA==", + "requires": { + "@deepnote/database-integrations": "1.7.1", + "fflate": "^0.8.3", + "zod": "3.25.76" + }, + "dependencies": { + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + } + } + }, + "@deepnote/cloud-sync": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", - "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "resolved": "https://registry.npmjs.org/@deepnote/cloud-sync/-/cloud-sync-0.1.0.tgz", + "integrity": "sha512-GrN8a8xpnEeM2d8E7y5g4FVWhvZWMhw3JEEta8HW+rHjALrWAPmLp88CvQD1JgSvRn0sL4v6r36Yt8MVDP92AQ==", "requires": { - "@deepnote/database-integrations": "1.6.0", - "fflate": "^0.8.2", + "@deepnote/blocks": "4.8.1", + "@deepnote/cloud": "0.2.1", + "@deepnote/database-integrations": "1.7.1", "zod": "3.25.76" }, "dependencies": { @@ -37627,23 +38325,23 @@ } }, "@deepnote/convert": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", - "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.3.tgz", + "integrity": "sha512-uEZxxrUAL4vNrlHdwSV7hOUgdcTMFHJqv1xWmrb8e2QtTpxL1YTACvAkT0ptRxpQN0Km6vagzLMqkNuiJRo21A==", "requires": { - "@deepnote/blocks": "4.7.0", + "@deepnote/blocks": "4.8.1", "chalk": "^5.6.2", - "cleye": "^2.0.0", - "ora": "^9.0.0", + "cleye": "^2.6.0", + "ora": "^9.4.1", "yaml": "2.8.3" } }, "@deepnote/database-integrations": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", - "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.7.1.tgz", + "integrity": "sha512-GbW+wibPjAd2/1fMOu4PlPdjfCu+aOoUWY6m6aHwCXmexutCoPN5H0U7RcxJB2qiafkEMUmtDwdSvxTqkbvFkA==", "requires": { - "@deepnote/blocks": "4.7.0", + "@deepnote/blocks": "4.8.1", "yaml": "2.8.3", "zod": "3.25.76" }, @@ -37665,6 +38363,137 @@ "@deepnote/convert": "4.0.1", "@deepnote/runtime-core": "0.5.0", "@jupyterlab/nbformat": "^4.3.2" + }, + "dependencies": { + "@ai-sdk/gateway": { + "version": "3.0.212", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.212.tgz", + "integrity": "sha512-iTJSim/6LuSxiG3RF0FWU7bEQ8dF6EplcWci5Km4Vc8SUcR8vxFuC7T/pfCutW00pI/3WYsHm8RCQ5141/Xykw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@vercel/oidc": "3.2.0" + } + }, + "@ai-sdk/mcp": { + "version": "1.0.93", + "resolved": "https://registry.npmjs.org/@ai-sdk/mcp/-/mcp-1.0.93.tgz", + "integrity": "sha512-d+PYwDtcpQ23lRu08LijaNN1KzuJ4nTloHjPl43xdqs10uUmLNxTmyOq7oLSBkdnUgSEzv8y2vuAyBUlNfZGpg==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "cross-spawn": "^7.0.6", + "pkce-challenge": "^5.0.0" + } + }, + "@ai-sdk/openai": { + "version": "3.0.126", + "resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.126.tgz", + "integrity": "sha512-IHOe6MQgrC22WDUA+rQcfK/H3rUmtgHFRYh/BsnpHzXPyVQyvhymKh5r7fgxQsJ8vVyp+esPpQ7/4fr8hr6Iuw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57" + } + }, + "@ai-sdk/provider": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.18.tgz", + "integrity": "sha512-IpefF5ssZVOZCD4Wu7zxt6PwZoGT2HLVjxpJ10Bjx6EAmRfLYJPjyrfxb0RYDlwvXnDb9nkQ7Do/oJVDuRFzew==", + "requires": { + "json-schema": "^0.4.0" + } + }, + "@ai-sdk/provider-utils": { + "version": "4.0.57", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.57.tgz", + "integrity": "sha512-89a7sPvZqXsP3PigcAQLWntu/duIVEcN/RyChAB+j6p3C7NwW767sVttvL8r0+BetTg63uAXQoYHCZZueQKBSw==", + "requires": { + "@ai-sdk/provider": "3.0.18", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.8", + "undici": "^6.28.0" + } + }, + "@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "requires": { + "ts-dedent": "^2.2.0", + "yaml": "2.8.3", + "zod": "3.25.76" + } + }, + "@deepnote/cloud": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@deepnote/cloud/-/cloud-0.1.0.tgz", + "integrity": "sha512-H71/mTymgYidKKPdEYKsyo4yW4UWWe1x+l7r7WIwxWwMAz9HbR5UZQE5xeKRb2GWDT5ohdAOdQ5a8LtJiMnruw==", + "requires": { + "@deepnote/database-integrations": "1.6.0", + "fflate": "^0.8.2", + "zod": "3.25.76" + } + }, + "@deepnote/convert": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@deepnote/convert/-/convert-4.0.1.tgz", + "integrity": "sha512-0Kb3SHZyU1rxWb0ynVkxzLEBd3BYkgShYTTFdqMm9rOXF2Bxn5yklxZBMb9SyA4yHqLx9sFv9c/+/X2SLJYDgA==", + "requires": { + "@deepnote/blocks": "4.7.0", + "chalk": "^5.6.2", + "cleye": "^2.0.0", + "ora": "^9.0.0", + "yaml": "2.8.3" + } + }, + "@deepnote/database-integrations": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@deepnote/database-integrations/-/database-integrations-1.6.0.tgz", + "integrity": "sha512-Xy8Lbo10KNUL6PDW6qXMF0opyKgmbZfV3HKXVPZrBl7IsZnJmTiHPPNd5neGQ3coefsjcs5Kl17o9iPEX2kw2Q==", + "requires": { + "@deepnote/blocks": "4.7.0", + "yaml": "2.8.3", + "zod": "3.25.76" + } + }, + "@deepnote/runtime-core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", + "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", + "requires": { + "@ai-sdk/mcp": "^1.0.25", + "@ai-sdk/openai": "^3.0.0", + "@deepnote/blocks": "4.7.0", + "@jupyterlab/nbformat": "^4.3.2", + "@jupyterlab/services": "^7.3.2", + "ai": "^6.0.0", + "tcp-port-used": "^1.0.2", + "ws": "8.21.3", + "zod": "3.25.76" + } + }, + "ai": { + "version": "6.0.304", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.304.tgz", + "integrity": "sha512-xhKVjs+pEGLVZ8mIbkv1UTThsNYnlr7lQRkroKFZDPYqhlPyUHGE+ICQsbAX4wyP0ELjej5/Bhd3a2owIf+UhQ==", + "requires": { + "@ai-sdk/gateway": "3.0.212", + "@ai-sdk/provider": "3.0.18", + "@ai-sdk/provider-utils": "4.0.57", + "@opentelemetry/api": "^1.9.0" + } + }, + "ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "requires": {} + }, + "zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==" + } } }, "@deepnote/reactivity": { @@ -37676,6 +38505,16 @@ "zod": "3.25.76" }, "dependencies": { + "@deepnote/blocks": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/blocks/-/blocks-4.7.0.tgz", + "integrity": "sha512-GW5jIpO2Sr7R2LaokuF8js60FHw4DLk1e4bq6BnMs2QkxGr2QF6JbRONl82V7zEV3iJQesXX+mFuTMWp9jCxPA==", + "requires": { + "ts-dedent": "^2.2.0", + "yaml": "2.8.3", + "zod": "3.25.76" + } + }, "zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", @@ -37684,25 +38523,28 @@ } }, "@deepnote/runtime-core": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.5.0.tgz", - "integrity": "sha512-+0Dbs5IhSsRFLg5XZqmr1gKo0GPeMwKNxezhAP6MgYmVECvxb+kHRoI5i4aazRsni/hk8i5AqipyYKYu4TT3Og==", - "requires": { - "@ai-sdk/mcp": "^1.0.25", - "@ai-sdk/openai": "^3.0.0", - "@deepnote/blocks": "4.7.0", - "@jupyterlab/nbformat": "^4.3.2", - "@jupyterlab/services": "^7.3.2", - "ai": "^6.0.0", - "tcp-port-used": "^1.0.2", - "ws": "8.21.0", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@deepnote/runtime-core/-/runtime-core-0.7.0.tgz", + "integrity": "sha512-/3kPiDBNUaIsbuh3DWT0SI+8z7UPtaN9PRxEz21Qn8pQBIJD/UzMQDX+RSsXCGZTdSgi7EbrzzVFKr7Kv8/z2Q==", + "requires": { + "@ai-sdk/anthropic": "^4.0.76", + "@ai-sdk/mcp": "^2.0.71", + "@ai-sdk/openai": "^4.0.89", + "@ai-sdk/openai-compatible": "^3.0.66", + "@deepnote/blocks": "4.8.1", + "@jupyterlab/nbformat": "^4.6.3", + "@jupyterlab/services": "^7.6.3", + "@lumino/signaling": "^2.1.6", + "ai": "^7.0.133", + "tcp-port-used": "^1.0.3", + "ws": "8.21.3", "zod": "3.25.76" }, "dependencies": { "ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "requires": {} }, "zod": { @@ -39405,9 +40247,9 @@ } }, "@jupyterlab/coreutils": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.5.1.tgz", - "integrity": "sha512-E95Zkvd9cTAnaT01iXkOAT2r7i6CYaFEs2e8NNlEnaQYgTB0xhUXRkYeK4tQ2Lc7uImyy3HKWfkkwH+iyc9x0w==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-6.6.4.tgz", + "integrity": "sha512-s787EqSgqtwTwhn/oQYOF/uveX4A/A1QlkXw/WD9iYNTfiIfwYtXkVo4uL2VopHWp+17Ikg4OqIbkqOfd/eeMQ==", "requires": { "@lumino/coreutils": "^2.2.2", "@lumino/disposable": "^2.1.5", @@ -40014,9 +40856,9 @@ } }, "@jupyterlab/nbformat": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.5.1.tgz", - "integrity": "sha512-aVPqZ7ao/RIwfPHNtTWy38J99zSi1/NZWOEfcnsb3Ap6p6HiCPI2LCKHk4SvBLIDzaFD68DesCA+ofac0P0bWw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-4.6.4.tgz", + "integrity": "sha512-p0oIcD2Ok4nh6qAxehVNjNjzzlKgkCxF4cNId1mVd0xf/glh+xXLJLhBkel8hI5yqVY07AUuu1izZge2XF6Rcg==", "requires": { "@lumino/coreutils": "^2.2.2" } @@ -40131,15 +40973,15 @@ } }, "@jupyterlab/services": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.5.1.tgz", - "integrity": "sha512-AwDp5mJHWxhj5E7Nqb49zA+4gMfuEx2pySs7k0ASNpPOpWD7eb5UtzZTAtlhQcY0SBRPWUGhMMMT/J56IgsEFg==", - "requires": { - "@jupyter/ydoc": "^3.1.0", - "@jupyterlab/coreutils": "^6.5.1", - "@jupyterlab/nbformat": "^4.5.1", - "@jupyterlab/settingregistry": "^4.5.1", - "@jupyterlab/statedb": "^4.5.1", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-7.6.4.tgz", + "integrity": "sha512-4phhiMy1vVpSOOtHyWKiBWzIZQ5rEbImgtKnqhR3cWUbZ2TW0Pwow9VFvkwSO0PMdoTcUn+KMIimJimuJdMZiQ==", + "requires": { + "@jupyter/ydoc": "^4.0.0", + "@jupyterlab/coreutils": "^6.6.4", + "@jupyterlab/nbformat": "^4.6.4", + "@jupyterlab/settingregistry": "^4.6.4", + "@jupyterlab/statedb": "^4.6.4", "@lumino/coreutils": "^2.2.2", "@lumino/disposable": "^2.1.5", "@lumino/polling": "^2.1.5", @@ -40148,6 +40990,19 @@ "ws": "7.5.11" }, "dependencies": { + "@jupyter/ydoc": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@jupyter/ydoc/-/ydoc-4.1.1.tgz", + "integrity": "sha512-fO6WMy2j+Hu+/SjfLycCQZq2OIPBPJtdgrY02mgaHUAsGJ+THQ9JmVgm/Lbai4x1+npXT663FJwVm7z0zJd3Pw==", + "requires": { + "@jupyterlab/nbformat": "^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0", + "@lumino/coreutils": "^1.11.0 || ^2.0.0", + "@lumino/disposable": "^1.10.0 || ^2.0.0", + "@lumino/signaling": "^1.10.0 || ^2.0.0", + "y-protocols": "^1.0.5", + "yjs": "^13.5.40" + } + }, "ws": { "version": "7.5.11", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", @@ -40157,12 +41012,12 @@ } }, "@jupyterlab/settingregistry": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.5.1.tgz", - "integrity": "sha512-3yJ2LUFMI32xV+sRmLAO4ooXUZPbHFkmivTn7dlJqAKO1XCgJJ3OtcEsXefIszTrjA+yj0Th15NL6vSGFgSTAQ==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-4.6.4.tgz", + "integrity": "sha512-g9p7GsRE41lakfRxPKy+jQCD4w4Vvnxk1IGo0YBevcQ4IHsQruknKUXrjX9SHMi87Ncj4JmsuujHWXrdBRknbg==", "requires": { - "@jupyterlab/nbformat": "^4.5.1", - "@jupyterlab/statedb": "^4.5.1", + "@jupyterlab/nbformat": "^4.6.4", + "@jupyterlab/statedb": "^4.6.4", "@lumino/commands": "^2.3.3", "@lumino/coreutils": "^2.2.2", "@lumino/disposable": "^2.1.5", @@ -40196,9 +41051,9 @@ } }, "@jupyterlab/statedb": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.5.1.tgz", - "integrity": "sha512-hyvts9YWOB5UeMWAJ8JJCtLs2mNLvverQUC1lSCENmmIP89lAql/HGE9IJJXb6z+GS8eP9hZVaeC3LheFU51rw==", + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-4.6.4.tgz", + "integrity": "sha512-BBXdGR3ykM5YEbA4B+MW7e31xEUVd3RCKRRZcQZ93BDMEzeukPVcbhDnB3sSk3mRC/KW131jAbdQmqH9SnHweg==", "requires": { "@lumino/commands": "^2.3.3", "@lumino/coreutils": "^2.2.2", @@ -41135,6 +41990,11 @@ "@octokit/openapi-types": "^27.0.0" } }, + "@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==" + }, "@oxlint-tsgolint/darwin-arm64": { "version": "7.0.2001", "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", @@ -43822,6 +44682,11 @@ "integrity": "sha512-lbnfCMoVe6O7aHL0TkQJSU+yd5Hs1A6mpr/zG/C+ibFXlhGq7rUEIHt4krf6P9gn0rgoAYJceJkvLHnvDWk5/A==", "dev": true }, + "@workflow/serde": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@workflow/serde/-/serde-4.1.0.tgz", + "integrity": "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ==" + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -43922,21 +44787,13 @@ } }, "ai": { - "version": "6.0.214", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.214.tgz", - "integrity": "sha512-9MlePEXT5pXtQv4fXqmiR0RG3DZU4Dbv+kU9ktEJC2COi2RH2WvI2GiyG9MuCqgPII6f1w+5kB5fNIiArqPzaQ==", + "version": "7.0.138", + "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.138.tgz", + "integrity": "sha512-rB2grfrM9se2GY/au4e5Eq0xadPM4+a+ip3EZ0ofedIPU8/GETWGjXO6gbzQDA+7nMaZnaUVaX2f+ByAj/LF8w==", "requires": { - "@ai-sdk/gateway": "3.0.139", - "@ai-sdk/provider": "3.0.12", - "@ai-sdk/provider-utils": "4.0.33", - "@opentelemetry/api": "^1.9.0" - }, - "dependencies": { - "@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==" - } + "@ai-sdk/gateway": "4.0.111", + "@ai-sdk/provider": "4.0.27", + "@ai-sdk/provider-utils": "5.0.59" } }, "ajv": { @@ -45741,12 +46598,20 @@ } }, "cleye": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cleye/-/cleye-2.0.1.tgz", - "integrity": "sha512-c7dha4cG1fF3TZOkzhRyhEvFxHbdVL5AuQ2+xEBB7dR0Y/mhgmuzIdG60+wRUIPyRCPhF17CDbwP+LZ/83PnOg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cleye/-/cleye-2.7.0.tgz", + "integrity": "sha512-vSxO418kPRu+oNh6dit+ZMUQf0fQYifZs/2cP85RQqbMtnlJ0tshkxyiMZlKOhMntct4hgi1GC1DXUS5T/zoxA==", "requires": { + "ansis": "4.2.0", "terminal-columns": "^2.0.0", - "type-flag": "^4.0.1" + "type-flag": "^4.5.2" + }, + "dependencies": { + "ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==" + } } }, "cli-cursor": { @@ -48567,9 +49432,9 @@ } }, "eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==" }, "evp_bytestokey": { "version": "1.0.3", @@ -50467,9 +51332,9 @@ "optional": true }, "ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==" }, "ipaddr.js": { "version": "1.9.1", @@ -50969,13 +51834,13 @@ } }, "is2": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", - "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", + "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", "requires": { "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" + "ip-regex": "^2.1.0", + "is-url": "^1.2.2" } }, "is64bit": { @@ -55074,9 +55939,9 @@ } }, "ora": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", - "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", "requires": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", @@ -55084,9 +55949,8 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.2.2", - "string-width": "^8.1.0", - "strip-ansi": "^7.1.2" + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" }, "dependencies": { "ansi-regex": { @@ -58211,9 +59075,9 @@ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" }, "stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==" }, "stop-iteration-iterator": { "version": "1.1.0", @@ -58835,12 +59699,12 @@ "integrity": "sha512-V+uqV66BOQnWxvI6HjDnE4VkInmYZUQ4dgB7gzaDyFyFSK1i1nF/j7DpS9UbQAgV9NaF1XpcyuavnM1qOeiEIg==" }, "tcp-port-used": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", - "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.3.tgz", + "integrity": "sha512-4CEQ3qRJYo+mtEbJ+OoQu3dF4TDkwaO3RDVC4UzP5cpAOIUWwuwPjD7sdxDFFqsMUjsXVVYBMlg/boAaloThMA==", "requires": { "debug": "4.3.1", - "is2": "^2.0.6" + "is2": "2.0.1" }, "dependencies": { "debug": { @@ -59216,9 +60080,9 @@ } }, "ts-dedent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==" }, "ts-mock-imports": { "version": "1.3.8", @@ -59328,9 +60192,9 @@ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" }, "type-flag": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/type-flag/-/type-flag-4.0.2.tgz", - "integrity": "sha512-diALGSg9wBpvowixHbIeCJtXNxUKJHUNR8oE67K9zuPkpBe5FMmg+dpevDWmEwGQDmn/RJD3m7szs6Ssdz5A4w==" + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/type-flag/-/type-flag-4.5.2.tgz", + "integrity": "sha512-v8O8xoLq3W6eKKYZvlyvk8LI46ryYkfnKrjGVdJzO+1LsxUu1ztvVAKFzQpphKQgS0ucinnbAiEGUbvibQmt/A==" }, "type-is": { "version": "2.1.0", @@ -59538,8 +60402,7 @@ "undici": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "dev": true + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==" }, "undici-types": { "version": "6.21.0", diff --git a/package.json b/package.json index c06a6cea0e..653b4d2771 100644 --- a/package.json +++ b/package.json @@ -2717,11 +2717,13 @@ }, "dependencies": { "@c4312/evt": "^0.1.1", - "@deepnote/blocks": "^4.6.0", + "@deepnote/blocks": "^4.8.1", "@deepnote/cli": "0.8.0", - "@deepnote/convert": "^4.0.0", - "@deepnote/database-integrations": "^1.5.0", - "@deepnote/runtime-core": "^0.5.0", + "@deepnote/cloud": "0.2.1", + "@deepnote/cloud-sync": "0.1.0", + "@deepnote/convert": "^4.0.3", + "@deepnote/database-integrations": "^1.7.1", + "@deepnote/runtime-core": "^0.7.0", "@deepnote/sql-language-server": "^3.0.0", "@enonic/fnv-plus": "^1.3.0", "@jupyter-widgets/base": "^6.0.8", @@ -2981,7 +2983,7 @@ "ws": "7.5.11" }, "@deepnote/runtime-core": { - "ws": "8.21.0" + "ws": "8.21.3" }, "@jupyterlab/filebrowser": { "ws": "8.21.0" diff --git a/src/test/mocks/deepnoteRuntimeCore.ts b/src/test/mocks/deepnoteRuntimeCore.ts index 183c6ef6e9..1ed0920579 100644 --- a/src/test/mocks/deepnoteRuntimeCore.ts +++ b/src/test/mocks/deepnoteRuntimeCore.ts @@ -1,5 +1,5 @@ import type { AgentBlock } from '@deepnote/blocks'; -import type { AgentBlockContext, ServerInfo, ServerOptions } from '@deepnote/runtime-core'; +import type { AgentBlockContext, ServerExit, ServerInfo, ServerOptions } from '@deepnote/runtime-core'; import type { ChildProcess } from 'child_process'; /** @@ -60,7 +60,11 @@ export const startServer: RuntimeCore['startServer'] = async (options) => { url: `http://127.0.0.1:${50000 + id}`, jupyterPort: 50000 + id, lspPort: 51000 + id, - process: makeFakeProcess(id) + process: makeFakeProcess(id), + // Never settles: the fake server never exits. + exited: new Promise(() => undefined), + stderrTail: '', + childPids: [] }; }; From 81994117b1519ab3705df8fe9781e1f748867a56 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 13 Oct 2026 15:25:10 +0000 Subject: [PATCH 2/3] chore(deps): drop the @deepnote/runtime-core ws override The override only restated what the lockfile already holds. The @deepnote/runtime-core used by the extension (0.7.0) requires ws ^8.21.3, which covers every current ws advisory, and the lockfile is byte-identical without the override. Keeping an exact pin would also conflict with every runtime-core release that raises its ws floor. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_017pqrotDqxwXxXYVEG5Z7wh --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index e08e73feb8..158521f6c9 100644 --- a/package.json +++ b/package.json @@ -2988,9 +2988,6 @@ "@jupyterlab/services": { "ws": "7.5.11" }, - "@deepnote/runtime-core": { - "ws": "8.21.3" - }, "@jupyterlab/filebrowser": { "ws": "8.21.0" }, From d279c45738a2a7c369c65090242cb366f53bc5a3 Mon Sep 17 00:00:00 2001 From: tomas Date: Tue, 13 Oct 2026 16:15:26 +0000 Subject: [PATCH 3/3] feat(cloud): store the Deepnote API token in .env and resolve it for Deepnote Cloud features Adds "Deepnote: Set API Token" and "Deepnote: Clear API Token", which keep DEEPNOTE_TOKEN in the nearest .env that already assigns it, else in the .env at the sync root or workspace folder root. Set checks the token against the Deepnote API first and offers to add .env to .gitignore when git does not ignore it. IDeepnoteApiTokenService returns the token that applies to a local file or folder and prompts for one when none is set. showCloudError reports failed Deepnote Cloud calls and offers to set a new token when Deepnote answers 401. Both commands and the service are disabled in untrusted workspaces; the commands are also disabled in the web extension host. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Got2L1hwAr7GBzdxTequW8 --- AGENTS.md | 1 + README.md | 2 + package.json | 12 + package.nls.json | 2 + src/commands.ts | 2 + .../deepnote/cloud/cloudErrors.node.ts | 39 + .../cloud/cloudErrors.node.unit.test.ts | 97 ++ .../cloud/deepnoteApiTokenService.node.ts | 491 +++++++ .../deepnoteApiTokenService.node.unit.test.ts | 1148 +++++++++++++++++ src/notebooks/deepnote/cloud/types.ts | 18 + src/notebooks/deepnote/importClient.node.ts | 2 +- src/notebooks/serviceRegistry.node.ts | 4 + src/platform/common/constants.ts | 2 + src/platform/common/utils/localize.ts | 30 + 14 files changed, 1849 insertions(+), 1 deletion(-) create mode 100644 src/notebooks/deepnote/cloud/cloudErrors.node.ts create mode 100644 src/notebooks/deepnote/cloud/cloudErrors.node.unit.test.ts create mode 100644 src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.ts create mode 100644 src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.unit.test.ts create mode 100644 src/notebooks/deepnote/cloud/types.ts diff --git a/AGENTS.md b/AGENTS.md index 31b405bd15..b0c261e588 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,7 @@ Start with the owning directory and its colocated tests before searching broadly | Environment snapshots & `requirements.txt` generation | `src/notebooks/deepnote/snapshots/` | | Database integrations (credentials, env refresh, webview) | `src/notebooks/deepnote/integrations/`, `src/webviews/webview-side/integrations/` | | Kernel selection, SQL LSP, server startup | `src/kernels/deepnote/` | +| Deepnote Cloud API token, sync and publishing | `src/notebooks/deepnote/cloud/` | | Shared Deepnote types & platform config | `src/platform/deepnote/`, `src/platform/notebooks/deepnote/` | | Core serializer architecture & data flow | `specs/architecture.md` | | Kernel management internals | `specs/DEBUGGING_KERNEL_MANAGEMENT.md`, `specs/DEEPNOTE_KERNEL_IMPLEMENTATION.md` | diff --git a/README.md b/README.md index 693e6dd010..6e95e1fad2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Open the Command Palette (`Cmd+Shift+P` or `Ctrl+Shift+P`) and type `Deepnote` t | `Deepnote: Manage Integrations` | Configure database connections and credentials | | `Deepnote: New Project` | Create a new Deepnote project | | `Deepnote: Import Notebook` | Import an existing notebook into your project | +| `Deepnote: Set API Token` | Save a Deepnote API token to `.env` for syncing and publishing | +| `Deepnote: Clear API Token` | Remove the saved Deepnote API token | | `Notebook: Select Notebook Kernel` | Select or switch kernels within your notebook | | `Notebook: Change Cell Language` | Change the language of the cell currently in focus | | `Deepnote: Enable Snapshots` | Enable snapshot mode for the current workspace | diff --git a/package.json b/package.json index 158521f6c9..d3d4dec271 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,18 @@ "category": "Deepnote", "icon": "$(globe)" }, + { + "command": "deepnote.setApiToken", + "title": "%deepnote.commands.setApiToken.title%", + "category": "Deepnote", + "enablement": "isWorkspaceTrusted && !deepnote.webExtension" + }, + { + "command": "deepnote.clearApiToken", + "title": "%deepnote.commands.clearApiToken.title%", + "category": "Deepnote", + "enablement": "isWorkspaceTrusted && !deepnote.webExtension" + }, { "command": "deepnote.newProject", "title": "%deepnote.commands.newProject.title%", diff --git a/package.nls.json b/package.nls.json index 388ab8d32c..df6c9d3ae6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -259,6 +259,8 @@ "deepnote.commands.manageIntegrations.title": "Manage Integrations", "deepnote.commands.addExistingIntegration.title": "Add Existing Integration", "deepnote.commands.authenticateIntegration.title": "Authenticate Integration", + "deepnote.commands.setApiToken.title": "Set API Token", + "deepnote.commands.clearApiToken.title": "Clear API Token", "deepnote.commands.newProject.title": "New Project", "deepnote.commands.importNotebook.title": "Import Notebook", "deepnote.commands.importJupyterNotebook.title": "Import Jupyter Notebook", diff --git a/src/commands.ts b/src/commands.ts index 363547f50f..7fb68d68f0 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -210,4 +210,6 @@ export interface ICommandNameArgumentTypeMapping { [DSCommands.DuplicateNotebook]: []; [DSCommands.AddNotebookToProject]: []; [DSCommands.OpenInDeepnote]: []; + [DSCommands.SetApiToken]: [Uri | undefined]; + [DSCommands.ClearApiToken]: []; } diff --git a/src/notebooks/deepnote/cloud/cloudErrors.node.ts b/src/notebooks/deepnote/cloud/cloudErrors.node.ts new file mode 100644 index 0000000000..bc7f68d052 --- /dev/null +++ b/src/notebooks/deepnote/cloud/cloudErrors.node.ts @@ -0,0 +1,39 @@ +import { ApiError } from '@deepnote/cloud-sync'; +import { commands, Uri, window } from 'vscode'; + +import { Commands } from '../../../platform/common/constants'; +import { DeepnoteCloud } from '../../../platform/common/utils/localize'; +import { logger } from '../../../platform/logging'; + +const UNAUTHORIZED_STATUS = 401; + +/** + * Whether Deepnote answered 401, meaning the token is missing, invalid or revoked. A 403 is not matched: the token was + * accepted, but the request is not allowed. + */ +export function isRejectedTokenError(error: unknown): boolean { + return error instanceof ApiError && error.statusCode === UNAUTHORIZED_STATUS; +} + +/** + * Logs a failed Deepnote Cloud call and tells the user. A rejected token offers "Set API Token" for `resource` and + * resolves once that prompt is dismissed; any other failure shows `: ` and resolves immediately. + */ +export async function showCloudError(message: string, error: unknown, resource?: Uri): Promise { + logger.error(message, error); + + if (isRejectedTokenError(error)) { + const choice = await window.showErrorMessage( + DeepnoteCloud.apiTokenRejectedForRequest, + DeepnoteCloud.setApiToken + ); + if (choice === DeepnoteCloud.setApiToken) { + await commands.executeCommand(Commands.SetApiToken, resource); + } + + return; + } + + const detail = error instanceof Error ? error.message : String(error); + void window.showErrorMessage(DeepnoteCloud.errorWithDetail(message, detail)); +} diff --git a/src/notebooks/deepnote/cloud/cloudErrors.node.unit.test.ts b/src/notebooks/deepnote/cloud/cloudErrors.node.unit.test.ts new file mode 100644 index 0000000000..8f97bdc761 --- /dev/null +++ b/src/notebooks/deepnote/cloud/cloudErrors.node.unit.test.ts @@ -0,0 +1,97 @@ +import { ApiError } from '@deepnote/cloud-sync'; +import { assert } from 'chai'; +import { anything, verify, when } from 'ts-mockito'; +import { Uri } from 'vscode'; + +import { DeepnoteCloud } from '../../../platform/common/utils/localize'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../../test/vscode-mock'; +import { isRejectedTokenError, showCloudError } from './cloudErrors.node'; + +suite('cloudErrors', () => { + setup(() => { + resetVSCodeMocks(); + when(mockedVSCodeNamespaces.commands.executeCommand(anything(), anything())).thenResolve(undefined); + }); + + suite('isRejectedTokenError', () => { + test('is true for an ApiError with status 401', () => { + assert.isTrue(isRejectedTokenError(new ApiError(401, 'Unauthorized'))); + }); + + test('is false for an ApiError with another status, including 403', () => { + assert.isFalse(isRejectedTokenError(new ApiError(403, 'Forbidden'))); + assert.isFalse(isRejectedTokenError(new ApiError(500, 'Boom'))); + }); + + test('is false for errors that are not an ApiError', () => { + assert.isFalse(isRejectedTokenError(new Error('offline'))); + assert.isFalse(isRejectedTokenError({ statusCode: 401 })); + assert.isFalse(isRejectedTokenError('401')); + assert.isFalse(isRejectedTokenError(undefined)); + }); + }); + + suite('showCloudError', () => { + test('offers Set API Token for a rejected token, and runs the command for the resource when chosen', async () => { + const resource = Uri.file('/r'); + when( + mockedVSCodeNamespaces.window.showErrorMessage( + DeepnoteCloud.apiTokenRejectedForRequest, + DeepnoteCloud.setApiToken + ) + ).thenResolve(DeepnoteCloud.setApiToken as any); + + await showCloudError( + 'Sync failed', + new ApiError(401, 'Authentication failed. Please check your API token.'), + resource + ); + + verify( + mockedVSCodeNamespaces.window.showErrorMessage( + DeepnoteCloud.apiTokenRejectedForRequest, + DeepnoteCloud.setApiToken + ) + ).once(); + verify(mockedVSCodeNamespaces.commands.executeCommand('deepnote.setApiToken', resource)).once(); + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).never(); + }); + + test('does not run the command when the rejected-token prompt is dismissed', async () => { + await showCloudError('Sync failed', new ApiError(401, 'Unauthorized'), Uri.file('/r')); + + verify(mockedVSCodeNamespaces.commands.executeCommand(anything(), anything())).never(); + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything())).never(); + }); + + test('shows the server message without a button for a 403', async () => { + await showCloudError('Publish failed', new ApiError(403, 'Insufficient permissions to publish this app.')); + + verify( + mockedVSCodeNamespaces.window.showErrorMessage( + 'Publish failed: Insufficient permissions to publish this app.' + ) + ).once(); + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything(), anything())).never(); + }); + + test('shows the message of any other ApiError', async () => { + await showCloudError('Sync failed', new ApiError(500, 'Boom')); + + verify(mockedVSCodeNamespaces.window.showErrorMessage('Sync failed: Boom')).once(); + }); + + test('shows the message of a plain Error without a button', async () => { + await showCloudError('Sync failed', new Error('offline')); + + verify(mockedVSCodeNamespaces.window.showErrorMessage('Sync failed: offline')).once(); + verify(mockedVSCodeNamespaces.window.showErrorMessage(anything(), anything())).never(); + }); + + test('stringifies a thrown value that is not an Error', async () => { + await showCloudError('Sync failed', 'timed out'); + + verify(mockedVSCodeNamespaces.window.showErrorMessage('Sync failed: timed out')).once(); + }); + }); +}); diff --git a/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.ts b/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.ts new file mode 100644 index 0000000000..3b531c7fb2 --- /dev/null +++ b/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.ts @@ -0,0 +1,491 @@ +import { getWorkspace, type Workspace } from '@deepnote/cloud'; +import { findSyncManifestRoot } from '@deepnote/cloud-sync'; +import { isUtf8 } from 'buffer'; +import dotenv from 'dotenv'; +import * as fs from 'fs'; +import { inject, injectable } from 'inversify'; +import { isDeepStrictEqual } from 'util'; +import { commands, extensions, ProgressLocation, Uri, window, workspace } from 'vscode'; + +import { IExtensionSyncActivationService } from '../../../platform/activation/types'; +import { Commands } from '../../../platform/common/constants'; +import { isFileNotFoundError } from '../../../platform/common/platform/errors'; +import { getFilePath } from '../../../platform/common/platform/fs-paths'; +import { IExtensionContext } from '../../../platform/common/types'; +import { DeepnoteCloud } from '../../../platform/common/utils/localize'; +import { logger } from '../../../platform/logging'; +import { + dirname, + extUriBiasedIgnorePathCase, + isEqual, + removeTrailingPathSeparator +} from '../../../platform/vscode-path/resources'; +import { getApiEndpoint } from '../importClient.node'; +import { isRejectedTokenError } from './cloudErrors.node'; +import { IDeepnoteApiTokenService } from './types'; + +const TOKEN_ENV_VAR = 'DEEPNOTE_TOKEN'; +const ENV_FILE_NAME = '.env'; +const GITIGNORE_FILE_NAME = '.gitignore'; + +// Both assignment forms `dotenv.parse` accepts: `KEY=value` and `KEY: value`, with an optional `export `. +const TOKEN_LINE_PATTERN = /^[ \t]*(?:export[ \t]+)?DEEPNOTE_TOKEN(?:[ \t]*=|:[ \t]).*$/; + +// What a `.env` line or a shell would split or unquote on, plus control characters: `fetch` rejects a NUL in a header +// with an error that quotes the whole `Authorization` value, token included. +const INVALID_TOKEN_CHARACTERS = /[\s"'#\p{Cc}]/u; +const BYTE_ORDER_MARK = '\uFEFF'; + +// Minimal shape of the members used from the built-in Git extension, which ships no types. +interface GitRepository { + checkIgnore?(paths: string[]): Promise>; +} +interface GitExtensionExports { + getAPI(version: 1): { getRepository(uri: Uri): GitRepository | null }; +} + +function withoutQueryAndFragment(uri: Uri): Uri { + return uri.with({ query: '', fragment: '' }); +} + +async function getStartFolder(resource: Uri): Promise { + const stats = await fs.promises.stat(getFilePath(resource)).catch(() => undefined); + + return stats?.isDirectory() ? resource : dirname(resource); +} + +async function readFileBytes(filePath: string): Promise { + try { + return await fs.promises.readFile(filePath); + } catch (error) { + if (error instanceof Error && isFileNotFoundError(error)) { + return undefined; + } + + throw error; + } +} + +async function readTextFile(filePath: string): Promise { + return (await readFileBytes(filePath))?.toString('utf8'); +} + +/** Throws for a file that is not UTF-8: writing back its lossy decode would replace every such byte with U+FFFD. */ +async function readTextFileToRewrite(filePath: string): Promise { + const bytes = await readFileBytes(filePath); + if (bytes && !isUtf8(bytes)) { + throw new Error(`${filePath} is not valid UTF-8`); + } + + return bytes?.toString('utf8'); +} + +function detectLineEnding(content: string): string { + return content.includes('\r\n') ? '\r\n' : '\n'; +} + +/** Splits into lines without the terminator of the last one, so a trailing line ending is not a blank line. */ +function splitLines(content: string): string[] { + const lines = content.split(/\r?\n/); + if (lines[lines.length - 1] === '') { + lines.pop(); + } + + return lines; +} + +function joinLines(lines: string[], lineEnding: string): string { + return lines.map((line) => line + lineEnding).join(''); +} + +function withToken(content: string, token: string): string { + const assignment = `${TOKEN_ENV_VAR}=${token}`; + const lines = splitLines(content); + const first = lines.findIndex((line) => TOKEN_LINE_PATTERN.test(line)); + const result = + first === -1 + ? [...lines, assignment] + : lines.flatMap((line, index) => { + if (index === first) { + return [assignment]; + } + + return TOKEN_LINE_PATTERN.test(line) ? [] : [line]; + }); + + return joinLines(result, detectLineEnding(content)); +} + +function withoutToken(content: string): string { + return joinLines( + splitLines(content).filter((line) => !TOKEN_LINE_PATTERN.test(line)), + detectLineEnding(content) + ); +} + +function otherVariables(content: string): Record { + const variables = dotenv.parse(content); + delete variables[TOKEN_ENV_VAR]; + + return variables; +} + +/** + * Whether `after` is `before` with only `DEEPNOTE_TOKEN` changed to `token` (absent when `token` is `undefined`), as + * `dotenv.parse` reads them. The line-based edit cannot tell a multi-line quoted value from separate assignments, so + * this is what stops it from corrupting another variable. + */ +function isSafeEnvEdit(before: string, after: string, token: string | undefined): boolean { + return ( + isDeepStrictEqual(otherVariables(before), otherVariables(after)) && dotenv.parse(after)[TOKEN_ENV_VAR] === token + ); +} + +@injectable() +export class DeepnoteApiTokenService implements IDeepnoteApiTokenService, IExtensionSyncActivationService { + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + + public activate(): void { + this.extensionContext.subscriptions.push( + commands.registerCommand(Commands.SetApiToken, async (resource?: Uri) => { + await this.promptAndSaveToken(resource); + }), + commands.registerCommand(Commands.ClearApiToken, () => this.clearToken()) + ); + } + + public async getOrPromptToken(resource: Uri): Promise { + if (!this.ensureTrusted()) { + return undefined; + } + + if (resource.scheme !== 'file') { + void window.showErrorMessage(DeepnoteCloud.localFilesOnly); + + return undefined; + } + + return (await this.getToken(resource)) ?? (await this.promptAndSaveToken(resource)); + } + + public async getToken(resource: Uri): Promise { + if (!workspace.isTrusted || resource.scheme !== 'file') { + return undefined; + } + + for (const dir of await this.getLookupDirs(resource)) { + const token = (await this.readEnvFile(dir))?.[TOKEN_ENV_VAR]?.trim(); + if (token) { + return token; + } + } + + return undefined; + } + + /** Seam for tests, which never reach the network. */ + protected fetchWorkspace(baseUrl: string, token: string): Promise { + return getWorkspace(baseUrl, token); + } + + /** `undefined` means the Git extension gave no answer. Never throws. */ + protected async isIgnoredByGit(envUri: Uri): Promise { + try { + const extension = extensions.getExtension('vscode.git'); + if (!extension) { + return undefined; + } + + const exports = extension.isActive ? extension.exports : await extension.activate(); + const repository = exports.getAPI(1).getRepository(envUri); + if (!repository || typeof repository.checkIgnore !== 'function') { + return undefined; + } + + return (await repository.checkIgnore([getFilePath(envUri)])).size > 0; + } catch (error) { + logger.debug('The Git extension could not tell whether the .env file is ignored', error); + + return undefined; + } + } + + private async addToGitignore(folder: Uri): Promise { + const gitignorePath = getFilePath(Uri.joinPath(folder, GITIGNORE_FILE_NAME)); + // git reads .gitignore as bytes, and latin1 maps every byte to one character and back, so a file in any + // encoding gets its new line without its other bytes being rewritten. + const before = (await readFileBytes(gitignorePath))?.toString('latin1') ?? ''; + + await fs.promises.writeFile( + gitignorePath, + joinLines([...splitLines(before), ENV_FILE_NAME], detectLineEnding(before)), + 'latin1' + ); + } + + /** Never throws. */ + private async checkEnvFileInGit(envUri: Uri): Promise { + try { + if ((await this.isIgnoredByGit(envUri)) !== false) { + return; + } + + const choice = await window.showWarningMessage( + DeepnoteCloud.envNotIgnored(workspace.asRelativePath(envUri)), + { modal: true }, + DeepnoteCloud.addToGitignore + ); + if (choice === DeepnoteCloud.addToGitignore) { + await this.addToGitignore(dirname(envUri)); + } + } catch (error) { + logger.warn('Could not add .env to .gitignore', error); + } + } + + /** Resolves `undefined` after telling the user why the token was not accepted. */ + private async checkToken(start: Uri, token: string): Promise<{ workspaceName?: string } | undefined> { + try { + const deepnoteWorkspace = await window.withProgress( + { location: ProgressLocation.Notification, title: DeepnoteCloud.checkingApiToken }, + () => this.fetchWorkspace(getApiEndpoint(start), token) + ); + + return { workspaceName: deepnoteWorkspace?.name }; + } catch (error) { + logger.warn('Could not check the Deepnote API token', error); + void window.showErrorMessage( + isRejectedTokenError(error) + ? DeepnoteCloud.apiTokenRejected + : DeepnoteCloud.apiTokenCheckFailed(error instanceof Error ? error.message : String(error)) + ); + + return undefined; + } + } + + private async clearToken(): Promise { + if (!this.ensureTrusted()) { + return; + } + + const start = await this.resolveStart(); + if (!start) { + return; + } + + const envFiles = await this.findAssigningEnvFiles(await this.getLookupDirs(start)); + if (envFiles.length === 0) { + void window.showInformationMessage(DeepnoteCloud.noApiTokenFound); + + return; + } + + let cleared = false; + for (const envUri of envFiles) { + cleared = (await this.editEnvFile(envUri, withoutToken, undefined)) || cleared; + } + + if (cleared) { + void window.showInformationMessage(DeepnoteCloud.apiTokenCleared); + } + } + + /** + * Rewrites `envUri` through `edit`, creating it when missing. Writes nothing and tells the user when the edit is + * not safe or the file cannot be read or written. + */ + private async editEnvFile( + envUri: Uri, + edit: (content: string) => string, + expectedToken: string | undefined + ): Promise { + const envPath = getFilePath(envUri); + try { + const before = (await readTextFileToRewrite(envPath)) ?? ''; + // dotenv reads past a byte order mark, TOKEN_LINE_PATTERN does not, so edit around it. + const bom = before.startsWith(BYTE_ORDER_MARK) ? BYTE_ORDER_MARK : ''; + const after = bom + edit(before.slice(bom.length)); + if (isSafeEnvEdit(before, after, expectedToken)) { + await fs.promises.writeFile(envPath, after); + + return true; + } + } catch (error) { + logger.warn(`Could not update ${envPath}`, error); + } + + void window.showErrorMessage(DeepnoteCloud.envEditFailed(workspace.asRelativePath(envUri))); + + return false; + } + + private ensureTrusted(): boolean { + if (workspace.isTrusted) { + return true; + } + + void window.showErrorMessage(DeepnoteCloud.workspaceNotTrusted); + + return false; + } + + /** The `.env` files among `dirs` that assign `DEEPNOTE_TOKEN`, even to an empty value, nearest first. */ + private async findAssigningEnvFiles(dirs: Uri[]): Promise { + const envFiles: Uri[] = []; + for (const dir of dirs) { + if ((await this.readEnvFile(dir))?.[TOKEN_ENV_VAR] !== undefined) { + envFiles.push(Uri.joinPath(dir, ENV_FILE_NAME)); + } + } + + return envFiles; + } + + private async findSyncRoot(folder: Uri): Promise { + try { + const root = await findSyncManifestRoot(getFilePath(folder)); + + return root ? Uri.file(root) : undefined; + } catch (error) { + logger.debug(`Could not look for a sync root above ${getFilePath(folder)}`, error); + + return undefined; + } + } + + /** The folders to search for a `.env`, nearest first, from the folder of `resource` up to the folder it is rooted in. */ + private async getLookupDirs(resource: Uri): Promise { + const start = withoutQueryAndFragment(resource); + const startFolder = await getStartFolder(start); + const stopFolder = removeTrailingPathSeparator(await this.getStopFolder(start, startFolder)); + + // VS Code matches workspace folders case-insensitively on macOS and Windows and keeps a trailing slash from a + // `.code-workspace` file. A stop folder that is not above `startFolder`, such as a workspace folder missing on + // disk, must end the search, not widen it to the filesystem root. + const dirs: Uri[] = []; + for (let dir = startFolder; extUriBiasedIgnorePathCase.isEqualOrParent(dir, stopFolder); dir = dirname(dir)) { + dirs.push(dir); + if (extUriBiasedIgnorePathCase.isEqual(dir, stopFolder) || isEqual(dirname(dir), dir)) { + break; + } + } + + return dirs; + } + + /** The higher of the workspace folder root and the sync root, else `startFolder`. */ + private async getStopFolder(start: Uri, startFolder: Uri): Promise { + const workspaceFolder = workspace.getWorkspaceFolder(start)?.uri; + const syncRoot = await this.findSyncRoot(startFolder); + if (workspaceFolder && syncRoot) { + return extUriBiasedIgnorePathCase.isEqualOrParent(syncRoot, workspaceFolder) ? workspaceFolder : syncRoot; + } + + return workspaceFolder ?? syncRoot ?? startFolder; + } + + private async pickWorkspaceFolder(): Promise { + const folders = workspace.workspaceFolders ?? []; + if (folders.length === 0) { + void window.showErrorMessage(DeepnoteCloud.openFolderFirst); + + return undefined; + } + + const folder = folders.length === 1 ? folders[0] : await window.showWorkspaceFolderPick(); + if (folder && folder.uri.scheme !== 'file') { + void window.showErrorMessage(DeepnoteCloud.localFilesOnly); + + return undefined; + } + + return folder?.uri; + } + + private async promptAndSaveToken(resource?: Uri): Promise { + if (!this.ensureTrusted()) { + return undefined; + } + + const start = await this.resolveStart(resource); + if (!start) { + return undefined; + } + + const target = await this.resolveTargetFolder(start); + const token = await this.promptForToken(); + if (!token) { + return undefined; + } + + const checked = await this.checkToken(start, token); + if (!checked) { + return undefined; + } + + const [assigningEnvFile] = await this.findAssigningEnvFiles(await this.getLookupDirs(start)); + const envUri = assigningEnvFile ?? Uri.joinPath(target, ENV_FILE_NAME); + if (!(await this.editEnvFile(envUri, (content) => withToken(content, token), token))) { + return undefined; + } + + await this.checkEnvFileInGit(envUri); + + // Notifications render `[label](command:...)` as runnable links, and this name comes from the Deepnote API. + const workspaceName = checked.workspaceName?.replace(/\]\(/g, '] ('); + const envPath = workspace.asRelativePath(envUri); + void window.showInformationMessage( + workspaceName + ? DeepnoteCloud.apiTokenSavedForWorkspace(workspaceName, envPath) + : DeepnoteCloud.apiTokenSaved(envPath) + ); + + return token; + } + + private async promptForToken(): Promise { + const value = await window.showInputBox({ + password: true, + ignoreFocusOut: true, + prompt: DeepnoteCloud.apiTokenPrompt, + placeHolder: DeepnoteCloud.apiTokenPlaceholder, + validateInput: (input) => + INVALID_TOKEN_CHARACTERS.test(input.trim()) ? DeepnoteCloud.apiTokenInvalidCharacters : undefined + }); + + return value?.trim() || undefined; + } + + /** The variables of the `.env` in `dir`; `undefined` when it is missing or cannot be read. */ + private async readEnvFile(dir: Uri): Promise | undefined> { + const envPath = getFilePath(Uri.joinPath(dir, ENV_FILE_NAME)); + try { + const content = await readTextFile(envPath); + + return content === undefined ? undefined : dotenv.parse(content); + } catch (error) { + logger.warn(`Could not read ${envPath}`, error); + + return undefined; + } + } + + /** The command argument, else the active notebook or file; with none, the workspace folder. Tells the user why not. */ + private async resolveStart(resource?: Uri): Promise { + const activeNotebook = window.activeNotebookEditor?.notebook.uri; + const start = + resource ?? + (activeNotebook && withoutQueryAndFragment(activeNotebook)) ?? + window.activeTextEditor?.document.uri; + + return start?.scheme === 'file' ? start : this.pickWorkspaceFolder(); + } + + private async resolveTargetFolder(start: Uri): Promise { + const cleared = withoutQueryAndFragment(start); + const startFolder = await getStartFolder(cleared); + + return (await this.findSyncRoot(startFolder)) ?? workspace.getWorkspaceFolder(cleared)?.uri ?? startFolder; + } +} diff --git a/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.unit.test.ts b/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.unit.test.ts new file mode 100644 index 0000000000..4c6093a32a --- /dev/null +++ b/src/notebooks/deepnote/cloud/deepnoteApiTokenService.node.unit.test.ts @@ -0,0 +1,1148 @@ +import type { Workspace } from '@deepnote/cloud'; +import { ApiError, findSyncManifestRoot } from '@deepnote/cloud-sync'; +import { assert } from 'chai'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as sinon from 'sinon'; +import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; +import { Extension, ExtensionKind, InputBoxOptions, Uri, WorkspaceFolder } from 'vscode'; + +import { Commands, NotebookCellScheme } from '../../../platform/common/constants'; +import { IExtensionContext } from '../../../platform/common/types'; +import { DeepnoteCloud } from '../../../platform/common/utils/localize'; +import * as path from '../../../platform/vscode-path/path'; +import { extUriBiasedIgnorePathCase, extUriIgnorePathCase } from '../../../platform/vscode-path/resources'; +import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../../test/vscode-mock'; +import { DeepnoteApiTokenService } from './deepnoteApiTokenService.node'; + +class TestableService extends DeepnoteApiTokenService { + public readonly fetchWorkspaceStub = sinon.stub<[string, string], Promise>(); + public readonly isIgnoredByGitStub = sinon.stub<[Uri], Promise>(); + + protected override fetchWorkspace(baseUrl: string, token: string): Promise { + return this.fetchWorkspaceStub(baseUrl, token); + } + + protected override isIgnoredByGit(envUri: Uri): Promise { + return this.isIgnoredByGitStub(envUri); + } +} + +class GitProbe extends DeepnoteApiTokenService { + public isIgnored(envUri: Uri): Promise { + return this.isIgnoredByGit(envUri); + } +} + +suite('DeepnoteApiTokenService', () => { + let tmp: string; + let ws: string; + let service: TestableService; + let sandbox: sinon.SinonSandbox; + let commandHandlers: Map Promise>; + + const win = () => mockedVSCodeNamespaces.window; + const wsp = () => mockedVSCodeNamespaces.workspace; + + async function writeFile(file: string, content: string): Promise { + await fs.promises.mkdir(path.dirname(file), { recursive: true }); + await fs.promises.writeFile(file, content); + } + + function readFile(file: string): Promise { + return fs.promises.readFile(file, 'utf8'); + } + + function exists(file: string): Promise { + return fs.promises.access(file).then( + () => true, + () => false + ); + } + + function isInside(candidate: string, folder: string): boolean { + return candidate === folder || candidate.startsWith(folder + path.sep); + } + + function openWorkspaceFolder(folderPath: string): void { + const folder: WorkspaceFolder = { uri: Uri.file(folderPath), name: path.basename(folderPath), index: 0 }; + when(wsp().workspaceFolders).thenReturn([folder]); + // fsPath lower-cases a Windows drive letter, so compare against the folder's fsPath, not folderPath. + when(wsp().getWorkspaceFolder(anything())).thenCall((uri: Uri) => + isInside(uri.fsPath, folder.uri.fsPath) ? folder : undefined + ); + when(wsp().asRelativePath(anything())).thenCall((uri: Uri) => path.relative(folder.uri.fsPath, uri.fsPath)); + } + + function activeFile(file: string): void { + when(win().activeTextEditor).thenReturn({ document: { uri: Uri.file(file) } } as any); + } + + function enterToken(token: string | undefined): void { + when(win().showInputBox(anything())).thenResolve(token as any); + } + + function setApiToken(resource?: Uri): Promise { + return commandHandlers.get(Commands.SetApiToken)!(resource); + } + + function clearApiToken(): Promise { + return commandHandlers.get(Commands.ClearApiToken)!(); + } + + function inputBoxOptions(): InputBoxOptions { + return capture(win().showInputBox).last()[0] as InputBoxOptions; + } + + function verifyError(message: string): void { + verify(win().showErrorMessage(message)).once(); + } + + function verifyNoInputBox(): void { + verify(win().showInputBox(anything())).never(); + } + + function createSyncManifest(folder: string): Promise { + return writeFile(path.join(folder, '.deepnote-sync.json'), '{}'); + } + + setup(async () => { + resetVSCodeMocks(); + sandbox = sinon.createSandbox(); + tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'deepnote-token-')); + assert.isUndefined( + await findSyncManifestRoot(tmp), + 'A .deepnote-sync.json above the temp dir would redirect token writes outside it' + ); + ws = path.join(tmp, 'ws'); + await fs.promises.mkdir(ws); + + openWorkspaceFolder(ws); + when(wsp().getConfiguration('deepnote', anything())).thenReturn({ + get: (_key: string, fallback: unknown) => fallback + } as any); + + commandHandlers = new Map(); + when(mockedVSCodeNamespaces.commands.registerCommand(anything(), anything())).thenCall( + (id: string, handler: (...args: unknown[]) => Promise) => { + commandHandlers.set(id, handler); + + return { dispose: () => undefined }; + } + ); + + service = new TestableService({ subscriptions: [] } as unknown as IExtensionContext); + service.fetchWorkspaceStub.resolves({ id: 'w1', name: 'Acme' }); + service.isIgnoredByGitStub.resolves(true); + service.activate(); + }); + + teardown(async () => { + sandbox.restore(); + await fs.promises.rm(tmp, { recursive: true, force: true }); + }); + + suite('activate', () => { + test('registers the Set and Clear commands', () => { + assert.sameMembers([...commandHandlers.keys()], ['deepnote.setApiToken', 'deepnote.clearApiToken']); + }); + }); + + suite('getToken', () => { + test('prefers the .env nearest the file over the one at the workspace root', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=root\n'); + await writeFile(path.join(ws, 'a', 'b', '.env'), 'DEEPNOTE_TOKEN=near\n'); + + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'a', 'b', 'nb.deepnote'))), 'near'); + }); + + test('skips a .env with an empty DEEPNOTE_TOKEN in favor of its parent', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=root\n'); + await writeFile(path.join(ws, 'a', 'b', '.env'), 'DEEPNOTE_TOKEN=\n'); + + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'a', 'b', 'nb.deepnote'))), 'root'); + }); + + test('trims the value and reads the `KEY: value` form and `export`', async () => { + await writeFile(path.join(ws, 'a', '.env'), "DEEPNOTE_TOKEN: ' spaced '\n"); + await writeFile(path.join(ws, 'b', '.env'), 'export DEEPNOTE_TOKEN=exported\n'); + + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'a', 'nb.deepnote'))), 'spaced'); + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'b', 'nb.deepnote'))), 'exported'); + }); + + test('is undefined when no .env on the path defines it', async () => { + await writeFile(path.join(ws, '.env'), 'OTHER=1\n'); + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=outside\n'); + + assert.isUndefined(await service.getToken(Uri.file(path.join(ws, 'a', 'nb.deepnote')))); + }); + + test('stops at the workspace folder root when the sync root is below it', async () => { + const syncRoot = path.join(ws, 'sync'); + await createSyncManifest(syncRoot); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=at-workspace\n'); + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-everything\n'); + + assert.strictEqual( + await service.getToken(Uri.file(path.join(syncRoot, 'P', 'nb.deepnote'))), + 'at-workspace' + ); + }); + + test('searches up to the sync root when it is above the workspace folder', async () => { + const syncRoot = path.join(ws, 'sync'); + const projectFolder = path.join(syncRoot, 'P'); + openWorkspaceFolder(projectFolder); + await createSyncManifest(syncRoot); + await writeFile(path.join(syncRoot, '.env'), 'DEEPNOTE_TOKEN=at-sync-root\n'); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=above-sync-root\n'); + + assert.strictEqual( + await service.getToken(Uri.file(path.join(projectFolder, 'nb.deepnote'))), + 'at-sync-root' + ); + }); + + test('treats a symbolic-link sync manifest as no sync root', async () => { + const syncRoot = path.join(ws, 'sync'); + await fs.promises.mkdir(path.join(syncRoot, 'P'), { recursive: true }); + await writeFile(path.join(tmp, 'real-manifest.json'), '{}'); + await fs.promises.symlink(path.join(tmp, 'real-manifest.json'), path.join(syncRoot, '.deepnote-sync.json')); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=at-workspace\n'); + + assert.strictEqual( + await service.getToken(Uri.file(path.join(syncRoot, 'P', 'nb.deepnote'))), + 'at-workspace' + ); + }); + + test('reads the folder itself when given a folder', async () => { + await writeFile(path.join(ws, 'a', '.env'), 'DEEPNOTE_TOKEN=in-folder\n'); + + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'a'))), 'in-folder'); + }); + + test('ignores the query and fragment of a notebook URI', async () => { + await writeFile(path.join(ws, 'a', '.env'), 'DEEPNOTE_TOKEN=in-folder\n'); + + assert.strictEqual( + await service.getToken( + Uri.file(path.join(ws, 'a', 'nb.deepnote')).with({ query: 'id=1', fragment: 'x' }) + ), + 'in-folder' + ); + }); + + test('does not search above the workspace folder for a notebook URI with a query', async () => { + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-workspace\n'); + + assert.isUndefined( + await service.getToken( + Uri.file(path.join(ws, 'a', 'nb.deepnote')).with({ query: 'id=1', fragment: 'x' }) + ) + ); + }); + + test('does not search above a workspace folder that is missing on disk', async () => { + const missing = path.join(tmp, 'projects', 'foo'); + openWorkspaceFolder(missing); + await writeFile(path.join(tmp, 'projects', '.env'), 'DEEPNOTE_TOKEN=other-project\n'); + + assert.isUndefined(await service.getToken(Uri.file(missing))); + }); + + test('does not search above the workspace folder for a folder URI with a trailing slash', async () => { + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-workspace\n'); + + assert.isUndefined(await service.getToken(Uri.file(ws + '/'))); + }); + + test('stops at, and still reads, a workspace folder whose URI ends in a slash', async () => { + const folder: WorkspaceFolder = { uri: Uri.parse(Uri.file(ws).toString() + '/'), name: 'ws', index: 0 }; + when(wsp().getWorkspaceFolder(anything())).thenReturn(folder); + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-workspace\n'); + const notebook = Uri.file(path.join(ws, 'a', 'nb.deepnote')); + + assert.isUndefined(await service.getToken(notebook)); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=at-root\n'); + assert.strictEqual(await service.getToken(notebook), 'at-root'); + }); + + test('does not search above a workspace folder matched in another path case', async () => { + // VS Code matches file URIs to workspace folders case-insensitively on macOS and Windows. + const folder: WorkspaceFolder = { uri: Uri.file(path.join(tmp, 'WS')), name: 'WS', index: 0 }; + when(wsp().getWorkspaceFolder(anything())).thenReturn(folder); + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-workspace\n'); + + assert.isUndefined(await service.getToken(Uri.file(path.join(ws, 'a', 'nb.deepnote')))); + }); + + test('reads up to a workspace folder matched in another path case, past a sync root below it', async () => { + // Path case is ignored only off Linux and CI runs on Linux, so apply the macOS and Windows comparison. + sandbox + .stub(extUriBiasedIgnorePathCase, 'isEqualOrParent') + .callsFake((...args) => extUriIgnorePathCase.isEqualOrParent(...args)); + sandbox + .stub(extUriBiasedIgnorePathCase, 'isEqual') + .callsFake((...args) => extUriIgnorePathCase.isEqual(...args)); + when(wsp().getWorkspaceFolder(anything())).thenReturn({ + uri: Uri.file(path.join(tmp, 'WS')), + name: 'WS', + index: 0 + }); + await createSyncManifest(path.join(ws, 'sync')); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=at-workspace\n'); + + assert.strictEqual( + await service.getToken(Uri.file(path.join(ws, 'sync', 'P', 'nb.deepnote'))), + 'at-workspace' + ); + }); + + test('resolves undefined for a non-file URI without touching the disk', async () => { + const stat = sandbox.spy(fs.promises, 'stat'); + const read = sandbox.spy(fs.promises, 'readFile'); + + assert.isUndefined(await service.getToken(Uri.parse('vscode-vfs://github/o/r/nb.deepnote'))); + assert.isTrue(stat.notCalled); + assert.isTrue(read.notCalled); + }); + + test('resolves undefined in an untrusted workspace without reading a file', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=t\n'); + when(wsp().isTrusted).thenReturn(false); + const read = sandbox.spy(fs.promises, 'readFile'); + + assert.isUndefined(await service.getToken(Uri.file(path.join(ws, 'nb.deepnote')))); + assert.isTrue(read.notCalled); + }); + + test('reads the last real assignment, not one inside a quoted multi-line value', async () => { + await writeFile( + path.join(ws, '.env'), + 'CERT="-----BEGIN\nDEEPNOTE_TOKEN=fake\n-----END"\nDEEPNOTE_TOKEN=real\n' + ); + + assert.strictEqual(await service.getToken(Uri.file(path.join(ws, 'nb.deepnote'))), 'real'); + }); + }); + + suite('getOrPromptToken', () => { + const notebook = () => Uri.file(path.join(ws, 'a', 'nb.deepnote')); + + test('returns the stored token without prompting', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=stored\n'); + + assert.strictEqual(await service.getOrPromptToken(notebook()), 'stored'); + verifyNoInputBox(); + }); + + test('prompts, saves and returns a new token when none is stored', async () => { + enterToken('fresh'); + + assert.strictEqual(await service.getOrPromptToken(notebook()), 'fresh'); + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=fresh\n'); + }); + + test('resolves undefined and writes nothing when the prompt is cancelled', async () => { + enterToken(undefined); + + assert.isUndefined(await service.getOrPromptToken(notebook())); + assert.isFalse(await exists(path.join(ws, '.env'))); + assert.isTrue(service.fetchWorkspaceStub.notCalled); + }); + + test('tells the user about local files and does not prompt for a non-file URI', async () => { + assert.isUndefined(await service.getOrPromptToken(Uri.parse('vscode-vfs://github/o/r/nb.deepnote'))); + + verifyError(DeepnoteCloud.localFilesOnly); + verifyNoInputBox(); + }); + + test('tells the user to trust the workspace and does not prompt when untrusted', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=t\n'); + when(wsp().isTrusted).thenReturn(false); + + assert.isUndefined(await service.getOrPromptToken(notebook())); + + verifyError(DeepnoteCloud.workspaceNotTrusted); + verifyNoInputBox(); + }); + }); + + suite('Set API Token', () => { + const NEW_TOKEN = 'new'; + + setup(() => { + activeFile(path.join(ws, 'a', 'b', 'nb.deepnote')); + enterToken(NEW_TOKEN); + }); + + test('creates the .env at the workspace root and names the Deepnote workspace', async () => { + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=new\n'); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenSavedForWorkspace('Acme', '.env'))).once(); + }); + + test('confirms without a workspace name when the API returns none', async () => { + service.fetchWorkspaceStub.resolves(undefined); + + await setApiToken(); + + verify(win().showInformationMessage(DeepnoteCloud.apiTokenSaved('.env'))).once(); + }); + + test('checks the token against the API of the configured domain', async () => { + await setApiToken(); + assert.deepStrictEqual(service.fetchWorkspaceStub.lastCall.args, ['https://api.deepnote.com', NEW_TOKEN]); + + const notebookUri = Uri.file(path.join(ws, 'a', 'b', 'nb.deepnote')).toString(); + when(wsp().getConfiguration('deepnote', anything())).thenCall((_section: string, resource?: Uri) => ({ + get: (key: string, fallback: unknown) => + key === 'domain' && resource?.toString() === notebookUri ? 'example.test' : fallback + })); + await setApiToken(); + assert.deepStrictEqual(service.fetchWorkspaceStub.lastCall.args, ['https://api.example.test', NEW_TOKEN]); + }); + + test('replaces the first assignment, drops the others, and keeps every other line', async () => { + await writeFile(path.join(ws, '.env'), 'A=1\nexport DEEPNOTE_TOKEN=old\nB=2\nDEEPNOTE_TOKEN=older\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\nDEEPNOTE_TOKEN=new\nB=2\n'); + }); + + test('keeps CRLF line endings', async () => { + await writeFile(path.join(ws, '.env'), 'A=1\r\nDEEPNOTE_TOKEN=old\r\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\r\nDEEPNOTE_TOKEN=new\r\n'); + }); + + test('appends after a last line that has no line ending', async () => { + await writeFile(path.join(ws, '.env'), 'A=1'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\nDEEPNOTE_TOKEN=new\n'); + }); + + test('rewrites the nearest .env that already assigns the token, not the workspace root', async () => { + await writeFile(path.join(ws, 'a', 'b', '.env'), 'A=1\nDEEPNOTE_TOKEN: old\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'a', 'b', '.env')), 'A=1\nDEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('rewrites an .env that assigns an empty token', async () => { + await writeFile(path.join(ws, 'a', '.env'), 'DEEPNOTE_TOKEN=\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'a', '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('writes to the sync root, not a folder in between, when no .env assigns the token', async () => { + const project = path.join(ws, 'sync', 'P'); + await createSyncManifest(path.join(ws, 'sync')); + await writeFile(path.join(project, 'nb.deepnote'), ''); + activeFile(path.join(project, 'nb.deepnote')); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'sync', '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(project, '.env'))); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('rewrites the .env next to the file even when a sync root is above it', async () => { + const project = path.join(ws, 'sync', 'P'); + await createSyncManifest(path.join(ws, 'sync')); + await writeFile(path.join(project, '.env'), 'DEEPNOTE_TOKEN=stale'); + activeFile(path.join(project, 'nb.deepnote')); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(project, '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, 'sync', '.env'))); + }); + + test('writes to the workspace root when the sync manifest is a symbolic link', async () => { + const project = path.join(ws, 'sync', 'P'); + await fs.promises.mkdir(project, { recursive: true }); + await writeFile(path.join(tmp, 'real-manifest.json'), '{}'); + await fs.promises.symlink( + path.join(tmp, 'real-manifest.json'), + path.join(ws, 'sync', '.deepnote-sync.json') + ); + activeFile(path.join(project, 'nb.deepnote')); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, 'sync', '.env'))); + }); + + test('rewrites only the nearest of several .env files that assign the token', async () => { + await writeFile(path.join(ws, 'a', 'b', '.env'), 'DEEPNOTE_TOKEN=old\n'); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=root\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'a', 'b', '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=root\n'); + }); + + test('keeps a variable whose name only starts with DEEPNOTE_TOKEN', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN_BACKUP=keep\nDEEPNOTE_TOKEN=old\n'); + + await setApiToken(); + + assert.strictEqual( + await readFile(path.join(ws, '.env')), + 'DEEPNOTE_TOKEN_BACKUP=keep\nDEEPNOTE_TOKEN=new\n' + ); + }); + + test('replaces a first-line assignment after a UTF-8 byte order mark and keeps the mark', async () => { + await writeFile(path.join(ws, '.env'), '\uFEFFDEEPNOTE_TOKEN=old\nB=2\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), '\uFEFFDEEPNOTE_TOKEN=new\nB=2\n'); + }); + + test('leaves an .env that is not UTF-8 byte-for-byte unchanged', async () => { + const latin1 = Buffer.from('DB_PASSWORD=p\xe4ss\nDEEPNOTE_TOKEN=old\n', 'latin1'); + await fs.promises.writeFile(path.join(ws, '.env'), latin1); + + await setApiToken(); + + assert.deepStrictEqual(await fs.promises.readFile(path.join(ws, '.env')), latin1); + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(anything())).never(); + }); + + test('does not overwrite an .env it cannot read with just the token', async () => { + const content = 'A=1\nDEEPNOTE_TOKEN=old\n'; + await writeFile(path.join(ws, '.env'), content); + const denied = Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' }); + sandbox.stub(fs.promises, 'readFile').rejects(denied); + const write = sandbox.spy(fs.promises, 'writeFile'); + + await setApiToken(); + + sandbox.restore(); + assert.isTrue(write.notCalled); + assert.strictEqual(await readFile(path.join(ws, '.env')), content); + verifyError(DeepnoteCloud.envEditFailed('.env')); + }); + + test('does not overwrite an .env above a workspace folder that is missing on disk', async () => { + const missing = path.join(tmp, 'projects', 'foo'); + openWorkspaceFolder(missing); + when(win().activeTextEditor).thenReturn(undefined); + await writeFile(path.join(tmp, 'projects', '.env'), 'DEEPNOTE_TOKEN=other-project\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(tmp, 'projects', '.env')), 'DEEPNOTE_TOKEN=other-project\n'); + }); + + test('keeps a workspace name from the API from forming a command link in the confirmation', async () => { + service.fetchWorkspaceStub.resolves({ id: 'w1', name: 'X [y](command:foo) [z](command:bar)' }); + + await setApiToken(); + + verify( + win().showInformationMessage( + DeepnoteCloud.apiTokenSavedForWorkspace('X [y] (command:foo) [z] (command:bar)', '.env') + ) + ).once(); + }); + + test('shows a workspace name with ordinary brackets and parentheses unchanged', async () => { + service.fetchWorkspaceStub.resolves({ id: 'w1', name: 'Acme [EU] (prod)' }); + + await setApiToken(); + + verify( + win().showInformationMessage(DeepnoteCloud.apiTokenSavedForWorkspace('Acme [EU] (prod)', '.env')) + ).once(); + }); + + test('uses the command argument over the active editor', async () => { + await writeFile(path.join(ws, 'other', '.env'), 'DEEPNOTE_TOKEN=old\n'); + await writeFile(path.join(ws, 'a', 'b', '.env'), 'DEEPNOTE_TOKEN=untouched\n'); + + await setApiToken(Uri.file(path.join(ws, 'other', 'nb.deepnote'))); + + assert.strictEqual(await readFile(path.join(ws, 'other', '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.strictEqual(await readFile(path.join(ws, 'a', 'b', '.env')), 'DEEPNOTE_TOKEN=untouched\n'); + }); + + test('starts from the active notebook, ignoring its query and fragment', async () => { + when(win().activeTextEditor).thenReturn(undefined); + when(win().activeNotebookEditor).thenReturn({ + notebook: { uri: Uri.file(path.join(ws, 'n', 'nb.deepnote')).with({ query: 'id=1', fragment: 'x' }) } + } as any); + await writeFile(path.join(ws, 'n', '.env'), 'DEEPNOTE_TOKEN=old\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'n', '.env')), 'DEEPNOTE_TOKEN=new\n'); + }); + + test('starts from the active notebook, not the focused cell editor inside it', async () => { + const notebookUri = Uri.file(path.join(ws, 'n', 'nb.deepnote')).with({ query: 'id=1' }); + when(win().activeNotebookEditor).thenReturn({ notebook: { uri: notebookUri } } as any); + when(win().activeTextEditor).thenReturn({ + document: { uri: notebookUri.with({ scheme: NotebookCellScheme, fragment: 'cell-1' }) } + } as any); + await writeFile(path.join(ws, 'n', '.env'), 'DEEPNOTE_TOKEN=old\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'n', '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('falls back to the only workspace folder when no file is open', async () => { + when(win().activeTextEditor).thenReturn(undefined); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=new\n'); + }); + + test('falls back to the workspace folder when the active file is not a local file', async () => { + when(win().activeTextEditor).thenReturn({ document: { uri: Uri.parse('untitled:Untitled-1') } } as any); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=new\n'); + }); + + test('asks which workspace folder to use when there are several', async () => { + const second = path.join(tmp, 'second'); + await fs.promises.mkdir(second); + when(win().activeTextEditor).thenReturn(undefined); + when(wsp().workspaceFolders).thenReturn([ + { uri: Uri.file(ws), name: 'ws', index: 0 }, + { uri: Uri.file(second), name: 'second', index: 1 } + ]); + when(win().showWorkspaceFolderPick()).thenResolve({ + uri: Uri.file(second), + name: 'second', + index: 1 + } as any); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(second, '.env')), 'DEEPNOTE_TOKEN=new\n'); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('stops without a prompt when the folder pick is cancelled', async () => { + when(win().activeTextEditor).thenReturn(undefined); + when(wsp().workspaceFolders).thenReturn([ + { uri: Uri.file(ws), name: 'ws', index: 0 }, + { uri: Uri.file(tmp), name: 'tmp', index: 1 } + ]); + when(win().showWorkspaceFolderPick()).thenResolve(undefined); + + await setApiToken(); + + verifyNoInputBox(); + }); + + test('asks to open a folder when there is neither a file nor a workspace folder', async () => { + when(win().activeTextEditor).thenReturn(undefined); + when(wsp().workspaceFolders).thenReturn(undefined); + + await setApiToken(); + + verifyError(DeepnoteCloud.openFolderFirst); + verifyNoInputBox(); + }); + + test('refuses a workspace folder that is not a local folder', async () => { + when(win().activeTextEditor).thenReturn(undefined); + when(wsp().workspaceFolders).thenReturn([ + { uri: Uri.parse('vscode-vfs://github/o/r'), name: 'r', index: 0 } + ]); + + await setApiToken(); + + verifyError(DeepnoteCloud.localFilesOnly); + verifyNoInputBox(); + }); + + test('writes nothing when the prompt is cancelled or blank', async () => { + enterToken(undefined); + await setApiToken(); + enterToken(' '); + await setApiToken(); + + assert.isTrue(service.fetchWorkspaceStub.notCalled); + assert.isFalse(await exists(path.join(ws, '.env'))); + }); + + test('trims the token before checking and saving it', async () => { + enterToken(' padded '); + + await setApiToken(); + + assert.strictEqual(service.fetchWorkspaceStub.lastCall.args[1], 'padded'); + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=padded\n'); + }); + + test('asks the input box for a masked prompt that rejects whitespace, quotes, # and control characters', async () => { + await setApiToken(); + + const { validateInput, ...options } = inputBoxOptions(); + assert.deepStrictEqual(options, { + password: true, + ignoreFocusOut: true, + prompt: DeepnoteCloud.apiTokenPrompt, + placeHolder: DeepnoteCloud.apiTokenPlaceholder + }); + for (const invalid of ['a b', 'a"b', "a'b", 'a#b', 'a\tb', 'a\u0000b', 'a\u007fb']) { + assert.strictEqual( + validateInput!(invalid), + DeepnoteCloud.apiTokenInvalidCharacters, + JSON.stringify(invalid) + ); + } + assert.isUndefined(validateInput!('abc-123_XYZ.=')); + assert.isUndefined(validateInput!(' abc ')); + }); + + test('writes nothing and says so when Deepnote rejects the token with 401', async () => { + service.fetchWorkspaceStub.rejects(new ApiError(401, 'Unauthorized')); + + await setApiToken(); + + assert.isFalse(await exists(path.join(ws, '.env'))); + verifyError(DeepnoteCloud.apiTokenRejected); + }); + + test('writes nothing and shows the reason when the check fails with 403', async () => { + service.fetchWorkspaceStub.rejects(new ApiError(403, 'Failed to fetch Deepnote workspace: HTTP 403')); + + await setApiToken(); + + assert.isFalse(await exists(path.join(ws, '.env'))); + verifyError(DeepnoteCloud.apiTokenCheckFailed('Failed to fetch Deepnote workspace: HTTP 403')); + }); + + test('writes nothing and shows the reason when the check fails for any other reason', async () => { + service.fetchWorkspaceStub.rejects(new Error('fetch failed')); + + await setApiToken(); + + assert.isFalse(await exists(path.join(ws, '.env'))); + verifyError(DeepnoteCloud.apiTokenCheckFailed('fetch failed')); + }); + + test('leaves an .env byte-for-byte unchanged when the edit would corrupt another variable', async () => { + const content = 'CERT="-----BEGIN\nDEEPNOTE_TOKEN=fake\n-----END"\nDEEPNOTE_TOKEN=real\n'; + await writeFile(path.join(ws, '.env'), content); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), content); + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(anything())).never(); + }); + + test('leaves the file unchanged when the token would not read back as typed', async () => { + enterToken('`quoted`'); + await writeFile(path.join(ws, '.env'), 'A=1\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\n'); + verifyError(DeepnoteCloud.envEditFailed('.env')); + }); + + test('reports a .env it cannot read or write instead of throwing', async () => { + await fs.promises.mkdir(path.join(ws, '.env')); + + await setApiToken(); + + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(anything())).never(); + }); + + test('stops for an untrusted workspace before any prompt, read, write or request', async () => { + when(wsp().isTrusted).thenReturn(false); + const read = sandbox.spy(fs.promises, 'readFile'); + const write = sandbox.spy(fs.promises, 'writeFile'); + + await setApiToken(); + + verifyError(DeepnoteCloud.workspaceNotTrusted); + verifyNoInputBox(); + assert.isTrue(service.fetchWorkspaceStub.notCalled); + assert.isTrue(read.notCalled); + assert.isTrue(write.notCalled); + }); + + suite('.gitignore offer', () => { + const offerAccepted = () => + when(win().showWarningMessage(anything(), anything(), anything())).thenResolve( + DeepnoteCloud.addToGitignore as any + ); + + test('adds .env to a new .gitignore next to the written .env when git does not ignore it', async () => { + service.isIgnoredByGitStub.resolves(false); + offerAccepted(); + + await setApiToken(); + + verify( + win().showWarningMessage( + DeepnoteCloud.envNotIgnored('.env'), + deepEqual({ modal: true }), + DeepnoteCloud.addToGitignore + ) + ).once(); + assert.strictEqual(await readFile(path.join(ws, '.gitignore')), '.env\n'); + }); + + test('appends to an existing .gitignore using its line endings', async () => { + service.isIgnoredByGitStub.resolves(false); + offerAccepted(); + await writeFile(path.join(ws, '.gitignore'), 'node_modules\r\nout'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.gitignore')), 'node_modules\r\nout\r\n.env\r\n'); + }); + + test('puts the .gitignore in the folder of an .env found below the workspace root', async () => { + service.isIgnoredByGitStub.resolves(false); + offerAccepted(); + await writeFile(path.join(ws, 'a', 'b', '.env'), 'DEEPNOTE_TOKEN=old\n'); + + await setApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'a', 'b', '.gitignore')), '.env\n'); + assert.isFalse(await exists(path.join(ws, '.gitignore'))); + }); + + test('leaves .gitignore alone when the offer is dismissed', async () => { + service.isIgnoredByGitStub.resolves(false); + + await setApiToken(); + + assert.isFalse(await exists(path.join(ws, '.gitignore'))); + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=new\n'); + }); + + test('appends .env to a .gitignore that is not UTF-8 and keeps its other bytes', async () => { + service.isIgnoredByGitStub.resolves(false); + offerAccepted(); + const latin1 = Buffer.from('# \xdc \xe9 docs\nnode_modules\n', 'latin1'); + await fs.promises.writeFile(path.join(ws, '.gitignore'), latin1); + + await setApiToken(); + + assert.deepStrictEqual( + await fs.promises.readFile(path.join(ws, '.gitignore')), + Buffer.concat([latin1, Buffer.from('.env\n')]) + ); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenSavedForWorkspace('Acme', '.env'))).once(); + }); + + test('shows no offer when git already ignores the file', async () => { + service.isIgnoredByGitStub.resolves(true); + + await setApiToken(); + + verify(win().showWarningMessage(anything(), anything(), anything())).never(); + }); + + test('shows no offer, and still confirms, when the Git extension gives no answer', async () => { + service.isIgnoredByGitStub.resolves(undefined); + + await setApiToken(); + + verify(win().showWarningMessage(anything(), anything(), anything())).never(); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenSavedForWorkspace('Acme', '.env'))).once(); + }); + + test('still confirms when the .gitignore cannot be written', async () => { + service.isIgnoredByGitStub.resolves(false); + offerAccepted(); + await fs.promises.mkdir(path.join(ws, '.gitignore')); + + await setApiToken(); + + verify(win().showInformationMessage(DeepnoteCloud.apiTokenSavedForWorkspace('Acme', '.env'))).once(); + }); + + test('asks the Git extension about the written file', async () => { + await setApiToken(); + + assert.strictEqual( + service.isIgnoredByGitStub.lastCall.args[0].fsPath, + Uri.file(path.join(ws, '.env')).fsPath + ); + }); + }); + }); + + suite('Clear API Token', () => { + setup(() => { + activeFile(path.join(ws, 'sync', 'P', 'nb.deepnote')); + }); + + test('removes the assignment from every .env on the path and keeps the other lines', async () => { + await createSyncManifest(path.join(ws, 'sync')); + await writeFile(path.join(ws, 'sync', 'P', '.env'), 'A=1\nDEEPNOTE_TOKEN=near\nB=2\n'); + await writeFile(path.join(ws, '.env'), 'export DEEPNOTE_TOKEN=root\nC=3\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'sync', 'P', '.env')), 'A=1\nB=2\n'); + assert.strictEqual(await readFile(path.join(ws, '.env')), 'C=3\n'); + assert.isUndefined(await service.getToken(Uri.file(path.join(ws, 'sync', 'P', 'nb.deepnote')))); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).once(); + }); + + test('clears the `KEY: value` form', async () => { + await writeFile(path.join(ws, 'sync', 'P', '.env'), 'A=1\nDEEPNOTE_TOKEN: old\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'sync', 'P', '.env')), 'A=1\n'); + assert.isUndefined(await service.getToken(Uri.file(path.join(ws, 'sync', 'P', 'nb.deepnote')))); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).once(); + }); + + test('keeps CRLF line endings', async () => { + await writeFile(path.join(ws, '.env'), 'A=1\r\nDEEPNOTE_TOKEN=t\r\nB=2\r\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\r\nB=2\r\n'); + }); + + test('never deletes a file, even when nothing else is left in it', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=t\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), ''); + }); + + test('says so when no .env assigns the token', async () => { + await writeFile(path.join(ws, '.env'), 'A=1\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\n'); + verify(win().showInformationMessage(DeepnoteCloud.noApiTokenFound)).once(); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).never(); + }); + + test('leaves an .env it cannot edit safely unchanged and names it', async () => { + const content = 'CERT="-----BEGIN\nDEEPNOTE_TOKEN=fake\n-----END"\nDEEPNOTE_TOKEN=real\n'; + await writeFile(path.join(ws, '.env'), content); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), content); + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).never(); + }); + + test('carries on with the next .env after one it cannot edit, and past a later one', async () => { + // Clear visits .env files nearest first: a failure before the editable file checks that it carries on, and + // one after it checks that the later failure does not hide the earlier success. + const unsafe = 'CERT="-----BEGIN\nDEEPNOTE_TOKEN=fake\n-----END"\nDEEPNOTE_TOKEN=real\n'; + await writeFile(path.join(ws, 'sync', 'P', '.env'), unsafe); + await writeFile(path.join(ws, 'sync', '.env'), 'A=1\nDEEPNOTE_TOKEN=mid\n'); + await writeFile(path.join(ws, '.env'), unsafe); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, 'sync', 'P', '.env')), unsafe); + assert.strictEqual(await readFile(path.join(ws, 'sync', '.env')), 'A=1\n'); + assert.strictEqual(await readFile(path.join(ws, '.env')), unsafe); + verifyError(DeepnoteCloud.envEditFailed(path.join('sync', 'P', '.env'))); + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).once(); + }); + + test('keeps a variable whose name only starts with DEEPNOTE_TOKEN', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN_BACKUP=keep\nDEEPNOTE_TOKEN=old\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN_BACKUP=keep\n'); + }); + + test('clears a first-line assignment after a UTF-8 byte order mark and keeps the mark', async () => { + await writeFile(path.join(ws, '.env'), '\uFEFFDEEPNOTE_TOKEN=t\nA=1\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), '\uFEFFA=1\n'); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).once(); + }); + + test('leaves an .env that is not UTF-8 byte-for-byte unchanged and names it', async () => { + const latin1 = Buffer.from('DB_PASSWORD=p\xe4ss\nDEEPNOTE_TOKEN=old\n', 'latin1'); + await fs.promises.writeFile(path.join(ws, '.env'), latin1); + + await clearApiToken(); + + assert.deepStrictEqual(await fs.promises.readFile(path.join(ws, '.env')), latin1); + verifyError(DeepnoteCloud.envEditFailed('.env')); + verify(win().showInformationMessage(DeepnoteCloud.apiTokenCleared)).never(); + }); + + test('leaves an .env above a workspace folder that is missing on disk unchanged', async () => { + const missing = path.join(tmp, 'projects', 'foo'); + openWorkspaceFolder(missing); + when(win().activeTextEditor).thenReturn(undefined); + await writeFile(path.join(tmp, 'projects', '.env'), 'DEEPNOTE_TOKEN=other-project\nA=1\n'); + + await clearApiToken(); + + assert.strictEqual( + await readFile(path.join(tmp, 'projects', '.env')), + 'DEEPNOTE_TOKEN=other-project\nA=1\n' + ); + verify(win().showInformationMessage(DeepnoteCloud.noApiTokenFound)).once(); + }); + + test('leaves an .env above a workspace folder matched in another path case unchanged', async () => { + when(wsp().getWorkspaceFolder(anything())).thenReturn({ + uri: Uri.file(path.join(tmp, 'WS')), + name: 'WS', + index: 0 + }); + await writeFile(path.join(tmp, '.env'), 'DEEPNOTE_TOKEN=above-workspace\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(tmp, '.env')), 'DEEPNOTE_TOKEN=above-workspace\n'); + }); + + test('clears the .env of the workspace folder when no file is open', async () => { + when(win().activeTextEditor).thenReturn(undefined); + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=t\nA=1\n'); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'A=1\n'); + }); + + test('does nothing but tell the user in an untrusted workspace', async () => { + await writeFile(path.join(ws, '.env'), 'DEEPNOTE_TOKEN=t\n'); + when(wsp().isTrusted).thenReturn(false); + + await clearApiToken(); + + assert.strictEqual(await readFile(path.join(ws, '.env')), 'DEEPNOTE_TOKEN=t\n'); + verifyError(DeepnoteCloud.workspaceNotTrusted); + }); + }); +}); + +suite('DeepnoteApiTokenService.isIgnoredByGit', () => { + // A drive letter makes fsPath, which the service passes to git, differ from uri.path on every platform. + const envUri = Uri.parse('file:///C:/repo/.env'); + const envPath = envUri.fsPath; + let probe: GitProbe; + + // Like VS Code, reading the exports of an extension that is not active yet throws. + function installGit(getAPI: () => unknown, isActive = true): void { + const exports = { getAPI }; + let active = isActive; + const extension: Extension = { + id: 'vscode.git', + extensionUri: Uri.file('/git'), + extensionPath: '/git', + get isActive() { + return active; + }, + packageJSON: {}, + extensionKind: ExtensionKind.Workspace, + get exports() { + if (!active) { + throw new Error("Extension 'vscode.git' is not known or not activated"); + } + + return exports; + }, + activate: () => { + active = true; + + return Promise.resolve(exports); + } + }; + when(mockedVSCodeNamespaces.extensions.getExtension('vscode.git')).thenReturn(extension); + } + + function apiIgnoring(ignoredPaths: string[]): () => unknown { + return () => ({ + getRepository: () => ({ + checkIgnore: (paths: string[]) => + Promise.resolve(new Set(paths.filter((candidate) => ignoredPaths.includes(candidate)))) + }) + }); + } + + setup(() => { + resetVSCodeMocks(); + probe = new GitProbe(instance(mock())); + }); + + test('is undefined when the Git extension is not installed', async () => { + when(mockedVSCodeNamespaces.extensions.getExtension('vscode.git')).thenReturn(undefined); + + assert.isUndefined(await probe.isIgnored(envUri)); + }); + + test('is undefined when the Git extension is not set up', async () => { + installGit(() => { + throw new Error('Git model not found'); + }); + + assert.isUndefined(await probe.isIgnored(envUri)); + }); + + test('is undefined outside an open repository and without checkIgnore', async () => { + installGit(() => ({ getRepository: () => null })); + assert.isUndefined(await probe.isIgnored(envUri)); + + installGit(() => ({ getRepository: () => ({}) })); + assert.isUndefined(await probe.isIgnored(envUri)); + }); + + test('is true when git ignores the file and false when it does not', async () => { + installGit(apiIgnoring([envPath])); + assert.isTrue(await probe.isIgnored(envUri)); + + installGit(apiIgnoring([])); + assert.isFalse(await probe.isIgnored(envUri)); + }); + + test('activates an inactive Git extension to get its API', async () => { + installGit(apiIgnoring([envPath]), false); + + assert.isTrue(await probe.isIgnored(envUri)); + }); +}); diff --git a/src/notebooks/deepnote/cloud/types.ts b/src/notebooks/deepnote/cloud/types.ts new file mode 100644 index 0000000000..92223698bc --- /dev/null +++ b/src/notebooks/deepnote/cloud/types.ts @@ -0,0 +1,18 @@ +import { Uri } from 'vscode'; + +export const IDeepnoteApiTokenService = Symbol('IDeepnoteApiTokenService'); +export interface IDeepnoteApiTokenService { + /** + * The trimmed `DEEPNOTE_TOKEN` from the nearest `.env` on the lookup path of `resource` that defines a non-empty + * value. `undefined` when no `.env` defines it, `resource` is not a local file, or the workspace is not trusted. + * Never prompts. + */ + getToken(resource: Uri): Promise; + + /** + * {@link getToken}, else asks the user for a token, checks it with Deepnote and saves it to `.env`. `undefined` + * means the user cancelled, `resource` is not a local file, or the workspace is not trusted; the user has already + * been told. + */ + getOrPromptToken(resource: Uri): Promise; +} diff --git a/src/notebooks/deepnote/importClient.node.ts b/src/notebooks/deepnote/importClient.node.ts index bd8506a959..3c3733546c 100644 --- a/src/notebooks/deepnote/importClient.node.ts +++ b/src/notebooks/deepnote/importClient.node.ts @@ -39,7 +39,7 @@ function getDomain(resource?: Uri): string { /** * Gets the API endpoint from configuration */ -function getApiEndpoint(resource?: Uri): string { +export function getApiEndpoint(resource?: Uri): string { const domain = getDomain(resource); return `https://api.${domain}`; } diff --git a/src/notebooks/serviceRegistry.node.ts b/src/notebooks/serviceRegistry.node.ts index 9c885cd583..e12cb0f36f 100644 --- a/src/notebooks/serviceRegistry.node.ts +++ b/src/notebooks/serviceRegistry.node.ts @@ -99,6 +99,8 @@ import { EphemeralCellStatusBarProvider } from './deepnote/ephemeralCellStatusBa import { OrphanedEphemeralCellCleaner } from './deepnote/orphanedEphemeralCellCleaner'; import { DeepnoteNewCellLanguageService } from './deepnote/deepnoteNewCellLanguageService'; import { DeepnoteCellCopyHandler } from './deepnote/deepnoteCellCopyHandler'; +import { DeepnoteApiTokenService } from './deepnote/cloud/deepnoteApiTokenService.node'; +import { IDeepnoteApiTokenService } from './deepnote/cloud/types'; import { OpenInDeepnoteHandler } from './deepnote/openInDeepnoteHandler.node'; import { IntegrationEnvRefreshHandler } from './deepnote/integrations/integrationEnvRefreshHandler'; import { IntegrationsEnvFileWatcher } from './deepnote/integrations/integrationsEnvFileWatcher.node'; @@ -230,6 +232,8 @@ export function registerTypes(serviceManager: IServiceManager, isDevMode: boolea IExtensionSyncActivationService, OpenInDeepnoteHandler ); + serviceManager.addSingleton(IDeepnoteApiTokenService, DeepnoteApiTokenService); + serviceManager.addBinding(IDeepnoteApiTokenService, IExtensionSyncActivationService); serviceManager.addSingleton( IExtensionSyncActivationService, IntegrationEnvRefreshHandler diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index dbbbb80601..464b442f97 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -273,6 +273,8 @@ export namespace Commands { export const AddNotebookToProject = 'deepnote.addNotebookToProject'; export const ExportNotebook = 'deepnote.exportNotebook'; export const OpenInDeepnote = 'deepnote.openInDeepnote'; + export const SetApiToken = 'deepnote.setApiToken'; + export const ClearApiToken = 'deepnote.clearApiToken'; export const ExportAsPythonScript = 'deepnote.exportAsPythonScript'; export const ExportToHTML = 'deepnote.exportToHTML'; export const ExportToPDF = 'deepnote.exportToPDF'; diff --git a/src/platform/common/utils/localize.ts b/src/platform/common/utils/localize.ts index 6ce22bf72b..e57378bdf8 100644 --- a/src/platform/common/utils/localize.ts +++ b/src/platform/common/utils/localize.ts @@ -1276,6 +1276,36 @@ export namespace Integrations { export const caCertificateTextPlaceholder = l10n.t('-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----'); } +export namespace DeepnoteCloud { + export const apiTokenPrompt = l10n.t( + 'Enter your Deepnote API token. Create one in Deepnote under Workspace > Settings & members > Security > API keys.' + ); + export const apiTokenPlaceholder = l10n.t('Deepnote API token'); + export const apiTokenInvalidCharacters = l10n.t('The token cannot contain spaces, quotes, # or control characters'); + export const checkingApiToken = l10n.t('Checking the Deepnote API token…'); + export const apiTokenRejected = l10n.t('Deepnote rejected this API token.'); + export const apiTokenCheckFailed = (message: string) => + l10n.t('Could not check the API token with Deepnote: {0}', message); + export const apiTokenSaved = (envPath: string) => l10n.t('Deepnote API token saved to {0}.', envPath); + export const envEditFailed = (envPath: string) => + l10n.t('Could not update DEEPNOTE_TOKEN in {0} automatically. Edit the file by hand.', envPath); + export const apiTokenSavedForWorkspace = (workspaceName: string, envPath: string) => + l10n.t('Signed in to the Deepnote workspace "{0}". Token saved to {1}.', workspaceName, envPath); + export const apiTokenCleared = l10n.t('Deepnote API token removed.'); + export const noApiTokenFound = l10n.t('No Deepnote API token is set for this folder.'); + export const envNotIgnored = (envPath: string) => + l10n.t('{0} is not ignored by git, so the token could be committed. Add .env to .gitignore?', envPath); + export const addToGitignore = l10n.t('Add to .gitignore'); + export const openFolderFirst = l10n.t('Open a folder to store the Deepnote API token in.'); + export const localFilesOnly = l10n.t('Deepnote Cloud sync and publishing work with local files only.'); + export const workspaceNotTrusted = l10n.t( + 'Deepnote Cloud features need a trusted workspace. Trust this workspace, then try again.' + ); + export const apiTokenRejectedForRequest = l10n.t('Deepnote rejected the API token. Set a new one and try again.'); + export const setApiToken = l10n.t('Set API Token'); + export const errorWithDetail = (message: string, detail: string) => l10n.t('{0}: {1}', message, detail); +} + export namespace SelectInputSettings { export const title = l10n.t('Select Input Settings'); export const allowMultipleValues = l10n.t('Allow to select multiple values');