@supacloud/admin 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +156 -12
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -25791,6 +25791,8 @@ var ACTION_POLICY = {
|
|
|
25791
25791
|
read: [
|
|
25792
25792
|
"list",
|
|
25793
25793
|
"get",
|
|
25794
|
+
"endpoints",
|
|
25795
|
+
"list_endpoints",
|
|
25794
25796
|
"settings",
|
|
25795
25797
|
"api_keys",
|
|
25796
25798
|
"health",
|
|
@@ -25900,7 +25902,7 @@ function requestedProjectRef(moduleName, action, args) {
|
|
|
25900
25902
|
if (moduleName === "ssh")
|
|
25901
25903
|
return sshRequestedProjectRef(action, args);
|
|
25902
25904
|
if (moduleName === "project") {
|
|
25903
|
-
return ["list", "create"].includes(action) ? null : stringArgument(args, "ref");
|
|
25905
|
+
return ["list", "list_endpoints", "create"].includes(action) ? null : stringArgument(args, "ref");
|
|
25904
25906
|
}
|
|
25905
25907
|
if (moduleName === "platform") {
|
|
25906
25908
|
return PLATFORM_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "ref") : null;
|
|
@@ -26376,7 +26378,7 @@ import { Transform as Transform2 } from "node:stream";
|
|
|
26376
26378
|
var RELEASE_MANIFEST_NAME = "SUPACLOUD-RELEASE.json";
|
|
26377
26379
|
var RELEASE_ATTESTATION_NAME = "SUPACLOUD-RELEASE.attestation.jsonl";
|
|
26378
26380
|
var RELEASE_CHECKSUMS_NAME = "SHA256SUMS";
|
|
26379
|
-
var RELEASE_REPOSITORY = "
|
|
26381
|
+
var RELEASE_REPOSITORY = "vibeunion/supacloud";
|
|
26380
26382
|
var RELEASE_SOURCE_REF = "refs/heads/main";
|
|
26381
26383
|
var RELEASE_SIGNER_WORKFLOW = `${RELEASE_REPOSITORY}/.github/workflows/release-please.yml`;
|
|
26382
26384
|
var MEBIBYTE = 1024 * 1024;
|
|
@@ -27946,7 +27948,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
|
|
|
27946
27948
|
}
|
|
27947
27949
|
|
|
27948
27950
|
// ../../scripts/lib/release_assets.sh
|
|
27949
|
-
var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"\nSUPACLOUD_RELEASES_API="${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/releases}"\nSUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"\nSUPACLOUD_GH_VERSION="${SUPACLOUD_GH_VERSION:-2.96.0}"\nSUPACLOUD_GH_MIN_VERSION="${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"\nSUPACLOUD_GH_AMD64_SHA256="${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"\nSUPACLOUD_GH_ARM64_SHA256="${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"\nSUPACLOUD_RELEASE_ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nSUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT="${SUPACLOUD_RELEASE_ASSETS_DIR}/../../packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256="3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE="5748"\n\nsupacloud_curl_release_json() {\n local url="$1"\n local output="$2"\n curl -fsSL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 60 \\\n --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\\n -o "$output" "$url"\n}\n\nsupacloud_curl_release_asset() {\n local url="$1"\n local output="$2"\n curl -fL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 180 \\\n --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\\n -o "$output" "$url"\n}\n\nsupacloud_component_tag() {\n local component="$1"\n local version="$2"\n case "$version" in\n "${component}-v"*) printf \'%s\' "$version" ;;\n v*) printf \'%s-%s\' "$component" "$version" ;;\n *) printf \'%s-v%s\' "$component" "$version" ;;\n esac\n}\n\nsupacloud_select_release() {\n local component="$1"\n shift\n local required_assets_json\n [[ $# -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "$@" | jq -Rsc \'split("\\n")[:-1]\')\n jq -ce --arg prefix "${component}-v" --argjson required "$required_assets_json" \'\n map(select(\n (.draft | not)\n and (.prerelease | not)\n and (.tag_name | startswith($prefix))\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n ))\n | first\n // error("no matching component release contains all required assets and SHA256SUMS")\n \'\n}\n\nsupacloud_fetch_component_release() {\n local component="$1"\n local version="${2:-latest}"\n shift 2\n local required_assets=("$@")\n local required_assets_json\n local response\n [[ ${#required_assets[@]} -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "${required_assets[@]}" | jq -Rsc \'split("\\n")[:-1]\')\n\n if [[ -n "$version" && "$version" != "latest" ]]; then\n local tag\n tag=$(supacloud_component_tag "$component" "$version")\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}/tags/${tag}") || return 1\n jq -ce --argjson required "$required_assets_json" \'\n select(\n (.draft | not)\n and (.prerelease | not)\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n )\n // error("release does not contain all required assets and SHA256SUMS")\n \' <<< "$response"\n return\n fi\n\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}?per_page=100") || return 1\n supacloud_select_release "$component" "${required_assets[@]}" <<< "$response"\n}\n\nsupacloud_fetch_release_json() (\n local url="$1"\n local response_file\n response_file=$(mktemp) || return 1\n trap \'rm -f "$response_file"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1\' HUP INT TERM\n supacloud_download_release_metadata_url "$url" "$response_file" || return 1\n cat "$response_file"\n)\n\nsupacloud_release_asset_url() {\n local release_json="$1"\n local asset_name="$2"\n jq -er --arg asset "$asset_name" \'\n first(.assets[]? | select(.name == $asset) | .browser_download_url)\n // error("release asset URL is missing")\n \' <<< "$release_json"\n}\n\nsupacloud_download_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_asset "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_asset "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_download_release_metadata_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_json "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_json "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_verify_checksum() {\n local artifact_file="$1"\n local asset_name="$2"\n local checksum_file="$3"\n local expected\n expected=$(awk -v asset="$asset_name" \'$2 == asset || $2 == "*" asset { print $1; exit }\' "$checksum_file")\n if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then\n echo "SHA256SUMS does not contain a valid checksum for ${asset_name}" >&2\n return 1\n fi\n\n local actual\n actual=$(sha256sum "$artifact_file" | awk \'{print $1}\')\n actual=$(printf \'%s\' "$actual" | tr \'[:upper:]\' \'[:lower:]\')\n expected=$(printf \'%s\' "$expected" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ "$actual" != "$expected" ]]; then\n echo "SHA256 mismatch for ${asset_name}" >&2\n return 1\n fi\n}\n\nsupacloud_validate_binary() {\n local artifact_file="$1"\n local asset_name="$2"\n local description\n description=$(file -b "$artifact_file")\n if [[ "$description" != *ELF* ]]; then\n echo "${asset_name} is not an ELF binary: ${description}" >&2\n return 1\n fi\n\n case "$asset_name" in\n *amd64)\n [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {\n echo "${asset_name} does not contain an x86-64 ELF binary" >&2\n return 1\n }\n ;;\n *arm64)\n [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {\n echo "${asset_name} does not contain an arm64 ELF binary" >&2\n return 1\n }\n ;;\n esac\n}\n\nsupacloud_install_pinned_tar_xz_binary() (\n local archive="$1"\n local member="$2"\n local expected_sha256="$3"\n local arch="$4"\n local target="$5"\n local actual_sha256 member_count member_details extract_dir candidate staged_target\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n actual_sha256=$(printf \'%s\' "$actual_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n expected_sha256=$(printf \'%s\' "$expected_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then\n echo "SHA256 mismatch for pinned archive" >&2\n return 1\n fi\n\n member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "Pinned archive must contain the exact member once: $member" >&2\n return 1\n fi\n member_details=$(tar -tvJf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "Pinned archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extract_dir=$(mktemp -d)\n trap \'rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"; exit 1\' HUP INT TERM\n if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then\n return 1\n fi\n candidate="${extract_dir}/${member}"\n supacloud_validate_binary "$candidate" "pinned-linux-${arch}" || return 1\n\n mkdir -p "$(dirname "$target")"\n staged_target=$(mktemp "${target}.tmp.XXXXXX")\n install -m 0755 "$candidate" "$staged_target"\n mv -f "$staged_target" "$target"\n staged_target=""\n)\n\nsupacloud_version_at_least() {\n local current="${1#v}"\n local required="${2#v}"\n local current_major=0 current_minor=0 current_patch=0\n local required_major=0 required_minor=0 required_patch=0\n [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n IFS=. read -r current_major current_minor current_patch <<EOF\n${current%%-*}\nEOF\n IFS=. read -r required_major required_minor required_patch <<EOF\n${required%%-*}\nEOF\n current_major=${current_major:-0}; current_minor=${current_minor:-0}; current_patch=${current_patch:-0}\n required_major=${required_major:-0}; required_minor=${required_minor:-0}; required_patch=${required_patch:-0}\n if (( current_major != required_major )); then (( current_major > required_major )); return; fi\n if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi\n (( current_patch >= required_patch ))\n}\n\nsupacloud_gh_version() {\n gh --version 2>/dev/null | awk \'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }\'\n}\n\nsupacloud_install_gh_archive() {\n local archive="$1"\n local version="$2"\n local arch="$3"\n local expected_sha256="$4"\n local target="$5"\n local member="gh_${version}_linux_${arch}/bin/gh"\n local actual_sha256 member_count member_details extracted_dir candidate version_output\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n if [[ "$actual_sha256" != "$expected_sha256" ]]; then\n echo "GitHub CLI archive SHA256 mismatch" >&2\n return 1\n fi\n\n member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "GitHub CLI archive does not contain the exact expected member: $member" >&2\n return 1\n fi\n member_details=$(tar -tvzf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "GitHub CLI archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extracted_dir=$(mktemp -d)\n candidate="${extracted_dir}/${member}"\n if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\\n || ! supacloud_validate_binary "$candidate" "gh-linux-${arch}"; then\n rm -rf "$extracted_dir"\n return 1\n fi\n chmod 0755 "$candidate"\n version_output=$("$candidate" --version 2>/dev/null | head -1) || {\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap binary failed its version check" >&2\n return 1\n }\n if [[ "$version_output" != "gh version ${version}"* ]]; then\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap version mismatch: ${version_output}" >&2\n return 1\n fi\n mkdir -p "$(dirname "$target")"\n install -m 0755 "$candidate" "$target"\n rm -rf "$extracted_dir"\n}\n\nsupacloud_install_pinned_gh() {\n local target="${1:-/usr/local/bin/gh}"\n local machine arch expected_sha256 asset url archive\n machine=$(uname -m)\n case "$machine" in\n x86_64|amd64)\n arch="amd64"\n expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"\n ;;\n aarch64|arm64)\n arch="arm64"\n expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"\n ;;\n *)\n echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2\n return 1\n ;;\n esac\n asset="gh_${SUPACLOUD_GH_VERSION}_linux_${arch}.tar.gz"\n url="https://github.com/cli/cli/releases/download/v${SUPACLOUD_GH_VERSION}/${asset}"\n archive=$(mktemp)\n if ! supacloud_download_url "$url" "$archive" \\\n || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then\n rm -f "$archive"\n return 1\n fi\n rm -f "$archive"\n}\n\nsupacloud_validate_tar() {\n local artifact_file="$1"\n local entries\n entries=$(tar -tzf "$artifact_file") || {\n echo "Web Console archive is not a readable gzip tarball" >&2\n return 1\n }\n if ! printf \'%s\\n\' "$entries" | awk \'\n /^\\// { exit 1 }\n /(^|\\/)\\.\\.($|\\/)/ { exit 1 }\n \'; then\n echo "Web Console archive contains an unsafe path" >&2\n return 1\n fi\n if ! tar -tvzf "$artifact_file" | awk \'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }\'; then\n echo "Web Console archive contains links or special files" >&2\n return 1\n fi\n printf \'%s\\n\' "$entries" | grep -Eq \'(^|/)index\\.html$\' || {\n echo "Web Console archive is invalid or does not contain index.html" >&2\n return 1\n }\n}\n\nsupacloud_record_integrity_mode() {\n local mode="$1"\n local record_file="${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"\n mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0\n printf \'%s\\n\' "$mode" > "$record_file" 2>/dev/null || return 0\n chmod 600 "$record_file" 2>/dev/null || true\n}\n\nsupacloud_fetch_attestation_bundle() {\n local artifact_file="$1"\n local bundle_file="$2"\n local digest response\n digest=$(sha256sum "$artifact_file" | awk \'{print $1}\') || return 1\n [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {\n echo "Unable to calculate the artifact digest for attestation lookup" >&2\n return 1\n }\n digest=$(printf \'%s\' "$digest" | tr \'[:upper:]\' \'[:lower:]\')\n response=$(supacloud_fetch_release_json \\\n "https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:${digest}") || {\n echo "Unable to download the public GitHub artifact attestation bundle" >&2\n return 1\n }\n if ! jq -ce \'\n .attestations\n | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")\n then error("no valid attestation bundles returned")\n else .[].bundle\n end\n \' <<< "$response" > "$bundle_file"; then\n echo "GitHub artifact attestation response did not contain a valid bundle" >&2\n return 1\n fi\n}\n\nsupacloud_attestation_trusted_root_available() {\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n local actual_size actual_sha256\n [[ "$trusted_root" == /* && -f "$trusted_root" && ! -L "$trusted_root" ]] || return 1\n actual_size=$(wc -c < "$trusted_root" | tr -d \'[:space:]\') || return 1\n [[ "$actual_size" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n actual_sha256=$(sha256sum "$trusted_root" | awk \'{print $1}\') || return 1\n [[ "$actual_sha256" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]] || return 1\n [[ "$(wc -l < "$trusted_root" | tr -d \'[:space:]\')" == "1" ]] || return 1\n jq -e \'type == "object" and .mediaType == "application/vnd.dev.sigstore.trustedroot+json;version=0.1"\' \\\n "$trusted_root" >/dev/null 2>&1\n}\n\nsupacloud_prepare_attestation_trusted_root() {\n local destination="$1"\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n supacloud_attestation_trusted_root_available || {\n echo "Pinned Sigstore Public Good trusted root is missing or invalid" >&2\n return 1\n }\n jq -ce . "$trusted_root" > "$destination" || return 1\n chmod 600 "$destination"\n [[ "$(wc -c < "$destination" | tr -d \'[:space:]\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n [[ "$(sha256sum "$destination" | awk \'{print $1}\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]]\n}\n\nsupacloud_verify_attestation() (\n local artifact_file="$1"\n if supacloud_attestation_verifier_available; then\n local verification_output bundle_dir bundle_file trusted_root_file\n bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1\n trap \'rm -rf -- "$bundle_dir"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1\' HUP INT TERM\n bundle_file="${bundle_dir}/bundle.jsonl"\n trusted_root_file="${bundle_dir}/trusted_root.jsonl"\n if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then\n return 1\n fi\n supacloud_prepare_attestation_trusted_root "$trusted_root_file" || return 1\n if ! verification_output=$(gh attestation verify "$artifact_file" \\\n --bundle "$bundle_file" \\\n --custom-trusted-root "$trusted_root_file" \\\n --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\\n --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\\n --source-ref "refs/heads/main" \\\n --deny-self-hosted-runners 2>&1); then\n echo "GitHub artifact attestation verification failed: ${verification_output}" >&2\n return 1\n fi\n supacloud_record_integrity_mode "github-attestation+same-release-sha256"\n return\n fi\n\n if [[ "${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then\n echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2\n supacloud_record_integrity_mode "break-glass:same-release-sha256-only"\n return 0\n fi\n\n echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2\n return 1\n)\n\nsupacloud_attestation_verifier_available() {\n local version help\n supacloud_attestation_trusted_root_available || return 1\n command -v gh >/dev/null 2>&1 || return 1\n version=$(supacloud_gh_version)\n [[ -n "$version" ]] || return 1\n supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1\n help=$(gh attestation verify --help 2>&1) || return 1\n grep -Eq -- \'(^|[[:space:]])--bundle([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--signer-workflow([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--source-ref([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--custom-trusted-root([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)\' <<< "$help"\n}\n\nsupacloud_download_release_asset() (\n local release_json="$1"\n local asset_name="$2"\n local destination="$3"\n local asset_kind="$4"\n local asset_url checksum_url temporary_artifact temporary_checksums\n\n asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1\n checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1\n mkdir -p "$(dirname "$destination")"\n temporary_artifact=$(mktemp "${destination}.tmp.XXXXXX")\n temporary_checksums=$(mktemp "${destination}.SHA256SUMS.tmp.XXXXXX")\n trap \'rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"; exit 1\' HUP INT TERM\n\n if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\\n || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\\n || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n # Authenticate the digest before parsing archives or inspecting binaries.\n if ! supacloud_verify_attestation "$temporary_artifact"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n case "$asset_kind" in\n binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;\n tar) supacloud_validate_tar "$temporary_artifact" ;;\n *)\n echo "Unknown release asset kind: $asset_kind" >&2\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n ;;\n esac || {\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n }\n\n mv -f "$temporary_artifact" "$destination"\n temporary_artifact=""\n rm -f "$temporary_checksums"\n temporary_checksums=""\n)\n';
|
|
27951
|
+
var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-vibeunion/supacloud}"\nSUPACLOUD_RELEASES_API="${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/releases}"\nSUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"\nSUPACLOUD_GH_VERSION="${SUPACLOUD_GH_VERSION:-2.96.0}"\nSUPACLOUD_GH_MIN_VERSION="${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"\nSUPACLOUD_GH_AMD64_SHA256="${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"\nSUPACLOUD_GH_ARM64_SHA256="${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"\nSUPACLOUD_RELEASE_ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nSUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT="${SUPACLOUD_RELEASE_ASSETS_DIR}/../../packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256="3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE="5748"\n\nsupacloud_curl_release_json() {\n local url="$1"\n local output="$2"\n curl -fsSL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 60 \\\n --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\\n -o "$output" "$url"\n}\n\nsupacloud_curl_release_asset() {\n local url="$1"\n local output="$2"\n curl -fL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 180 \\\n --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\\n -o "$output" "$url"\n}\n\nsupacloud_component_tag() {\n local component="$1"\n local version="$2"\n case "$version" in\n "${component}-v"*) printf \'%s\' "$version" ;;\n v*) printf \'%s-%s\' "$component" "$version" ;;\n *) printf \'%s-v%s\' "$component" "$version" ;;\n esac\n}\n\nsupacloud_select_release() {\n local component="$1"\n shift\n local required_assets_json\n [[ $# -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "$@" | jq -Rsc \'split("\\n")[:-1]\')\n jq -ce --arg prefix "${component}-v" --argjson required "$required_assets_json" \'\n map(select(\n (.draft | not)\n and (.prerelease | not)\n and (.tag_name | startswith($prefix))\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n ))\n | first\n // error("no matching component release contains all required assets and SHA256SUMS")\n \'\n}\n\nsupacloud_fetch_component_release() {\n local component="$1"\n local version="${2:-latest}"\n shift 2\n local required_assets=("$@")\n local required_assets_json\n local response\n [[ ${#required_assets[@]} -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "${required_assets[@]}" | jq -Rsc \'split("\\n")[:-1]\')\n\n if [[ -n "$version" && "$version" != "latest" ]]; then\n local tag\n tag=$(supacloud_component_tag "$component" "$version")\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}/tags/${tag}") || return 1\n jq -ce --argjson required "$required_assets_json" \'\n select(\n (.draft | not)\n and (.prerelease | not)\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n )\n // error("release does not contain all required assets and SHA256SUMS")\n \' <<< "$response"\n return\n fi\n\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}?per_page=100") || return 1\n supacloud_select_release "$component" "${required_assets[@]}" <<< "$response"\n}\n\nsupacloud_fetch_release_json() (\n local url="$1"\n local response_file\n response_file=$(mktemp) || return 1\n trap \'rm -f "$response_file"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1\' HUP INT TERM\n supacloud_download_release_metadata_url "$url" "$response_file" || return 1\n cat "$response_file"\n)\n\nsupacloud_release_asset_url() {\n local release_json="$1"\n local asset_name="$2"\n jq -er --arg asset "$asset_name" \'\n first(.assets[]? | select(.name == $asset) | .browser_download_url)\n // error("release asset URL is missing")\n \' <<< "$release_json"\n}\n\nsupacloud_download_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_asset "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_asset "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_download_release_metadata_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_json "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_json "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_verify_checksum() {\n local artifact_file="$1"\n local asset_name="$2"\n local checksum_file="$3"\n local expected\n expected=$(awk -v asset="$asset_name" \'$2 == asset || $2 == "*" asset { print $1; exit }\' "$checksum_file")\n if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then\n echo "SHA256SUMS does not contain a valid checksum for ${asset_name}" >&2\n return 1\n fi\n\n local actual\n actual=$(sha256sum "$artifact_file" | awk \'{print $1}\')\n actual=$(printf \'%s\' "$actual" | tr \'[:upper:]\' \'[:lower:]\')\n expected=$(printf \'%s\' "$expected" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ "$actual" != "$expected" ]]; then\n echo "SHA256 mismatch for ${asset_name}" >&2\n return 1\n fi\n}\n\nsupacloud_validate_binary() {\n local artifact_file="$1"\n local asset_name="$2"\n local description\n description=$(file -b "$artifact_file")\n if [[ "$description" != *ELF* ]]; then\n echo "${asset_name} is not an ELF binary: ${description}" >&2\n return 1\n fi\n\n case "$asset_name" in\n *amd64)\n [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {\n echo "${asset_name} does not contain an x86-64 ELF binary" >&2\n return 1\n }\n ;;\n *arm64)\n [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {\n echo "${asset_name} does not contain an arm64 ELF binary" >&2\n return 1\n }\n ;;\n esac\n}\n\nsupacloud_install_pinned_tar_xz_binary() (\n local archive="$1"\n local member="$2"\n local expected_sha256="$3"\n local arch="$4"\n local target="$5"\n local actual_sha256 member_count member_details extract_dir candidate staged_target\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n actual_sha256=$(printf \'%s\' "$actual_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n expected_sha256=$(printf \'%s\' "$expected_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then\n echo "SHA256 mismatch for pinned archive" >&2\n return 1\n fi\n\n member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "Pinned archive must contain the exact member once: $member" >&2\n return 1\n fi\n member_details=$(tar -tvJf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "Pinned archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extract_dir=$(mktemp -d)\n trap \'rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"; exit 1\' HUP INT TERM\n if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then\n return 1\n fi\n candidate="${extract_dir}/${member}"\n supacloud_validate_binary "$candidate" "pinned-linux-${arch}" || return 1\n\n mkdir -p "$(dirname "$target")"\n staged_target=$(mktemp "${target}.tmp.XXXXXX")\n install -m 0755 "$candidate" "$staged_target"\n mv -f "$staged_target" "$target"\n staged_target=""\n)\n\nsupacloud_version_at_least() {\n local current="${1#v}"\n local required="${2#v}"\n local current_major=0 current_minor=0 current_patch=0\n local required_major=0 required_minor=0 required_patch=0\n [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n IFS=. read -r current_major current_minor current_patch <<EOF\n${current%%-*}\nEOF\n IFS=. read -r required_major required_minor required_patch <<EOF\n${required%%-*}\nEOF\n current_major=${current_major:-0}; current_minor=${current_minor:-0}; current_patch=${current_patch:-0}\n required_major=${required_major:-0}; required_minor=${required_minor:-0}; required_patch=${required_patch:-0}\n if (( current_major != required_major )); then (( current_major > required_major )); return; fi\n if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi\n (( current_patch >= required_patch ))\n}\n\nsupacloud_gh_version() {\n gh --version 2>/dev/null | awk \'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }\'\n}\n\nsupacloud_install_gh_archive() {\n local archive="$1"\n local version="$2"\n local arch="$3"\n local expected_sha256="$4"\n local target="$5"\n local member="gh_${version}_linux_${arch}/bin/gh"\n local actual_sha256 member_count member_details extracted_dir candidate version_output\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n if [[ "$actual_sha256" != "$expected_sha256" ]]; then\n echo "GitHub CLI archive SHA256 mismatch" >&2\n return 1\n fi\n\n member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "GitHub CLI archive does not contain the exact expected member: $member" >&2\n return 1\n fi\n member_details=$(tar -tvzf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "GitHub CLI archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extracted_dir=$(mktemp -d)\n candidate="${extracted_dir}/${member}"\n if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\\n || ! supacloud_validate_binary "$candidate" "gh-linux-${arch}"; then\n rm -rf "$extracted_dir"\n return 1\n fi\n chmod 0755 "$candidate"\n version_output=$("$candidate" --version 2>/dev/null | head -1) || {\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap binary failed its version check" >&2\n return 1\n }\n if [[ "$version_output" != "gh version ${version}"* ]]; then\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap version mismatch: ${version_output}" >&2\n return 1\n fi\n mkdir -p "$(dirname "$target")"\n install -m 0755 "$candidate" "$target"\n rm -rf "$extracted_dir"\n}\n\nsupacloud_install_pinned_gh() {\n local target="${1:-/usr/local/bin/gh}"\n local machine arch expected_sha256 asset url archive\n machine=$(uname -m)\n case "$machine" in\n x86_64|amd64)\n arch="amd64"\n expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"\n ;;\n aarch64|arm64)\n arch="arm64"\n expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"\n ;;\n *)\n echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2\n return 1\n ;;\n esac\n asset="gh_${SUPACLOUD_GH_VERSION}_linux_${arch}.tar.gz"\n url="https://github.com/cli/cli/releases/download/v${SUPACLOUD_GH_VERSION}/${asset}"\n archive=$(mktemp)\n if ! supacloud_download_url "$url" "$archive" \\\n || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then\n rm -f "$archive"\n return 1\n fi\n rm -f "$archive"\n}\n\nsupacloud_validate_tar() {\n local artifact_file="$1"\n local entries\n entries=$(tar -tzf "$artifact_file") || {\n echo "Web Console archive is not a readable gzip tarball" >&2\n return 1\n }\n if ! printf \'%s\\n\' "$entries" | awk \'\n /^\\// { exit 1 }\n /(^|\\/)\\.\\.($|\\/)/ { exit 1 }\n \'; then\n echo "Web Console archive contains an unsafe path" >&2\n return 1\n fi\n if ! tar -tvzf "$artifact_file" | awk \'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }\'; then\n echo "Web Console archive contains links or special files" >&2\n return 1\n fi\n printf \'%s\\n\' "$entries" | grep -Eq \'(^|/)index\\.html$\' || {\n echo "Web Console archive is invalid or does not contain index.html" >&2\n return 1\n }\n}\n\nsupacloud_record_integrity_mode() {\n local mode="$1"\n local record_file="${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"\n mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0\n printf \'%s\\n\' "$mode" > "$record_file" 2>/dev/null || return 0\n chmod 600 "$record_file" 2>/dev/null || true\n}\n\nsupacloud_fetch_attestation_bundle() {\n local artifact_file="$1"\n local bundle_file="$2"\n local digest response\n digest=$(sha256sum "$artifact_file" | awk \'{print $1}\') || return 1\n [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {\n echo "Unable to calculate the artifact digest for attestation lookup" >&2\n return 1\n }\n digest=$(printf \'%s\' "$digest" | tr \'[:upper:]\' \'[:lower:]\')\n response=$(supacloud_fetch_release_json \\\n "https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:${digest}") || {\n echo "Unable to download the public GitHub artifact attestation bundle" >&2\n return 1\n }\n if ! jq -ce \'\n .attestations\n | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")\n then error("no valid attestation bundles returned")\n else .[].bundle\n end\n \' <<< "$response" > "$bundle_file"; then\n echo "GitHub artifact attestation response did not contain a valid bundle" >&2\n return 1\n fi\n}\n\nsupacloud_attestation_trusted_root_available() {\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n local actual_size actual_sha256\n [[ "$trusted_root" == /* && -f "$trusted_root" && ! -L "$trusted_root" ]] || return 1\n actual_size=$(wc -c < "$trusted_root" | tr -d \'[:space:]\') || return 1\n [[ "$actual_size" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n actual_sha256=$(sha256sum "$trusted_root" | awk \'{print $1}\') || return 1\n [[ "$actual_sha256" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]] || return 1\n [[ "$(wc -l < "$trusted_root" | tr -d \'[:space:]\')" == "1" ]] || return 1\n jq -e \'type == "object" and .mediaType == "application/vnd.dev.sigstore.trustedroot+json;version=0.1"\' \\\n "$trusted_root" >/dev/null 2>&1\n}\n\nsupacloud_prepare_attestation_trusted_root() {\n local destination="$1"\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n supacloud_attestation_trusted_root_available || {\n echo "Pinned Sigstore Public Good trusted root is missing or invalid" >&2\n return 1\n }\n jq -ce . "$trusted_root" > "$destination" || return 1\n chmod 600 "$destination"\n [[ "$(wc -c < "$destination" | tr -d \'[:space:]\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n [[ "$(sha256sum "$destination" | awk \'{print $1}\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]]\n}\n\nsupacloud_verify_attestation() (\n local artifact_file="$1"\n if supacloud_attestation_verifier_available; then\n local verification_output bundle_dir bundle_file trusted_root_file\n bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1\n trap \'rm -rf -- "$bundle_dir"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1\' HUP INT TERM\n bundle_file="${bundle_dir}/bundle.jsonl"\n trusted_root_file="${bundle_dir}/trusted_root.jsonl"\n if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then\n return 1\n fi\n supacloud_prepare_attestation_trusted_root "$trusted_root_file" || return 1\n if ! verification_output=$(gh attestation verify "$artifact_file" \\\n --bundle "$bundle_file" \\\n --custom-trusted-root "$trusted_root_file" \\\n --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\\n --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\\n --source-ref "refs/heads/main" \\\n --deny-self-hosted-runners 2>&1); then\n echo "GitHub artifact attestation verification failed: ${verification_output}" >&2\n return 1\n fi\n supacloud_record_integrity_mode "github-attestation+same-release-sha256"\n return\n fi\n\n if [[ "${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then\n echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2\n supacloud_record_integrity_mode "break-glass:same-release-sha256-only"\n return 0\n fi\n\n echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2\n return 1\n)\n\nsupacloud_attestation_verifier_available() {\n local version help\n supacloud_attestation_trusted_root_available || return 1\n command -v gh >/dev/null 2>&1 || return 1\n version=$(supacloud_gh_version)\n [[ -n "$version" ]] || return 1\n supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1\n help=$(gh attestation verify --help 2>&1) || return 1\n grep -Eq -- \'(^|[[:space:]])--bundle([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--signer-workflow([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--source-ref([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--custom-trusted-root([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)\' <<< "$help"\n}\n\nsupacloud_download_release_asset() (\n local release_json="$1"\n local asset_name="$2"\n local destination="$3"\n local asset_kind="$4"\n local asset_url checksum_url temporary_artifact temporary_checksums\n\n asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1\n checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1\n mkdir -p "$(dirname "$destination")"\n temporary_artifact=$(mktemp "${destination}.tmp.XXXXXX")\n temporary_checksums=$(mktemp "${destination}.SHA256SUMS.tmp.XXXXXX")\n trap \'rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"; exit 1\' HUP INT TERM\n\n if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\\n || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\\n || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n # Authenticate the digest before parsing archives or inspecting binaries.\n if ! supacloud_verify_attestation "$temporary_artifact"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n case "$asset_kind" in\n binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;\n tar) supacloud_validate_tar "$temporary_artifact" ;;\n *)\n echo "Unknown release asset kind: $asset_kind" >&2\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n ;;\n esac || {\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n }\n\n mv -f "$temporary_artifact" "$destination"\n temporary_artifact=""\n rm -f "$temporary_checksums"\n temporary_checksums=""\n)\n';
|
|
27950
27952
|
|
|
27951
27953
|
// src/shared/tools/ssh-tools.ts
|
|
27952
27954
|
var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
|
@@ -28907,7 +28909,7 @@ Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot,
|
|
|
28907
28909
|
const CONFIG = "/etc/supabase/install.env";
|
|
28908
28910
|
const INPUT = `/etc/supabase/.install-input-${installId}.env`;
|
|
28909
28911
|
const BOOTSTRAP = `/opt/.supacloud-bootstrap-${installId}`;
|
|
28910
|
-
const REPO = "https://github.com/
|
|
28912
|
+
const REPO = "https://github.com/vibeunion/supacloud.git";
|
|
28911
28913
|
const configuredProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : "direct";
|
|
28912
28914
|
const proxyDisabled = ["direct", "none"].includes(configuredProxy.toLowerCase());
|
|
28913
28915
|
const proxyPrefix = proxyDisabled ? "" : configuredProxy.endsWith("/") ? configuredProxy : `${configuredProxy}/`;
|
|
@@ -30427,6 +30429,133 @@ function projectGetRead(response, expectedRef) {
|
|
|
30427
30429
|
const project = projectDetails(response.data, expectedRef);
|
|
30428
30430
|
return project ? successfulResult(project) : failedResult("Invalid project response");
|
|
30429
30431
|
}
|
|
30432
|
+
// ../cli/src/shared/tools/project-endpoint-read.ts
|
|
30433
|
+
var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
30434
|
+
var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
30435
|
+
var PROJECT_REF_PATTERN2 = /^[a-z0-9-]{1,20}$/;
|
|
30436
|
+
var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
|
|
30437
|
+
var PROJECT_ENDPOINT_SOURCES = new Set([
|
|
30438
|
+
"explicit_api_domain",
|
|
30439
|
+
"explicit_auth_domain",
|
|
30440
|
+
"explicit_studio_domain",
|
|
30441
|
+
"custom_domain",
|
|
30442
|
+
"derived_api_domain",
|
|
30443
|
+
"generated"
|
|
30444
|
+
]);
|
|
30445
|
+
var ROOT_KEYS = new Set(["schema", "project_ref", "endpoints"]);
|
|
30446
|
+
var ENDPOINTS_KEYS = new Set(["api", "auth", "studio"]);
|
|
30447
|
+
var ENDPOINT_KEYS = new Set(["origin", "host", "scheme", "source", "aliases"]);
|
|
30448
|
+
var MAX_PROJECTS2 = 1e4;
|
|
30449
|
+
var MAX_ALIASES = 64;
|
|
30450
|
+
function plainRecord2(candidate) {
|
|
30451
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
30452
|
+
return null;
|
|
30453
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
30454
|
+
return prototype === Object.prototype || prototype === null ? candidate : null;
|
|
30455
|
+
}
|
|
30456
|
+
function hasOnlyKeys2(record, allowedKeys) {
|
|
30457
|
+
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
30458
|
+
}
|
|
30459
|
+
function boundedText2(candidate, maxLength) {
|
|
30460
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
|
|
30461
|
+
}
|
|
30462
|
+
function canonicalHost(candidate, scheme) {
|
|
30463
|
+
const host = boundedText2(candidate, 255);
|
|
30464
|
+
if (!host)
|
|
30465
|
+
return null;
|
|
30466
|
+
try {
|
|
30467
|
+
const parsed = new URL(`${scheme}://${host}`);
|
|
30468
|
+
return parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.host !== host ? null : host;
|
|
30469
|
+
} catch {
|
|
30470
|
+
return null;
|
|
30471
|
+
}
|
|
30472
|
+
}
|
|
30473
|
+
function projectEndpoint2(candidate) {
|
|
30474
|
+
const endpoint = plainRecord2(candidate);
|
|
30475
|
+
if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
|
|
30476
|
+
return null;
|
|
30477
|
+
const scheme = endpoint.scheme === "http" || endpoint.scheme === "https" ? endpoint.scheme : null;
|
|
30478
|
+
const origin = boundedText2(endpoint.origin, 2048);
|
|
30479
|
+
const source = boundedText2(endpoint.source, 64);
|
|
30480
|
+
if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
|
|
30481
|
+
return null;
|
|
30482
|
+
let parsedOrigin;
|
|
30483
|
+
try {
|
|
30484
|
+
parsedOrigin = new URL(origin);
|
|
30485
|
+
} catch {
|
|
30486
|
+
return null;
|
|
30487
|
+
}
|
|
30488
|
+
if (parsedOrigin.protocol !== `${scheme}:` || parsedOrigin.origin !== origin || parsedOrigin.username || parsedOrigin.password || parsedOrigin.pathname !== "/" || parsedOrigin.search || parsedOrigin.hash)
|
|
30489
|
+
return null;
|
|
30490
|
+
const host = canonicalHost(endpoint.host, scheme);
|
|
30491
|
+
if (!host || host !== parsedOrigin.host || !Array.isArray(endpoint.aliases) || endpoint.aliases.length > MAX_ALIASES)
|
|
30492
|
+
return null;
|
|
30493
|
+
const aliases = [];
|
|
30494
|
+
const seenAliases = new Set;
|
|
30495
|
+
for (const aliasCandidate of endpoint.aliases) {
|
|
30496
|
+
const alias = canonicalHost(aliasCandidate, scheme);
|
|
30497
|
+
if (!alias || alias === host || seenAliases.has(alias))
|
|
30498
|
+
return null;
|
|
30499
|
+
seenAliases.add(alias);
|
|
30500
|
+
aliases.push(alias);
|
|
30501
|
+
}
|
|
30502
|
+
return { origin, host, scheme, source, aliases };
|
|
30503
|
+
}
|
|
30504
|
+
function projectEndpointProjection(candidate) {
|
|
30505
|
+
const projection = plainRecord2(candidate);
|
|
30506
|
+
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN2.test(projection.project_ref))
|
|
30507
|
+
return null;
|
|
30508
|
+
const endpoints = plainRecord2(projection.endpoints);
|
|
30509
|
+
if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
|
|
30510
|
+
return null;
|
|
30511
|
+
const api = projectEndpoint2(endpoints.api);
|
|
30512
|
+
const auth = projectEndpoint2(endpoints.auth);
|
|
30513
|
+
const studio = projectEndpoint2(endpoints.studio);
|
|
30514
|
+
return api && auth && studio ? {
|
|
30515
|
+
schema: PROJECT_ENDPOINTS_SCHEMA,
|
|
30516
|
+
project_ref: projection.project_ref,
|
|
30517
|
+
endpoints: { api, auth, studio }
|
|
30518
|
+
} : null;
|
|
30519
|
+
}
|
|
30520
|
+
function validHttpStatus2(status) {
|
|
30521
|
+
return Number.isSafeInteger(status) && status >= 100 && status <= 599;
|
|
30522
|
+
}
|
|
30523
|
+
function successfulResponse2(response) {
|
|
30524
|
+
return response.ok === true && validHttpStatus2(response.status) && response.status >= 200 && response.status <= 299;
|
|
30525
|
+
}
|
|
30526
|
+
function failedResult2(message) {
|
|
30527
|
+
return { text: `❌ ${message}`, isError: true };
|
|
30528
|
+
}
|
|
30529
|
+
function failedHttpResult2(label, status) {
|
|
30530
|
+
return failedResult2(validHttpStatus2(status) ? `${label} request failed (${status})` : `${label} request failed`);
|
|
30531
|
+
}
|
|
30532
|
+
function successfulResult2(payload) {
|
|
30533
|
+
return { text: JSON.stringify(payload, null, 2), isError: false };
|
|
30534
|
+
}
|
|
30535
|
+
function projectEndpointRead(response, expectedRef) {
|
|
30536
|
+
if (!successfulResponse2(response))
|
|
30537
|
+
return failedHttpResult2("Project endpoints", response.status);
|
|
30538
|
+
const projection = projectEndpointProjection(response.data);
|
|
30539
|
+
return projection && projection.project_ref === expectedRef ? successfulResult2(projection) : failedResult2("Invalid project endpoint response");
|
|
30540
|
+
}
|
|
30541
|
+
function projectEndpointListRead(response) {
|
|
30542
|
+
if (!successfulResponse2(response))
|
|
30543
|
+
return failedHttpResult2("Project endpoint list", response.status);
|
|
30544
|
+
if (!Array.isArray(response.data) || response.data.length > MAX_PROJECTS2) {
|
|
30545
|
+
return failedResult2("Invalid project endpoint list response");
|
|
30546
|
+
}
|
|
30547
|
+
const projections = [];
|
|
30548
|
+
const refs = new Set;
|
|
30549
|
+
for (const candidate of response.data) {
|
|
30550
|
+
const projection = projectEndpointProjection(candidate);
|
|
30551
|
+
if (!projection || refs.has(projection.project_ref)) {
|
|
30552
|
+
return failedResult2("Invalid project endpoint list response");
|
|
30553
|
+
}
|
|
30554
|
+
refs.add(projection.project_ref);
|
|
30555
|
+
projections.push(projection);
|
|
30556
|
+
}
|
|
30557
|
+
return successfulResult2(projections);
|
|
30558
|
+
}
|
|
30430
30559
|
// src/shared/tools/project-runtime-snapshot.ts
|
|
30431
30560
|
var RUNTIME_SNAPSHOT_SCHEMA = "supacloud.runtime-snapshot.v1";
|
|
30432
30561
|
var ATTESTED_REVISION_PATTERN = /^hmac-sha256:[a-f0-9]{64}$/;
|
|
@@ -30914,13 +31043,18 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
30914
31043
|
throw new Error("'ref' is required for this action");
|
|
30915
31044
|
return ref;
|
|
30916
31045
|
}
|
|
31046
|
+
function projectEndpointProjectionPath(ref) {
|
|
31047
|
+
return `/v1/projects/${encodeURIComponent(ref)}/endpoint/projection`;
|
|
31048
|
+
}
|
|
30917
31049
|
function registerAdminProjectCliTools(server, http, options = {}) {
|
|
30918
31050
|
const fileOperations = options.projectEnvFileOperations;
|
|
30919
|
-
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, runtime_snapshot, service_control", {
|
|
31051
|
+
server.tool("project", "Platform-level project lifecycle management. Actions: list, list_endpoints, create, get, endpoints, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, runtime_snapshot, service_control", {
|
|
30920
31052
|
action: withDescription(stringEnum([
|
|
30921
31053
|
"list",
|
|
31054
|
+
"list_endpoints",
|
|
30922
31055
|
"create",
|
|
30923
31056
|
"get",
|
|
31057
|
+
"endpoints",
|
|
30924
31058
|
"delete",
|
|
30925
31059
|
"pause",
|
|
30926
31060
|
"restore",
|
|
@@ -30935,7 +31069,7 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30935
31069
|
"runtime_snapshot",
|
|
30936
31070
|
"service_control"
|
|
30937
31071
|
]), "Action to perform"),
|
|
30938
|
-
ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
|
|
31072
|
+
ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list', 'list_endpoints', and 'create')"),
|
|
30939
31073
|
name: optional(Type.String(), "[create] Project name"),
|
|
30940
31074
|
region: optional(Type.String(), "[create] Region (default: local)"),
|
|
30941
31075
|
organization_id: optional(Type.String(), "[create] Organization ID"),
|
|
@@ -30972,6 +31106,10 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
30972
31106
|
return projectReadResponse(projectListRead(await http.get("/v1/projects", {
|
|
30973
31107
|
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
30974
31108
|
})));
|
|
31109
|
+
case "list_endpoints":
|
|
31110
|
+
return projectReadResponse(projectEndpointListRead(await http.get("/v1/projects/endpoints", {
|
|
31111
|
+
maxResponseBytes: PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES
|
|
31112
|
+
})));
|
|
30975
31113
|
case "create": {
|
|
30976
31114
|
if (!name)
|
|
30977
31115
|
throw new Error("'name' is required for create");
|
|
@@ -31015,6 +31153,12 @@ function registerAdminProjectCliTools(server, http, options = {}) {
|
|
|
31015
31153
|
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
31016
31154
|
}), resolvedRef));
|
|
31017
31155
|
}
|
|
31156
|
+
case "endpoints": {
|
|
31157
|
+
const resolvedRef = resolveRef(ref);
|
|
31158
|
+
return projectReadResponse(projectEndpointRead(await http.get(projectEndpointProjectionPath(resolvedRef), {
|
|
31159
|
+
maxResponseBytes: PROJECT_ENDPOINT_RESPONSE_MAX_BYTES
|
|
31160
|
+
}), resolvedRef));
|
|
31161
|
+
}
|
|
31018
31162
|
case "delete": {
|
|
31019
31163
|
const resolvedRef = resolveRef(ref);
|
|
31020
31164
|
text = simple(await http.delete(`/v1/projects/${resolvedRef}`), `Project ${resolvedRef} deleted`);
|
|
@@ -31425,7 +31569,7 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
31425
31569
|
import { constants as fsConstants2 } from "node:fs";
|
|
31426
31570
|
import { open as open2 } from "node:fs/promises";
|
|
31427
31571
|
import { resolve as resolve3 } from "node:path";
|
|
31428
|
-
var
|
|
31572
|
+
var PROJECT_REF_PATTERN3 = /^[A-Za-z0-9_-]{1,20}$/u;
|
|
31429
31573
|
var DEPLOYMENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/u;
|
|
31430
31574
|
var RELEASE_ID_PATTERN = /^[0-9a-f]{64}$/u;
|
|
31431
31575
|
var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
@@ -31468,7 +31612,7 @@ function releaseRecord(candidate) {
|
|
|
31468
31612
|
"created_at",
|
|
31469
31613
|
"kind"
|
|
31470
31614
|
];
|
|
31471
|
-
if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !
|
|
31615
|
+
if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp3(record.created_at) || record.kind !== "prebuilt_static")
|
|
31472
31616
|
return null;
|
|
31473
31617
|
return {
|
|
31474
31618
|
project_ref: record.project_ref,
|
|
@@ -31514,7 +31658,7 @@ function releaseInventory(candidate, expected) {
|
|
|
31514
31658
|
"releases",
|
|
31515
31659
|
"next_cursor"
|
|
31516
31660
|
];
|
|
31517
|
-
if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !
|
|
31661
|
+
if (!exactKeys(record, keys) || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || !Array.isArray(record.releases))
|
|
31518
31662
|
return null;
|
|
31519
31663
|
const activeReleaseId = nullableIdentity(record.active_release_id, RELEASE_ID_PATTERN);
|
|
31520
31664
|
const activeActivationId = nullableIdentity(record.active_activation_id, MUTATION_ID_PATTERN);
|
|
@@ -31535,7 +31679,7 @@ function releaseInventory(candidate, expected) {
|
|
|
31535
31679
|
};
|
|
31536
31680
|
}
|
|
31537
31681
|
function releaseEndpoint(projectRef, deploymentId) {
|
|
31538
|
-
if (!
|
|
31682
|
+
if (!PROJECT_REF_PATTERN3.test(projectRef))
|
|
31539
31683
|
throw new Error("'ref' is invalid for frontend releases");
|
|
31540
31684
|
if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
|
|
31541
31685
|
throw new Error("'id' is invalid for frontend releases");
|
|
@@ -31896,7 +32040,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
|
|
|
31896
32040
|
// package.json
|
|
31897
32041
|
var package_default = {
|
|
31898
32042
|
name: "@supacloud/admin",
|
|
31899
|
-
version: "0.
|
|
32043
|
+
version: "0.15.0",
|
|
31900
32044
|
description: "Platform administration CLI for SupaCloud operators",
|
|
31901
32045
|
type: "module",
|
|
31902
32046
|
main: "./dist/index.js",
|
|
@@ -31922,7 +32066,7 @@ var package_default = {
|
|
|
31922
32066
|
license: "MIT",
|
|
31923
32067
|
repository: {
|
|
31924
32068
|
type: "git",
|
|
31925
|
-
url: "https://github.com/
|
|
32069
|
+
url: "https://github.com/vibeunion/supacloud.git",
|
|
31926
32070
|
directory: "packages/admin"
|
|
31927
32071
|
},
|
|
31928
32072
|
dependencies: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Platform administration CLI for SupaCloud operators",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/
|
|
29
|
+
"url": "https://github.com/vibeunion/supacloud.git",
|
|
30
30
|
"directory": "packages/admin"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|