Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/tests/repository_tokens_with_real_clients.rs

//! ★ **Real package clients carry a repository token** (lane P3): `cargo`, `twine` with `pip`,
//! and `npm`, each configured the way its own documentation says, against a REAL holger whose
//! repositories all demand a credential — and each is refused once the token is revoked.
//!
//! * **cargo**: `cargo publish` to the sparse registry `crates` (the token from
//!   `CARGO_REGISTRIES_HOLGER_TOKEN`, sent as the bare `Authorization` header), then a consumer
//!   resolves and fetches it (`"auth-required": true` in `config.json` makes cargo send the
//!   token on index and download requests). Revoked: the resolve fails with the token's code.
//! * **twine and pip**: Ivan, an Importer, uploads a wheel with `twine upload -u __token__`
//!   into the intake chain; it is walked to AVAILABLE; `pip download` reads it from the
//!   production PEP 503 index with `__token__` in the index URL. Anna, the admin, may mint a
//!   write token for the intake's door, but the chain still refuses her upload: a token never
//!   holds more than its owner's duties.
//! * **npm**: a token in `.npmrc` (`//host/npm/:_authToken=`) is sent as Bearer: `npm view`
//!   and `npm install` read a packument and its tarball from a raw repository; at the chain's
//!   npm door (lane P5) Ivan's token publishes into the intake chain, Anna's reads the
//!   AVAILABLE version, a read token is refused a publish (`holger-token-5`), and a revoked
//!   one publishes nothing.
//!
//! A client that is not on `PATH` is skipped with a line saying so; the toolchain container
//! carries cargo, and the lane's client image adds pip, twine and npm.

use std::io::Write;
use std::path::Path;
use std::process::Command;
use traits::test_support::hold_port;

use holger_server_lib::grpc::holger_token_proto::token_service_client::TokenServiceClient;
use holger_server_lib::grpc::holger_token_proto::{
    ListTokensRequest, MintTokenRequest, RevokeTokenRequest, TokenScope,
};
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use nornir_auth_webauthn::softkey::SoftKey;

const RP_ID: &str = "holger.estate.test";
const ORIGIN: &str = "https://holger.estate.test";
const CONSOLE: &str = "c0nsole-bearer-0123456789abcdefghij";
const WHEEL: &str = "demo_pkg-1.0.0-py3-none-any.whl";

struct Running {
    _data: tempfile::TempDir,
    _cfg: tempfile::NamedTempFile,
    _holger: Holger,
    base: String,
    grpc: String,
    http: u16,
}

fn start() -> Running {
    let mut last = String::new();
    for _ in 0..8 {
        let (grpc, http) = (hold_port(), hold_port());
        match try_start(grpc, http) {
            Ok(r) => return r,
            Err(e) => last = e,
        }
    }
    panic!("server never started; last error: {last}");
}

fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
    let data = tempfile::tempdir().unwrap();
    let d = data.path().display().to_string();
    let raw = |name: &str| {
        format!(
            r#"(ron_name: "{name}", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
         ron_data_dir: Some("{d}/{name}"), ron_require_auth: true),"#
        )
    };
    let ron = format!(
        r#"(
    repositories: [
        (ron_name: "crates", ron_repo_type: "rust", ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
         ron_writable_archive: Some("{d}/crates.znippy"), ron_base_url: Some("http://127.0.0.1:{http}"),
         ron_require_auth: true),
        {incoming} {quarantine} {approved} {production} {npm}
        (ron_name: "npm", ron_repo_type: "npm", ron_base_url: Some("http://127.0.0.1:{http}"), ron_upstreams: [], ron_in: None,
         ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door"))),
    ],
    exposed_endpoints: [
        (ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")),
    ],
    storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
    audit: (dir: "{d}/audit", enabled: true),
    auth: Some((
        methods: [
            StaticToken(subject: "console", token: Some("{CONSOLE}")),
            Passkey(rp_id: "{RP_ID}", rp_origin: "{ORIGIN}"),
        ],
        roles: {{"console": Admin}},
        group_roles: {{"holger-admins": Admin, "holger-writers": Writer, "holger-readers": Reader}},
    )),
    promotion: Some((
        ladder_name: "intake",
        stages: [
            (name: "incoming", repo: "incoming"),
            (name: "quarantine", repo: "quarantine"),
            (name: "approved", repo: "approved"),
            (name: "production", repo: "production"),
        ],
        intake: Some((rules: (sandbox: off), 
            incoming: "incoming",
            quarantine: "quarantine",
            approved: "approved",
            production: "production",
            approvals: 1,
            npm: Some("npm"),
        )),
    )),
)"#,
        incoming = raw("incoming"),
        quarantine = raw("quarantine"),
        approved = raw("approved"),
        production = raw("production"),
        npm = raw("npm-raw"),
    );
    let mut cfg = tempfile::NamedTempFile::new().unwrap();
    cfg.write_all(ron.as_bytes()).unwrap();
    let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
    holger_server_lib::auth::estate_duties(holger.auth.as_mut().expect("an auth block"));
    holger
        .instantiate_backends()
        .map_err(|e| format!("backends: {e}"))?;
    wire_holger(&mut holger).map_err(|e| format!("wire: {e}"))?;
    holger.start().map_err(|e| format!("start: {e}"))?;
    Ok(Running {
        _data: data,
        _cfg: cfg,
        _holger: holger,
        base: format!("http://127.0.0.1:{http}"),
        grpc: format!("http://127.0.0.1:{grpc}"),
        http,
    })
}

async fn wait_healthy(c: &reqwest::Client, base: &str) {
    for _ in 0..200 {
        if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
            if r.status() == 200 {
                return;
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("gateway never became healthy at {base}");
}

async fn post(
    c: &reqwest::Client,
    base: &str,
    path: &str,
    bearer: Option<&str>,
    body: serde_json::Value,
) -> (u16, serde_json::Value) {
    let mut req = c.post(format!("{base}{path}")).json(&body);
    if let Some(b) = bearer {
        req = req.bearer_auth(b);
    }
    let r = req.send().await.expect("the door answers");
    let status = r.status().as_u16();
    let text = r.text().await.unwrap_or_default();
    (
        status,
        serde_json::from_str(&text).unwrap_or(serde_json::Value::String(text)),
    )
}

async fn register(
    c: &reqwest::Client,
    base: &str,
    key: &mut SoftKey,
    bearer: Option<&str>,
    body: serde_json::Value,
) -> (u16, serde_json::Value) {
    let (status, start) = post(c, base, "/-/auth/passkey/register/start", bearer, body).await;
    if status != 200 {
        return (status, start);
    }
    let ceremony = start["ceremony_id"]
        .as_str()
        .expect("a ceremony id")
        .to_owned();
    let challenge = start["publicKey"]["challenge"]
        .as_str()
        .expect("a challenge");
    let (client_data, attestation) = key.register(RP_ID, ORIGIN, challenge, true);
    post(
        c,
        base,
        "/-/auth/passkey/register/finish",
        bearer,
        serde_json::json!({
            "ceremony_id": ceremony,
            "id": key.credential_id_b64(),
            "response": { "clientDataJSON": client_data, "attestationObject": attestation },
        }),
    )
    .await
}

async fn login(c: &reqwest::Client, base: &str, key: &mut SoftKey) -> String {
    let (status, start) = post(
        c,
        base,
        "/-/auth/passkey/login/start",
        None,
        serde_json::json!({ "uv": "required" }),
    )
    .await;
    assert_eq!(status, 200, "{start}");
    let ceremony = start["ceremony_id"]
        .as_str()
        .expect("a ceremony id")
        .to_owned();
    let challenge = start["publicKey"]["challenge"]
        .as_str()
        .expect("a challenge");
    let (client_data, auth_data, signature) = key.authenticate(RP_ID, ORIGIN, challenge, true);
    let (status, out) = post(
        c,
        base,
        "/-/auth/passkey/login/finish",
        None,
        serde_json::json!({
            "ceremony_id": ceremony,
            "id": key.credential_id_b64(),
            "response": { "clientDataJSON": client_data, "authenticatorData": auth_data, "signature": signature, "userHandle": null },
        }),
    )
    .await;
    assert_eq!(status, 200, "logs in: {out}");
    out["token"].as_str().expect("a token").to_owned()
}

fn authed<T>(msg: T, bearer: &str) -> tonic::Request<T> {
    let mut r = tonic::Request::new(msg);
    r.metadata_mut()
        .insert("authorization", format!("Bearer {bearer}").parse().unwrap());
    r
}

async fn tokens(grpc: &str) -> TokenServiceClient<tonic::transport::Channel> {
    for _ in 0..200 {
        if let Ok(c) = TokenServiceClient::connect(grpc.to_string()).await {
            return c;
        }
        tokio::time::sleep(std::time::Duration::from_millis(20)).await;
    }
    panic!("gRPC never came up at {grpc}");
}

async fn mint(
    ts: &mut TokenServiceClient<tonic::transport::Channel>,
    by: &str,
    label: &str,
    repos: &[&str],
    scope: TokenScope,
) -> String {
    let m = MintTokenRequest {
        label: label.into(),
        repositories: repos.iter().map(|s| s.to_string()).collect(),
        scope: scope as i32,
        days: 1,
    };
    ts.mint(authed(m, by))
        .await
        .unwrap_or_else(|e| panic!("{label}: {e}"))
        .into_inner()
        .secret
}

async fn revoke(ts: &mut TokenServiceClient<tonic::transport::Channel>, by: &str, label: &str) {
    let all = ts
        .list(authed(ListTokensRequest {}, by))
        .await
        .unwrap()
        .into_inner()
        .tokens;
    let id = all
        .iter()
        .find(|t| t.label == label)
        .unwrap_or_else(|| panic!("{label} is listed"))
        .id
        .clone();
    ts.revoke(authed(RevokeTokenRequest { id }, by))
        .await
        .expect("revoked");
}

/// Is `bin` on PATH (it answers `--version`)?
fn have(bin: &str, arg: &str) -> bool {
    Command::new(bin)
        .arg(arg)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}

/// Run `cmd`; its exit and its stdout and stderr together.
fn run(cmd: &mut Command) -> (bool, String) {
    let out = cmd.output().unwrap_or_else(|e| panic!("{cmd:?}: {e}"));
    (
        out.status.success(),
        format!(
            "{}{}",
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        ),
    )
}

fn write(path: &Path, body: &str) {
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(path, body).unwrap();
}

/// A cargo home of its own: the registry, and no other credential.
fn cargo_home(dir: &Path, http: u16) {
    write(
        &dir.join("config.toml"),
        &format!(
            "[registries.holger]\nindex = \"sparse+http://127.0.0.1:{http}/crates/index/\"\ncredential-provider = \"cargo:token\"\n\n[net]\nretry = 0\n\n[publish]\ntimeout = 30\n"
        ),
    );
}

fn cargo(home: &Path, token: &str, dir: &Path, args: &[&str]) -> (bool, String) {
    run(Command::new("cargo")
        .args(args)
        .current_dir(dir)
        .env("CARGO_HOME", home)
        .env("CARGO_REGISTRIES_HOLGER_TOKEN", token)
        .env("CARGO_TARGET_DIR", dir.join("target"))
        .env_remove("RUSTC_WRAPPER")
        .env_remove("CARGO_BUILD_JOBS"))
}

#[tokio::test(flavor = "multi_thread")]
async fn cargo_twine_pip_and_npm_carry_a_repository_token_and_stop_when_it_is_revoked() {
    let r = start();
    let c = reqwest::Client::builder().build().expect("reqwest client");
    wait_healthy(&c, &r.base).await;
    let base = r.base.as_str();
    let mut ts = tokens(&r.grpc).await;
    let work = tempfile::tempdir().unwrap();

    let mut anna_key = SoftKey::new(0x41, b"cred-anna-0001");
    let (status, reg) = register(
        &c,
        base,
        &mut anna_key,
        Some(CONSOLE),
        serde_json::json!({ "display": "anna", "role": "admin" }),
    )
    .await;
    assert_eq!(status, 200, "{reg}");
    let anna = login(&c, base, &mut anna_key).await;
    // Ivan, the Importer: what twine and npm publish as.
    let (status, minted) = post(
        &c,
        base,
        "/-/auth/passkey/invites",
        Some(&anna),
        serde_json::json!({ "display": "ivan", "duty": "importer" }),
    )
    .await;
    assert_eq!(status, 200, "{minted}");
    let mut ivan_key = SoftKey::new(0x49, b"cred-ivan-0001");
    let (status, reg) = register(
        &c,
        base,
        &mut ivan_key,
        None,
        serde_json::json!({ "display": "ivan", "invite": minted["invite"].as_str().unwrap() }),
    )
    .await;
    assert_eq!(status, 200, "{reg}");
    let ivan = login(&c, base, &mut ivan_key).await;

    // ── cargo: publish, resolve and fetch with the token; refused once revoked ──
    if have("cargo", "--version") {
        let token = mint(&mut ts, &anna, "cargo", &["crates"], TokenScope::ReadWrite).await;
        let home = work.path().join("cargo-home");
        cargo_home(&home, r.http);
        let lib = work.path().join("tokdemo");
        write(&lib.join("Cargo.toml"), "[package]\nname = \"tokdemo\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"a crate published with a repository token\"\nlicense = \"MIT\"\npublish = [\"holger\"]\n");
        write(&lib.join("src/lib.rs"), "pub fn answer() -> u32 { 42 }\n");
        let (ok, out) = cargo(
            &home,
            &token,
            &lib,
            &[
                "publish",
                "--registry",
                "holger",
                "--no-verify",
                "--allow-dirty",
            ],
        );
        assert!(ok, "cargo publish with the token: {out}");
        let app = work.path().join("app");
        write(&app.join("Cargo.toml"), "[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2021\"\npublish = false\n\n[dependencies]\ntokdemo = { version = \"0.1.0\", registry = \"holger\" }\n");
        write(
            &app.join("src/main.rs"),
            "fn main() { println!(\"{}\", tokdemo::answer()); }\n",
        );
        let (ok, out) = cargo(&home, &token, &app, &["fetch"]);
        assert!(
            ok,
            "cargo resolves and fetches from the gated registry with the token: {out}"
        );
        revoke(&mut ts, &anna, "cargo").await;
        let fresh = work.path().join("cargo-home-2");
        cargo_home(&fresh, r.http);
        let _ = std::fs::remove_file(app.join("Cargo.lock"));
        let (ok, out) = cargo(&fresh, &token, &app, &["fetch"]);
        assert!(!ok, "a revoked token resolves nothing: {out}");
        assert!(out.contains("401"), "cargo says the server refused: {out}");
        println!(
            "cargo, revoked: {}",
            out.lines()
                .filter(|l| l.contains("401") || l.contains("holger-token"))
                .collect::<Vec<_>>()
                .join(" | ")
        );
    } else {
        println!("SKIPPED cargo: not on PATH");
    }

    // ── twine and pip: through the intake chain, as the Importer ──
    if have("twine", "--version")
        && Command::new("python3")
            .args(["-m", "pip", "--version"])
            .output()
            .is_ok_and(|o| o.status.success())
    {
        let wheel = work.path().join(WHEEL);
        std::fs::write(&wheel, wheel_bytes()).unwrap();
        let twine = |token: &str| {
            run(Command::new("twine")
                .args([
                    "upload",
                    "--non-interactive",
                    "--disable-progress-bar",
                    "--repository-url",
                    &format!("{base}/incoming/legacy/"),
                    "-u",
                    "__token__",
                    "-p",
                    token,
                ])
                .arg(&wheel)
                .env("TWINE_CERT", "")
                .env_remove("TWINE_PASSWORD"))
        };
        // Anna holds write on the door, so she may mint this; the chain still refuses her.
        let annas = mint(
            &mut ts,
            &anna,
            "anna twine",
            &["incoming"],
            TokenScope::ReadWrite,
        )
        .await;
        let (ok, out) = twine(&annas);
        assert!(
            !ok && out.contains("403"),
            "an admin never imports, token or not: {out}"
        );
        let up = mint(
            &mut ts,
            &ivan,
            "ivan twine",
            &["incoming"],
            TokenScope::ReadWrite,
        )
        .await;
        let (ok, out) = twine(&up);
        assert!(ok, "twine uploads with __token__: {out}");
        for (who, bearer) in [
            ("ivan", &ivan),
            ("ivan", &ivan),
            ("anna", &anna),
            ("anna", &anna),
            ("anna", &anna),
            ("anna", &anna),
        ] {
            let res = c
                .post(format!("{base}/-/intake/advance?name={WHEEL}&verb=advance"))
                .bearer_auth(bearer)
                .send()
                .await
                .unwrap();
            let status = res.status().as_u16();
            assert_eq!(
                status,
                200,
                "{who} advances: {}",
                res.text().await.unwrap_or_default()
            );
        }
        let read = mint(
            &mut ts,
            &ivan,
            "ivan pip",
            &["production"],
            TokenScope::Read,
        )
        .await;
        let pip = |token: &str, into: &Path| {
            run(Command::new("python3")
                .args([
                    "-m",
                    "pip",
                    "download",
                    "--no-deps",
                    "--no-cache-dir",
                    "--disable-pip-version-check",
                    "--retries",
                    "0",
                    "--index-url",
                ])
                .arg(format!(
                    "http://__token__:{token}@127.0.0.1:{}/production/simple/",
                    r.http
                ))
                .arg("-d")
                .arg(into)
                .arg("demo-pkg")
                .env("PIP_CONFIG_FILE", "/dev/null"))
        };
        let got = work.path().join("pip-out");
        let (ok, out) = pip(&read, &got);
        assert!(ok, "pip reads the production index with __token__: {out}");
        assert!(got.join(WHEEL).is_file(), "the wheel is downloaded: {out}");
        revoke(&mut ts, &ivan, "ivan pip").await;
        let (ok, out) = pip(&read, &work.path().join("pip-out-2"));
        assert!(
            !ok && out.contains("401"),
            "a revoked token reads nothing: {out}"
        );
    } else {
        println!("SKIPPED twine and pip: not on PATH");
    }

    // ── npm: Bearer from .npmrc, for a packument and its tarball in a raw repository ──
    if have("npm", "--version") {
        let tarball = npm_tarball(work.path());
        let integrity = {
            use base64::Engine as _;
            use sha2::Digest as _;
            format!(
                "sha512-{}",
                base64::engine::general_purpose::STANDARD.encode(sha2::Sha512::digest(&tarball))
            )
        };
        let packument = serde_json::json!({
            "name": "tokdemo",
            "dist-tags": { "latest": "1.0.0" },
            "versions": { "1.0.0": {
                "name": "tokdemo", "version": "1.0.0",
                "dist": { "tarball": format!("{base}/npm-raw/-/tokdemo-1.0.0.tgz"), "integrity": integrity },
            }},
        });
        for (path, body) in [
            ("tokdemo", serde_json::to_vec(&packument).unwrap()),
            ("-/tokdemo-1.0.0.tgz", tarball),
        ] {
            let res = c
                .put(format!("{base}/npm-raw/{path}"))
                .bearer_auth(CONSOLE)
                .body(body)
                .send()
                .await
                .unwrap();
            assert_eq!(
                res.status(),
                201,
                "{path}: {}",
                res.text().await.unwrap_or_default()
            );
        }
        let token = mint(&mut ts, &anna, "npm", &["npm-raw"], TokenScope::Read).await;
        let npmrc = work.path().join("npmrc");
        write(&npmrc, &format!("registry={base}/npm-raw/\n//127.0.0.1:{}/npm-raw/:_authToken={token}\naudit=false\nfund=false\n", r.http));
        let npm = |args: &[&str]| {
            run(Command::new("npm")
                .args(args)
                .arg("--userconfig")
                .arg(&npmrc)
                .arg("--cache")
                .arg(work.path().join("npm-cache"))
                .env("NO_UPDATE_NOTIFIER", "1"))
        };
        let (ok, out) = npm(&["view", "tokdemo", "version"]);
        assert!(
            ok && out.contains("1.0.0"),
            "npm view with the token from .npmrc: {out}"
        );
        let prefix = work.path().join("npm-app");
        std::fs::create_dir_all(&prefix).unwrap();
        let (ok, out) = npm(&["install", "tokdemo", "--prefix", prefix.to_str().unwrap()]);
        assert!(ok, "npm install with the token: {out}");
        assert!(
            prefix.join("node_modules/tokdemo/package.json").is_file(),
            "installed: {out}"
        );
        revoke(&mut ts, &anna, "npm").await;
        let (ok, out) = npm(&["view", "tokdemo", "version", "--prefer-online"]);
        assert!(
            !ok && (out.contains("401") || out.contains("E401")),
            "a revoked token reads nothing: {out}"
        );
    } else {
        println!("SKIPPED npm: not on PATH");
    }

    // ── the npm door (lane P5): a read token never publishes there ──
    let ivan_reads = mint(&mut ts, &ivan, "ivan npm read", &["npm"], TokenScope::Read).await;
    let res = c
        .put(format!("{base}/npm/tokdoor"))
        .bearer_auth(&ivan_reads)
        .body("{}")
        .send()
        .await
        .unwrap();
    let status = res.status().as_u16();
    let said = res.text().await.unwrap_or_default();
    assert_eq!(status, 403, "{said}");
    assert!(
        said.starts_with("[holger-token-5]"),
        "a read token publishes nothing at the npm door: {said}"
    );

    // ── the npm door: `npm publish` with Ivan's token, walked to AVAILABLE, `npm install` with Anna's ──
    if have("npm", "--version") {
        let publish = mint(
            &mut ts,
            &ivan,
            "ivan npm publish",
            &["npm"],
            TokenScope::ReadWrite,
        )
        .await;
        let pkg = work.path().join("tokdoor");
        write(&pkg.join("package.json"), "{\"name\":\"tokdoor\",\"version\":\"1.0.0\",\"main\":\"index.js\",\"license\":\"MIT\"}\n");
        write(&pkg.join("index.js"), "module.exports = 7;\n");
        let rc = |name: &str, token: &str| {
            let f = work.path().join(name);
            write(&f, &format!("registry={base}/npm/\n//127.0.0.1:{}/npm/:_authToken={token}\naudit=false\nfund=false\n", r.http));
            f
        };
        let npm_with = |rcfile: &Path, dir: &Path, args: &[&str]| {
            run(Command::new("npm")
                .args(args)
                .current_dir(dir)
                .arg("--userconfig")
                .arg(rcfile)
                .arg("--cache")
                .arg(work.path().join("npm-cache-door"))
                .env("NO_UPDATE_NOTIFIER", "1"))
        };
        let ivan_rc = rc("npmrc-ivan", &publish);
        let (ok, out) = npm_with(&ivan_rc, &pkg, &["publish"]);
        assert!(
            ok,
            "npm publish with the Importer's token, into the chain: {out}"
        );
        let name = "tokdoor/-/tokdoor-1.0.0.tgz";
        let mut state = String::from("INGESTED");
        'walk: for _ in 0..12 {
            if state == "AVAILABLE" {
                break;
            }
            for bearer in [&ivan, &anna] {
                let res = c
                    .post(format!("{base}/-/intake/advance?name={name}&verb=advance"))
                    .bearer_auth(bearer)
                    .send()
                    .await
                    .unwrap();
                if res.status() == 200 {
                    let v: serde_json::Value =
                        serde_json::from_str(&res.text().await.unwrap_or_default())
                            .unwrap_or_default();
                    state = v["state"].as_str().unwrap_or("?").to_owned();
                    continue 'walk;
                }
            }
            panic!("held at {state}");
        }
        assert_eq!(state, "AVAILABLE");
        let reads = mint(&mut ts, &anna, "anna npm door", &["npm"], TokenScope::Read).await;
        let anna_rc = rc("npmrc-anna", &reads);
        let (ok, out) = npm_with(&anna_rc, work.path(), &["view", "tokdoor", "version"]);
        assert!(
            ok && out.contains("1.0.0"),
            "npm view at the npm door with a token: {out}"
        );
        let prefix = work.path().join("npm-door-app");
        std::fs::create_dir_all(&prefix).unwrap();
        let (ok, out) = npm_with(
            &anna_rc,
            work.path(),
            &["install", "tokdoor", "--prefix", prefix.to_str().unwrap()],
        );
        assert!(
            ok && prefix.join("node_modules/tokdoor/package.json").is_file(),
            "npm install at the npm door with a token: {out}"
        );
        revoke(&mut ts, &ivan, "ivan npm publish").await;
        write(&pkg.join("package.json"), "{\"name\":\"tokdoor\",\"version\":\"1.0.1\",\"main\":\"index.js\",\"license\":\"MIT\"}\n");
        let (ok, out) = npm_with(&ivan_rc, &pkg, &["publish"]);
        assert!(
            !ok && (out.contains("401") || out.contains("E401")),
            "a revoked token publishes nothing: {out}"
        );
    } else {
        println!("SKIPPED npm at the npm door: not on PATH");
    }
}

/// `package/package.json` and `package/index.js`, as `npm pack` lays them, gzipped.
fn npm_tarball(work: &Path) -> Vec<u8> {
    let dir = work.join("npm-src");
    write(
        &dir.join("package/package.json"),
        "{\"name\":\"tokdemo\",\"version\":\"1.0.0\",\"main\":\"index.js\"}\n",
    );
    write(&dir.join("package/index.js"), "module.exports = 42;\n");
    let out = work.join("tokdemo-1.0.0.tgz");
    let (ok, said) = run(Command::new("tar")
        .arg("czf")
        .arg(&out)
        .arg("-C")
        .arg(&dir)
        .arg("package"));
    assert!(ok, "{said}");
    std::fs::read(out).unwrap()
}

/// A small but well-formed wheel: a stored-only zip with the dist-info METADATA and WHEEL members.
fn wheel_bytes() -> Vec<u8> {
    fn crc32(data: &[u8]) -> u32 {
        let mut crc = 0xFFFF_FFFFu32;
        for b in data {
            crc ^= *b as u32;
            for _ in 0..8 {
                crc = if crc & 1 != 0 {
                    (crc >> 1) ^ 0xEDB8_8320
                } else {
                    crc >> 1
                };
            }
        }
        !crc
    }
    fn member(out: &mut Vec<u8>, central: &mut Vec<u8>, name: &str, body: &[u8]) {
        let crc = crc32(body);
        let offset = out.len() as u32;
        out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
        out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        out.extend_from_slice(&crc.to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(body.len() as u32).to_le_bytes());
        out.extend_from_slice(&(name.len() as u16).to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(name.as_bytes());
        out.extend_from_slice(body);
        central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
        central.extend_from_slice(&[20, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&crc.to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(body.len() as u32).to_le_bytes());
        central.extend_from_slice(&(name.len() as u16).to_le_bytes());
        central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&offset.to_le_bytes());
        central.extend_from_slice(name.as_bytes());
    }
    let mut out = Vec::new();
    let mut central = Vec::new();
    member(
        &mut out,
        &mut central,
        "demo_pkg/__init__.py",
        b"__version__ = \"1.0.0\"\n",
    );
    member(
        &mut out,
        &mut central,
        "demo_pkg-1.0.0.dist-info/METADATA",
        b"Metadata-Version: 2.4\nName: demo-pkg\nVersion: 1.0.0\nLicense-Expression: MIT\n",
    );
    member(
        &mut out,
        &mut central,
        "demo_pkg-1.0.0.dist-info/WHEEL",
        b"Wheel-Version: 1.0\nGenerator: holger-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
    );
    let cd_offset = out.len() as u32;
    let cd_len = central.len() as u32;
    out.extend_from_slice(&central);
    out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
    out.extend_from_slice(&[0, 0, 0, 0, 3, 0, 3, 0]);
    out.extend_from_slice(&cd_len.to_le_bytes());
    out.extend_from_slice(&cd_offset.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out
}