From 05befa7b915deefa567476ed33431ee5708b933b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 08:43:12 +0000 Subject: [PATCH] Merge into ~/.kube/config instead of round-tripping it through a model One entry the @kubernetes/client-node loader disliked cost the user every user, every context and their current-context. `readExistingConfig()` called `loadFromString`, which throws on an entry that fails its validation - `contexts[i].context.cluster is missing` being the easy one to hit, since that is what `kubectl config set-context --namespace=` writes. The throw was caught and logged at `debug`, and the half-loaded KubeConfig it left behind (`loadFromOptions` assigns clusters first, so those survive and nothing else does) was then merged into and written over the file. What is left has no current context, so kubectl falls back to localhost:8080 and every call fails. The same round trip was lossy even when it did not throw: the model carries only the fields it knows about, so `preferences`, per-entry `extensions`, `as`/`as-groups`, `disable-compression` and anything kubectl grows later were dropped on every write, while a `token-file` was read and inlined into the file as a bare `token`. So drop the model. `mergeKubeconfig(existing, clusters)` parses the YAML, edits the three lists in place and dumps it back; everything else in the document is handed back untouched. A same-named entry is updated by merge and stays where it is, so a namespace set on one of Frost's contexts survives the next refresh. A file that will not parse is left alone and logged at `error` - skipping an update beats replacing a file we could not read. Unchanged contents skip the write, and `lineWidth: -1` keeps certificate data and the authenticator path on one line, as `aws eks update-kubeconfig` writes them. Also fixes `getNamePattern`'s `uniqueClusters`, which compared the lengths of two lists built by mapping over the same array and so was always true. It now compares distinct cluster names against distinct cluster ids, which is what having both lists was for: two different clusters sharing a name used to collapse onto one context, silently losing one of them. @kubernetes/client-node was used nowhere else, so it and the @types/ws devDep it needed are gone. Verified with kubectl against the generated files: a config carrying a cluster-less context keeps all of its entries through a merge, two clusters sharing a name get two contexts pointing at two endpoints, the exec plugin runs and the CA parses, and a second run is a no-op. --- AGENTS.md | 48 ++- docs/docs/eks.html | 11 +- docs/docs/troubleshooting.html | 21 + package-lock.json | 695 +-------------------------------- package.json | 2 - src/kubeconfig.ts | 239 +++++++----- 6 files changed, 228 insertions(+), 788 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2ab013a..eb4eb78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -133,6 +133,39 @@ hand. `mergeAwsConfig()` is pure. - Writes go through `writeFilePreservingMode()` and are skipped when the merged contents are unchanged. +## `~/.kube/config` ownership + +`src/kubeconfig.ts` merges into the user's file for the same reason +`aws-config.ts` does: most of it is theirs. `mergeKubeconfig()` is pure. + +- The parsed YAML document is **edited in place**, never rebuilt from a model of + a kubeconfig. Everything Frost does not write — `current-context`, + `preferences`, per-entry `extensions`, impersonation keys, `token-file` — is + in the document it was loaded from and goes back out untouched. +- That is what `@kubernetes/client-node` got wrong, and why it is gone. Its + model carried only the fields it knew, so a round trip dropped the rest, and + its loader **throws** on an entry it dislikes — a context with no cluster, + which is what `kubectl config set-context x --namespace=y` writes. The throw + was caught at `debug` and the half-loaded config overwrote the file, taking + every user, every context and `current-context` with it. +- One cluster, one user and one context per discovered cluster, named by + `getNamePattern()`. A same-named entry is updated **in place and by merge**, + so a namespace the user set on one of Frost's contexts survives a refresh. +- A file that does not parse is **left alone**, logged at `error`. Skipping an + update beats replacing a file we could not read. +- Unchanged contents skip the write. `yaml.dump` runs with `lineWidth: -1`: the + default 80 folds certificate data and the authenticator path across lines, + which is legal but unlike what `aws eks update-kubeconfig` writes. +- Writes go through `writeFilePreservingMode()`. + +`getNamePattern()` picks the shortest context name that still tells the +discovered clusters apart (`docs/docs/eks.html` has the table users see). Its +`uniqueClusters` test compares distinct cluster **names** against distinct +cluster **ids** (`name:account:region`) — one cluster reached through several +profiles is one of each; two clusters sharing a name are two ids and one name. +Comparing the two lists' lengths, as it did originally, is always true, and two +clusters sharing a name then collapse onto one context with one of them lost. + ## Cross-platform Each platform branch exists for a reason. @@ -180,10 +213,12 @@ Each platform branch exists for a reason. - **electron-log v5**: import `electron-log/main` in the main process; error catching moved to `log.errorHandler`. - **update-electron-app v3**: named export. -- **@kubernetes/client-node v2**, **electron-store v11**, **delay v7**, - **uuid v14**: pure ESM. The k8s client has no default export. It does not - declare `@types/ws` but its types reach `ws` through `isomorphic-ws`, so - `@types/ws` must stay a devDependency or `tsc` fails. +- **electron-store v11**, **delay v7**, **uuid v14**: pure ESM. +- **@kubernetes/client-node is gone, and should not come back.** It was a lossy + YAML shuttle for a file the app never talks to a cluster about — see + "`~/.kube/config` ownership". Dropping it also dropped `@types/ws`, a + devDependency only because the client's types reach `ws` through + `isomorphic-ws`. - **oxlint, not ESLint.** oxlint has its own parser and never loads the TypeScript compiler API, which is what lets this repo hold a single TypeScript — see the TypeScript note below. `eslint`, `typescript-eslint` and @@ -411,6 +446,11 @@ Worth doing headlessly, since nothing else covers it: another directory — assert the link survives and the target changed. - **`src/aws-config.ts`**: the merge is pure and importable, so exercise it with a throwaway script (stub the logging import). Include a CRLF case. +- **`src/kubeconfig.ts`**: `mergeKubeconfig` is pure and exported, so exercise + it the same way and run a real `kubectl --kubeconfig` over the output. Point + `AWS_IAM_AUTHENTICATOR_PATH` at a stub that prints an `ExecCredential` and + kubectl walks the whole exec path with no AWS account. Cover an existing + config carrying an entry the old loader rejected — a context with no cluster. - **`src/schedule.ts`**: pure and electron-free, so its delay arithmetic can be exercised directly. diff --git a/docs/docs/eks.html b/docs/docs/eks.html index afbdfe7..48318e3 100644 --- a/docs/docs/eks.html +++ b/docs/docs/eks.html @@ -167,12 +167,21 @@

How contexts are named

How the merge treats your kubeconfig

- Frost reads ~/.kube/config, replaces any cluster, user or + Frost reads ~/.kube/config, updates any cluster, user or context whose name matches one it just discovered, and writes the file back. Entries you added yourself — clusters from other providers, contexts from kind or Docker Desktop — are kept, as is your current context.

+

+ Only the keys Frost owns are rewritten, and matched entries stay where + they are in the file. A namespace you set on one of Frost's contexts with + kubectl config set-context --namespace is still there after + the next refresh. If the file cannot be parsed at all, Frost leaves it + untouched and records the reason on the + Activity & Logs page rather than writing + over something it could not read. +

diff --git a/docs/docs/troubleshooting.html b/docs/docs/troubleshooting.html index 2dadb0e..fb5e4cd 100644 --- a/docs/docs/troubleshooting.html +++ b/docs/docs/troubleshooting.html @@ -171,6 +171,27 @@

The name is too long, or in the wrong region

kubectl cannot reach a cluster

+

Contexts you did not get from Frost disappeared after a refresh

+

+ Versions up to v0.3.2 merged into ~/.kube/config through a + parser that rejected the whole file over one entry it did not like — most + often a context with no cluster, which is what + kubectl config set-context name --namespace=ns + writes. The rejection was swallowed, and the file was then written from + what little had loaded, taking your users, your contexts and your + current-context with it. kubectl falls back to + localhost:8080 with no current context, which is the + "connection to the server localhost:8080 was refused" you are probably + looking at. +

+

+ Later versions edit the file in place and leave one they cannot parse + alone. There is nothing to recover from Frost — it keeps no copy — so + rebuild the entries you lost (aws eks update-kubeconfig for + EKS clusters you reach another way, or your provider's own command) and + run a refresh; Frost's own contexts come back on their own. +

+

"executable … not found" or a path error

The kubeconfig entry points at the authenticator inside the Frost diff --git a/package-lock.json b/package-lock.json index 95a580b..f4d6f68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@aws-sdk/client-sso": "^3.1121.0", "@aws-sdk/client-sso-oidc": "^3.1121.0", "@aws-sdk/credential-providers": "^3.1121.0", - "@kubernetes/client-node": "^2.0.0", "atomically": "^2.1.1", "delay": "^7.0.0", "electron-log": "^5.4.4", @@ -33,7 +32,6 @@ "@electron-forge/maker-zip": "8.0.0-alpha.10", "@types/electron-prompt": "^1.6.5", "@types/ini": "^4.1.1", - "@types/ws": "^8.18.1", "copyfiles": "^2.4.1", "electron": "^44.0.0", "oxlint": "^1.80.0", @@ -803,63 +801,6 @@ "node": ">=18.0.0" } }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@kubernetes/client-node": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-2.0.0.tgz", - "integrity": "sha512-Jx2cRxEVb4XkDNiR/cg8SX9dH6ZHdMhKmZdQcfhn2vdBWHdb2ldwM0P3I0dK4msYhFmC6TCODsNHH144IpA06w==", - "license": "Apache-2.0", - "dependencies": { - "@types/js-yaml": "^4.0.1", - "@types/node": "^26.0.0", - "@types/stream-buffers": "^3.0.3", - "form-data": "^4.0.0", - "hpagent": "^1.2.0", - "isomorphic-ws": "^5.0.0", - "js-yaml": "^5.1.0", - "jsonpath-plus": "^10.3.0", - "openid-client": "^6.1.3", - "rfc4648": "^1.3.0", - "socks": "^2.8.4", - "socks-proxy-agent": "^10.0.0", - "stream-buffers": "^3.0.2", - "tar-fs": "^3.0.9", - "undici": "^8.7.0", - "ws": "^8.18.2" - } - }, - "node_modules/@kubernetes/client-node/node_modules/undici": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", - "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -1304,40 +1245,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/js-yaml": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", - "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/stream-buffers": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.8.tgz", - "integrity": "sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript/typescript-aix-ppc64": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", @@ -1698,15 +1605,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1801,12 +1699,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/atomically": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", @@ -1817,20 +1709,6 @@ "when-exit": "^2.1.4" } }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1841,86 +1719,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.1.tgz", - "integrity": "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.1.tgz", - "integrity": "sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==", - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.2.tgz", - "integrity": "sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1961,19 +1759,6 @@ "node": "20 || >=22" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -2155,18 +1940,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -2376,6 +2149,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2405,15 +2179,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/dot-prop": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.2.0.tgz", @@ -2429,20 +2194,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2696,15 +2447,6 @@ "dev": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/env-paths": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", @@ -2724,51 +2466,6 @@ "dev": true, "license": "MIT" }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2786,15 +2483,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -2808,12 +2496,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", @@ -2890,22 +2572,6 @@ "node": ">=22.12.0" } }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -2929,15 +2595,6 @@ "dev": true, "license": "ISC" }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/galactus": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/galactus/-/galactus-2.0.2.tgz", @@ -2962,43 +2619,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/github-url-to-object": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/github-url-to-object/-/github-url-to-object-4.0.6.tgz", @@ -3026,18 +2646,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3045,54 +2653,6 @@ "dev": true, "license": "ISC" }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hpagent": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", - "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3121,15 +2681,6 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, - "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", @@ -3179,15 +2730,6 @@ "node": ">=20" } }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "license": "MIT", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -3198,15 +2740,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/jose": { - "version": "6.2.10", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", - "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/js-yaml": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.1.tgz", @@ -3229,15 +2762,6 @@ "js-yaml": "bin/js-yaml.mjs" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -3261,24 +2785,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/jsonpath-plus": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", - "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/junk": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", @@ -3348,36 +2854,6 @@ "node": "20 || >=22" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -3573,19 +3049,11 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/oauth4webapi": { - "version": "3.8.7", - "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.7.tgz", - "integrity": "sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3607,19 +3075,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openid-client": { - "version": "6.8.7", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.8.7.tgz", - "integrity": "sha512-gtKthNu7evSBvTdrrlHb4F3Fi9dcwlb5QaITlCs+9mfpvuOi0Q3qtBf5+iY4sEP8hy1qCoAdxBNPDcmZeVSDzQ==", - "license": "MIT", - "dependencies": { - "jose": "^6.2.8", - "oauth4webapi": "^3.8.7" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/oxlint": { "version": "1.80.0", "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz", @@ -3816,16 +3271,6 @@ "node": ">=10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/random-int": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/random-int/-/random-int-3.1.0.tgz", @@ -3928,12 +3373,6 @@ "node": ">= 4" } }, - "node_modules/rfc4648": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.4.tgz", - "integrity": "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==", - "license": "MIT" - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -4088,64 +3527,6 @@ "node": ">=8.0.0" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", - "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", - "license": "MIT", - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/stream-buffers": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", - "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", - "license": "Unlicense", - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", @@ -4244,41 +3625,6 @@ "node": ">=18" } }, - "node_modules/tar-fs": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", - "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -4308,15 +3654,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -4457,12 +3794,6 @@ "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "license": "MIT" - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -4570,29 +3901,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, - "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/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 578b28d..9bc1571 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@electron-forge/maker-zip": "8.0.0-alpha.10", "@types/electron-prompt": "^1.6.5", "@types/ini": "^4.1.1", - "@types/ws": "^8.18.1", "copyfiles": "^2.4.1", "electron": "^44.0.0", "oxlint": "^1.80.0", @@ -39,7 +38,6 @@ "@aws-sdk/client-sso": "^3.1121.0", "@aws-sdk/client-sso-oidc": "^3.1121.0", "@aws-sdk/credential-providers": "^3.1121.0", - "@kubernetes/client-node": "^2.0.0", "atomically": "^2.1.1", "delay": "^7.0.0", "electron-log": "^5.4.4", diff --git a/src/kubeconfig.ts b/src/kubeconfig.ts index cdae6b0..573e8ea 100644 --- a/src/kubeconfig.ts +++ b/src/kubeconfig.ts @@ -5,7 +5,6 @@ import { existsSync } from "fs"; import { writeFilePreservingMode } from "./atomic-write.js"; import log from "electron-log/main"; import { ClusterInfo } from "./aws-eks.js"; -import { KubeConfig } from "@kubernetes/client-node"; import * as yaml from "js-yaml"; const AWS_IAM_AUTHENTICATOR_BASENAME = @@ -49,79 +48,172 @@ const AWS_IAM_AUTHENTICATOR = findAwsIamAuthenticator(); type NamePattern = (info: ClusterInfo) => string; -async function readExistingConfig(): Promise<{ - path: string; - currentKubeconfig: KubeConfig; -}> { - const path = join(homedir(), ".kube", "config"); - log.info("[readExistingConfig] Reading %s", path); - const currentKubeconfig = new KubeConfig(); +/** The per-entry key that holds the body, one per kubeconfig list. */ +type Section = "cluster" | "user" | "context"; + +/** One entry of `clusters`, `users` or `contexts`. */ +interface NamedEntry { + name?: unknown; + [key: string]: unknown; +} + +/** + * A kubeconfig as it is on disk. Only the three lists are spelled out because + * they are the only thing Frost writes; `current-context`, `preferences`, + * per-entry `extensions` and whatever kubectl grows next ride along in the + * index signature and are written back untouched. + */ +interface KubeconfigDocument { + clusters?: NamedEntry[]; + users?: NamedEntry[]; + contexts?: NamedEntry[]; + [key: string]: unknown; +} + +function kubeconfigPath(): string { + return join(homedir(), ".kube", "config"); +} + +async function readKubeconfigFile(fullPath: string): Promise { try { - const contents = await readFile(path); - currentKubeconfig.loadFromString(contents.toString()); + return (await readFile(fullPath)).toString(); } catch (err) { log.debug( - "[readExistingConfig] While trying to read %s: %s", - path, + "[readKubeconfigFile] While trying to read %s: %s", + fullPath, err ); + return ""; } - return { path, currentKubeconfig }; } export async function writeKubeconfig(clusters: ClusterInfo[]) { - const { path, currentKubeconfig } = await readExistingConfig(); - log.info("[writeKubeconfig] Writing %s", path); + const fullPath = kubeconfigPath(); + const existing = await readKubeconfigFile(fullPath); - const namePattern = getNamePattern(clusters); - - const kubeconfig = clusters - .map((cluster) => toKubeconfig(cluster, namePattern)) - .reduce(mergeKubeConfigs, currentKubeconfig); + let contents: string; + try { + contents = mergeKubeconfig(existing, clusters); + } catch (err) { + // Most of this file is the user's - their own clusters, their + // current-context. One we cannot parse is one we cannot merge into, + // and writing our entries on their own would throw the rest away. + log.error( + "[writeKubeconfig] Leaving %s alone, cannot parse it: %s", + fullPath, + err + ); + return; + } - const exported = JSON.parse(kubeconfig.exportConfig()); - const contents = yaml.dump(exported); + if (contents === existing) { + log.info("[writeKubeconfig] %s is up to date", fullPath); + return; + } + log.info("[writeKubeconfig] Writing %s", fullPath); // Holds the user's own clusters as well as ours, so an interrupted run // must not truncate it and their permissions are theirs to keep. - await writeFilePreservingMode(path, contents); + await writeFilePreservingMode(fullPath, contents); } -function mergeKubeConfigs( - existing: KubeConfig, - update: KubeConfig -): KubeConfig { - if (!existing.clusters) { - existing.clusters = []; +/** + * Merges the discovered clusters into an existing kubeconfig. + * + * The parsed document is edited in place rather than rebuilt from a model of a + * kubeconfig. A model only carries the fields it knows about, so a round trip + * through one drops the rest - `preferences`, `extensions`, impersonation keys + * - and a single entry it refuses to parse costs the user every context and + * user in the file. `kubectl config set-context x --namespace=y` writes such + * an entry: a context with no cluster. + * + * Pure and importable, so it can be exercised without an Electron app. + */ +export function mergeKubeconfig( + existing: string, + infos: ClusterInfo[] +): string { + const document = parseKubeconfig(existing); + const getName = getNamePattern(infos); + + for (const info of infos) { + const name = getName(info); + + document.clusters = upsert(document.clusters, name, "cluster", { + server: info.cluster.endpoint!, + "certificate-authority-data": + info.cluster.certificateAuthority!.data!, + }); + + document.users = upsert(document.users, name, "user", { + exec: { + apiVersion: "client.authentication.k8s.io/v1", + command: AWS_IAM_AUTHENTICATOR, + args: ["token", "-i", info.cluster.name!], + env: [{ name: "AWS_PROFILE", value: info.profile.name }], + interactiveMode: "Never", + }, + }); + + document.contexts = upsert(document.contexts, name, "context", { + cluster: name, + user: name, + }); } - update.clusters.forEach((newCluster) => { - existing.clusters = existing.clusters.filter( - (existingCluster) => existingCluster.name !== newCluster.name - ); - existing.clusters.push(newCluster); - }); - if (!existing.users) { - existing.users = []; + // Every value on one line. Folding a base64 certificate or an application + // path across lines is legal YAML and kubectl reads it back, but it leaves + // a file unlike what `aws eks update-kubeconfig` writes and unreadable in + // a diff. + return yaml.dump(document, { lineWidth: -1 }); +} + +function parseKubeconfig(existing: string): KubeconfigDocument { + if (existing.trim() === "") { + return { apiVersion: "v1", kind: "Config", preferences: {} }; } - update.users.forEach((newUser) => { - existing.users = existing.users.filter( - (existingUser) => existingUser.name !== newUser.name - ); - existing.users.push(newUser); - }); - if (!existing.contexts) { - existing.contexts = []; + const document = yaml.load(existing); + if ( + typeof document !== "object" || + document === null || + Array.isArray(document) + ) { + throw new Error("kubeconfig is not a YAML mapping"); } - update.contexts.forEach((newContext) => { - existing.contexts = existing.contexts.filter( - (existingContext) => existingContext.name !== newContext.name - ); - existing.contexts.push(newContext); - }); + return document as KubeconfigDocument; +} - return existing; +/** + * Writes one `{ name,

: {...} }` entry, replacing the same-named one + * if it is already there. + * + * That replacement merges, and keeps the entry where it was: Frost owns the + * keys it writes and nothing else, so a namespace the user set on one of these + * contexts, or a `proxy-url` on the cluster, survives the next refresh. + */ +function upsert( + entries: NamedEntry[] | undefined, + name: string, + section: Section, + body: Record +): NamedEntry[] { + const list = Array.isArray(entries) ? entries : []; + const index = list.findIndex((entry) => entry?.name === name); + + if (index < 0) { + return [...list, { name, [section]: body }]; + } + + const previous = list[index][section]; + const merged = + typeof previous === "object" && previous !== null + ? { ...previous, ...body } + : body; + + return list.map((entry, at) => + at === index ? { ...entry, name, [section]: merged } : entry + ); } function getNamePattern(infos: ClusterInfo[]): NamePattern { @@ -133,7 +225,12 @@ function getNamePattern(infos: ClusterInfo[]): NamePattern { const roleNames = infos.map((info) => info.profile.roleName); const regionNames = infos.map((info) => info.region.RegionName); - const uniqueClusters = clusterNames.length === clusterIds.length; + // Whether the bare name tells two *different* clusters apart. One cluster + // reached through several profiles is one id and one name; two clusters + // that happen to share a name are two ids and one name, and every pattern + // below would then hand them the same entry and lose one of them. + const uniqueClusters = + new Set(clusterNames).size === new Set(clusterIds).size; const sameRoleName = new Set(roleNames).size === 1; const sameRegion = new Set(regionNames).size === 1; @@ -156,39 +253,3 @@ function getNamePattern(infos: ClusterInfo[]): NamePattern { return (info: ClusterInfo) => `${info.cluster.name}:${info.profile.accountName}:${info.region.RegionName}:${info.profile.roleName}`; } - -function toKubeconfig(info: ClusterInfo, getName: NamePattern): KubeConfig { - const kubeconfig = new KubeConfig(); - const name = getName(info); - - kubeconfig.addCluster({ - name, - server: info.cluster.endpoint!, - caData: info.cluster.certificateAuthority!.data!, - skipTLSVerify: false, - }); - - kubeconfig.addUser({ - name, - exec: { - apiVersion: "client.authentication.k8s.io/v1", - command: AWS_IAM_AUTHENTICATOR, - args: ["token", "-i", info.cluster.name], - env: [ - { - name: "AWS_PROFILE", - value: info.profile.name, - }, - ], - interactiveMode: "Never", - }, - }); - - kubeconfig.addContext({ - name, - user: name, - cluster: name, - }); - - return kubeconfig; -}