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; -}