diff --git a/.gitattributes b/.gitattributes index 7c486a5e6..32cd82512 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ vendor/* linguist-generated=true +internal/cli/data/universal-portals/universal-portals.mjs linguist-generated=true diff --git a/internal/cli/data/universal-portals/_source/package-lock.json b/internal/cli/data/universal-portals/_source/package-lock.json index 7b811c5bd..29f0e3986 100644 --- a/internal/cli/data/universal-portals/_source/package-lock.json +++ b/internal/cli/data/universal-portals/_source/package-lock.json @@ -8,18 +8,18 @@ "name": "universal-portals", "version": "0.0.0-dev", "dependencies": { - "@clack/prompts": "1.8.1", - "auth0": "4.29.0" + "@clack/prompts": "1.8.1" }, "bin": { "universal-portals": "dist/src/cli.js" }, "devDependencies": { "@types/node": "^22.0.0", + "esbuild": "0.28.2", "typescript": "^5.0.0" }, "engines": { - "node": ">=20" + "node": ">=20.12" } }, "node_modules/@clack/core": { @@ -50,6 +50,448 @@ "node": ">= 20.12.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@types/node": { "version": "22.20.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", @@ -60,18 +502,46 @@ "undici-types": "~6.21.0" } }, - "node_modules/auth0": { - "version": "4.29.0", - "resolved": "https://registry.npmjs.org/auth0/-/auth0-4.29.0.tgz", - "integrity": "sha512-sTwC+579zoHthOFJLspVkghpaS9iAbA8U4ttirvZot4zvIG6EP0tpWUukVr9gzGXHUZcA+UdlKbHQwRSF8KCjg==", + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "jose": "^4.13.2", - "undici-types": "^6.15.0", - "uuid": "^9.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/fast-string-truncated-width": { @@ -98,15 +568,6 @@ "fast-string-width": "^3.0.2" } }, - "node_modules/jose": { - "version": "4.15.9", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", - "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -131,21 +592,8 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } } } } diff --git a/internal/cli/data/universal-portals/_source/package.json b/internal/cli/data/universal-portals/_source/package.json index 6097933fe..fec709ee5 100644 --- a/internal/cli/data/universal-portals/_source/package.json +++ b/internal/cli/data/universal-portals/_source/package.json @@ -11,6 +11,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json && chmod +x dist/src/cli.js", + "bundle": "npm run build && node scripts/bundle.mjs", "test": "rm -rf dist && npm run build && node --test dist/test/*.test.js", "preview": "npm run preview:setup", "preview:setup": "npm run build >/dev/null && node scripts/preview-setup.mjs", @@ -20,11 +21,11 @@ "node": ">=20.12" }, "dependencies": { - "@clack/prompts": "1.8.1", - "auth0": "4.29.0" + "@clack/prompts": "1.8.1" }, "devDependencies": { "@types/node": "^22.0.0", + "esbuild": "0.28.2", "typescript": "^5.0.0" } } diff --git a/internal/cli/data/universal-portals/_source/scripts/bundle.mjs b/internal/cli/data/universal-portals/_source/scripts/bundle.mjs new file mode 100644 index 000000000..42dd889d6 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/scripts/bundle.mjs @@ -0,0 +1,31 @@ +// Builds the single file the Auth0 CLI embeds: the setup and audit tool and its dependencies in one +// ES module that `node` can run. Run it with `npm run bundle`, which compiles first so the hash +// code is current. The output is generated: change the sources and run this again. +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; + +import { sourceHash } from "../dist/src/source-hash.js"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const outfile = fileURLToPath(new URL("../../universal-portals.mjs", import.meta.url)); + +await build({ + absWorkingDir: root, + entryPoints: ["src/cli.ts"], + bundle: true, + platform: "node", + format: "esm", + target: "node20", + outfile, + legalComments: "none", + logLevel: "warning", + banner: { + js: [ + '// Generated by "npm run bundle" in the _source folder. Do not edit.', + `// source-sha256: ${sourceHash(root)}`, + // Some dependencies still call require(), which an ES module does not have. + 'import { createRequire } from "node:module";', + "const require = createRequire(import.meta.url);", + ].join("\n"), + }, +}); diff --git a/internal/cli/data/universal-portals/_source/src/api.ts b/internal/cli/data/universal-portals/_source/src/api.ts index e0a66f24e..58e7f4de9 100644 --- a/internal/cli/data/universal-portals/_source/src/api.ts +++ b/internal/cli/data/universal-portals/_source/src/api.ts @@ -1,5 +1,5 @@ -// Management API client. Requests go through the CLI's loopback proxy, which -// swaps the proxy token for the real access token, so this module never holds it. +// Management API client interface. An implementation sends the requests through the CLI, either its +// loopback proxy or the `auth0 api` command (see cli-api.ts), so this module never holds a token. export type HttpMethod = "GET" | "POST" | "PATCH" | "DELETE"; @@ -62,8 +62,9 @@ export function listItems(body: unknown, key: string): T[] { const PAGE_SIZE = 100; const MAX_PAGES = 100; -// Reads every page of a list endpoint. A short page is the last one, so the total is -// not needed, and endpoints that reject extra query parameters are not affected. +// Reads every page of a list endpoint that takes page and per_page. A short page is the last +// one, so the total is not needed. A few endpoints page by checkpoint instead and reject these +// parameters: they use listAllByCheckpoint. export async function listAll(api: ApiClient, path: string, key: string): Promise { const separator = path.includes("?") ? "&" : "?"; const items: T[] = []; @@ -75,6 +76,22 @@ export async function listAll(api: ApiClient, path: string, key: string): Pro throw new Error(`${path} returned more than ${MAX_PAGES * PAGE_SIZE} items. The listing did not stop paging.`); } +// Reads every page of an endpoint that pages by checkpoint: the request takes a count and the +// token of the page to start from, and the response carries `next` while there is more. +export async function listAllByCheckpoint(api: ApiClient, path: string, key: string): Promise { + const separator = path.includes("?") ? "&" : "?"; + const items: T[] = []; + let from: string | undefined; + for (let page = 0; page < MAX_PAGES; page += 1) { + const query = `take=${PAGE_SIZE}${from ? `&from=${encodeURIComponent(from)}` : ""}`; + const body = await api.request("GET", `${path}${separator}${query}`); + items.push(...listItems(body, key)); + from = isRecord(body) && typeof body.next === "string" && body.next !== "" ? body.next : undefined; + if (!from) return items; + } + throw new Error(`${path} returned more than ${MAX_PAGES * PAGE_SIZE} items. The listing did not stop paging.`); +} + function safeJson(text: string): unknown { try { return JSON.parse(text); diff --git a/internal/cli/data/universal-portals/_source/src/audit-command.ts b/internal/cli/data/universal-portals/_source/src/audit-command.ts index d6c145880..98120067c 100644 --- a/internal/cli/data/universal-portals/_source/src/audit-command.ts +++ b/internal/cli/data/universal-portals/_source/src/audit-command.ts @@ -1,7 +1,6 @@ -import { createApiClient } from "./api.js"; import type { ParsedSetupArgs } from "./args.js"; import { CancelledError, type Prompter } from "./prompt.js"; -import { checkTenantFlag, readContext } from "./context.js"; +import { apiFor, checkTenantFlag, readContext } from "./context.js"; import { applyFixRounds, fixableFindings } from "./audit/fix.js"; import type { Finding } from "./audit/checks.js"; import { AuditOptions, AuditResult, runAudit } from "./audit/run.js"; @@ -19,9 +18,9 @@ export async function runAuditCommand( prompter: Prompter | undefined, ): Promise { try { - const context = readContext(env); + const context = await readContext(env, args.tenant, io.run); checkTenantFlag(args.tenant, context); - const api = createApiClient(context.proxyUrl, context.proxyToken, io.fetchImpl); + const api = apiFor(context, { fetchImpl: io.fetchImpl, run: io.run }); const portalId = args.portalId ?? ""; const organizationId = args.organization; const readSnapshot = () => fetchPortalSnapshot(api, portalId, context.tenantDomain, { organizationId }); diff --git a/internal/cli/data/universal-portals/_source/src/audit/snapshot.ts b/internal/cli/data/universal-portals/_source/src/audit/snapshot.ts index d41c25f5c..6a381630c 100644 --- a/internal/cli/data/universal-portals/_source/src/audit/snapshot.ts +++ b/internal/cli/data/universal-portals/_source/src/audit/snapshot.ts @@ -1,4 +1,4 @@ -import { ApiClient, ApiError, listAll } from "../api.js"; +import { ApiClient, ApiError, listAll, listAllByCheckpoint } from "../api.js"; // Everything the audit needs about one portal, read from the tenant. The checks // work on this snapshot only, so they can be tested without a tenant. @@ -74,25 +74,18 @@ export async function fetchPortalSnapshot( ): Promise { const portal = await api.request("GET", `portals/${encodeURIComponent(portalId)}`); - let application: ApplicationShape | undefined; - let grants: PortalSnapshot["grants"] = []; - let applicationConnections: PortalSnapshot["applicationConnections"] = []; - const clientId = portal.client?.client_id; - if (clientId) application = await readApplication(api, clientId); - if (application && clientId) { - grants = await listAll(api, `client-grants?client_id=${encodeURIComponent(clientId)}`, "client_grants"); - applicationConnections = await listAll<{ id: string; name?: string }>(api, `clients/${encodeURIComponent(clientId)}/connections`, "connections"); - } - - const resourceServers = (await listAll<{ identifier: string }>(api, "resource-servers", "resource_servers")).map((server) => server.identifier); - - const forms: Record = {}; - for (const formId of formIdsOf(portal)) { - forms[formId] = await formExists(api, formId); - } - - const customDomain = await readCustomDomain(api); - const organization = options.organizationId ? await readOrganization(api, options.organizationId) : undefined; + // Once the portal is read, what else the audit needs does not depend on each other, so it is + // read together. + const [applicationParts, resourceServers, forms, customDomain, organization] = await Promise.all([ + readApplicationParts(api, portal.client?.client_id), + listAll<{ identifier: string }>(api, "resource-servers", "resource_servers").then((servers) => + servers.map((server) => server.identifier), + ), + readForms(api, portal), + readCustomDomain(api), + options.organizationId ? readOrganization(api, options.organizationId) : undefined, + ]); + const { application, grants, applicationConnections } = applicationParts; return { tenantDomain, @@ -107,6 +100,28 @@ export async function fetchPortalSnapshot( }; } +// The application, and when it exists its grants and connections. +async function readApplicationParts( + api: ApiClient, + clientId: string | undefined, +): Promise> { + const application = clientId ? await readApplication(api, clientId) : undefined; + if (!application || !clientId) return { application, grants: [], applicationConnections: [] }; + + const [grants, applicationConnections] = await Promise.all([ + listAll(api, `client-grants?client_id=${encodeURIComponent(clientId)}`, "client_grants"), + // This endpoint pages by checkpoint and rejects page and per_page. + listAllByCheckpoint<{ id: string; name?: string }>(api, `clients/${encodeURIComponent(clientId)}/connections`, "connections"), + ]); + return { application, grants, applicationConnections }; +} + +async function readForms(api: ApiClient, portal: PortalShape): Promise> { + const formIds = formIdsOf(portal); + const exist = await Promise.all(formIds.map((formId) => formExists(api, formId))); + return Object.fromEntries(formIds.map((formId, index) => [formId, exist[index]])); +} + export function formIdsOf(portal: PortalShape): string[] { const ids: string[] = []; const visit = (components: ComponentShape[] = []) => { diff --git a/internal/cli/data/universal-portals/_source/src/cli-api.ts b/internal/cli/data/universal-portals/_source/src/cli-api.ts new file mode 100644 index 000000000..f7b9d48d6 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/src/cli-api.ts @@ -0,0 +1,204 @@ +// Management API client that calls the Auth0 CLI's `auth0 api` command. The CLI owns the +// login and the token, so this module never holds a credential. Request bodies go to the +// command on stdin, because the bodies carry client secrets and arguments are visible to +// every process on the machine. + +import { spawn } from "node:child_process"; + +import { ApiClient, HttpMethod, toApiError } from "./api.js"; +import { PluginError } from "./plugin-error.js"; + +export interface CommandResult { + exitCode: number; + stdout: string; + stderr: string; +} + +export type RunCommand = (bin: string, args: string[], input: string | undefined) => Promise; + +export interface CliApiOptions { + // Path of the auth0 executable. + bin: string; + // Domain of the tenant to call. The CLI's active tenant is used when it is not set. + tenant?: string; + // Starts the process. Replaced in tests. + run?: RunCommand; + // Requests allowed at once after the first one has finished. + concurrency?: number; +} + +const DEFAULT_CONCURRENCY = 4; +const INSTALL_URL = "https://github.com/auth0/auth0-cli#installation"; + +// What the CLI prints when it cannot use the session. Each needs the user to log in again. +const SIGN_IN_PROBLEM = + /auth token expired|login session has expired|token is invalid|corrupted authentication token|failed to find tenant|config\.json file is missing/i; + +export function createCliApiClient(options: CliApiOptions): ApiClient { + const { bin, tenant, run = runCommand } = options; + const gate = createGate(options.concurrency ?? DEFAULT_CONCURRENCY); + + return { + async request(method: HttpMethod, path: string, body?: unknown): Promise { + await gate.enter(); + try { + const args = ["api", method, path.replace(/^\//, ""), "--no-color", "--no-input"]; + if (tenant) args.push("--tenant", tenant); + if (method === "DELETE") args.push("--force"); + + const what = `auth0 api ${method} ${path}`; + const result = await runOrExplain(run, bin, args, body === undefined ? undefined : JSON.stringify(body)); + if (result.exitCode !== 0) throw explainFailure(result.stderr, what, tenant); + return parseOutput(result.stdout, what); + } finally { + gate.leave(); + } + }, + }; +} + +// The first request runs alone. If the CLI has to refresh an expired token it saves the +// session file, and several processes doing that at once could damage it. Once one request +// has finished the token is fresh, so the rest can run side by side. +function createGate(concurrency: number): { enter(): Promise; leave(): void } { + let limit = 1; + let active = 0; + let firstFinished = false; + const waiting: Array<() => void> = []; + + const admit = (): void => { + while (active < limit && waiting.length > 0) { + active += 1; + waiting.shift()?.(); + } + }; + + return { + enter: () => + new Promise((resolve) => { + waiting.push(resolve); + admit(); + }), + leave: () => { + active -= 1; + if (!firstFinished) { + firstFinished = true; + limit = concurrency; + } + admit(); + }, + }; +} + +async function runOrExplain( + run: RunCommand, + bin: string, + args: string[], + input: string | undefined, +): Promise { + try { + return await run(bin, args, input); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + throw new PluginError( + `The Auth0 CLI was not found (tried "${bin}"). Install it from ${INSTALL_URL}, or set AUTH0_CLI_BIN to its path.`, + "auth0_cli_not_found", + ); + } + throw error; + } +} + +function parseOutput(stdout: string, what: string): T { + const text = stdout.trim(); + if (text === "") return undefined as T; + try { + return JSON.parse(text) as T; + } catch { + throw new Error(`${what} printed output that is not JSON: ${text.slice(0, 120)}`); + } +} + +// What went wrong, from the CLI's message. `what` names the command for the messages that +// have no better explanation. +function explainFailure(stderr: string, what: string, tenant: string | undefined): Error { + // An API error comes first: its body can quote any text, including a scope message. The body runs + // to the end of the output, because the CLI prints it over several lines when it is long. + const api = /(? entry.trim()) + .find((entry) => entry !== "" && !entry.startsWith("===")); + return (line ?? "no message").replace(/^[^A-Za-z0-9]+/, ""); +} + +// The tenant the CLI is logged in to by default. +export async function findActiveTenant(bin: string, run: RunCommand = runCommand): Promise { + const what = "auth0 tenants list"; + const result = await runOrExplain(run, bin, ["tenants", "list", "--json", "--no-color", "--no-input"], undefined); + if (result.exitCode !== 0) throw explainFailure(result.stderr, what, undefined); + + let tenants: unknown; + try { + tenants = parseOutput(result.stdout, what); + } catch { + tenants = undefined; + } + const active = Array.isArray(tenants) ? tenants.find((tenant) => tenant?.active === true) : undefined; + if (typeof active?.name !== "string") throw loginRequired("The Auth0 CLI has no active tenant."); + return active.name; +} + +export function runCommand(bin: string, args: string[], input: string | undefined): Promise { + return new Promise((resolve, reject) => { + const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); + child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk)); + child.on("error", reject); + child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr })); + // The process may exit before it reads stdin, which is reported through its exit code. + child.stdin.on("error", () => undefined); + child.stdin.end(input ?? ""); + }); +} diff --git a/internal/cli/data/universal-portals/_source/src/cli.ts b/internal/cli/data/universal-portals/_source/src/cli.ts index e999d3d6b..6251028be 100644 --- a/internal/cli/data/universal-portals/_source/src/cli.ts +++ b/internal/cli/data/universal-portals/_source/src/cli.ts @@ -5,6 +5,7 @@ import { pathToFileURL } from "node:url"; import { parseSetupArgs, ParsedSetupArgs, UsageError, usage } from "./args.js"; import type { FetchLike } from "./api.js"; import { clackPrompter } from "./clack-prompter.js"; +import type { RunCommand } from "./cli-api.js"; import type { Prompter } from "./prompt.js"; import { runAuditCommand } from "./audit-command.js"; import { describeError } from "./errors.js"; @@ -17,6 +18,8 @@ export interface IoStreams { stdinStream: NodeJS.ReadStream; isTTY: boolean; fetchImpl?: FetchLike; + // Starts the auth0 executable when requests go through the CLI. Replaced in tests. + run?: RunCommand; // Answers the questions. Defaults to the terminal when the run is interactive. prompter?: Prompter; // Opens the portal after setup. Defaults to the system browser; previews replace it. diff --git a/internal/cli/data/universal-portals/_source/src/context.ts b/internal/cli/data/universal-portals/_source/src/context.ts index b996597aa..be7620b04 100644 --- a/internal/cli/data/universal-portals/_source/src/context.ts +++ b/internal/cli/data/universal-portals/_source/src/context.ts @@ -1,16 +1,24 @@ -// The only module that reads the plugin runtime environment. The CLI sets -// these variables when it starts the plugin. +// The only module that reads the runtime environment and decides how the script reaches the +// Management API. There are three ways it can be started: +// - By the Auth0 CLI's plugin framework: a local proxy and its token are in the environment. +// - By the Auth0 CLI itself, which passes its own path and the tenant in AUTH0_CLI_BIN and +// AUTH0_CLI_TENANT_DOMAIN. Requests go through `auth0 api`. +// - By hand, with nothing set: `auth0` on the PATH, and the tenant the CLI is logged in to. +import { ApiClient, createApiClient, FetchLike } from "./api.js"; import { UsageError } from "./args.js"; +import { createCliApiClient, findActiveTenant, RunCommand, runCommand } from "./cli-api.js"; + +export type Transport = { kind: "proxy"; url: string; token: string } | { kind: "cli"; bin: string }; export interface PluginContext { - proxyUrl: string; - proxyToken: string; tenantDomain: string; + transport: Transport; } -// The --tenant flag must name the tenant the CLI is running against. The optional -// consequence says what the user can rely on, such as that nothing was written. +// The --tenant flag must name the tenant a proxy is bound to. The optional consequence says what +// the user can rely on, such as that nothing was written. When the script calls the CLI, the +// flag selects the tenant instead, so it always matches. export function checkTenantFlag(requested: string | undefined, context: PluginContext, consequence = ""): void { if (requested && requested !== context.tenantDomain) { throw new UsageError( @@ -19,24 +27,41 @@ export function checkTenantFlag(requested: string | undefined, context: PluginCo } } -export function readContext(env: NodeJS.ProcessEnv): PluginContext { +// The tenant is taken from the --tenant flag first, then AUTH0_CLI_TENANT_DOMAIN, then the CLI's +// active tenant. +export async function readContext( + env: NodeJS.ProcessEnv, + requestedTenant?: string, + run: RunCommand = runCommand, +): Promise { const proxyUrl = env.AUTH0_CLI_PROXY_URL; const proxyToken = env.AUTH0_CLI_PROXY_TOKEN; - const tenantDomain = env.AUTH0_CLI_TENANT_DOMAIN; - - const missing = [ - ["AUTH0_CLI_PROXY_URL", proxyUrl], - ["AUTH0_CLI_PROXY_TOKEN", proxyToken], - ["AUTH0_CLI_TENANT_DOMAIN", tenantDomain], - ] - .filter(([, value]) => !value) - .map(([name]) => name); - - if (missing.length > 0 || !proxyUrl || !proxyToken || !tenantDomain) { - throw new Error( - `Run this plugin through the Auth0 CLI (auth0 universal-portals). Missing: ${missing.join(", ")}`, - ); + + if (proxyUrl || proxyToken) { + const tenantDomain = env.AUTH0_CLI_TENANT_DOMAIN; + const missing = [ + ["AUTH0_CLI_PROXY_URL", proxyUrl], + ["AUTH0_CLI_PROXY_TOKEN", proxyToken], + ["AUTH0_CLI_TENANT_DOMAIN", tenantDomain], + ] + .filter(([, value]) => !value) + .map(([name]) => name); + if (missing.length > 0 || !proxyUrl || !proxyToken || !tenantDomain) { + throw new Error( + `The Auth0 CLI started this command with an incomplete environment. Missing: ${missing.join(", ")}`, + ); + } + return { tenantDomain, transport: { kind: "proxy", url: proxyUrl, token: proxyToken } }; } - return { proxyUrl, proxyToken, tenantDomain }; + const bin = env.AUTH0_CLI_BIN || "auth0"; + const tenantDomain = requestedTenant || env.AUTH0_CLI_TENANT_DOMAIN || (await findActiveTenant(bin, run)); + return { tenantDomain, transport: { kind: "cli", bin } }; +} + +// Builds the API client for the context, in one place for every command. +export function apiFor(context: PluginContext, options: { fetchImpl?: FetchLike; run?: RunCommand } = {}): ApiClient { + const { transport } = context; + if (transport.kind === "proxy") return createApiClient(transport.url, transport.token, options.fetchImpl); + return createCliApiClient({ bin: transport.bin, tenant: context.tenantDomain, run: options.run }); } diff --git a/internal/cli/data/universal-portals/_source/src/errors.ts b/internal/cli/data/universal-portals/_source/src/errors.ts index 6cf419951..4941dccb5 100644 --- a/internal/cli/data/universal-portals/_source/src/errors.ts +++ b/internal/cli/data/universal-portals/_source/src/errors.ts @@ -2,6 +2,8 @@ import { ApiError } from "./api.js"; import { UsageError } from "./args.js"; import type { IoStreams } from "./cli.js"; import { PluginError } from "./plugin-error.js"; +import { scopeFixes } from "./scope-fixes.js"; +import { parseScopeGap } from "./setup/preflight.js"; import type { Ui } from "./ui.js"; export function reportError(error: unknown, json: boolean, io: IoStreams, ui: Ui): number { @@ -20,7 +22,12 @@ export function describeError(error: unknown): { code: string; message: string; return { code: error.code, message: error.message, details: error.fixes ? { fixes: error.fixes } : {} }; } if (error instanceof ApiError) { - return { code: error.errorCode ?? "api_error", message: error.message, details: { status: error.status } }; + // A missing scope is fixed the same way whichever command hit it. When any one of several + // scopes is enough, the first is suggested. + const gap = parseScopeGap("", error); + const scopes = gap && gap.requirement === "any" ? gap.scopes.slice(0, 1) : (gap?.scopes ?? []); + const fixes = scopes.length > 0 ? { fixes: scopeFixes(scopes) } : {}; + return { code: error.errorCode ?? "api_error", message: error.message, details: { status: error.status, ...fixes } }; } if (error instanceof Error) return { code: "setup_failed", message: error.message, details: {} }; return { code: "setup_failed", message: String(error), details: {} }; diff --git a/internal/cli/data/universal-portals/_source/src/scope-fixes.ts b/internal/cli/data/universal-portals/_source/src/scope-fixes.ts new file mode 100644 index 000000000..2323f023c --- /dev/null +++ b/internal/cli/data/universal-portals/_source/src/scope-fixes.ts @@ -0,0 +1,9 @@ +// What to do when the session lacks a scope. The advice depends on how the Auth0 CLI is signed in: +// a user login can ask for more scopes, while a client-credentials login (machine to machine) has +// only the scopes granted to its application, so they must be added there. +export function scopeFixes(scopes: string[]): string[] { + return [ + `Signed in as a user: auth0 login --scopes ${scopes.join(",")}`, + `Signed in with client credentials: add ${scopes.join(", ")} to the application's Management API grant`, + ]; +} diff --git a/internal/cli/data/universal-portals/_source/src/setup-command.ts b/internal/cli/data/universal-portals/_source/src/setup-command.ts index 74c8afc6a..901767a0b 100644 --- a/internal/cli/data/universal-portals/_source/src/setup-command.ts +++ b/internal/cli/data/universal-portals/_source/src/setup-command.ts @@ -1,6 +1,5 @@ -import { createApiClient } from "./api.js"; import { missingFlags, ParsedSetupArgs, UsageError } from "./args.js"; -import { checkTenantFlag, readContext } from "./context.js"; +import { apiFor, checkTenantFlag, readContext } from "./context.js"; import { openBrowser } from "./browser.js"; import { CancelledError, type Prompter } from "./prompt.js"; import type { TemplateId } from "./templates/types.js"; @@ -20,7 +19,7 @@ export async function runSetupCommand( prompter: Prompter | undefined, ): Promise { try { - const context = readContext(env); + const context = await readContext(env, args.tenant, io.run); checkTenantFlag(args.tenant, context, " Nothing was created."); if (!interactive) { const missing = missingFlags(args); @@ -28,7 +27,7 @@ export async function runSetupCommand( throw new UsageError(`Missing required flags for a non-interactive run: ${missing.join(", ")}`); } } - const tracked = trackWrites(createApiClient(context.proxyUrl, context.proxyToken, io.fetchImpl)); + const tracked = trackWrites(apiFor(context, { fetchImpl: io.fetchImpl, run: io.run })); ui.banner("Universal Portals setup", context.tenantDomain); try { diff --git a/internal/cli/data/universal-portals/_source/src/setup/preflight.ts b/internal/cli/data/universal-portals/_source/src/setup/preflight.ts index 13d9a558d..f456d702a 100644 --- a/internal/cli/data/universal-portals/_source/src/setup/preflight.ts +++ b/internal/cli/data/universal-portals/_source/src/setup/preflight.ts @@ -65,19 +65,30 @@ export async function runPreflight( const gaps: ScopeGap[] = []; const unexpected: PreflightResult["unexpected"] = []; - for (const probe of probes) { - try { - await api.request(probe.method, probe.path, probe.body); - } catch (error) { - if (!(error instanceof ApiError)) throw error; - if (error.status === 400) continue; - if (error.errorCode === "feature_not_enabled") throw new FeatureNotEnabledError(tenantDomain); - const gap = parseScopeGap(probe.name, error); - if (gap) { - gaps.push(gap); - } else { - unexpected.push({ probe: probe.name, status: error.status, message: error.message }); + // The probes do not depend on each other, so they are sent together. The answers are read in + // the order of the probes, so the report and the first error do not depend on which came first. + const failures = await Promise.all( + probes.map(async (probe) => { + try { + await api.request(probe.method, probe.path, probe.body); + return undefined; + } catch (error) { + return { probe, error }; } + }), + ); + + for (const failure of failures) { + if (!failure) continue; + const { probe, error } = failure; + if (!(error instanceof ApiError)) throw error; + if (error.status === 400) continue; + if (error.errorCode === "feature_not_enabled") throw new FeatureNotEnabledError(tenantDomain); + const gap = parseScopeGap(probe.name, error); + if (gap) { + gaps.push(gap); + } else { + unexpected.push({ probe: probe.name, status: error.status, message: error.message }); } } diff --git a/internal/cli/data/universal-portals/_source/src/setup/run.ts b/internal/cli/data/universal-portals/_source/src/setup/run.ts index e086b87a3..bf04f720a 100644 --- a/internal/cli/data/universal-portals/_source/src/setup/run.ts +++ b/internal/cli/data/universal-portals/_source/src/setup/run.ts @@ -1,5 +1,6 @@ import { ApiClient } from "../api.js"; import { PluginError } from "../plugin-error.js"; +import { scopeFixes } from "../scope-fixes.js"; import { applicationUrl, flowEditUrl, formEditUrl, portalEditUrl } from "../dashboard.js"; import { buildPortalForms, FORM_NAMES, FormKey } from "../forms.js"; import { isValidPortalSlug } from "../portal-model.js"; @@ -56,7 +57,7 @@ export class MissingScopesError extends PluginError { ? `The CLI session is missing ${scopes.length} scope(s): ${scopes.join(", ")}.` : "The CLI session is missing a scope it needs.", "missing_scopes", - scopes.length > 0 ? [`auth0 login --scopes ${scopes.join(",")}`] : undefined, + scopes.length > 0 ? scopeFixes(scopes) : undefined, ); } } @@ -229,9 +230,6 @@ async function checkPermissions(probes: Probe[], deps: SetupDeps): Promise deps.ui.error("The CLI session is missing permissions setup needs"); for (const scope of required) deps.ui.detail(scope); for (const choice of choices) deps.ui.detail(`one of: ${choice.scopes.join(", ")}`); - if (required.length > 0) { - deps.ui.detail(`Run: auth0 login --scopes ${required.join(",")}`); - } throw new MissingScopesError(required, choices); } diff --git a/internal/cli/data/universal-portals/_source/src/source-hash.ts b/internal/cli/data/universal-portals/_source/src/source-hash.ts new file mode 100644 index 000000000..ed8c22599 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/src/source-hash.ts @@ -0,0 +1,31 @@ +import { createHash } from "node:crypto"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +// Everything the single-file bundle is built from: the sources, the script that builds it, and the +// lockfile that fixes the dependencies. Paths use "/" and are sorted, so the list is the same on +// every machine. +export function sourceFiles(root: string): string[] { + const files = ["package-lock.json", "scripts/bundle.mjs"]; + const walk = (directory: string): void => { + for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) { + const path = `${directory}/${entry.name}`; + if (entry.isDirectory()) walk(path); + else files.push(path); + } + }; + walk("src"); + return files.sort(); +} + +// A hash of those files, written into the bundle's first lines. A test recomputes it, so a bundle +// that is older than its sources is caught. Line endings are normalized so a Windows checkout +// gives the same value. +export function sourceHash(root: string): string { + const hash = createHash("sha256"); + for (const file of sourceFiles(root)) { + const text = readFileSync(join(root, file), "utf8").replace(/\r\n/g, "\n"); + hash.update(`${file}\0${text}\0`); + } + return hash.digest("hex"); +} diff --git a/internal/cli/data/universal-portals/_source/test/api.test.ts b/internal/cli/data/universal-portals/_source/test/api.test.ts index 1deecd780..b9afdb14d 100644 --- a/internal/cli/data/universal-portals/_source/test/api.test.ts +++ b/internal/cli/data/universal-portals/_source/test/api.test.ts @@ -1,8 +1,7 @@ import { strict as assert } from "node:assert"; import { test } from "node:test"; -import { ApiClient, ApiError, createApiClient, listAll, listItems, type FetchLike } from "../src/api.js"; -import { readContext } from "../src/context.js"; +import { ApiClient, ApiError, createApiClient, listAll, listAllByCheckpoint, listItems, type FetchLike } from "../src/api.js"; function fakeFetch(status: number, body: unknown): { fetch: FetchLike; calls: { url: string; init: RequestInit }[] } { const calls: { url: string; init: RequestInit }[] = []; @@ -78,25 +77,36 @@ test("listItems reads both array responses and keyed responses", () => { assert.deepEqual(listItems({ other: [] }, "portals"), []); }); -test("readContext names every missing variable", () => { - assert.throws(() => readContext({}), (error: unknown) => { - assert.ok(error instanceof Error); - assert.match(error.message, /AUTH0_CLI_PROXY_URL/); - assert.match(error.message, /AUTH0_CLI_PROXY_TOKEN/); - assert.match(error.message, /AUTH0_CLI_TENANT_DOMAIN/); - return true; - }); +test("listAllByCheckpoint follows the next token until there is none, and never sends page numbers", async () => { + const everything = Array.from({ length: 250 }, (_, index) => ({ id: `item_${index}` })); + const paths: string[] = []; + const api = { + async request(_method: string, path: string) { + paths.push(path); + const params = new URL(`https://x/${path}`).searchParams; + const start = params.has("from") ? everything.findIndex((item) => item.id === params.get("from")) : 0; + const take = Number(params.get("take")); + const next = everything[start + take]; + return { things: everything.slice(start, start + take), ...(next ? { next: next.id } : {}) }; + }, + } as ApiClient; + + const items = await listAllByCheckpoint<{ id: string }>(api, "things?strategy=auth0", "things"); + + assert.equal(items.length, 250); + assert.deepEqual(paths, [ + "things?strategy=auth0&take=100", + "things?strategy=auth0&take=100&from=item_100", + "things?strategy=auth0&take=100&from=item_200", + ]); }); -test("readContext returns the three values when all are set", () => { - const context = readContext({ - AUTH0_CLI_PROXY_URL: "https://127.0.0.1:1", - AUTH0_CLI_PROXY_TOKEN: "t", - AUTH0_CLI_TENANT_DOMAIN: "acme.us.auth0.com", - }); - assert.deepEqual(context, { - proxyUrl: "https://127.0.0.1:1", - proxyToken: "t", - tenantDomain: "acme.us.auth0.com", - }); +test("listAllByCheckpoint stops with an error when the token never runs out", async () => { + const api = { + async request() { + return { things: [{ id: "a" }], next: "again" }; + }, + } as ApiClient; + + await assert.rejects(listAllByCheckpoint(api, "things", "things"), /did not stop paging/); }); diff --git a/internal/cli/data/universal-portals/_source/test/bundle.test.ts b/internal/cli/data/universal-portals/_source/test/bundle.test.ts new file mode 100644 index 000000000..e9594c3e0 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/bundle.test.ts @@ -0,0 +1,21 @@ +import { strict as assert } from "node:assert"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +import { sourceHash } from "../src/source-hash.js"; + +// The Auth0 CLI embeds this file, so a bundle that is older than its sources would ship old code. +test("the committed bundle was built from the current sources", () => { + const root = fileURLToPath(new URL("../../", import.meta.url)); + const bundle = readFileSync(new URL("../../../universal-portals.mjs", import.meta.url), "utf8"); + + const recorded = /^\/\/ source-sha256: ([0-9a-f]{64})$/m.exec(bundle.split("\n").slice(0, 5).join("\n"))?.[1]; + + assert.ok(recorded, "the bundle does not start with its source hash"); + assert.equal( + recorded, + sourceHash(root), + 'the sources changed after the bundle was built: run "npm run bundle" in the _source folder and commit the result', + ); +}); diff --git a/internal/cli/data/universal-portals/_source/test/cli-api.test.ts b/internal/cli/data/universal-portals/_source/test/cli-api.test.ts new file mode 100644 index 000000000..b2d06eb1f --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/cli-api.test.ts @@ -0,0 +1,248 @@ +import { strict as assert } from "node:assert"; +import { chmodSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; + +import { ApiError } from "../src/api.js"; +import { CommandResult, createCliApiClient, RunCommand } from "../src/cli-api.js"; +import { PluginError } from "../src/plugin-error.js"; + +// What `auth0 api` prints on stderr when a request fails. The 400, 404 and unknown-tenant +// samples were recorded from the CLI (v1.32.0) with the tenant name replaced. The 403 and 409 +// samples are built from the CLI's own message formats, because the recording tenant's token +// had no missing scope and a conflict needs a write. +const STDERR = { + notFound: + "\n=== acme.us.auth0.com error\n\n ▸ 404: API request failed: " + + '{"statusCode":404,"error":"Not Found","message":"The client does not exist","errorCode":"inexistent_client"}.\n\n', + badQuery: + "\n=== acme.us.auth0.com error\n\n ▸ 400: API request failed: " + + `{"statusCode":400,"error":"Bad Request","message":"Query validation error: 'Value 9999 is greater than maximum 100' on property per_page (Number of results per page. Default value is 50, maximum value is 100).","errorCode":"invalid_query_string"}.\n\n`, + conflict: + "\n=== acme.us.auth0.com error\n\n ▸ 409: API request failed: " + + '{"statusCode":409,"error":"Conflict","message":"Slug already in use","errorCode":"portal_conflict"}.\n\n', + missingScope: + "\n=== acme.us.auth0.com error\n\n ▸ Request failed because access token lacks scope: read:portals.\n " + + "If authenticated via client credentials, add this scope to the designated client. " + + "If authenticated as a user, request this scope during login by running `auth0 login --scopes read:portals`.\n\n", + unknownTenant: + "\n=== nope.us.auth0.com error\n\n ▸ Failed to find tenant: nope.us.auth0.com. " + + "Run 'auth0 tenants list' to see your configured tenants or run 'auth0 login' to configure a new tenant.\n\n", + expiredSession: + "\n=== acme.us.auth0.com error\n\n ▸ Auth token expired and --no-input is set; run 'auth0 login' to re-authenticate.\n\n", + somethingElse: "\n=== acme.us.auth0.com error\n\n ▸ Failed to send request: dial tcp: lookup acme.us.auth0.com: no such host.\n\n", +}; + +interface Call { + bin: string; + args: string[]; + input: string | undefined; +} + +function runner(result: Partial): { run: RunCommand; calls: Call[] } { + const calls: Call[] = []; + const run: RunCommand = async (bin, args, input) => { + calls.push({ bin, args, input }); + return { exitCode: 0, stdout: "", stderr: "", ...result }; + }; + return { run, calls }; +} + +async function failure(stderr: string): Promise { + const { run } = runner({ exitCode: 1, stderr }); + const api = createCliApiClient({ bin: "auth0", tenant: "acme.us.auth0.com", run }); + try { + await api.request("GET", "clients/x"); + } catch (error) { + return error; + } + throw new Error("the request was expected to fail"); +} + +test("a successful request returns the parsed JSON from stdout", async () => { + const { run, calls } = runner({ stdout: '{\n "client_id": "abc"\n}\n' }); + const api = createCliApiClient({ bin: "auth0", tenant: "acme.us.auth0.com", run }); + + const result = await api.request("GET", "clients/abc?fields=client_id"); + + assert.deepEqual(result, { client_id: "abc" }); + assert.deepEqual(calls[0].args, [ + "api", "GET", "clients/abc?fields=client_id", "--no-color", "--no-input", "--tenant", "acme.us.auth0.com", + ]); +}); + +test("an empty response is undefined, like a 204 from the API", async () => { + const { run } = runner({ stdout: "" }); + const api = createCliApiClient({ bin: "auth0", run }); + + assert.equal(await api.request("PATCH", "clients/abc", { name: "x" }), undefined); +}); + +test("the request body travels on stdin and never in the arguments", async () => { + const secret = "s3cr3t-value-that-must-not-leak"; + const { run, calls } = runner({ stdout: "{}" }); + const api = createCliApiClient({ bin: "auth0", run }); + + await api.request("POST", "flows/vault/connections", { setup: { client_secret: secret } }); + + assert.equal(calls[0].input, JSON.stringify({ setup: { client_secret: secret } })); + assert.ok(!calls[0].args.join(" ").includes(secret), "the secret must not appear in the process arguments"); +}); + +test("a request without a body sends nothing on stdin, a leading slash is dropped, and DELETE is forced", async () => { + const { run, calls } = runner({}); + const api = createCliApiClient({ bin: "auth0", run }); + + await api.request("DELETE", "/client-grants/cgr_1"); + + assert.equal(calls[0].input, undefined); + assert.deepEqual(calls[0].args, ["api", "DELETE", "client-grants/cgr_1", "--no-color", "--no-input", "--force"]); +}); + +test("a 404 becomes an ApiError with the status, code and message from the API", async () => { + const error = await failure(STDERR.notFound); + + assert.ok(error instanceof ApiError); + assert.equal(error.status, 404); + assert.equal(error.errorCode, "inexistent_client"); + assert.equal(error.message, "The client does not exist"); +}); + +test("a 400 and a 409 keep their status and error code", async () => { + const bad = await failure(STDERR.badQuery); + const conflict = await failure(STDERR.conflict); + + assert.ok(bad instanceof ApiError && conflict instanceof ApiError); + assert.deepEqual([bad.status, bad.errorCode], [400, "invalid_query_string"]); + assert.deepEqual([conflict.status, conflict.errorCode], [409, "portal_conflict"]); +}); + +test("an API error whose message quotes a missing scope stays an API error", async () => { + const error = await failure( + "\n=== acme.us.auth0.com error\n\n ▸ 400: API request failed: " + + '{"statusCode":400,"error":"Bad Request","message":"Invalid description: access token lacks scope: read:portals","errorCode":"invalid_body"}.\n\n', + ); + + assert.ok(error instanceof ApiError); + assert.deepEqual([error.status, error.errorCode], [400, "invalid_body"]); +}); + +test("an API error body printed over several lines keeps its status, code and message", async () => { + const body = JSON.stringify( + { statusCode: 404, error: "Not Found", message: "The portal does not exist", errorCode: "inexistent_portal" }, + null, + 2, + ); + const error = await failure(`\n=== acme.us.auth0.com error\n\n ▸ 404: API request failed: ${body}.\n\n`); + + assert.ok(error instanceof ApiError); + assert.deepEqual([error.status, error.errorCode, error.message], [404, "inexistent_portal", "The portal does not exist"]); +}); + +test("a number that only ends in three digits is not read as a status", async () => { + const error = await failure("\n=== acme.us.auth0.com error\n\n ▸ 1404: API request failed: {}.\n\n"); + + assert.ok(!(error instanceof ApiError), "1404 is not an HTTP status"); +}); + +test("a missing scope becomes the 403 the preflight already understands", async () => { + const error = await failure(STDERR.missingScope); + + assert.ok(error instanceof ApiError); + assert.equal(error.status, 403); + assert.equal(error.errorCode, "insufficient_scope"); + assert.equal(error.message, "Insufficient scope, expected any of: read:portals"); +}); + +test("an expired session or an unknown tenant asks the user to log in", async () => { + const cases: [string, RegExp][] = [ + [STDERR.expiredSession, /Auth token expired/], + [STDERR.unknownTenant, /Failed to find tenant: nope\.us\.auth0\.com/], + ]; + for (const [stderr, reason] of cases) { + const error = await failure(stderr); + assert.ok(error instanceof PluginError); + assert.equal(error.code, "auth0_login_required"); + assert.deepEqual(error.fixes, ["auth0 login"]); + assert.match(error.message, reason, "the CLI's own reason is kept"); + } +}); + +test("any other failure is reported with the CLI's own message", async () => { + const error = await failure(STDERR.somethingElse); + + assert.ok(error instanceof Error && !(error instanceof ApiError)); + assert.match(error.message, /auth0 api GET clients\/x failed: Failed to send request: dial tcp/); +}); + +test("a missing auth0 binary tells the user how to install it or point to it", async () => { + const api = createCliApiClient({ bin: join(tmpdir(), "no-such-auth0-binary") }); + + await assert.rejects(api.request("GET", "clients"), (error: unknown) => { + assert.ok(error instanceof PluginError); + assert.equal(error.code, "auth0_cli_not_found"); + assert.match(error.message, /AUTH0_CLI_BIN/); + return true; + }); +}); + +test("the first request runs alone, then up to four run together", async () => { + let active = 0; + let peak = 0; + const peaks: number[] = []; + const run: RunCommand = async () => { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 15)); + peaks.push(peak); + active -= 1; + return { exitCode: 0, stdout: "{}", stderr: "" }; + }; + const api = createCliApiClient({ bin: "auth0", run, concurrency: 4 }); + + await Promise.all(Array.from({ length: 9 }, (_, index) => api.request("GET", `clients/${index}`))); + + assert.equal(peaks[0], 1, "the first request must finish before any other starts"); + assert.equal(peak, 4); +}); + +test("the real runner passes the body on stdin to the process and reads its stdout", async () => { + const dir = mkdtempSync(join(tmpdir(), "up-fake-auth0-")); + const script = join(dir, "auth0"); + // A stand-in for the CLI: echoes its arguments and the stdin it received. + writeFileSync( + script, + '#!/usr/bin/env node\nlet input = "";\nprocess.stdin.on("data", (c) => (input += c));\n' + + 'process.stdin.on("end", () => console.log(JSON.stringify({ args: process.argv.slice(2), input })));\n', + ); + chmodSync(script, 0o755); + const api = createCliApiClient({ bin: script, tenant: "acme.us.auth0.com" }); + + const echoed = await api.request<{ args: string[]; input: string }>("POST", "clients", { name: "x" }); + + assert.deepEqual(JSON.parse(echoed.input), { name: "x" }); + assert.deepEqual(echoed.args.slice(0, 3), ["api", "POST", "clients"]); +}); + +// `auth0 api` reads stdin until it ends for every method except GET, even when there is no body. A +// request without a body must still close it, or the command waits forever. +test("a request without a body closes stdin, so a command that reads it to the end does not hang", async () => { + const dir = mkdtempSync(join(tmpdir(), "up-fake-auth0-")); + const script = join(dir, "auth0"); + writeFileSync( + script, + '#!/usr/bin/env node\nprocess.stdin.resume();\nprocess.stdin.on("end", () => console.log("{}"));\n' + + // If stdin is never closed, stop on our own, so a failing test does not keep the run alive. + "setTimeout(() => process.exit(2), 8000).unref();\n", + ); + chmodSync(script, 0o755); + const api = createCliApiClient({ bin: script }); + + const outcome = await Promise.race([ + api.request("DELETE", "client-grants/cgr_1").then(() => "finished"), + new Promise((resolve) => setTimeout(() => resolve("hung"), 5000)), + ]); + + assert.equal(outcome, "finished"); +}); diff --git a/internal/cli/data/universal-portals/_source/test/cli.test.ts b/internal/cli/data/universal-portals/_source/test/cli.test.ts index b617af160..d5a58f415 100644 --- a/internal/cli/data/universal-portals/_source/test/cli.test.ts +++ b/internal/cli/data/universal-portals/_source/test/cli.test.ts @@ -10,6 +10,7 @@ import { fileURLToPath } from "node:url"; import { ApiError, FetchLike } from "../src/api.js"; import { main, IoStreams } from "../src/cli.js"; import { missingFlags, parseSetupArgs, UsageError } from "../src/args.js"; +import { scopeFixes } from "../src/scope-fixes.js"; import { toPortalSlug } from "../src/slug.js"; import type { Prompter } from "../src/prompt.js"; import { captureOutput } from "./capture-output.js"; @@ -50,6 +51,11 @@ function captureIo(fetchImpl: FetchLike, isTTY = false): { io: IoStreams; stdout stdinStream: process.stdin, isTTY, fetchImpl, + // A test must never start the real auth0 CLI: it would call a real tenant. Tests that + // need the CLI pass their own runner. + run: async () => { + throw new Error("a test tried to start the real auth0 CLI"); + }, }; return { io, stdout: () => out, stderr: () => captureErr.text() }; } @@ -230,6 +236,18 @@ async function auditInTerminal(tenant: FakeTenant, portalId: string, prompter: P return { code, stderr: stderr() }; } +test("audit tells the user how to get a missing scope, for a user login and a client-credentials login", async () => { + const { tenant, portalId } = await tenantWithPortal(); + tenant.deny(`GET portals/${portalId}`, "Insufficient scope, expected any of: read:portals"); + const { io, stderr } = captureIo(fetchFor(tenant)); + + const code = await main(["audit", portalId], ENV, io); + + assert.equal(code, 1); + assert.match(stderr(), /Signed in as a user: auth0 login --scopes read:portals/); + assert.match(stderr(), /Signed in with client credentials: add read:portals to the application's Management API grant/); +}); + test("the fix menu's 'fix all' applies every error and the portal passes the audit", async () => { const { tenant, portalId } = await tenantWithPortal(); tenant.portals.get(portalId)!.navigation.sidebar.components = [LEGACY_LINK]; @@ -322,12 +340,28 @@ test("a non-interactive run without the required flags stops before any API call assert.equal(tenant.calls.length, 0); }); -test("a run outside the CLI names the variables it is missing", async () => { - const { io, stderr } = captureIo(fetchFor(new FakeTenant())); +test("a run without the Auth0 CLI installed says how to install it, and calls nothing", async () => { + const tenant = new FakeTenant(); + const { io, stderr } = captureIo(fetchFor(tenant)); + io.run = async () => { + throw Object.assign(new Error("spawn auth0 ENOENT"), { code: "ENOENT" }); + }; const code = await main(["setup", "--no-input", "--template", "consumer", "--name", "x"], {}, io); assert.equal(code, 1); - assert.match(stderr(), /AUTH0_CLI_PROXY_URL, AUTH0_CLI_PROXY_TOKEN, AUTH0_CLI_TENANT_DOMAIN/); + assert.match(stderr(), /The Auth0 CLI was not found/); + assert.match(stderr(), /AUTH0_CLI_BIN/); + assert.equal(tenant.calls.length, 0); +}); + +test("a half-set proxy environment stops the run before any API call", async () => { + const tenant = new FakeTenant(); + const { io, stderr } = captureIo(fetchFor(tenant)); + const code = await main(["setup", "--no-input", "--template", "consumer", "--name", "x"], { AUTH0_CLI_PROXY_TOKEN: "t" }, io); + + assert.equal(code, 1); + assert.match(stderr(), /Missing: AUTH0_CLI_PROXY_URL, AUTH0_CLI_TENANT_DOMAIN/); + assert.equal(tenant.calls.length, 0); }); test("a non-interactive consumer run prints the result as JSON and never the full secret", async () => { @@ -361,7 +395,7 @@ test("in agent mode a missing scope is reported as a JSON error on stderr", asyn assert.equal(stdout(), ""); const envelope = JSON.parse(stderr()); assert.equal(envelope.error, "missing_scopes"); - assert.deepEqual(envelope.fixes, ["auth0 login --scopes create:portals"]); + assert.deepEqual(envelope.fixes, scopeFixes(["create:portals"])); }); test("a Business run that is missing its user stops with a usage error", async () => { diff --git a/internal/cli/data/universal-portals/_source/test/context.test.ts b/internal/cli/data/universal-portals/_source/test/context.test.ts new file mode 100644 index 000000000..527c39062 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/context.test.ts @@ -0,0 +1,125 @@ +import { strict as assert } from "node:assert"; +import { test } from "node:test"; + +import { RunCommand } from "../src/cli-api.js"; +import { apiFor, readContext } from "../src/context.js"; +import { PluginError } from "../src/plugin-error.js"; + +const PROXY_ENV = { + AUTH0_CLI_PROXY_URL: "https://127.0.0.1:1", + AUTH0_CLI_PROXY_TOKEN: "proxy-token", + AUTH0_CLI_TENANT_DOMAIN: "acme.us.auth0.com", +}; + +// `auth0 tenants list --json`, recorded from the CLI with the names replaced. +const TENANTS = JSON.stringify([ + { active: false, name: "other.eu.auth0.com" }, + { active: true, name: "acme.us.auth0.com" }, +]); + +function runner(result: { exitCode?: number; stdout?: string; stderr?: string } = {}) { + const calls: { bin: string; args: string[] }[] = []; + const run: RunCommand = async (bin, args) => { + calls.push({ bin, args }); + return { exitCode: 0, stdout: "", stderr: "", ...result }; + }; + return { run, calls }; +} + +test("started by the framework, the proxy variables are used as they are", async () => { + const { run, calls } = runner(); + + const context = await readContext(PROXY_ENV, undefined, run); + + assert.deepEqual(context, { + tenantDomain: "acme.us.auth0.com", + transport: { kind: "proxy", url: "https://127.0.0.1:1", token: "proxy-token" }, + }); + assert.equal(calls.length, 0, "nothing needs to be asked of the CLI"); +}); + +test("a half-set proxy environment names every variable that is missing", async () => { + await assert.rejects(readContext({ AUTH0_CLI_PROXY_TOKEN: "t" }), (error: unknown) => { + assert.ok(error instanceof Error); + assert.match(error.message, /AUTH0_CLI_PROXY_URL/); + assert.match(error.message, /AUTH0_CLI_TENANT_DOMAIN/); + assert.doesNotMatch(error.message, /AUTH0_CLI_PROXY_TOKEN/, "the variable that is set is not listed as missing"); + assert.match(error.message, /Auth0 CLI/); + return true; + }); +}); + +test("started by the CLI with its path and tenant, nothing is discovered", async () => { + const { run, calls } = runner(); + + const context = await readContext( + { AUTH0_CLI_BIN: "/opt/bin/auth0", AUTH0_CLI_TENANT_DOMAIN: "acme.us.auth0.com" }, + undefined, + run, + ); + + assert.deepEqual(context, { tenantDomain: "acme.us.auth0.com", transport: { kind: "cli", bin: "/opt/bin/auth0" } }); + assert.equal(calls.length, 0); +}); + +test("the --tenant flag wins over the tenant in the environment", async () => { + const context = await readContext( + { AUTH0_CLI_BIN: "auth0", AUTH0_CLI_TENANT_DOMAIN: "acme.us.auth0.com" }, + "other.eu.auth0.com", + runner().run, + ); + + assert.equal(context.tenantDomain, "other.eu.auth0.com"); +}); + +test("run by hand, the CLI's active tenant is used", async () => { + const { run, calls } = runner({ stdout: TENANTS }); + + const context = await readContext({}, undefined, run); + + assert.equal(context.tenantDomain, "acme.us.auth0.com"); + assert.deepEqual(context.transport, { kind: "cli", bin: "auth0" }); + assert.deepEqual(calls, [{ bin: "auth0", args: ["tenants", "list", "--json", "--no-color", "--no-input"] }]); +}); + +test("run by hand with --tenant, the CLI is not asked which tenant is active", async () => { + const { run, calls } = runner({ stdout: TENANTS }); + + const context = await readContext({}, "other.eu.auth0.com", run); + + assert.equal(context.tenantDomain, "other.eu.auth0.com"); + assert.equal(calls.length, 0); +}); + +test("with no active tenant, or no login at all, the user is asked to log in", async () => { + const cases = [ + runner({ stdout: JSON.stringify([{ active: false, name: "acme.us.auth0.com" }]) }), + runner({ stdout: "[]" }), + runner({ exitCode: 1, stderr: "\n=== error\n\n ▸ config.json file is missing.\n\n" }), + ]; + + for (const { run } of cases) { + await assert.rejects(readContext({}, undefined, run), (error: unknown) => { + assert.ok(error instanceof PluginError); + assert.equal(error.code, "auth0_login_required"); + assert.deepEqual(error.fixes, ["auth0 login"]); + return true; + }); + } +}); + +test("the API client follows the transport: proxy with the token, CLI with the tenant", async () => { + const requests: string[] = []; + const fetchImpl = async (url: string, init: RequestInit) => { + requests.push(`${url} ${(init.headers as Record).authorization}`); + return new Response("{}", { status: 200 }); + }; + const proxy = apiFor(await readContext(PROXY_ENV), { fetchImpl }); + await proxy.request("GET", "clients"); + assert.deepEqual(requests, ["https://127.0.0.1:1/api/v2/clients Bearer proxy-token"]); + + const { run, calls } = runner({ stdout: "{}" }); + const cli = apiFor(await readContext({ AUTH0_CLI_BIN: "auth0", AUTH0_CLI_TENANT_DOMAIN: "acme.us.auth0.com" }), { run }); + await cli.request("GET", "clients"); + assert.deepEqual(calls[0].args.slice(-2), ["--tenant", "acme.us.auth0.com"]); +}); diff --git a/internal/cli/data/universal-portals/_source/test/errors.test.ts b/internal/cli/data/universal-portals/_source/test/errors.test.ts index 057d2c6ed..b81a58fed 100644 --- a/internal/cli/data/universal-portals/_source/test/errors.test.ts +++ b/internal/cli/data/universal-portals/_source/test/errors.test.ts @@ -6,6 +6,7 @@ import { UsageError } from "../src/args.js"; import { describeError } from "../src/errors.js"; import { BusinessAccessError } from "../src/setup/business-types.js"; import { FeatureNotEnabledError } from "../src/setup/preflight.js"; +import { scopeFixes } from "../src/scope-fixes.js"; import { MissingScopesError, SlugRaceError } from "../src/setup/run.js"; test("each error reports its own code, and the fixes it names", () => { @@ -16,7 +17,10 @@ test("each error reports its own code, and the fixes it names", () => { fixes: ["Rerun with --user."], }); assert.deepEqual(describeError(new MissingScopesError(["read:clients"], [])).details, { - fixes: ["auth0 login --scopes read:clients"], + fixes: [ + "Signed in as a user: auth0 login --scopes read:clients", + "Signed in with client credentials: add read:clients to the application's Management API grant", + ], }); }); @@ -25,6 +29,16 @@ test("an error with no fixes leaves fixes out of the JSON", () => { assert.equal("fixes" in details, false); }); +test("an API error about a missing scope carries the fixes, whichever command hit it", () => { + const anyOf = new ApiError(403, "insufficient_scope", "Insufficient scope, expected any of: read:portals,update:portals", {}); + const allOf = new ApiError(403, "insufficient_scope", "Insufficient scope, expected all of: read:portals,update:portals", {}); + const other = new ApiError(403, "forbidden", "Forbidden", {}); + + assert.deepEqual(describeError(anyOf).details.fixes, scopeFixes(["read:portals"]), "any one of them is enough"); + assert.deepEqual(describeError(allOf).details.fixes, scopeFixes(["read:portals", "update:portals"])); + assert.equal("fixes" in describeError(other).details, false); +}); + test("an API error keeps the code the API sent", () => { const error = new ApiError(403, "insufficient_scope", "Insufficient scope", {}); assert.deepEqual(describeError(error), { code: "insufficient_scope", message: "Insufficient scope", details: { status: 403 } }); diff --git a/internal/cli/data/universal-portals/_source/test/fake-tenant.ts b/internal/cli/data/universal-portals/_source/test/fake-tenant.ts index 850f3c4ce..5e81d5432 100644 --- a/internal/cli/data/universal-portals/_source/test/fake-tenant.ts +++ b/internal/cli/data/universal-portals/_source/test/fake-tenant.ts @@ -240,8 +240,21 @@ export class FakeTenant implements ApiClient { this.organizations.set(org.id, org); return org; } + // Like the real endpoint: pages are taken by checkpoint (take, from, and a next token), and + // page and per_page are rejected. if (method === "GET" && parts[0] === "clients" && parts[2] === "connections") { - return { connections: [...this.connections.values()] }; + const params = new URLSearchParams(query); + if (params.has("page") || params.has("per_page")) { + throw toApiError(400, { + message: "Query validation error: 'Additional properties not allowed: per_page,page'.", + errorCode: "invalid_query_string", + }); + } + const all = [...this.connections.values()]; + const take = Number(params.get("take") ?? 50); + const start = params.has("from") ? all.findIndex((connection) => connection.id === params.get("from")) : 0; + const next = all[start + take]; + return { connections: all.slice(start, start + take), ...(next ? { next: next.id } : {}) }; } if (method === "GET" && path === "connections") { return [...this.connections.values()]; diff --git a/internal/cli/data/universal-portals/_source/test/parallel.test.ts b/internal/cli/data/universal-portals/_source/test/parallel.test.ts new file mode 100644 index 000000000..32aa25251 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/parallel.test.ts @@ -0,0 +1,96 @@ +import { strict as assert } from "node:assert"; +import { test } from "node:test"; + +import { ApiClient, HttpMethod } from "../src/api.js"; +import { fetchPortalSnapshot } from "../src/audit/snapshot.js"; +import { runPreflight, SETUP_PROBES } from "../src/setup/preflight.js"; +import { runSetup } from "../src/setup/run.js"; +import { Ui } from "../src/ui.js"; +import { captureOutput } from "./capture-output.js"; +import { FakeTenant } from "./fake-tenant.js"; + +const DOMAIN = "acme.us.auth0.com"; + +// Answers like a real network: each request takes a while, and the delay can differ per request, so +// answers arrive out of order. It records the most requests that were in flight at once. +function slowNetwork(tenant: FakeTenant, delayFor: (method: HttpMethod, path: string) => number) { + let active = 0; + let peak = 0; + const api: ApiClient = { + async request(method: HttpMethod, path: string, body?: unknown): Promise { + active += 1; + peak = Math.max(peak, active); + try { + await new Promise((resolve) => setTimeout(resolve, delayFor(method, path))); + return await tenant.request(method, path, body); + } finally { + active -= 1; + } + }, + }; + return { api, peak: () => peak }; +} + +test("the permission probes run together, and the gaps keep the order of the probes", async () => { + const tenant = new FakeTenant(); + tenant.deny("POST clients", "Insufficient scope, expected any of: create:clients"); + tenant.deny("POST portals", "Insufficient scope, expected any of: create:portals"); + // The first gap answers last, the second answers first. + const network = slowNetwork(tenant, (_method, path) => (path === "clients" ? 40 : 2)); + + const result = await runPreflight(network.api, SETUP_PROBES, DOMAIN); + + assert.deepEqual(result.gaps.map((gap) => gap.scopes), [["create:clients"], ["create:portals"]]); + assert.ok(network.peak() > 1, "the probes were sent one after another"); +}); + +test("the audit reads the independent parts of a portal together and gets the same snapshot", async () => { + const tenant = new FakeTenant(); + tenant.connections.set("con_db", { id: "con_db", name: "Username-Password-Authentication", strategy: "auth0" }); + const quiet = new Ui(captureOutput().stream, false, true); + const setup = await runSetup( + { template: "consumer", name: "Acme Account", slug: "acme-account" }, + { + api: tenant, + ui: quiet, + prompt: { + ask: async (_q, d) => d ?? "", + confirm: async (_q, d) => d, + choose: async (_q, o) => o[0].value, + checklist: async () => [], + }, + interactive: false, + tenantDomain: DOMAIN, + }, + ); + const sequential = await fetchPortalSnapshot(tenant, setup.portal.id, DOMAIN); + + const network = slowNetwork(tenant, () => 10); + const together = await fetchPortalSnapshot(network.api, setup.portal.id, DOMAIN); + + assert.deepEqual(together, sequential); + assert.ok(network.peak() > 1, "the reads were sent one after another"); +}); + +test("the audit reads every connection of an application, not only the first page", async () => { + const tenant = new FakeTenant(); + tenant.connections.set("con_db", { id: "con_db", name: "Username-Password-Authentication", strategy: "auth0" }); + const quiet = new Ui(captureOutput().stream, false, true); + const setup = await runSetup( + { template: "consumer", name: "Acme Account", slug: "acme-account" }, + { + api: tenant, + ui: quiet, + prompt: { ask: async (_q, d) => d ?? "", confirm: async (_q, d) => d, choose: async (_q, o) => o[0].value, checklist: async () => [] }, + interactive: false, + tenantDomain: DOMAIN, + }, + ); + for (let index = 0; index < 129; index += 1) { + tenant.connections.set(`con_${index}`, { id: `con_${index}`, name: `Connection ${index}`, strategy: "auth0" }); + } + + const snapshot = await fetchPortalSnapshot(tenant, setup.portal.id, DOMAIN); + + assert.equal(snapshot.applicationConnections.length, 130); +}); diff --git a/internal/cli/data/universal-portals/_source/test/scope-fixes.test.ts b/internal/cli/data/universal-portals/_source/test/scope-fixes.test.ts new file mode 100644 index 000000000..56f240e3a --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/scope-fixes.test.ts @@ -0,0 +1,11 @@ +import { strict as assert } from "node:assert"; +import { test } from "node:test"; + +import { scopeFixes } from "../src/scope-fixes.js"; + +test("a missing scope has one fix for a user login and one for a client-credentials login", () => { + assert.deepEqual(scopeFixes(["read:portals", "create:portals"]), [ + "Signed in as a user: auth0 login --scopes read:portals,create:portals", + "Signed in with client credentials: add read:portals, create:portals to the application's Management API grant", + ]); +}); diff --git a/internal/cli/data/universal-portals/_source/test/scopes.test.ts b/internal/cli/data/universal-portals/_source/test/scopes.test.ts index c22572546..687204995 100644 --- a/internal/cli/data/universal-portals/_source/test/scopes.test.ts +++ b/internal/cli/data/universal-portals/_source/test/scopes.test.ts @@ -15,7 +15,7 @@ import { captureOutput } from "./capture-output.js"; const DOMAIN = "acme.us.auth0.com"; const quiet = () => new Ui(captureOutput().stream, false, true); -// Mirrors the CLI proxy's rule (internal/plugins/proxy.go, requiredScopeFor): the verb +// Mirrors the rule the CLI proxy of the plugin framework uses (requiredScopeFor): the verb // comes from the HTTP method and the resource is the first path segment. const VERBS: Record = { GET: "read", POST: "create", PATCH: "update", PUT: "update", DELETE: "delete" }; diff --git a/internal/cli/data/universal-portals/_source/test/source-hash.test.ts b/internal/cli/data/universal-portals/_source/test/source-hash.test.ts new file mode 100644 index 000000000..eb1162314 --- /dev/null +++ b/internal/cli/data/universal-portals/_source/test/source-hash.test.ts @@ -0,0 +1,53 @@ +import { strict as assert } from "node:assert"; +import { createHash } from "node:crypto"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; + +import { sourceHash } from "../src/source-hash.js"; + +function tree(files: Record): string { + const root = mkdtempSync(join(tmpdir(), "up-source-hash-")); + for (const [name, text] of Object.entries(files)) { + mkdirSync(dirname(join(root, name)), { recursive: true }); + writeFileSync(join(root, name), text); + } + return root; +} + +const FILES = { "package-lock.json": "{}", "scripts/bundle.mjs": "x", "src/a.ts": "a", "src/deep/b.ts": "b" }; + +// The Go test that guards the committed bundle computes the same value, so this pins the definition: +// sorted paths, each followed by a NUL, its text with LF line endings, and a NUL. +test("the hash covers the lockfile, the bundle script and every file under src, in sorted order", () => { + const expected = createHash("sha256") + .update("package-lock.json\0{}\0scripts/bundle.mjs\0x\0src/a.ts\0a\0src/deep/b.ts\0b\0") + .digest("hex"); + + assert.equal(sourceHash(tree(FILES)), expected); +}); + +// "src/a-b/x" sorts before "src/a/x" because "-" comes before "/", while a directory walk finds "a" first. +test("the files are sorted by path, not in the order the folders are walked", () => { + const files = { "package-lock.json": "{}", "scripts/bundle.mjs": "x", "src/a/x": "1", "src/a-b/x": "2" }; + const expected = createHash("sha256") + .update(["package-lock.json", "{}", "scripts/bundle.mjs", "x", "src/a-b/x", "2", "src/a/x", "1", ""].join("\0")) + .digest("hex"); + + assert.equal(sourceHash(tree(files)), expected); +}); + +test("a change under src changes the hash, a change outside it does not", () => { + const before = sourceHash(tree(FILES)); + + assert.notEqual(sourceHash(tree({ ...FILES, "src/a.ts": "changed" })), before); + assert.equal(sourceHash(tree({ ...FILES, "test/x.test.ts": "new", "dist/y.js": "z" })), before); +}); + +test("Windows line endings give the same hash as Unix ones", () => { + const unix = tree({ ...FILES, "src/a.ts": "line one\nline two\n" }); + const windows = tree({ ...FILES, "src/a.ts": "line one\r\nline two\r\n" }); + + assert.equal(sourceHash(windows), sourceHash(unix)); +}); diff --git a/internal/cli/data/universal-portals/universal-portals.mjs b/internal/cli/data/universal-portals/universal-portals.mjs new file mode 100755 index 000000000..ae35bd3e5 --- /dev/null +++ b/internal/cli/data/universal-portals/universal-portals.mjs @@ -0,0 +1,4378 @@ +#!/usr/bin/env node +// Generated by "npm run bundle" in the _source folder. Do not edit. +// source-sha256: 229a16a14dba7461826d1b828358f469e9f41e78b501a4dc5ffabb54f89513f9 +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + try { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + } catch (e) { + throw mod = 0, e; + } +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// node_modules/sisteransi/src/index.js +var require_src = __commonJS({ + "node_modules/sisteransi/src/index.js"(exports, module) { + "use strict"; + var ESC2 = "\x1B"; + var CSI2 = `${ESC2}[`; + var beep = "\x07"; + var cursor3 = { + to(x, y2) { + if (!y2) return `${CSI2}${x + 1}G`; + return `${CSI2}${y2 + 1};${x + 1}H`; + }, + move(x, y2) { + let ret = ""; + if (x < 0) ret += `${CSI2}${-x}D`; + else if (x > 0) ret += `${CSI2}${x}C`; + if (y2 < 0) ret += `${CSI2}${-y2}A`; + else if (y2 > 0) ret += `${CSI2}${y2}B`; + return ret; + }, + up: (count = 1) => `${CSI2}${count}A`, + down: (count = 1) => `${CSI2}${count}B`, + forward: (count = 1) => `${CSI2}${count}C`, + backward: (count = 1) => `${CSI2}${count}D`, + nextLine: (count = 1) => `${CSI2}E`.repeat(count), + prevLine: (count = 1) => `${CSI2}F`.repeat(count), + left: `${CSI2}G`, + hide: `${CSI2}?25l`, + show: `${CSI2}?25h`, + save: `${ESC2}7`, + restore: `${ESC2}8` + }; + var scroll = { + up: (count = 1) => `${CSI2}S`.repeat(count), + down: (count = 1) => `${CSI2}T`.repeat(count) + }; + var erase3 = { + screen: `${CSI2}2J`, + up: (count = 1) => `${CSI2}1J`.repeat(count), + down: (count = 1) => `${CSI2}J`.repeat(count), + line: `${CSI2}2K`, + lineEnd: `${CSI2}K`, + lineStart: `${CSI2}1K`, + lines(count) { + let clear = ""; + for (let i2 = 0; i2 < count; i2++) + clear += this.line + (i2 < count - 1 ? cursor3.up() : ""); + if (count) + clear += cursor3.left; + return clear; + } + }; + module.exports = { cursor: cursor3, scroll, erase: erase3, beep }; + } +}); + +// src/cli.ts +import { realpathSync } from "node:fs"; +import { pathToFileURL } from "node:url"; + +// src/args.ts +import { parseArgs } from "node:util"; + +// src/plugin-error.ts +var PluginError = class extends Error { + constructor(message, code, fixes) { + super(message); + this.code = code; + this.fixes = fixes; + this.name = new.target.name; + } + code; + fixes; +}; + +// src/args.ts +var UsageError = class extends PluginError { + constructor(message) { + super(message, "usage"); + } +}; +function parseSetupArgs(argv) { + const { globals, rest } = splitGlobalFlags(argv); + const { values, positionals } = parseArgs({ + args: rest, + allowPositionals: true, + strict: true, + options: { + template: { type: "string" }, + name: { type: "string", short: "n" }, + slug: { type: "string", short: "s" }, + user: { type: "string", short: "u" }, + organization: { type: "string", short: "o" }, + json: { type: "boolean" }, + "no-input": { type: "boolean" }, + help: { type: "boolean", short: "h" }, + strict: { type: "boolean" }, + fix: { type: "boolean" } + } + }); + const [first] = positionals; + if (values.help || first === void 0) { + return { + command: "help", + strict: false, + tenant: globals.tenant, + noColor: globals.noColor, + json: Boolean(values.json), + noInput: Boolean(values["no-input"]) + }; + } + if (first === "audit") { + if (positionals.length !== 2) { + throw new UsageError("Usage: auth0 universal-portals audit [--organization ] [--strict]"); + } + return { + command: "audit", + portalId: positionals[1], + tenant: globals.tenant, + noColor: globals.noColor, + organization: values.organization, + strict: Boolean(values.strict), + fix: Boolean(values.fix), + json: Boolean(values.json), + noInput: Boolean(values["no-input"]) + }; + } + if (first !== "setup") { + throw new UsageError(`Unknown command "${first}". Run: auth0 universal-portals --help`); + } + if (positionals.length > 1) { + throw new UsageError(`Unexpected arguments: ${positionals.slice(1).join(" ")}`); + } + const template = values.template; + if (template !== void 0 && template !== "consumer" && template !== "business") { + throw new UsageError(`--template must be "consumer" or "business", not "${template}".`); + } + return { + command: "setup", + strict: false, + tenant: globals.tenant, + noColor: globals.noColor, + template, + name: values.name, + slug: values.slug, + user: values.user, + organization: values.organization, + json: Boolean(values.json), + noInput: Boolean(values["no-input"]) + }; +} +function missingFlags(args) { + const missing = []; + if (!args.template) missing.push("--template"); + if (!args.name) missing.push("--name"); + if (args.template === "business" && !args.user) missing.push("--user"); + return missing; +} +function usage() { + return [ + "Usage: auth0 universal-portals setup [flags]", + "", + "Creates an Auth0 Portal with its application, forms and pages.", + "", + "Flags:", + " --template Portal template. Asked when omitted.", + " -n, --name Portal name. Asked when omitted.", + " -s, --slug URL slug. Defaults to a slug of the name.", + " -u, --user Business only: the user who will test the portal.", + " -o, --organization Business only: the organization, when the user has several.", + " --no-input Never prompt; fail on anything missing.", + " --json Print the result as JSON on stdout.", + " -h, --help Show this help.", + "", + "Usage: auth0 universal-portals audit [flags]", + "", + "Checks a portal and its application against the setup defaults.", + "", + "Flags:", + " -o, --organization Business only: the organization the portal opens in.", + " --fix Apply the errors the CLI can fix, without asking.", + " --strict Also exit 1 when there are warnings.", + " --json Print the result as JSON on stdout.", + " -h, --help Show this help." + ].join("\n"); +} +function splitGlobalFlags(argv) { + const globals = { noColor: false }; + const rest = []; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--no-color") { + globals.noColor = true; + continue; + } + if (arg === "--debug") continue; + if (arg === "--tenant") { + globals.tenant = argv[index + 1]; + index += 1; + continue; + } + if (arg.startsWith("--tenant=")) { + globals.tenant = arg.slice("--tenant=".length); + continue; + } + rest.push(arg); + } + return { globals, rest }; +} + +// node_modules/@clack/core/dist/index.mjs +import { styleText } from "node:util"; +import { stdout, stdin } from "node:process"; +import * as l from "node:readline"; +import l__default from "node:readline"; + +// node_modules/fast-string-truncated-width/dist/utils.js +var getCodePointsLength = /* @__PURE__ */ (() => { + const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; + return (input) => { + let surrogatePairsNr = 0; + SURROGATE_PAIR_RE.lastIndex = 0; + while (SURROGATE_PAIR_RE.test(input)) { + surrogatePairsNr += 1; + } + return input.length - surrogatePairsNr; + }; +})(); +var isFullWidth = (x) => { + return x === 12288 || x >= 65281 && x <= 65376 || x >= 65504 && x <= 65510; +}; +var isWideNotCJKTNotEmoji = (x) => { + return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141; +}; + +// node_modules/fast-string-truncated-width/dist/index.js +var ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y; +var CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y; +var CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu; +var TAB_RE = /\t{1,1000}/y; +var EMOJI_RE = new RegExp("[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F\\u20E3?))*", "yu"); +var LATIN_RE = /(?:[\x20-\x7E\xA0-\xFF](?!\uFE0F)){1,1000}/y; +var MODIFIER_RE = new RegExp("\\p{M}+", "gu"); +var NO_TRUNCATION = { limit: Infinity, ellipsis: "" }; +var getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {}) => { + const LIMIT = truncationOptions.limit ?? Infinity; + const ELLIPSIS = truncationOptions.ellipsis ?? ""; + const ELLIPSIS_WIDTH = truncationOptions?.ellipsisWidth ?? (ELLIPSIS ? getStringTruncatedWidth(ELLIPSIS, NO_TRUNCATION, widthOptions).width : 0); + const ANSI_WIDTH = 0; + const CONTROL_WIDTH = widthOptions.controlWidth ?? 0; + const TAB_WIDTH = widthOptions.tabWidth ?? 8; + const EMOJI_WIDTH = widthOptions.emojiWidth ?? 2; + const FULL_WIDTH_WIDTH = 2; + const REGULAR_WIDTH = widthOptions.regularWidth ?? 1; + const WIDE_WIDTH = widthOptions.wideWidth ?? FULL_WIDTH_WIDTH; + const PARSE_BLOCKS = [ + [LATIN_RE, REGULAR_WIDTH], + [ANSI_RE, ANSI_WIDTH], + [CONTROL_RE, CONTROL_WIDTH], + [TAB_RE, TAB_WIDTH], + [EMOJI_RE, EMOJI_WIDTH], + [CJKT_WIDE_RE, WIDE_WIDTH] + ]; + let indexPrev = 0; + let index = 0; + let length = input.length; + let lengthExtra = 0; + let truncationEnabled = false; + let truncationIndex = length; + let truncationLimit = Math.max(0, LIMIT - ELLIPSIS_WIDTH); + let unmatchedStart = 0; + let unmatchedEnd = 0; + let width = 0; + let widthExtra = 0; + outer: while (true) { + if (unmatchedEnd > unmatchedStart || index >= length && index > indexPrev) { + const unmatched = input.slice(unmatchedStart, unmatchedEnd) || input.slice(indexPrev, index); + lengthExtra = 0; + for (const char of unmatched.replaceAll(MODIFIER_RE, "")) { + const codePoint = char.codePointAt(0) || 0; + if (isFullWidth(codePoint)) { + widthExtra = FULL_WIDTH_WIDTH; + } else if (isWideNotCJKTNotEmoji(codePoint)) { + widthExtra = WIDE_WIDTH; + } else { + widthExtra = REGULAR_WIDTH; + } + if (width + widthExtra > truncationLimit) { + truncationIndex = Math.min(truncationIndex, Math.max(unmatchedStart, indexPrev) + lengthExtra); + } + if (width + widthExtra > LIMIT) { + truncationEnabled = true; + break outer; + } + lengthExtra += char.length; + width += widthExtra; + } + unmatchedStart = unmatchedEnd = 0; + } + if (index >= length) { + break outer; + } + for (let i2 = 0, l2 = PARSE_BLOCKS.length; i2 < l2; i2++) { + const [BLOCK_RE, BLOCK_WIDTH] = PARSE_BLOCKS[i2]; + BLOCK_RE.lastIndex = index; + if (BLOCK_RE.test(input)) { + lengthExtra = BLOCK_RE === CJKT_WIDE_RE ? getCodePointsLength(input.slice(index, BLOCK_RE.lastIndex)) : BLOCK_RE === EMOJI_RE ? 1 : BLOCK_RE.lastIndex - index; + widthExtra = lengthExtra * BLOCK_WIDTH; + if (width + widthExtra > truncationLimit) { + truncationIndex = Math.min(truncationIndex, index + Math.floor((truncationLimit - width) / BLOCK_WIDTH)); + } + if (width + widthExtra > LIMIT) { + truncationEnabled = true; + break outer; + } + width += widthExtra; + unmatchedStart = indexPrev; + unmatchedEnd = index; + index = indexPrev = BLOCK_RE.lastIndex; + continue outer; + } + } + index += 1; + } + return { + width: truncationEnabled ? truncationLimit : width, + index: truncationEnabled ? truncationIndex : length, + truncated: truncationEnabled, + ellipsed: truncationEnabled && LIMIT >= ELLIPSIS_WIDTH + }; +}; +var dist_default = getStringTruncatedWidth; + +// node_modules/fast-string-width/dist/index.js +var NO_TRUNCATION2 = { + limit: Infinity, + ellipsis: "", + ellipsisWidth: 0 +}; +var fastStringWidth = (input, options = {}) => { + return dist_default(input, NO_TRUNCATION2, options).width; +}; +var dist_default2 = fastStringWidth; + +// node_modules/fast-wrap-ansi/lib/main.js +var ESC = "\x1B"; +var CSI = "\x9B"; +var END_CODE = 39; +var ANSI_ESCAPE_BELL = "\x07"; +var ANSI_CSI = "["; +var ANSI_OSC = "]"; +var ANSI_SGR_TERMINATOR = "m"; +var ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`; +var GROUP_REGEX = new RegExp(`(?:\\${ANSI_CSI}(?\\d+)m|\\${ANSI_ESCAPE_LINK}(?.*)${ANSI_ESCAPE_BELL})`, "y"); +var getClosingCode = (openingCode) => { + if (openingCode >= 30 && openingCode <= 37) + return 39; + if (openingCode >= 90 && openingCode <= 97) + return 39; + if (openingCode >= 40 && openingCode <= 47) + return 49; + if (openingCode >= 100 && openingCode <= 107) + return 49; + if (openingCode === 1 || openingCode === 2) + return 22; + if (openingCode === 3) + return 23; + if (openingCode === 4) + return 24; + if (openingCode === 7) + return 27; + if (openingCode === 8) + return 28; + if (openingCode === 9) + return 29; + if (openingCode === 0) + return 0; + return void 0; +}; +var wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`; +var wrapAnsiHyperlink = (url) => `${ESC}${ANSI_ESCAPE_LINK}${url}${ANSI_ESCAPE_BELL}`; +var wrapWord = (rows, word, columns) => { + const characters = word[Symbol.iterator](); + let isInsideEscape = false; + let isInsideLinkEscape = false; + let lastRow = rows.at(-1); + let visible = lastRow === void 0 ? 0 : dist_default2(lastRow); + let currentCharacter = characters.next(); + let nextCharacter = characters.next(); + let rawCharacterIndex = 0; + while (!currentCharacter.done) { + const character = currentCharacter.value; + const characterLength = dist_default2(character); + if (visible + characterLength <= columns) { + rows[rows.length - 1] += character; + } else { + rows.push(character); + visible = 0; + } + if (character === ESC || character === CSI) { + isInsideEscape = true; + isInsideLinkEscape = word.startsWith(ANSI_ESCAPE_LINK, rawCharacterIndex + 1); + } + if (isInsideEscape) { + if (isInsideLinkEscape) { + if (character === ANSI_ESCAPE_BELL) { + isInsideEscape = false; + isInsideLinkEscape = false; + } + } else if (character === ANSI_SGR_TERMINATOR) { + isInsideEscape = false; + } + } else { + visible += characterLength; + if (visible === columns && !nextCharacter.done) { + rows.push(""); + visible = 0; + } + } + currentCharacter = nextCharacter; + nextCharacter = characters.next(); + rawCharacterIndex += character.length; + } + lastRow = rows.at(-1); + if (!visible && lastRow !== void 0 && lastRow.length && rows.length > 1) { + rows[rows.length - 2] += rows.pop(); + } +}; +var stringVisibleTrimSpacesRight = (string) => { + const words = string.split(" "); + let last = words.length; + while (last) { + if (dist_default2(words[last - 1])) { + break; + } + last--; + } + if (last === words.length) { + return string; + } + return words.slice(0, last).join(" ") + words.slice(last).join(""); +}; +var exec = (string, columns, options = {}) => { + if (options.trim !== false && string.trim() === "") { + return ""; + } + let returnValue = ""; + let escapeCode; + let escapeUrl; + const words = string.split(" "); + let rows = [""]; + let rowLength = 0; + for (let index = 0; index < words.length; index++) { + const word = words[index]; + if (options.trim !== false) { + const row = rows.at(-1) ?? ""; + const trimmed = row.trimStart(); + if (row.length !== trimmed.length) { + rows[rows.length - 1] = trimmed; + rowLength = dist_default2(trimmed); + } + } + if (index !== 0) { + if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) { + rows.push(""); + rowLength = 0; + } + if (rowLength || options.trim === false) { + rows[rows.length - 1] += " "; + rowLength++; + } + } + const wordLength = dist_default2(word); + if (options.hard && wordLength > columns) { + const remainingColumns = columns - rowLength; + const breaksStartingThisLine = 1 + Math.floor((wordLength - remainingColumns - 1) / columns); + const breaksStartingNextLine = Math.floor((wordLength - 1) / columns); + if (breaksStartingNextLine < breaksStartingThisLine) { + rows.push(""); + } + wrapWord(rows, word, columns); + rowLength = dist_default2(rows.at(-1) ?? ""); + continue; + } + if (rowLength + wordLength > columns && rowLength && wordLength) { + if (options.wordWrap === false && rowLength < columns) { + wrapWord(rows, word, columns); + rowLength = dist_default2(rows.at(-1) ?? ""); + continue; + } + rows.push(""); + rowLength = 0; + } + if (rowLength + wordLength > columns && options.wordWrap === false) { + wrapWord(rows, word, columns); + rowLength = dist_default2(rows.at(-1) ?? ""); + continue; + } + rows[rows.length - 1] += word; + rowLength += wordLength; + } + if (options.trim !== false) { + rows = rows.map((row) => stringVisibleTrimSpacesRight(row)); + } + const preString = rows.join("\n"); + let inSurrogate = false; + for (let i2 = 0; i2 < preString.length; i2++) { + const character = preString[i2]; + returnValue += character; + if (!inSurrogate) { + inSurrogate = character >= "\uD800" && character <= "\uDBFF"; + if (inSurrogate) { + continue; + } + } else { + inSurrogate = false; + } + if (character === ESC || character === CSI) { + GROUP_REGEX.lastIndex = i2 + 1; + const groupsResult = GROUP_REGEX.exec(preString); + const groups = groupsResult?.groups; + if (groups?.code !== void 0) { + const code = Number.parseFloat(groups.code); + escapeCode = code === END_CODE ? void 0 : code; + } else if (groups?.uri !== void 0) { + escapeUrl = groups.uri.length === 0 ? void 0 : groups.uri; + } + } + if (preString[i2 + 1] === "\n") { + if (escapeUrl) { + returnValue += wrapAnsiHyperlink(""); + } + const closingCode = escapeCode ? getClosingCode(escapeCode) : void 0; + if (escapeCode && closingCode) { + returnValue += wrapAnsiCode(closingCode); + } + } else if (character === "\n") { + if (escapeCode && getClosingCode(escapeCode)) { + returnValue += wrapAnsiCode(escapeCode); + } + if (escapeUrl) { + returnValue += wrapAnsiHyperlink(escapeUrl); + } + } + } + return returnValue; +}; +var CRLF_OR_LF = /\r?\n/; +function wrapAnsi(string, columns, options) { + return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join("\n"); +} + +// node_modules/@clack/core/dist/index.mjs +var import_sisteransi = __toESM(require_src(), 1); +import { ReadStream } from "node:tty"; +function findCursor(s, o, l2) { + if (!l2.some((r2) => !r2.disabled)) + return s; + const t2 = s + o, n3 = Math.max(l2.length - 1, 0), e = t2 < 0 ? n3 : t2 > n3 ? 0 : t2; + return l2[e]?.disabled ? findCursor(e, o < 0 ? -1 : 1, l2) : e; +} +var a$1 = ["up", "down", "left", "right", "space", "enter", "cancel"]; +var t = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" +]; +var settings = { + actions: new Set(a$1), + aliases: /* @__PURE__ */ new Map([ + // vim support + ["k", "up"], + ["j", "down"], + ["h", "left"], + ["l", "right"], + ["", "cancel"], + // opinionated defaults! + ["escape", "cancel"] + ]), + messages: { + cancel: "Canceled", + error: "Something went wrong" + }, + withGuide: true, + accessible: void 0, + date: { + monthNames: [...t], + messages: { + required: "Please enter a valid date", + invalidMonth: "There are only 12 months in a year", + invalidDay: (n3, e) => `There are only ${n3} days in ${e}`, + afterMin: (n3) => `Date must be on or after ${n3.toISOString().slice(0, 10)}`, + beforeMax: (n3) => `Date must be on or before ${n3.toISOString().slice(0, 10)}` + } + } +}; +function isAccessible(n3) { + if (n3 !== void 0) return n3; + if (settings.accessible !== void 0) return settings.accessible; + const e = process.env.ACCESSIBLE; + return e !== void 0 && e !== "" && e !== "0" && e !== "false"; +} +function isActionKey(n3, e) { + if (typeof n3 == "string") + return settings.aliases.get(n3) === e; + for (const s of n3) + if (s !== void 0 && isActionKey(s, e)) + return true; + return false; +} +function diffLines(i2, s) { + if (i2 === s) return; + const e = i2.split(` +`), t2 = s.split(` +`), r2 = Math.max(e.length, t2.length), f = []; + for (let n3 = 0; n3 < r2; n3++) + e[n3] !== t2[n3] && f.push(n3); + return { + lines: f, + numLinesBefore: e.length, + numLinesAfter: t2.length, + numLines: r2 + }; +} +var R = globalThis.process.platform.startsWith("win"); +var CANCEL_SYMBOL = /* @__PURE__ */ Symbol("clack:cancel"); +function isCancel(e) { + return e === CANCEL_SYMBOL; +} +function setRawMode(e, r2) { + const o = e; + o.isTTY && o.setRawMode(r2); +} +function block({ + input: e = stdin, + output: r2 = stdout, + overwrite: o = true, + hideCursor: n3 = true +} = {}) { + const s = l.createInterface({ + input: e, + output: r2, + prompt: "", + tabSize: 1 + }); + l.emitKeypressEvents(e, s), e instanceof ReadStream && e.isTTY && e.setRawMode(true); + const t2 = (f, { name: a2, sequence: w }) => { + const c2 = String(f); + if (isActionKey([c2, a2, w], "cancel")) { + n3 && r2.write(import_sisteransi.cursor.show), process.exit(0); + return; + } + if (!o) return; + const i2 = a2 === "return" ? 0 : -1, m2 = a2 === "return" ? -1 : 0; + l.moveCursor(r2, i2, m2, () => { + l.clearLine(r2, 1, () => { + e.once("keypress", t2); + }); + }); + }; + return n3 && r2.write(import_sisteransi.cursor.hide), e.once("keypress", t2), () => { + e.off("keypress", t2), n3 && r2.write(import_sisteransi.cursor.show), e instanceof ReadStream && e.isTTY && !R && e.setRawMode(false), s.terminal = false, s.close(); + }; +} +var getColumns = (e) => "columns" in e && typeof e.columns == "number" ? e.columns : 80; +var getRows = (e) => "rows" in e && typeof e.rows == "number" ? e.rows : 20; +function wrapTextWithPrefix(e, r2, o, n3 = o, s = o, t2) { + const f = getColumns(e ?? stdout); + return wrapAnsi(r2, f - o.length, { + hard: true, + trim: false + }).split(` +`).map((c2, i2, m2) => { + const d = t2 ? t2(c2, i2) : c2; + return i2 === 0 ? `${n3}${d}` : i2 === m2.length - 1 ? `${s}${d}` : `${o}${d}`; + }).join(` +`); +} +function runValidation(e, a2) { + if ("~standard" in e) { + const n3 = e["~standard"].validate(a2); + return n3 instanceof Promise ? n3.then((r2) => r2.issues?.at(0)?.message) : n3.issues?.at(0)?.message; + } + return e(a2); +} +var y = class { + input; + output; + _abortSignal; + rl; + opts; + _render; + _track = false; + _prevFrame = ""; + _subscribers = /* @__PURE__ */ new Map(); + _cursor = 0; + state = "initial"; + error = ""; + value; + userInput = ""; + /** + * Whether accessible (static, screen-reader friendly) output is enabled for + * this prompt, resolved from the `accessible` option, the global setting, + * and the `ACCESSIBLE` env var. + */ + get accessible() { + return isAccessible(this.opts.accessible); + } + constructor(t2, e = true) { + const { input: i2 = stdin, output: s = stdout, render: r2, signal: n3, ...o } = t2; + this.opts = o, this.onKeypress = this.onKeypress.bind(this), this.close = this.close.bind(this), this.render = this.render.bind(this), this._render = r2.bind(this), this._track = e, this._abortSignal = n3, this.input = i2, this.output = s; + } + /** + * Unsubscribe all listeners + */ + unsubscribe() { + this._subscribers.clear(); + } + /** + * Set a subscriber with opts + * @param event - The event name + */ + setSubscriber(t2, e) { + const i2 = this._subscribers.get(t2) ?? []; + i2.push(e), this._subscribers.set(t2, i2); + } + /** + * Subscribe to an event + * @param event - The event name + * @param cb - The callback + */ + on(t2, e) { + this.setSubscriber(t2, { cb: e }); + } + /** + * Subscribe to an event once + * @param event - The event name + * @param cb - The callback + */ + once(t2, e) { + this.setSubscriber(t2, { cb: e, once: true }); + } + /** + * Emit an event with data + * @param event - The event name + * @param data - The data to pass to the callback + */ + emit(t2, ...e) { + const i2 = this._subscribers.get(t2) ?? [], s = []; + for (const r2 of i2) + r2.cb(...e), r2.once && s.push(() => i2.splice(i2.indexOf(r2), 1)); + for (const r2 of s) + r2(); + } + prompt() { + return new Promise((t2) => { + if (this._abortSignal) { + if (this._abortSignal.aborted) + return this.state = "cancel", this.close(), t2(CANCEL_SYMBOL); + this._abortSignal.addEventListener( + "abort", + () => { + this.state = "cancel", this.close(); + }, + { once: true } + ); + } + this.rl = l__default.createInterface({ + input: this.input, + tabSize: 2, + prompt: "", + escapeCodeTimeout: 50, + terminal: true + }), this.rl.prompt(), this.opts.initialUserInput !== void 0 && this._setUserInput(this.opts.initialUserInput, true), this.input.on("keypress", this.onKeypress), setRawMode(this.input, true), this.output.on("resize", this.render), this.render(), this.once("submit", () => { + this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t2(this.value); + }), this.once("cancel", () => { + this.output.write(import_sisteransi.cursor.show), this.output.off("resize", this.render), setRawMode(this.input, false), t2(CANCEL_SYMBOL); + }); + }); + } + _isActionKey(t2, e) { + return t2 === " "; + } + _shouldSubmit(t2, e) { + return true; + } + _setValue(t2) { + this.value = t2, this.emit("value", this.value); + } + _setUserInput(t2, e) { + this.userInput = t2 ?? "", this.emit("userInput", this.userInput), e && this._track && this.rl && (this.rl.write(this.userInput), this._cursor = this.rl.cursor); + } + _clearUserInput() { + this.rl?.write(null, { ctrl: true, name: "u" }), this._setUserInput(""); + } + async onKeypress(t2, e) { + if (this.state !== "validating") { + if (this._track && e.name !== "return" && (e.name && this._isActionKey(t2, e) && this.rl?.write(null, { ctrl: true, name: "h" }), this._cursor = this.rl?.cursor ?? 0, this._setUserInput(this.rl?.line)), this.state === "error" && (this.state = "active"), e?.name && (!this._track && settings.aliases.has(e.name) && this.emit("cursor", settings.aliases.get(e.name)), settings.actions.has(e.name) && this.emit("cursor", e.name)), t2 && (t2.toLowerCase() === "y" || t2.toLowerCase() === "n") && this.emit("confirm", t2.toLowerCase() === "y"), this.emit("key", t2, e), e?.name === "return" && this._shouldSubmit(t2, e)) { + if (this.opts.validate) { + const i2 = runValidation(this.opts.validate, this.value); + let s; + i2 instanceof Promise ? (this.state = "validating", this.render(), s = await i2) : s = i2, s && (this.error = s instanceof Error ? s.message : s, this.state = "error", this.rl?.write(this.userInput)); + } + this.state !== "error" && (this.state = "submit"); + } + isActionKey([t2, e?.name, e?.sequence], "cancel") && (this.state = "cancel"), (this.state === "submit" || this.state === "cancel") && this.emit("finalize"), this.render(), (this.state === "submit" || this.state === "cancel") && this.close(); + } + } + close() { + this.input.unpipe(), this.input.removeListener("keypress", this.onKeypress), this.output.write(` +`), setRawMode(this.input, false), this.rl?.close(), this.rl = void 0, this.emit(`${this.state}`, this.value), this.unsubscribe(); + } + restoreCursor() { + const t2 = wrapAnsi(this._prevFrame, process.stdout.columns, { hard: true, trim: false }).split(` +`).length - 1; + this.output.write(import_sisteransi.cursor.move(-999, t2 * -1)); + } + render() { + const t2 = wrapAnsi(this._render(this) ?? "", process.stdout.columns, { + hard: true, + trim: false + }); + if (t2 !== this._prevFrame) { + if (this.state === "initial") + this.output.write(import_sisteransi.cursor.hide); + else { + const e = diffLines(this._prevFrame, t2), i2 = getRows(this.output); + if (this.restoreCursor(), e) { + const s = Math.max(0, e.numLinesAfter - i2), r2 = Math.max(0, e.numLinesBefore - i2); + let n3 = e.lines.find((o) => o >= s); + if (n3 === void 0) { + this._prevFrame = t2; + return; + } + if (e.lines.length === 1) { + this.output.write(import_sisteransi.cursor.move(0, n3 - r2)), this.output.write(import_sisteransi.erase.lines(1)); + const o = t2.split(` +`); + this.output.write(o[n3]), this._prevFrame = t2, this.output.write(import_sisteransi.cursor.move(0, o.length - n3 - 1)); + return; + } else if (e.lines.length > 1) { + if (s < r2) + n3 = s; + else { + const h2 = n3 - r2; + h2 > 0 && this.output.write(import_sisteransi.cursor.move(0, h2)); + } + this.output.write(import_sisteransi.erase.down()); + const f = t2.split(` +`).slice(n3); + this.output.write(f.join(` +`)), this._prevFrame = t2; + return; + } + } + this.output.write(import_sisteransi.erase.down()); + } + this.output.write(t2), this.state === "initial" && (this.state = "active"), this._prevFrame = t2; + } + } +}; +var r = class extends y { + get cursor() { + return this.value ? 0 : 1; + } + get _value() { + return this.cursor === 0; + } + constructor(t2) { + super(t2, false), this.value = !!t2.initialValue, this.on("userInput", () => { + this.value = this._value; + }), this.on("confirm", (i2) => { + this.output.write(import_sisteransi.cursor.move(0, -1)), this.value = i2, this.state = "submit", this.close(); + }), this.on("cursor", () => { + this.value = !this.value; + }); + } +}; +var a = class extends y { + options; + cursor = 0; + get _value() { + return this.options[this.cursor]?.value; + } + get _enabledOptions() { + return this.options.filter((e) => e.disabled !== true); + } + toggleAll() { + const e = this._enabledOptions, i2 = this.value !== void 0 && this.value.length === e.length; + this.value = i2 ? [] : e.map((t2) => t2.value); + } + toggleInvert() { + const e = this.value; + if (!e) + return; + const i2 = this._enabledOptions.filter((t2) => !e.includes(t2.value)); + this.value = i2.map((t2) => t2.value); + } + toggleValue() { + this.value === void 0 && (this.value = []); + const e = this.value.includes(this._value); + this.value = e ? this.value.filter((i2) => i2 !== this._value) : [...this.value, this._value]; + } + constructor(e) { + super(e, false), this.options = e.options, this.value = [...e.initialValues ?? []]; + const i2 = Math.max( + this.options.findIndex(({ value: t2 }) => t2 === e.cursorAt), + 0 + ); + this.cursor = this.options[i2]?.disabled ? findCursor(i2, 1, this.options) : i2, this.on("key", (t2, l2) => { + l2.name === "a" && this.toggleAll(), l2.name === "i" && this.toggleInvert(); + }), this.on("cursor", (t2) => { + switch (t2) { + case "left": + case "up": + this.cursor = findCursor(this.cursor, -1, this.options); + break; + case "down": + case "right": + this.cursor = findCursor(this.cursor, 1, this.options); + break; + case "space": + this.toggleValue(); + break; + } + }); + } +}; +var n$1 = class n extends y { + options; + cursor = 0; + get _selectedValue() { + return this.options[this.cursor]; + } + changeValue() { + const e = this._selectedValue; + this.value = e === void 0 ? void 0 : e.value; + } + constructor(e) { + super(e, false), this.options = e.options; + const o = this.options.findIndex(({ value: s }) => s === e.initialValue), t2 = o === -1 ? 0 : o; + this.cursor = this.options[t2]?.disabled ? findCursor(t2, 1, this.options) : t2, this.changeValue(), this.on("cursor", (s) => { + switch (s) { + case "left": + case "up": + this.cursor = findCursor(this.cursor, -1, this.options); + break; + case "down": + case "right": + this.cursor = findCursor(this.cursor, 1, this.options); + break; + } + this.changeValue(); + }); + } +}; +var n2 = class extends y { + get userInputWithCursor() { + if (this.state === "submit") + return this.userInput; + const t2 = this.userInput; + if (this.cursor >= t2.length) + return `${this.userInput}\u2588`; + const r2 = t2.slice(0, this.cursor), s = t2.slice(this.cursor, this.cursor + 1), e = t2.slice(this.cursor + 1); + return `${r2}${styleText("inverse", s)}${e}`; + } + get cursor() { + return this._cursor; + } + constructor(t2) { + super({ + ...t2, + initialUserInput: t2.initialUserInput ?? t2.initialValue + }), this.on("userInput", (r2) => { + this._setValue(r2); + }), this.on("finalize", () => { + this.value || (this.value = t2.defaultValue), this.value === void 0 && (this.value = ""); + }); + } +}; + +// node_modules/@clack/prompts/dist/index.mjs +import { styleText as styleText2, stripVTControlCharacters } from "node:util"; +import process$1 from "node:process"; +var import_sisteransi2 = __toESM(require_src(), 1); +function isUnicodeSupported() { + if (process$1.platform !== "win32") { + return process$1.env.TERM !== "linux"; + } + return Boolean(process$1.env.CI) || Boolean(process$1.env.WT_SESSION) || Boolean(process$1.env.TERMINUS_SUBLIME) || process$1.env.ConEmuTask === "{cmd::Cmder}" || process$1.env.TERM_PROGRAM === "Terminus-Sublime" || process$1.env.TERM_PROGRAM === "vscode" || process$1.env.TERM === "xterm-256color" || process$1.env.TERM === "alacritty" || process$1.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; +} +var unicode = isUnicodeSupported(); +var isCI = () => process.env.CI === "true"; +var unicodeOr = (o, e) => unicode ? o : e; +var S_STEP_ACTIVE = unicodeOr("\u25C6", "*"); +var S_STEP_CANCEL = unicodeOr("\u25A0", "x"); +var S_STEP_ERROR = unicodeOr("\u25B2", "x"); +var S_STEP_SUBMIT = unicodeOr("\u25C7", "o"); +var S_BAR_START = unicodeOr("\u250C", "T"); +var S_BAR = unicodeOr("\u2502", "|"); +var S_BAR_END = unicodeOr("\u2514", "\u2014"); +var S_BAR_START_RIGHT = unicodeOr("\u2510", "T"); +var S_BAR_END_RIGHT = unicodeOr("\u2518", "\u2014"); +var S_RADIO_ACTIVE = unicodeOr("\u25CF", ">"); +var S_RADIO_INACTIVE = unicodeOr("\u25CB", " "); +var S_CHECKBOX_ACTIVE = unicodeOr("\u25FB", "[\u2022]"); +var S_CHECKBOX_SELECTED = unicodeOr("\u25FC", "[+]"); +var S_CHECKBOX_INACTIVE = unicodeOr("\u25FB", "[ ]"); +var S_PASSWORD_MASK = unicodeOr("\u25AA", "\u2022"); +var S_BAR_H = unicodeOr("\u2500", "-"); +var S_CORNER_TOP_RIGHT = unicodeOr("\u256E", "+"); +var S_CONNECT_LEFT = unicodeOr("\u251C", "+"); +var S_CORNER_BOTTOM_RIGHT = unicodeOr("\u256F", "+"); +var S_CORNER_BOTTOM_LEFT = unicodeOr("\u2570", "+"); +var S_CORNER_TOP_LEFT = unicodeOr("\u256D", "+"); +var S_INFO = unicodeOr("\u25CF", "\u2022"); +var S_SUCCESS = unicodeOr("\u25C6", "*"); +var S_WARN = unicodeOr("\u25B2", "!"); +var S_ERROR = unicodeOr("\u25A0", "x"); +var symbol = (o) => { + switch (o) { + case "initial": + case "active": + return styleText2("cyan", S_STEP_ACTIVE); + case "cancel": + return styleText2("red", S_STEP_CANCEL); + case "error": + return styleText2("yellow", S_STEP_ERROR); + case "submit": + return styleText2("green", S_STEP_SUBMIT); + case "validating": + return styleText2("dim", S_STEP_ACTIVE); + } +}; +var symbolBar = (o) => { + switch (o) { + case "initial": + case "active": + return styleText2("cyan", S_BAR); + case "cancel": + return styleText2("red", S_BAR); + case "error": + return styleText2("yellow", S_BAR); + case "submit": + return styleText2("green", S_BAR); + } +}; +function formatInstructionFooter(o, e) { + const r2 = [`${e ? `${styleText2("cyan", S_BAR)} ` : ""}${o.join(" \u2022 ")}`]; + return e && r2.push(styleText2("cyan", S_BAR_END)), r2; +} +var I = (l2, e, w, p, b, C2 = false) => { + let r2 = e, O = 0; + if (C2) + for (let i2 = p - 1; i2 >= w; i2--) { + const m2 = l2[i2]; + if (m2 && (r2 -= m2.length), O++, r2 <= b) break; + } + else + for (let i2 = w; i2 < p; i2++) { + const m2 = l2[i2]; + if (m2 && (r2 -= m2.length), O++, r2 <= b) break; + } + return { lineCount: r2, removals: O }; +}; +var limitOptions = ({ + cursor: l2, + options: e, + style: w, + output: p = process.stdout, + maxItems: b = Number.POSITIVE_INFINITY, + columnPadding: C2 = 0, + rowPadding: r2 = 4 +}) => { + const i2 = getColumns(p) - C2, m2 = getRows(p), M = styleText2("dim", "..."), v = Math.max(m2 - r2, 0), a2 = Math.max(Math.min(b, v), 5); + let f = 0; + l2 >= a2 - 3 && (f = Math.max( + Math.min(l2 - a2 + 3, e.length - a2), + 0 + )); + let d = a2 < e.length && f > 0, c2 = a2 < e.length && f + a2 < e.length; + const W2 = Math.min( + f + a2, + e.length + ), s = []; + let g = 0; + d && g++, c2 && g++; + const T = f + (d ? 1 : 0), y2 = W2 - (c2 ? 1 : 0); + for (let t2 = T; t2 < y2; t2++) { + const n3 = e[t2], o = n3 ? w(n3, t2 === l2) : "", h2 = wrapAnsi(o, i2, { + hard: true, + trim: false + }).split(` +`); + s.push(h2), g += h2.length; + } + if (g > v) { + let t2 = 0, n3 = 0, o = g; + const h2 = l2 - T; + let u3 = v; + const L = () => I(s, o, 0, h2, u3), E = () => I( + s, + o, + h2 + 1, + s.length, + u3, + true + ); + d ? ({ lineCount: o, removals: t2 } = L(), o > u3 && (c2 || (u3 -= 1), { lineCount: o, removals: n3 } = E())) : (c2 || (u3 -= 1), { lineCount: o, removals: n3 } = E(), o > u3 && (u3 -= 1, { lineCount: o, removals: t2 } = L())), t2 > 0 && (d = true, s.splice(0, t2)), n3 > 0 && (c2 = true, s.splice(s.length - n3, n3)); + } + const x = []; + d && x.push(M); + for (const t2 of s) + for (const n3 of t2) + x.push(n3); + return c2 && x.push(M), x; +}; +var K = [ + S_CORNER_TOP_LEFT, + S_CORNER_TOP_RIGHT, + S_CORNER_BOTTOM_LEFT, + S_CORNER_BOTTOM_RIGHT +]; +var Q = [S_BAR_START, S_BAR_START_RIGHT, S_BAR_END, S_BAR_END_RIGHT]; +function A$1(n3, e, t2, o) { + let i2 = t2, f = t2; + return o === "center" ? i2 = Math.floor((e - n3) / 2) : o === "right" && (i2 = e - n3 - t2), f = e - i2 - n3, [i2, f]; +} +var U2 = (n3) => n3; +var box = (n3 = "", e = "", t2) => { + const o = t2?.output ?? process.stdout, i2 = getColumns(o), R2 = 1 * 2, u3 = t2?.titlePadding ?? 1, h2 = t2?.contentPadding ?? 2, w = t2?.width === void 0 || t2.width === "auto" ? 1 : Math.min(1, t2.width), m2 = t2?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR)} ` : "", b = t2?.formatBorder ?? U2, a2 = (t2?.rounded ? K : Q).map(b), _ = b(S_BAR_H), p = b(S_BAR), x = dist_default2(m2), B = dist_default2(e), y2 = i2 - x; + let r2 = Math.floor(i2 * w) - x; + if (t2?.width === "auto") { + const c2 = n3.split(` +`); + let s = B + u3 * 2; + for (const G of c2) { + const P = dist_default2(G) + h2 * 2; + P > s && (s = P); + } + const g = s + R2; + g < r2 && (r2 = g); + } + r2 % 2 !== 0 && (r2 < y2 ? r2++ : r2--); + const d = r2 - R2, T = d - u3 * 2, S = B > T ? `${e.slice(0, T - 3)}...` : e, [O, W2] = A$1( + dist_default2(S), + d, + u3, + t2?.titleAlign + ), L = wrapAnsi(n3, d - h2 * 2, { + hard: true, + trim: false + }); + o.write( + `${m2}${a2[0]}${_.repeat(O)}${S}${_.repeat(W2)}${a2[1]} +` + ); + const E = L.split(` +`); + for (const c2 of E) { + const [s, g] = A$1( + dist_default2(c2), + d, + h2, + t2?.contentAlign + ); + o.write( + `${m2}${p}${" ".repeat(s)}${c2}${" ".repeat(g)}${p} +` + ); + } + o.write(`${m2}${a2[2]}${_.repeat(d)}${a2[3]} +`); +}; +var confirm = (e) => { + const a2 = e.active ?? "Yes", o = e.inactive ?? "No"; + return new r({ + active: a2, + inactive: o, + signal: e.signal, + input: e.input, + output: e.output, + initialValue: e.initialValue ?? true, + render() { + const i2 = e.withGuide ?? settings.withGuide, u3 = `${symbol(this.state)} `, l2 = i2 ? `${styleText2("gray", S_BAR)} ` : "", f = wrapTextWithPrefix( + e.output, + e.message, + l2, + u3 + ), s = `${i2 ? `${styleText2("gray", S_BAR)} +` : ""}${f} +`, c2 = this.value ? a2 : o; + switch (this.state) { + case "submit": { + const r2 = i2 ? `${styleText2("gray", S_BAR)} ` : ""; + return `${s}${r2}${styleText2("dim", c2)}`; + } + case "cancel": { + const r2 = i2 ? `${styleText2("gray", S_BAR)} ` : ""; + return `${s}${r2}${styleText2(["strikethrough", "dim"], c2)}${i2 ? ` +${styleText2("gray", S_BAR)}` : ""}`; + } + default: { + const r2 = i2 ? `${styleText2("cyan", S_BAR)} ` : "", g = i2 ? styleText2("cyan", S_BAR_END) : ""; + return `${s}${r2}${this.value ? `${styleText2("green", S_RADIO_ACTIVE)} ${a2}` : `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", a2)}`}${e.vertical ? i2 ? ` +${styleText2("cyan", S_BAR)} ` : ` +` : ` ${styleText2("dim", "/")} `}${this.value ? `${styleText2("dim", S_RADIO_INACTIVE)} ${styleText2("dim", o)}` : `${styleText2("green", S_RADIO_ACTIVE)} ${o}`} +${g} +`; + } + } + } + }).prompt(); +}; +var MULTISELECT_INSTRUCTIONS = [ + `${styleText2("dim", "\u2191/\u2193")} to navigate`, + `${styleText2("dim", "Space:")} select`, + `${styleText2("dim", "Enter:")} confirm` +]; +var m = (i2, u3) => i2.split(` +`).map((d) => u3(d)).join(` +`); +var multiselect = (i2) => { + const u3 = (t2, a2) => { + const r2 = t2.label ?? String(t2.value); + return a2 === "disabled" ? `${styleText2("gray", S_CHECKBOX_INACTIVE)} ${m(r2, (o) => styleText2(["strikethrough", "gray"], o))}${t2.hint ? ` ${styleText2("dim", `(${t2.hint ?? "disabled"})`)}` : ""}` : a2 === "active" ? `${styleText2("cyan", S_CHECKBOX_ACTIVE)} ${r2}${t2.hint ? ` ${styleText2("dim", `(${t2.hint})`)}` : ""}` : a2 === "selected" ? `${styleText2("green", S_CHECKBOX_SELECTED)} ${m(r2, (o) => styleText2("dim", o))}${t2.hint ? ` ${styleText2("dim", `(${t2.hint})`)}` : ""}` : a2 === "cancelled" ? `${m(r2, (o) => styleText2(["strikethrough", "dim"], o))}` : a2 === "active-selected" ? `${styleText2("green", S_CHECKBOX_SELECTED)} ${r2}${t2.hint ? ` ${styleText2("dim", `(${t2.hint})`)}` : ""}` : a2 === "submitted" ? `${m(r2, (o) => styleText2("dim", o))}` : `${styleText2("dim", S_CHECKBOX_INACTIVE)} ${m(r2, (o) => styleText2("dim", o))}`; + }, d = i2.required ?? true, x = i2.showInstructions ?? true; + return new a({ + options: i2.options, + signal: i2.signal, + input: i2.input, + output: i2.output, + initialValues: i2.initialValues, + required: d, + cursorAt: i2.cursorAt, + validate(t2) { + if (d && (t2 === void 0 || t2.length === 0)) + return `Please select at least one option. +${styleText2( + "reset", + styleText2( + "dim", + `Press ${styleText2(["gray", "bgWhite", "inverse"], " space ")} to select, ${styleText2( + "gray", + styleText2("bgWhite", styleText2("inverse", " enter ")) + )} to submit` + ) + )}`; + }, + render() { + const t2 = i2.withGuide ?? settings.withGuide, a2 = wrapTextWithPrefix( + i2.output, + i2.message, + t2 ? `${symbolBar(this.state)} ` : "", + `${symbol(this.state)} ` + ), r2 = `${t2 ? `${styleText2("gray", S_BAR)} +` : ""}${a2} +`, o = this.value ?? [], g = (n3, l2) => { + if (n3.disabled) + return u3(n3, "disabled"); + const s = o.includes(n3.value); + return l2 && s ? u3(n3, "active-selected") : s ? u3(n3, "selected") : u3(n3, l2 ? "active" : "inactive"); + }; + switch (this.state) { + case "submit": { + const n3 = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u3(s, "submitted")).join(styleText2("dim", ", ")) || styleText2("dim", "none"), l2 = wrapTextWithPrefix( + i2.output, + n3, + t2 ? `${styleText2("gray", S_BAR)} ` : "" + ); + return `${r2}${l2}`; + } + case "cancel": { + const n3 = this.options.filter(({ value: s }) => o.includes(s)).map((s) => u3(s, "cancelled")).join(styleText2("dim", ", ")); + if (n3.trim() === "") + return `${r2}${styleText2("gray", S_BAR)}`; + const l2 = wrapTextWithPrefix( + i2.output, + n3, + t2 ? `${styleText2("gray", S_BAR)} ` : "" + ); + return `${r2}${l2}${t2 ? ` +${styleText2("gray", S_BAR)}` : ""}`; + } + case "error": { + const n3 = t2 ? `${styleText2("yellow", S_BAR)} ` : "", l2 = this.error.split(` +`).map( + ($, v) => v === 0 ? `${t2 ? `${styleText2("yellow", S_BAR_END)} ` : ""}${styleText2("yellow", $)}` : ` ${$}` + ).join(` +`), s = r2.split(` +`).length, h2 = l2.split(` +`).length + 1; + return `${r2}${n3}${limitOptions({ + output: i2.output, + options: this.options, + cursor: this.cursor, + maxItems: i2.maxItems, + columnPadding: n3.length, + rowPadding: s + h2, + style: g + }).join(` +${n3}`)} +${l2} +`; + } + default: { + const n3 = t2 ? `${styleText2("cyan", S_BAR)} ` : "", l2 = r2.split(` +`).length, s = x ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS, t2) : t2 ? [styleText2("cyan", S_BAR_END)] : [], h2 = s.join(` +`), $ = s.length + 1; + return `${r2}${n3}${limitOptions({ + output: i2.output, + options: this.options, + cursor: this.cursor, + maxItems: i2.maxItems, + columnPadding: n3.length, + rowPadding: l2 + $, + style: g + }).join(` +${n3}`)} +${h2} +`; + } + } + } + }).prompt(); +}; +var log = { + message: (s = [], { + symbol: e = styleText2("gray", S_BAR), + secondarySymbol: r2 = styleText2("gray", S_BAR), + output: m2 = process.stdout, + spacing: l2 = 1, + withGuide: c2 + } = {}) => { + const t2 = [], o = c2 ?? settings.withGuide, f = o ? r2 : "", O = o ? `${e} ` : "", u3 = o ? `${r2} ` : ""; + for (let i2 = 0; i2 < l2; i2++) + t2.push(f); + const g = Array.isArray(s) ? s : s.split(` +`); + if (g.length > 0) { + const [i2, ...y2] = g; + i2.length > 0 ? t2.push(`${O}${i2}`) : t2.push(o ? e : ""); + for (const p of y2) + p.length > 0 ? t2.push(`${u3}${p}`) : t2.push(o ? r2 : ""); + } + m2.write(`${t2.join(` +`)} +`); + }, + info: (s, e) => { + log.message(s, { ...e, symbol: styleText2("blue", S_INFO) }); + }, + success: (s, e) => { + log.message(s, { ...e, symbol: styleText2("green", S_SUCCESS) }); + }, + step: (s, e) => { + log.message(s, { ...e, symbol: styleText2("green", S_STEP_SUBMIT) }); + }, + warn: (s, e) => { + log.message(s, { ...e, symbol: styleText2("yellow", S_WARN) }); + }, + /** alias for `log.warn()`. */ + warning: (s, e) => { + log.warn(s, e); + }, + error: (s, e) => { + log.message(s, { ...e, symbol: styleText2("red", S_ERROR) }); + } +}; +var cancel = (o = "", t2) => { + const i2 = t2?.output ?? process.stdout, e = t2?.withGuide ?? settings.withGuide ? `${styleText2("gray", S_BAR_END)} ` : ""; + i2.write(`${e}${styleText2("red", o)} + +`); +}; +var W$1 = (o) => o; +var C = (o, e, s) => { + const a2 = { + hard: true, + trim: false + }, i2 = wrapAnsi(o, e, a2).split(` +`), c2 = i2.reduce((n3, t2) => Math.max(dist_default2(t2), n3), 0), u3 = i2.map(s).reduce((n3, t2) => Math.max(dist_default2(t2), n3), 0), g = e - (u3 - c2); + return wrapAnsi(o, g, a2); +}; +var note = (o = "", e = "", s) => { + const a2 = s?.output ?? process$1.stdout, i2 = s?.withGuide ?? settings.withGuide, c2 = s?.format ?? W$1, g = ["", ...C(o, getColumns(a2) - 6, c2).split(` +`).map(c2), ""], n3 = dist_default2(e), t2 = Math.max( + g.reduce((m2, F) => { + const O = dist_default2(F); + return O > m2 ? O : m2; + }, 0), + n3 + ) + 2, h2 = g.map( + (m2) => `${styleText2("gray", S_BAR)} ${m2}${" ".repeat(t2 - dist_default2(m2))}${styleText2("gray", S_BAR)}` + ).join(` +`), T = i2 ? `${styleText2("gray", S_BAR)} +` : "", l$1 = i2 ? S_CONNECT_LEFT : S_CORNER_BOTTOM_LEFT; + a2.write( + `${T}${styleText2("green", S_STEP_SUBMIT)} ${styleText2("reset", e)} ${styleText2( + "gray", + S_BAR_H.repeat(Math.max(t2 - n3 - 1, 1)) + S_CORNER_TOP_RIGHT + )} +${h2} +${styleText2("gray", l$1 + S_BAR_H.repeat(t2 + 2) + S_CORNER_BOTTOM_RIGHT)} +` + ); +}; +var W = (l2) => styleText2("magenta", l2); +var spinner = ({ + indicator: l2 = "dots", + onCancel: h2, + output: n3 = process.stdout, + cancelMessage: G, + errorMessage: O, + frames: E = unicode ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], + delay: F = unicode ? 80 : 120, + signal: m2, + ...I2 +} = {}) => { + const u3 = isCI(); + let M, T, d = false, S = false, s = "", p, w = performance.now(); + const x = getColumns(n3), k = I2?.styleFrame ?? W, g = (e) => { + const r2 = e > 1 ? O ?? settings.messages.error : G ?? settings.messages.cancel; + S = e === 1, d && (a2(r2, e), S && typeof h2 == "function" && h2()); + }, f = () => g(2), i2 = () => g(1), A = () => { + process.on("uncaughtExceptionMonitor", f), process.on("unhandledRejection", f), process.on("SIGINT", i2), process.on("SIGTERM", i2), process.on("exit", g), m2 && m2.addEventListener("abort", i2); + }, H = () => { + process.removeListener("uncaughtExceptionMonitor", f), process.removeListener("unhandledRejection", f), process.removeListener("SIGINT", i2), process.removeListener("SIGTERM", i2), process.removeListener("exit", g), m2 && m2.removeEventListener("abort", i2); + }, y2 = () => { + if (p === void 0) return; + u3 && n3.write(` +`); + const r2 = wrapAnsi(p, x, { + hard: true, + trim: false + }).split(` +`); + r2.length > 1 && n3.write(import_sisteransi2.cursor.up(r2.length - 1)), n3.write(import_sisteransi2.cursor.to(0)), n3.write(import_sisteransi2.erase.down()); + }, C2 = (e) => e.replace(/\.+$/, ""), _ = (e) => { + const r2 = (performance.now() - e) / 1e3, t2 = Math.floor(r2 / 60), o = Math.floor(r2 % 60); + return t2 > 0 ? `[${t2}m ${o}s]` : `[${o}s]`; + }, N = I2.withGuide ?? settings.withGuide, P = (e = "") => { + d = true, M = block({ output: n3 }), s = C2(e), w = performance.now(), N && n3.write(`${styleText2("gray", S_BAR)} +`); + let r2 = 0, t2 = 0; + A(), T = setInterval(() => { + if (u3 && s === p) + return; + y2(), p = s; + const o = k(E[r2]); + let v; + if (u3) + v = `${o} ${s}...`; + else if (l2 === "timer") + v = `${o} ${s} ${_(w)}`; + else { + const B = ".".repeat(Math.floor(t2)).slice(0, 3); + v = `${o} ${s}${B}`; + } + const j = wrapAnsi(v, x, { + hard: true, + trim: false + }); + n3.write(j), r2 = r2 + 1 < E.length ? r2 + 1 : 0, t2 = t2 < 4 ? t2 + 0.125 : 0; + }, F); + }, a2 = (e = "", r2 = 0, t2 = false) => { + if (!d) return; + d = false, clearInterval(T), y2(); + const o = r2 === 0 ? styleText2("green", S_STEP_SUBMIT) : r2 === 1 ? styleText2("red", S_STEP_CANCEL) : styleText2("red", S_STEP_ERROR); + s = e ?? s, t2 || (l2 === "timer" ? n3.write(`${o} ${s} ${_(w)} +`) : n3.write(`${o} ${s} +`)), H(), M(); + }; + return { + start: P, + stop: (e = "") => a2(e, 0), + message: (e = "") => { + s = C2(e ?? s); + }, + cancel: (e = "") => a2(e, 1), + error: (e = "") => a2(e, 2), + clear: () => a2("", 0, true), + get isCancelled() { + return S; + } + }; +}; +var u2 = { + light: unicodeOr("\u2500", "-"), + heavy: unicodeOr("\u2501", "="), + block: unicodeOr("\u2588", "#") +}; +var SELECT_INSTRUCTIONS = [ + `${styleText2("dim", "\u2191/\u2193")} to navigate`, + `${styleText2("dim", "Enter:")} confirm` +]; +var c = (t2, o) => t2.includes(` +`) ? t2.split(` +`).map((d) => o(d)).join(` +`) : o(t2); +var select = (t2) => { + const o = (n3, m2) => { + if (n3 === void 0) + return ""; + const s = n3.label ?? String(n3.value); + switch (m2) { + case "disabled": + return `${styleText2("gray", S_RADIO_INACTIVE)} ${c(s, (i2) => styleText2("gray", i2))}${n3.hint ? ` ${styleText2("dim", `(${n3.hint ?? "disabled"})`)}` : ""}`; + case "selected": + return `${c(s, (i2) => styleText2("dim", i2))}`; + case "active": + return `${styleText2("green", S_RADIO_ACTIVE)} ${s}${n3.hint ? ` ${styleText2("dim", `(${n3.hint})`)}` : ""}`; + case "cancelled": + return `${c(s, (i2) => styleText2(["strikethrough", "dim"], i2))}`; + default: + return `${styleText2("dim", S_RADIO_INACTIVE)} ${c(s, (i2) => styleText2("dim", i2))}`; + } + }, d = t2.showInstructions ?? true; + return new n$1({ + options: t2.options, + signal: t2.signal, + input: t2.input, + output: t2.output, + initialValue: t2.initialValue, + render() { + const n3 = t2.withGuide ?? settings.withGuide, m2 = `${symbol(this.state)} `, s = `${symbolBar(this.state)} `, i2 = wrapTextWithPrefix( + t2.output, + t2.message, + s, + m2 + ), u3 = `${n3 ? `${styleText2("gray", S_BAR)} +` : ""}${i2} +`; + switch (this.state) { + case "submit": { + const r2 = n3 ? `${styleText2("gray", S_BAR)} ` : "", a2 = wrapTextWithPrefix( + t2.output, + o(this.options[this.cursor], "selected"), + r2 + ); + return `${u3}${a2}`; + } + case "cancel": { + const r2 = n3 ? `${styleText2("gray", S_BAR)} ` : "", a2 = wrapTextWithPrefix( + t2.output, + o(this.options[this.cursor], "cancelled"), + r2 + ); + return `${u3}${a2}${n3 ? ` +${styleText2("gray", S_BAR)}` : ""}`; + } + default: { + const r2 = n3 ? `${styleText2("cyan", S_BAR)} ` : "", a2 = u3.split(` +`).length, p = d ? formatInstructionFooter(SELECT_INSTRUCTIONS, n3) : n3 ? [styleText2("cyan", S_BAR_END)] : [], f = p.join(` +`), b = p.length + 1; + return `${u3}${r2}${limitOptions({ + output: t2.output, + cursor: this.cursor, + options: this.options, + maxItems: t2.maxItems, + columnPadding: r2.length, + rowPadding: a2 + b, + style: (g, x) => o(g, g.disabled ? "disabled" : x ? "active" : "inactive") + }).join(` +${r2}`)} +${f} +`; + } + } + } + }).prompt(); +}; +var i = `${styleText2("gray", S_BAR)} `; +var text = (t2) => new n2({ + validate: t2.validate, + placeholder: t2.placeholder, + defaultValue: t2.defaultValue, + initialValue: t2.initialValue, + output: t2.output, + signal: t2.signal, + input: t2.input, + render() { + const r2 = t2?.withGuide ?? settings.withGuide, l2 = `${`${r2 ? `${styleText2("gray", S_BAR)} +` : ""}${symbol(this.state)} `}${t2.message} +`, d = t2.placeholder && t2.placeholder.length > 0 ? ( + // biome-ignore lint/style/noNonNullAssertion: guarded by placeholder.length > 0 + styleText2("inverse", t2.placeholder[0]) + styleText2("dim", t2.placeholder.slice(1)) + ) : styleText2(["inverse", "hidden"], "_"), o = this.userInput ? this.userInputWithCursor : d, s = this.value ?? ""; + switch (this.state) { + case "validating": { + const n3 = r2 ? `${styleText2("cyan", S_BAR)} ` : "", i2 = r2 ? styleText2("cyan", S_BAR_END) : "", c2 = styleText2("dim", o), $ = styleText2("dim", "Validating..."); + return `${l2}${n3}${c2} +${i2} ${$} +`; + } + case "error": { + const n3 = this.error ? ` ${styleText2("yellow", this.error)}` : "", i2 = r2 ? `${styleText2("yellow", S_BAR)} ` : "", c2 = r2 ? styleText2("yellow", S_BAR_END) : ""; + return `${l2.trim()} +${i2}${o} +${c2}${n3} +`; + } + case "submit": { + const n3 = s ? `${r2 ? " " : ""}${styleText2("dim", s)}` : "", i2 = r2 ? styleText2("gray", S_BAR) : ""; + return `${l2}${i2}${n3}`; + } + case "cancel": { + const n3 = s ? ` ${styleText2(["strikethrough", "dim"], s)}` : "", i2 = r2 ? styleText2("gray", S_BAR) : ""; + return `${l2}${i2}${n3}${s.trim() ? ` +${i2}` : ""}`; + } + default: { + const n3 = r2 ? `${styleText2("cyan", S_BAR)} ` : "", i2 = r2 ? styleText2("cyan", S_BAR_END) : ""; + return `${l2}${n3}${o} +${i2} +`; + } + } + } +}).prompt(); + +// src/prompt.ts +var CancelledError = class extends Error { + constructor() { + super("Cancelled."); + this.name = "CancelledError"; + } +}; + +// src/clack-prompter.ts +function clackPrompter(input, output) { + return { + async ask(question, defaultValue, example) { + const answer = await text({ message: question, defaultValue, placeholder: defaultValue ?? example, input, output }); + return unwrap(answer).trim() || defaultValue || ""; + }, + async confirm(question, defaultValue) { + return unwrap(await confirm({ message: question, initialValue: defaultValue, input, output })); + }, + async checklist(question, items) { + const selected = unwrap( + await multiselect({ + message: question, + options: items.map((item, position) => ({ value: position, label: item.label, hint: item.hint })), + initialValues: items.flatMap((item, position) => item.checked ? [position] : []), + required: false, + input, + output + }) + ); + return selected.map((position) => items[position].value); + }, + async choose(question, options) { + const index = unwrap( + await select({ + message: question, + options: options.map((option, position) => ({ value: position, label: option.label, hint: option.hint })), + input, + output + }) + ); + return options[index].value; + } + }; +} +function unwrap(answer) { + if (isCancel(answer)) throw new CancelledError(); + return answer; +} + +// src/api.ts +var ApiError = class extends Error { + constructor(status, errorCode, message, body) { + super(message); + this.status = status; + this.errorCode = errorCode; + this.body = body; + this.name = "ApiError"; + } + status; + errorCode; + body; +}; +function createApiClient(baseUrl, token, fetchImpl = fetch) { + const root = `${baseUrl.replace(/\/$/, "")}/api/v2/`; + return { + async request(method, path, body) { + const headers = { authorization: `Bearer ${token}`, accept: "application/json" }; + const init = { method, headers }; + if (body !== void 0) { + headers["content-type"] = "application/json"; + init.body = JSON.stringify(body); + } + const response = await fetchImpl(root + path, init); + const text2 = await response.text(); + const parsed = text2 ? safeJson(text2) : void 0; + if (!response.ok) { + throw toApiError(response.status, parsed); + } + return parsed; + } + }; +} +function toApiError(status, body) { + const record = isRecord(body) ? body : {}; + const message = typeof record.message === "string" ? record.message : `HTTP ${status}`; + const errorCode = typeof record.errorCode === "string" ? record.errorCode : void 0; + return new ApiError(status, errorCode, message, body); +} +function listItems(body, key) { + if (Array.isArray(body)) return body; + if (isRecord(body) && Array.isArray(body[key])) return body[key]; + return []; +} +var PAGE_SIZE = 100; +var MAX_PAGES = 100; +async function listAll(api, path, key) { + const separator = path.includes("?") ? "&" : "?"; + const items = []; + for (let page = 0; page < MAX_PAGES; page += 1) { + const batch = listItems(await api.request("GET", `${path}${separator}page=${page}&per_page=${PAGE_SIZE}`), key); + items.push(...batch); + if (batch.length < PAGE_SIZE) return items; + } + throw new Error(`${path} returned more than ${MAX_PAGES * PAGE_SIZE} items. The listing did not stop paging.`); +} +async function listAllByCheckpoint(api, path, key) { + const separator = path.includes("?") ? "&" : "?"; + const items = []; + let from; + for (let page = 0; page < MAX_PAGES; page += 1) { + const query = `take=${PAGE_SIZE}${from ? `&from=${encodeURIComponent(from)}` : ""}`; + const body = await api.request("GET", `${path}${separator}${query}`); + items.push(...listItems(body, key)); + from = isRecord(body) && typeof body.next === "string" && body.next !== "" ? body.next : void 0; + if (!from) return items; + } + throw new Error(`${path} returned more than ${MAX_PAGES * PAGE_SIZE} items. The listing did not stop paging.`); +} +function safeJson(text2) { + try { + return JSON.parse(text2); + } catch { + return text2; + } +} +function isRecord(value) { + return typeof value === "object" && value !== null; +} + +// src/cli-api.ts +import { spawn } from "node:child_process"; +var DEFAULT_CONCURRENCY = 4; +var INSTALL_URL = "https://github.com/auth0/auth0-cli#installation"; +var SIGN_IN_PROBLEM = /auth token expired|login session has expired|token is invalid|corrupted authentication token|failed to find tenant|config\.json file is missing/i; +function createCliApiClient(options) { + const { bin, tenant, run = runCommand } = options; + const gate = createGate(options.concurrency ?? DEFAULT_CONCURRENCY); + return { + async request(method, path, body) { + await gate.enter(); + try { + const args = ["api", method, path.replace(/^\//, ""), "--no-color", "--no-input"]; + if (tenant) args.push("--tenant", tenant); + if (method === "DELETE") args.push("--force"); + const what = `auth0 api ${method} ${path}`; + const result = await runOrExplain(run, bin, args, body === void 0 ? void 0 : JSON.stringify(body)); + if (result.exitCode !== 0) throw explainFailure(result.stderr, what, tenant); + return parseOutput(result.stdout, what); + } finally { + gate.leave(); + } + } + }; +} +function createGate(concurrency) { + let limit = 1; + let active = 0; + let firstFinished = false; + const waiting = []; + const admit = () => { + while (active < limit && waiting.length > 0) { + active += 1; + waiting.shift()?.(); + } + }; + return { + enter: () => new Promise((resolve) => { + waiting.push(resolve); + admit(); + }), + leave: () => { + active -= 1; + if (!firstFinished) { + firstFinished = true; + limit = concurrency; + } + admit(); + } + }; +} +async function runOrExplain(run, bin, args, input) { + try { + return await run(bin, args, input); + } catch (error) { + if (error.code === "ENOENT") { + throw new PluginError( + `The Auth0 CLI was not found (tried "${bin}"). Install it from ${INSTALL_URL}, or set AUTH0_CLI_BIN to its path.`, + "auth0_cli_not_found" + ); + } + throw error; + } +} +function parseOutput(stdout2, what) { + const text2 = stdout2.trim(); + if (text2 === "") return void 0; + try { + return JSON.parse(text2); + } catch { + throw new Error(`${what} printed output that is not JSON: ${text2.slice(0, 120)}`); + } +} +function explainFailure(stderr, what, tenant) { + const api = /(? entry.trim()).find((entry) => entry !== "" && !entry.startsWith("===")); + return (line ?? "no message").replace(/^[^A-Za-z0-9]+/, ""); +} +async function findActiveTenant(bin, run = runCommand) { + const what = "auth0 tenants list"; + const result = await runOrExplain(run, bin, ["tenants", "list", "--json", "--no-color", "--no-input"], void 0); + if (result.exitCode !== 0) throw explainFailure(result.stderr, what, void 0); + let tenants; + try { + tenants = parseOutput(result.stdout, what); + } catch { + tenants = void 0; + } + const active = Array.isArray(tenants) ? tenants.find((tenant) => tenant?.active === true) : void 0; + if (typeof active?.name !== "string") throw loginRequired("The Auth0 CLI has no active tenant."); + return active.name; +} +function runCommand(bin, args, input) { + return new Promise((resolve, reject) => { + const child = spawn(bin, args, { stdio: ["pipe", "pipe", "pipe"] }); + let stdout2 = ""; + let stderr = ""; + child.stdout.setEncoding("utf8").on("data", (chunk) => stdout2 += chunk); + child.stderr.setEncoding("utf8").on("data", (chunk) => stderr += chunk); + child.on("error", reject); + child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout: stdout2, stderr })); + child.stdin.on("error", () => void 0); + child.stdin.end(input ?? ""); + }); +} + +// src/context.ts +function checkTenantFlag(requested, context, consequence = "") { + if (requested && requested !== context.tenantDomain) { + throw new UsageError( + `--tenant ${requested} does not match the tenant the CLI is running against (${context.tenantDomain}).${consequence}` + ); + } +} +async function readContext(env, requestedTenant, run = runCommand) { + const proxyUrl = env.AUTH0_CLI_PROXY_URL; + const proxyToken = env.AUTH0_CLI_PROXY_TOKEN; + if (proxyUrl || proxyToken) { + const tenantDomain2 = env.AUTH0_CLI_TENANT_DOMAIN; + const missing = [ + ["AUTH0_CLI_PROXY_URL", proxyUrl], + ["AUTH0_CLI_PROXY_TOKEN", proxyToken], + ["AUTH0_CLI_TENANT_DOMAIN", tenantDomain2] + ].filter(([, value]) => !value).map(([name]) => name); + if (missing.length > 0 || !proxyUrl || !proxyToken || !tenantDomain2) { + throw new Error( + `The Auth0 CLI started this command with an incomplete environment. Missing: ${missing.join(", ")}` + ); + } + return { tenantDomain: tenantDomain2, transport: { kind: "proxy", url: proxyUrl, token: proxyToken } }; + } + const bin = env.AUTH0_CLI_BIN || "auth0"; + const tenantDomain = requestedTenant || env.AUTH0_CLI_TENANT_DOMAIN || await findActiveTenant(bin, run); + return { tenantDomain, transport: { kind: "cli", bin } }; +} +function apiFor(context, options = {}) { + const { transport } = context; + if (transport.kind === "proxy") return createApiClient(transport.url, transport.token, options.fetchImpl); + return createCliApiClient({ bin: transport.bin, tenant: context.tenantDomain, run: options.run }); +} + +// src/templates/icons.ts +var SIDEBAR_ICONS = { + profile: "user", + security: "shield-check", + legalPrivacy: "file-text", + organization: "building-2", + domains: "globe", + preferences: "sliders-horizontal" +}; + +// src/templates/security.ts +var SECURITY_PAGE = { + title: "Security", + slug: "security", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Multi-factor authentication", + description: "Add an extra layer of protection to your account by requiring a second verification step each time you sign in.", + variant: "card", + children: [{ type: "page:component:auth0:my_account:mfa_management" }] + } + }, + { + type: "page:component:auth0:structure:section", + config: { + title: "Passkeys", + description: "Use your fingerprint, face, or screen lock instead of a password to sign in quickly and more securely.", + variant: "card", + children: [{ type: "page:component:auth0:my_account:passkey_management" }] + } + } + ] +}; +var SECURITY_SIDEBAR_ICON = SIDEBAR_ICONS.security; + +// src/templates/business.ts +function businessTemplate(forms) { + const pages = [ + { + title: "Organization", + slug: "organization", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Organization details", + description: "Update your organization's name and other details visible to its members.", + variant: "card", + children: [{ type: "page:component:auth0:my_organization:details_edit" }] + } + } + ] + }, + { + title: "Domains", + slug: "domains", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Verified domains", + description: "Add and verify the domains that your organization members sign in with.", + variant: "card", + children: [{ type: "page:component:auth0:my_organization:domain_table" }] + } + } + ] + }, + SECURITY_PAGE, + { + title: "Preferences", + slug: "preferences", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Personal information", + description: "Basic info about you, like your name and contact details, that you use across services.", + variant: "card", + children: [ + { + type: "page:component:auth0:form", + config: { + form_id: forms.personalInfo, + completion_message: "Your personal information has been updated." + } + } + ] + } + }, + { + type: "page:component:auth0:structure:section", + config: { + title: "Communication preferences", + description: "Choose which emails and notifications you'd like to receive from us.", + variant: "card", + children: [ + { + type: "page:component:auth0:form", + config: { + form_id: forms.communicationPreferences, + completion_message: "Your communication preferences have been updated." + } + } + ] + } + } + ] + } + ]; + const sidebar = [ + link("Organization", "organization", SIDEBAR_ICONS.organization), + link("Domains", "domains", SIDEBAR_ICONS.domains), + link("Security", "security", SECURITY_SIDEBAR_ICON), + link("Preferences", "preferences", SIDEBAR_ICONS.preferences) + ]; + return { + id: "business", + apis: ["my-organization", "my-account"], + defaultPage: "organization", + sidebar, + pages + }; +} +function link(label, pageSlug, icon) { + return { type: "sidebar:component:auth0:internal_link:v2", config: { label, page_slug: pageSlug, icon } }; +} + +// src/templates/consumer.ts +function consumerTemplate(forms) { + const pages = [ + { + title: "Profile", + slug: "profile", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Personal information", + description: "Basic info about you, like your name and contact details, that you use across services.", + variant: "card", + children: [ + { + type: "page:component:auth0:form", + config: { + form_id: forms.personalInfo, + completion_message: "Your personal information has been updated." + } + } + ] + } + } + ] + }, + SECURITY_PAGE, + { + title: "Legal & privacy", + slug: "legal-privacy", + components: [ + { + type: "page:component:auth0:structure:section", + config: { + title: "Privacy & data consent", + description: "Control how your personal data is collected and used across our services.", + variant: "card", + children: [ + { + type: "page:component:auth0:form", + config: { + form_id: forms.privacyConsent, + completion_message: "Your privacy preferences have been saved." + } + } + ] + } + }, + { + type: "page:component:auth0:structure:section", + config: { + title: "Communication preferences", + description: "Choose which emails and notifications you'd like to receive from us.", + variant: "card", + children: [ + { + type: "page:component:auth0:form", + config: { + form_id: forms.communicationPreferences, + completion_message: "Your communication preferences have been updated." + } + } + ] + } + } + ] + } + ]; + const sidebar = [ + link2("Profile", "profile", SIDEBAR_ICONS.profile), + link2("Security", "security", SECURITY_SIDEBAR_ICON), + link2("Legal & privacy", "legal-privacy", SIDEBAR_ICONS.legalPrivacy) + ]; + return { + id: "consumer", + apis: ["my-account"], + defaultPage: "profile", + sidebar, + pages + }; +} +function link2(label, pageSlug, icon) { + return { type: "sidebar:component:auth0:internal_link:v2", config: { label, page_slug: pageSlug, icon } }; +} + +// src/setup/payload.ts +var MY_ACCOUNT_SCOPES = [ + "read:me:factors", + "read:me:authentication_methods", + "create:me:authentication_methods", + "delete:me:authentication_methods" +]; +var MY_ORGANIZATION_SCOPES = [ + "read:my_org:details", + "update:my_org:details", + "read:my_org:configuration", + "read:my_org:domains", + "create:my_org:domains", + "update:my_org:domains", + "delete:my_org:domains", + "read:my_org:identity_providers", + "create:my_org:identity_providers_domains", + "delete:my_org:identity_providers_domains" +]; +var FORMS_VAULT_SCOPES = ["update:users", "update:users_app_metadata"]; +var PORTAL_APP_SUFFIX = "(Universal Portals)"; +var PORTAL_GRANT_TYPES = [ + "authorization_code", + "refresh_token", + "http://auth0.com/oauth/grant-type/mfa-oob", + "http://auth0.com/oauth/grant-type/mfa-otp", + "http://auth0.com/oauth/grant-type/mfa-recovery-code" +]; +var REFRESH_TOKEN_LIFETIMES = { + idleSeconds: 3600, + absoluteSeconds: 86400 +}; +function myAccountAudience(tenantDomain) { + return `https://${tenantDomain}/me/`; +} +function myOrganizationAudience(tenantDomain) { + return `https://${tenantDomain}/my-org/`; +} +function managementAudience(tenantDomain) { + return `https://${tenantDomain}/api/v2/`; +} +function audiencesFor(template, tenantDomain) { + const account = { audience: myAccountAudience(tenantDomain), scope: MY_ACCOUNT_SCOPES }; + if (template === "consumer") return [account]; + return [{ audience: myOrganizationAudience(tenantDomain), scope: MY_ORGANIZATION_SCOPES }, account]; +} +function buildPortalClient(input) { + const client = { + name: input.name, + app_type: "regular_web", + is_first_party: true, + oidc_conformant: true, + token_endpoint_auth_method: "client_secret_post", + is_token_endpoint_ip_header_trusted: true, + callbacks: [`https://${input.portalHost}/portals/auth/callback`], + // Users land on the portal's own host after logout. + allowed_logout_urls: [`https://${input.portalHost}`], + grant_types: PORTAL_GRANT_TYPES, + organization_usage: "allow", + organization_require_behavior: "no_prompt", + refresh_token: refreshTokenSettings(input.template, input.tenantDomain), + client_metadata: { portals_setup_slug: input.slug }, + // Lets a native app open the portal with a session transfer token. + session_transfer: { allowed_authentication_methods: ["query"], enforce_device_binding: "ip" } + }; + if (input.isCustomDomain) { + client.oidc_logout = backChannelLogout(input.portalHost); + } + return client; +} +function refreshTokenSettings(template, tenantDomain) { + return { + rotation_type: "non-rotating", + expiration_type: "expiring", + token_lifetime: REFRESH_TOKEN_LIFETIMES.absoluteSeconds, + idle_token_lifetime: REFRESH_TOKEN_LIFETIMES.idleSeconds, + policies: audiencesFor(template, tenantDomain).map((entry) => ({ + audience: entry.audience, + scope: entry.scope + })) + }; +} +function backChannelLogout(portalHost) { + return { + backchannel_logout_urls: [`https://${portalHost}/portals/auth/backchannel-logout`], + backchannel_logout_initiators: { + mode: "custom", + selected_initiators: [ + "idp-logout", + "rp-logout", + "session-expired", + "session-revoked", + "account-deleted", + "account-deactivated" + ] + } + }; +} +function buildFormsVaultClient(name) { + return { + name, + app_type: "non_interactive", + client_metadata: { auth0_portals_role: "forms-vault" } + }; +} +function buildPortalRequest(template, input) { + return { + slug: input.slug, + name: input.name, + client: { + token_endpoint_auth_method: "client_secret_post", + client_id: input.clientId, + client_secret: input.clientSecret + }, + navigation: { sidebar: { components: template.sidebar } }, + pages: { default: template.defaultPage, standard: template.pages } + }; +} +function templateFor(id, forms) { + return id === "consumer" ? consumerTemplate(forms) : businessTemplate(forms); +} + +// src/setup/resources.ts +function describe(status) { + return status === "reused" ? "already exists" : status === "updated" ? "updated" : "created"; +} +var NameInUseError = class extends Error { + constructor(name) { + super( + `An application named "${name}" already exists and was not created by this setup. Rename or delete it, then rerun.` + ); + this.name = name; + this.name = "NameInUseError"; + } + name; +}; +var SlugTakenError = class extends Error { + constructor(slug) { + super(`The portal slug "${slug}" is already used by another portal.`); + this.slug = slug; + this.name = "SlugTakenError"; + } + slug; +}; +async function ensureResourceServer(api, input) { + const existing = await findResourceServer(api, input.identifier); + if (existing) return { value: { id: existing.id }, status: "reused" }; + try { + const created = await api.request("POST", "resource-servers", { + name: input.name, + identifier: input.identifier, + skip_consent_for_verifiable_first_party_clients: true, + token_dialect: "rfc9068_profile" + }); + return { value: { id: created.id }, status: "created" }; + } catch (error) { + if (error instanceof ApiError && error.status === 409) { + const raced = await findResourceServer(api, input.identifier); + if (raced) return { value: { id: raced.id }, status: "reused" }; + } + throw error; + } +} +async function findResourceServer(api, identifier) { + return (await listAll(api, "resource-servers", "resource_servers")).find((server) => server.identifier === identifier); +} +async function ensurePortalClient(api, input) { + const existing = await findClientByName(api, input.name); + const payload = buildPortalClient(input); + if (existing) { + if (existing.client_metadata?.portals_setup_slug !== input.slug) { + throw new NameInUseError(input.name); + } + await api.request("PATCH", `clients/${existing.client_id}`, { + callbacks: payload.callbacks, + allowed_logout_urls: payload.allowed_logout_urls, + grant_types: payload.grant_types, + organization_usage: payload.organization_usage, + organization_require_behavior: payload.organization_require_behavior, + is_token_endpoint_ip_header_trusted: payload.is_token_endpoint_ip_header_trusted, + refresh_token: payload.refresh_token, + ...payload.oidc_logout ? { oidc_logout: payload.oidc_logout } : {}, + // Set only when missing, so a session transfer setting changed on purpose survives a rerun. + ...existing.session_transfer?.allowed_authentication_methods?.length ? {} : { session_transfer: payload.session_transfer } + }); + const clientSecret = await readClientSecret(api, existing.client_id); + return { value: { clientId: existing.client_id, clientSecret }, status: "reused" }; + } + const created = await api.request("POST", "clients", payload); + return { value: { clientId: created.client_id, clientSecret: created.client_secret }, status: "created" }; +} +async function findClientByName(api, name) { + const clients = await listAll( + api, + "clients?fields=client_id,name,client_metadata,session_transfer&include_fields=true", + "clients" + ); + return clients.find((client) => client.name === name); +} +async function readClientSecret(api, clientId) { + const client = await api.request( + "GET", + `clients/${encodeURIComponent(clientId)}?fields=client_id,client_secret&include_fields=true` + ); + if (!client.client_secret) { + throw new Error(`Could not read the client secret for ${clientId}. The session needs read:client_keys.`); + } + return client.client_secret; +} +async function ensureClientGrant(api, input) { + const existing = (await listAll(api, `client-grants?client_id=${encodeURIComponent(input.clientId)}`, "client_grants")).find((grant) => grant.audience === input.audience); + if (!existing) { + const created = await api.request("POST", "client-grants", { + client_id: input.clientId, + audience: input.audience, + scope: input.scope, + ...input.subjectType ? { subject_type: input.subjectType } : {} + }); + return { value: { id: created.id }, status: "created" }; + } + if (sameScopes(existing.scope ?? [], input.scope)) { + return { value: { id: existing.id }, status: "reused" }; + } + await api.request("PATCH", `client-grants/${existing.id}`, { scope: input.scope }); + return { value: { id: existing.id }, status: "updated" }; +} +function sameScopes(left, right) { + const a2 = [...new Set(left)].sort(); + const b = [...new Set(right)].sort(); + return a2.length === b.length && a2.every((scope, index) => scope === b[index]); +} +async function ensureFormsVaultClient(api, name) { + const existing = await findClientByName(api, name); + if (existing) { + if (existing.client_metadata?.auth0_portals_role !== "forms-vault") { + throw new NameInUseError(name); + } + const clientSecret = await readClientSecret(api, existing.client_id); + return { value: { clientId: existing.client_id, clientSecret }, status: "reused" }; + } + const created = await api.request( + "POST", + "clients", + buildFormsVaultClient(name) + ); + return { value: { clientId: created.client_id, clientSecret: created.client_secret }, status: "created" }; +} +function vaultSetupPayload(setup) { + return { + domain: setup.domain, + client_id: setup.clientId, + client_secret: setup.clientSecret, + type: "OAUTH_APP" + }; +} +async function ensureVaultConnection(api, name, setup) { + const existing = (await listAll(api, "flows/vault/connections", "connections")).find((connection) => connection.name === name); + if (existing) return { value: { id: existing.id }, status: "reused" }; + const created = await api.request("POST", "flows/vault/connections", { + app_id: "AUTH0", + name, + setup: vaultSetupPayload(setup) + }); + return { value: { id: created.id }, status: "created" }; +} +async function refreshVaultConnection(api, vaultId, setup) { + await api.request("PATCH", `flows/vault/connections/${encodeURIComponent(vaultId)}`, { + setup: vaultSetupPayload(setup) + }); +} +function flowIdFromImport(bundle) { + return Object.values(bundle.flows ?? {}).find((flow) => flow.id)?.id; +} +function flowIdFromNodes(form) { + return form.nodes?.find((node) => node.type === "FLOW" && node.config?.flow_id)?.config?.flow_id; +} +async function ensureForms(api, forms) { + const existing = await listAll(api, "forms", "forms"); + const result = {}; + for (const form of forms) { + const match = existing.find((candidate) => candidate.name === form.name); + if (match) { + const detail = await api.request( + "GET", + `forms/${encodeURIComponent(match.id)}` + ); + result[form.key] = { value: { id: match.id, flowId: flowIdFromNodes(detail) }, status: "reused" }; + continue; + } + const imported = await api.request("POST", "forms/import", form.document); + const flowId = flowIdFromImport(imported) ?? flowIdFromNodes(imported.form); + result[form.key] = { value: { id: imported.form.id, flowId }, status: "created" }; + } + return result; +} +async function findPortalBySlug(api, slug) { + const listed = (await listAll(api, "portals", "portals")).find((portal) => portal.slug === slug); + if (!listed) return void 0; + return api.request("GET", `portals/${encodeURIComponent(listed.id)}`); +} +async function ensurePortal(api, request) { + const existing = await findPortalBySlug(api, request.slug); + if (existing) { + if (existing.client?.client_id !== request.client.client_id) { + throw new SlugTakenError(request.slug); + } + return { value: { id: existing.id, slug: existing.slug }, status: "reused" }; + } + try { + const created = await api.request("POST", "portals", request); + return { value: { id: created.id, slug: created.slug }, status: "created" }; + } catch (error) { + if (error instanceof ApiError && error.status === 409) throw new SlugTakenError(request.slug); + throw error; + } +} + +// src/audit/snapshot.ts +async function fetchPortalSnapshot(api, portalId, tenantDomain, options = {}) { + const portal = await api.request("GET", `portals/${encodeURIComponent(portalId)}`); + const [applicationParts, resourceServers, forms, customDomain, organization] = await Promise.all([ + readApplicationParts(api, portal.client?.client_id), + listAll(api, "resource-servers", "resource_servers").then( + (servers) => servers.map((server) => server.identifier) + ), + readForms(api, portal), + readCustomDomain(api), + options.organizationId ? readOrganization(api, options.organizationId) : void 0 + ]); + const { application, grants, applicationConnections } = applicationParts; + return { + tenantDomain, + portal, + application, + grants, + resourceServers, + forms, + applicationConnections, + customDomain, + organization + }; +} +async function readApplicationParts(api, clientId) { + const application = clientId ? await readApplication(api, clientId) : void 0; + if (!application || !clientId) return { application, grants: [], applicationConnections: [] }; + const [grants, applicationConnections] = await Promise.all([ + listAll(api, `client-grants?client_id=${encodeURIComponent(clientId)}`, "client_grants"), + // This endpoint pages by checkpoint and rejects page and per_page. + listAllByCheckpoint(api, `clients/${encodeURIComponent(clientId)}/connections`, "connections") + ]); + return { application, grants, applicationConnections }; +} +async function readForms(api, portal) { + const formIds = formIdsOf(portal); + const exist = await Promise.all(formIds.map((formId) => formExists(api, formId))); + return Object.fromEntries(formIds.map((formId, index) => [formId, exist[index]])); +} +function formIdsOf(portal) { + const ids = []; + const visit = (components = []) => { + for (const component of components) { + if (component.type === "page:component:auth0:form" && component.config?.form_id) { + ids.push(component.config.form_id); + } + visit(component.config?.children); + } + }; + for (const page of portal.pages?.standard ?? []) visit(page.components); + return [...new Set(ids)]; +} +async function readApplication(api, clientId) { + try { + return await api.request("GET", `clients/${encodeURIComponent(clientId)}`); + } catch (error) { + if (error instanceof ApiError && error.status === 404) return void 0; + throw error; + } +} +async function formExists(api, formId) { + try { + await api.request("GET", `forms/${encodeURIComponent(formId)}`); + return true; + } catch (error) { + if (error instanceof ApiError && (error.status === 404 || error.status === 400)) return false; + throw error; + } +} +async function readCustomDomain(api) { + try { + const custom = await api.request("GET", "custom-domains/default"); + return custom; + } catch (error) { + if (error instanceof ApiError && error.status === 404) return void 0; + throw error; + } +} +async function readOrganization(api, organizationId) { + const org = await api.request("GET", `organizations/${encodeURIComponent(organizationId)}`); + const connections = (await listAll(api, `organizations/${encodeURIComponent(organizationId)}/enabled_connections`, "enabled_connections")).map((connection) => connection.connection_id); + return { id: org.id, name: org.name, connections }; +} +function hostOf(snapshot) { + return snapshot.customDomain?.status === "ready" ? snapshot.customDomain.domain : snapshot.tenantDomain; +} + +// src/audit/remedies.ts +function isLegacyLink(item) { + return !item.type.endsWith(":v2") || item.config?.to !== void 0 || item.config?.url !== void 0; +} +function toV2Link(item) { + if (!isLegacyLink(item)) return item; + const external = item.config?.url !== void 0 || item.config?.href !== void 0 || item.type.includes("external"); + const config = { label: item.config?.label, icon: item.config?.icon }; + if (external) { + config.href = item.config?.href ?? item.config?.url; + return { type: "sidebar:component:auth0:external_link:v2", config }; + } + config.page_slug = item.config?.page_slug ?? item.config?.to; + return { type: "sidebar:component:auth0:internal_link:v2", config }; +} +async function createPortalApplication(api, snapshot, usesOrganization) { + const client = await ensurePortalClient(api, { + name: `${snapshot.portal.name ?? snapshot.portal.slug} ${PORTAL_APP_SUFFIX}`, + slug: snapshot.portal.slug, + template: usesOrganization ? "business" : "consumer", + tenantDomain: snapshot.tenantDomain, + portalHost: hostOf(snapshot), + isCustomDomain: snapshot.customDomain?.status === "ready" + }); + return api.request("PATCH", `portals/${encodeURIComponent(snapshot.portal.id)}`, { + client: { + client_id: client.value.clientId, + client_secret: client.value.clientSecret, + token_endpoint_auth_method: "client_secret_post" + } + }); +} +function patchRefresh(api, app, changes) { + const current = app.refresh_token ?? {}; + return api.request("PATCH", `clients/${encodeURIComponent(app.client_id)}`, { + refresh_token: { + rotation_type: current.rotation_type ?? "non-rotating", + expiration_type: "expiring", + token_lifetime: current.token_lifetime ?? REFRESH_TOKEN_LIFETIMES.absoluteSeconds, + idle_token_lifetime: current.idle_token_lifetime ?? REFRESH_TOKEN_LIFETIMES.idleSeconds, + policies: current.policies ?? [], + ...changes + } + }); +} +function patchPages(api, portalId, snapshot, home) { + return api.request("PATCH", `portals/${encodeURIComponent(portalId)}`, { + pages: { default: home, standard: snapshot.portal.pages?.standard ?? [] } + }); +} +function addSidebarLink(api, portalId, snapshot, pageSlug, label, icon) { + const components = snapshot.portal.navigation?.sidebar?.components ?? []; + return api.request("PATCH", `portals/${encodeURIComponent(portalId)}`, { + navigation: { + sidebar: { + components: [ + ...components, + { type: "sidebar:component:auth0:internal_link:v2", config: { label, page_slug: pageSlug, icon } } + ] + } + } + }); +} + +// src/audit/checks.ts +function auditPortal(snapshot) { + const findings = []; + const add = (finding) => findings.push(finding); + const portalId = snapshot.portal.id; + const pages = snapshot.portal.pages?.standard ?? []; + const pageSlugs = new Set(pages.map((page) => page.slug)); + const sidebar = snapshot.portal.navigation?.sidebar?.components ?? []; + const sidebarPages = new Set(sidebar.map((item) => String(item.config?.page_slug ?? ""))); + const usedComponents = allComponents(pages); + const usesAccount = usedComponents.some((c2) => c2.type.includes("my_account")); + const usesOrganization = usedComponents.some((c2) => c2.type.includes("my_organization")); + const home = snapshot.portal.pages?.default; + if (!home || !pageSlugs.has(home)) { + const first = pages[0]; + add({ + id: "P001", + severity: "error", + check: "home page", + message: home ? `The home page "${home}" does not match any page, so users land on an error.` : "No home page is set, so users land on an error.", + remedy: first ? { + summary: `Set "${first.title}" as the home page`, + command: `auth0 api patch "portals/${portalId}" --data '{"pages":{"default":"${first.slug}","standard":[...]}}'`, + apply: (api) => patchPages(api, portalId, snapshot, first.slug) + } : { + summary: "Add a page and set it as the home page", + command: `auth0 api patch "portals/${portalId}" --data '{"pages":{"default":"","standard":[...]}}'` + } + }); + } else if (!sidebarPages.has(home)) { + const page = pages.find((candidate) => candidate.slug === home); + add({ + id: "P002", + severity: "warning", + check: "home page in navigation", + message: `The home page "${home}" is not in the sidebar, so users cannot get back to it.`, + remedy: { + summary: `Add "${page?.title ?? home}" to the sidebar`, + apply: (api) => addSidebarLink(api, portalId, snapshot, home, page?.title ?? home, "house") + } + }); + } + const legacyLinks = sidebar.filter(isLegacyLink); + if (legacyLinks.length > 0) { + add({ + id: "P003", + severity: "error", + check: "navigation shape", + message: `${legacyLinks.length} sidebar link(s) use the legacy shape.`, + remedy: { + summary: "Convert the sidebar links to the :v2 link types", + apply: (api) => api.request("PATCH", `portals/${encodeURIComponent(portalId)}`, { + navigation: { sidebar: { components: sidebar.map(toV2Link) } } + }) + } + }); + } + for (const item of sidebar) { + const target = item.config?.page_slug; + if (typeof target === "string" && !pageSlugs.has(target)) { + add({ + id: "P004", + subject: target, + severity: "error", + check: "sidebar link target", + message: `A sidebar link points to the page "${target}", which does not exist.`, + remedy: { summary: "Point the link at an existing page, or remove it" } + }); + } + } + for (const page of pages) { + if (!sidebarPages.has(page.slug)) { + add({ + id: "P005", + subject: page.slug, + severity: "warning", + check: "page in navigation", + message: `The page "${page.title}" is not in the sidebar, so users cannot reach it.`, + remedy: { + summary: `Add "${page.title}" to the sidebar`, + apply: (api) => addSidebarLink(api, portalId, snapshot, page.slug, page.title, "file") + } + }); + } + if ((page.components ?? []).length === 0) { + add({ + id: "P006", + subject: page.slug, + severity: "warning", + check: "empty page", + message: `The page "${page.title}" has no components.`, + remedy: { summary: "Add components to the page, or remove it" } + }); + } + } + for (const formId of Object.keys(snapshot.forms)) { + if (!snapshot.forms[formId]) { + add({ + id: "P007", + subject: formId, + severity: "error", + check: "form exists", + message: `A form on the portal (${formId}) does not exist in the tenant, so its component fails.`, + remedy: { summary: "Recreate the form, or remove the component from the page" } + }); + } + } + if (snapshot.portal.custom_code?.head && !snapshot.customDomain) { + add({ + id: "P008", + severity: "warning", + check: "custom code", + message: "Custom code is set, but the tenant has no ready custom domain, so it is never injected.", + remedy: { summary: "Set up a custom domain, or remove the custom code" } + }); + } + const app = snapshot.application; + if (!app) { + add({ + id: "P009", + severity: "error", + check: "application", + message: "The portal has no application linked to it.", + remedy: { + summary: "Create the portal application with the portal defaults and link it to the portal", + apply: (api) => createPortalApplication(api, snapshot, usesOrganization) + } + }); + return findings; + } + const clientId = app.client_id; + if (app.grant_types?.includes("client_credentials")) { + add({ + id: "P010", + severity: "error", + check: "client credentials", + message: "The portal application has the client_credentials grant, which the setup guide says must be off.", + remedy: { + summary: "Turn off the client_credentials grant", + apply: (api) => api.request("PATCH", `clients/${encodeURIComponent(clientId)}`, { + grant_types: (app.grant_types ?? []).filter((grant) => grant !== "client_credentials") + }) + } + }); + } + if (app.require_proof_of_possession) { + add({ + id: "P023", + severity: "error", + check: "DPoP", + message: "DPoP is enabled on the portal application. Portals are confidential clients and do not use DPoP.", + remedy: { + summary: "Turn DPoP off on the portal application", + apply: (api) => api.request("PATCH", `clients/${encodeURIComponent(clientId)}`, { require_proof_of_possession: false }) + } + }); + } + const managementGrants = snapshot.grants.filter((candidate) => candidate.audience.endsWith("/api/v2/")); + if (managementGrants.length > 0) { + const ids = managementGrants.map((grant) => grant.id).filter((id) => Boolean(id)); + add({ + id: "P011", + severity: "error", + check: "management api grant", + message: "The portal application has a Management API grant, which the setup guide says must not be there.", + remedy: { + summary: "Remove the Management API grant from the portal application", + apply: ids.length === managementGrants.length ? async (api) => { + for (const id of ids) await api.request("DELETE", `client-grants/${encodeURIComponent(id)}`); + } : void 0, + command: ids.length === managementGrants.length ? void 0 : 'auth0 api get "client-grants?client_id=' + clientId + '"' + } + }); + } + const callback = `https://${hostOf(snapshot)}/portals/auth/callback`; + if (!app.callbacks?.includes(callback)) { + add({ + id: "P012", + severity: "error", + check: "callback url", + message: `The callback URL ${callback} is missing, so sign-in to the portal fails.`, + remedy: { + summary: "Add the portal callback URL", + apply: (api) => api.request("PATCH", `clients/${encodeURIComponent(clientId)}`, { + callbacks: [...app.callbacks ?? [], callback] + }) + } + }); + } + const refresh = app.refresh_token; + if (refresh?.rotation_type && refresh.rotation_type !== "non-rotating") { + add({ + id: "P013", + severity: "error", + check: "refresh token rotation", + message: "Refresh token rotation is on, and portals do not support it. Components fail intermittently.", + remedy: { + summary: "Turn refresh token rotation off", + apply: (api) => patchRefresh(api, app, { rotation_type: "non-rotating" }) + } + }); + } + if (refresh?.infinite_idle_token_lifetime) { + add({ + id: "P014", + severity: "error", + check: "refresh token idle lifetime", + message: "The refresh token has no idle expiry, so the portal session never ends by inactivity.", + remedy: { + summary: `Set the idle lifetime to ${REFRESH_TOKEN_LIFETIMES.idleSeconds} seconds`, + apply: (api) => patchRefresh(api, app, { infinite_idle_token_lifetime: false, idle_token_lifetime: REFRESH_TOKEN_LIFETIMES.idleSeconds }) + } + }); + } else if (refresh && (refresh.idle_token_lifetime !== REFRESH_TOKEN_LIFETIMES.idleSeconds || refresh.token_lifetime !== REFRESH_TOKEN_LIFETIMES.absoluteSeconds)) { + add({ + id: "P015", + severity: "warning", + check: "refresh token lifetimes", + message: `The refresh token lifetimes differ from the guide (${REFRESH_TOKEN_LIFETIMES.idleSeconds}s idle, ${REFRESH_TOKEN_LIFETIMES.absoluteSeconds}s total).`, + remedy: { + summary: "Set the lifetimes to the guide's values", + apply: (api) => patchRefresh(api, app, { + idle_token_lifetime: REFRESH_TOKEN_LIFETIMES.idleSeconds, + token_lifetime: REFRESH_TOKEN_LIFETIMES.absoluteSeconds + }) + } + }); + } + if (app.organization_usage !== "allow" && usesOrganization) { + add({ + id: "P016", + severity: "error", + check: "organization usage", + message: "The portal has organization components, but the application does not allow organizations.", + remedy: { + summary: "Allow organizations on the application", + apply: (api) => api.request("PATCH", `clients/${encodeURIComponent(clientId)}`, { organization_usage: "allow" }) + } + }); + } + if (app.is_token_endpoint_ip_header_trusted === false) { + add({ + id: "P017", + severity: "warning", + check: "token endpoint IP header", + message: "The guide says to trust the token endpoint IP header, and it is off.", + remedy: { + summary: "Trust the token endpoint IP header", + apply: (api) => api.request("PATCH", `clients/${encodeURIComponent(clientId)}`, { is_token_endpoint_ip_header_trusted: true }) + } + }); + } + const needed = [ + { audience: myAccountAudience(snapshot.tenantDomain), name: "My Account API", scope: MY_ACCOUNT_SCOPES, used: usesAccount }, + { + audience: myOrganizationAudience(snapshot.tenantDomain), + name: "My Organization API", + scope: MY_ORGANIZATION_SCOPES, + used: usesOrganization + } + ]; + for (const api of needed.filter((entry) => entry.used)) { + const usedBy = componentsOn(usedComponents, api.name).map((component) => componentLabel(component)).join(", "); + if (!snapshot.resourceServers.includes(api.audience)) { + add({ + id: "P018", + subject: api.audience, + severity: "error", + check: "resource server", + message: `The components (${usedBy}) need the ${api.name}, but the tenant has no resource server for ${api.audience}.`, + remedy: { + summary: `Create the ${api.name} resource server`, + apply: (client) => client.request("POST", "resource-servers", { + name: `Auth0 ${api.name}`, + identifier: api.audience, + skip_consent_for_verifiable_first_party_clients: true, + token_dialect: "rfc9068_profile" + }) + } + }); + } + if (!snapshot.grants.some((grant) => grant.audience === api.audience)) { + add({ + id: "P019", + subject: api.audience, + severity: "error", + check: "client grant", + message: `The components (${usedBy}) need the ${api.name}, but the application has no client grant for it.`, + remedy: { + summary: `Grant the application access to the ${api.name}`, + apply: (client) => client.request("POST", "client-grants", { + client_id: clientId, + audience: api.audience, + scope: api.scope, + subject_type: "user" + }) + } + }); + } + if (!refresh?.policies?.some((policy) => policy.audience === api.audience)) { + add({ + id: "P020", + subject: api.audience, + severity: "error", + check: "refresh token policy", + message: `The components (${usedBy}) need the ${api.name}, but the refresh token has no policy for it, so the portal cannot get its tokens.`, + remedy: { + summary: `Add the ${api.name} to the multi-resource refresh token`, + apply: (client) => patchRefresh(client, app, { + policies: [...refresh?.policies ?? [], { audience: api.audience, scope: api.scope }] + }) + } + }); + } + } + if (snapshot.applicationConnections.length === 0) { + add({ + id: "P021", + severity: "error", + check: "connections", + message: "No connection is enabled for the application, so nobody can sign in.", + remedy: { + summary: "Enable a connection for the application, or promote one to the tenant level", + command: `auth0 api patch "connections//clients" --data '[{"client_id":"${clientId}","status":true}]'` + } + }); + } + if (snapshot.organization) { + const shared = snapshot.organization.connections.some( + (id) => snapshot.applicationConnections.some((connection) => connection.id === id) + ); + if (!shared) { + add({ + id: "P022", + severity: "error", + check: "organization connections", + message: `Organization "${snapshot.organization.name}" has no connection in common with the application, so its members cannot sign in.`, + remedy: { + summary: "Enable a connection that the application and the organization share", + command: `auth0 api post "organizations/${snapshot.organization.id}/enabled_connections" --data '{"connection_id":"","assign_membership_on_login":false}'` + } + }); + } + } + if (!app.session_transfer?.allowed_authentication_methods?.length) { + add({ + id: "P024", + severity: "warning", + check: "session transfer", + message: "Session transfer is not enabled, so a native app cannot open the portal with a session.", + remedy: { + summary: "Allow session transfer on the portal application", + command: `auth0 api patch "clients/${clientId}" --data '{"session_transfer":{"allowed_authentication_methods":["query"],"enforce_device_binding":"ip"}}'` + } + }); + } + return findings; +} +function componentsOn(components, apiName) { + const needle = apiName === "My Account API" ? "my_account" : "my_organization"; + return components.filter((component) => component.type.includes(needle)); +} +function componentLabel(component) { + const name = component.type.split(":").pop() ?? component.type; + return name.replace(/_/g, " "); +} +function allComponents(pages) { + const all = []; + const visit = (components = []) => { + for (const component of components) { + all.push(component); + visit(component.config?.children); + } + }; + for (const page of pages) visit(page.components); + return all; +} + +// src/audit/fix.ts +function fixableFindings(findings) { + return findings.filter((finding) => finding.severity === "error" && finding.remedy?.apply); +} +function findingKey(finding) { + return `${finding.id}:${finding.subject ?? ""}`; +} +async function applyFixes(selected, deps) { + const { ui } = deps; + const result = { applied: 0, failed: 0, alreadyFixed: 0 }; + for (const finding of selected) { + const fresh = auditPortal(await deps.readSnapshot()).find((candidate) => findingKey(candidate) === findingKey(finding)); + if (!fresh?.remedy?.apply) { + result.alreadyFixed += 1; + ui.success(`${finding.id} ${finding.check}`, "already fixed"); + continue; + } + const label = `${fresh.id} ${fresh.check}`; + try { + await ui.task(`Fixing ${fresh.check}`, () => fresh.remedy.apply(deps.api)); + result.applied += 1; + ui.fixed(label, fresh.remedy.summary); + } catch (error) { + result.failed += 1; + ui.error(`${label}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return result; +} +var MAX_FIX_ROUNDS = 3; +async function applyFixRounds(selected, deps, chooseNext) { + const total = { applied: 0, failed: 0, alreadyFixed: 0 }; + const attempted = /* @__PURE__ */ new Set(); + let pending = selected; + for (let round = 0; round < MAX_FIX_ROUNDS && pending.length > 0; round += 1) { + for (const finding of pending) attempted.add(findingKey(finding)); + const result = await applyFixes(pending, deps); + total.applied += result.applied; + total.failed += result.failed; + total.alreadyFixed += result.alreadyFixed; + const exposed = fixableFindings(auditPortal(await deps.readSnapshot())).filter((finding) => !attempted.has(findingKey(finding))); + pending = exposed.length > 0 ? await chooseNext(exposed) : []; + } + return total; +} + +// src/audit/run.ts +async function runAudit(options, deps) { + const { api, ui, tenantDomain } = deps; + if (options.showBanner !== false) ui.banner("Universal Portals audit", tenantDomain); + const snapshot = await ui.task( + "Reading the portal", + () => fetchPortalSnapshot(api, options.portalId, tenantDomain, { organizationId: options.organizationId }) + ); + const findings = auditPortal(snapshot); + const errors = findings.filter((finding) => finding.severity === "error").length; + const warnings = findings.length - errors; + ui.section(`Portal "${snapshot.portal.slug}"`); + if (findings.length === 0) { + ui.success("No problems found"); + } else { + ui.table( + ["", "ID", "Check", "Fix"], + findings.map((finding) => [ + finding.severity === "error" ? "\u2716" : "\u25B2", + finding.id, + finding.check, + finding.remedy?.summary ?? finding.message + ]) + ); + for (const finding of findings.filter((item) => item.remedy?.command && !item.remedy.apply)) { + ui.detail(`${finding.id} manually: ${finding.remedy?.command}`); + } + } + ui.newline(); + const summary = `${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`; + if (errors > 0) ui.error(summary); + else if (warnings > 0) ui.warn(summary); + else ui.success(summary); + return { + portalId: options.portalId, + findings, + errors, + warnings, + failed: errors > 0 || options.strict && warnings > 0 + }; +} + +// src/scope-fixes.ts +function scopeFixes(scopes) { + return [ + `Signed in as a user: auth0 login --scopes ${scopes.join(",")}`, + `Signed in with client credentials: add ${scopes.join(", ")} to the application's Management API grant` + ]; +} + +// src/setup/preflight.ts +var SETUP_PROBES = [ + { name: "list resource servers", method: "GET", path: "resource-servers?per_page=1" }, + { name: "create resource server", method: "POST", path: "resource-servers", body: {} }, + { name: "list clients", method: "GET", path: "clients?per_page=1" }, + { name: "create client", method: "POST", path: "clients", body: {} }, + { name: "list client grants", method: "GET", path: "client-grants?per_page=1" }, + { name: "create client grant", method: "POST", path: "client-grants", body: {} }, + { name: "list vault connections", method: "GET", path: "flows/vault/connections" }, + { name: "create vault connection", method: "POST", path: "flows/vault/connections", body: {} }, + { name: "list forms", method: "GET", path: "forms?per_page=1" }, + { name: "import forms", method: "POST", path: "forms/import", body: {} }, + { name: "list portals", method: "GET", path: "portals" }, + { name: "create portal", method: "POST", path: "portals", body: {} }, + { + name: "look up users by email", + method: "GET", + path: "users?q=email%3A%22preflight%40example.invalid%22&search_engine=v3", + businessOnly: true + }, + { name: "list organizations", method: "GET", path: "organizations?per_page=1", businessOnly: true } +]; +var FeatureNotEnabledError = class extends PluginError { + constructor(tenantDomain) { + super( + `Universal Portals is not enabled for ${tenantDomain}. Ask Auth0 to enable it for this tenant, then run the command again.`, + "feature_not_enabled" + ); + this.tenantDomain = tenantDomain; + } + tenantDomain; +}; +async function runPreflight(api, probes, tenantDomain) { + const gaps = []; + const unexpected = []; + const failures = await Promise.all( + probes.map(async (probe) => { + try { + await api.request(probe.method, probe.path, probe.body); + return void 0; + } catch (error) { + return { probe, error }; + } + }) + ); + for (const failure of failures) { + if (!failure) continue; + const { probe, error } = failure; + if (!(error instanceof ApiError)) throw error; + if (error.status === 400) continue; + if (error.errorCode === "feature_not_enabled") throw new FeatureNotEnabledError(tenantDomain); + const gap = parseScopeGap(probe.name, error); + if (gap) { + gaps.push(gap); + } else { + unexpected.push({ probe: probe.name, status: error.status, message: error.message }); + } + } + return { gaps, unexpected }; +} +function parseScopeGap(probe, error) { + if (error.status !== 403) return void 0; + const match = /expected (any|all) of:\s*(.+)$/.exec(error.message); + if (!match) return void 0; + const requirement = match[1] === "all" ? "all" : "any"; + const scopes = match[2].split(",").map((scope) => scope.trim()).filter(Boolean); + return { probe, requirement, scopes }; +} +function requiredMissingScopes(result) { + const required = result.gaps.filter((gap) => gap.requirement === "all" || gap.scopes.length === 1); + return [...new Set(required.flatMap((gap) => gap.scopes))].sort(); +} +function choiceGaps(result) { + return result.gaps.filter((gap) => gap.requirement === "any" && gap.scopes.length > 1); +} + +// src/errors.ts +function reportError(error, json, io, ui) { + const { code, message, details } = describeError(error); + if (json) { + io.stderr.write(`${JSON.stringify({ error: code, message, ...details })} +`); + return 1; + } + ui.error(message); + for (const fix of details.fixes ?? []) ui.detail(fix); + return error instanceof UsageError ? 2 : 1; +} +function describeError(error) { + if (error instanceof PluginError) { + return { code: error.code, message: error.message, details: error.fixes ? { fixes: error.fixes } : {} }; + } + if (error instanceof ApiError) { + const gap = parseScopeGap("", error); + const scopes = gap && gap.requirement === "any" ? gap.scopes.slice(0, 1) : gap?.scopes ?? []; + const fixes = scopes.length > 0 ? { fixes: scopeFixes(scopes) } : {}; + return { code: error.errorCode ?? "api_error", message: error.message, details: { status: error.status, ...fixes } }; + } + if (error instanceof Error) return { code: "setup_failed", message: error.message, details: {} }; + return { code: "setup_failed", message: String(error), details: {} }; +} + +// src/audit-command.ts +async function runAuditCommand(args, env, io, ui, prompter) { + try { + const context = await readContext(env, args.tenant, io.run); + checkTenantFlag(args.tenant, context); + const api = apiFor(context, { fetchImpl: io.fetchImpl, run: io.run }); + const portalId = args.portalId ?? ""; + const organizationId = args.organization; + const readSnapshot = () => fetchPortalSnapshot(api, portalId, context.tenantDomain, { organizationId }); + const options = { portalId, organizationId, strict: args.strict }; + const report = await runAudit(options, { api, ui, tenantDomain: context.tenantDomain }); + const fixable = fixableFindings(report.findings); + const applyDeps = { api, ui, readSnapshot }; + let result = report; + if (args.fix) { + const errors = fixable.filter((finding) => finding.severity === "error"); + if (errors.length > 0) { + ui.section("Fix"); + await applyFixRounds(errors, applyDeps, async (exposed) => exposed); + result = await finalAudit(options, ui, api, context.tenantDomain); + } + } else if (prompter && fixable.length > 0) { + const selected = await chooseFixes(prompter, fixable, ui); + if (selected.length > 0) { + ui.section("Fix"); + await applyFixRounds(selected, applyDeps, async (exposed) => { + ui.detail("These errors appeared after the fixes above."); + return chooseFixes(prompter, exposed, ui); + }); + result = await finalAudit(options, ui, api, context.tenantDomain); + } + } else if (fixable.length > 0) { + ui.detail("Run with --fix to apply the errors the CLI can fix."); + } + if (args.json) io.stdout.write(`${JSON.stringify(result, null, 2)} +`); + return result.failed ? 1 : 0; + } catch (error) { + if (error instanceof CancelledError && !args.json) { + ui.warn("Cancelled. Fixes already applied stay in your tenant."); + ui.cancelled("Audit cancelled."); + return 130; + } + return reportError(error, args.json, io, ui); + } +} +async function chooseFixes(prompter, errors, ui) { + ui.detail("Warnings are not changed. They may be intentional."); + const choice = await prompter.choose("What do you want to fix?", [ + { label: "Fix all errors", hint: "recommended", value: "all" }, + { label: "Choose errors to fix", hint: "every error starts checked", value: "choose" }, + { label: "Skip", hint: "leave the portal as it is", value: "skip" } + ]); + if (choice === "skip") return []; + if (choice === "all") return errors; + return prompter.checklist( + "Errors to fix", + errors.map((finding) => ({ + label: `${finding.id} ${finding.remedy?.summary ?? finding.message}`, + value: finding, + checked: true + })) + ); +} +async function finalAudit(options, ui, api, tenantDomain) { + ui.section("Checking the portal again"); + return runAudit({ ...options, showBanner: false }, { api, ui, tenantDomain }); +} + +// src/browser.ts +import { spawn as spawn2 } from "node:child_process"; +function browserCommand(url, platform) { + if (platform === "darwin") return { command: "open", args: [url] }; + if (platform === "win32") return { command: "cmd", args: ["/c", "start", "", url] }; + return { command: "xdg-open", args: [url] }; +} +function openBrowser(url, platform = process.platform) { + const { command, args } = browserCommand(url, platform); + const child = spawn2(command, args, { detached: true, stdio: "ignore" }); + child.on("error", () => void 0); + child.unref(); +} + +// src/dashboard.ts +function tenantLocation(tenantDomain) { + const parts = tenantDomain.split("."); + const hasRegion = parts.length >= 4 && parts[1].length === 2; + const serviceDomain = hasRegion ? parts.slice(2) : parts.slice(1); + return { + base: `https://manage.${serviceDomain.join(".")}`, + region: hasRegion ? parts[1] : "us", + name: parts[0] + }; +} +function dashboardRoot(tenantDomain) { + const { base, region, name } = tenantLocation(tenantDomain); + return `${base}/dashboard/${region}/${name}`; +} +function applicationUrl(tenantDomain, clientId) { + return `${dashboardRoot(tenantDomain)}/applications/${clientId}/settings`; +} +function portalEditUrl(tenantDomain, portalId) { + return `${dashboardRoot(tenantDomain)}/universal-portals/${portalId}/edit`; +} +function formsRoot(tenantDomain) { + const { region, name } = tenantLocation(tenantDomain); + return `https://forms.auth0.com/tenants/${region}/${name}`; +} +function formEditUrl(tenantDomain, formId) { + return `${formsRoot(tenantDomain)}/forms/${formId}/edit`; +} +function flowEditUrl(tenantDomain, flowId) { + return `${formsRoot(tenantDomain)}/flows/${flowId}/edit`; +} +function vaultAppsUrl(tenantDomain) { + return `${formsRoot(tenantDomain)}/vault/apps/AUTH0/edit`; +} + +// src/forms.ts +var FORM_NAMES = { + personalInfo: "Profile update (Universal Portals)", + privacyConsent: "Privacy settings (Universal Portals)", + communicationPreferences: "Marketing communication preferences (Universal Portals)" +}; +function buildPortalForms(vaultConnectionId) { + return [ + { key: "personalInfo", name: FORM_NAMES.personalInfo, document: personalInfoDocument(vaultConnectionId) }, + { + key: "communicationPreferences", + name: FORM_NAMES.communicationPreferences, + document: communicationPreferencesDocument(vaultConnectionId) + }, + { key: "privacyConsent", name: FORM_NAMES.privacyConsent, document: privacyDocument(vaultConnectionId) } + ]; +} +function field(id, label, type, extra = {}) { + return { id, category: "FIELD", type, label, required: false, sensitive: false, ...extra }; +} +function nextButton(id, text2) { + return { id, category: "BLOCK", type: "NEXT_BUTTON", config: { text: text2 } }; +} +function formDocument(name, step, start, ending, flowName, actions, connectionId) { + return { + version: "4.0.0", + form: { + name, + languages: { primary: "en" }, + nodes: [ + { + id: step.id, + type: "STEP", + coordinates: step.coordinates, + config: { components: step.components, next_node: step.flowNodeId } + }, + { + id: step.flowNodeId, + type: "FLOW", + coordinates: { x: 988, y: 9 }, + config: { flow_id: "#FLOW-1#", next_node: "$ending" } + } + ], + start, + ending + }, + flows: { "#FLOW-1#": { name: flowName, actions } }, + connections: { "#CONN-1#": { id: connectionId } } + }; +} +function updateUserAction(id, changes) { + return { + id, + type: "AUTH0", + action: "UPDATE_USER", + allow_failure: false, + mask_output: false, + params: { + connection_id: "#CONN-1#", + user_id: "{{context.user.user_id}}", + changes + } + }; +} +function personalInfoDocument(connectionId) { + return formDocument( + FORM_NAMES.personalInfo, + { + id: "step_BL3V", + coordinates: { x: 346, y: -221 }, + flowNodeId: "flow_DcHK", + components: [ + field("full_name", "Full name", "TEXT", { config: { multiline: false } }), + field("job_title", "Job title", "TEXT", { config: { multiline: false } }), + field("mobile_number", "Mobile number", "TEL", { config: { country_picker: true } }), + field("date_of_birth", "Date of birth", "DATE", { config: { format: "DATE" } }), + field("linkedin", "LinkedIn", "URL"), + nextButton("next_button_UmEY", "Update") + ] + }, + { next_node: "step_BL3V", coordinates: { x: 0, y: 0 } }, + { resume_flow: true, coordinates: { x: 1367, y: -7 } }, + "Update profile", + [ + updateUserAction("update_user_v4C0", { + user_metadata: { + linkedin: "{{fields.linkedin}}", + full_name: "{{fields.full_name}}", + job_title: "{{fields.job_title}}", + date_of_birth: "{{fields.date_of_birth}}", + mobile_number: "{{fields.mobile_number.international_number}}" + } + }) + ], + connectionId + ); +} +function communicationPreferencesDocument(connectionId) { + return formDocument( + FORM_NAMES.communicationPreferences, + { + id: "step_4Td2", + coordinates: { x: 217, y: -218 }, + flowNodeId: "flow_etvH", + components: [ + field("communication_preferences", "Communication preferences", "CHOICE", { + config: { + multiple: true, + options: [ + { label: "Product Announcements", value: "Product Announcements" }, + { label: "Featured Content", value: "Featured Content" }, + { label: "Digest News", value: "Digest News" }, + { label: "Events", value: "Events" } + ] + } + }), + nextButton("next_button_US5N", "Update") + ] + }, + { next_node: "step_4Td2", coordinates: { x: -25, y: -98 } }, + { resume_flow: true, coordinates: { x: 1194, y: -59 } }, + "Update preferences", + [ + updateUserAction("update_user_HitP", { + app_metadata: { communication_preferences: "{{fields.communication_preferences}}" } + }) + ], + connectionId + ); +} +function privacyDocument(connectionId) { + const consentField = (id, label, hint) => ({ + id, + category: "FIELD", + type: "BOOLEAN", + label, + hint, + required: true, + sensitive: false, + config: { default_value: false } + }); + const learnMore = (text2) => `

${text2} Learn more.

`; + return formDocument( + FORM_NAMES.privacyConsent, + { + id: "step_y0Ri", + coordinates: { x: 500, y: 0 }, + flowNodeId: "flow_AB1r", + components: [ + consentField("data_sharing", "Data sharing", learnMore("Allow app usage data to improve features.")), + consentField("profile_visibility", "Profile visibility", learnMore("Show my profile in search results.")), + consentField( + "location_tracking", + "Location tracking", + learnMore("Allow location tracking for personalized recommendations.") + ), + consentField("ad_personalization", "Ad personalization", learnMore("Use my data to personalize ads.")), + nextButton("next_button_aB8e", "Update") + ] + }, + { next_node: "step_y0Ri", coordinates: { x: 202, y: 247 } }, + { resume_flow: true, coordinates: { x: 1445, y: 247 } }, + "Update privacy settings", + [ + updateUserAction("update_user_uOZW", { + app_metadata: { + data_sharing: "{{fields.data_sharing}}", + location_tracking: "{{fields.location_tracking}}", + ad_personalization: "{{fields.ad_personalization}}", + profile_visibility: "{{fields.profile_visibility}}" + } + }) + ], + connectionId + ); +} + +// src/portal-model.ts +var PORTAL_SLUG_PATTERN = /^[a-z][a-z0-9-]{0,99}$/; +function isValidPortalSlug(slug) { + return PORTAL_SLUG_PATTERN.test(slug); +} + +// src/setup/business-types.ts +var BusinessAccessError = class extends PluginError { + constructor(message, fixes) { + super(message, "business_access", fixes); + } +}; +function userLabel(user) { + return user.email ?? user.user_id; +} + +// src/setup/business-organization.ts +var ORG_NAME_PATTERN = /^[a-z0-9_-]{1,50}$/; +function suggestOrgName(displayName) { + return displayName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50); +} +async function createOrganization(user, deps) { + const displayName = (await deps.prompt.ask("Organization display name", void 0, "Acme Corp")).trim(); + if (!displayName) throw new BusinessAccessError("The organization needs a display name.", []); + let suggestion = suggestOrgName(displayName); + for (; ; ) { + const orgName = await askOrgName(deps, suggestion); + const confirmed = await deps.prompt.confirm( + `Create organization "${displayName}" (${orgName}), enable a database connection on it, and add ${userLabel(user)}?`, + true + ); + if (!confirmed) { + throw new BusinessAccessError("No organization was created.", [ + `Create one: auth0 orgs create --name ${orgName} --display "${displayName}"` + ]); + } + let created; + try { + created = await deps.api.request("POST", "organizations", { + name: orgName, + display_name: displayName + }); + } catch (error) { + if (!(error instanceof ApiError) || error.status !== 409) throw error; + deps.ui.warn(`The organization name "${orgName}" is already used in this tenant.`); + suggestion = `${orgName}-2`.slice(0, 50); + continue; + } + deps.ui.success(`Created organization "${displayName}"`); + const connection = await chooseDatabaseConnection(deps); + await deps.api.request("POST", `organizations/${encodeURIComponent(created.id)}/enabled_connections`, { + connection_id: connection, + assign_membership_on_login: false + }); + await deps.api.request("POST", `organizations/${encodeURIComponent(created.id)}/members`, { + members: [user.user_id] + }); + deps.ui.success(`Added ${userLabel(user)} to ${displayName}`); + return created; + } +} +async function askOrgName(deps, suggestion) { + for (; ; ) { + const answer = (await deps.prompt.ask("Organization name", suggestion)).trim(); + if (ORG_NAME_PATTERN.test(answer)) return answer; + deps.ui.error("Use lowercase letters, numbers, hyphens and underscores, up to 50 characters."); + } +} +async function chooseDatabaseConnection(deps) { + const connections = (await listAll(deps.api, "connections", "connections")).filter((connection) => connection.strategy === "auth0"); + if (connections.length === 0) { + throw new BusinessAccessError("This tenant has no database connection to sign in with.", [ + "Create one in the dashboard, then rerun setup." + ]); + } + if (connections.length === 1) return connections[0].id; + return deps.prompt.choose( + "Database connection for the organization", + connections.map((connection) => ({ label: connection.name, value: connection.id })) + ); +} +async function requireEnabledConnection(organization, api) { + const connections = await listAll(api, `organizations/${encodeURIComponent(organization.id)}/enabled_connections`, "enabled_connections"); + if (connections.length === 0) { + throw new BusinessAccessError(`Organization "${organization.name}" has no enabled connections, so members cannot sign in.`, [ + `Enable one: auth0 api post "organizations/${organization.id}/enabled_connections" --data '{"connection_id":"","assign_membership_on_login":false}'` + ]); + } +} + +// src/setup/business-role.ts +async function ensureOrganizationRole(user, organization, deps) { + const roles = await listAll( + deps.api, + `organizations/${encodeURIComponent(organization.id)}/members/${encodeURIComponent(user.user_id)}/roles`, + "roles" + ); + const granted = /* @__PURE__ */ new Set(); + for (const role2 of roles) { + for (const scope of await permissionsOf(role2.id, deps)) granted.add(scope); + } + const missing = MY_ORGANIZATION_SCOPES.filter((scope) => !granted.has(scope)); + if (missing.length === 0) return true; + const missingReads = missing.filter((scope) => scope.startsWith("read:")); + const missingWrites = missing.filter((scope) => !scope.startsWith("read:")); + if (missingReads.length > 0) { + deps.ui.warn( + `${userLabel(user)} lacks read permissions for the organization components in "${organization.name}": ${missingReads.join(", ")}. Those components will not open for them.` + ); + } + if (missingWrites.length > 0) { + deps.ui.warn( + `${userLabel(user)} lacks write permissions for the organization components in "${organization.name}": ${missingWrites.join(", ")}. The components open, but changes cannot be saved.` + ); + } + if (!deps.interactive) return false; + const candidates = await rolesGranting(missing, deps); + let role; + if (candidates.length === 0) { + const create = await deps.prompt.confirm( + `No role in this tenant has these permissions. Create "${PORTAL_ROLE_NAME}" and assign it to ${userLabel(user)}?`, + true + ); + if (!create) return false; + role = await createPortalRole(deps); + } else { + const assign = await deps.prompt.confirm("Assign an organization role now?", true); + if (!assign) return false; + const picked = await deps.prompt.choose("Role to assign", [ + ...candidates.map((candidate) => ({ label: candidate.name, value: candidate })), + { label: `Create "${PORTAL_ROLE_NAME}"`, hint: "only the permissions the portal needs", value: "create" } + ]); + role = picked === "create" ? await createPortalRole(deps) : picked; + } + await deps.api.request( + "POST", + `organizations/${encodeURIComponent(organization.id)}/members/${encodeURIComponent(user.user_id)}/roles`, + { roles: [role.id] } + ); + deps.ui.success(`Assigned role ${role.name} to ${userLabel(user)}`); + return true; +} +var PORTAL_ROLE_NAME = "Universal Portals - Admin"; +async function createPortalRole(deps) { + const tenantRoles = await listAll(deps.api, "roles", "roles"); + const role = tenantRoles.find((candidate) => candidate.name === PORTAL_ROLE_NAME) ?? await deps.api.request("POST", "roles", { + name: PORTAL_ROLE_NAME, + description: "The organization permissions the Universal Portals components need." + }); + const granted = await permissionsOf(role.id, deps); + const missing = MY_ORGANIZATION_SCOPES.filter((scope) => !granted.includes(scope)); + if (missing.length > 0) { + await deps.api.request("POST", `roles/${encodeURIComponent(role.id)}/permissions`, { + permissions: missing.map((permission_name) => ({ + permission_name, + resource_server_identifier: myOrganizationAudience(deps.tenantDomain) + })) + }); + } + return role; +} +async function permissionsOf(roleId, deps) { + const permissions = await listAll( + deps.api, + `roles/${encodeURIComponent(roleId)}/permissions`, + "permissions" + ); + return permissions.map((permission) => permission.permission_name); +} +async function rolesGranting(scopes, deps) { + const tenantRoles = await listAll(deps.api, "roles", "roles"); + const matches = []; + for (const role of tenantRoles) { + const granted = await permissionsOf(role.id, deps); + if (scopes.every((scope) => granted.includes(scope))) matches.push(role); + } + return matches; +} + +// src/setup/business.ts +async function resolveBusinessAccess(input, deps) { + const user = await chooseUser(input.userQuery, deps); + deps.ui.success(`"${userLabel(user)}" found`); + const organization = await chooseOrganization(user, input.organization, deps); + deps.ui.success(`Organization "${organization.name}" selected`); + await requireEnabledConnection(organization, deps.api); + const hasOrganizationRole = await ensureOrganizationRole(user, organization, deps); + return { + userId: user.user_id, + organizationId: organization.id, + organizationName: organization.name, + hasOrganizationRole + }; +} +function describeUser(user) { + return `${user.email ?? "no email"} \xB7 ${user.user_id}${user.name ? ` \xB7 ${user.name}` : ""}`; +} +async function lookupUsers(query, api) { + if (query.includes("@")) { + const phrase = `email:"${query.replace(/["\\]/g, "\\$&")}"`; + const found = await listAll(api, `users?q=${encodeURIComponent(phrase)}&search_engine=v3`, "users"); + return found.filter((user) => user.email?.toLowerCase() === query.toLowerCase()); + } + try { + return [await api.request("GET", `users/${encodeURIComponent(query)}`)]; + } catch (error) { + if (error instanceof ApiError && error.status === 404) return []; + throw error; + } +} +async function chooseUser(initialQuery, deps) { + let query = initialQuery; + for (; ; ) { + if (!query) { + if (!deps.interactive) { + throw new BusinessAccessError("A Business portal needs the user who will test it.", [ + "Rerun with --user ." + ]); + } + query = (await deps.prompt.ask("Enter the test user's email or user ID", void 0, "jane@acme.com")).trim(); + if (!query) continue; + } + const matches = await lookupUsers(query, deps.api); + if (matches.length === 1) return matches[0]; + if (!deps.interactive) { + if (matches.length === 0) { + throw new BusinessAccessError(`No user matches "${query}".`, ["Check the email or user ID, then rerun with --user."]); + } + throw new BusinessAccessError(`More than one user has the email "${query}".`, [ + "Rerun with the user ID instead: " + matches.map((user) => user.user_id).join(", ") + ]); + } + if (matches.length === 0) { + deps.ui.warn(`No user matches "${query}".`); + query = void 0; + continue; + } + const choice = await deps.prompt.choose(`${matches.length} users match "${query}"`, [ + ...matches.map((user) => ({ label: describeUser(user), value: user })), + { label: "Try another email or user ID", value: "retry" } + ]); + if (choice === "retry") { + query = void 0; + continue; + } + return choice; + } +} +async function chooseOrganization(user, requested, deps) { + const memberships = await listAll(deps.api, `users/${encodeURIComponent(user.user_id)}/organizations`, "organizations"); + if (requested) { + const match = memberships.find((org) => org.id === requested || org.name === requested); + if (match) return match; + throw new BusinessAccessError(`${userLabel(user)} is not a member of "${requested}".`, [ + `Add them: auth0 api post "organizations/${requested}/members" --data '{"members":["${user.user_id}"]}'` + ]); + } + if (!deps.interactive) { + if (memberships.length === 1) return memberships[0]; + if (memberships.length > 1) { + throw new BusinessAccessError(`${userLabel(user)} belongs to ${memberships.length} organizations.`, [ + "Rerun with --organization : " + memberships.map((org) => org.id).join(", ") + ]); + } + throw new BusinessAccessError(`${userLabel(user)} is not a member of any organization.`, [ + `Add them: auth0 api post "organizations//members" --data '{"members":["${user.user_id}"]}'` + ]); + } + if (memberships.length === 1) return memberships[0]; + const candidates = memberships.length > 0 ? memberships : await listTenantOrganizations(deps.api); + if (memberships.length === 0) deps.ui.warn(`${userLabel(user)} is not a member of any organization.`); + deps.ui.info("The portal opens in one organization, and the test user has to be a member of it."); + const choice = await deps.prompt.choose("Choose the organization to test the portal in", [ + ...candidates.map((org) => ({ + label: `${org.name} (${org.id})`, + value: org + })), + { label: "None of these, create a new organization", value: "create" } + ]); + if (choice === "create") return createOrganization(user, deps); + if (memberships.length > 0) return choice; + return addToOrganization(user, choice, deps); +} +async function listTenantOrganizations(api) { + return await listAll(api, "organizations", "organizations"); +} +async function addToOrganization(user, organization, deps) { + const confirmed = await deps.prompt.confirm(`Add ${userLabel(user)} as a member of ${organization.name}?`, true); + if (!confirmed) { + throw new BusinessAccessError(`${userLabel(user)} is not a member of an organization, and setup did not add them.`, [ + `Add them: auth0 api post "organizations/${organization.id}/members" --data '{"members":["${user.user_id}"]}'` + ]); + } + await deps.api.request("POST", `organizations/${encodeURIComponent(organization.id)}/members`, { + members: [user.user_id] + }); + deps.ui.success(`Added ${userLabel(user)} to ${organization.name}`); + return organization; +} +var BUSINESS_REQUIREMENTS = "Business portals only work in the context of an organization: without it, the organization components do not load. Pick the user who will test the portal, so the link opens in their organization."; +var SKIPPED_TEST_USER_REQUIREMENTS = "Without a test user, the link needs an organization. The portal only signs members in when the URL carries ?organization=."; +var ORGANIZATION_PLACEHOLDER = ""; +async function chooseTestLink(input, deps) { + const { ui, prompt, interactive } = deps; + ui.section("Test user"); + let userQuery = input.userQuery; + if (userQuery === void 0 && interactive) { + ui.info(BUSINESS_REQUIREMENTS); + userQuery = (await prompt.ask("Enter the test user's email or user ID (press Enter to skip)", void 0, "jane@acme.com")).trim() || void 0; + } + if (userQuery) { + const access = await resolveBusinessAccess({ userQuery, organization: input.organization }, deps); + return { organizationId: access.organizationId, isPlaceholder: false, access }; + } + if (interactive) ui.info(SKIPPED_TEST_USER_REQUIREMENTS); + const organizationId = await prompt.ask( + "Enter the organization ID to test in (the link adds ?organization=)", + ORGANIZATION_PLACEHOLDER + ); + return { organizationId, isPlaceholder: organizationId === ORGANIZATION_PLACEHOLDER }; +} + +// src/setup/host.ts +async function resolvePortalHost(api, tenantDomain) { + const custom = await readCustomDomain(api); + if (custom?.status === "ready" && custom.domain) return { domain: custom.domain, isCustom: true }; + return { domain: tenantDomain, isCustom: false }; +} +function portalUrl(domain, slug, organizationId) { + const base = `https://${domain}/portals/${slug}`; + if (!organizationId) return base; + const value = organizationId === ORGANIZATION_PLACEHOLDER ? organizationId : encodeURIComponent(organizationId); + return `${base}?organization=${value}`; +} + +// src/setup/vault.ts +var FORMS_VAULT_NAME = "Universal Portals forms vault"; +async function ensureFormsVault(api, ui, tenantDomain, resources) { + const app = await ui.task("Preparing the forms vault", () => ensureFormsVaultClient(api, FORMS_VAULT_NAME)); + const grant = await ensureClientGrant(api, { + clientId: app.value.clientId, + audience: managementAudience(tenantDomain), + scope: FORMS_VAULT_SCOPES, + subjectType: "client" + }); + const setup = { domain: tenantDomain, clientId: app.value.clientId, clientSecret: app.value.clientSecret }; + const vault = await ensureVaultConnection(api, FORMS_VAULT_NAME, setup); + if (grant.status === "updated" && vault.status === "reused") { + await refreshVaultConnection(api, vault.value.id, setup); + ui.success("Forms vault refreshed with the updated permissions"); + } + resources.push({ type: "forms_vault_client", id: app.value.clientId, status: app.status }); + resources.push({ type: "forms_vault_grant", id: grant.value.id, status: grant.status }); + resources.push({ type: "forms_vault", id: vault.value.id, status: vault.status }); + ui.success(`"${FORMS_VAULT_NAME}"`, describe(vault.status)); + ui.reference("Vault connection", vault.value.id, vaultAppsUrl(tenantDomain)); + return vault; +} + +// src/setup/run.ts +var SlugRaceError = class extends PluginError { + constructor(slug) { + super( + `The slug "${slug}" was taken while setup was running. Rerun with a different --name and --slug.`, + "slug_race" + ); + this.slug = slug; + } + slug; +}; +var MissingScopesError = class extends PluginError { + constructor(scopes, choices) { + super( + scopes.length > 0 ? `The CLI session is missing ${scopes.length} scope(s): ${scopes.join(", ")}.` : "The CLI session is missing a scope it needs.", + "missing_scopes", + scopes.length > 0 ? scopeFixes(scopes) : void 0 + ); + this.scopes = scopes; + this.choices = choices; + } + scopes; + choices; +}; +async function runSetup(options, deps) { + const resources = []; + const { ui, api, tenantDomain } = deps; + const probes = SETUP_PROBES.filter((probe) => !probe.businessOnly || options.template === "business"); + await checkPermissions(probes, deps); + let slug = options.slug; + if (!isValidPortalSlug(slug)) throw new Error(`The slug "${slug}" must match ^[a-z][a-z0-9-]{0,99}$.`); + if (options.template === "business" && !options.userQuery && !deps.interactive) { + throw new BusinessAccessError("A Business portal needs the user who will test it.", [ + "Rerun with --user ." + ]); + } + const appName = `${options.name} ${PORTAL_APP_SUFFIX}`; + slug = await availableSlug(api, slug, appName, deps); + const testLink = options.template === "business" ? await chooseTestLink(options, deps) : void 0; + const host = await resolvePortalHost(api, tenantDomain); + ui.section("Resource servers"); + for (const server of resourceServersFor(options.template, tenantDomain)) { + const outcome = await ui.task(`Checking ${server.name}`, () => ensureResourceServer(api, server)); + resources.push({ type: "resource_server", id: outcome.value.id, status: outcome.status }); + ui.success(`"${server.name}"`, describe(outcome.status)); + } + ui.result("Resource servers ready"); + ui.section("Application"); + const client = await ui.task( + "Creating application", + () => ensurePortalClient(api, { + name: appName, + slug, + template: options.template, + tenantDomain, + portalHost: host.domain, + isCustomDomain: host.isCustom + }) + ); + resources.push({ type: "application", id: client.value.clientId, status: client.status }); + ui.success(`"${appName}"`, describe(client.status)); + ui.reference("Application", client.value.clientId, applicationUrl(tenantDomain, client.value.clientId)); + for (const audience of audiencesFor(options.template, tenantDomain)) { + const grant = await ui.task( + `Granting ${apiLabel(audience.audience, tenantDomain)}`, + () => ensureClientGrant(api, { + clientId: client.value.clientId, + audience: audience.audience, + scope: audience.scope, + subjectType: "user" + }) + ); + resources.push({ type: "client_grant", id: grant.value.id, status: grant.status }); + ui.success(`"${apiLabel(audience.audience, tenantDomain)}" access`, describe(grant.status)); + } + ui.result("Application ready"); + ui.section("Forms"); + const vault = await ensureFormsVault(api, deps.ui, tenantDomain, resources); + const forms = await ui.task("Creating forms", () => ensureForms(api, buildPortalForms(vault.value.id))); + for (const [key, outcome] of Object.entries(forms)) { + resources.push({ type: "form", id: outcome.value.id, status: outcome.status }); + ui.success(`"${FORM_NAMES[key]}"`, describe(outcome.status)); + ui.reference("Form", outcome.value.id, formEditUrl(tenantDomain, outcome.value.id)); + if (outcome.value.flowId) ui.reference("Flow", outcome.value.flowId, flowEditUrl(tenantDomain, outcome.value.flowId)); + } + ui.result("Forms ready"); + const template = templateFor(options.template, { + personalInfo: forms.personalInfo.value.id, + privacyConsent: forms.privacyConsent.value.id, + communicationPreferences: forms.communicationPreferences.value.id + }); + ui.section("Portal"); + let portal; + try { + portal = await ui.task( + "Creating portal", + () => ensurePortal( + api, + buildPortalRequest(template, { + slug, + name: options.name, + clientId: client.value.clientId, + clientSecret: client.value.clientSecret + }) + ) + ); + } catch (error) { + if (!(error instanceof SlugTakenError)) throw error; + throw new SlugRaceError(slug); + } + resources.push({ type: "portal", id: portal.value.id, status: portal.status }); + ui.success(`"${options.name}"`, describe(portal.status)); + ui.result("Portal ready"); + ui.reference("Portal", portal.value.id, portalEditUrl(tenantDomain, portal.value.id)); + const url = portalUrl(host.domain, portal.value.slug, testLink?.organizationId); + if (testLink?.isPlaceholder) { + ui.info(`Replace ${ORGANIZATION_PLACEHOLDER} in the link with the organization ID to test with.`); + } + return { + template: options.template, + portal: { id: portal.value.id, slug: portal.value.slug, url, status: portal.status }, + application: { clientId: client.value.clientId, status: client.status }, + vault: { id: vault.value.id, status: vault.status }, + forms: Object.fromEntries( + Object.entries(forms).map(([key, outcome]) => [key, { id: outcome.value.id, status: outcome.status }]) + ), + resources, + organization: testLink?.access ? { + id: testLink.access.organizationId, + name: testLink.access.organizationName, + hasRole: testLink.access.hasOrganizationRole + } : void 0 + }; +} +async function checkPermissions(probes, deps) { + deps.ui.section("Permissions"); + const result = await deps.ui.task("Checking permissions", () => runPreflight(deps.api, probes, deps.tenantDomain)); + for (const issue of result.unexpected) { + deps.ui.warn(`Could not verify "${issue.probe}": ${issue.message} (HTTP ${issue.status})`); + } + const required = requiredMissingScopes(result); + const choices = choiceGaps(result); + if (required.length === 0 && choices.length === 0) { + deps.ui.result("The CLI session has every permission setup needs"); + return; + } + deps.ui.error("The CLI session is missing permissions setup needs"); + for (const scope of required) deps.ui.detail(scope); + for (const choice of choices) deps.ui.detail(`one of: ${choice.scopes.join(", ")}`); + throw new MissingScopesError(required, choices); +} +function resourceServersFor(template, tenantDomain) { + const account = { name: "Auth0 My Account API", identifier: myAccountAudience(tenantDomain) }; + if (template === "consumer") return [account]; + return [{ name: "Auth0 My Organization API", identifier: myOrganizationAudience(tenantDomain) }, account]; +} +function apiLabel(audience, tenantDomain) { + if (audience === myAccountAudience(tenantDomain)) return "My Account API"; + if (audience === myOrganizationAudience(tenantDomain)) return "My Organization API"; + return "Management API"; +} +async function availableSlug(api, slug, appName, deps) { + let current = slug; + for (; ; ) { + const portal = await findPortalBySlug(api, current); + if (!portal) return current; + const app = await findClientByName(api, appName); + if (app !== void 0 && portal.client?.client_id === app.client_id) return current; + if (!deps.interactive) { + throw new Error(`The slug "${current}" is already used by another portal. Rerun with a different --slug.`); + } + deps.ui.warn(`The slug "${current}" is already taken.`); + current = await askValidSlug(deps.prompt, `${current}-2`, deps.ui); + } +} +async function askValidSlug(prompt, suggestion, ui) { + for (; ; ) { + const answer = await prompt.ask("Slug", suggestion); + if (isValidPortalSlug(answer)) return answer; + ui.error("The slug must start with a lowercase letter and use only lowercase letters, digits and hyphens."); + } +} + +// src/slug.ts +function toPortalSlug(name) { + const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "").slice(0, 100).replace(/-+$/, ""); + return slug || "my-portal"; +} + +// src/write-tracker.ts +function trackWrites(api) { + let wrote = false; + return { + api: { + async request(method, path, body) { + const result = await api.request(method, path, body); + if (method !== "GET") wrote = true; + return result; + } + }, + wrote: () => wrote + }; +} + +// src/setup-command.ts +async function runSetupCommand(args, env, io, ui, interactive, prompter) { + try { + const context = await readContext(env, args.tenant, io.run); + checkTenantFlag(args.tenant, context, " Nothing was created."); + if (!interactive) { + const missing = missingFlags(args); + if (missing.length > 0) { + throw new UsageError(`Missing required flags for a non-interactive run: ${missing.join(", ")}`); + } + } + const tracked = trackWrites(apiFor(context, { fetchImpl: io.fetchImpl, run: io.run })); + ui.banner("Universal Portals setup", context.tenantDomain); + try { + const options = prompter ? await askForMissing(args, prompter, context.tenantDomain) : optionsFromArgs(args); + const result = await runSetup(options, { + api: tracked.api, + ui, + prompt: prompter ?? unansweredPrompter(), + interactive, + tenantDomain: context.tenantDomain + }); + if (args.json) { + io.stdout.write(`${JSON.stringify(result, null, 2)} +`); + return 0; + } + ui.ready(result.portal.url); + if (prompter && await declinesToOpen(prompter)) return 0; + if (prompter) (io.openBrowser ?? openBrowser)(result.portal.url); + return 0; + } catch (error) { + if (error instanceof CancelledError && !args.json) return reportCancelled(ui, tracked.wrote()); + if (!args.json && tracked.wrote() && !(error instanceof SlugRaceError)) { + const code = reportError(error, false, io, ui); + ui.detail(RESUME_HINT); + return code; + } + throw error; + } + } catch (error) { + return reportError(error, args.json, io, ui); + } +} +async function declinesToOpen(prompter) { + try { + return !await prompter.confirm("Open the portal in your browser?", true); + } catch (error) { + if (error instanceof CancelledError) return true; + throw error; + } +} +var RESUME_HINT = "Run the same command again to resume. Existing resources are reused."; +function reportCancelled(ui, wrote) { + if (wrote) { + ui.warn("Cancelled. Nothing was rolled back: what was already created is still in your tenant."); + ui.warn(RESUME_HINT); + } else { + ui.warn("Cancelled. Nothing was created."); + } + ui.cancelled("Setup cancelled."); + return 130; +} +function optionsFromArgs(args) { + const name = args.name ?? ""; + return { + template: args.template ?? "consumer", + name, + slug: args.slug ?? toPortalSlug(name), + userQuery: args.user, + organization: args.organization + }; +} +async function askForMissing(args, prompt, tenantDomain) { + const template = args.template ?? await prompt.choose("Which kind of portal do you want to set up?", [ + { + label: "Consumer portal", + hint: "for end-users to manage their account in a self-service way", + value: "consumer" + }, + { + label: "Business portal", + hint: "for organization administrators to manage their organization in a self-service way", + value: "business" + } + ]); + const name = args.name ?? await prompt.ask("Portal name", "My Portal"); + const slug = args.slug ?? await prompt.ask("URL slug", toPortalSlug(name)); + return { template, name, slug, userQuery: args.user, organization: args.organization }; +} +function unansweredPrompter() { + const refuse = () => { + throw new UsageError("A question was needed but this run cannot prompt. Pass the flag instead."); + }; + return { + ask: async () => refuse(), + confirm: async () => refuse(), + choose: async () => refuse(), + checklist: async () => refuse() + }; +} + +// src/ui.ts +var RESET = "\x1B[0m"; +var codes = { + bold: "\x1B[1m", + dim: "\x1B[2m", + green: "\x1B[32m", + yellow: "\x1B[33m", + red: "\x1B[91m", + blue: "\x1B[34m", + cyan: "\x1B[36m", + magenta: "\x1B[35m" +}; +function colorEnabled(env, isTTY, noColorFlag = false) { + if (env.NO_COLOR !== void 0 || noColorFlag) return false; + return isTTY; +} +var Ui = class { + constructor(output, color, silent, animate = false) { + this.output = output; + this.color = color; + this.silent = silent; + this.animate = animate; + } + output; + color; + silent; + animate; + // silent suppresses all human output (agent and --json modes). animate enables + // the spinner, which needs a terminal. + spinnerGap = false; + paint(style, text2) { + if (!this.color) return text2; + return `${codes[style]}${text2}${RESET}`; + } + // The title box: the command, and the tenant every change will go to. It prints + // before any question, so a wrong tenant is visible from the start. + banner(title, tenant) { + if (this.silent) return; + box(this.paint("cyan", tenant), this.paint("bold", title), { + output: this.output, + width: "auto", + rounded: true, + contentAlign: "center", + titleAlign: "center" + }); + } + // A section header starts a block of items, closed by its result. + section(title) { + if (this.silent) return; + this.breakLine(); + log.step(title, { output: this.output, spacing: 0 }); + } + // One item of a section: a plain line. Only the section result carries a symbol. + success(text2, detail) { + if (this.silent) return; + const suffix = detail ? ` ${this.paint("dim", detail)}` : ""; + this.breakLine(); + log.message(`${text2}${suffix}`, { output: this.output, spacing: 0 }); + } + // A change the CLI made, with a green check so it reads as done. + fixed(text2, detail) { + if (this.silent) return; + const suffix = detail ? ` ${this.paint("dim", detail)}` : ""; + this.breakLine(); + log.message(`${this.paint("green", "\u2713")} ${text2}${suffix}`, { output: this.output, spacing: 0 }); + } + // The outcome of a section, drawn like the other results. + result(text2) { + if (this.silent) return; + this.breakLine(); + log.success(text2, { output: this.output, spacing: 0 }); + } + warn(text2) { + if (this.silent) return; + log.warn(text2, { output: this.output, spacing: 0 }); + } + error(text2) { + if (this.silent) return; + log.error(text2, { output: this.output, spacing: 0 }); + } + info(text2) { + if (this.silent) return; + log.info(text2, { output: this.output, spacing: 0 }); + } + detail(text2) { + if (this.silent) return; + log.message(this.paint("dim", text2), { output: this.output, spacing: 0 }); + } + // Shows an ID that links to its dashboard page. Terminals with OSC 8 support + // make the ID clickable. Without color (no terminal, NO_COLOR) it stays plain + // text, as the core CLI does. + reference(label, id, url) { + if (this.silent) return; + const linked = this.color ? `\x1B]8;;${url}\x1B\\${this.paint("cyan", id)}\x1B]8;;\x1B\\` : id; + log.message(`${this.paint("dim", `${label}:`)} ${linked}`, { output: this.output, spacing: 0 }); + } + // The closing block of a successful setup: the result, then the portal URL in a note. + ready(url) { + if (this.silent) return; + this.breakLine(); + const linked = this.color ? `\x1B]8;;${url}\x1B\\${this.paint("cyan", url)}\x1B]8;;\x1B\\` : url; + note(`${this.paint("dim", "Open portal:")} ${linked}`, "Portal URL", { output: this.output }); + } + // The closing line when the user cancels. Clack draws it in the frame. + cancelled(message) { + if (this.silent) return; + cancel(message, { output: this.output }); + } + // A table with a header row. Widths follow the longest cell, and the first + // column is the status symbol, colored by its text. + table(headers, rows) { + if (this.silent) return; + const widths = headers.map( + (header, column) => Math.max(header.length, ...rows.map((row) => row[column].length)) + ); + const format = (cells, style) => cells.map((cell, column) => { + const padded = cell.padEnd(widths[column]); + return style ? style(padded, column) : padded; + }).join(" "); + log.message(this.paint("dim", format(headers)), { output: this.output, spacing: 0 }); + for (const row of rows) { + const status = row[0] === "\u2716" ? "red" : "yellow"; + log.message(format(row, (cell, column) => column === 0 ? this.paint(status, cell) : cell), { output: this.output, spacing: 0 }); + } + } + newline() { + if (this.silent) return; + log.message("", { output: this.output, spacing: 0 }); + } + // Runs work behind a spinner. The spinner line is cleared when the work ends, + // so the caller prints the result in its place. Without a terminal there is no + // spinner, only the work itself. + async task(label, work) { + if (this.silent || !this.animate) return work(); + const running = spinner({ output: this.output }); + running.start(label); + try { + return await work(); + } finally { + running.clear(); + this.spinnerGap = true; + } + } + // A blank line before a header, item or result. Right after a spinner the gap is + // already there, so no second one is drawn. + breakLine() { + if (this.spinnerGap) { + this.spinnerGap = false; + return; + } + log.message("", { output: this.output, spacing: 0 }); + } +}; + +// src/cli.ts +async function main(argv, env, io) { + let args; + try { + args = parseSetupArgs(argv); + } catch (error) { + io.stderr.write(`${describeError(error).message} +`); + return error instanceof UsageError ? 2 : 1; + } + if (args.command === "help") { + io.stdout.write(`${usage()} +`); + return 0; + } + const agentMode = args.json || env.AUTH0_AGENT_MODE === "1" || env.AUTH0_AGENT_MODE === "true"; + const interactive = !args.noInput && !agentMode && io.isTTY; + const ui = new Ui( + io.stderr, + colorEnabled(env, io.isTTY, args.noColor) && !agentMode, + agentMode, + io.isTTY && !agentMode + ); + const prompter = interactive ? io.prompter ?? clackPrompter(io.stdinStream, io.stderr) : void 0; + if (args.command === "audit") return runAuditCommand(args, env, io, ui, prompter); + return runSetupCommand(args, env, io, ui, interactive, prompter); +} +var entryPoint = process.argv[1] ? realpathSync(process.argv[1]) : void 0; +if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) { + main(process.argv.slice(2), process.env, { + stdout: process.stdout, + stderr: process.stderr, + stdinStream: process.stdin, + isTTY: Boolean(process.stdin.isTTY && process.stderr.isTTY) + }).then((code) => { + process.exitCode = code; + }); +} +export { + main +};