diff --git a/.agents/skills/develop-maple-proxy/SKILL.md b/.agents/skills/develop-maple-proxy/SKILL.md index 42890b9bd..1b9d84cc6 100644 --- a/.agents/skills/develop-maple-proxy/SKILL.md +++ b/.agents/skills/develop-maple-proxy/SKILL.md @@ -5,7 +5,7 @@ description: Develop and review the maple-proxy Rust crate, binary, container, a # Develop Maple Proxy -Work from the `OpenSecretCloud/Maple` repository root; the component source is +Work from the `MaplePrivacyLabs/Maple` repository root; the component source is under `proxy/`. Read the repository-root `AGENTS.md`, `proxy/README.md`, affected source and tests, and the root `.github/workflows/proxy-*.yml` files relevant to the change. @@ -108,11 +108,11 @@ Release, `.github/workflows/proxy-publish.yml` independently compares that release's proxy version with the previous stable Maple Release. Unchanged versions skip, including the unbackfilled 0.3.3 baseline. A strictly newer, previously unpublished version automatically publishes Linux AMD64/ARM64 to -`ghcr.io/opensecretcloud/maple-proxy` with exact, minor, major, and `latest` +`ghcr.io/mapleprivacylabs/maple-proxy` with exact, minor, major, and `latest` tags. The workflow is serialized, rejects rollback and stale releases, verifies -the public manifest, and supports manual retry from `master`. Maple's Actions -repository must retain write access to that existing organization-scoped GHCR -package. Treat any namespace, trigger, version policy, or package-access change +the public manifest, and supports manual retry from `master`. The transferred Maple repository uses its `GITHUB_TOKEN` to publish in +`MaplePrivacyLabs`; the new package must be public and grant Maple Actions write +access. Existing old-namespace images remain available but receive no updates. Treat any namespace, trigger, version policy, or package-access change as a separate production-authority decision. ## Report diff --git a/.agents/skills/develop-maple/SKILL.md b/.agents/skills/develop-maple/SKILL.md index a0a8791a9..7d2e7fc48 100644 --- a/.agents/skills/develop-maple/SKILL.md +++ b/.agents/skills/develop-maple/SKILL.md @@ -5,7 +5,7 @@ description: Develop and debug ordinary non-Agent-Mode Maple features and fixes # Develop Maple -Work from the `OpenSecretCloud/Maple` repository root. Treat `justfile`, `frontend/package.json`, `flake.nix`, `scripts/ci/`, and `.github/workflows/` as the command sources of truth. Check them again when they disagree with prose documentation. +Work from the `MaplePrivacyLabs/Maple` repository root. Treat `justfile`, `frontend/package.json`, `flake.nix`, `scripts/ci/`, and `.github/workflows/` as the command sources of truth. Check them again when they disagree with prose documentation. ## Route Specialized Work diff --git a/.agents/skills/develop-opensecret-sdk/SKILL.md b/.agents/skills/develop-opensecret-sdk/SKILL.md index 4cc4daad3..db0285499 100644 --- a/.agents/skills/develop-opensecret-sdk/SKILL.md +++ b/.agents/skills/develop-opensecret-sdk/SKILL.md @@ -5,7 +5,7 @@ description: Develop and review the OpenSecret TypeScript/React and Rust SDKs un # Develop the OpenSecret SDK -Work from `OpenSecretCloud/Maple/sdk`. Read the repository-root `AGENTS.md`, +Work from `MaplePrivacyLabs/Maple/sdk`. Read the repository-root `AGENTS.md`, `sdk/README.md`, the affected implementation and tests, and the root `.github/workflows/sdk-*.yml` files relevant to the change. diff --git a/.agents/skills/release-maple/SKILL.md b/.agents/skills/release-maple/SKILL.md index f04426a48..ab0c993c5 100644 --- a/.agents/skills/release-maple/SKILL.md +++ b/.agents/skills/release-maple/SKILL.md @@ -33,13 +33,30 @@ commit, external effect, and authority provided by the user. and repairs minor, major, and `latest` aliases without rebuilding an existing exact image. Version `0.3.3` is the explicit unbackfilled migration baseline. The container remains separately versioned at - `ghcr.io/opensecretcloud/maple-proxy`. + `ghcr.io/mapleprivacylabs/maple-proxy`. - GitHub Release creation does not itself submit the release IPA or AAB to Apple App Store review or Google Play. Never push or merge `master`, create a release, retry a workflow, upload to a store, submit for review, or alter a rollout merely to see whether it works. +## Repository-transfer checkpoint + +After the transfer to `MaplePrivacyLabs/Maple`, merge the prepared canonical +repository metadata before creating a new release. Keep the existing updater +Worker serving its deployed metadata until the first normal new-org release. +Do not manually republish retained v3.3.10 updater metadata: its asset URLs use +`OpenSecretCloud/Maple`, while the publisher and new Worker correctly require +the current canonical owner. The next release generates new-owner URLs without +changing the legacy updater fallback compiled into existing clients. + +The first eligible proxy container publication creates +`ghcr.io/mapleprivacylabs/maple-proxy`. GitHub creates new packages privately; +make that package public in its settings, retain Maple Actions write access, +and rerun only the proxy publisher if anonymous verification stops there. Do +not create another release, change proxy versions, or overwrite exact tags to +repair package visibility. Existing old-namespace images receive no updates. + ## Prepare the version 1. Prepare the bump on a clean focused branch based on current `origin/master`; @@ -50,7 +67,7 @@ store, submit for review, or alter a rollout merely to see whether it works. git fetch origin master git merge-base --is-ancestor origin/master HEAD current_version="$(nix develop --no-update-lock-file .#ci -c just get-version | tail -n 1)" - released_version="$(gh api repos/OpenSecretCloud/Maple/releases/latest --jq '.tag_name | ltrimstr("v")')" + released_version="$(gh api repos/MaplePrivacyLabs/Maple/releases/latest --jq '.tag_name | ltrimstr("v")')" printf 'current=%s released=%s\n' "$current_version" "$released_version" ``` @@ -112,7 +129,7 @@ crates.io or GHCR publish. Preview GitHub's generated notes: ```bash -gh api --method POST repos/OpenSecretCloud/Maple/releases/generate-notes \ +gh api --method POST repos/MaplePrivacyLabs/Maple/releases/generate-notes \ -f tag_name="$tag" \ -f target_commitish="$head_sha" \ -f previous_tag_name="$previous_tag" | jq -r '.name, .body' @@ -134,7 +151,7 @@ Create the GitHub Release exactly once. This creates the tag in the same flow: ```bash gh release create "$tag" \ - --repo OpenSecretCloud/Maple \ + --repo MaplePrivacyLabs/Maple \ --target "$head_sha" \ --title "$tag" \ --notes-file "$notes_file" @@ -148,12 +165,12 @@ release and workflow resolve to `head_sha`. Find and watch the new `Release` run: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow Release --event release \ +gh run list --repo MaplePrivacyLabs/Maple --workflow Release --event release \ --commit "$head_sha" --limit 10 \ --json databaseId,displayTitle,headSha,status,conclusion,url gh run watch RELEASE_RUN_ID \ - --repo OpenSecretCloud/Maple --exit-status --compact + --repo MaplePrivacyLabs/Maple --exit-status --compact ``` Stay with every platform build, signature/canonical proof, artifact upload, @@ -166,17 +183,17 @@ After `Release` succeeds, inspect the two required publication handoffs. Do not rerun the core Release to repair either sibling: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish updater metadata' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish updater metadata' \ --commit "$head_sha" --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url pages_workflow='Promote Pages production' -pages_enabled="$(gh variable list --repo OpenSecretCloud/Maple --json name,value \ +pages_enabled="$(gh variable list --repo MaplePrivacyLabs/Maple --json name,value \ --jq '.[] | select(.name == "MAPLE_PAGES_PRODUCTION_ENABLED") | .value')" || exit 1 if [ "$pages_enabled" = true ]; then pages_workflow='Publish Pages' fi -gh run list --repo OpenSecretCloud/Maple --workflow "$pages_workflow" \ +gh run list --repo MaplePrivacyLabs/Maple --workflow "$pages_workflow" \ --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` @@ -192,7 +209,7 @@ the unbackfilled `0.3.3` baseline. Retry it with manual dispatch; never create a proxy tag or Release and never rerun the core Release to repair it: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish proxy container' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish proxy container' \ --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` @@ -209,14 +226,14 @@ Report and repair sibling failures without altering completed release artifacts. Confirm the production ref in either mode: ```bash -pages_sha="$(gh api repos/OpenSecretCloud/Maple/git/ref/heads/pages-production --jq .object.sha)" +pages_sha="$(gh api repos/MaplePrivacyLabs/Maple/git/ref/heads/pages-production --jq .object.sha)" [[ "$pages_sha" == "$head_sha" ]] ``` In legacy mode only, inspect Cloudflare's exact-commit check: ```bash -gh api "repos/OpenSecretCloud/Maple/commits/$head_sha/check-runs" --jq ' +gh api "repos/MaplePrivacyLabs/Maple/commits/$head_sha/check-runs" --jq ' [.check_runs[] | select(.name == "Cloudflare Pages") | select(.app.name == "Cloudflare Workers and Pages") @@ -240,13 +257,13 @@ recreate a Release or rerun the core release merely to repair Pages. On failure, read the failed logs before acting: ```bash -gh run view RELEASE_RUN_ID --repo OpenSecretCloud/Maple --log-failed +gh run view RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --log-failed ``` Retry only a terminal failure proven to be transient infrastructure trouble: ```bash -gh run rerun RELEASE_RUN_ID --repo OpenSecretCloud/Maple --failed +gh run rerun RELEASE_RUN_ID --repo MaplePrivacyLabs/Maple --failed ``` Do not classify version/proof mismatches, deterministic builds, signing @@ -258,11 +275,11 @@ or recreate a published release without separate explicit direction. Verify the published release and its assets: ```bash -gh release view "$tag" --repo OpenSecretCloud/Maple \ +gh release view "$tag" --repo MaplePrivacyLabs/Maple \ --json tagName,name,isDraft,isPrerelease,publishedAt,targetCommitish,url,assets mkdir -p artifacts -gh release download "$tag" --repo OpenSecretCloud/Maple --dir artifacts +gh release download "$tag" --repo MaplePrivacyLabs/Maple --dir artifacts nix develop --no-update-lock-file .#ci -c \ ./scripts/ci/verify-release-artifacts.sh artifacts proxy ``` @@ -303,7 +320,7 @@ trigger a release retry, or be reported as a Maple release failure. Inspect it only when Zapstore status is specifically useful: ```bash -gh run list --repo OpenSecretCloud/Maple --workflow 'Publish to Zapstore' \ +gh run list --repo MaplePrivacyLabs/Maple --workflow 'Publish to Zapstore' \ --commit "$head_sha" --limit 10 \ --json databaseId,status,conclusion,headSha,createdAt,url ``` diff --git a/.agents/skills/release-maple/scripts/preflight.sh b/.agents/skills/release-maple/scripts/preflight.sh index 53944e010..3208e279b 100755 --- a/.agents/skills/release-maple/scripts/preflight.sh +++ b/.agents/skills/release-maple/scripts/preflight.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -readonly expected_repo="OpenSecretCloud/Maple" -readonly remote_url="https://github.com/OpenSecretCloud/Maple.git" +readonly expected_repo="MaplePrivacyLabs/Maple" +readonly remote_url="https://github.com/MaplePrivacyLabs/Maple.git" fail() { printf 'release preflight: %s\n' "$*" >&2 diff --git a/.github/workflows/proxy-publish.yml b/.github/workflows/proxy-publish.yml index daf4651bd..a016937b6 100644 --- a/.github/workflows/proxy-publish.yml +++ b/.github/workflows/proxy-publish.yml @@ -18,21 +18,28 @@ concurrency: env: REGISTRY: ghcr.io - IMAGE_NAME: opensecretcloud/maple-proxy + IMAGE_NAME: mapleprivacylabs/maple-proxy UNBACKFILLED_PROXY_BASELINE: 0.3.3 jobs: prepare: name: Validate proxy publication if: >- - (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master') || + github.repository == 'MaplePrivacyLabs/Maple' && + github.repository_id == '923138240' && + github.repository_owner_id == '322649754' && + github.ref == 'refs/heads/master' && + ((github.event_name == 'workflow_dispatch') || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release' && github.event.workflow_run.path == '.github/workflows/release.yml' && - github.event.workflow_run.head_repository.full_name == github.repository) + github.event.workflow_run.head_repository.full_name == github.repository)) runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + packages: read outputs: publish: ${{ steps.plan.outputs.publish }} reconcile: ${{ steps.plan.outputs.reconcile }} @@ -157,13 +164,9 @@ jobs: esac fi - registry_token="$( - curl --fail --silent --show-error \ - "https://${REGISTRY}/token?scope=repository:${IMAGE_NAME}:pull" | jq -er '.token' - )" - curl --fail --silent --show-error \ - --header "Authorization: Bearer ${registry_token}" \ - "https://${REGISTRY}/v2/${IMAGE_NAME}/tags/list?n=10000" \ + # Only authenticated package metadata can confirm initial creation. + # Anonymous denial/network errors must never mean an empty registry. + python3 -I scripts/ci/proxy_registry_inventory.py \ >"${RUNNER_TEMP}/proxy-tags.json" plan="${RUNNER_TEMP}/proxy-publish-plan" @@ -452,6 +455,10 @@ jobs: } assert_release_is_current + # GitHub creates a new container package as private. An operator must + # make it public before anonymous verification or alias publication. + # This workflow never changes package visibility on the user's behalf. + python3 -I scripts/ci/proxy_registry_inventory.py --require-public >/dev/null image="${REGISTRY}/${IMAGE_NAME}" source_digest_dir="" if [ "${PUBLISHED_NOW}" = "true" ]; then diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index d7bb8bcd0..4cea35304 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -4,7 +4,7 @@ version = "3.3.11" description = "Maple AI" authors = ["tony@trymaple.ai"] license = "MIT" -repository = "https://github.com/OpenSecretCloud/Maple" +repository = "https://github.com/MaplePrivacyLabs/Maple" edition = "2021" rust-version = "1.94.1" diff --git a/frontend/src/routes/proof.tsx b/frontend/src/routes/proof.tsx index aaf4aa3a6..cc6357017 100644 --- a/frontend/src/routes/proof.tsx +++ b/frontend/src/routes/proof.tsx @@ -612,7 +612,7 @@ function Verify() {

Both our{" "} Our code is open source:{" "} { test("builds the current Maple GitHub URLs from the shared repository identity", () => { - expect(GITHUB_REPOSITORY).toBe("OpenSecretCloud/Maple"); - expect(GITHUB_REPOSITORY_URL).toBe("https://github.com/OpenSecretCloud/Maple"); + expect(GITHUB_REPOSITORY).toBe("MaplePrivacyLabs/Maple"); + expect(GITHUB_REPOSITORY_URL).toBe("https://github.com/MaplePrivacyLabs/Maple"); expect(GITHUB_RELEASES_API_URL).toBe( - "https://api.github.com/repos/OpenSecretCloud/Maple/releases/latest" + "https://api.github.com/repos/MaplePrivacyLabs/Maple/releases/latest" ); expect(GITHUB_RELEASES_LATEST_URL).toBe( - "https://github.com/OpenSecretCloud/Maple/releases/latest" + "https://github.com/MaplePrivacyLabs/Maple/releases/latest" ); }); }); @@ -117,7 +117,7 @@ describe("getLatestDownloadInfo", () => { tag_name: "v9.9.9", name: "v9.9.9", published_at: "2026-08-23T00:00:00Z", - html_url: "https://github.com/OpenSecretCloud/Maple/releases/tag/v9.9.9", + html_url: "https://github.com/MaplePrivacyLabs/Maple/releases/tag/v9.9.9", assets: [ { name: "Maple_9.9.9_universal.dmg", @@ -144,7 +144,7 @@ describe("getLatestDownloadInfo", () => { linuxRpm: GITHUB_RELEASES_LATEST_URL, androidApk: GITHUB_RELEASES_LATEST_URL }, - releaseUrl: "https://github.com/OpenSecretCloud/Maple/releases/tag/v9.9.9" + releaseUrl: "https://github.com/MaplePrivacyLabs/Maple/releases/tag/v9.9.9" }); }); diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index c78dece6f..5b3132de3 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" authors = ["OpenSecret"] description = "Lightweight OpenAI-compatible proxy server for Maple/OpenSecret TEE infrastructure" license = "MIT" -repository = "https://github.com/OpenSecretCloud/Maple" -homepage = "https://github.com/OpenSecretCloud/Maple/tree/master/proxy" +repository = "https://github.com/MaplePrivacyLabs/Maple" +homepage = "https://github.com/MaplePrivacyLabs/Maple/tree/master/proxy" keywords = ["openai", "proxy", "tee", "opensecret", "maple"] categories = ["web-programming::http-server", "api-bindings"] include = [ diff --git a/proxy/Dockerfile b/proxy/Dockerfile index ca1f45c08..f2a5e9954 100644 --- a/proxy/Dockerfile +++ b/proxy/Dockerfile @@ -20,7 +20,7 @@ RUN cargo build --locked --release --bin maple-proxy # Runtime stage - minimal image for production FROM docker.io/debian:bookworm-slim AS runtime -LABEL org.opencontainers.image.source="https://github.com/OpenSecretCloud/Maple" +LABEL org.opencontainers.image.source="https://github.com/MaplePrivacyLabs/Maple" # Install runtime dependencies RUN apt-get update && apt-get install -y \ diff --git a/proxy/README.md b/proxy/README.md index a704a9f94..38f380928 100644 --- a/proxy/README.md +++ b/proxy/README.md @@ -28,8 +28,8 @@ After that release, verify the assets are present and use the stable download URLs, for example: ```bash -curl -LO https://github.com/OpenSecretCloud/Maple/releases/latest/download/maple-proxy-linux-x86_64.tar.gz -curl -LO https://github.com/OpenSecretCloud/Maple/releases/latest/download/maple-proxy-release-final.sha256 +curl -LO https://github.com/MaplePrivacyLabs/Maple/releases/latest/download/maple-proxy-linux-x86_64.tar.gz +curl -LO https://github.com/MaplePrivacyLabs/Maple/releases/latest/download/maple-proxy-release-final.sha256 sha256sum --check --ignore-missing maple-proxy-release-final.sha256 ``` @@ -37,7 +37,7 @@ There is no separate proxy GitHub Release or proxy release tag. To build from source now: ```bash -git clone https://github.com/OpenSecretCloud/Maple.git +git clone https://github.com/MaplePrivacyLabs/Maple.git cd Maple/proxy cargo build --locked --release ``` @@ -289,24 +289,34 @@ cargo run --locked ### Pre-built Image -The currently published GHCR `latest` image is the independently released -standalone proxy 0.3.2. The first Maple-owned publication deliberately does not -backfill the in-tree 0.3.3 version. After the next proxy version change ships in -a successful stable Maple Release, the release-following publisher updates the -same `ghcr.io/opensecretcloud/maple-proxy` package automatically. +New container publications use `ghcr.io/mapleprivacylabs/maple-proxy` after the +Maple repository transfer. The release-following publisher builds the eligible +proxy version from a successful stable Maple Release and verifies its exact tag, +platforms, provenance, and aliases. The first publication in this namespace must +complete and the package must be public before the commands below will work. +GitHub creates new packages privately. Once the first image exists, set the new +package to public in GitHub package settings, retain Maple Actions write access, +and rerun only the container publisher if anonymous verification was blocked. +The retry verifies an existing exact version rather than overwriting it. -To run the legacy image deliberately: +The old `ghcr.io/opensecretcloud/maple-proxy` package remains available at its +existing versions but receives no new publications. Existing deployments must +change their image URL to receive future updates; registry image names do not +follow the repository transfer redirect. Version `0.3.3` remains the explicit +unbackfilled baseline. The next prepared proxy version is `0.3.4`. + +After the first new-namespace publication is verified: ```bash # Pull the latest image -docker pull ghcr.io/opensecretcloud/maple-proxy:latest +docker pull ghcr.io/mapleprivacylabs/maple-proxy:latest # Run with your API key docker run -p 8080:8080 \ -e MAPLE_BACKEND_URL=https://enclave.trymaple.ai \ -e MAPLE_REQUEST_TIMEOUT_SECS=300 \ -e MAPLE_STREAM_IDLE_TIMEOUT_SECS=300 \ - ghcr.io/opensecretcloud/maple-proxy:latest + ghcr.io/mapleprivacylabs/maple-proxy:latest ``` ### Build from Source @@ -324,7 +334,7 @@ just docker-run 1. **Option A: Use the published image from GHCR** ```bash # In your docker-compose.yml, use: -image: ghcr.io/opensecretcloud/maple-proxy:latest +image: ghcr.io/mapleprivacylabs/maple-proxy:latest ``` 2. **Option B: Build your own image** @@ -412,7 +422,7 @@ environment: ### Docker Images & CI/CD -The source lives under [`proxy/`](https://github.com/OpenSecretCloud/Maple/tree/master/proxy) +The source lives under [`proxy/`](https://github.com/MaplePrivacyLabs/Maple/tree/master/proxy) in the Maple repository. Root, path-scoped workflows run locked Rust checks, supply-chain policy, and non-publishing AMD64/ARM64 container builds for proxy changes. After every successful stable Maple Release, a separate serialized @@ -423,8 +433,10 @@ later version publishes native AMD64/ARM64 images with per-platform provenance, verifies their exact build digests and release labels, then reconciles the minor, major, and `latest` aliases. Existing exact versions are verified without being overwritten, and manual dispatch safely retries verification or alias repair. -Maple must have Actions write access to the existing organization-scoped GHCR -package; local recipes intentionally cannot publish to it. +The transferred Maple repository publishes with its `GITHUB_TOKEN` in the +`MaplePrivacyLabs` organization. No old-organization package token is needed. +The package must grant Maple Actions write access and be public for anonymous +verification; local recipes intentionally cannot publish to it. `proxy/Cargo.lock`, `sdk/rust/Cargo.lock`, and `frontend/src-tauri/Cargo.lock` remain separate lockfiles. Runtime dependency diff --git a/proxy/justfile b/proxy/justfile index 016f3324e..8159c57b2 100644 --- a/proxy/justfile +++ b/proxy/justfile @@ -240,8 +240,8 @@ docker-clean: # Pull from GitHub Container Registry ghcr-pull tag="latest": @echo "📥 Pulling from GitHub Container Registry..." - @{{container}} pull ghcr.io/opensecretcloud/maple-proxy:{{tag}} - @echo "✅ Pulled ghcr.io/opensecretcloud/maple-proxy:{{tag}}" + @{{container}} pull ghcr.io/mapleprivacylabs/maple-proxy:{{tag}} + @echo "✅ Pulled ghcr.io/mapleprivacylabs/maple-proxy:{{tag}}" # Compatibility alias for the complete repository check. check-all: check diff --git a/repo.meta.json b/repo.meta.json index 23014371d..90bb069c1 100644 --- a/repo.meta.json +++ b/repo.meta.json @@ -1,6 +1,6 @@ { "github": { - "owner": "OpenSecretCloud", + "owner": "MaplePrivacyLabs", "repository": "Maple" } } diff --git a/scripts/ci/proxy_registry_inventory.py b/scripts/ci/proxy_registry_inventory.py new file mode 100644 index 000000000..8859021a5 --- /dev/null +++ b/scripts/ci/proxy_registry_inventory.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Read the canonical proxy package inventory without masking registry failures. + +Only GitHub's authenticated package metadata can admit an initial package. A +registry 401, 403, 404, timeout or malformed response is never an empty registry. +The workflow supplies a repository-scoped GITHUB_TOKEN with packages: read. +""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import json +import os +import re +import sys +from urllib.error import HTTPError, URLError +from urllib.request import HTTPRedirectHandler, Request, build_opener + +OWNER = "MaplePrivacyLabs" +OWNER_ID = 322649754 +REPOSITORY = "MaplePrivacyLabs/Maple" +REPOSITORY_ID = 923138240 +PACKAGE = "maple-proxy" +IMAGE = "mapleprivacylabs/maple-proxy" +API = "https://api.github.com" +REGISTRY = "https://ghcr.io" +PACKAGE_URL = f"{API}/orgs/{OWNER}/packages/container/{PACKAGE}" +MAX_JSON_BYTES = 4 * 1024 * 1024 + + +class InventoryError(ValueError): + """Fixed-category error, with no remote body or credential interpolation.""" + + +@dataclass(frozen=True) +class Response: + status: int + data: object = None + link: str | None = None + + +class NoRedirect(HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +def get_json(url: str, token: str | None = None) -> Response: + headers = {"Accept": "application/vnd.github+json", "User-Agent": "Maple-proxy-package-inventory"} + if url.startswith(API + "/"): + headers["X-GitHub-Api-Version"] = "2022-11-28" + if token: + headers["Authorization"] = "Bearer " + token + try: + with build_opener(NoRedirect).open(Request(url, headers=headers), timeout=30) as response: + body = response.read(MAX_JSON_BYTES + 1) + if len(body) > MAX_JSON_BYTES: + raise InventoryError("Package inventory response exceeds size limit") + return Response(response.status, json.loads(body), response.headers.get("Link")) + except HTTPError as error: + status = error.code + error.close() + return Response(status) + except (OSError, URLError, ValueError): + raise InventoryError("Package inventory request failed") from None + + +def require_public(metadata: object) -> None: + if not isinstance(metadata, dict): + raise InventoryError("Invalid package metadata") + owner = metadata.get("owner") or {} + repository = metadata.get("repository") or {} + if ( + metadata.get("name") != PACKAGE + or metadata.get("package_type") != "container" + or not isinstance(owner, dict) + or owner.get("id") != OWNER_ID + or owner.get("login") != OWNER + or not isinstance(repository, dict) + or repository.get("id") != REPOSITORY_ID + or repository.get("full_name") != REPOSITORY + ): + raise InventoryError("Package is not linked to the canonical Maple repository") + if metadata.get("visibility") != "public": + raise InventoryError( + "The new proxy package must be made public in GitHub package settings; " + "then rerun Publish proxy container to verify and reconcile its aliases" + ) + + +def confirm_missing(token: str, get=get_json) -> None: + # GitHub can mask unavailable resources as 404. Also require successful + # authenticated package enumeration; a missing packages permission must not + # authorize treating an inventory failure as an empty package. This lists + # packages readable by this repository token, not unrelated private packages. + # GitHub denies pushes to an existing package not connected to this repository. + for page in range(1, 101): + response = get(f"{API}/orgs/{OWNER}/packages?package_type=container&per_page=100&page={page}", token) + if response.status != 200 or not isinstance(response.data, list): + raise InventoryError("Cannot confirm initial package with authenticated package listing") + if len(response.data) > 100: + raise InventoryError("Invalid package listing length") + for package in response.data: + if not isinstance(package, dict) or not isinstance(package.get("name"), str): + raise InventoryError("Invalid package listing entry") + if package["name"].casefold() == PACKAGE: + raise InventoryError("Package metadata and package listing disagree") + if not response.link: + return + # Construct the next fixed-origin URL ourselves; never follow a Link URL + # carrying an API credential. Fail on unexpected pagination metadata. + link_pattern = r'<[^>\r\n]+>;\s*rel="(?:next|prev|first|last)"' + if not re.fullmatch(link_pattern + r'(?:,\s*' + link_pattern + r')*', response.link): + raise InventoryError("Invalid package listing pagination") + if 'rel="next"' not in response.link: + return + raise InventoryError("Package listing exceeds pagination limit") + + +def inventory(token: str, *, public_only: bool = False, get=get_json) -> dict: + if not token: + raise InventoryError("Package inventory requires the workflow token") + response = get(PACKAGE_URL, token) + if response.status == 404 and not public_only: + confirm_missing(token, get) + return {"name": IMAGE, "tags": []} + if response.status != 200: + raise InventoryError("Authenticated proxy package metadata is unavailable") + require_public(response.data) + if public_only: + return {"name": IMAGE, "visibility": "public"} + + # Public access is part of the contract. Do not use the workflow credential + # to hide a public-pull regression; this registry token is anonymous/pull-only. + response = get(f"{REGISTRY}/token?scope=repository:{IMAGE}:pull") + if response.status != 200 or not isinstance(response.data, dict): + raise InventoryError("Cannot obtain anonymous proxy registry token") + registry_token = response.data.get("token") + if not isinstance(registry_token, str) or not re.fullmatch(r"[A-Za-z0-9._~+/=-]{1,16384}", registry_token): + raise InventoryError("Invalid anonymous proxy registry token") + response = get(f"{REGISTRY}/v2/{IMAGE}/tags/list?n=10000", registry_token) + if response.status != 200 or not isinstance(response.data, dict) or response.link: + raise InventoryError("Public proxy tag inventory is unavailable or incomplete") + tags = response.data.get("tags") + if ( + response.data.get("name") != IMAGE + or not isinstance(tags, list) + or len(tags) > 10000 + or any(not isinstance(tag, str) or not re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}", tag) for tag in tags) + or len(set(tags)) != len(tags) + ): + raise InventoryError("Invalid public proxy tag inventory") + return {"name": IMAGE, "tags": tags} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--require-public", action="store_true") + args = parser.parse_args() + try: + if ( + os.environ.get("GITHUB_REPOSITORY") != REPOSITORY + or os.environ.get("GITHUB_REPOSITORY_ID") != str(REPOSITORY_ID) + or os.environ.get("GITHUB_REPOSITORY_OWNER_ID") != str(OWNER_ID) + or os.environ.get("IMAGE_NAME") != IMAGE + or os.environ.get("REGISTRY") != "ghcr.io" + ): + raise InventoryError("Proxy package inventory must run for the canonical repository and image") + print(json.dumps(inventory(os.environ.get("GH_TOKEN", ""), public_only=args.require_public), sort_keys=True)) + except InventoryError as error: + print(f"Proxy package inventory failed: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ci/release-verification-guide.sh b/scripts/ci/release-verification-guide.sh index 8d87e84b6..e95eb818c 100755 --- a/scripts/ci/release-verification-guide.sh +++ b/scripts/ci/release-verification-guide.sh @@ -25,7 +25,7 @@ if [ ! -d "${artifacts_dir}" ]; then exit 1 fi -repo="${GITHUB_REPOSITORY:-OpenSecretCloud/Maple}" +repo="${GITHUB_REPOSITORY:-MaplePrivacyLabs/Maple}" release_tag="${RELEASE_TAG:-${GITHUB_REF_NAME:-}}" if [ -z "${release_tag}" ]; then echo "RELEASE_TAG or GITHUB_REF_NAME is required." >&2 diff --git a/scripts/ci/test-release-gates.sh b/scripts/ci/test-release-gates.sh index 23e5e8ee5..f47b7a097 100755 --- a/scripts/ci/test-release-gates.sh +++ b/scripts/ci/test-release-gates.sh @@ -471,10 +471,10 @@ check( proxy_container_publish.get("env") == { "REGISTRY": "ghcr.io", - "IMAGE_NAME": "opensecretcloud/maple-proxy", + "IMAGE_NAME": "mapleprivacylabs/maple-proxy", "UNBACKFILLED_PROXY_BASELINE": "0.3.3", }, - "Proxy container publisher must preserve the existing GHCR package", + "Proxy container publisher must use the canonical MaplePrivacyLabs GHCR package", ) container_jobs = proxy_container_publish.get("jobs", {}) @@ -485,12 +485,20 @@ check( prepare = container_jobs["prepare"] prepare_if = str(prepare.get("if", "")) for required_gate in ( + "github.repository == 'MaplePrivacyLabs/Maple'", + "github.repository_id == '923138240'", + "github.repository_owner_id == '322649754'", + "github.ref == 'refs/heads/master'", "workflow_run.conclusion == 'success'", "workflow_run.event == 'release'", "workflow_run.path == '.github/workflows/release.yml'", "workflow_run.head_repository.full_name == github.repository", ): check(required_gate in prepare_if, f"Proxy container publisher is missing gate: {required_gate}") +check( + prepare.get("permissions") == {"contents": "read", "packages": "read"}, + "Proxy preparation must read package metadata without package write permission", +) prepare_runs = "\n".join(str(step.get("run", "")) for step in prepare.get("steps", [])) for required_control in ( "repos/${REPOSITORY}/releases/latest", @@ -500,6 +508,7 @@ for required_control in ( "proxy container runtime inputs changed without a proxy version bump", '"${UNBACKFILLED_PROXY_BASELINE}"', "plan-proxy-container-publish.sh", + "python3 -I scripts/ci/proxy_registry_inventory.py", ): check(required_control in prepare_runs, f"Proxy publication plan is missing control: {required_control}") prepare_checkouts = [ @@ -595,6 +604,7 @@ check( finalize_runs = "\n".join(str(step.get("run", "")) for step in container_finalize.get("steps", [])) for required_control in ( "inspect-proxy-container-manifest.sh", + "python3 -I scripts/ci/proxy_registry_inventory.py --require-public", "published exact tag does not match the current proxy runtime inputs", 'for tag in "${PROXY_MINOR}" "${PROXY_MAJOR}" latest', "docker buildx imagetools create", @@ -619,4 +629,7 @@ pass "proxy release artifact verifier accepts only the complete native asset set bash "${script_dir}/test-plan-proxy-container-publish.sh" >/dev/null pass "proxy container publish planner preserves immutable versions and recovery" +python3 "${script_dir}/test_proxy_registry_inventory.py" >/dev/null +pass "proxy registry inventory distinguishes initial creation from failed access" + printf '1..%d\n' "${passed}" diff --git a/scripts/ci/test_pages_deploy.py b/scripts/ci/test_pages_deploy.py index 6342654ad..b6ec4b47a 100644 --- a/scripts/ci/test_pages_deploy.py +++ b/scripts/ci/test_pages_deploy.py @@ -7,6 +7,7 @@ import os from pathlib import Path import sys +import tarfile import tempfile import unittest from unittest.mock import Mock, patch @@ -195,6 +196,82 @@ def test_current_release_can_be_redeployed_without_a_new_release(self): self.event = {} self.assertEqual(self.production()["previous_sha"], SHA) + def transferred_release_values(self): + self.setup_release() + self.values[self.root + "/git/ref/heads/pages-production"]["object"]["sha"] = SHA + for key in ("repository", "head_repository"): + self.run[key].update(full_name=self.repo, owner={"id": 185423582}) + for asset in self.release["assets"]: + asset["browser_download_url"] = ( + f"https://github.com/{self.repo}/releases/download/v3.3.10/{asset['name']}" + ) + new_repo = "MaplePrivacyLabs/Maple" + new_root = "/repos/" + new_repo + moved = {key.replace(self.root, new_root, 1): value for key, value in self.values.items()} + moved[new_root] = {"id": REPO_ID, "default_branch": "master", "full_name": new_repo, + "owner": {"id": 322649754}} + return new_repo, moved + + def test_manual_production_prepare_after_owner_transfer_uses_retained_release(self): + new_repo, moved = self.transferred_release_values() + archive = io.BytesIO() + contents = b"Retained production release" + with tarfile.open(fileobj=archive, mode="w:gz") as bundle: + member = tarfile.TarInfo("index.html") + member.size = len(contents) + bundle.addfile(member, io.BytesIO(contents)) + archive_bytes = archive.getvalue() + archive_digest = hashlib.sha256(archive_bytes).hexdigest() + checksum = f"{archive_digest} maple-web-dist.tar.gz\n".encode() + new_root = "/repos/" + new_repo + downloads = {} + for asset, body in zip(self.release["assets"], (archive_bytes, checksum)): + asset.update(size=len(body), digest="sha256:" + hashlib.sha256(body).hexdigest()) + downloads[f"{new_root}/releases/assets/{asset['id']}"] = body + + def request(path, method="GET", data=None, accept="application/vnd.github+json"): + self.assertEqual(method, "GET") + self.assertIsNone(data) + self.assertTrue(path.startswith(new_root)) + if path in downloads: + self.assertEqual(accept, "application/octet-stream") + return io.BytesIO(downloads[path]) + return io.BytesIO(json.dumps(moved[path]).encode()) + + api = pages.API("https://api.github.com", "fake-transfer-canary") + gh = pages.GitHub(api, new_repo, REPO_ID) + with tempfile.TemporaryDirectory() as tmp, patch.object(api, "request", side_effect=request) as read: + state = Path(tmp) / "prepared" + plan = pages.prepare(gh, {}, "production", state) + self.assertEqual((plan["target"], plan["profile"], plan["sha"], plan["previous_sha"]), + ("production", "release", SHA, SHA)) + self.assertEqual((plan["release_id"], plan["run_id"], plan["run_attempt"]), (900, 100, 2)) + self.assertEqual((state / "assets/index.html").read_bytes(), contents) + saved = json.loads((state / "plan.json").read_text()) + self.assertEqual(saved["files"], {"index.html": hashlib.sha256(contents).hexdigest()}) + self.assertEqual([call.args[0] for call in read.call_args_list if call.args[0] in downloads], + list(downloads)) + + def test_transferred_production_rejects_wrong_or_missing_repository_ids_before_download(self): + new_repo, moved = self.transferred_release_values() + new_root = "/repos/" + new_repo + for location in ("current", "repository", "head_repository"): + for missing in (False, True): + with self.subTest(location=location, missing=missing): + values = copy.deepcopy(moved) + identity = (values[new_root] if location == "current" + else values[new_root + "/actions/runs/100"][location]) + if missing: + del identity["id"] + else: + identity["id"] = 99 + api = MemoryAPI(values) + api.download = Mock(side_effect=AssertionError("Untrusted release must not be downloaded")) + gh = pages.GitHub(api, new_repo, REPO_ID) + with tempfile.TemporaryDirectory() as tmp, self.assertRaises((pages.Rejected, KeyError)): + pages.prepare(gh, {}, "production", Path(tmp) / "prepared") + api.download.assert_not_called() + def test_draft_prerelease_and_malformed_tag_rejected(self): self.setup_release() for field, value in (("draft", True), ("prerelease", True), ("tag_name", "v3.3.10\n::error::injection")): diff --git a/scripts/ci/test_proxy_registry_inventory.py b/scripts/ci/test_proxy_registry_inventory.py new file mode 100644 index 000000000..9ed7a6775 --- /dev/null +++ b/scripts/ci/test_proxy_registry_inventory.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +"""Regression coverage for an empty namespace versus unavailable package access.""" + +import contextlib +import copy +import io +import json +import os +import sys +import unittest +from unittest.mock import patch +from urllib.error import HTTPError, URLError + +import proxy_registry_inventory as inventory + +TOKEN = "FAKE_WORKFLOW_TOKEN_NOT_A_CREDENTIAL" +REGISTRY_TOKEN = "FAKE_ANONYMOUS_PULL_TOKEN" +PACKAGE = { + "name": inventory.PACKAGE, + "package_type": "container", + "visibility": "public", + "owner": {"id": inventory.OWNER_ID, "login": inventory.OWNER}, + "repository": {"id": inventory.REPOSITORY_ID, "full_name": inventory.REPOSITORY}, +} + + +class FakeGet: + def __init__(self, *responses): + self.responses = list(responses) + self.calls = [] + + def __call__(self, url, token=None): + self.calls.append((url, token)) + if not self.responses: + raise AssertionError("Unexpected extra request") + result = self.responses.pop(0) + if isinstance(result, Exception): + raise result + return result + + +class ProxyRegistryInventoryTests(unittest.TestCase): + def public_responses(self, tags=None): + return ( + inventory.Response(200, copy.deepcopy(PACKAGE)), + inventory.Response(200, {"token": REGISTRY_TOKEN}), + inventory.Response(200, {"name": inventory.IMAGE, "tags": ["0.3.4", "latest"] if tags is None else tags}), + ) + + def test_missing_package_requires_authenticated_metadata_and_listing(self): + get = FakeGet(inventory.Response(404), inventory.Response(200, [])) + self.assertEqual(inventory.inventory(TOKEN, get=get), {"name": inventory.IMAGE, "tags": []}) + self.assertEqual(len(get.calls), 2) + self.assertTrue(all(url.startswith(inventory.API + "/") and token == TOKEN for url, token in get.calls)) + + def test_metadata_auth_network_and_service_errors_are_not_empty(self): + for status in (301, 400, 401, 403, 429, 500, 502, 503): + with self.subTest(status=status): + get = FakeGet(inventory.Response(status)) + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=get) + self.assertEqual(len(get.calls), 1) + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.InventoryError("Package inventory request failed"))) + + def test_metadata_404_does_not_mask_unauthorized_or_failed_listing(self): + for status in (301, 401, 403, 404, 429, 500): + with self.subTest(status=status), self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.Response(404), inventory.Response(status))) + for data in ({}, None, [{"name": inventory.PACKAGE}], [{"name": "MAPLE-PROXY"}], ["invalid"], [{}]): + with self.subTest(data=data), self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.Response(404), inventory.Response(200, data))) + + def test_missing_package_checks_all_readable_package_pages(self): + get = FakeGet( + inventory.Response(404), + inventory.Response(200, [{"name": "other"}], '; rel="next"'), + inventory.Response(200, [{"name": inventory.PACKAGE}]), + ) + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=get) + self.assertEqual(get.calls[-1], (f"{inventory.API}/orgs/{inventory.OWNER}/packages?package_type=container&per_page=100&page=2", TOKEN)) + + def test_malformed_or_unbounded_package_pagination_fails(self): + for link in ('invalid', '; rel="unknown"', 'secret\n::error::injected'): + with self.subTest(link=link), self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.Response(404), inventory.Response(200, [], link))) + responses = [inventory.Response(404)] + [inventory.Response(200, [], '; rel="next"')] * 100 + with self.assertRaisesRegex(inventory.InventoryError, "pagination limit"): + inventory.inventory(TOKEN, get=FakeGet(*responses)) + + def test_public_package_keeps_real_tags_and_uses_anonymous_registry_access(self): + get = FakeGet(*self.public_responses()) + self.assertEqual(inventory.inventory(TOKEN, get=get), {"name": inventory.IMAGE, "tags": ["0.3.4", "latest"]}) + self.assertEqual(get.calls[0], (inventory.PACKAGE_URL, TOKEN)) + self.assertEqual(get.calls[1], (f"{inventory.REGISTRY}/token?scope=repository:{inventory.IMAGE}:pull", None)) + self.assertEqual(get.calls[2], (f"{inventory.REGISTRY}/v2/{inventory.IMAGE}/tags/list?n=10000", REGISTRY_TOKEN)) + + def test_private_package_requires_operator_visibility_change(self): + for visibility in ("private", "internal", None): + with self.subTest(visibility=visibility): + package = copy.deepcopy(PACKAGE) + package["visibility"] = visibility + get = FakeGet(inventory.Response(200, package)) + with self.assertRaisesRegex(inventory.InventoryError, "must be made public"): + inventory.inventory(TOKEN, get=get) + self.assertEqual(len(get.calls), 1) + + def test_existing_package_must_belong_to_expected_owner_and_repository(self): + for field, value in ( + ("name", "other"), ("package_type", "npm"), + ("owner", {"id": 1, "login": inventory.OWNER}), + ("owner", {"id": inventory.OWNER_ID, "login": "Other"}), + ("repository", {"id": 1, "full_name": inventory.REPOSITORY}), + ("repository", {"id": inventory.REPOSITORY_ID, "full_name": "Other/Maple"}), + ("repository", None), ("repository", "invalid"), + ): + with self.subTest(field=field, value=value): + package = copy.deepcopy(PACKAGE) + package[field] = value + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.Response(200, package))) + + def test_final_visibility_gate_requires_an_existing_public_package(self): + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, public_only=True, get=FakeGet(inventory.Response(404))) + get = FakeGet(inventory.Response(200, PACKAGE)) + self.assertEqual(inventory.inventory(TOKEN, public_only=True, get=get), {"name": inventory.IMAGE, "visibility": "public"}) + self.assertEqual(len(get.calls), 1) + + def test_anonymous_auth_and_tags_failures_never_become_an_empty_inventory(self): + for status in (301, 401, 403, 404, 429, 500): + for index in (1, 2): + with self.subTest(status=status, request=index): + responses = list(self.public_responses())[:index] + [inventory.Response(status)] + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(*responses)) + + def test_invalid_registry_tokens_and_inventory_cannot_inject_outputs(self): + for token in (None, "", "bad\n::error::injected", "x" * 16385): + with self.subTest(token=token), self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(inventory.Response(200, PACKAGE), inventory.Response(200, {"token": token}))) + for tags in (None, {}, ["bad\n::error::injected"], ["0.3.4", "0.3.4"], [1], ["/bad"], ["a" * 129]): + with self.subTest(tags=tags): + responses = list(self.public_responses()) + responses[2] = inventory.Response(200, {"name": inventory.IMAGE, "tags": tags}) + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(*responses)) + + def test_partial_registry_inventory_is_rejected(self): + responses = list(self.public_responses()) + responses[2] = inventory.Response(200, {"name": inventory.IMAGE, "tags": ["0.3.4"]}, 'next; rel="next"') + with self.assertRaises(inventory.InventoryError): + inventory.inventory(TOKEN, get=FakeGet(*responses)) + + def test_transport_errors_are_sanitized_and_http_status_is_preserved(self): + for error in (URLError(TOKEN), OSError(TOKEN)): + with self.subTest(error=type(error).__name__), patch.object(inventory, "build_opener") as opener: + opener.return_value.open.side_effect = error + with self.assertRaises(inventory.InventoryError) as result: + inventory.get_json(inventory.PACKAGE_URL, TOKEN) + self.assertNotIn(TOKEN, str(result.exception)) + with patch.object(inventory, "build_opener") as opener: + opener.return_value.open.side_effect = HTTPError(inventory.PACKAGE_URL, 403, TOKEN, {}, io.BytesIO(TOKEN.encode())) + self.assertEqual(inventory.get_json(inventory.PACKAGE_URL, TOKEN), inventory.Response(403)) + + def test_invalid_or_oversized_response_is_sanitized(self): + for payload, limit in ((TOKEN.encode(), 4096), (b" " * 10, 4)): + response = io.BytesIO(payload) + response.status = 200 + response.headers = {} + with self.subTest(limit=limit), patch.object(inventory, "MAX_JSON_BYTES", limit), patch.object(inventory, "build_opener") as opener: + opener.return_value.open.return_value = response + with self.assertRaises(inventory.InventoryError) as result: + inventory.get_json(inventory.PACKAGE_URL, TOKEN) + self.assertNotIn(TOKEN, str(result.exception)) + + def test_http_redirects_never_forward_the_workflow_token(self): + handler = inventory.NoRedirect() + self.assertIsNone(handler.redirect_request(None, None, 302, None, None, "https://attacker.invalid")) + + def test_wrong_workflow_repository_fails_before_any_request(self): + environment = { + "GITHUB_REPOSITORY": inventory.REPOSITORY, + "GITHUB_REPOSITORY_ID": str(inventory.REPOSITORY_ID), + "GITHUB_REPOSITORY_OWNER_ID": str(inventory.OWNER_ID), + "IMAGE_NAME": inventory.IMAGE, + "REGISTRY": "ghcr.io", + "GH_TOKEN": TOKEN, + } + for key in environment.keys() - {"GH_TOKEN"}: + invalid = dict(environment, **{key: "invalid"}) + captured = io.StringIO() + with self.subTest(key=key), patch.dict(os.environ, invalid, clear=True), patch.object(sys, "argv", ["inventory"]), patch.object(inventory, "inventory") as operation, contextlib.redirect_stderr(captured): + self.assertEqual(inventory.main(), 1) + operation.assert_not_called() + self.assertNotIn(TOKEN, captured.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/updates/src/worker.test.ts b/updates/src/worker.test.ts index 19a7c4037..ad410b4b8 100644 --- a/updates/src/worker.test.ts +++ b/updates/src/worker.test.ts @@ -61,7 +61,7 @@ function envReturning(response: Response, requests: Request[] = []): Env { describe("latest.json validation", () => { test("uses the shared Maple GitHub repository identity", () => { - expect(GITHUB_REPOSITORY).toBe("OpenSecretCloud/Maple"); + expect(GITHUB_REPOSITORY).toBe("MaplePrivacyLabs/Maple"); }); test("accepts the Maple release schema", () => { @@ -88,7 +88,7 @@ describe("latest.json validation", () => { const nonDefaultPort = validRelease(); nonDefaultPort.platforms["windows-x86_64"].url = - "https://github.com:8443/OpenSecretCloud/Maple/releases/download/v3.3.8/Maple_3.3.8_x64-setup.exe"; + "https://github.com:8443/MaplePrivacyLabs/Maple/releases/download/v3.3.8/Maple_3.3.8_x64-setup.exe"; expect(isLatestRelease(nonDefaultPort)).toBe(false); }); diff --git a/zapstore.yaml b/zapstore.yaml index 30a23ebc2..458a809f6 100644 --- a/zapstore.yaml +++ b/zapstore.yaml @@ -1,4 +1,4 @@ -repository: https://github.com/OpenSecretCloud/Maple +repository: https://github.com/MaplePrivacyLabs/Maple release_source: ./app-universal-release.apk