From 9a4769399a6b906115e52f6c38f1974e7698fdcf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 20:43:19 +0000 Subject: [PATCH 01/41] Deliver fallback installer and path hints for non-VS Code use The setup-cert.sh fallback the README points at for JetBrains / CLI users never actually reached the running container - install.sh ran from a temp build mount that gets discarded after the layer commit. Copy it to /usr/local/bin/devcontainer-dev-certs-install so the documented fallback is actually invokable. Also surface the canonical store and trust-dir paths (plus the installer location) as DEVCONTAINER_DEV_CERTS_* env vars in both /etc/profile.d and /etc/environment, so manual integrations don't have to hardcode ~/.dotnet/corefx/... and friends. New opt-in installFallbackTools option installs openssl + jq (the script's runtime prerequisites) for images that don't already provide them. --- .../devcontainer-feature.json | 5 ++ .../src/devcontainer-dev-certs/install.sh | 58 ++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json b/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json index c842e8e..9cb5866 100644 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json @@ -43,6 +43,11 @@ "type": "string", "default": "", "description": "Comma-separated additional directories to write cert artifacts to. Each entry: [=] where format is pem|key|pem-bundle|pfx|all (defaults to all). Every synced cert is written under the directory as {name}.{pem,key,pfx} (and/or {name}-bundle.pem). Example: /etc/nginx/certs=pem,/var/myapp." + }, + "installFallbackTools": { + "type": "boolean", + "default": false, + "description": "Install the runtime prerequisites (openssl, jq) that the fallback `devcontainer-dev-certs-install` script needs. The script is always delivered to /usr/local/bin/ regardless of this option — set this to true only when you intend to invoke it manually (e.g. for JetBrains / Vim / CLI users) and your base image does not already provide openssl and jq." } }, "customizations": { diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh b/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh index b0a05b9..6f92f9e 100755 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh @@ -8,6 +8,12 @@ GENERATE_DOTNET_CERT="${GENERATEDOTNETCERT:-true}" SYNC_USER_CERTIFICATES="${SYNCUSERCERTIFICATES:-true}" SYNC_CONTAINER_CERT="${SYNCCONTAINERCERT:-false}" EXTRA_CERT_DESTINATIONS="${EXTRACERTDESTINATIONS:-}" +INSTALL_FALLBACK_TOOLS="${INSTALLFALLBACKTOOLS:-false}" + +# Resolve our own source directory so the fallback script copy works +# regardless of where the devcontainer CLI mounts us. +FEATURE_SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FALLBACK_BIN_PATH="/usr/local/bin/devcontainer-dev-certs-install" REMOTE_USER="${_REMOTE_USER:-vscode}" REMOTE_USER_HOME="${_REMOTE_USER_HOME:-/home/${REMOTE_USER}}" @@ -26,7 +32,7 @@ fi # Validate that no feature option contains a newline. We append these to # /etc/environment, and an embedded newline would inject an extra env line # (potentially with a name the operator didn't intend). -for varname in TRUST_NSS SSL_CERT_DIRS GENERATE_DOTNET_CERT SYNC_USER_CERTIFICATES SYNC_CONTAINER_CERT EXTRA_CERT_DESTINATIONS; do +for varname in TRUST_NSS SSL_CERT_DIRS GENERATE_DOTNET_CERT SYNC_USER_CERTIFICATES SYNC_CONTAINER_CERT EXTRA_CERT_DESTINATIONS INSTALL_FALLBACK_TOOLS; do case "${!varname}" in *$'\n'*) echo "Error: feature option ${varname} must not contain newlines." >&2 @@ -49,6 +55,39 @@ if [ "${TRUST_NSS}" = "true" ]; then fi fi +# Install fallback-script prerequisites if requested. The script requires +# openssl unconditionally and jq for the --bundle-json form; install only +# what's missing so this is a no-op on images that already provide them. +if [ "${INSTALL_FALLBACK_TOOLS}" = "true" ]; then + declare -a FALLBACK_PKGS=() + command -v openssl &>/dev/null || FALLBACK_PKGS+=("openssl") + command -v jq &>/dev/null || FALLBACK_PKGS+=("jq") + if [ "${#FALLBACK_PKGS[@]}" -gt 0 ]; then + if command -v apt-get &>/dev/null; then + apt-get update -y + apt-get install -y --no-install-recommends "${FALLBACK_PKGS[@]}" + rm -rf /var/lib/apt/lists/* + elif command -v apk &>/dev/null; then + apk add --no-cache "${FALLBACK_PKGS[@]}" + elif command -v dnf &>/dev/null; then + dnf install -y "${FALLBACK_PKGS[@]}" + dnf clean all + else + echo "Warning: installFallbackTools=true but no supported package manager found; skipping." >&2 + fi + fi +fi + +# Deliver the fallback installer to a stable PATH location so non-VS Code +# consumers (JetBrains, Vim, raw CLI) have something to invoke. The script +# is small and inert at rest, so we always copy regardless of options — +# only its runtime prerequisites are gated by installFallbackTools above. +if [ -f "${FEATURE_SRC_DIR}/scripts/setup-cert.sh" ]; then + install -m 0755 "${FEATURE_SRC_DIR}/scripts/setup-cert.sh" "${FALLBACK_BIN_PATH}" +else + echo "Warning: scripts/setup-cert.sh not found under ${FEATURE_SRC_DIR}; fallback installer will not be available." >&2 +fi + # Create .NET X509Store CurrentUser\My directory # This is where Kestrel discovers dev certs via X509Store fallback DOTNET_STORE_DIR="${REMOTE_USER_HOME}/.dotnet/corefx/cryptography/x509stores/my" @@ -220,6 +259,17 @@ append_profile "DEVCONTAINER_DEV_CERTS_SYNC_USER" "${SYNC_USER_CERTIFICATES}" append_profile "DEVCONTAINER_DEV_CERTS_SYNC_FROM_CONTAINER" "${SYNC_CONTAINER_CERT}" append_profile "DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS" "${EXTRA_CERT_DESTINATIONS}" +# Path hints for non-VS Code consumers. These let a postStartCommand or an +# editor "external tool" config invoke the installer and locate the trust +# stores without hardcoding the canonical paths. We export the resolved +# per-user paths into /etc/environment (pam_env can't expand $HOME) and the +# $HOME-expanded form into profile.d so each user gets their own at login. +append_profile "DEVCONTAINER_DEV_CERTS_INSTALL_BIN" "${FALLBACK_BIN_PATH}" +# Profile.d entries that need per-user $HOME expansion at login time. +echo "export DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/my\"" >> "${PROFILE_SCRIPT}" +echo "export DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/root\"" >> "${PROFILE_SCRIPT}" +echo "export DEVCONTAINER_DEV_CERTS_TRUST_DIR=\"\$HOME/.aspnet/dev-certs/trust\"" >> "${PROFILE_SCRIPT}" + # Suppress dotnet's first-run HTTPS dev cert provisioning ONLY when the # host is the source. # @@ -266,6 +316,10 @@ append_env "DEVCONTAINER_DEV_CERTS_GENERATE_DOTNET" "${GENERATE_DOTNET_CERT}" append_env "DEVCONTAINER_DEV_CERTS_SYNC_USER" "${SYNC_USER_CERTIFICATES}" append_env "DEVCONTAINER_DEV_CERTS_SYNC_FROM_CONTAINER" "${SYNC_CONTAINER_CERT}" append_env "DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS" "${EXTRA_CERT_DESTINATIONS}" +append_env "DEVCONTAINER_DEV_CERTS_INSTALL_BIN" "${FALLBACK_BIN_PATH}" +append_env "DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR" "${DOTNET_STORE_DIR}" +append_env "DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR" "${DOTNET_ROOT_STORE_DIR}" +append_env "DEVCONTAINER_DEV_CERTS_TRUST_DIR" "${TRUST_DIR}" # Mirror the dotnet-autogen suppression into /etc/environment so PAM-based # sessions (sshd, console) see the same gating logic as login shells. Same # conditional as the profile.d write above — see the comment there. @@ -310,9 +364,11 @@ echo " .NET cert store: ${DOTNET_STORE_DIR}" echo " .NET root store: ${DOTNET_ROOT_STORE_DIR}" echo " OpenSSL trust: ${TRUST_DIR}" echo " SSL_CERT_DIR: ${SSL_CERT_DIR_RESOLVED}" +echo " fallback installer: ${FALLBACK_BIN_PATH}" echo " generateDotNetCert: ${GENERATE_DOTNET_CERT}" echo " syncUserCertificates: ${SYNC_USER_CERTIFICATES}" echo " syncContainerCert: ${SYNC_CONTAINER_CERT}" +echo " installFallbackTools: ${INSTALL_FALLBACK_TOOLS}" if [ "${SUPPRESS_DOTNET_AUTOGEN}" = "true" ]; then echo " DOTNET_GENERATE_ASPNET_CERTIFICATE: false (host generates the dev cert — suppressing dotnet's racing first-run auto-gen)" else From d01e4e851c901fac204799179e12507cb886c15f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 20:46:04 +0000 Subject: [PATCH 02/41] Add read-only --doctor diagnostics to setup-cert.sh Non-VS Code users have no equivalent of the workspace extension's warnOnStaleDevCerts notifications or its silent post-install verification. When TLS misbehaves, the only way to find out why today is to grep the source. The new --doctor mode runs read-only checks across the trust infrastructure: prerequisite presence, trust-directory existence and writability, SSL_CERT_DIR / DOTNET_GENERATE_ASPNET_CERTIFICATE state, cert listings (subject / notAfter / SHA1) for both X509Store dirs, hash symlink integrity in the OpenSSL trust dir, and expired-cert / multiple- dev-cert detection. Exit code is non-zero only when something is demonstrably broken, so it's safe to wire into postStartCommand or CI checks. --- .../scripts/setup-cert.sh | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh index 15b7196..1aa0757 100755 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh @@ -5,6 +5,7 @@ # Usage: # setup-cert.sh # setup-cert.sh --bundle-json +# setup-cert.sh --doctor # # Bundle JSON form (accepts one or more certs plus optional extra destinations): # { @@ -25,6 +26,11 @@ # ] # } # +# Doctor mode runs read-only checks over the trust infrastructure (directories, +# env vars, cert presence, hash symlinks) and reports findings. Exits non-zero +# only when something is actually broken (missing prereqs, missing or +# unwritable trust dirs, expired managed certs). +# # This script requires openssl to be installed for hash computation and (in # the bundle form) PFX/root-PFX conversion. The bundle form additionally # requires `jq` for JSON parsing. @@ -156,6 +162,181 @@ write_extra_destination() { esac } +# --- Doctor mode: read-only diagnostics --- +if [ "${1:-}" = "--doctor" ]; then + doctor_errors=0 + doctor_warnings=0 + + # Output helpers. The tags double as a grep-able stable interface for + # tooling that wants to parse this — keep them short and stable. + ok() { printf ' [ok] %s\n' "$*"; } + warn() { printf ' [warn] %s\n' "$*"; doctor_warnings=$((doctor_warnings + 1)); } + fail() { printf ' [fail] %s\n' "$*"; doctor_errors=$((doctor_errors + 1)); } + info() { printf ' [info] %s\n' "$*"; } + section() { printf '\n%s\n' "$*"; } + + section "Prerequisites:" + if command -v openssl &>/dev/null; then + ok "openssl present ($(openssl version 2>/dev/null | head -1))" + else + fail "openssl missing — required for hash symlinks and root-PFX synthesis" + fi + if command -v jq &>/dev/null; then + ok "jq present ($(jq --version 2>/dev/null))" + else + warn "jq missing — needed only for --bundle-json mode; install if you use bundle invocations" + fi + + # Helper: check a directory exists and is writable by the current user. + # Doctor runs as whoever invoked it (typically the remote user, not root), + # which is the same identity that will later need to write certs here. + check_dir() { + local label="$1" + local dir="$2" + if [ ! -d "${dir}" ]; then + fail "${label}: ${dir} does not exist" + return + fi + if [ ! -w "${dir}" ]; then + fail "${label}: ${dir} exists but is not writable by $(id -un)" + return + fi + ok "${label}: ${dir}" + } + + section "Trust directories:" + check_dir ".NET CurrentUser/My " "${DOTNET_STORE_DIR}" + check_dir ".NET CurrentUser/Root" "${DOTNET_ROOT_STORE_DIR}" + check_dir "OpenSSL trust " "${TRUST_DIR}" + + section "Environment:" + if [ -n "${SSL_CERT_DIR:-}" ]; then + case ":${SSL_CERT_DIR}:" in + *:"${TRUST_DIR}":*) ok "SSL_CERT_DIR includes ${TRUST_DIR}" ;; + *) warn "SSL_CERT_DIR is set but does NOT include ${TRUST_DIR} — OpenSSL clients (curl, wget) won't trust the dev cert" ;; + esac + info "SSL_CERT_DIR=${SSL_CERT_DIR}" + else + warn "SSL_CERT_DIR is unset — log out / log back in to source /etc/profile.d, or export it manually" + fi + + case "${DOTNET_GENERATE_ASPNET_CERTIFICATE:-}" in + false) ok "DOTNET_GENERATE_ASPNET_CERTIFICATE=false (suppresses dotnet's first-run cert race)" ;; + "") info "DOTNET_GENERATE_ASPNET_CERTIFICATE unset — expected when syncContainerCert=true or generateDotNetCert=false; otherwise dotnet's first-run auto-gen may race the managed install" ;; + *) info "DOTNET_GENERATE_ASPNET_CERTIFICATE=${DOTNET_GENERATE_ASPNET_CERTIFICATE}" ;; + esac + + # Helper: describe a .pfx — subject + notAfter + fingerprint. Empty + # passphrase is assumed (matches the on-disk posture for the .NET + # X509Store on Linux). Returns 1 if the file can't be parsed. + describe_pfx() { + local path="$1" + local pem + pem=$(openssl pkcs12 -in "${path}" -nokeys -passin pass: 2>/dev/null) || return 1 + local subject not_after fp + subject=$(printf '%s' "${pem}" | openssl x509 -noout -subject 2>/dev/null | sed 's/^subject= *//') + not_after=$(printf '%s' "${pem}" | openssl x509 -noout -enddate 2>/dev/null | sed 's/^notAfter=//') + # OpenSSL 1.x prints `SHA1 Fingerprint=...`, OpenSSL 3.x prints + # `sha1 Fingerprint=...`. Strip up to and including the `=` to + # cover both spellings. + fp=$(printf '%s' "${pem}" | openssl x509 -noout -fingerprint -sha1 2>/dev/null | sed 's/^[^=]*=//' | tr -d ':') + printf '%s | notAfter=%s | sha1=%s' "${subject}" "${not_after}" "${fp}" + } + + # Helper: 0 if the cert PEM's notAfter is in the past, 1 otherwise. + pfx_is_expired() { + local path="$1" + local pem + pem=$(openssl pkcs12 -in "${path}" -nokeys -passin pass: 2>/dev/null) || return 1 + printf '%s' "${pem}" | openssl x509 -noout -checkend 0 &>/dev/null + # checkend exits 0 when cert is still valid for at least N seconds, + # 1 when expired. Invert. + [ $? -ne 0 ] + } + + section ".NET CurrentUser/My contents:" + if command -v openssl &>/dev/null && [ -d "${DOTNET_STORE_DIR}" ]; then + my_count=0 + # Glob pattern may yield the literal pattern when no matches; guard. + for pfx in "${DOTNET_STORE_DIR}"/*.pfx; do + [ -f "${pfx}" ] || continue + my_count=$((my_count + 1)) + desc=$(describe_pfx "${pfx}" 2>/dev/null) || desc="(unparseable PFX)" + info "$(basename "${pfx}"): ${desc}" + if pfx_is_expired "${pfx}"; then + fail "$(basename "${pfx}") has expired — Kestrel will not serve HTTPS with this cert" + fi + done + if [ "${my_count}" -eq 0 ]; then + warn "no .pfx files present — Kestrel's X509Store fallback will find nothing" + elif [ "${my_count}" -gt 1 ]; then + warn "${my_count} .pfx files present — multiple dev certs in this store cause nondeterministic Kestrel selection (run the host extension's 'clean up other dev certificates' command, or remove manually)" + fi + fi + + section ".NET CurrentUser/Root contents:" + if command -v openssl &>/dev/null && [ -d "${DOTNET_ROOT_STORE_DIR}" ]; then + root_count=0 + for pfx in "${DOTNET_ROOT_STORE_DIR}"/*.pfx; do + [ -f "${pfx}" ] || continue + root_count=$((root_count + 1)) + desc=$(describe_pfx "${pfx}" 2>/dev/null) || desc="(unparseable PFX)" + info "$(basename "${pfx}"): ${desc}" + done + if [ "${root_count}" -eq 0 ]; then + info "no .pfx files present — .NET will not consider any of My's certs as locally trusted" + fi + fi + + section "OpenSSL trust directory contents:" + if [ -d "${TRUST_DIR}" ]; then + pem_count=0 + for pem in "${TRUST_DIR}"/*.pem; do + [ -f "${pem}" ] || continue + pem_count=$((pem_count + 1)) + pem_name=$(basename "${pem}") + if command -v openssl &>/dev/null; then + hash=$(openssl x509 -hash -noout -in "${pem}" 2>/dev/null) + if [ -n "${hash}" ]; then + # Look for at least one symlink "${hash}.N" that resolves to this PEM. + found_link="" + for i in 0 1 2 3 4 5 6 7 8 9; do + link="${TRUST_DIR}/${hash}.${i}" + if [ -L "${link}" ] && [ "$(readlink "${link}")" = "${pem_name}" ]; then + found_link="${link}" + break + fi + done + if [ -n "${found_link}" ]; then + info "${pem_name} → $(basename "${found_link}") (hash ${hash})" + else + fail "${pem_name}: no c_rehash symlink (${hash}.N) pointing back — OpenSSL won't find this cert; re-run the installer or `openssl rehash ${TRUST_DIR}`" + fi + else + fail "${pem_name}: openssl could not compute a subject hash" + fi + else + info "${pem_name}" + fi + done + if [ "${pem_count}" -eq 0 ]; then + warn "no .pem files in ${TRUST_DIR} — OpenSSL-based tools (curl, wget) won't trust any dev cert" + fi + fi + + section "Summary:" + if [ "${doctor_errors}" -gt 0 ]; then + printf ' %d error(s), %d warning(s).\n' "${doctor_errors}" "${doctor_warnings}" + exit 1 + fi + if [ "${doctor_warnings}" -gt 0 ]; then + printf ' 0 errors, %d warning(s) — review above before relying on this setup.\n' "${doctor_warnings}" + else + printf ' All checks passed.\n' + fi + exit 0 +fi + # --- Bundle JSON form --- if [ "${1:-}" = "--bundle-json" ]; then BUNDLE="${2:?Usage: setup-cert.sh --bundle-json }" From 49c94b9088781a29c857dc48167534762c195aed Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 20:49:37 +0000 Subject: [PATCH 03/41] Document non-VS Code use and ship a worked example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A single "Limitations" bullet pointing at setup-cert.sh isn't enough for a JetBrains / Vim / CLI user to actually drive the fallback. The bundle JSON schema lived only in a script comment, there was no end-to-end walkthrough, and the lifecycle question (when to re-run the installer) was unaddressed. Three coordinated pieces: - schema/bundle.schema.json — a JSON Schema for the bundle format, consumable via $schema by any editor that honors it (JetBrains, vim + LSP, plain VS Code). Documents every field and constraint in a machine-readable form. - examples/manual-setup/ — a runnable example: devcontainer.json snippet with the postStartCommand pattern, a bundle.json referencing the schema, and a README walking through dotnet dev-certs export → thumbprint computation → verification. - README.md "Manual / non-VS Code use" section — pulls the bundle schema, lifecycle pattern, env-var hints, and --doctor invocation into one place; updates the feature-options table with the new installFallbackTools knob; replaces the dismissive Limitations bullet with an honest description of what's actually supported and what's still on the user. --- README.md | 117 +++++++++++++++++++++++- examples/manual-setup/README.md | 103 +++++++++++++++++++++ examples/manual-setup/bundle.json | 17 ++++ examples/manual-setup/devcontainer.json | 13 +++ schema/bundle.schema.json | 88 ++++++++++++++++++ 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 examples/manual-setup/README.md create mode 100644 examples/manual-setup/bundle.json create mode 100644 examples/manual-setup/devcontainer.json create mode 100644 schema/bundle.schema.json diff --git a/README.md b/README.md index 878dd13..a4e9e6c 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ Set under the feature entry in `devcontainer.json`: | `syncUserCertificates` | `true` | Per-container opt-out for syncing certs configured in the host `devcontainerDevCerts.userCertificates` VS Code setting. | | `syncContainerCert` | `false` | **Reverse sync (opt-in).** When the container itself already has a valid ASP.NET dev certificate (e.g. baked into the image with `dotnet dev-certs https`), push it to the host so the host trusts it instead of generating its own. Enabling this also implicitly overrides `generateDotNetCert` for this container — you don't need to set both. See "[Syncing a certificate from the container to the host](#syncing-a-certificate-from-the-container-to-the-host)". | | `extraCertDestinations` | `""` | Comma-separated list of additional directories to write cert artifacts to. Each entry is `[=]` where `format` is `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). Every synced cert is written under the directory as `{name}.{pem,key,pfx}` (and/or `{name}-bundle.pem`). Example: `/etc/nginx/certs=pem,/var/myapp`. | +| `installFallbackTools` | `false` | Install the runtime prerequisites (`openssl`, `jq`) the fallback `devcontainer-dev-certs-install` script needs. The script itself is always delivered to `/usr/local/bin/` regardless of this option — set this to `true` only when you intend to invoke it manually (JetBrains / Vim / CLI users) and your base image does not already provide `openssl` and `jq`. See "[Manual / non-VS Code use](#manual--non-vs-code-use)". | ### Host VS Code settings @@ -324,6 +325,120 @@ When this is on, non-local SAN entries are shown in the consent modal so you can Pushes from a Dev Container without the matching feature option are ignored — the host setting on its own doesn't do anything until a container actively pushes. Host trust prompts fire per unique thumbprint, so opening multiple containers with different container-generated certs will accumulate trust prompts; this is intentional and is why the option isn't on by default. +## Manual / non-VS Code use + +The companion-extension pattern is VS Code-specific, but the underlying container-side machinery isn't. JetBrains, Vim, raw CLI, and CI users can drive the same trust state through a small shell tool the feature installs into the container. + +For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference. + +### The fallback installer + +The feature delivers `devcontainer-dev-certs-install` to `/usr/local/bin/` during install. It writes to the same canonical paths the VS Code workspace extension uses — `~/.dotnet/corefx/cryptography/x509stores/{my,root}` and `~/.aspnet/dev-certs/trust/` (with c_rehash symlinks) — so Kestrel's `X509Store` fallback and OpenSSL clients discover certs installed this way exactly as they would extension-installed ones. + +Three invocation forms: + +```bash +# Single cert (legacy positional form) +devcontainer-dev-certs-install /path/to/cert.pfx /path/to/cert.pem + +# Multi-cert + extra destinations (preferred) +devcontainer-dev-certs-install --bundle-json /path/to/bundle.json + +# Read-only diagnostics +devcontainer-dev-certs-install --doctor +``` + +The bundle form requires `openssl` and `jq`; the positional form needs only `openssl`. Set the feature option `installFallbackTools: true` to have the feature install them when they're missing from the base image. + +### Bundle JSON + +A bundle describes one or more certs and (optionally) extra destinations. The full schema lives at [`schema/bundle.schema.json`](schema/bundle.schema.json); reference it from your bundle file to get autocomplete and validation in any editor that honors JSON Schema: + +```jsonc +{ + "$schema": "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json", + "certs": [ + { + "name": "aspnetcore-dev", + "kind": "dotnet-dev", + "thumbprint": "ABCDEF...", + "pfxPath": "/host-dev-certs/aspnetcore-dev.pfx", + "pemPath": "/host-dev-certs/aspnetcore-dev.pem", + "pemKeyPath": "/host-dev-certs/aspnetcore-dev.key", + "trustInContainer": true + } + ], + "extraDestinations": [ + { "path": "/etc/nginx/certs", "format": "pem" } + ] +} +``` + +Key fields: + +| Field | Notes | +|-------|-------| +| `certs[].name` | Filename stem in extra destinations and (for user certs) in the OpenSSL trust dir. `[A-Za-z0-9._-]`, 1-64 chars. | +| `certs[].thumbprint` | SHA-1 fingerprint, hex, no separators. Used as the `.pfx` filename in the .NET store where Kestrel discovers it. | +| `certs[].pemPath` | Required. PEM-encoded cert. | +| `certs[].pfxPath` | PFX with private key. Required if you want Kestrel to serve TLS with this cert. | +| `certs[].pemKeyPath` | Private key in PEM form. Optional; needed only for `key` / `pem-bundle` extra destination formats. | +| `certs[].rootPfxPath` | Public-cert-only PFX for the .NET Root store. Synthesized via `openssl pkcs12 -nokeys` when omitted. | +| `certs[].trustInContainer` | Default `true`. Install into the Root store + OpenSSL trust dir, not just My. | +| `certs[].kind` | `dotnet-dev` uses the historic `aspnetcore-localhost-{thumbprint}.pem` filename; `user` (default) uses `{name}.pem`. | +| `extraDestinations[].path` | Absolute directory to write artifacts under. | +| `extraDestinations[].format` | `pem`, `key`, `pem-bundle`, `pfx`, or `all` (default). | + +### Lifecycle: invoking the installer from `devcontainer.json` + +Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host takes effect on the next container start without a rebuild: + +```jsonc +{ + "features": { + "ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": { + "installFallbackTools": true + } + }, + "mounts": [ + "source=${localEnv:HOME}/.dev-certs,target=/host-dev-certs,type=bind,readonly" + ], + "postStartCommand": "devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json || true" +} +``` + +The `|| true` keeps a missing or malformed bundle from blocking container startup. + +### Path hints for integrations + +The feature exports a handful of `DEVCONTAINER_DEV_CERTS_*` env vars so integration scripts don't have to hardcode the canonical paths. They're available in login shells (via `/etc/profile.d/`) and PAM-based sessions (via `/etc/environment`): + +| Variable | Value | +|----------|-------| +| `DEVCONTAINER_DEV_CERTS_INSTALL_BIN` | `/usr/local/bin/devcontainer-dev-certs-install` | +| `DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR` | `~/.dotnet/corefx/cryptography/x509stores/my` | +| `DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR` | `~/.dotnet/corefx/cryptography/x509stores/root` | +| `DEVCONTAINER_DEV_CERTS_TRUST_DIR` | `~/.aspnet/dev-certs/trust` | +| `DEVCONTAINER_DEV_CERTS_GENERATE_DOTNET` | Mirror of the `generateDotNetCert` feature option. | +| `DEVCONTAINER_DEV_CERTS_SYNC_USER` | Mirror of the `syncUserCertificates` feature option. | +| `DEVCONTAINER_DEV_CERTS_SYNC_FROM_CONTAINER` | Mirror of the `syncContainerCert` feature option. | +| `DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS` | Mirror of the `extraCertDestinations` feature option. | + +### Verifying with `--doctor` + +`devcontainer-dev-certs-install --doctor` runs read-only checks across the trust infrastructure: prerequisite presence, trust-directory existence and writability, `SSL_CERT_DIR` state, the gating of `DOTNET_GENERATE_ASPNET_CERTIFICATE`, per-store cert listings (subject / `notAfter` / SHA-1), c_rehash symlink integrity, and expired-cert / multiple-dev-cert detection. Exits non-zero only when something is demonstrably broken, so it's safe to chain into CI: + +```bash +devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json \ + && devcontainer-dev-certs-install --doctor +``` + +### What's still VS Code-only + +- **Host-side cert generation and trust.** Use `dotnet dev-certs https --trust` (or your platform's equivalent) and export the PFX / PEM into your mounted host directory. The host extension's editor-agnostic generator is on the roadmap. +- **`defaultKestrelCertificate`.** Lives in a VS Code setting and is delivered via `EnvironmentVariableCollection`. Set `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` yourself in `devcontainer.json` `containerEnv` if you need this outside VS Code. +- **Reverse sync (`syncContainerCert`).** Needs a privileged host-side process with a consent UI; the script can't perform host trust. + ## Development ### Prerequisites @@ -353,7 +468,7 @@ This feature applies the workaround at the end of its own install script, so com ## Limitations - **Auto-generated dev cert matches .NET's format only.** The `generateDotNetCert` flow produces a cert identical to `dotnet dev-certs https` (specific OID marker, subject, SAN entries). To sync differently-shaped certs (corporate CAs, custom wildcard certs, etc.), add them via the `devcontainerDevCerts.userCertificates` VS Code setting — they're copied as-is. -- **VS Code only.** The companion extension pattern relies on VS Code's cross-host command routing. Other editors (JetBrains, Vim, etc.) are not supported, though the devcontainer feature includes a `setup-cert.sh` fallback script (with a `--bundle-json` form for multi-cert bundles) for manual use. +- **Full automation is VS Code-only.** The host extension's certificate generation, OS trust, and cross-host routing rely on VS Code APIs. JetBrains, Vim, and CLI users can drive the same container-side trust state through the `devcontainer-dev-certs-install` fallback installer the feature ships at `/usr/local/bin/` — see "[Manual / non-VS Code use](#manual--non-vs-code-use)" — but cert generation and host trust are still on the user. - **Host trust requires user interaction.** On Windows, trusting the auto-generated dev cert triggers a system dialog. On macOS, the keychain may prompt for a password. This only happens once and only for the .NET dev cert — user-managed certs are never added to the host OS trust store. ## Supported Platforms diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md new file mode 100644 index 0000000..53976cb --- /dev/null +++ b/examples/manual-setup/README.md @@ -0,0 +1,103 @@ +# manual-setup + +End-to-end example for using `devcontainer-dev-certs` outside of VS Code (JetBrains, Vim, raw CLI, CI). + +## What this gives you + +The same canonical trust state the VS Code workspace extension produces — `~/.dotnet/corefx/cryptography/x509stores/{my,root}` populated with your dev cert and `~/.aspnet/dev-certs/trust/` populated with hash-symlinked PEMs — without VS Code being involved. Kestrel discovers the cert via its `X509Store` fallback; `curl`, `wget`, and other OpenSSL clients trust it via `SSL_CERT_DIR`. + +## Prerequisites + +- The `devcontainer-dev-certs` feature in your `devcontainer.json` with `installFallbackTools: true` (or `openssl` and `jq` already present in your base image). +- A directory on the host containing the cert files you want installed, plus a `bundle.json` describing them. + +## One-time host setup + +Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`). On Windows / macOS / Linux: + +```bash +mkdir -p ~/.dev-certs +``` + +Generate the ASP.NET dev cert and export both forms: + +```bash +dotnet dev-certs https --trust +dotnet dev-certs https --format Pfx --no-password \ + --export-path ~/.dev-certs/aspnetcore-dev.pfx +dotnet dev-certs https --format PEM --no-password \ + --export-path ~/.dev-certs/aspnetcore-dev.pem +``` + +Compute the SHA-1 fingerprint (this is what `bundle.json` calls `thumbprint`): + +```bash +openssl x509 -in ~/.dev-certs/aspnetcore-dev.pem -noout -fingerprint -sha1 \ + | sed 's/^[^=]*=//' | tr -d ':' +``` + +Drop a copy of [`bundle.json`](./bundle.json) into `~/.dev-certs/` and replace `REPLACE_WITH_SHA1_FINGERPRINT_NO_COLONS` with the fingerprint you just computed. + +> **Note on `dotnet dev-certs`-generated certs vs the host extension's certs.** This example assumes you're using `dotnet dev-certs https` for cert generation. The host extension produces functionally equivalent certs with the same OID marker and SAN entries — either source works against the same fallback installer in the container. If you need to share a *specific* cert with the host extension (e.g. a Windows developer also runs the extension), generate it once and have both flows consume the same PFX. + +## Project setup + +Copy the bits of [`devcontainer.json`](./devcontainer.json) you want into your own `.devcontainer/devcontainer.json`: + +- the `devcontainer-dev-certs` feature reference with `installFallbackTools: true` +- the `mounts` entry that binds your host cert directory to `/host-dev-certs` read-only +- the `postStartCommand` that invokes the fallback installer + +The fallback installer is delivered to `/usr/local/bin/devcontainer-dev-certs-install` by the feature. + +Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`dotnet dev-certs https --clean && dotnet dev-certs https --trust && …re-export…`) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. + +## Verifying + +After the container starts, run: + +```bash +devcontainer-dev-certs-install --doctor +``` + +You should see `[ok]` for every check. If you see `[fail]` or `[warn]`, the message tells you what to fix. + +You can also sanity-check from inside the container: + +```bash +# Kestrel-style discovery +ls ~/.dotnet/corefx/cryptography/x509stores/my/ + +# OpenSSL trust (curl, wget, etc.) +echo "$SSL_CERT_DIR" +ls ~/.aspnet/dev-certs/trust/ +``` + +## Adding more certs + +The bundle is a list — add corporate CAs, wildcard certs, etc. as additional entries: + +```jsonc +{ + "$schema": "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json", + "certs": [ + { "name": "aspnetcore-dev", "kind": "dotnet-dev", ... }, + { + "name": "corp-ca", + "thumbprint": "...", + "pemPath": "/host-dev-certs/corp-ca.pem", + "trustInContainer": true + } + ] +} +``` + +CA-only entries (no `pfxPath`, no `pemKeyPath`) are valid — they get planted in the trust store but no private key is synced. + +See the [bundle schema](../../schema/bundle.schema.json) for the full field reference. + +## Limitations + +- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `dotnet dev-certs https --trust` (the example above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. +- **No `defaultKestrelCertificate` equivalent.** The VS Code-only `defaultKestrelCertificate` setting writes `ASPNETCORE_Kestrel__Certificates__Default__Path/__Password` via VS Code's `EnvironmentVariableCollection`. To pin a custom Kestrel default outside VS Code, set those env vars yourself in `devcontainer.json` `containerEnv`. +- **No reverse sync (container → host).** The `syncContainerCert` flow needs a privileged host-side process to add the cert to the host OS trust store; without the host extension's UI there's nowhere to surface the consent prompt. diff --git a/examples/manual-setup/bundle.json b/examples/manual-setup/bundle.json new file mode 100644 index 0000000..6d430b7 --- /dev/null +++ b/examples/manual-setup/bundle.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json", + "certs": [ + { + "name": "aspnetcore-dev", + "kind": "dotnet-dev", + "thumbprint": "REPLACE_WITH_SHA1_FINGERPRINT_NO_COLONS", + "pfxPath": "/host-dev-certs/aspnetcore-dev.pfx", + "pemPath": "/host-dev-certs/aspnetcore-dev.pem", + "pemKeyPath": "/host-dev-certs/aspnetcore-dev.key", + "trustInContainer": true + } + ], + "extraDestinations": [ + { "path": "/etc/nginx/certs", "format": "pem" } + ] +} diff --git a/examples/manual-setup/devcontainer.json b/examples/manual-setup/devcontainer.json new file mode 100644 index 0000000..6c9d13b --- /dev/null +++ b/examples/manual-setup/devcontainer.json @@ -0,0 +1,13 @@ +{ + "name": "manual-setup-example", + "image": "mcr.microsoft.com/devcontainers/dotnet:9.0", + "features": { + "ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1": { + "installFallbackTools": true + } + }, + "mounts": [ + "source=${localEnv:HOME}/.dev-certs,target=/host-dev-certs,type=bind,readonly" + ], + "postStartCommand": "devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json || true" +} diff --git a/schema/bundle.schema.json b/schema/bundle.schema.json new file mode 100644 index 0000000..18fa56a --- /dev/null +++ b/schema/bundle.schema.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json", + "title": "devcontainer-dev-certs bundle", + "description": "Input schema for the `devcontainer-dev-certs-install --bundle-json` fallback installer. Use this when invoking the installer outside of VS Code (JetBrains, Vim, raw CLI, CI). Reference from your bundle file via `\"$schema\": \"https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json\"` to get autocomplete and validation in any editor that honors JSON Schema.", + "type": "object", + "additionalProperties": false, + "required": ["certs"], + "properties": { + "$schema": { + "type": "string", + "description": "Schema URL. Optional; present so editors can pick up validation without out-of-band configuration." + }, + "certs": { + "type": "array", + "description": "Certificates to install into the container's .NET X509Store and (when trustInContainer is true) the OpenSSL trust directory.", + "items": { "$ref": "#/$defs/cert" } + }, + "extraDestinations": { + "type": "array", + "description": "Additional directories to write cert artifacts to. Every cert in `certs` is written to every destination according to the destination's `format`. Useful for non-.NET workloads (nginx, Java keystores, Python requests bundles, etc.).", + "items": { "$ref": "#/$defs/extraDestination" } + } + }, + "$defs": { + "cert": { + "type": "object", + "additionalProperties": false, + "required": ["name", "thumbprint", "pemPath"], + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z0-9._-]{1,64}$", + "description": "Filename stem used for this cert in extra destinations and (for user certs) in the OpenSSL trust directory. Constrained to [A-Za-z0-9._-], 1-64 chars." + }, + "thumbprint": { + "type": "string", + "pattern": "^[A-Fa-f0-9]+$", + "description": "SHA-1 fingerprint of the certificate, hex-encoded with no separators. Used as the on-disk filename in the .NET X509Store (`{thumbprint}.pfx`) where Kestrel discovers it." + }, + "pemPath": { + "type": "string", + "description": "Absolute path to the certificate in PEM form. Required; used to compute the OpenSSL trust-dir filename and to synthesize the root-store PFX when `rootPfxPath` is omitted." + }, + "pfxPath": { + "type": "string", + "description": "Absolute path to a PFX (PKCS#12) containing the certificate and its private key. When present, copied verbatim to the .NET X509Store as `{thumbprint}.pfx`. Optional only for CA-only certs (no private key)." + }, + "pemKeyPath": { + "type": "string", + "description": "Absolute path to the private key in PEM form. Optional; used when writing `{name}-bundle.pem` or `{name}.key` to extra destinations." + }, + "rootPfxPath": { + "type": "string", + "description": "Absolute path to a public-cert-only PFX for installation into the .NET CurrentUser/Root store. Optional; synthesized via `openssl pkcs12 -nokeys` when omitted." + }, + "trustInContainer": { + "type": "boolean", + "default": true, + "description": "When true (default), install into the .NET CurrentUser/Root store and the OpenSSL trust directory in addition to CurrentUser/My. When false, only the My store is populated — useful when the cert is for serving only, not for trust." + }, + "kind": { + "type": "string", + "enum": ["dotnet-dev", "user"], + "default": "user", + "description": "`dotnet-dev` uses the historic `aspnetcore-localhost-{thumbprint}.pem` filename in the OpenSSL trust directory; `user` (default) uses `{name}.pem`." + } + } + }, + "extraDestination": { + "type": "object", + "additionalProperties": false, + "required": ["path"], + "properties": { + "path": { + "type": "string", + "description": "Absolute path of a directory to write cert artifacts into. Created if it does not exist." + }, + "format": { + "type": "string", + "enum": ["pem", "key", "pem-bundle", "pfx", "all"], + "default": "all", + "description": "Which artifact(s) to write per cert. `pem` = `{name}.pem` (cert only); `key` = `{name}.key`; `pem-bundle` = `{name}-bundle.pem` (cert+key); `pfx` = `{name}.pfx`; `all` = every applicable artifact." + } + } + } + } +} From f331b7f0433c22dfc0bff0a36c60e2d9cb9fadf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 21:16:31 +0000 Subject: [PATCH 04/41] Move cert generator and exporter into the shared package Phase 1a of carving a reusable host-side cert library out of the UI extension so a future host CLI (and an eventual non-VS Code distribution path) can consume the same code. Continues the existing `@devcontainer-dev-certs/shared` re-export pattern already in use for loader / pfx / properties / cert-types. `src/shared/src/cert/exporter.ts` and `src/shared/src/cert/generator.ts` now hold the canonical implementations; the previous UI-extension copies become thin re-export shims so existing `./cert/exporter` / `./cert/generator` imports across the extension and its 18-file test suite resolve unchanged. No behavior change: 207 tests, type-check, and full-repo lint all pass. --- src/shared/src/cert/exporter.ts | 134 ++++++++++ src/shared/src/cert/generator.ts | 223 ++++++++++++++++ src/shared/src/index.ts | 12 + src/vscode-ui-extension/src/cert/exporter.ts | 148 +---------- src/vscode-ui-extension/src/cert/generator.ts | 242 +----------------- 5 files changed, 395 insertions(+), 364 deletions(-) create mode 100644 src/shared/src/cert/exporter.ts create mode 100644 src/shared/src/cert/generator.ts diff --git a/src/shared/src/cert/exporter.ts b/src/shared/src/cert/exporter.ts new file mode 100644 index 0000000..0a4a432 --- /dev/null +++ b/src/shared/src/cert/exporter.ts @@ -0,0 +1,134 @@ +import * as fs from "fs"; +import * as path from "path"; +import type { LoadedCert } from "./loader"; +import { type DevCert, type DevKey } from "./types"; +import { ASPNET_HTTPS_OID_FRIENDLY_NAME } from "./properties"; +import { buildPfx } from "./pfx"; + +// PFX and unencrypted key PEM contain private key material. Use mode 0o600 on +// every write — the temp dir created upstream is already mkdtemp'd 0o700, but +// explicit file modes survive being copied or extracted elsewhere. +const PRIVATE_FILE_MODE = 0o600; +const PUBLIC_FILE_MODE = 0o644; + +/** + * Export a certificate with its private key as a PFX/PKCS12 file. + */ +export async function exportPfx( + cert: DevCert, + key: DevKey, + outputDir: string, + password?: string +): Promise { + fs.mkdirSync(outputDir, { recursive: true }); + const der = await buildPfx({ + cert, + key, + password, + friendlyName: ASPNET_HTTPS_OID_FRIENDLY_NAME, + }); + const outPath = path.join(outputDir, "aspnetcore-dev.pfx"); + fs.writeFileSync(outPath, der, { mode: PRIVATE_FILE_MODE }); + return outPath; +} + +/** + * Export a certificate and private key as PEM files. + * Returns { certPath, keyPath }. + */ +export function exportPem( + cert: DevCert, + key: DevKey, + outputDir: string +): { certPath: string; keyPath: string } { + fs.mkdirSync(outputDir, { recursive: true }); + + const certPath = path.join(outputDir, "aspnetcore-dev.pem"); + const keyPath = path.join(outputDir, "aspnetcore-dev.key"); + + fs.writeFileSync(certPath, cert.pem, { mode: PUBLIC_FILE_MODE }); + fs.writeFileSync(keyPath, key.pem, { mode: PRIVATE_FILE_MODE }); + + return { certPath, keyPath }; +} + +/** + * Convert a certificate to PEM format string. + */ +export function certToPem(cert: DevCert): string { + return cert.pem; +} + +/** + * Convert a private key to PEM format string (PKCS#8 unencrypted). + */ +export function keyToPem(key: DevKey): string { + return key.pem; +} + +/** + * Export certificate as DER-encoded bytes (public cert only, no private key). + */ +export function certToDer(cert: DevCert): Buffer { + return cert.der; +} + +/** + * Export a public-cert-only PFX for the .NET Root store. + * This matches what `dotnet dev-certs https --trust` writes to + * ~/.dotnet/corefx/cryptography/x509stores/root/ on Linux. + */ +export async function exportRootPfx( + cert: DevCert, + outputDir: string +): Promise { + fs.mkdirSync(outputDir, { recursive: true }); + const der = await buildPfx({ cert }); + const outPath = path.join(outputDir, "aspnetcore-dev-root.pfx"); + fs.writeFileSync(outPath, der, { mode: PUBLIC_FILE_MODE }); + return outPath; +} + +export interface ExportedLoadedCert { + pemCertPath: string; + pemKeyPath: string | null; + rootPfxPath: string | null; +} + +/** + * Export a user-managed (or generically loaded) certificate's PEM artifacts + * to a directory under a stable `{name}.*` filename scheme. The cert is + * always written; the key is only written when a key is attached; the + * public-cert-only root PFX is only produced when `includeRootPfx` is true. + * + * Notably this does NOT synthesize a key-bearing `{name}.pfx`. That decision + * lives with the host orchestrator, where the user's pfxPassword is in + * scope — silently encoding a passwordless PFX here would strip the user's + * password without their consent. + */ +export async function exportLoadedCert( + loaded: LoadedCert, + name: string, + outputDir: string, + options: { includeRootPfx?: boolean } = {} +): Promise { + fs.mkdirSync(outputDir, { recursive: true }); + + const pemCertPath = path.join(outputDir, `${name}.pem`); + fs.writeFileSync(pemCertPath, loaded.cert.pem, { mode: PUBLIC_FILE_MODE }); + + let pemKeyPath: string | null = null; + if (loaded.key) { + pemKeyPath = path.join(outputDir, `${name}.key`); + fs.writeFileSync(pemKeyPath, loaded.key.pem, { mode: PRIVATE_FILE_MODE }); + } + + let rootPfxPath: string | null = null; + if (options.includeRootPfx) { + const rootBytes = await buildPfx({ cert: loaded.cert }); + rootPfxPath = path.join(outputDir, `${name}-root.pfx`); + fs.writeFileSync(rootPfxPath, rootBytes, { mode: PUBLIC_FILE_MODE }); + } + + return { pemCertPath, pemKeyPath, rootPfxPath }; +} diff --git a/src/shared/src/cert/generator.ts b/src/shared/src/cert/generator.ts new file mode 100644 index 0000000..a10bc17 --- /dev/null +++ b/src/shared/src/cert/generator.ts @@ -0,0 +1,223 @@ +import { + AuthorityKeyIdentifierExtension, + BasicConstraintsExtension, + ExtendedKeyUsage, + ExtendedKeyUsageExtension, + Extension, + KeyUsageFlags, + KeyUsagesExtension, + SubjectAlternativeNameExtension, + SubjectKeyIdentifierExtension, + X509CertificateGenerator, + cryptoProvider, +} from "@peculiar/x509"; +import { randomBytes, webcrypto } from "node:crypto"; +import { DevCert, DevKey } from "./types"; +import { + ASPNET_HTTPS_OID, + CURRENT_CERTIFICATE_VERSION, + RSA_KEY_SIZE, + SAN_DNS_NAMES, + SAN_IP_ADDRESSES, +} from "./properties"; + +let cryptoProviderConfigured = false; +function ensureCryptoProvider(): void { + if (cryptoProviderConfigured) return; + cryptoProvider.set(webcrypto as unknown as Crypto); + cryptoProviderConfigured = true; +} + +export interface GeneratedCert { + cert: DevCert; + key: DevKey; + /** + * SHA-1 thumbprint, uppercase hex. This is the .NET-compatible + * `X509Certificate2.Thumbprint` value used as the X509Store filename + * (`{thumbprint}.pfx`). For a stronger cert identifier, use + * `cert.thumbprint` (SHA-256). + */ + thumbprint: string; +} + +/** + * Algorithm choices for `generateCertificate`. Defaults to RSA-2048 to match + * the historical ASP.NET dev cert format, but the same code path supports + * ECDSA P-256/P-384/P-521 and Ed25519 for user-managed flows. + */ +export type GenerateAlgorithm = + | { kind: "rsa"; modulusLength?: number } + | { kind: "ec"; namedCurve: "P-256" | "P-384" | "P-521" } + | { kind: "ed25519" } + | { kind: "ed448" }; + +/** + * Generate a self-signed certificate matching the ASP.NET Core HTTPS dev + * cert format (subject, validity, SANs, custom OID, SKI/AKI). + * + * The default algorithm is RSA-2048 with SHA-256 signing — a byte-for-byte + * compatible replacement for the previous `node-forge` path. Pass an + * `algorithm` to opt into ECDSA / Ed25519 (used by user-managed cert flows + * that need to round-trip non-RSA keys). + */ +export async function generateCertificate( + notBefore: Date, + notAfter: Date, + algorithm: GenerateAlgorithm = { kind: "rsa" } +): Promise { + ensureCryptoProvider(); + + const { keyPair, signingAlgorithm } = await generateKeyPair(algorithm); + + const serialNumber = generateSerialNumber(); + const subject = "CN=localhost"; + const issuer = subject; + + const sanEntries = [ + ...SAN_DNS_NAMES.map( + (dns) => ({ type: "dns" as const, value: dns }) + ), + ...SAN_IP_ADDRESSES.map( + (ip) => ({ type: "ip" as const, value: ip }) + ), + ]; + + const extensions: Extension[] = [ + new BasicConstraintsExtension(false, undefined, true), + new KeyUsagesExtension( + KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, + true + ), + new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true), + new SubjectAlternativeNameExtension(sanEntries, true), + new Extension( + ASPNET_HTTPS_OID, + false, + new Uint8Array([CURRENT_CERTIFICATE_VERSION]).buffer + ), + await SubjectKeyIdentifierExtension.create(keyPair.publicKey), + await AuthorityKeyIdentifierExtension.create(keyPair.publicKey), + ]; + + const cert = await X509CertificateGenerator.create({ + serialNumber, + subject, + issuer, + notBefore, + notAfter, + signingAlgorithm, + publicKey: keyPair.publicKey, + signingKey: keyPair.privateKey, + extensions, + }); + + const devCert = new DevCert(cert); + const devKey = await DevKey.fromCryptoKey(keyPair.privateKey); + + return { + cert: devCert, + key: devKey, + thumbprint: devCert.thumbprintSha1, + }; +} + +async function generateKeyPair( + algorithm: GenerateAlgorithm +): Promise<{ + keyPair: CryptoKeyPair; + signingAlgorithm: Algorithm | RsaHashedKeyGenParams | EcdsaParams; +}> { + const subtle = webcrypto.subtle; + + if (algorithm.kind === "rsa") { + const modulusLength = algorithm.modulusLength ?? RSA_KEY_SIZE; + const params: RsaHashedKeyGenParams = { + name: "RSASSA-PKCS1-v1_5", + modulusLength, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }; + const keyPair = (await subtle.generateKey(params, true, [ + "sign", + "verify", + ])); + return { + keyPair, + signingAlgorithm: { + name: "RSASSA-PKCS1-v1_5", + hash: "SHA-256", + } as RsaHashedKeyGenParams, + }; + } + + if (algorithm.kind === "ec") { + const params: EcKeyGenParams = { + name: "ECDSA", + namedCurve: algorithm.namedCurve, + }; + const keyPair = (await subtle.generateKey(params, true, [ + "sign", + "verify", + ])); + return { + keyPair, + signingAlgorithm: { + name: "ECDSA", + hash: defaultEcHash(algorithm.namedCurve), + } as EcdsaParams, + }; + } + + if (algorithm.kind === "ed25519") { + const keyPair = (await subtle.generateKey( + "Ed25519", + true, + ["sign", "verify"] + )) as CryptoKeyPair; + return { + keyPair, + signingAlgorithm: { name: "Ed25519" }, + }; + } + + if (algorithm.kind === "ed448") { + const keyPair = (await subtle.generateKey( + { name: "Ed448" }, + true, + ["sign", "verify"] + )) as CryptoKeyPair; + return { + keyPair, + signingAlgorithm: { name: "Ed448" }, + }; + } + + throw new Error( + `Unsupported algorithm: ${(algorithm as { kind: string }).kind}` + ); +} + +function defaultEcHash(curve: string): string { + switch (curve) { + case "P-256": + return "SHA-256"; + case "P-384": + return "SHA-384"; + case "P-521": + return "SHA-512"; + default: + return "SHA-256"; + } +} + +function generateSerialNumber(): string { + const maxAttempts = 5; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const bytes = randomBytes(16); + bytes[0] &= 0x7f; // ensure non-negative + if (bytes.some((value) => value !== 0)) { + return bytes.toString("hex"); + } + } + throw new Error("Failed to generate a non-zero certificate serial number."); +} diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index 728656c..a6f9406 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -68,3 +68,15 @@ export type { SelectBestOptions, UsableDevCert, } from "./cert/classify"; +export { generateCertificate } from "./cert/generator"; +export type { GenerateAlgorithm, GeneratedCert } from "./cert/generator"; +export { + exportPfx, + exportPem, + exportRootPfx, + exportLoadedCert, + certToPem, + keyToPem, + certToDer, +} from "./cert/exporter"; +export type { ExportedLoadedCert } from "./cert/exporter"; diff --git a/src/vscode-ui-extension/src/cert/exporter.ts b/src/vscode-ui-extension/src/cert/exporter.ts index 62700d6..58f7f41 100644 --- a/src/vscode-ui-extension/src/cert/exporter.ts +++ b/src/vscode-ui-extension/src/cert/exporter.ts @@ -1,134 +1,14 @@ -import * as fs from "fs"; -import * as path from "path"; -import type { LoadedCert } from "./loader"; -import { type DevCert, type DevKey } from "./types"; -import { ASPNET_HTTPS_OID_FRIENDLY_NAME } from "./properties"; -import { buildPfx } from "./pfx"; - -// PFX and unencrypted key PEM contain private key material. Use mode 0o600 on -// every write — the temp dir created upstream is already mkdtemp'd 0o700, but -// explicit file modes survive being copied or extracted elsewhere. -const PRIVATE_FILE_MODE = 0o600; -const PUBLIC_FILE_MODE = 0o644; - -/** - * Export a certificate with its private key as a PFX/PKCS12 file. - */ -export async function exportPfx( - cert: DevCert, - key: DevKey, - outputDir: string, - password?: string -): Promise { - fs.mkdirSync(outputDir, { recursive: true }); - const der = await buildPfx({ - cert, - key, - password, - friendlyName: ASPNET_HTTPS_OID_FRIENDLY_NAME, - }); - const outPath = path.join(outputDir, "aspnetcore-dev.pfx"); - fs.writeFileSync(outPath, der, { mode: PRIVATE_FILE_MODE }); - return outPath; -} - -/** - * Export a certificate and private key as PEM files. - * Returns { certPath, keyPath }. - */ -export function exportPem( - cert: DevCert, - key: DevKey, - outputDir: string -): { certPath: string; keyPath: string } { - fs.mkdirSync(outputDir, { recursive: true }); - - const certPath = path.join(outputDir, "aspnetcore-dev.pem"); - const keyPath = path.join(outputDir, "aspnetcore-dev.key"); - - fs.writeFileSync(certPath, cert.pem, { mode: PUBLIC_FILE_MODE }); - fs.writeFileSync(keyPath, key.pem, { mode: PRIVATE_FILE_MODE }); - - return { certPath, keyPath }; -} - -/** - * Convert a certificate to PEM format string. - */ -export function certToPem(cert: DevCert): string { - return cert.pem; -} - -/** - * Convert a private key to PEM format string (PKCS#8 unencrypted). - */ -export function keyToPem(key: DevKey): string { - return key.pem; -} - -/** - * Export certificate as DER-encoded bytes (public cert only, no private key). - */ -export function certToDer(cert: DevCert): Buffer { - return cert.der; -} - -/** - * Export a public-cert-only PFX for the .NET Root store. - * This matches what `dotnet dev-certs https --trust` writes to - * ~/.dotnet/corefx/cryptography/x509stores/root/ on Linux. - */ -export async function exportRootPfx( - cert: DevCert, - outputDir: string -): Promise { - fs.mkdirSync(outputDir, { recursive: true }); - const der = await buildPfx({ cert }); - const outPath = path.join(outputDir, "aspnetcore-dev-root.pfx"); - fs.writeFileSync(outPath, der, { mode: PUBLIC_FILE_MODE }); - return outPath; -} - -export interface ExportedLoadedCert { - pemCertPath: string; - pemKeyPath: string | null; - rootPfxPath: string | null; -} - -/** - * Export a user-managed (or generically loaded) certificate's PEM artifacts - * to a directory under a stable `{name}.*` filename scheme. The cert is - * always written; the key is only written when a key is attached; the - * public-cert-only root PFX is only produced when `includeRootPfx` is true. - * - * Notably this does NOT synthesize a key-bearing `{name}.pfx`. That decision - * lives in certProvider, where the user's pfxPassword is in scope — silently - * encoding a passwordless PFX here would strip the user's password without - * their consent. See certProvider.ts:loadUserCert for the PFX byte source. - */ -export async function exportLoadedCert( - loaded: LoadedCert, - name: string, - outputDir: string, - options: { includeRootPfx?: boolean } = {} -): Promise { - fs.mkdirSync(outputDir, { recursive: true }); - - const pemCertPath = path.join(outputDir, `${name}.pem`); - fs.writeFileSync(pemCertPath, loaded.cert.pem, { mode: PUBLIC_FILE_MODE }); - - let pemKeyPath: string | null = null; - if (loaded.key) { - pemKeyPath = path.join(outputDir, `${name}.key`); - fs.writeFileSync(pemKeyPath, loaded.key.pem, { mode: PRIVATE_FILE_MODE }); - } - - let rootPfxPath: string | null = null; - if (options.includeRootPfx) { - const rootBytes = await buildPfx({ cert: loaded.cert }); - rootPfxPath = path.join(outputDir, `${name}-root.pfx`); - fs.writeFileSync(rootPfxPath, rootBytes, { mode: PUBLIC_FILE_MODE }); - } - - return { pemCertPath, pemKeyPath, rootPfxPath }; -} +// Re-export shim: the canonical home for the exporter helpers is now +// `@devcontainer-dev-certs/shared`. Keeping this thin re-export so existing +// `./cert/exporter` imports across the UI extension (and its test suite) +// keep resolving without a sweeping rename. +export { + exportPfx, + exportPem, + exportRootPfx, + exportLoadedCert, + certToPem, + keyToPem, + certToDer, +} from "@devcontainer-dev-certs/shared"; +export type { ExportedLoadedCert } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/cert/generator.ts b/src/vscode-ui-extension/src/cert/generator.ts index f1fc159..7b106be 100644 --- a/src/vscode-ui-extension/src/cert/generator.ts +++ b/src/vscode-ui-extension/src/cert/generator.ts @@ -1,235 +1,17 @@ -import { - AuthorityKeyIdentifierExtension, - BasicConstraintsExtension, - ExtendedKeyUsage, - ExtendedKeyUsageExtension, - Extension, - KeyUsageFlags, - KeyUsagesExtension, - SubjectAlternativeNameExtension, - SubjectKeyIdentifierExtension, - X509CertificateGenerator, - cryptoProvider, -} from "@peculiar/x509"; -import { randomBytes, webcrypto } from "node:crypto"; -import { - DevCert, - DevKey, - ASPNET_HTTPS_OID, - CURRENT_CERTIFICATE_VERSION, - RSA_KEY_SIZE, - SAN_DNS_NAMES, - SAN_IP_ADDRESSES, -} from "@devcontainer-dev-certs/shared"; - -// `isValidDevCert` and `getCertificateVersion` used to live in this file; -// they now live in `@devcontainer-dev-certs/shared/cert/validation` so the -// workspace extension can call the same code path when scanning for a -// container-side dev cert to push to the host. Re-exported here so existing -// imports of `./cert/generator` keep working unchanged. +// Re-export shim: the canonical home for `generateCertificate` is now +// `@devcontainer-dev-certs/shared`. Keeping this thin re-export so existing +// `./cert/generator` imports across the UI extension (and its test suite) +// keep resolving without a sweeping rename. The validation helpers +// (`isValidDevCert`, `getCertificateVersion`, `computeThumbprint`) used to be +// defined alongside the generator and were re-exported here for historical +// reasons — we preserve those re-exports so existing call sites still resolve. export { + generateCertificate, isValidDevCert, getCertificateVersion, computeThumbprint, } from "@devcontainer-dev-certs/shared"; - -let cryptoProviderConfigured = false; -function ensureCryptoProvider(): void { - if (cryptoProviderConfigured) return; - cryptoProvider.set(webcrypto as unknown as Crypto); - cryptoProviderConfigured = true; -} - -export interface GeneratedCert { - cert: DevCert; - key: DevKey; - /** - * SHA-1 thumbprint, uppercase hex. This is the .NET-compatible - * `X509Certificate2.Thumbprint` value used as the X509Store filename - * (`{thumbprint}.pfx`). For a stronger cert identifier, use - * `cert.thumbprint` (SHA-256). - */ - thumbprint: string; -} - -/** - * Algorithm choices for `generateCertificate`. Defaults to RSA-2048 to match - * the historical ASP.NET dev cert format, but the same code path supports - * ECDSA P-256/P-384/P-521 and Ed25519 for user-managed flows. - */ -export type GenerateAlgorithm = - | { kind: "rsa"; modulusLength?: number } - | { kind: "ec"; namedCurve: "P-256" | "P-384" | "P-521" } - | { kind: "ed25519" } - | { kind: "ed448" }; - -/** - * Generate a self-signed certificate matching the ASP.NET Core HTTPS dev - * cert format (subject, validity, SANs, custom OID, SKI/AKI). - * - * The default algorithm is RSA-2048 with SHA-256 signing — a byte-for-byte - * compatible replacement for the previous `node-forge` path. Pass an - * `algorithm` to opt into ECDSA / Ed25519 (used by user-managed cert flows - * that need to round-trip non-RSA keys). - */ -export async function generateCertificate( - notBefore: Date, - notAfter: Date, - algorithm: GenerateAlgorithm = { kind: "rsa" } -): Promise { - ensureCryptoProvider(); - - const { keyPair, signingAlgorithm } = await generateKeyPair(algorithm); - - const serialNumber = generateSerialNumber(); - const subject = "CN=localhost"; - const issuer = subject; - - const sanEntries = [ - ...SAN_DNS_NAMES.map( - (dns) => ({ type: "dns" as const, value: dns }) - ), - ...SAN_IP_ADDRESSES.map( - (ip) => ({ type: "ip" as const, value: ip }) - ), - ]; - - const extensions: Extension[] = [ - new BasicConstraintsExtension(false, undefined, true), - new KeyUsagesExtension( - KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, - true - ), - new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth], true), - new SubjectAlternativeNameExtension(sanEntries, true), - new Extension( - ASPNET_HTTPS_OID, - false, - new Uint8Array([CURRENT_CERTIFICATE_VERSION]).buffer - ), - await SubjectKeyIdentifierExtension.create(keyPair.publicKey), - await AuthorityKeyIdentifierExtension.create(keyPair.publicKey), - ]; - - const cert = await X509CertificateGenerator.create({ - serialNumber, - subject, - issuer, - notBefore, - notAfter, - signingAlgorithm, - publicKey: keyPair.publicKey, - signingKey: keyPair.privateKey, - extensions, - }); - - const devCert = new DevCert(cert); - const devKey = await DevKey.fromCryptoKey(keyPair.privateKey); - - return { - cert: devCert, - key: devKey, - thumbprint: devCert.thumbprintSha1, - }; -} - -async function generateKeyPair( - algorithm: GenerateAlgorithm -): Promise<{ - keyPair: CryptoKeyPair; - signingAlgorithm: Algorithm | RsaHashedKeyGenParams | EcdsaParams; -}> { - const subtle = webcrypto.subtle; - - if (algorithm.kind === "rsa") { - const modulusLength = algorithm.modulusLength ?? RSA_KEY_SIZE; - const params: RsaHashedKeyGenParams = { - name: "RSASSA-PKCS1-v1_5", - modulusLength, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }; - const keyPair = (await subtle.generateKey(params, true, [ - "sign", - "verify", - ])); - return { - keyPair, - signingAlgorithm: { - name: "RSASSA-PKCS1-v1_5", - hash: "SHA-256", - } as RsaHashedKeyGenParams, - }; - } - - if (algorithm.kind === "ec") { - const params: EcKeyGenParams = { - name: "ECDSA", - namedCurve: algorithm.namedCurve, - }; - const keyPair = (await subtle.generateKey(params, true, [ - "sign", - "verify", - ])); - return { - keyPair, - signingAlgorithm: { - name: "ECDSA", - hash: defaultEcHash(algorithm.namedCurve), - } as EcdsaParams, - }; - } - - if (algorithm.kind === "ed25519") { - const keyPair = (await subtle.generateKey( - "Ed25519", - true, - ["sign", "verify"] - )) as CryptoKeyPair; - return { - keyPair, - signingAlgorithm: { name: "Ed25519" }, - }; - } - - if (algorithm.kind === "ed448") { - const keyPair = (await subtle.generateKey( - { name: "Ed448" }, - true, - ["sign", "verify"] - )) as CryptoKeyPair; - return { - keyPair, - signingAlgorithm: { name: "Ed448" }, - }; - } - - throw new Error( - `Unsupported algorithm: ${(algorithm as { kind: string }).kind}` - ); -} - -function defaultEcHash(curve: string): string { - switch (curve) { - case "P-256": - return "SHA-256"; - case "P-384": - return "SHA-384"; - case "P-521": - return "SHA-512"; - default: - return "SHA-256"; - } -} - -function generateSerialNumber(): string { - const maxAttempts = 5; - for (let attempt = 0; attempt < maxAttempts; attempt++) { - const bytes = randomBytes(16); - bytes[0] &= 0x7f; // ensure non-negative - if (bytes.some((value) => value !== 0)) { - return bytes.toString("hex"); - } - } - throw new Error("Failed to generate a non-zero certificate serial number."); -} +export type { + GenerateAlgorithm, + GeneratedCert, +} from "@devcontainer-dev-certs/shared"; From 849fe7d2ede0cc9cae3576606622ca25f61c80b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 21:32:08 +0000 Subject: [PATCH 05/41] Move the platform trust-store layer into the shared package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1b of carving the host-side cert library out of the UI extension. Moves `CertManager`, `BaseCertificateStore`, the per-OS stores (Linux / Mac / Windows), `nssTrust`, and `processUtil` from `src/vscode-ui-extension/src/{cert,platform}/` into `src/shared/src/{cert,platform}/`. The UI extension keeps thin re-export shims so existing import paths across the extension and its test suite resolve without a rename. `baseStore` / `macStore` / `windowsStore` previously imported `vscode.l10n.t` directly for skip-log and multi-candidate-warning strings — that's the only thing keeping the platform layer pinned to the extension host. To break it, the move introduces a `Localizer` callback (signature-compatible with `vscode.l10n.t`) plumbed through `BaseStoreOptions` / `CertManagerOptions`. The extension wires up `vscode.l10n.t` at the `CertManager` construction site in `extension.ts`; non-VS-Code consumers (a future host CLI, scripts) get an identity localizer that performs the same `{0}` placeholder substitution `vscode.l10n.t` produces in the no-translation-loaded case, so log output stays identical. Test mocks for `runProcess` / `trustInNss` / `createPlatformStore` previously targeted the extension's local `../src/platform/*` paths. After the move the implementations import from the shared internal modules; the mock targets have to follow the import the implementation actually uses, so the affected tests now mock the `@devcontainer-dev-certs/shared/src/platform/*` subpaths. No production-code behavior change: 207 UI tests, 79 workspace tests, type-check, esbuild bundle, and full-repo lint all pass. --- src/shared/src/cert/manager.ts | 217 +++++++++ src/shared/src/index.ts | 41 ++ src/shared/src/localizer.ts | 27 ++ src/shared/src/platform/baseStore.ts | 359 +++++++++++++++ src/shared/src/platform/linuxStore.ts | 274 +++++++++++ src/shared/src/platform/macStore.ts | 323 +++++++++++++ src/shared/src/platform/nssTrust.ts | 264 +++++++++++ src/shared/src/platform/processUtil.ts | 39 ++ src/shared/src/platform/types.ts | 127 ++++++ src/shared/src/platform/windowsStore.ts | 421 +++++++++++++++++ src/vscode-ui-extension/src/cert/manager.ts | 210 +-------- src/vscode-ui-extension/src/extension.ts | 1 + .../src/platform/baseStore.ts | 332 +------------- .../src/platform/linuxStore.ts | 278 +----------- .../src/platform/macStore.ts | 331 +------------- .../src/platform/nssTrust.ts | 267 +---------- .../src/platform/processUtil.ts | 42 +- src/vscode-ui-extension/src/platform/types.ts | 124 +---- .../src/platform/windowsStore.ts | 426 +----------------- .../tests/linuxStore.test.ts | 21 +- .../tests/macStore.test.ts | 6 +- src/vscode-ui-extension/tests/manager.test.ts | 11 +- .../tests/nssTrust.test.ts | 9 +- .../tests/windowsStore.test.ts | 8 +- 24 files changed, 2178 insertions(+), 1980 deletions(-) create mode 100644 src/shared/src/cert/manager.ts create mode 100644 src/shared/src/localizer.ts create mode 100644 src/shared/src/platform/baseStore.ts create mode 100644 src/shared/src/platform/linuxStore.ts create mode 100644 src/shared/src/platform/macStore.ts create mode 100644 src/shared/src/platform/nssTrust.ts create mode 100644 src/shared/src/platform/processUtil.ts create mode 100644 src/shared/src/platform/types.ts create mode 100644 src/shared/src/platform/windowsStore.ts diff --git a/src/shared/src/cert/manager.ts b/src/shared/src/cert/manager.ts new file mode 100644 index 0000000..1a428b9 --- /dev/null +++ b/src/shared/src/cert/manager.ts @@ -0,0 +1,217 @@ +import { generateCertificate, type GeneratedCert } from "./generator"; +import { exportPfx, exportPem, exportRootPfx } from "./exporter"; +import { VALIDITY_DAYS } from "./properties"; +import { + type PlatformCertificateStore, + type CertificateStatus, + type LinuxNssTrustReporter, + createPlatformStore, +} from "../platform/types"; +import { log } from "../logger"; +import { type Localizer } from "../localizer"; + +export interface CertManagerOptions { + /** + * Optional reporter invoked by the Linux platform store after attempting + * browser-NSS trust as part of `trustCertificate`. No-op on other + * platforms. + */ + linuxNssTrustReporter?: LinuxNssTrustReporter; + + /** + * Optional Localizer threaded into the platform store so log lines emitted + * inside `findExistingDevCert` (multi-candidate selection, forced skips) + * pass through the host's `vscode.l10n.t`. Non-VS-Code consumers (CLI, + * scripts) can omit it and fall back to the identity localizer. + */ + localize?: Localizer; +} + +/** + * Certificate manager that orchestrates generation, trust, export, and status + * checking using platform-specific stores. + */ +export class CertManager { + private store: PlatformCertificateStore | null = null; + private currentCert: GeneratedCert | null = null; + + constructor(private readonly options: CertManagerOptions = {}) {} + + private async getStore(): Promise { + this.store ??= await createPlatformStore({ + linuxNssTrustReporter: this.options.linuxNssTrustReporter, + localize: this.options.localize, + }); + return this.store; + } + + /** + * Generate a new dev cert and save it to the platform store. + * If force is true, removes existing certs first. + */ + async generate(force: boolean = false): Promise { + const store = await this.getStore(); + + if (force) { + log("Removing existing certificates..."); + await store.removeCertificates(); + } + + log("Generating new dev certificate..."); + const now = new Date(); + const expiry = new Date( + now.getTime() + VALIDITY_DAYS * 24 * 60 * 60 * 1000 + ); + const generated = await generateCertificate(now, expiry); + this.currentCert = generated; + + log(`Certificate generated. Thumbprint: ${generated.thumbprint}`); + await store.saveCertificate( + generated.cert, + generated.key, + generated.thumbprint + ); + log("Certificate saved to platform store."); + } + + /** + * Trust an externally-supplied certificate (e.g. one pushed from a Dev + * Container via the syncContainerCert reverse-sync flow) in the host + * OS trust store. + * + * Delegates directly to `store.trustCertificate(cert)` — the SAME hook + * the host-generation flow (`trust()`) uses on its final step — so + * "trusted on the host" means the same thing regardless of whether + * the cert was generated here or accepted from a container. On Linux + * that's `.NET Root store + OpenSSL trust dir + NSS browser DBs` (the + * NSS step uses the same `linuxNssTrustReporter` callback the host + * generation flow wires up). On macOS, login keychain trust policy. + * On Windows, CurrentUser/Root via certutil. + * + * Public-cert-only: the cert lands in every trust surface listed + * above but NEVER in CurrentUser/My, the keychain's identity slot, or + * the .NET store's `my/` directory. Skipping `saveCertificate` is + * deliberate — the host doesn't need (and shouldn't store) the + * private key, because Kestrel runs in the container with its own + * copy of the key. Future changes to this method MUST preserve both + * properties: (a) trust goes through `store.trustCertificate`; (b) + * no `saveCertificate` call. `tests/manager.test.ts` pins both. + * + * Does NOT update `currentCert`. The host's auto-generation flow + * (`generate()` / `trust()`) is a separate state machine that the + * container-push path doesn't feed into; if the user also has + * `generateDotNetCert: true` and a subsequent `getAllCertMaterial` + * pull arrives, the host will generate its own (separate) cert as + * normal. + */ + async trustExternalCertificate( + cert: GeneratedCert["cert"] + ): Promise { + const store = await this.getStore(); + + // Verify on-disk state before invoking the platform trust step. + // Skipping a redundant call matters on macOS where + // `security add-trusted-cert` is not a no-op for an already-trusted + // cert (re-touches the trust-settings record, may re-prompt for + // the keychain password). The same cache-as-goal-state / + // verify-on-disk pattern is used by the host-generation flow's + // `trust()` method, just expressed differently because it goes + // through `checkStatus()` instead of a direct `isCertTrusted`. + if (await store.isCertTrusted(cert)) { + log( + `Externally-supplied dev certificate ${cert.thumbprintSha1} is already trusted on host; skipping platform trust call.` + ); + return; + } + + log( + `Trusting externally-supplied dev certificate ${cert.thumbprintSha1} (public cert only, via the same platform trust path as host-generated)...` + ); + await store.trustCertificate(cert); + log("Externally-supplied certificate trusted."); + } + + /** + * Ensure a cert exists and is trusted. Generates one if needed. + */ + async trust(): Promise { + const store = await this.getStore(); + const status = await store.checkStatus(); + + if (!status.exists) { + await this.generate(); + } + + // Re-check: load from store if we didn't just generate + if (!this.currentCert) { + const found = await store.findExistingDevCert(); + if (!found) { + throw new Error("Failed to find certificate after generation."); + } + this.currentCert = found; + } + + const recheck = await store.checkStatus(); + if (!recheck.isTrusted) { + log("Trusting certificate in OS store..."); + await store.trustCertificate(this.currentCert.cert); + log("Certificate trusted."); + } + } + + /** + * Export the current cert in the specified format. + */ + async exportCert( + format: "pfx" | "pem" | "root-pfx", + outputDir: string, + password?: string + ): Promise { + await this.ensureLoaded(); + + if (format === "pfx") { + await exportPfx( + this.currentCert!.cert, + this.currentCert!.key, + outputDir, + password + ); + } else if (format === "root-pfx") { + await exportRootPfx(this.currentCert!.cert, outputDir); + } else { + exportPem(this.currentCert!.cert, this.currentCert!.key, outputDir); + } + } + + /** + * Check the status of the dev certificate. + */ + async check(): Promise { + const store = await this.getStore(); + return store.checkStatus(); + } + + /** + * Remove all dev certificates from the platform store. + */ + async clean(): Promise { + const store = await this.getStore(); + await store.removeCertificates(); + this.currentCert = null; + log("All dev certificates removed."); + } + + /** + * Ensure we have a loaded cert (from store or freshly generated). + */ + private async ensureLoaded(): Promise { + if (this.currentCert) return; + + const store = await this.getStore(); + const found = await store.findExistingDevCert(); + if (!found) { + throw new Error("No dev certificate found. Generate one first."); + } + this.currentCert = found; + } +} diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index a6f9406..ed053c6 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -1,4 +1,6 @@ export { initLogger, log, revealLogger } from "./logger"; +export { identityLocalizer } from "./localizer"; +export type { Localizer } from "./localizer"; export type { CertMaterial, CertKind, @@ -80,3 +82,42 @@ export { certToDer, } from "./cert/exporter"; export type { ExportedLoadedCert } from "./cert/exporter"; +export { CertManager } from "./cert/manager"; +export type { CertManagerOptions } from "./cert/manager"; + +// Platform trust-store layer — orchestrates per-OS dev-cert storage and +// trust. Lives in shared so a future host CLI can share the implementation +// with the VS Code extension. +export { + createPlatformStore, +} from "./platform/types"; +export type { + PlatformCertificateStore, + CertificateStatus, + CreatePlatformStoreOptions, + BaseStoreOptions, + LinuxNssTrustReporter, +} from "./platform/types"; +export { + BaseCertificateStore, + classifyCandidate as classifyPlatformCandidate, + selectBestDevCert as selectBestPlatformDevCert, +} from "./platform/baseStore"; +export type { ClassifyOptions as PlatformClassifyOptions } from "./platform/baseStore"; +export { LinuxCertificateStore } from "./platform/linuxStore"; +export type { LinuxCertificateStoreOptions } from "./platform/linuxStore"; +export { MacCertificateStore } from "./platform/macStore"; +export { + WindowsCertificateStore, +} from "./platform/windowsStore"; +export type { + WindowsStoreLocation, + PsCandidate, + PsSkipped, + PsSkipReason, + PsEnumeration, +} from "./platform/windowsStore"; +export { trustInNss } from "./platform/nssTrust"; +export type { NssTrustResult } from "./platform/nssTrust"; +export { runProcess } from "./platform/processUtil"; +export type { ProcessResult } from "./platform/processUtil"; diff --git a/src/shared/src/localizer.ts b/src/shared/src/localizer.ts new file mode 100644 index 0000000..98e8fa2 --- /dev/null +++ b/src/shared/src/localizer.ts @@ -0,0 +1,27 @@ +/** Argument type accepted by `Localizer` — matches `vscode.l10n.t`'s. */ +export type LocalizerArg = string | number | boolean; + +/** + * A function that resolves a template string + arguments into a localized + * string. Signature-compatible with `vscode.l10n.t` so the host extension can + * pass it through verbatim; non-VS-Code consumers (CLI, scripts, tests) use + * the identity implementation below to keep the English source strings. + */ +export type Localizer = (template: string, ...args: LocalizerArg[]) => string; + +/** + * Default Localizer used when no host-supplied l10n is wired up. Performs + * `{0}` placeholder substitution against `args` so the formatted output + * matches what `vscode.l10n.t` produces in the no-translation-loaded case + * — that's also the behavior the test l10n mock relies on, so platform + * stores constructed without a localizer still emit the same log strings. + */ +export const identityLocalizer: Localizer = (template, ...args) => + template.replace(/\{(\w+)\}/g, (_match, key: string) => { + const idx = Number(key); + if (!Number.isNaN(idx)) { + const value = args[idx]; + return value === undefined ? "" : String(value); + } + return ""; + }); diff --git a/src/shared/src/platform/baseStore.ts b/src/shared/src/platform/baseStore.ts new file mode 100644 index 0000000..4cea1de --- /dev/null +++ b/src/shared/src/platform/baseStore.ts @@ -0,0 +1,359 @@ +import * as fs from "fs"; +import * as path from "path"; +import { type PlatformCertificateStore, type CertificateStatus, type BaseStoreOptions } from "./types"; +import { identityLocalizer, type Localizer } from "../localizer"; +import { log } from "../logger"; +import { type DevCert, type DevKey } from "../cert/types"; +import { buildPfx, parsePfx } from "../cert/pfx"; +import { getCertificateVersion } from "../cert/validation"; +import { + classifyCandidate as classifyCandidateShared, + selectBestDevCert as selectBestDevCertShared, + extractThumbprintHintFromFilename, + type CandidateInput, + type ClassifiedCandidate, + type SkipReport, + type UsableDevCert, +} from "../cert/classify"; + +// Re-export the pure shared types so existing imports of +// `./platform/baseStore` keep working without a sweeping rename. +export type { + ClassifiedCandidate, + CandidateInput, + CandidateMetadata, + UsableDevCert, +} from "../cert/classify"; +export { extractThumbprintHintFromFilename } from "../cert/classify"; + +export interface ClassifyOptions { + /** Optional Localizer; defaults to `identityLocalizer`. */ + localize?: Localizer; +} + +/** + * Side-effectful classifier wrapper. Delegates the pure classification to the + * shared module and emits the same "skipping ASP.NET dev cert ..." log line + * the host extension has always produced — now templated via an injected + * Localizer so non-VS-Code consumers (CLI / scripts / tests) can reuse the + * exact same skip-log surface without taking on a `vscode` dependency. + */ +export function classifyCandidate( + input: CandidateInput, + options: ClassifyOptions = {} +): ClassifiedCandidate | null { + const localize = options.localize ?? identityLocalizer; + return classifyCandidateShared(input, { + onSkipped: (report) => emitSkipLog(report, localize), + }); +} + +/** + * Side-effectful selection wrapper. Delegates to the shared selector and + * emits the multi-candidate warning when more than one usable candidate is + * present, templated via the injected Localizer. + */ +export function selectBestDevCert( + usable: UsableDevCert[], + context: string, + options: ClassifyOptions = {} +): UsableDevCert | null { + const localize = options.localize ?? identityLocalizer; + return selectBestDevCertShared(usable, context, { + onMultipleCandidates: ({ selected, candidates }) => { + const header = localize( + "Multiple valid ASP.NET dev certs found in {0}; selected {1}.", + context, + selected.thumbprint + ); + const candidatesHeader = localize(" Candidates:"); + const selectedTag = localize("[selected]"); + const skippedTag = localize("[skipped] "); + const lines = [ + header, + candidatesHeader, + ...candidates.map((c, i) => + localize( + " {0} thumbprint={1} version={2} notBefore={3} notAfter={4}", + i === 0 ? selectedTag : skippedTag, + c.thumbprint, + getCertificateVersion(c.cert), + c.cert.notBefore.toISOString(), + c.cert.notAfter.toISOString() + ) + ), + ]; + log(lines.join("\n")); + }, + }); +} + +/** + * Render the localized "skipping ASP.NET dev cert" log line for one skipped + * candidate. Maps the shared classifier's reason code to a localized string. + * `forced` skips carry the caller's free-form reason verbatim — the platform + * stores (linuxStore / macStore / windowsStore) localize their own forced- + * skip reasons before handing them in, so we pass through. + */ +function emitSkipLog(report: SkipReport, localize: Localizer): void { + let localizedReason: string; + switch (report.reasonCode) { + case "missing-private-key": + localizedReason = localize( + "PFX contains certificate without matching private key" + ); + break; + case "parse-failed": + localizedReason = localize( + "failed to parse PFX (corrupt or wrong password)" + ); + break; + case "forced": + // Caller localized this string before classifyCandidate received it. + localizedReason = report.forcedReason ?? ""; + break; + } + const unknown = localize("(unknown)"); + const meta = report.metadata; + const subjectCN = meta.subjectCN ?? unknown; + const version = + meta.version === undefined || meta.version === null + ? unknown + : String(meta.version); + const notBefore = meta.notBefore ? meta.notBefore.toISOString() : unknown; + const notAfter = meta.notAfter ? meta.notAfter.toISOString() : unknown; + log( + localize( + "Skipping ASP.NET dev cert {0} ({1}): {2}.\n subjectCN={3} version={4} notBefore={5} notAfter={6}", + meta.thumbprint ?? unknown, + report.source, + localizedReason, + subjectCN, + version, + notBefore, + notAfter + ) + ); +} + +/** + * Base implementation for platform certificate stores. + * + * Provides common logic shared across Windows, macOS, and Linux: + * - checkStatus() with a consistent pattern (find → check trust → build status) + * - PFX loading and writing helpers + * - The localized classifier / selector wrappers above, threaded through + * `this.localize` so subclasses don't repeat the plumbing. + * + * Subclasses implement the platform-specific methods: findExistingDevCert, + * saveCertificate, trustCertificate, removeCertificates, and isTrusted. + */ +export abstract class BaseCertificateStore implements PlatformCertificateStore { + protected readonly localize: Localizer; + + constructor(options: BaseStoreOptions = {}) { + this.localize = options.localize ?? identityLocalizer; + } + + async checkStatus(): Promise { + const found = await this.findExistingDevCert(); + if (!found) { + return { + exists: false, + isTrusted: false, + thumbprint: null, + notBefore: null, + notAfter: null, + version: -1, + }; + } + + const { cert, thumbprint } = found; + const trusted = await this.isTrusted(cert, thumbprint); + const version = getCertificateVersion(cert); + + return { + exists: true, + isTrusted: trusted, + thumbprint, + notBefore: cert.notBefore.toISOString(), + notAfter: cert.notAfter.toISOString(), + version, + }; + } + + abstract findExistingDevCert(): Promise; + + abstract saveCertificate( + cert: DevCert, + key: DevKey, + thumbprint: string + ): Promise; + + abstract trustCertificate(cert: DevCert): Promise; + + abstract removeCertificates(): Promise; + + /** + * Public wrapper around `isTrusted` that satisfies the + * `PlatformCertificateStore.isCertTrusted` contract — verify the + * current on-disk / OS trust state for a specific certificate. Lets + * callers (notably `CertManager.trustExternalCertificate`) decide + * whether the platform-level trust step needs to run at all, + * avoiding redundant `security add-trusted-cert` / `certutil + * -addstore` calls that aren't true no-ops. + */ + async isCertTrusted(cert: DevCert): Promise { + return this.isTrusted(cert, cert.thumbprintSha1); + } + + /** + * Platform-specific trust verification. + * Called by checkStatus() to determine if the certificate is trusted. + */ + protected abstract isTrusted( + cert: DevCert, + thumbprint: string + ): Promise; + + // --- Shared helpers --- + + /** + * Parse a PFX file and extract the certificate, private key, and thumbprint. + * Returns `{ cert, key }` where `key` may be null when the PFX is cert-only. + * Returns null only on outright parse failure (corrupt bytes, wrong + * password, unsupported PBE). + */ + protected async loadPfxLenient( + pfxPath: string, + password: string = "" + ): Promise<{ cert: DevCert; key: DevKey | null; thumbprint: string } | null> { + try { + const pfxBytes = fs.readFileSync(pfxPath); + const { cert, key } = await parsePfx(pfxBytes, password); + return { cert, key: key ?? null, thumbprint: cert.thumbprintSha1 }; + } catch { + return null; + } + } + + /** + * Parse a PFX file and extract the certificate, private key, and thumbprint. + * Returns null if the file cannot be parsed or is missing cert/key bags. + * Strict variant — used by existing call sites that want a usable identity + * or nothing. + */ + protected async loadPfx( + pfxPath: string, + password: string = "" + ): Promise { + const loaded = await this.loadPfxLenient(pfxPath, password); + if (!loaded || !loaded.key) return null; + return { cert: loaded.cert, key: loaded.key, thumbprint: loaded.thumbprint }; + } + + /** + * Write a certificate and key as a PFX file. + */ + protected async writePfx( + cert: DevCert, + key: DevKey, + pfxPath: string, + password: string = "", + mode?: number + ): Promise { + const der = await buildPfx({ cert, key, password }); + const options = mode !== undefined ? { mode } : undefined; + fs.writeFileSync(pfxPath, der, options); + } + + /** + * Subclass-facing convenience: classify one candidate using this store's + * Localizer, so subclasses don't have to thread it through manually. + */ + protected classify(input: CandidateInput): ClassifiedCandidate | null { + return classifyCandidate(input, { localize: this.localize }); + } + + /** + * Subclass-facing convenience: select the best candidate using this + * store's Localizer. + */ + protected selectBest( + usable: UsableDevCert[], + context: string + ): UsableDevCert | null { + return selectBestDevCert(usable, context, { localize: this.localize }); + } + + /** + * Scan a directory for `*.pfx` files and classify each one. For every + * match: + * + * 1. Try to parse it. Parse failures on canonically-named files + * (`aspnetcore-localhost-.pfx` or `.pfx`) emit the + * "failed to parse PFX" unusable warning; parse failures on other + * filenames are silent — they may belong to other tools. + * 2. If parsed and the cert passes `isValidDevCert` but there's no private + * key, emit the "no matching private key" unusable warning. + * 3. Otherwise classify as `usable`. + * + * Selection runs only over the surviving usable candidates via + * `selectBestDevCert`. Multi-candidate selection emits its own warning. + * + * Callers may narrow which files to consider via `filenamePredicate` + * (default: accept every `*.pfx`). Whether a parse failure produces a + * warning is controlled separately by `extractThumbprintHintFromFilename` + * — see step 1. + */ + protected async findBestDevCertInDir( + dir: string, + password: string = "", + options: { + filenamePredicate?: (filename: string) => boolean; + context?: string; + } = {} + ): Promise { + if (!fs.existsSync(dir)) return null; + + const context = options.context ?? dir; + const usable: UsableDevCert[] = []; + + const files = fs + .readdirSync(dir) + .filter((f) => f.endsWith(".pfx")) + .filter((f) => + options.filenamePredicate ? options.filenamePredicate(f) : true + ); + + for (const file of files) { + const filePath = path.join(dir, file); + const thumbprintHint = extractThumbprintHintFromFilename(file); + const loaded = await this.loadPfxLenient(filePath, password); + + if (!loaded) { + // Canonical name + parse failure → warn; otherwise silent. + this.classify({ + kind: "parseFailure", + source: filePath, + thumbprintHint, + }); + continue; + } + + const classified = this.classify({ + kind: "loaded", + source: filePath, + loaded, + }); + + if (classified === null) continue; + if (classified.kind === "usable") { + usable.push(classified); + } + // skipped → already logged inside this.classify + } + + return this.selectBest(usable, context); + } +} diff --git a/src/shared/src/platform/linuxStore.ts b/src/shared/src/platform/linuxStore.ts new file mode 100644 index 0000000..704f811 --- /dev/null +++ b/src/shared/src/platform/linuxStore.ts @@ -0,0 +1,274 @@ +import * as fs from "fs"; +import * as path from "path"; +import { BaseCertificateStore } from "./baseStore"; +import { trustInNss, type NssTrustResult } from "./nssTrust"; +import { runProcess } from "./processUtil"; +import { type LinuxNssTrustReporter, type BaseStoreOptions } from "./types"; +import { type DevCert, type DevKey } from "../cert/types"; +import { ASPNET_HTTPS_OID } from "../cert/properties"; +import { buildPfx } from "../cert/pfx"; +import { + getDotNetStorePath, + getDotNetRootStorePath, + getOpenSslTrustDir, + getPemFileName, +} from "../paths"; +import { log } from "../logger"; + +export interface LinuxCertificateStoreOptions extends BaseStoreOptions { + /** + * Optional callback invoked once with the result of the best-effort + * browser-NSS trust step inside `trustCertificate`. Omitting it disables + * the NSS step entirely (used by integration tests and CLI contexts + * where browser trust isn't relevant). + */ + nssTrustReporter?: LinuxNssTrustReporter; +} + +/** + * Linux certificate store implementation. + * + * Storage locations: + * - .NET X509Store path: ~/.dotnet/corefx/cryptography/x509stores/my/ + * - OpenSSL trust dir: ~/.aspnet/dev-certs/trust/ (or DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY) + * + * Trust is established by: + * 1. Writing a PFX to the .NET Root store path (for .NET runtime validation) + * 2. Writing a PEM to the OpenSSL trust directory with hash symlinks (for OpenSSL/curl/etc.) + * 3. Best-effort import into Linux browser NSS databases, when a reporter + * is configured. + */ +export class LinuxCertificateStore extends BaseCertificateStore { + private readonly nssTrustReporter?: LinuxNssTrustReporter; + + constructor(options: LinuxCertificateStoreOptions = {}) { + super(options); + this.nssTrustReporter = options.nssTrustReporter; + } + + private get dotNetRootStorePath(): string { + return getDotNetRootStorePath(); + } + + async findExistingDevCert(): Promise<{ + cert: DevCert; + key: DevKey; + thumbprint: string; + } | null> { + return this.findBestDevCertInDir(getDotNetStorePath()); + } + + async saveCertificate( + cert: DevCert, + key: DevKey, + thumbprint: string + ): Promise { + const storeDir = getDotNetStorePath(); + fs.mkdirSync(storeDir, { recursive: true }); + await this.writePfx( + cert, + key, + path.join(storeDir, `${thumbprint}.pfx`), + "", + 0o600 + ); + } + + async trustCertificate(cert: DevCert): Promise { + await this.trustInDotNetRootStore(cert); + await this.trustViaOpenSsl(cert); + await this.trustInNssBrowsers(cert); + } + + /** + * Best-effort browser NSS trust. Mirrors the Windows / macOS pattern where + * `trustCertificate` performs every trust step the platform supports out + * of the box — on Linux that includes adding the cert to the user's NSS + * databases for Firefox / Chromium-family browsers. Never throws: NSS + * tooling or stores are commonly absent and shouldn't break .NET / OpenSSL + * trust. Results are surfaced via the reporter callback (typically used by + * the extension host to show a manual-guidance toast on failure). + */ + private async trustInNssBrowsers(cert: DevCert): Promise { + if (!this.nssTrustReporter) return; + + const pemPath = path.join( + getOpenSslTrustDir(), + getPemFileName(cert.thumbprintSha1) + ); + if (!fs.existsSync(pemPath)) { + log(`Linux NSS trust: PEM not found at ${pemPath}, skipping.`); + return; + } + + let result: NssTrustResult; + try { + result = await trustInNss(pemPath); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + log(`Linux NSS trust threw unexpectedly: ${message}`); + result = { success: false, message }; + } + + this.nssTrustReporter(result, pemPath); + } + + async removeCertificates(): Promise { + await this.removeDevCertsFromDir(getDotNetStorePath()); + await this.removeDevCertsFromDir(this.dotNetRootStorePath); + + const trustDir = getOpenSslTrustDir(); + if (fs.existsSync(trustDir)) { + const entries = fs.readdirSync(trustDir); + for (const entry of entries) { + const fullPath = path.join(trustDir, entry); + if (entry.startsWith("aspnetcore-localhost-")) { + fs.unlinkSync(fullPath); + } else if (isHashSymlink(entry)) { + try { + if (fs.lstatSync(fullPath).isSymbolicLink()) { + fs.unlinkSync(fullPath); + } + } catch { + // ignore + } + } + } + } + } + + protected isTrusted( + _cert: DevCert, + thumbprint: string + ): Promise { + const pemPath = path.join(getOpenSslTrustDir(), getPemFileName(thumbprint)); + return Promise.resolve(fs.existsSync(pemPath)); + } + + // --- Linux-specific trust helpers --- + + private async trustInDotNetRootStore(cert: DevCert): Promise { + fs.mkdirSync(this.dotNetRootStorePath, { recursive: true }); + + const thumbprint = cert.thumbprintSha1; + const certPath = path.join(this.dotNetRootStorePath, `${thumbprint}.pfx`); + + // .NET's X509Store on Linux stores certs as individual PFX files. + // For the Root store, we need a PFX containing only the public cert (no private key). + const pfxBytes = await buildPfx({ cert }); + fs.writeFileSync(certPath, pfxBytes, { mode: 0o644 }); + } + + private async trustViaOpenSsl(cert: DevCert): Promise { + const trustDir = getOpenSslTrustDir(); + fs.mkdirSync(trustDir, { recursive: true }); + + const thumbprint = cert.thumbprintSha1; + const pemFileName = getPemFileName(thumbprint); + const pemPath = path.join(trustDir, pemFileName); + + // Purely additive: write the new PEM, but do NOT delete other + // `aspnetcore-localhost-*.pem` files in the trust dir. The previous + // implementation swept "old rotations" defensively, but that turned + // every `trustCertificate` call into an implicit revocation of every + // other dev cert in the trust dir — including the host-generated + // cert when the container-push reverse-sync flow trusts an + // additional one, and vice versa. Removing trust from a cert the + // user (or another flow) explicitly trusted is the cleanup + // command's job — gated by an explicit user prompt — not + // trustCertificate's. The cleanup sweep continues to handle + // legitimately stale artifacts; this method stays additive so the + // generation flow and the reverse-sync flow can coexist without + // ping-ponging trust. + // + // Writing to the exact same path on a same-thumbprint repeat call + // is idempotent (overwrites identical content); rehashing afterward + // is a no-op when nothing changed. + fs.writeFileSync(pemPath, cert.pem, { mode: 0o644 }); + await this.rehashDirectory(trustDir); + } + + private async rehashDirectory(directory: string): Promise { + const entries = fs.readdirSync(directory); + + // Remove existing hash symlinks + for (const entry of entries) { + if (isHashSymlink(entry)) { + const fullPath = path.join(directory, entry); + try { + if (fs.lstatSync(fullPath).isSymbolicLink()) { + fs.unlinkSync(fullPath); + } + } catch { + // ignore + } + } + } + + // Create new hash symlinks for all PEM/CRT files + const certFiles = fs + .readdirSync(directory) + .filter((f) => /\.(pem|crt|cer)$/i.test(f)); + + for (const certFile of certFiles) { + const fullPath = path.join(directory, certFile); + try { + if (fs.lstatSync(fullPath).isSymbolicLink()) continue; + } catch { + continue; + } + + const hash = await this.getOpenSslSubjectHash(fullPath); + if (!hash) continue; + + // Slot 0-9 covers any realistic collision count. Catch EEXIST so a + // concurrent rehash can't crash this one mid-loop. + for (let i = 0; i < 10; i++) { + const linkPath = path.join(directory, `${hash}.${i}`); + if (fs.existsSync(linkPath)) continue; + try { + fs.symlinkSync(certFile, linkPath); + break; + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "EEXIST") continue; + throw err; + } + } + } + } + + private async getOpenSslSubjectHash( + certPath: string + ): Promise { + const result = await runProcess("openssl", [ + "x509", + "-hash", + "-noout", + "-in", + certPath, + ]); + if (result.exitCode !== 0) return null; + return result.stdout.trim() || null; + } + + private async removeDevCertsFromDir(dir: string): Promise { + if (!fs.existsSync(dir)) return; + + const files = fs.readdirSync(dir).filter((f) => f.endsWith(".pfx")); + for (const file of files) { + const pfxPath = path.join(dir, file); + try { + const result = await this.loadPfx(pfxPath); + if (result && result.cert.hasExtension(ASPNET_HTTPS_OID)) { + fs.unlinkSync(pfxPath); + } + } catch { + // Skip files that can't be parsed + } + } + } +} + +function isHashSymlink(filename: string): boolean { + return /^[0-9a-f]{8}\.\d+$/.test(filename); +} diff --git a/src/shared/src/platform/macStore.ts b/src/shared/src/platform/macStore.ts new file mode 100644 index 0000000..8698e03 --- /dev/null +++ b/src/shared/src/platform/macStore.ts @@ -0,0 +1,323 @@ +import { randomUUID } from "crypto"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { + BaseCertificateStore, + extractThumbprintHintFromFilename, + type UsableDevCert, +} from "./baseStore"; +import { runProcess } from "./processUtil"; +import { getCertificateVersion, isValidDevCert } from "../cert/validation"; +import { certToDer } from "../cert/exporter"; +import { ASPNET_HTTPS_OID } from "../cert/properties"; +import { DevCert, type DevKey } from "../cert/types"; + +/** + * macOS certificate store implementation. + * + * Storage locations: + * - Disk: ~/.aspnet/dev-certs/https/aspnetcore-localhost-{thumbprint}.pfx + * - Keychain: login keychain for trust validation + * + * Uses the `security` CLI for keychain trust operations. + */ +export class MacCertificateStore extends BaseCertificateStore { + private get devCertsDir(): string { + return path.join(os.homedir(), ".aspnet", "dev-certs", "https"); + } + + private get keychainPath(): string { + return path.join(os.homedir(), "Library", "Keychains", "login.keychain-db"); + } + + async findExistingDevCert(): Promise { + const context = "macOS login keychain"; + const usable: UsableDevCert[] = []; + const seenThumbprints = new Set(); + + // 1) Walk ~/.aspnet/dev-certs/https/. For each parseable PFX whose cert + // passes isValidDevCert, additionally verify a matching cert is + // actually present in the login keychain via `security find- + // certificate -Z ` (public-only, no prompt). PFXs whose cert + // isn't in the keychain are classified as "orphaned cache file" + // skipped entries and excluded from selection. + if (fs.existsSync(this.devCertsDir)) { + const pfxFiles = fs + .readdirSync(this.devCertsDir) + .filter( + (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + ); + + for (const pfxFile of pfxFiles) { + const pfxPath = path.join(this.devCertsDir, pfxFile); + const loaded = await this.loadPfxLenient(pfxPath); + + if (!loaded) { + this.classify({ + kind: "parseFailure", + source: pfxPath, + thumbprintHint: extractThumbprintHintFromFilename(pfxFile), + }); + continue; + } + + const classified = this.classify({ + kind: "loaded", + source: pfxPath, + loaded, + }); + if (classified === null) continue; + if (classified.kind !== "usable") { + seenThumbprints.add(loaded.thumbprint); + continue; + } + + // Verify keychain presence — no prompt, public-only. + const inKeychain = await this.isCertInKeychain(classified.thumbprint); + if (!inKeychain) { + this.classify({ + kind: "forcedSkip", + source: pfxPath, + reason: this.localize( + "PFX present on disk but matching certificate not in macOS login keychain (orphaned cache file)" + ), + metadata: { + thumbprint: classified.thumbprint, + subjectCN: classified.cert.subjectCN, + version: getCertificateVersion(classified.cert), + notBefore: classified.cert.notBefore, + notAfter: classified.cert.notAfter, + }, + }); + seenThumbprints.add(classified.thumbprint); + continue; + } + + seenThumbprints.add(classified.thumbprint); + usable.push(classified); + } + } + + // 2) Soft keychain enumeration — emit a warning for keychain-resident + // dev certs that lack a matching cache PFX. Public-only read, never + // triggers an ACL prompt, low EDR signal. + const keychainEntries = await this.enumerateKeychainDevCerts(); + for (const entry of keychainEntries) { + if (seenThumbprints.has(entry.thumbprint)) continue; + this.classify({ + kind: "forcedSkip", + source: context, + reason: this.localize( + "present in keychain but no matching PFX in {0}", + `${this.devCertsDir}/aspnetcore-localhost-${entry.thumbprint}.pfx` + ), + metadata: { + thumbprint: entry.thumbprint, + subjectCN: entry.cert.subjectCN, + version: getCertificateVersion(entry.cert), + notBefore: entry.cert.notBefore, + notAfter: entry.cert.notAfter, + }, + }); + } + + return this.selectBest(usable, this.devCertsDir); + } + + /** + * Returns true if a certificate with the given SHA-1 thumbprint is + * present in the login keychain. Uses `security find-certificate -Z` + * which only reads the public certificate — no ACL prompt is raised + * regardless of the cert's private-key ACL. + */ + private async isCertInKeychain(thumbprint: string): Promise { + const result = await runProcess("security", [ + "find-certificate", + "-Z", + thumbprint, + this.keychainPath, + ]); + return result.exitCode === 0; + } + + /** + * Enumerate ASP.NET dev cert candidates that exist in the login keychain. + * Returns parsed certs whose CN is `localhost`, that bear the ASP.NET + * custom OID, and whose validity window is current. Public-only, no + * prompts. + */ + private async enumerateKeychainDevCerts(): Promise< + Array<{ cert: DevCert; thumbprint: string }> + > { + const result = await runProcess("security", [ + "find-certificate", + "-a", + "-p", + "-Z", + this.keychainPath, + ]); + if (result.exitCode !== 0) return []; + + const out: Array<{ cert: DevCert; thumbprint: string }> = []; + const pemBlocks = extractPemBlocks(result.stdout); + for (const pem of pemBlocks) { + try { + const cert = new DevCert(pem); + if (cert.subjectCN !== "localhost") continue; + if (!cert.hasExtension(ASPNET_HTTPS_OID)) continue; + if (!isValidDevCert(cert)) continue; + out.push({ cert, thumbprint: cert.thumbprintSha1 }); + } catch { + // Skip lines that don't parse as a cert (the -a -p -Z output + // includes SHA-1 lines interleaved with PEM blocks). + } + } + return out; + } + + async saveCertificate( + cert: DevCert, + key: DevKey, + thumbprint: string + ): Promise { + fs.mkdirSync(this.devCertsDir, { recursive: true }); + const pfxPath = path.join( + this.devCertsDir, + `aspnetcore-localhost-${thumbprint}.pfx` + ); + // ~/.aspnet/dev-certs/https/*.pfx contains the private key; force 0o600 + // so it can't be read by other users on a multi-user mac. + await this.writePfx(cert, key, pfxPath, "", 0o600); + } + + async trustCertificate(cert: DevCert): Promise { + // /tmp is shared on macOS; an unguessable filename rules out symlink + // races on the temporary public-cert artifact. + const tmpCert = path.join(os.tmpdir(), `devcert-trust-${randomUUID()}.cer`); + fs.writeFileSync(tmpCert, certToDer(cert)); + + const result = await runProcess("security", [ + "add-trusted-cert", + "-p", + "basic", + "-p", + "ssl", + "-k", + this.keychainPath, + tmpCert, + ]); + + try { + fs.unlinkSync(tmpCert); + } catch { + /* ignore */ + } + + if (result.exitCode !== 0) { + throw new Error( + `Failed to trust certificate in keychain: ${result.stderr}` + ); + } + } + + async removeCertificates(): Promise { + // Collect SHA-1 thumbprints of every dev cert we have on disk so we + // can delete keychain entries by hash. Matching on `-c localhost` is + // too broad — the user may have unrelated `localhost` certs added + // for other tools and we don't want to nuke those. + const thumbprints = new Set(); + if (fs.existsSync(this.devCertsDir)) { + const pfxFiles = fs + .readdirSync(this.devCertsDir) + .filter( + (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + ); + for (const pfxFile of pfxFiles) { + try { + const result = await this.loadPfx(path.join(this.devCertsDir, pfxFile)); + if (result && result.cert.hasExtension(ASPNET_HTTPS_OID)) { + thumbprints.add(result.thumbprint); + } + } catch { + // Skip unparseable files; they're not ours to delete by hash. + } + } + } + + for (const thumbprint of thumbprints) { + // delete-certificate exits non-zero once there are no more entries + // matching the hash; loop with a generous bound to drain any + // duplicates left by past regenerations. + for (let i = 0; i < 100; i++) { + const result = await runProcess("security", [ + "delete-certificate", + "-Z", + thumbprint, + this.keychainPath, + ]); + if (result.exitCode !== 0) break; + } + } + + // Remove trust settings entries that pointed at any of those certs. + await runProcess("security", [ + "remove-trusted-cert", + "-d", + this.keychainPath, + ]); + + // Remove PFX files from disk + if (fs.existsSync(this.devCertsDir)) { + const pfxFiles = fs + .readdirSync(this.devCertsDir) + .filter( + (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + ); + for (const pfxFile of pfxFiles) { + fs.unlinkSync(path.join(this.devCertsDir, pfxFile)); + } + } + } + + protected async isTrusted( + cert: DevCert, + _thumbprint: string + ): Promise { + const tmpCert = path.join(os.tmpdir(), `devcert-verify-${randomUUID()}.cer`); + try { + fs.writeFileSync(tmpCert, certToDer(cert)); + + const result = await runProcess("security", [ + "verify-cert", + "-c", + tmpCert, + "-p", + "ssl", + ]); + + return result.exitCode === 0; + } finally { + try { + fs.unlinkSync(tmpCert); + } catch { + // best effort cleanup + } + } + } +} + +/** + * Pull PEM-encoded CERTIFICATE blocks out of `security find-certificate -p` + * output. The CLI interleaves SHA-1 hash lines (when `-Z` is passed) with + * the PEM blocks; we just grab the blocks. + */ +function extractPemBlocks(text: string): string[] { + const blocks: string[] = []; + const regex = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g; + let m: RegExpExecArray | null; + while ((m = regex.exec(text)) !== null) { + blocks.push(m[0]); + } + return blocks; +} diff --git a/src/shared/src/platform/nssTrust.ts b/src/shared/src/platform/nssTrust.ts new file mode 100644 index 0000000..d89b5f4 --- /dev/null +++ b/src/shared/src/platform/nssTrust.ts @@ -0,0 +1,264 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { runProcess } from "./processUtil"; +import { log } from "../logger"; + +export interface NssTrustResult { + success: boolean; + message: string; +} + +const CERT_NAME = "Dev Container Dev Cert"; + +type NssTargetKind = "chromium-shared" | "firefox-profiles"; + +interface NssTarget { + label: string; + kind: NssTargetKind; + root: string; +} + +interface DbOutcome { + label: string; + ok: boolean; + stderr?: string; +} + +/** + * Enumerate well-known NSS database roots on Linux. Covers native packages, + * Snap and Flatpak sandbox roots, and a handful of Firefox forks that keep + * their own profile directory. The list is ordered most-common-first; missing + * roots are silently skipped at scan time. + */ +function getNssTargets(home: string): NssTarget[] { + const chromium = (label: string, ...segs: string[]): NssTarget => ({ + label, + kind: "chromium-shared", + root: path.join(home, ...segs), + }); + const firefox = (label: string, ...segs: string[]): NssTarget => ({ + label, + kind: "firefox-profiles", + root: path.join(home, ...segs), + }); + + return [ + // Chromium-family browsers share a single user NSS DB. The native + // ~/.pki/nssdb path is the historical shared store used by Chromium, + // Chrome, Brave, Vivaldi, Edge, and Opera when installed as deb/rpm/AUR. + chromium("Chromium", ".pki", "nssdb"), + chromium("Chromium (Snap)", "snap", "chromium", "common", ".pki", "nssdb"), + chromium( + "Chromium (Flatpak)", + ".var", + "app", + "org.chromium.Chromium", + ".pki", + "nssdb" + ), + chromium( + "Chrome (Flatpak)", + ".var", + "app", + "com.google.Chrome", + ".pki", + "nssdb" + ), + chromium( + "Brave (Flatpak)", + ".var", + "app", + "com.brave.Browser", + ".pki", + "nssdb" + ), + chromium( + "Vivaldi (Flatpak)", + ".var", + "app", + "com.vivaldi.Vivaldi", + ".pki", + "nssdb" + ), + chromium( + "Edge (Flatpak)", + ".var", + "app", + "com.microsoft.Edge", + ".pki", + "nssdb" + ), + + // Firefox-family browsers keep an NSS DB per profile under a known root. + firefox("Firefox", ".mozilla", "firefox"), + firefox( + "Firefox (Snap)", + "snap", + "firefox", + "common", + ".mozilla", + "firefox" + ), + firefox( + "Firefox (Flatpak)", + ".var", + "app", + "org.mozilla.firefox", + ".mozilla", + "firefox" + ), + firefox("Firefox ESR", ".mozilla", "firefox-esr"), + firefox("LibreWolf", ".librewolf"), + firefox( + "LibreWolf (Flatpak)", + ".var", + "app", + "io.gitlab.librewolf-community", + ".librewolf" + ), + firefox("Waterfox", ".waterfox"), + firefox("Floorp", ".floorp"), + ]; +} + +/** + * Attempt to trust a PEM certificate in NSS databases used by Linux browsers. + * + * Requires `certutil` on the PATH (from libnss3-tools / nss-tools / nss). + * Enumerates well-known NSS database roots — native deb/rpm installs, Snap + * and Flatpak sandboxes, and Firefox forks — and adds the certificate to each + * existing database. Targets that aren't installed are skipped silently; the + * returned message lists only databases we actually touched. + */ +export async function trustInNss(pemPath: string): Promise { + const which = await runProcess("which", ["certutil"]); + if (which.exitCode !== 0) { + return { + success: false, + message: + "certutil is not installed. Install libnss3-tools (Debian/Ubuntu), nss-tools (Fedora/RHEL), or nss (Arch) to enable automatic browser trust.", + }; + } + + const outcomes: DbOutcome[] = []; + const targets = getNssTargets(os.homedir()); + + for (const target of targets) { + if (target.kind === "chromium-shared") { + await scanChromiumShared(target, pemPath, outcomes); + } else { + await scanFirefoxProfiles(target, pemPath, outcomes); + } + } + + if (outcomes.length === 0) { + return { + success: false, + message: + "No browser NSS databases found. Open Firefox or Chromium at least once to create a profile, then try again.", + }; + } + + const trusted = outcomes.filter((o) => o.ok).map((o) => o.label); + const failed = outcomes.filter((o) => !o.ok); + + const parts: string[] = []; + if (trusted.length > 0) parts.push(`Trusted in: ${trusted.join(", ")}`); + for (const f of failed) { + parts.push(`${f.label}: failed (${f.stderr?.trim() ?? "unknown error"})`); + } + + return { + success: failed.length === 0, + message: parts.join("; "), + }; +} + +async function scanChromiumShared( + target: NssTarget, + pemPath: string, + outcomes: DbOutcome[] +): Promise { + if (!fs.existsSync(path.join(target.root, "cert9.db"))) { + log(`NSS scan: ${target.label} not present at ${target.root}, skipping.`); + return; + } + const r = await trustInNssDb(`sql:${target.root}`, pemPath); + outcomes.push({ + label: target.label, + ok: r.exitCode === 0, + stderr: r.stderr, + }); +} + +async function scanFirefoxProfiles( + target: NssTarget, + pemPath: string, + outcomes: DbOutcome[] +): Promise { + if (!fs.existsSync(target.root)) { + log(`NSS scan: ${target.label} not present at ${target.root}, skipping.`); + return; + } + + let entries: string[]; + try { + entries = fs.readdirSync(target.root); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + log(`NSS scan: failed to enumerate ${target.label} profiles: ${message}`); + return; + } + + const profiles = entries.filter((d) => { + try { + return fs.existsSync(path.join(target.root, d, "cert9.db")); + } catch { + return false; + } + }); + + if (profiles.length === 0) { + log(`NSS scan: ${target.label} has no profiles with cert9.db, skipping.`); + return; + } + + for (const profile of profiles) { + const dbPath = path.join(target.root, profile); + const r = await trustInNssDb(`sql:${dbPath}`, pemPath); + outcomes.push({ + label: `${target.label} (${profile})`, + ok: r.exitCode === 0, + stderr: r.stderr, + }); + } +} + +async function trustInNssDb( + dbArg: string, + pemPath: string +): Promise<{ exitCode: number; stderr: string }> { + // Remove any existing cert with this name first to make the operation idempotent + await runProcess("certutil", ["-D", "-d", dbArg, "-n", CERT_NAME]); + + const result = await runProcess("certutil", [ + "-A", + "-d", + dbArg, + "-t", + "CT,,", + "-n", + CERT_NAME, + "-i", + pemPath, + ]); + + if (result.exitCode === 0) { + log(`Trusted cert in NSS database: ${dbArg}`); + } else { + log(`Failed to trust cert in ${dbArg}: ${result.stderr}`); + } + + return { exitCode: result.exitCode, stderr: result.stderr }; +} diff --git a/src/shared/src/platform/processUtil.ts b/src/shared/src/platform/processUtil.ts new file mode 100644 index 0000000..0ccdbb9 --- /dev/null +++ b/src/shared/src/platform/processUtil.ts @@ -0,0 +1,39 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +export interface ProcessResult { + exitCode: number; + stdout: string; + stderr: string; +} + +/** + * Run an external process and return its exit code, stdout, and stderr. + * Does not throw on non-zero exit codes. + */ +export async function runProcess( + command: string, + args: string[], + timeout: number = 30000 +): Promise { + try { + const result = await execFileAsync(command, args, { timeout }); + return { exitCode: 0, stdout: result.stdout, stderr: result.stderr }; + } catch (err: unknown) { + const error = err as Error & { + code?: number | string; + stdout?: string; + stderr?: string; + }; + // If the process ran but returned non-zero, we still have stdout/stderr + const exitCode = + typeof error.code === "number" ? error.code : 1; + return { + exitCode, + stdout: error.stdout ?? "", + stderr: error.stderr ?? error.message, + }; + } +} diff --git a/src/shared/src/platform/types.ts b/src/shared/src/platform/types.ts new file mode 100644 index 0000000..f67f462 --- /dev/null +++ b/src/shared/src/platform/types.ts @@ -0,0 +1,127 @@ +import { type DevCert, type DevKey } from "../cert/types"; +import { type Localizer } from "../localizer"; +import { type NssTrustResult } from "./nssTrust"; + +/** + * Callback the Linux store invokes after attempting browser-NSS trust as + * part of `trustCertificate`. Lets the extension host surface a guidance + * toast on failure without giving the platform store a direct dependency + * on `vscode`. + */ +export type LinuxNssTrustReporter = ( + result: NssTrustResult, + pemPath: string +) => void; + +/** + * Common options accepted by every platform certificate store. Currently + * carries the host-supplied `Localizer` so log lines produced inside the + * shared platform layer match what the host extension surfaces via + * `vscode.l10n.t`. Non-VS-Code consumers (CLI, scripts, tests) can omit it + * and fall back to the identity localizer. + */ +export interface BaseStoreOptions { + /** Optional Localizer; defaults to `identityLocalizer`. */ + localize?: Localizer; +} + +export interface CreatePlatformStoreOptions extends BaseStoreOptions { + /** Optional reporter for NSS trust outcomes; honored only on Linux. */ + linuxNssTrustReporter?: LinuxNssTrustReporter; +} + +export interface CertificateStatus { + exists: boolean; + isTrusted: boolean; + /** SHA-1 thumbprint, uppercase hex (matches `X509Certificate2.Thumbprint`). */ + thumbprint: string | null; + notBefore: string | null; + notAfter: string | null; + version: number; +} + +/** + * Platform-specific certificate store interface. + * + * Throughout this interface, `thumbprint` is the SHA-1 thumbprint + * (`DevCert.thumbprintSha1`). This is what .NET, OpenSSL trust dirs, and + * the Windows / macOS stores use to identify and name cert files. The + * stronger `DevCert.thumbprint` (SHA-256) is for in-process identity only. + */ +export interface PlatformCertificateStore { + /** + * Find an existing valid ASP.NET dev cert in the platform store. + * Returns the cert, key, and SHA-1 thumbprint if found. + */ + findExistingDevCert(): Promise<{ + cert: DevCert; + key: DevKey; + thumbprint: string; + } | null>; + + /** + * Save a certificate with its private key to the platform store. + * `thumbprint` is the SHA-1 thumbprint and becomes the .NET X509Store + * filename stem (`{thumbprint}.pfx`). + */ + saveCertificate( + cert: DevCert, + key: DevKey, + thumbprint: string + ): Promise; + + /** + * Trust a certificate so the OS/browser accepts it. + */ + trustCertificate(cert: DevCert): Promise; + + /** + * Verify on-disk / OS trust state for a specific certificate. Callers + * use this to short-circuit redundant `trustCertificate` calls — on + * macOS in particular, `security add-trusted-cert` is not a true + * no-op for an already-trusted cert (it re-touches the trust-settings + * record and may re-prompt for the keychain password). The cache that + * the host's CertProvider maintains is a goal-state, not a record of + * machine state — every trust operation re-verifies trust here + * before deciding whether to invoke trustCertificate again. + */ + isCertTrusted(cert: DevCert): Promise; + + /** + * Remove dev certificates from all stores. + */ + removeCertificates(): Promise; + + /** + * Check the status of the dev certificate. + */ + checkStatus(): Promise; +} + +/** + * Create the appropriate store for the current platform. + */ +export async function createPlatformStore( + options: CreatePlatformStoreOptions = {} +): Promise { + const baseOptions: BaseStoreOptions = { localize: options.localize }; + switch (process.platform) { + case "win32": { + const { WindowsCertificateStore } = await import("./windowsStore.js"); + return new WindowsCertificateStore("CurrentUser", baseOptions); + } + case "darwin": { + const { MacCertificateStore } = await import("./macStore.js"); + return new MacCertificateStore(baseOptions); + } + case "linux": { + const { LinuxCertificateStore } = await import("./linuxStore.js"); + return new LinuxCertificateStore({ + ...baseOptions, + nssTrustReporter: options.linuxNssTrustReporter, + }); + } + default: + throw new Error(`Unsupported platform: ${process.platform}`); + } +} diff --git a/src/shared/src/platform/windowsStore.ts b/src/shared/src/platform/windowsStore.ts new file mode 100644 index 0000000..01f5eed --- /dev/null +++ b/src/shared/src/platform/windowsStore.ts @@ -0,0 +1,421 @@ +import { randomUUID } from "crypto"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { + BaseCertificateStore, + type UsableDevCert, +} from "./baseStore"; +import { runProcess } from "./processUtil"; +import { type BaseStoreOptions } from "./types"; +import { ASPNET_HTTPS_OID } from "../cert/properties"; +import { certToDer } from "../cert/exporter"; +import { type DevCert, type DevKey } from "../cert/types"; + +/** Cached PowerShell executable name — prefers pwsh (PowerShell 7+) over powershell (5.1). */ +let resolvedPwsh: string | null = null; + +export type WindowsStoreLocation = "CurrentUser" | "LocalMachine"; + +async function getPowerShell(): Promise { + if (resolvedPwsh) return resolvedPwsh; + + const pwshResult = await runProcess("pwsh", ["-NoProfile", "-Command", "echo ok"]); + if (pwshResult.exitCode === 0) { + resolvedPwsh = "pwsh"; + } else { + resolvedPwsh = "powershell"; + } + return resolvedPwsh; +} + +/** + * Shape of one entry in the PowerShell enumeration script's `candidates` + * array — a cert whose private key was successfully exported as a PFX. + */ +export interface PsCandidate { + thumbprint: string; + pfxPath: string; + subjectCN: string | null; + notBefore: string; + notAfter: string; +} + +/** Classification of why the PS script couldn't export a cert. */ +export type PsSkipReason = + | "no-private-key" + | "not-exportable" + | "export-failed"; + +/** + * Shape of one entry in the PowerShell enumeration script's `skipped` + * array — a cert that matched the dev-cert OID but couldn't be exported + * (no private key, or key not exportable). + * + * `reasonDetail` carries the underlying exception message for the + * "export-failed" code (we can't classify it more precisely without + * reaching into .NET-specific types). TS-side maps the code to a + * localized human-readable reason; details get appended verbatim. + */ +export interface PsSkipped { + thumbprint: string; + subjectCN: string | null; + notBefore: string; + notAfter: string; + reasonCode: PsSkipReason; + reasonDetail?: string; +} + +export interface PsEnumeration { + candidates: PsCandidate[]; + skipped: PsSkipped[]; +} + +/** + * Windows certificate store implementation. + * + * Uses PowerShell to interact with the Windows Certificate Store: + * - CurrentUser\My: stores cert with private key + * - CurrentUser\Root: trusts the public cert + * + * Prefers pwsh (PowerShell 7+) when available, falls back to powershell (5.1). + */ +export class WindowsCertificateStore extends BaseCertificateStore { + private readonly storeLocation: WindowsStoreLocation; + + constructor( + storeLocation: WindowsStoreLocation = "CurrentUser", + options: BaseStoreOptions = {} + ) { + super(options); + this.storeLocation = storeLocation; + } + + async findExistingDevCert(): Promise { + // Enumerate every dev-cert candidate in the configured My store, then + // attempt to export each as a PBES2/AES PFX. We hand selection back to + // shared TS so the version-byte tiebreaker logic stays in one place. + // + // The script stays inside the PS cert-provider surface and built-in + // cmdlets — no `New-Object System.*`, no explicit [System.X.Y.Z] type + // references, no .NET-specific property paths (e.g. CNG-only + // PrivateKey.Key.ExportPolicy). Properties accessed on the + // X509Certificate2 objects yielded by `Cert:\…` are the same surface + // the rest of the codebase already relies on (Thumbprint, Subject, + // NotBefore/NotAfter, HasPrivateKey, Extensions). + const script = ` + $ErrorActionPreference = 'Stop' + $oid = '${ASPNET_HTTPS_OID}' + $candidates = @() + $skipped = @() + $certs = Get-ChildItem Cert:\\${this.storeLocation}\\My | Where-Object { + $_.Extensions | Where-Object { $_.Oid.Value -eq $oid } + } + foreach ($cert in $certs) { + $thumb = $cert.Thumbprint + # Subject is a comma-separated RDN string ("CN=localhost, O=..."). Pull + # the first CN out via regex instead of GetNameInfo, which would need + # an explicit [System.Security.Cryptography.X509Certificates.X509NameType] + # type reference. + $cn = $null + if ($cert.Subject -match 'CN=([^,]+)') { $cn = $matches[1].Trim() } + $nbf = $cert.NotBefore.ToUniversalTime().ToString('o') + $exp = $cert.NotAfter.ToUniversalTime().ToString('o') + if (-not $cert.HasPrivateKey) { + $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'no-private-key' } + continue + } + try { + $tmpPfx = Join-Path $env:TEMP ("devcert-" + [guid]::NewGuid().ToString("N") + ".pfx") + $pwd = ConvertTo-SecureString -String 'export' -Force -AsPlainText + # AES256_SHA256 forces a PBES2/AES PFX. The default (TripleDES_SHA1) + # produces a legacy PKCS#12 PBE format that our pkijs-based parser + # deliberately rejects (see cert/pfx.ts). + Export-PfxCertificate -Cert $cert -FilePath $tmpPfx -Password $pwd -CryptoAlgorithmOption AES256_SHA256 | Out-Null + $candidates += @{ thumbprint = $thumb; pfxPath = $tmpPfx; subjectCN = $cn; notBefore = $nbf; notAfter = $exp } + } catch { + # No CNG / RSA-specific introspection here — that would mean + # touching .NET types beyond what the cert provider already + # surfaces. Coarse message-string matching distinguishes the + # "key locked" case from everything else; TS-side maps the code + # to a localized human-readable reason. + $msg = $_.Exception.Message + if ($msg -match 'not exportable' -or $msg -match 'cannot be exported') { + $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'not-exportable' } + } else { + $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'export-failed'; reasonDetail = $msg } + } + } + } + $payload = @{ candidates = $candidates; skipped = $skipped } + $payload | ConvertTo-Json -Compress -Depth 4 + `; + + const pwsh = await getPowerShell(); + const result = await runProcess(pwsh, [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + + if (result.exitCode !== 0) return null; + + const parsed = parseEnumeration(result.stdout); + if (!parsed) return null; + + const tempPfxPaths: string[] = parsed.candidates.map((c) => c.pfxPath); + try { + const usable: UsableDevCert[] = []; + const storeContext = `Windows ${this.storeLocation}\\My`; + + for (const cand of parsed.candidates) { + const loaded = await this.loadPfxLenient(cand.pfxPath, "export"); + if (!loaded) { + // PFX produced by Export-PfxCertificate failed to parse — surface + // as an unusable warning so the user has visibility. + this.classify({ + kind: "forcedSkip", + source: storeContext, + reason: this.localize( + "Export-PfxCertificate produced a PFX that could not be parsed" + ), + metadata: { + thumbprint: cand.thumbprint, + subjectCN: cand.subjectCN, + notBefore: parseDateOrNull(cand.notBefore), + notAfter: parseDateOrNull(cand.notAfter), + }, + }); + continue; + } + + const classified = this.classify({ + kind: "loaded", + source: storeContext, + loaded, + }); + if (classified === null) continue; + if (classified.kind === "usable") usable.push(classified); + } + + for (const sk of parsed.skipped) { + // Re-apply isValidDevCert gates against the metadata we received so + // we don't emit the unusable warning for clearly-unrelated certs + // that happened to share the OID but are e.g. expired. + if (!metadataLooksLikeValidDevCert(sk)) continue; + this.classify({ + kind: "forcedSkip", + source: storeContext, + reason: this.localizeSkipReason(sk), + metadata: { + thumbprint: sk.thumbprint, + subjectCN: sk.subjectCN, + notBefore: parseDateOrNull(sk.notBefore), + notAfter: parseDateOrNull(sk.notAfter), + }, + }); + } + + return this.selectBest(usable, storeContext); + } finally { + for (const p of tempPfxPaths) { + try { + fs.unlinkSync(p); + } catch { + // best effort cleanup + } + } + } + } + + async saveCertificate( + cert: DevCert, + key: DevKey, + _thumbprint: string + ): Promise { + // Export to temp PFX, then import via Import-PfxCertificate. Our + // hand-rolled DER PFX writer (cert/pfx.ts) emits a PFX that CryptoAPI's + // PFXImportCertStore — the function this cmdlet wraps — accepts cleanly. + // Unguessable filename + 0o600 keeps the PFX (with private key) + // unreadable to other local users during the import window. + const tmpPfx = path.join(os.tmpdir(), `devcert-save-${randomUUID()}.pfx`); + await this.writePfx(cert, key, tmpPfx, "import", 0o600); + + const script = + `$ErrorActionPreference = 'Stop'; ` + + `$pwd = ConvertTo-SecureString -String 'import' -Force -AsPlainText; ` + + `Import-PfxCertificate -FilePath '${tmpPfx.replace(/'/g, "''")}' -CertStoreLocation Cert:\\${this.storeLocation}\\My -Password $pwd -Exportable | Out-Null; ` + + `Remove-Item '${tmpPfx.replace(/'/g, "''")}'`; + + const pwsh = await getPowerShell(); + const result = await runProcess(pwsh, [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + + if (result.exitCode !== 0) { + // Clean up temp file if PowerShell didn't + try { + fs.unlinkSync(tmpPfx); + } catch { + /* ignore */ + } + throw new Error( + `Failed to save certificate to Windows store: ${result.stderr}` + ); + } + } + + async trustCertificate(cert: DevCert): Promise { + // Use certutil.exe — the built-in Windows CA admin tool from + // %SystemRoot%\System32, present on every Windows install since XP — to + // add the public cert to the configured Root store. + // + // We can't go back to `Import-Certificate` (the PowerShell PKI cmdlet + // PR #36 switched to): when it targets Cert:\CurrentUser\Root the + // underlying CryptoAPI call shows a "You are about to install a + // certificate from a certification authority..." confirmation dialog, + // which fails under `-NonInteractive` with "UI is not allowed in this + // operation." We also intentionally avoid the older path of + // `New-Object System.Security.Cryptography.X509Certificates.X509Store` + // — the host extension is meant to work without taking on a .NET + // dependency. certutil.exe uses CryptoAPI directly, skips the + // confirmation dialog, and is the same tool mkcert and similar dev-cert + // utilities use on Windows for the same reason. + // + // Public-cert only — no private key — but the random name still + // prevents concurrent invocations from colliding on the same temp path. + const tmpCert = path.join(os.tmpdir(), `devcert-trust-${randomUUID()}.cer`); + fs.writeFileSync(tmpCert, certToDer(cert)); + + const args = ["-f"]; + if (this.storeLocation === "CurrentUser") args.push("-user"); + args.push("-addstore", "Root", tmpCert); + + const result = await runProcess("certutil.exe", args); + + try { + fs.unlinkSync(tmpCert); + } catch { + /* best effort */ + } + + if (result.exitCode !== 0) { + throw new Error( + `Failed to trust certificate on Windows: ${result.stderr || result.stdout}` + ); + } + } + + async removeCertificates(): Promise { + const script = ` + $ErrorActionPreference = 'SilentlyContinue' + $oid = '${ASPNET_HTTPS_OID}' + foreach ($storePath in @('Cert:\\${this.storeLocation}\\My', 'Cert:\\${this.storeLocation}\\Root')) { + Get-ChildItem $storePath | Where-Object { + $_.Extensions | Where-Object { $_.Oid.Value -eq $oid } + } | ForEach-Object { + Remove-Item -LiteralPath $_.PSPath -Force + } + } + `; + + const pwsh = await getPowerShell(); + await runProcess(pwsh, [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + } + + protected async isTrusted( + _cert: DevCert, + thumbprint: string + ): Promise { + const script = ` + $cert = Get-ChildItem Cert:\\${this.storeLocation}\\Root | Where-Object { $_.Thumbprint -eq '${thumbprint}' } + if ($cert) { Write-Output 'true' } else { Write-Output 'false' } + `; + + const pwsh = await getPowerShell(); + const result = await runProcess(pwsh, [ + "-NoProfile", + "-NonInteractive", + "-Command", + script, + ]); + + return result.stdout.trim() === "true"; + } + + private localizeSkipReason(sk: PsSkipped): string { + switch (sk.reasonCode) { + case "no-private-key": + return this.localize("no private key in store"); + case "not-exportable": + return this.localize("private key not exportable"); + case "export-failed": + return this.localize( + "Export-PfxCertificate failed: {0}", + sk.reasonDetail ?? "" + ); + } + } +} + +function parseEnumeration(stdout: string): PsEnumeration | null { + const trimmed = stdout.trim(); + if (!trimmed) return { candidates: [], skipped: [] }; + + let parsed: unknown; + try { + parsed = JSON.parse(trimmed); + } catch { + return null; + } + + // PowerShell's ConvertTo-Json emits a hashtable with a single child as a + // bare object rather than a 1-element array; coerce defensively. + if (!parsed || typeof parsed !== "object") return null; + const root = parsed as Record; + return { + candidates: coerceArray(root.candidates), + skipped: coerceArray(root.skipped), + }; +} + +function coerceArray(value: unknown): T[] { + if (value === undefined || value === null) return []; + if (Array.isArray(value)) return value as T[]; + return [value as T]; +} + +function parseDateOrNull(s: string | null | undefined): Date | null { + if (!s) return null; + const d = new Date(s); + return Number.isNaN(d.getTime()) ? null : d; +} + +function metadataLooksLikeValidDevCert(sk: PsSkipped): boolean { + // The PS script already filtered by ASPNET_HTTPS_OID. We layer CN + + // validity-window checks on top so we don't emit the unusable warning + // for clearly-unrelated certs (e.g. expired or differently-named) that + // happened to carry the same OID. We can't check the version byte from + // TS without parsing the cert, so we skip that gate here. + // + // CN comparison is intentionally exact-match to mirror isValidDevCert + // (see cert/validation.ts:isValidDevCert) — staying loose here while the + // canonical gate is strict would just produce warnings for certs we'd + // never accept downstream anyway. + if (sk.subjectCN !== "localhost") return false; + const nbf = parseDateOrNull(sk.notBefore); + const exp = parseDateOrNull(sk.notAfter); + if (!nbf || !exp) return false; + const now = new Date(); + if (nbf > now || exp < now) return false; + return true; +} diff --git a/src/vscode-ui-extension/src/cert/manager.ts b/src/vscode-ui-extension/src/cert/manager.ts index acdb937..78922f1 100644 --- a/src/vscode-ui-extension/src/cert/manager.ts +++ b/src/vscode-ui-extension/src/cert/manager.ts @@ -1,207 +1,3 @@ -import { generateCertificate, type GeneratedCert } from "./generator"; -import { exportPfx, exportPem, exportRootPfx } from "./exporter"; -import { VALIDITY_DAYS } from "./properties"; -import { - type PlatformCertificateStore, - type CertificateStatus, - type LinuxNssTrustReporter, - createPlatformStore, -} from "../platform/types"; -import { log } from "@devcontainer-dev-certs/shared"; - -export interface CertManagerOptions { - /** - * Optional reporter invoked by the Linux platform store after attempting - * browser-NSS trust as part of `trustCertificate`. No-op on other - * platforms. - */ - linuxNssTrustReporter?: LinuxNssTrustReporter; -} - -/** - * Certificate manager that orchestrates generation, trust, export, and status - * checking using platform-specific stores. - */ -export class CertManager { - private store: PlatformCertificateStore | null = null; - private currentCert: GeneratedCert | null = null; - - constructor(private readonly options: CertManagerOptions = {}) {} - - private async getStore(): Promise { - this.store ??= await createPlatformStore({ - linuxNssTrustReporter: this.options.linuxNssTrustReporter, - }); - return this.store; - } - - /** - * Generate a new dev cert and save it to the platform store. - * If force is true, removes existing certs first. - */ - async generate(force: boolean = false): Promise { - const store = await this.getStore(); - - if (force) { - log("Removing existing certificates..."); - await store.removeCertificates(); - } - - log("Generating new dev certificate..."); - const now = new Date(); - const expiry = new Date( - now.getTime() + VALIDITY_DAYS * 24 * 60 * 60 * 1000 - ); - const generated = await generateCertificate(now, expiry); - this.currentCert = generated; - - log(`Certificate generated. Thumbprint: ${generated.thumbprint}`); - await store.saveCertificate( - generated.cert, - generated.key, - generated.thumbprint - ); - log("Certificate saved to platform store."); - } - - /** - * Trust an externally-supplied certificate (e.g. one pushed from a Dev - * Container via the syncContainerCert reverse-sync flow) in the host - * OS trust store. - * - * Delegates directly to `store.trustCertificate(cert)` — the SAME hook - * the host-generation flow (`trust()`) uses on its final step — so - * "trusted on the host" means the same thing regardless of whether - * the cert was generated here or accepted from a container. On Linux - * that's `.NET Root store + OpenSSL trust dir + NSS browser DBs` (the - * NSS step uses the same `linuxNssTrustReporter` callback the host - * generation flow wires up). On macOS, login keychain trust policy. - * On Windows, CurrentUser/Root via certutil. - * - * Public-cert-only: the cert lands in every trust surface listed - * above but NEVER in CurrentUser/My, the keychain's identity slot, or - * the .NET store's `my/` directory. Skipping `saveCertificate` is - * deliberate — the host doesn't need (and shouldn't store) the - * private key, because Kestrel runs in the container with its own - * copy of the key. Future changes to this method MUST preserve both - * properties: (a) trust goes through `store.trustCertificate`; (b) - * no `saveCertificate` call. `tests/manager.test.ts` pins both. - * - * Does NOT update `currentCert`. The host's auto-generation flow - * (`generate()` / `trust()`) is a separate state machine that the - * container-push path doesn't feed into; if the user also has - * `generateDotNetCert: true` and a subsequent `getAllCertMaterial` - * pull arrives, the host will generate its own (separate) cert as - * normal. - */ - async trustExternalCertificate( - cert: GeneratedCert["cert"] - ): Promise { - const store = await this.getStore(); - - // Verify on-disk state before invoking the platform trust step. - // Skipping a redundant call matters on macOS where - // `security add-trusted-cert` is not a no-op for an already-trusted - // cert (re-touches the trust-settings record, may re-prompt for - // the keychain password). The same cache-as-goal-state / - // verify-on-disk pattern is used by the host-generation flow's - // `trust()` method, just expressed differently because it goes - // through `checkStatus()` instead of a direct `isCertTrusted`. - if (await store.isCertTrusted(cert)) { - log( - `Externally-supplied dev certificate ${cert.thumbprintSha1} is already trusted on host; skipping platform trust call.` - ); - return; - } - - log( - `Trusting externally-supplied dev certificate ${cert.thumbprintSha1} (public cert only, via the same platform trust path as host-generated)...` - ); - await store.trustCertificate(cert); - log("Externally-supplied certificate trusted."); - } - - /** - * Ensure a cert exists and is trusted. Generates one if needed. - */ - async trust(): Promise { - const store = await this.getStore(); - const status = await store.checkStatus(); - - if (!status.exists) { - await this.generate(); - } - - // Re-check: load from store if we didn't just generate - if (!this.currentCert) { - const found = await store.findExistingDevCert(); - if (!found) { - throw new Error("Failed to find certificate after generation."); - } - this.currentCert = found; - } - - const recheck = await store.checkStatus(); - if (!recheck.isTrusted) { - log("Trusting certificate in OS store..."); - await store.trustCertificate(this.currentCert.cert); - log("Certificate trusted."); - } - } - - /** - * Export the current cert in the specified format. - */ - async exportCert( - format: "pfx" | "pem" | "root-pfx", - outputDir: string, - password?: string - ): Promise { - await this.ensureLoaded(); - - if (format === "pfx") { - await exportPfx( - this.currentCert!.cert, - this.currentCert!.key, - outputDir, - password - ); - } else if (format === "root-pfx") { - await exportRootPfx(this.currentCert!.cert, outputDir); - } else { - exportPem(this.currentCert!.cert, this.currentCert!.key, outputDir); - } - } - - /** - * Check the status of the dev certificate. - */ - async check(): Promise { - const store = await this.getStore(); - return store.checkStatus(); - } - - /** - * Remove all dev certificates from the platform store. - */ - async clean(): Promise { - const store = await this.getStore(); - await store.removeCertificates(); - this.currentCert = null; - log("All dev certificates removed."); - } - - /** - * Ensure we have a loaded cert (from store or freshly generated). - */ - private async ensureLoaded(): Promise { - if (this.currentCert) return; - - const store = await this.getStore(); - const found = await store.findExistingDevCert(); - if (!found) { - throw new Error("No dev certificate found. Generate one first."); - } - this.currentCert = found; - } -} +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { CertManager } from "@devcontainer-dev-certs/shared"; +export type { CertManagerOptions } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/extension.ts b/src/vscode-ui-extension/src/extension.ts index 6f4e55b..c7c2e0b 100644 --- a/src/vscode-ui-extension/src/extension.ts +++ b/src/vscode-ui-extension/src/extension.ts @@ -27,6 +27,7 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push(initLogger("Dev Container Dev Certs")); const certManager = new CertManager({ + localize: vscode.l10n.t, linuxNssTrustReporter: (result, pemPath) => { if (result.success) { log(`Linux NSS trust: ${result.message}`); diff --git a/src/vscode-ui-extension/src/platform/baseStore.ts b/src/vscode-ui-extension/src/platform/baseStore.ts index 030ac66..d3c1ed1 100644 --- a/src/vscode-ui-extension/src/platform/baseStore.ts +++ b/src/vscode-ui-extension/src/platform/baseStore.ts @@ -1,325 +1,19 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as vscode from "vscode"; -import { type PlatformCertificateStore, type CertificateStatus } from "./types"; -import { - log, - type DevCert, - type DevKey, - buildPfx, - parsePfx, - getCertificateVersion, - classifyCandidate as classifyCandidateShared, - selectBestDevCert as selectBestDevCertShared, +// Re-export shim: the canonical home for the localized platform classifier +// wrappers and `BaseCertificateStore` is now in +// `@devcontainer-dev-certs/shared`. Imports go through the submodule path +// (rather than the barrel) because the barrel aliases the platform-flavored +// `classifyCandidate` / `selectBestDevCert` to disambiguate from the pure +// classifier; existing tests and call sites expect the unaliased names. +export { + BaseCertificateStore, + classifyCandidate, + selectBestDevCert, extractThumbprintHintFromFilename, - type CandidateInput, - type ClassifiedCandidate, - type SkipReport, - type UsableDevCert, -} from "@devcontainer-dev-certs/shared"; - -// Re-export the pure shared types so existing imports of -// `../platform/baseStore` keep working without touching ~15 test files. +} from "@devcontainer-dev-certs/shared/src/platform/baseStore"; export type { ClassifiedCandidate, CandidateInput, CandidateMetadata, UsableDevCert, -} from "@devcontainer-dev-certs/shared"; -export { extractThumbprintHintFromFilename } from "@devcontainer-dev-certs/shared"; - -/** - * Side-effectful host-side classifier wrapper. Delegates the pure - * classification to the shared module and emits the same localized "skipping - * ASP.NET dev cert ..." log line via `vscode.l10n.t` that the host has - * always produced. New container-side scan paths can call the shared - * `classifyCandidate` directly with their own (or no) localizer. - */ -export function classifyCandidate( - input: CandidateInput -): ClassifiedCandidate | null { - return classifyCandidateShared(input, { - onSkipped: (report) => emitHostSkipLog(report), - }); -} - -/** - * Side-effectful host-side selection wrapper. Delegates to the shared - * selector and emits the multi-candidate localized warning when more than - * one usable candidate is present. - */ -export function selectBestDevCert( - usable: UsableDevCert[], - context: string -): UsableDevCert | null { - return selectBestDevCertShared(usable, context, { - onMultipleCandidates: ({ selected, candidates }) => { - const header = vscode.l10n.t( - "Multiple valid ASP.NET dev certs found in {0}; selected {1}.", - context, - selected.thumbprint - ); - const candidatesHeader = vscode.l10n.t(" Candidates:"); - const selectedTag = vscode.l10n.t("[selected]"); - const skippedTag = vscode.l10n.t("[skipped] "); - const lines = [ - header, - candidatesHeader, - ...candidates.map((c, i) => - vscode.l10n.t( - " {0} thumbprint={1} version={2} notBefore={3} notAfter={4}", - i === 0 ? selectedTag : skippedTag, - c.thumbprint, - getCertificateVersion(c.cert), - c.cert.notBefore.toISOString(), - c.cert.notAfter.toISOString() - ) - ), - ]; - log(lines.join("\n")); - }, - }); -} - -/** - * Render the localized "skipping ASP.NET dev cert" log line for one skipped - * candidate. Maps the shared classifier's reason code to a host-localized - * string. `forced` skips carry the caller's free-form reason verbatim — the - * platform stores (linuxStore / macStore / windowsStore) localize their own - * forced-skip reasons before handing them in, so we pass through. - */ -function emitHostSkipLog(report: SkipReport): void { - let localizedReason: string; - switch (report.reasonCode) { - case "missing-private-key": - localizedReason = vscode.l10n.t( - "PFX contains certificate without matching private key" - ); - break; - case "parse-failed": - localizedReason = vscode.l10n.t( - "failed to parse PFX (corrupt or wrong password)" - ); - break; - case "forced": - // Caller localized this string before classifyCandidate received it. - localizedReason = report.forcedReason ?? ""; - break; - } - const unknown = vscode.l10n.t("(unknown)"); - const meta = report.metadata; - const subjectCN = meta.subjectCN ?? unknown; - const version = - meta.version === undefined || meta.version === null - ? unknown - : String(meta.version); - const notBefore = meta.notBefore ? meta.notBefore.toISOString() : unknown; - const notAfter = meta.notAfter ? meta.notAfter.toISOString() : unknown; - log( - vscode.l10n.t( - "Skipping ASP.NET dev cert {0} ({1}): {2}.\n subjectCN={3} version={4} notBefore={5} notAfter={6}", - meta.thumbprint ?? unknown, - report.source, - localizedReason, - subjectCN, - version, - notBefore, - notAfter - ) - ); -} - -/** - * Base implementation for platform certificate stores. - * - * Provides common logic shared across Windows, macOS, and Linux: - * - checkStatus() with a consistent pattern (find → check trust → build status) - * - PFX loading and writing helpers - * - * Subclasses implement the platform-specific methods: findExistingDevCert, - * saveCertificate, trustCertificate, removeCertificates, and isTrusted. - */ -export abstract class BaseCertificateStore implements PlatformCertificateStore { - async checkStatus(): Promise { - const found = await this.findExistingDevCert(); - if (!found) { - return { - exists: false, - isTrusted: false, - thumbprint: null, - notBefore: null, - notAfter: null, - version: -1, - }; - } - - const { cert, thumbprint } = found; - const trusted = await this.isTrusted(cert, thumbprint); - const version = getCertificateVersion(cert); - - return { - exists: true, - isTrusted: trusted, - thumbprint, - notBefore: cert.notBefore.toISOString(), - notAfter: cert.notAfter.toISOString(), - version, - }; - } - - abstract findExistingDevCert(): Promise; - - abstract saveCertificate( - cert: DevCert, - key: DevKey, - thumbprint: string - ): Promise; - - abstract trustCertificate(cert: DevCert): Promise; - - abstract removeCertificates(): Promise; - - /** - * Public wrapper around `isTrusted` that satisfies the - * `PlatformCertificateStore.isCertTrusted` contract — verify the - * current on-disk / OS trust state for a specific certificate. Lets - * callers (notably `CertManager.trustExternalCertificate`) decide - * whether the platform-level trust step needs to run at all, - * avoiding redundant `security add-trusted-cert` / `certutil - * -addstore` calls that aren't true no-ops. - */ - async isCertTrusted(cert: DevCert): Promise { - return this.isTrusted(cert, cert.thumbprintSha1); - } - - /** - * Platform-specific trust verification. - * Called by checkStatus() to determine if the certificate is trusted. - */ - protected abstract isTrusted( - cert: DevCert, - thumbprint: string - ): Promise; - - // --- Shared helpers --- - - /** - * Parse a PFX file and extract the certificate, private key, and thumbprint. - * Returns `{ cert, key }` where `key` may be null when the PFX is cert-only. - * Returns null only on outright parse failure (corrupt bytes, wrong - * password, unsupported PBE). - */ - protected async loadPfxLenient( - pfxPath: string, - password: string = "" - ): Promise<{ cert: DevCert; key: DevKey | null; thumbprint: string } | null> { - try { - const pfxBytes = fs.readFileSync(pfxPath); - const { cert, key } = await parsePfx(pfxBytes, password); - return { cert, key: key ?? null, thumbprint: cert.thumbprintSha1 }; - } catch { - return null; - } - } - - /** - * Parse a PFX file and extract the certificate, private key, and thumbprint. - * Returns null if the file cannot be parsed or is missing cert/key bags. - * Strict variant — used by existing call sites that want a usable identity - * or nothing. - */ - protected async loadPfx( - pfxPath: string, - password: string = "" - ): Promise { - const loaded = await this.loadPfxLenient(pfxPath, password); - if (!loaded || !loaded.key) return null; - return { cert: loaded.cert, key: loaded.key, thumbprint: loaded.thumbprint }; - } - - /** - * Write a certificate and key as a PFX file. - */ - protected async writePfx( - cert: DevCert, - key: DevKey, - pfxPath: string, - password: string = "", - mode?: number - ): Promise { - const der = await buildPfx({ cert, key, password }); - const options = mode !== undefined ? { mode } : undefined; - fs.writeFileSync(pfxPath, der, options); - } - - /** - * Scan a directory for `*.pfx` files and classify each one. For every - * match: - * - * 1. Try to parse it. Parse failures on canonically-named files - * (`aspnetcore-localhost-.pfx` or `.pfx`) emit the - * "failed to parse PFX" unusable warning; parse failures on other - * filenames are silent — they may belong to other tools. - * 2. If parsed and the cert passes `isValidDevCert` but there's no private - * key, emit the "no matching private key" unusable warning. - * 3. Otherwise classify as `usable`. - * - * Selection runs only over the surviving usable candidates via - * `selectBestDevCert`. Multi-candidate selection emits its own warning. - * - * Callers may narrow which files to consider via `filenamePredicate` - * (default: accept every `*.pfx`). Whether a parse failure produces a - * warning is controlled separately by `extractThumbprintHintFromFilename` - * — see step 1. - */ - protected async findBestDevCertInDir( - dir: string, - password: string = "", - options: { - filenamePredicate?: (filename: string) => boolean; - context?: string; - } = {} - ): Promise { - if (!fs.existsSync(dir)) return null; - - const context = options.context ?? dir; - const usable: UsableDevCert[] = []; - - const files = fs - .readdirSync(dir) - .filter((f) => f.endsWith(".pfx")) - .filter((f) => - options.filenamePredicate ? options.filenamePredicate(f) : true - ); - - for (const file of files) { - const filePath = path.join(dir, file); - const thumbprintHint = extractThumbprintHintFromFilename(file); - const loaded = await this.loadPfxLenient(filePath, password); - - if (!loaded) { - // Canonical name + parse failure → warn; otherwise silent. - classifyCandidate({ - kind: "parseFailure", - source: filePath, - thumbprintHint, - }); - continue; - } - - const classified = classifyCandidate({ - kind: "loaded", - source: filePath, - loaded, - }); - - if (classified === null) continue; - if (classified.kind === "usable") { - usable.push(classified); - } - // skipped → already logged inside classifyCandidate - } - - return selectBestDevCert(usable, context); - } -} + ClassifyOptions, +} from "@devcontainer-dev-certs/shared/src/platform/baseStore"; diff --git a/src/vscode-ui-extension/src/platform/linuxStore.ts b/src/vscode-ui-extension/src/platform/linuxStore.ts index 1f52473..dac9222 100644 --- a/src/vscode-ui-extension/src/platform/linuxStore.ts +++ b/src/vscode-ui-extension/src/platform/linuxStore.ts @@ -1,275 +1,3 @@ -import * as fs from "fs"; -import * as path from "path"; -import { BaseCertificateStore } from "./baseStore"; -import { trustInNss, type NssTrustResult } from "./nssTrust"; -import { runProcess } from "./processUtil"; -import { type LinuxNssTrustReporter } from "./types"; -import { type DevCert, type DevKey } from "../cert/types"; -import { ASPNET_HTTPS_OID } from "../cert/properties"; -import { buildPfx } from "../cert/pfx"; -import { - getDotNetStorePath, - getDotNetRootStorePath, - getOpenSslTrustDir, - getPemFileName, - log, -} from "@devcontainer-dev-certs/shared"; - -export interface LinuxCertificateStoreOptions { - /** - * Optional callback invoked once with the result of the best-effort - * browser-NSS trust step inside `trustCertificate`. Omitting it disables - * the NSS step entirely (used by integration tests and CLI contexts - * where browser trust isn't relevant). - */ - nssTrustReporter?: LinuxNssTrustReporter; -} - -/** - * Linux certificate store implementation. - * - * Storage locations: - * - .NET X509Store path: ~/.dotnet/corefx/cryptography/x509stores/my/ - * - OpenSSL trust dir: ~/.aspnet/dev-certs/trust/ (or DOTNET_DEV_CERTS_OPENSSL_CERTIFICATE_DIRECTORY) - * - * Trust is established by: - * 1. Writing a PFX to the .NET Root store path (for .NET runtime validation) - * 2. Writing a PEM to the OpenSSL trust directory with hash symlinks (for OpenSSL/curl/etc.) - * 3. Best-effort import into Linux browser NSS databases, when a reporter - * is configured. - */ -export class LinuxCertificateStore extends BaseCertificateStore { - private readonly nssTrustReporter?: LinuxNssTrustReporter; - - constructor(options: LinuxCertificateStoreOptions = {}) { - super(); - this.nssTrustReporter = options.nssTrustReporter; - } - - private get dotNetRootStorePath(): string { - return getDotNetRootStorePath(); - } - - async findExistingDevCert(): Promise<{ - cert: DevCert; - key: DevKey; - thumbprint: string; - } | null> { - return this.findBestDevCertInDir(getDotNetStorePath()); - } - - async saveCertificate( - cert: DevCert, - key: DevKey, - thumbprint: string - ): Promise { - const storeDir = getDotNetStorePath(); - fs.mkdirSync(storeDir, { recursive: true }); - await this.writePfx( - cert, - key, - path.join(storeDir, `${thumbprint}.pfx`), - "", - 0o600 - ); - } - - async trustCertificate(cert: DevCert): Promise { - await this.trustInDotNetRootStore(cert); - await this.trustViaOpenSsl(cert); - await this.trustInNssBrowsers(cert); - } - - /** - * Best-effort browser NSS trust. Mirrors the Windows / macOS pattern where - * `trustCertificate` performs every trust step the platform supports out - * of the box — on Linux that includes adding the cert to the user's NSS - * databases for Firefox / Chromium-family browsers. Never throws: NSS - * tooling or stores are commonly absent and shouldn't break .NET / OpenSSL - * trust. Results are surfaced via the reporter callback (typically used by - * the extension host to show a manual-guidance toast on failure). - */ - private async trustInNssBrowsers(cert: DevCert): Promise { - if (!this.nssTrustReporter) return; - - const pemPath = path.join( - getOpenSslTrustDir(), - getPemFileName(cert.thumbprintSha1) - ); - if (!fs.existsSync(pemPath)) { - log(`Linux NSS trust: PEM not found at ${pemPath}, skipping.`); - return; - } - - let result: NssTrustResult; - try { - result = await trustInNss(pemPath); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - log(`Linux NSS trust threw unexpectedly: ${message}`); - result = { success: false, message }; - } - - this.nssTrustReporter(result, pemPath); - } - - async removeCertificates(): Promise { - await this.removeDevCertsFromDir(getDotNetStorePath()); - await this.removeDevCertsFromDir(this.dotNetRootStorePath); - - const trustDir = getOpenSslTrustDir(); - if (fs.existsSync(trustDir)) { - const entries = fs.readdirSync(trustDir); - for (const entry of entries) { - const fullPath = path.join(trustDir, entry); - if (entry.startsWith("aspnetcore-localhost-")) { - fs.unlinkSync(fullPath); - } else if (isHashSymlink(entry)) { - try { - if (fs.lstatSync(fullPath).isSymbolicLink()) { - fs.unlinkSync(fullPath); - } - } catch { - // ignore - } - } - } - } - } - - protected isTrusted( - _cert: DevCert, - thumbprint: string - ): Promise { - const pemPath = path.join(getOpenSslTrustDir(), getPemFileName(thumbprint)); - return Promise.resolve(fs.existsSync(pemPath)); - } - - // --- Linux-specific trust helpers --- - - private async trustInDotNetRootStore(cert: DevCert): Promise { - fs.mkdirSync(this.dotNetRootStorePath, { recursive: true }); - - const thumbprint = cert.thumbprintSha1; - const certPath = path.join(this.dotNetRootStorePath, `${thumbprint}.pfx`); - - // .NET's X509Store on Linux stores certs as individual PFX files. - // For the Root store, we need a PFX containing only the public cert (no private key). - const pfxBytes = await buildPfx({ cert }); - fs.writeFileSync(certPath, pfxBytes, { mode: 0o644 }); - } - - private async trustViaOpenSsl(cert: DevCert): Promise { - const trustDir = getOpenSslTrustDir(); - fs.mkdirSync(trustDir, { recursive: true }); - - const thumbprint = cert.thumbprintSha1; - const pemFileName = getPemFileName(thumbprint); - const pemPath = path.join(trustDir, pemFileName); - - // Purely additive: write the new PEM, but do NOT delete other - // `aspnetcore-localhost-*.pem` files in the trust dir. The previous - // implementation swept "old rotations" defensively, but that turned - // every `trustCertificate` call into an implicit revocation of every - // other dev cert in the trust dir — including the host-generated - // cert when the container-push reverse-sync flow trusts an - // additional one, and vice versa. Removing trust from a cert the - // user (or another flow) explicitly trusted is the cleanup - // command's job — gated by an explicit user prompt — not - // trustCertificate's. The cleanup sweep continues to handle - // legitimately stale artifacts; this method stays additive so the - // generation flow and the reverse-sync flow can coexist without - // ping-ponging trust. - // - // Writing to the exact same path on a same-thumbprint repeat call - // is idempotent (overwrites identical content); rehashing afterward - // is a no-op when nothing changed. - fs.writeFileSync(pemPath, cert.pem, { mode: 0o644 }); - await this.rehashDirectory(trustDir); - } - - private async rehashDirectory(directory: string): Promise { - const entries = fs.readdirSync(directory); - - // Remove existing hash symlinks - for (const entry of entries) { - if (isHashSymlink(entry)) { - const fullPath = path.join(directory, entry); - try { - if (fs.lstatSync(fullPath).isSymbolicLink()) { - fs.unlinkSync(fullPath); - } - } catch { - // ignore - } - } - } - - // Create new hash symlinks for all PEM/CRT files - const certFiles = fs - .readdirSync(directory) - .filter((f) => /\.(pem|crt|cer)$/i.test(f)); - - for (const certFile of certFiles) { - const fullPath = path.join(directory, certFile); - try { - if (fs.lstatSync(fullPath).isSymbolicLink()) continue; - } catch { - continue; - } - - const hash = await this.getOpenSslSubjectHash(fullPath); - if (!hash) continue; - - // Slot 0-9 covers any realistic collision count. Catch EEXIST so a - // concurrent rehash can't crash this one mid-loop. - for (let i = 0; i < 10; i++) { - const linkPath = path.join(directory, `${hash}.${i}`); - if (fs.existsSync(linkPath)) continue; - try { - fs.symlinkSync(certFile, linkPath); - break; - } catch (err: unknown) { - if ((err as NodeJS.ErrnoException).code === "EEXIST") continue; - throw err; - } - } - } - } - - private async getOpenSslSubjectHash( - certPath: string - ): Promise { - const result = await runProcess("openssl", [ - "x509", - "-hash", - "-noout", - "-in", - certPath, - ]); - if (result.exitCode !== 0) return null; - return result.stdout.trim() || null; - } - - private async removeDevCertsFromDir(dir: string): Promise { - if (!fs.existsSync(dir)) return; - - const files = fs.readdirSync(dir).filter((f) => f.endsWith(".pfx")); - for (const file of files) { - const pfxPath = path.join(dir, file); - try { - const result = await this.loadPfx(pfxPath); - if (result && result.cert.hasExtension(ASPNET_HTTPS_OID)) { - fs.unlinkSync(pfxPath); - } - } catch { - // Skip files that can't be parsed - } - } - } -} - -function isHashSymlink(filename: string): boolean { - return /^[0-9a-f]{8}\.\d+$/.test(filename); -} - +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { LinuxCertificateStore } from "@devcontainer-dev-certs/shared"; +export type { LinuxCertificateStoreOptions } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/platform/macStore.ts b/src/vscode-ui-extension/src/platform/macStore.ts index 70b00d6..d7f2551 100644 --- a/src/vscode-ui-extension/src/platform/macStore.ts +++ b/src/vscode-ui-extension/src/platform/macStore.ts @@ -1,329 +1,2 @@ -import { randomUUID } from "crypto"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import * as vscode from "vscode"; -import { - BaseCertificateStore, - classifyCandidate, - extractThumbprintHintFromFilename, - selectBestDevCert, - type UsableDevCert, -} from "./baseStore"; -import { runProcess } from "./processUtil"; -import { - getCertificateVersion, - isValidDevCert, -} from "../cert/generator"; -import { certToDer } from "../cert/exporter"; -import { ASPNET_HTTPS_OID } from "../cert/properties"; -import { DevCert, type DevKey } from "../cert/types"; - -/** - * macOS certificate store implementation. - * - * Storage locations: - * - Disk: ~/.aspnet/dev-certs/https/aspnetcore-localhost-{thumbprint}.pfx - * - Keychain: login keychain for trust validation - * - * Uses the `security` CLI for keychain trust operations. - */ -export class MacCertificateStore extends BaseCertificateStore { - private get devCertsDir(): string { - return path.join(os.homedir(), ".aspnet", "dev-certs", "https"); - } - - private get keychainPath(): string { - return path.join(os.homedir(), "Library", "Keychains", "login.keychain-db"); - } - - async findExistingDevCert(): Promise { - const context = "macOS login keychain"; - const usable: UsableDevCert[] = []; - const seenThumbprints = new Set(); - - // 1) Walk ~/.aspnet/dev-certs/https/. For each parseable PFX whose cert - // passes isValidDevCert, additionally verify a matching cert is - // actually present in the login keychain via `security find- - // certificate -Z ` (public-only, no prompt). PFXs whose cert - // isn't in the keychain are classified as "orphaned cache file" - // skipped entries and excluded from selection. - if (fs.existsSync(this.devCertsDir)) { - const pfxFiles = fs - .readdirSync(this.devCertsDir) - .filter( - (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") - ); - - for (const pfxFile of pfxFiles) { - const pfxPath = path.join(this.devCertsDir, pfxFile); - const loaded = await this.loadPfxLenient(pfxPath); - - if (!loaded) { - classifyCandidate({ - kind: "parseFailure", - source: pfxPath, - thumbprintHint: extractThumbprintHintFromFilename(pfxFile), - }); - continue; - } - - const classified = classifyCandidate({ - kind: "loaded", - source: pfxPath, - loaded, - }); - if (classified === null) continue; - if (classified.kind !== "usable") { - seenThumbprints.add(loaded.thumbprint); - continue; - } - - // Verify keychain presence — no prompt, public-only. - const inKeychain = await this.isCertInKeychain(classified.thumbprint); - if (!inKeychain) { - classifyCandidate({ - kind: "forcedSkip", - source: pfxPath, - reason: vscode.l10n.t( - "PFX present on disk but matching certificate not in macOS login keychain (orphaned cache file)" - ), - metadata: { - thumbprint: classified.thumbprint, - subjectCN: classified.cert.subjectCN, - version: getCertificateVersion(classified.cert), - notBefore: classified.cert.notBefore, - notAfter: classified.cert.notAfter, - }, - }); - seenThumbprints.add(classified.thumbprint); - continue; - } - - seenThumbprints.add(classified.thumbprint); - usable.push(classified); - } - } - - // 2) Soft keychain enumeration — emit a warning for keychain-resident - // dev certs that lack a matching cache PFX. Public-only read, never - // triggers an ACL prompt, low EDR signal. - const keychainEntries = await this.enumerateKeychainDevCerts(); - for (const entry of keychainEntries) { - if (seenThumbprints.has(entry.thumbprint)) continue; - classifyCandidate({ - kind: "forcedSkip", - source: context, - reason: vscode.l10n.t( - "present in keychain but no matching PFX in {0}", - `${this.devCertsDir}/aspnetcore-localhost-${entry.thumbprint}.pfx` - ), - metadata: { - thumbprint: entry.thumbprint, - subjectCN: entry.cert.subjectCN, - version: getCertificateVersion(entry.cert), - notBefore: entry.cert.notBefore, - notAfter: entry.cert.notAfter, - }, - }); - } - - return selectBestDevCert(usable, this.devCertsDir); - } - - /** - * Returns true if a certificate with the given SHA-1 thumbprint is - * present in the login keychain. Uses `security find-certificate -Z` - * which only reads the public certificate — no ACL prompt is raised - * regardless of the cert's private-key ACL. - */ - private async isCertInKeychain(thumbprint: string): Promise { - const result = await runProcess("security", [ - "find-certificate", - "-Z", - thumbprint, - this.keychainPath, - ]); - return result.exitCode === 0; - } - - /** - * Enumerate ASP.NET dev cert candidates that exist in the login keychain. - * Returns parsed certs whose CN is `localhost`, that bear the ASP.NET - * custom OID, and whose validity window is current. Public-only, no - * prompts. - */ - private async enumerateKeychainDevCerts(): Promise< - Array<{ cert: DevCert; thumbprint: string }> - > { - const result = await runProcess("security", [ - "find-certificate", - "-a", - "-p", - "-Z", - this.keychainPath, - ]); - if (result.exitCode !== 0) return []; - - const out: Array<{ cert: DevCert; thumbprint: string }> = []; - const pemBlocks = extractPemBlocks(result.stdout); - for (const pem of pemBlocks) { - try { - const cert = new DevCert(pem); - if (cert.subjectCN !== "localhost") continue; - if (!cert.hasExtension(ASPNET_HTTPS_OID)) continue; - if (!isValidDevCert(cert)) continue; - out.push({ cert, thumbprint: cert.thumbprintSha1 }); - } catch { - // Skip lines that don't parse as a cert (the -a -p -Z output - // includes SHA-1 lines interleaved with PEM blocks). - } - } - return out; - } - - async saveCertificate( - cert: DevCert, - key: DevKey, - thumbprint: string - ): Promise { - fs.mkdirSync(this.devCertsDir, { recursive: true }); - const pfxPath = path.join( - this.devCertsDir, - `aspnetcore-localhost-${thumbprint}.pfx` - ); - // ~/.aspnet/dev-certs/https/*.pfx contains the private key; force 0o600 - // so it can't be read by other users on a multi-user mac. - await this.writePfx(cert, key, pfxPath, "", 0o600); - } - - async trustCertificate(cert: DevCert): Promise { - // /tmp is shared on macOS; an unguessable filename rules out symlink - // races on the temporary public-cert artifact. - const tmpCert = path.join(os.tmpdir(), `devcert-trust-${randomUUID()}.cer`); - fs.writeFileSync(tmpCert, certToDer(cert)); - - const result = await runProcess("security", [ - "add-trusted-cert", - "-p", - "basic", - "-p", - "ssl", - "-k", - this.keychainPath, - tmpCert, - ]); - - try { - fs.unlinkSync(tmpCert); - } catch { - /* ignore */ - } - - if (result.exitCode !== 0) { - throw new Error( - `Failed to trust certificate in keychain: ${result.stderr}` - ); - } - } - - async removeCertificates(): Promise { - // Collect SHA-1 thumbprints of every dev cert we have on disk so we - // can delete keychain entries by hash. Matching on `-c localhost` is - // too broad — the user may have unrelated `localhost` certs added - // for other tools and we don't want to nuke those. - const thumbprints = new Set(); - if (fs.existsSync(this.devCertsDir)) { - const pfxFiles = fs - .readdirSync(this.devCertsDir) - .filter( - (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") - ); - for (const pfxFile of pfxFiles) { - try { - const result = await this.loadPfx(path.join(this.devCertsDir, pfxFile)); - if (result && result.cert.hasExtension(ASPNET_HTTPS_OID)) { - thumbprints.add(result.thumbprint); - } - } catch { - // Skip unparseable files; they're not ours to delete by hash. - } - } - } - - for (const thumbprint of thumbprints) { - // delete-certificate exits non-zero once there are no more entries - // matching the hash; loop with a generous bound to drain any - // duplicates left by past regenerations. - for (let i = 0; i < 100; i++) { - const result = await runProcess("security", [ - "delete-certificate", - "-Z", - thumbprint, - this.keychainPath, - ]); - if (result.exitCode !== 0) break; - } - } - - // Remove trust settings entries that pointed at any of those certs. - await runProcess("security", [ - "remove-trusted-cert", - "-d", - this.keychainPath, - ]); - - // Remove PFX files from disk - if (fs.existsSync(this.devCertsDir)) { - const pfxFiles = fs - .readdirSync(this.devCertsDir) - .filter( - (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") - ); - for (const pfxFile of pfxFiles) { - fs.unlinkSync(path.join(this.devCertsDir, pfxFile)); - } - } - } - - protected async isTrusted( - cert: DevCert, - _thumbprint: string - ): Promise { - const tmpCert = path.join(os.tmpdir(), `devcert-verify-${randomUUID()}.cer`); - try { - fs.writeFileSync(tmpCert, certToDer(cert)); - - const result = await runProcess("security", [ - "verify-cert", - "-c", - tmpCert, - "-p", - "ssl", - ]); - - return result.exitCode === 0; - } finally { - try { - fs.unlinkSync(tmpCert); - } catch { - // best effort cleanup - } - } - } -} - -/** - * Pull PEM-encoded CERTIFICATE blocks out of `security find-certificate -p` - * output. The CLI interleaves SHA-1 hash lines (when `-Z` is passed) with - * the PEM blocks; we just grab the blocks. - */ -function extractPemBlocks(text: string): string[] { - const blocks: string[] = []; - const regex = /-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g; - let m: RegExpExecArray | null; - while ((m = regex.exec(text)) !== null) { - blocks.push(m[0]); - } - return blocks; -} +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { MacCertificateStore } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/platform/nssTrust.ts b/src/vscode-ui-extension/src/platform/nssTrust.ts index f178fe4..dba495a 100644 --- a/src/vscode-ui-extension/src/platform/nssTrust.ts +++ b/src/vscode-ui-extension/src/platform/nssTrust.ts @@ -1,264 +1,3 @@ -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import { runProcess } from "./processUtil"; -import { log } from "@devcontainer-dev-certs/shared"; - -export interface NssTrustResult { - success: boolean; - message: string; -} - -const CERT_NAME = "Dev Container Dev Cert"; - -type NssTargetKind = "chromium-shared" | "firefox-profiles"; - -interface NssTarget { - label: string; - kind: NssTargetKind; - root: string; -} - -interface DbOutcome { - label: string; - ok: boolean; - stderr?: string; -} - -/** - * Enumerate well-known NSS database roots on Linux. Covers native packages, - * Snap and Flatpak sandbox roots, and a handful of Firefox forks that keep - * their own profile directory. The list is ordered most-common-first; missing - * roots are silently skipped at scan time. - */ -function getNssTargets(home: string): NssTarget[] { - const chromium = (label: string, ...segs: string[]): NssTarget => ({ - label, - kind: "chromium-shared", - root: path.join(home, ...segs), - }); - const firefox = (label: string, ...segs: string[]): NssTarget => ({ - label, - kind: "firefox-profiles", - root: path.join(home, ...segs), - }); - - return [ - // Chromium-family browsers share a single user NSS DB. The native - // ~/.pki/nssdb path is the historical shared store used by Chromium, - // Chrome, Brave, Vivaldi, Edge, and Opera when installed as deb/rpm/AUR. - chromium("Chromium", ".pki", "nssdb"), - chromium("Chromium (Snap)", "snap", "chromium", "common", ".pki", "nssdb"), - chromium( - "Chromium (Flatpak)", - ".var", - "app", - "org.chromium.Chromium", - ".pki", - "nssdb" - ), - chromium( - "Chrome (Flatpak)", - ".var", - "app", - "com.google.Chrome", - ".pki", - "nssdb" - ), - chromium( - "Brave (Flatpak)", - ".var", - "app", - "com.brave.Browser", - ".pki", - "nssdb" - ), - chromium( - "Vivaldi (Flatpak)", - ".var", - "app", - "com.vivaldi.Vivaldi", - ".pki", - "nssdb" - ), - chromium( - "Edge (Flatpak)", - ".var", - "app", - "com.microsoft.Edge", - ".pki", - "nssdb" - ), - - // Firefox-family browsers keep an NSS DB per profile under a known root. - firefox("Firefox", ".mozilla", "firefox"), - firefox( - "Firefox (Snap)", - "snap", - "firefox", - "common", - ".mozilla", - "firefox" - ), - firefox( - "Firefox (Flatpak)", - ".var", - "app", - "org.mozilla.firefox", - ".mozilla", - "firefox" - ), - firefox("Firefox ESR", ".mozilla", "firefox-esr"), - firefox("LibreWolf", ".librewolf"), - firefox( - "LibreWolf (Flatpak)", - ".var", - "app", - "io.gitlab.librewolf-community", - ".librewolf" - ), - firefox("Waterfox", ".waterfox"), - firefox("Floorp", ".floorp"), - ]; -} - -/** - * Attempt to trust a PEM certificate in NSS databases used by Linux browsers. - * - * Requires `certutil` on the PATH (from libnss3-tools / nss-tools / nss). - * Enumerates well-known NSS database roots — native deb/rpm installs, Snap - * and Flatpak sandboxes, and Firefox forks — and adds the certificate to each - * existing database. Targets that aren't installed are skipped silently; the - * returned message lists only databases we actually touched. - */ -export async function trustInNss(pemPath: string): Promise { - const which = await runProcess("which", ["certutil"]); - if (which.exitCode !== 0) { - return { - success: false, - message: - "certutil is not installed. Install libnss3-tools (Debian/Ubuntu), nss-tools (Fedora/RHEL), or nss (Arch) to enable automatic browser trust.", - }; - } - - const outcomes: DbOutcome[] = []; - const targets = getNssTargets(os.homedir()); - - for (const target of targets) { - if (target.kind === "chromium-shared") { - await scanChromiumShared(target, pemPath, outcomes); - } else { - await scanFirefoxProfiles(target, pemPath, outcomes); - } - } - - if (outcomes.length === 0) { - return { - success: false, - message: - "No browser NSS databases found. Open Firefox or Chromium at least once to create a profile, then try again.", - }; - } - - const trusted = outcomes.filter((o) => o.ok).map((o) => o.label); - const failed = outcomes.filter((o) => !o.ok); - - const parts: string[] = []; - if (trusted.length > 0) parts.push(`Trusted in: ${trusted.join(", ")}`); - for (const f of failed) { - parts.push(`${f.label}: failed (${f.stderr?.trim() ?? "unknown error"})`); - } - - return { - success: failed.length === 0, - message: parts.join("; "), - }; -} - -async function scanChromiumShared( - target: NssTarget, - pemPath: string, - outcomes: DbOutcome[] -): Promise { - if (!fs.existsSync(path.join(target.root, "cert9.db"))) { - log(`NSS scan: ${target.label} not present at ${target.root}, skipping.`); - return; - } - const r = await trustInNssDb(`sql:${target.root}`, pemPath); - outcomes.push({ - label: target.label, - ok: r.exitCode === 0, - stderr: r.stderr, - }); -} - -async function scanFirefoxProfiles( - target: NssTarget, - pemPath: string, - outcomes: DbOutcome[] -): Promise { - if (!fs.existsSync(target.root)) { - log(`NSS scan: ${target.label} not present at ${target.root}, skipping.`); - return; - } - - let entries: string[]; - try { - entries = fs.readdirSync(target.root); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - log(`NSS scan: failed to enumerate ${target.label} profiles: ${message}`); - return; - } - - const profiles = entries.filter((d) => { - try { - return fs.existsSync(path.join(target.root, d, "cert9.db")); - } catch { - return false; - } - }); - - if (profiles.length === 0) { - log(`NSS scan: ${target.label} has no profiles with cert9.db, skipping.`); - return; - } - - for (const profile of profiles) { - const dbPath = path.join(target.root, profile); - const r = await trustInNssDb(`sql:${dbPath}`, pemPath); - outcomes.push({ - label: `${target.label} (${profile})`, - ok: r.exitCode === 0, - stderr: r.stderr, - }); - } -} - -async function trustInNssDb( - dbArg: string, - pemPath: string -): Promise<{ exitCode: number; stderr: string }> { - // Remove any existing cert with this name first to make the operation idempotent - await runProcess("certutil", ["-D", "-d", dbArg, "-n", CERT_NAME]); - - const result = await runProcess("certutil", [ - "-A", - "-d", - dbArg, - "-t", - "CT,,", - "-n", - CERT_NAME, - "-i", - pemPath, - ]); - - if (result.exitCode === 0) { - log(`Trusted cert in NSS database: ${dbArg}`); - } else { - log(`Failed to trust cert in ${dbArg}: ${result.stderr}`); - } - - return { exitCode: result.exitCode, stderr: result.stderr }; -} +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { trustInNss } from "@devcontainer-dev-certs/shared"; +export type { NssTrustResult } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/platform/processUtil.ts b/src/vscode-ui-extension/src/platform/processUtil.ts index 0ccdbb9..df7686c 100644 --- a/src/vscode-ui-extension/src/platform/processUtil.ts +++ b/src/vscode-ui-extension/src/platform/processUtil.ts @@ -1,39 +1,3 @@ -import { execFile } from "child_process"; -import { promisify } from "util"; - -const execFileAsync = promisify(execFile); - -export interface ProcessResult { - exitCode: number; - stdout: string; - stderr: string; -} - -/** - * Run an external process and return its exit code, stdout, and stderr. - * Does not throw on non-zero exit codes. - */ -export async function runProcess( - command: string, - args: string[], - timeout: number = 30000 -): Promise { - try { - const result = await execFileAsync(command, args, { timeout }); - return { exitCode: 0, stdout: result.stdout, stderr: result.stderr }; - } catch (err: unknown) { - const error = err as Error & { - code?: number | string; - stdout?: string; - stderr?: string; - }; - // If the process ran but returned non-zero, we still have stdout/stderr - const exitCode = - typeof error.code === "number" ? error.code : 1; - return { - exitCode, - stdout: error.stdout ?? "", - stderr: error.stderr ?? error.message, - }; - } -} +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { runProcess } from "@devcontainer-dev-certs/shared"; +export type { ProcessResult } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/platform/types.ts b/src/vscode-ui-extension/src/platform/types.ts index 7ac2040..12314f6 100644 --- a/src/vscode-ui-extension/src/platform/types.ts +++ b/src/vscode-ui-extension/src/platform/types.ts @@ -1,112 +1,12 @@ -import { type DevCert, type DevKey } from "../cert/types"; -import { type NssTrustResult } from "./nssTrust"; - -/** - * Callback the Linux store invokes after attempting browser-NSS trust as - * part of `trustCertificate`. Lets the extension host surface a guidance - * toast on failure without giving the platform store a direct dependency - * on `vscode`. - */ -export type LinuxNssTrustReporter = ( - result: NssTrustResult, - pemPath: string -) => void; - -export interface CreatePlatformStoreOptions { - /** Optional reporter for NSS trust outcomes; honored only on Linux. */ - linuxNssTrustReporter?: LinuxNssTrustReporter; -} - -export interface CertificateStatus { - exists: boolean; - isTrusted: boolean; - /** SHA-1 thumbprint, uppercase hex (matches `X509Certificate2.Thumbprint`). */ - thumbprint: string | null; - notBefore: string | null; - notAfter: string | null; - version: number; -} - -/** - * Platform-specific certificate store interface. - * - * Throughout this interface, `thumbprint` is the SHA-1 thumbprint - * (`DevCert.thumbprintSha1`). This is what .NET, OpenSSL trust dirs, and - * the Windows / macOS stores use to identify and name cert files. The - * stronger `DevCert.thumbprint` (SHA-256) is for in-process identity only. - */ -export interface PlatformCertificateStore { - /** - * Find an existing valid ASP.NET dev cert in the platform store. - * Returns the cert, key, and SHA-1 thumbprint if found. - */ - findExistingDevCert(): Promise<{ - cert: DevCert; - key: DevKey; - thumbprint: string; - } | null>; - - /** - * Save a certificate with its private key to the platform store. - * `thumbprint` is the SHA-1 thumbprint and becomes the .NET X509Store - * filename stem (`{thumbprint}.pfx`). - */ - saveCertificate( - cert: DevCert, - key: DevKey, - thumbprint: string - ): Promise; - - /** - * Trust a certificate so the OS/browser accepts it. - */ - trustCertificate(cert: DevCert): Promise; - - /** - * Verify on-disk / OS trust state for a specific certificate. Callers - * use this to short-circuit redundant `trustCertificate` calls — on - * macOS in particular, `security add-trusted-cert` is not a true - * no-op for an already-trusted cert (it re-touches the trust-settings - * record and may re-prompt for the keychain password). The cache that - * the host's CertProvider maintains is a goal-state, not a record of - * machine state — every trust operation re-verifies trust here - * before deciding whether to invoke trustCertificate again. - */ - isCertTrusted(cert: DevCert): Promise; - - /** - * Remove dev certificates from all stores. - */ - removeCertificates(): Promise; - - /** - * Check the status of the dev certificate. - */ - checkStatus(): Promise; -} - -/** - * Create the appropriate store for the current platform. - */ -export async function createPlatformStore( - options: CreatePlatformStoreOptions = {} -): Promise { - switch (process.platform) { - case "win32": { - const { WindowsCertificateStore } = await import("./windowsStore.js"); - return new WindowsCertificateStore(); - } - case "darwin": { - const { MacCertificateStore } = await import("./macStore.js"); - return new MacCertificateStore(); - } - case "linux": { - const { LinuxCertificateStore } = await import("./linuxStore.js"); - return new LinuxCertificateStore({ - nssTrustReporter: options.linuxNssTrustReporter, - }); - } - default: - throw new Error(`Unsupported platform: ${process.platform}`); - } -} +// Re-export shim: the canonical home for the platform store types and the +// `createPlatformStore` factory is now `@devcontainer-dev-certs/shared`. +// Keeping this thin re-export so existing `./platform/types` imports across +// the UI extension and its test suite keep resolving without a rename. +export type { + LinuxNssTrustReporter, + BaseStoreOptions, + CreatePlatformStoreOptions, + CertificateStatus, + PlatformCertificateStore, +} from "@devcontainer-dev-certs/shared"; +export { createPlatformStore } from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/src/platform/windowsStore.ts b/src/vscode-ui-extension/src/platform/windowsStore.ts index 939034a..f3ab097 100644 --- a/src/vscode-ui-extension/src/platform/windowsStore.ts +++ b/src/vscode-ui-extension/src/platform/windowsStore.ts @@ -1,417 +1,9 @@ -import { randomUUID } from "crypto"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import * as vscode from "vscode"; -import { - BaseCertificateStore, - classifyCandidate, - selectBestDevCert, - type UsableDevCert, -} from "./baseStore"; -import { runProcess } from "./processUtil"; -import { ASPNET_HTTPS_OID } from "../cert/properties"; -import { certToDer } from "../cert/exporter"; -import { type DevCert, type DevKey } from "../cert/types"; - -/** Cached PowerShell executable name — prefers pwsh (PowerShell 7+) over powershell (5.1). */ -let resolvedPwsh: string | null = null; - -export type WindowsStoreLocation = "CurrentUser" | "LocalMachine"; - -async function getPowerShell(): Promise { - if (resolvedPwsh) return resolvedPwsh; - - const pwshResult = await runProcess("pwsh", ["-NoProfile", "-Command", "echo ok"]); - if (pwshResult.exitCode === 0) { - resolvedPwsh = "pwsh"; - } else { - resolvedPwsh = "powershell"; - } - return resolvedPwsh; -} - -/** - * Shape of one entry in the PowerShell enumeration script's `candidates` - * array — a cert whose private key was successfully exported as a PFX. - */ -export interface PsCandidate { - thumbprint: string; - pfxPath: string; - subjectCN: string | null; - notBefore: string; - notAfter: string; -} - -/** Classification of why the PS script couldn't export a cert. */ -export type PsSkipReason = - | "no-private-key" - | "not-exportable" - | "export-failed"; - -/** - * Shape of one entry in the PowerShell enumeration script's `skipped` - * array — a cert that matched the dev-cert OID but couldn't be exported - * (no private key, or key not exportable). - * - * `reasonDetail` carries the underlying exception message for the - * "export-failed" code (we can't classify it more precisely without - * reaching into .NET-specific types). TS-side maps the code to a - * localized human-readable reason; details get appended verbatim. - */ -export interface PsSkipped { - thumbprint: string; - subjectCN: string | null; - notBefore: string; - notAfter: string; - reasonCode: PsSkipReason; - reasonDetail?: string; -} - -export interface PsEnumeration { - candidates: PsCandidate[]; - skipped: PsSkipped[]; -} - -/** - * Windows certificate store implementation. - * - * Uses PowerShell to interact with the Windows Certificate Store: - * - CurrentUser\My: stores cert with private key - * - CurrentUser\Root: trusts the public cert - * - * Prefers pwsh (PowerShell 7+) when available, falls back to powershell (5.1). - */ -export class WindowsCertificateStore extends BaseCertificateStore { - constructor(private readonly storeLocation: WindowsStoreLocation = "CurrentUser") { - super(); - } - - async findExistingDevCert(): Promise { - // Enumerate every dev-cert candidate in the configured My store, then - // attempt to export each as a PBES2/AES PFX. We hand selection back to - // shared TS so the version-byte tiebreaker logic stays in one place. - // - // The script stays inside the PS cert-provider surface and built-in - // cmdlets — no `New-Object System.*`, no explicit [System.X.Y.Z] type - // references, no .NET-specific property paths (e.g. CNG-only - // PrivateKey.Key.ExportPolicy). Properties accessed on the - // X509Certificate2 objects yielded by `Cert:\…` are the same surface - // the rest of the codebase already relies on (Thumbprint, Subject, - // NotBefore/NotAfter, HasPrivateKey, Extensions). - const script = ` - $ErrorActionPreference = 'Stop' - $oid = '${ASPNET_HTTPS_OID}' - $candidates = @() - $skipped = @() - $certs = Get-ChildItem Cert:\\${this.storeLocation}\\My | Where-Object { - $_.Extensions | Where-Object { $_.Oid.Value -eq $oid } - } - foreach ($cert in $certs) { - $thumb = $cert.Thumbprint - # Subject is a comma-separated RDN string ("CN=localhost, O=..."). Pull - # the first CN out via regex instead of GetNameInfo, which would need - # an explicit [System.Security.Cryptography.X509Certificates.X509NameType] - # type reference. - $cn = $null - if ($cert.Subject -match 'CN=([^,]+)') { $cn = $matches[1].Trim() } - $nbf = $cert.NotBefore.ToUniversalTime().ToString('o') - $exp = $cert.NotAfter.ToUniversalTime().ToString('o') - if (-not $cert.HasPrivateKey) { - $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'no-private-key' } - continue - } - try { - $tmpPfx = Join-Path $env:TEMP ("devcert-" + [guid]::NewGuid().ToString("N") + ".pfx") - $pwd = ConvertTo-SecureString -String 'export' -Force -AsPlainText - # AES256_SHA256 forces a PBES2/AES PFX. The default (TripleDES_SHA1) - # produces a legacy PKCS#12 PBE format that our pkijs-based parser - # deliberately rejects (see cert/pfx.ts). - Export-PfxCertificate -Cert $cert -FilePath $tmpPfx -Password $pwd -CryptoAlgorithmOption AES256_SHA256 | Out-Null - $candidates += @{ thumbprint = $thumb; pfxPath = $tmpPfx; subjectCN = $cn; notBefore = $nbf; notAfter = $exp } - } catch { - # No CNG / RSA-specific introspection here — that would mean - # touching .NET types beyond what the cert provider already - # surfaces. Coarse message-string matching distinguishes the - # "key locked" case from everything else; TS-side maps the code - # to a localized human-readable reason. - $msg = $_.Exception.Message - if ($msg -match 'not exportable' -or $msg -match 'cannot be exported') { - $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'not-exportable' } - } else { - $skipped += @{ thumbprint = $thumb; subjectCN = $cn; notBefore = $nbf; notAfter = $exp; reasonCode = 'export-failed'; reasonDetail = $msg } - } - } - } - $payload = @{ candidates = $candidates; skipped = $skipped } - $payload | ConvertTo-Json -Compress -Depth 4 - `; - - const pwsh = await getPowerShell(); - const result = await runProcess(pwsh, [ - "-NoProfile", - "-NonInteractive", - "-Command", - script, - ]); - - if (result.exitCode !== 0) return null; - - const parsed = parseEnumeration(result.stdout); - if (!parsed) return null; - - const tempPfxPaths: string[] = parsed.candidates.map((c) => c.pfxPath); - try { - const usable: UsableDevCert[] = []; - const storeContext = `Windows ${this.storeLocation}\\My`; - - for (const cand of parsed.candidates) { - const loaded = await this.loadPfxLenient(cand.pfxPath, "export"); - if (!loaded) { - // PFX produced by Export-PfxCertificate failed to parse — surface - // as an unusable warning so the user has visibility. - classifyCandidate({ - kind: "forcedSkip", - source: storeContext, - reason: vscode.l10n.t( - "Export-PfxCertificate produced a PFX that could not be parsed" - ), - metadata: { - thumbprint: cand.thumbprint, - subjectCN: cand.subjectCN, - notBefore: parseDateOrNull(cand.notBefore), - notAfter: parseDateOrNull(cand.notAfter), - }, - }); - continue; - } - - const classified = classifyCandidate({ - kind: "loaded", - source: storeContext, - loaded, - }); - if (classified === null) continue; - if (classified.kind === "usable") usable.push(classified); - } - - for (const sk of parsed.skipped) { - // Re-apply isValidDevCert gates against the metadata we received so - // we don't emit the unusable warning for clearly-unrelated certs - // that happened to share the OID but are e.g. expired. - if (!metadataLooksLikeValidDevCert(sk)) continue; - classifyCandidate({ - kind: "forcedSkip", - source: storeContext, - reason: localizeSkipReason(sk), - metadata: { - thumbprint: sk.thumbprint, - subjectCN: sk.subjectCN, - notBefore: parseDateOrNull(sk.notBefore), - notAfter: parseDateOrNull(sk.notAfter), - }, - }); - } - - return selectBestDevCert(usable, storeContext); - } finally { - for (const p of tempPfxPaths) { - try { - fs.unlinkSync(p); - } catch { - // best effort cleanup - } - } - } - } - - async saveCertificate( - cert: DevCert, - key: DevKey, - _thumbprint: string - ): Promise { - // Export to temp PFX, then import via Import-PfxCertificate. Our - // hand-rolled DER PFX writer (cert/pfx.ts) emits a PFX that CryptoAPI's - // PFXImportCertStore — the function this cmdlet wraps — accepts cleanly. - // Unguessable filename + 0o600 keeps the PFX (with private key) - // unreadable to other local users during the import window. - const tmpPfx = path.join(os.tmpdir(), `devcert-save-${randomUUID()}.pfx`); - await this.writePfx(cert, key, tmpPfx, "import", 0o600); - - const script = - `$ErrorActionPreference = 'Stop'; ` + - `$pwd = ConvertTo-SecureString -String 'import' -Force -AsPlainText; ` + - `Import-PfxCertificate -FilePath '${tmpPfx.replace(/'/g, "''")}' -CertStoreLocation Cert:\\${this.storeLocation}\\My -Password $pwd -Exportable | Out-Null; ` + - `Remove-Item '${tmpPfx.replace(/'/g, "''")}'`; - - const pwsh = await getPowerShell(); - const result = await runProcess(pwsh, [ - "-NoProfile", - "-NonInteractive", - "-Command", - script, - ]); - - if (result.exitCode !== 0) { - // Clean up temp file if PowerShell didn't - try { - fs.unlinkSync(tmpPfx); - } catch { - /* ignore */ - } - throw new Error( - `Failed to save certificate to Windows store: ${result.stderr}` - ); - } - } - - async trustCertificate(cert: DevCert): Promise { - // Use certutil.exe — the built-in Windows CA admin tool from - // %SystemRoot%\System32, present on every Windows install since XP — to - // add the public cert to the configured Root store. - // - // We can't go back to `Import-Certificate` (the PowerShell PKI cmdlet - // PR #36 switched to): when it targets Cert:\CurrentUser\Root the - // underlying CryptoAPI call shows a "You are about to install a - // certificate from a certification authority..." confirmation dialog, - // which fails under `-NonInteractive` with "UI is not allowed in this - // operation." We also intentionally avoid the older path of - // `New-Object System.Security.Cryptography.X509Certificates.X509Store` - // — the host extension is meant to work without taking on a .NET - // dependency. certutil.exe uses CryptoAPI directly, skips the - // confirmation dialog, and is the same tool mkcert and similar dev-cert - // utilities use on Windows for the same reason. - // - // Public-cert only — no private key — but the random name still - // prevents concurrent invocations from colliding on the same temp path. - const tmpCert = path.join(os.tmpdir(), `devcert-trust-${randomUUID()}.cer`); - fs.writeFileSync(tmpCert, certToDer(cert)); - - const args = ["-f"]; - if (this.storeLocation === "CurrentUser") args.push("-user"); - args.push("-addstore", "Root", tmpCert); - - const result = await runProcess("certutil.exe", args); - - try { - fs.unlinkSync(tmpCert); - } catch { - /* best effort */ - } - - if (result.exitCode !== 0) { - throw new Error( - `Failed to trust certificate on Windows: ${result.stderr || result.stdout}` - ); - } - } - - async removeCertificates(): Promise { - const script = ` - $ErrorActionPreference = 'SilentlyContinue' - $oid = '${ASPNET_HTTPS_OID}' - foreach ($storePath in @('Cert:\\${this.storeLocation}\\My', 'Cert:\\${this.storeLocation}\\Root')) { - Get-ChildItem $storePath | Where-Object { - $_.Extensions | Where-Object { $_.Oid.Value -eq $oid } - } | ForEach-Object { - Remove-Item -LiteralPath $_.PSPath -Force - } - } - `; - - const pwsh = await getPowerShell(); - await runProcess(pwsh, [ - "-NoProfile", - "-NonInteractive", - "-Command", - script, - ]); - } - - protected async isTrusted( - _cert: DevCert, - thumbprint: string - ): Promise { - const script = ` - $cert = Get-ChildItem Cert:\\${this.storeLocation}\\Root | Where-Object { $_.Thumbprint -eq '${thumbprint}' } - if ($cert) { Write-Output 'true' } else { Write-Output 'false' } - `; - - const pwsh = await getPowerShell(); - const result = await runProcess(pwsh, [ - "-NoProfile", - "-NonInteractive", - "-Command", - script, - ]); - - return result.stdout.trim() === "true"; - } -} - -function parseEnumeration(stdout: string): PsEnumeration | null { - const trimmed = stdout.trim(); - if (!trimmed) return { candidates: [], skipped: [] }; - - let parsed: unknown; - try { - parsed = JSON.parse(trimmed); - } catch { - return null; - } - - // PowerShell's ConvertTo-Json emits a hashtable with a single child as a - // bare object rather than a 1-element array; coerce defensively. - if (!parsed || typeof parsed !== "object") return null; - const root = parsed as Record; - return { - candidates: coerceArray(root.candidates), - skipped: coerceArray(root.skipped), - }; -} - -function coerceArray(value: unknown): T[] { - if (value === undefined || value === null) return []; - if (Array.isArray(value)) return value as T[]; - return [value as T]; -} - -function parseDateOrNull(s: string | null | undefined): Date | null { - if (!s) return null; - const d = new Date(s); - return Number.isNaN(d.getTime()) ? null : d; -} - -function localizeSkipReason(sk: PsSkipped): string { - switch (sk.reasonCode) { - case "no-private-key": - return vscode.l10n.t("no private key in store"); - case "not-exportable": - return vscode.l10n.t("private key not exportable"); - case "export-failed": - return vscode.l10n.t( - "Export-PfxCertificate failed: {0}", - sk.reasonDetail ?? "" - ); - } -} - -function metadataLooksLikeValidDevCert(sk: PsSkipped): boolean { - // The PS script already filtered by ASPNET_HTTPS_OID. We layer CN + - // validity-window checks on top so we don't emit the unusable warning - // for clearly-unrelated certs (e.g. expired or differently-named) that - // happened to carry the same OID. We can't check the version byte from - // TS without parsing the cert, so we skip that gate here. - // - // CN comparison is intentionally exact-match to mirror isValidDevCert - // (see cert/generator.ts:isValidDevCert) — staying loose here while the - // canonical gate is strict would just produce warnings for certs we'd - // never accept downstream anyway. - if (sk.subjectCN !== "localhost") return false; - const nbf = parseDateOrNull(sk.notBefore); - const exp = parseDateOrNull(sk.notAfter); - if (!nbf || !exp) return false; - const now = new Date(); - if (nbf > now || exp < now) return false; - return true; -} +// Re-export shim: canonical home is `@devcontainer-dev-certs/shared`. +export { WindowsCertificateStore } from "@devcontainer-dev-certs/shared"; +export type { + WindowsStoreLocation, + PsCandidate, + PsSkipped, + PsSkipReason, + PsEnumeration, +} from "@devcontainer-dev-certs/shared"; diff --git a/src/vscode-ui-extension/tests/linuxStore.test.ts b/src/vscode-ui-extension/tests/linuxStore.test.ts index c3b554a..3d31780 100644 --- a/src/vscode-ui-extension/tests/linuxStore.test.ts +++ b/src/vscode-ui-extension/tests/linuxStore.test.ts @@ -11,8 +11,13 @@ import { logMessages } from "./__mocks__/vscode"; initLogger("test"); -// Mock runProcess so tests don't need an actual openssl binary. -vi.mock("../src/platform/processUtil", () => ({ +// Mock runProcess so tests don't need an actual openssl binary. After the +// platform-layer move into the shared package, the LinuxCertificateStore +// imports `runProcess` from the shared internal path (`./processUtil`), so +// the mock target has to be that same shared file — mocking the extension's +// thin re-export shim won't intercept the import the implementation actually +// uses. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ runProcess: vi.fn().mockResolvedValue({ exitCode: 0, stdout: "abcd1234\n", @@ -24,16 +29,18 @@ vi.mock("../src/platform/processUtil", () => ({ // Tests that exercise the NSS step explicitly construct a store with a // reporter; the default tests construct it without one, in which case the // step is skipped entirely and this mock is never invoked. -vi.mock("../src/platform/nssTrust", () => ({ +vi.mock("@devcontainer-dev-certs/shared/src/platform/nssTrust", () => ({ trustInNss: vi.fn(), })); -// Override the shared paths to point at temp directories. +// Override the shared paths to point at temp directories. The +// LinuxCertificateStore imports these from the shared internal `paths` +// module, so the mock has to target that same file. let testStoreDir: string; let testRootStoreDir: string; let testTrustDir: string; -vi.mock("@devcontainer-dev-certs/shared", async (importOriginal) => { +vi.mock("@devcontainer-dev-certs/shared/src/paths", async (importOriginal) => { const original = await importOriginal(); return { ...original, @@ -44,8 +51,8 @@ vi.mock("@devcontainer-dev-certs/shared", async (importOriginal) => { }); import { LinuxCertificateStore } from "../src/platform/linuxStore"; -import { runProcess } from "../src/platform/processUtil"; -import { trustInNss } from "../src/platform/nssTrust"; +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; +import { trustInNss } from "@devcontainer-dev-certs/shared/src/platform/nssTrust"; const mockedRunProcess = vi.mocked(runProcess); const mockedTrustInNss = vi.mocked(trustInNss); diff --git a/src/vscode-ui-extension/tests/macStore.test.ts b/src/vscode-ui-extension/tests/macStore.test.ts index cb6593c..623c730 100644 --- a/src/vscode-ui-extension/tests/macStore.test.ts +++ b/src/vscode-ui-extension/tests/macStore.test.ts @@ -16,12 +16,14 @@ vi.mock("os", async (importOriginal) => { }); // Mock runProcess so tests don't shell out to the real `security` CLI. -vi.mock("../src/platform/processUtil", () => ({ +// Target the shared internal module since MacCertificateStore lives there +// and imports its runProcess via the local `./processUtil`. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ runProcess: vi.fn(), })); import { MacCertificateStore } from "../src/platform/macStore"; -import { runProcess } from "../src/platform/processUtil"; +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; const mockedRunProcess = vi.mocked(runProcess); diff --git a/src/vscode-ui-extension/tests/manager.test.ts b/src/vscode-ui-extension/tests/manager.test.ts index c0c52a4..d8a28f7 100644 --- a/src/vscode-ui-extension/tests/manager.test.ts +++ b/src/vscode-ui-extension/tests/manager.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type * as PlatformTypes from "../src/platform/types"; +import type * as PlatformTypes from "@devcontainer-dev-certs/shared/src/platform/types"; import { type PlatformCertificateStore, type CertificateStatus, @@ -7,8 +7,11 @@ import { import { generateCertificate } from "../src/cert/generator"; import { VALIDITY_DAYS } from "../src/cert/properties"; -// Mock createPlatformStore so the CertManager uses our fake store. -vi.mock("../src/platform/types", async (importOriginal) => { +// Mock createPlatformStore so the CertManager uses our fake store. The +// CertManager now lives in `@devcontainer-dev-certs/shared` and imports +// `createPlatformStore` from the sibling `../platform/types`; the mock has +// to target that exact module path so the shared CertManager sees it too. +vi.mock("@devcontainer-dev-certs/shared/src/platform/types", async (importOriginal) => { const original = await importOriginal(); return { ...original, @@ -17,7 +20,7 @@ vi.mock("../src/platform/types", async (importOriginal) => { }); import { CertManager } from "../src/cert/manager"; -import { createPlatformStore } from "../src/platform/types"; +import { createPlatformStore } from "@devcontainer-dev-certs/shared/src/platform/types"; const mockedCreateStore = vi.mocked(createPlatformStore); diff --git a/src/vscode-ui-extension/tests/nssTrust.test.ts b/src/vscode-ui-extension/tests/nssTrust.test.ts index 88c8d65..fe1b55c 100644 --- a/src/vscode-ui-extension/tests/nssTrust.test.ts +++ b/src/vscode-ui-extension/tests/nssTrust.test.ts @@ -3,8 +3,11 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -// Mock runProcess to simulate certutil and which commands -vi.mock("../src/platform/processUtil", () => ({ +// Mock runProcess to simulate certutil and which commands. Target the +// shared internal module that `trustInNss` actually imports from — mocking +// the extension's re-export shim would leave the implementation calling +// the real runProcess. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ runProcess: vi.fn(), })); @@ -19,7 +22,7 @@ vi.mock("os", async (importOriginal) => { }); import { trustInNss } from "../src/platform/nssTrust"; -import { runProcess } from "../src/platform/processUtil"; +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; const mockedRunProcess = vi.mocked(runProcess); diff --git a/src/vscode-ui-extension/tests/windowsStore.test.ts b/src/vscode-ui-extension/tests/windowsStore.test.ts index 2188592..7d61573 100644 --- a/src/vscode-ui-extension/tests/windowsStore.test.ts +++ b/src/vscode-ui-extension/tests/windowsStore.test.ts @@ -10,7 +10,11 @@ import { buildPfx } from "../src/cert/pfx"; import { type DevCert, type DevKey } from "../src/cert/types"; -vi.mock("../src/platform/processUtil", () => ({ +// Mock runProcess at the shared internal path — WindowsCertificateStore +// lives in `@devcontainer-dev-certs/shared/src/platform/windowsStore` after +// the platform-layer move and imports `runProcess` from the sibling +// `./processUtil`, so the mock must target that file. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ runProcess: vi.fn(), })); @@ -19,7 +23,7 @@ import { type PsCandidate, type PsSkipped, } from "../src/platform/windowsStore"; -import { runProcess } from "../src/platform/processUtil"; +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; const mockedRunProcess = vi.mocked(runProcess); From a55cf4313afee28c5b9dcecf2710859d48936183 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 23:35:41 +0000 Subject: [PATCH 06/41] =?UTF-8?q?Add=20ddc=20=E2=80=94=20a=20host=20CLI=20?= =?UTF-8?q?for=20generating,=20inspecting,=20and=20trusting=20dev=20certs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of carving the host-side cert toolkit out of the UI extension. Introduces a new `@devcontainer-dev-certs/cli` workspace producing a `ddc` binary that wraps the same shared cert / platform layer the VS Code host extension uses, so users on JetBrains / Vim / raw CLI / CI get a first-class generation and bundle-emission flow without needing VS Code to be involved. Commands (commander-driven): - `ddc generate` — pick a backend (auto / native / dotnet, with auto preferring dotnet on macOS when available for its signed-binary keychain-trust UX, otherwise native everywhere), produce a PFX + PEM + key in the out-dir, run the host trust step unless `--no-trust` is passed, and emit `bundle.json` with paths rewritten to a configurable container-mount target (default `/host-dev-certs`) so the in-container installer can read them directly. - `ddc inspect ` — load a PFX or PEM and print subject CN, thumbprints (SHA-1 + SHA-256), validity window, SAN entries (flagged non-local), ASP.NET dev-cert OID + version byte, and validation warnings. Text by default; `--json` switches to a scripting-friendly schema. - `ddc bundle ` — produce a bundle.json referencing an already-existing cert file. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by naming convention. - `ddc trust ` — add an existing cert to the host's OS trust store via the shared platform layer, with an isCertTrusted short-circuit so repeated invocations don't re-prompt. - `ddc doctor` — read-only diagnostics: backend availability, `--backend auto` resolution, out-dir / bundle.json presence, host platform-store state, and (on Linux) `openssl` / `certutil` presence on PATH. The Aspire pass-through backend was deferred per the phasing plan; `--backend aspire` is rejected with a clear "not implemented yet" error so the option stays visible in help text for when it lands. `shared/src/logger.ts` was vscode-free'd to make the CLI bundleable — the previous `initLogger` (which calls `vscode.window.createOutput- Channel`) was extracted into a new `loggerVscode.ts` submodule that only the extensions import; the shared logger now exposes `setLogSink`/`LogSink` so the CLI can plug a stderr-backed sink in via `--verbose`. Extensions and tests updated to import `initLogger` from the submodule path. All checks pass: 207 UI tests, 79 workspace tests, 17 new CLI tests, type-check across all four workspaces, esbuild bundles for the extensions and the CLI, full-repo lint. Smoke-tested `ddc generate / inspect / bundle / doctor` end-to-end against an isolated out-dir and confirmed the produced bundle.json matches the existing `schema/bundle.schema.json`. --- eslint.config.mjs | 1 + package-lock.json | 35 ++++ package.json | 1 + src/cli/esbuild.mjs | 22 +++ src/cli/package.json | 30 +++ src/cli/src/backends/dotnet.ts | 88 +++++++++ src/cli/src/backends/native.ts | 111 +++++++++++ src/cli/src/backends/select.ts | 49 +++++ src/cli/src/backends/types.ts | 48 +++++ src/cli/src/bundle/writer.ts | 111 +++++++++++ src/cli/src/commands/bundle.ts | 95 ++++++++++ src/cli/src/commands/doctor.ts | 150 +++++++++++++++ src/cli/src/commands/generate.ts | 69 +++++++ src/cli/src/commands/inspect.ts | 173 ++++++++++++++++++ src/cli/src/commands/trust.ts | 55 ++++++ src/cli/src/index.ts | 133 ++++++++++++++ src/cli/src/logger.ts | 20 ++ src/cli/tests/select.test.ts | 122 ++++++++++++ src/cli/tests/writer.test.ts | 142 ++++++++++++++ src/cli/tsconfig.json | 19 ++ src/cli/tsconfig.lint.json | 7 + src/cli/vitest.config.ts | 8 + src/cli/vitest.setup.ts | 4 + src/shared/src/index.ts | 3 +- src/shared/src/logger.ts | 42 +++-- src/shared/src/loggerVscode.ts | 17 ++ src/vscode-ui-extension/src/extension.ts | 2 +- .../tests/classifyCandidate.test.ts | 2 +- .../tests/containerCertAccept.test.ts | 2 +- .../tests/linuxStore.test.ts | 2 +- .../tests/macStore.test.ts | 2 +- .../tests/selectBestDevCert.test.ts | 2 +- .../tests/windowsStore.test.ts | 2 +- .../src/extension.ts | 3 +- .../tests/containerCertPush.test.ts | 2 +- 35 files changed, 1550 insertions(+), 24 deletions(-) create mode 100644 src/cli/esbuild.mjs create mode 100644 src/cli/package.json create mode 100644 src/cli/src/backends/dotnet.ts create mode 100644 src/cli/src/backends/native.ts create mode 100644 src/cli/src/backends/select.ts create mode 100644 src/cli/src/backends/types.ts create mode 100644 src/cli/src/bundle/writer.ts create mode 100644 src/cli/src/commands/bundle.ts create mode 100644 src/cli/src/commands/doctor.ts create mode 100644 src/cli/src/commands/generate.ts create mode 100644 src/cli/src/commands/inspect.ts create mode 100644 src/cli/src/commands/trust.ts create mode 100644 src/cli/src/index.ts create mode 100644 src/cli/src/logger.ts create mode 100644 src/cli/tests/select.test.ts create mode 100644 src/cli/tests/writer.test.ts create mode 100644 src/cli/tsconfig.json create mode 100644 src/cli/tsconfig.lint.json create mode 100644 src/cli/vitest.config.ts create mode 100644 src/cli/vitest.setup.ts create mode 100644 src/shared/src/loggerVscode.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 387ec5d..c1d4c81 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,6 +41,7 @@ export default tseslint.config( parserOptions: { project: [ "./src/shared/tsconfig.json", + "./src/cli/tsconfig.lint.json", "./src/vscode-ui-extension/tsconfig.lint.json", "./src/vscode-workspace-extension/tsconfig.lint.json", ], diff --git a/package-lock.json b/package-lock.json index f679cfb..55c39c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "devcontainer-dev-certs", "workspaces": [ "src/shared", + "src/cli", "src/vscode-ui-extension", "src/vscode-workspace-extension" ], @@ -238,6 +239,10 @@ "node": ">=6.9.0" } }, + "node_modules/@devcontainer-dev-certs/cli": { + "resolved": "src/cli", + "link": true + }, "node_modules/@devcontainer-dev-certs/shared": { "resolved": "src/shared", "link": true @@ -7062,6 +7067,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "src/cli": { + "name": "@devcontainer-dev-certs/cli", + "version": "1.3.1-pre", + "dependencies": { + "@devcontainer-dev-certs/shared": "*", + "@peculiar/x509": "^2.0.0", + "asn1js": "^3.0.10", + "commander": "^14.0.1", + "pkijs": "^3.4.0", + "reflect-metadata": "^0.2.2" + }, + "bin": { + "ddc": "dist/ddc.js" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.28.0", + "typescript": "^6.0.3", + "vitest": "^4.1.5" + } + }, + "src/cli/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "src/shared": { "name": "@devcontainer-dev-certs/shared", "version": "1.3.1-pre", diff --git a/package.json b/package.json index daa3c22..274bd42 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "workspaces": [ "src/shared", + "src/cli", "src/vscode-ui-extension", "src/vscode-workspace-extension" ], diff --git a/src/cli/esbuild.mjs b/src/cli/esbuild.mjs new file mode 100644 index 0000000..526a9da --- /dev/null +++ b/src/cli/esbuild.mjs @@ -0,0 +1,22 @@ +import * as esbuild from "esbuild"; + +const production = process.argv.includes("--production"); + +await esbuild.build({ + entryPoints: ["src/index.ts"], + bundle: true, + outfile: "dist/ddc.js", + // `vscode` is a build-time stub used only by the shared package's + // `loggerVscode.ts` helper, which the CLI never imports. Marking it + // external prevents esbuild from trying to resolve a module that + // doesn't exist outside the VS Code extension host. + external: ["vscode"], + format: "cjs", + platform: "node", + target: "node18", + sourcemap: !production, + minify: production, + banner: { + js: "#!/usr/bin/env node\n", + }, +}); diff --git a/src/cli/package.json b/src/cli/package.json new file mode 100644 index 0000000..0c6c544 --- /dev/null +++ b/src/cli/package.json @@ -0,0 +1,30 @@ +{ + "name": "@devcontainer-dev-certs/cli", + "version": "1.3.1-pre", + "private": true, + "description": "ddc — host-side CLI for generating, inspecting, and trusting dev certs without VS Code.", + "main": "./dist/ddc.js", + "bin": { + "ddc": "./dist/ddc.js" + }, + "scripts": { + "build": "node esbuild.mjs", + "build:prod": "node esbuild.mjs --production", + "test": "vitest run", + "lint": "eslint src tests" + }, + "dependencies": { + "@devcontainer-dev-certs/shared": "*", + "@peculiar/x509": "^2.0.0", + "asn1js": "^3.0.10", + "commander": "^14.0.1", + "pkijs": "^3.4.0", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "esbuild": "^0.28.0", + "typescript": "^6.0.3", + "vitest": "^4.1.5" + } +} diff --git a/src/cli/src/backends/dotnet.ts b/src/cli/src/backends/dotnet.ts new file mode 100644 index 0000000..3dbd81b --- /dev/null +++ b/src/cli/src/backends/dotnet.ts @@ -0,0 +1,88 @@ +import * as fs from "fs"; +import * as path from "path"; +import { + loadPfx, + runProcess, +} from "@devcontainer-dev-certs/shared"; +import type { Backend, GenerateOptions, GenerateResult } from "./types"; + +/** + * Dotnet backend: shells out to `dotnet dev-certs https`. On macOS this is + * the canonical way to get a signed-binary-attributed keychain trust prompt + * — the host extension's `security add-trusted-cert` flow works but has a + * less polished UX because the calling binary isn't a notarized Apple + * cert-management tool. On Windows / Linux the dotnet backend is equivalent + * to the native backend modulo cert format differences. + * + * Two-pass: one invocation exports PFX, a second exports PEM. We pass + * `--trust` only on the first invocation (the second pass would re-trust + * the same cert and add nothing). + */ +export class DotnetBackend implements Backend { + readonly kind = "dotnet" as const; + + async isAvailable(): Promise { + const result = await runProcess("dotnet", ["--version"], 5000); + return result.exitCode === 0; + } + + async generate(options: GenerateOptions): Promise { + fs.mkdirSync(options.outDir, { recursive: true }); + + const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); + const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); + // dotnet dev-certs --format PEM writes both `` (cert) and + // `.key` (key) when invoked without --no-password and without + // `-ep`. We rely on that companion key file to populate pemKeyPath. + const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.pem.key"); + + // First pass: export PFX, trust (unless --no-trust). + const pfxArgs = ["dev-certs", "https"]; + if (!options.noTrust) pfxArgs.push("--trust"); + pfxArgs.push("--format", "Pfx", "--no-password", "--export-path", pfxPath); + + const pfxResult = await runProcess("dotnet", pfxArgs, 60_000); + if (pfxResult.exitCode !== 0) { + throw new Error( + `dotnet dev-certs (PFX pass) failed (exit ${pfxResult.exitCode}): ${pfxResult.stderr || pfxResult.stdout}` + ); + } + + // Second pass: export PEM. No --trust here — already done above. + const pemResult = await runProcess( + "dotnet", + [ + "dev-certs", + "https", + "--format", + "PEM", + "--no-password", + "--export-path", + pemPath, + ], + 60_000 + ); + if (pemResult.exitCode !== 0) { + throw new Error( + `dotnet dev-certs (PEM pass) failed (exit ${pemResult.exitCode}): ${pemResult.stderr || pemResult.stdout}` + ); + } + + // Recover the thumbprint from the PFX we just wrote. + const loaded = await loadPfx(pfxPath); + if (!loaded || !loaded.cert) { + throw new Error( + `dotnet wrote ${pfxPath} but the resulting PFX could not be parsed.` + ); + } + + return { + pfxPath, + pemPath, + pemKeyPath: fs.existsSync(pemKeyPath) ? pemKeyPath : null, + thumbprint: loaded.cert.thumbprintSha1, + trusted: !options.noTrust, + backendUsed: "dotnet", + }; + } +} diff --git a/src/cli/src/backends/native.ts b/src/cli/src/backends/native.ts new file mode 100644 index 0000000..4ec4e31 --- /dev/null +++ b/src/cli/src/backends/native.ts @@ -0,0 +1,111 @@ +import * as fs from "fs"; +import * as path from "path"; +import { + CertManager, + VALIDITY_DAYS, + generateCertificate, + exportPfx, + exportPem, + loadPfx, + buildPfx, + type DevCert, + type DevKey, +} from "@devcontainer-dev-certs/shared"; +import type { Backend, GenerateOptions, GenerateResult } from "./types"; + +/** + * Native backend: uses the shared `CertManager` directly. Same code path + * the VS Code host extension uses for generation, host trust, and + * platform-store I/O — no shelling out to other tools. + */ +export class NativeBackend implements Backend { + readonly kind = "native" as const; + + isAvailable(): Promise { + // The native backend is always available — the shared layer ships + // implementations for all three supported platforms (Linux/macOS/Windows) + // and the cert primitives themselves have no external runtime + // dependencies. + return Promise.resolve(true); + } + + async generate(options: GenerateOptions): Promise { + fs.mkdirSync(options.outDir, { recursive: true }); + + const manager = new CertManager(); + + // Drive the manager through the same generate+trust flow the host + // extension uses, then export the live cert to the out-dir. Trust is + // skipped when --no-trust is passed. + if (options.noTrust) { + // Generate-only: produce a cert, save it to the platform store, but + // don't trust. The manager exposes `generate()` separately for this. + await manager.generate(false); + } else { + await manager.trust(); + } + + // Export current cert. The manager doesn't expose the live cert/key + // directly; export it through the manager's `exportCert` which writes + // the canonical filenames. We follow up by reading the PFX back to + // recover the thumbprint — the manager will have loaded the same cert + // from the platform store so the bytes match. + await manager.exportCert("pfx", options.outDir); + await manager.exportCert("pem", options.outDir); + + const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); + const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); + const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.key"); + + const loaded = await loadPfx(pfxPath); + if (!loaded || !loaded.cert) { + throw new Error( + `Native backend export wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` + ); + } + + return { + pfxPath, + pemPath, + pemKeyPath, + thumbprint: loaded.cert.thumbprintSha1, + trusted: !options.noTrust, + backendUsed: "native", + }; + } +} + +/** + * Generate a brand-new cert in memory (no platform-store interaction) and + * write it to the given out-dir. Used by `ddc generate` when the user opts + * out of the manager flow and just wants files on disk; also useful as a + * lower-level building block for tests. + * + * Exposed alongside `NativeBackend` because some flows (e.g. a future + * `ddc bundle --regen`) want the artifacts without trust as a side effect. + */ +export async function generateAndWriteFiles( + outDir: string +): Promise<{ + cert: DevCert; + key: DevKey; + thumbprint: string; + pfxPath: string; + pemPath: string; + pemKeyPath: string; + rootPfxPath: string; +}> { + fs.mkdirSync(outDir, { recursive: true }); + + const now = new Date(); + const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); + const { cert, key, thumbprint } = await generateCertificate(now, expiry); + + const pfxPath = await exportPfx(cert, key, outDir); + const { certPath: pemPath, keyPath: pemKeyPath } = exportPem(cert, key, outDir); + + const rootPfxPath = path.join(outDir, "aspnetcore-dev-root.pfx"); + fs.writeFileSync(rootPfxPath, await buildPfx({ cert }), { mode: 0o644 }); + + return { cert, key, thumbprint, pfxPath, pemPath, pemKeyPath, rootPfxPath }; +} diff --git a/src/cli/src/backends/select.ts b/src/cli/src/backends/select.ts new file mode 100644 index 0000000..0d9bf8b --- /dev/null +++ b/src/cli/src/backends/select.ts @@ -0,0 +1,49 @@ +import { DotnetBackend } from "./dotnet"; +import { NativeBackend } from "./native"; +import type { Backend, BackendKind, BackendMode } from "./types"; + +/** + * Resolve a `--backend` choice (possibly `auto`) into a concrete backend + * instance. `auto` prefers `dotnet` on macOS when the `dotnet` CLI is on + * PATH (better keychain-trust UX via a signed binary), `native` everywhere + * else. + */ +export async function selectBackend(mode: BackendMode): Promise { + if (mode === "native") return new NativeBackend(); + if (mode === "dotnet") { + const backend = new DotnetBackend(); + if (!(await backend.isAvailable())) { + throw new Error( + "Requested --backend dotnet but the `dotnet` CLI was not found on PATH." + ); + } + return backend; + } + if (mode === "aspire") { + throw new Error( + "--backend aspire is not implemented yet. Use 'native' or 'dotnet'." + ); + } + if (mode === "auto") return autoSelect(); + throw new Error(`Unknown backend mode: ${String(mode)}`); +} + +async function autoSelect(): Promise { + if (process.platform === "darwin") { + const dotnet = new DotnetBackend(); + if (await dotnet.isAvailable()) return dotnet; + } + return new NativeBackend(); +} + +/** + * Report which backend `auto` would pick on this host without actually + * constructing it. Useful for `ddc doctor`. + */ +export async function describeAutoBackend(): Promise { + if (process.platform === "darwin") { + const dotnet = new DotnetBackend(); + if (await dotnet.isAvailable()) return "dotnet"; + } + return "native"; +} diff --git a/src/cli/src/backends/types.ts b/src/cli/src/backends/types.ts new file mode 100644 index 0000000..2d403a5 --- /dev/null +++ b/src/cli/src/backends/types.ts @@ -0,0 +1,48 @@ +/** + * One of the three concrete backends `ddc` can use to generate (and trust) + * a dev cert. `auto` is resolved at command-dispatch time into one of the + * concrete kinds. + */ +export type BackendKind = "native" | "dotnet" | "aspire"; + +export type BackendMode = BackendKind | "auto"; + +export interface GenerateOptions { + /** Directory that receives PFX / PEM / bundle.json artifacts. */ + outDir: string; + /** Skip the host trust step. PFX / PEM are still emitted. */ + noTrust: boolean; + /** + * Container-side path the host out-dir maps to via a Docker mount — + * recorded into bundle.json's `pfxPath` / `pemPath` so the in-container + * installer reads from the right place. Defaults to `/host-dev-certs`. + */ + containerMount: string; +} + +export interface GenerateResult { + /** Absolute host path of the PFX. */ + pfxPath: string; + /** Absolute host path of the PEM cert. */ + pemPath: string; + /** Absolute host path of the PEM key (null when backend didn't emit one). */ + pemKeyPath: string | null; + /** SHA-1 thumbprint, uppercase hex. */ + thumbprint: string; + /** Whether the host trust step ran and succeeded. */ + trusted: boolean; + /** Backend that actually produced the cert (auto resolves to one of these). */ + backendUsed: BackendKind; +} + +/** + * Backends implement generation (+ optional trust) and platform-availability + * detection. `auto` selection asks each candidate whether it's available and + * picks per platform preference. + */ +export interface Backend { + readonly kind: BackendKind; + /** Is this backend usable on the current host? */ + isAvailable(): Promise; + generate(options: GenerateOptions): Promise; +} diff --git a/src/cli/src/bundle/writer.ts b/src/cli/src/bundle/writer.ts new file mode 100644 index 0000000..2f32933 --- /dev/null +++ b/src/cli/src/bundle/writer.ts @@ -0,0 +1,111 @@ +import * as fs from "fs"; +import * as path from "path"; + +/** + * Schema URL the manual-setup example references. Including `$schema` in the + * generated bundle.json lets editors like VS Code (and language servers like + * jsonls / coc-jsonls) pick up validation + autocomplete automatically. + */ +export const BUNDLE_SCHEMA_URL = + "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json"; + +export interface BundleCertEntry { + /** Filename stem (e.g. `aspnetcore-dev`). */ + name: string; + /** SHA-1 thumbprint, uppercase hex, no separators. */ + thumbprint: string; + /** `dotnet-dev` (auto-generated) or `user` (user-supplied). */ + kind: "dotnet-dev" | "user"; + /** Host filesystem absolute path to the PFX, or null if not produced. */ + hostPfxPath: string | null; + /** Host filesystem absolute path to the PEM cert. */ + hostPemPath: string; + /** Host filesystem absolute path to the PEM key, or null if not produced. */ + hostPemKeyPath: string | null; + /** + * Whether the in-container installer should plant this cert into the OS + * trust store (CA bundle + .NET root) inside the container. `true` for + * default `dotnet-dev` certs. + */ + trustInContainer: boolean; +} + +export interface WriteBundleOptions { + /** Absolute path to the host out-dir holding the cert files. */ + hostOutDir: string; + /** + * Container-side path the host out-dir bind-mounts to (e.g. + * `/host-dev-certs`). Bundle paths are rewritten to this prefix because the + * in-container installer is what reads bundle.json — not anything on the + * host. + */ + containerMount: string; + entries: BundleCertEntry[]; + /** + * Extra destinations to write into bundle.json — directories inside the + * container that the installer will additionally drop artifacts into + * (e.g. `/etc/nginx/certs`). Optional; mirrors the existing schema field. + */ + extraDestinations?: Array<{ path: string; format?: string }>; +} + +/** + * Write `bundle.json` into the host out-dir. The on-disk JSON references + * the *container-side* paths (mount target + filename), because that file is + * consumed by the in-container `devcontainer-dev-certs-install` script — not + * by anything that sees the host filesystem. + */ +export function writeBundle(options: WriteBundleOptions): string { + const bundlePath = path.join(options.hostOutDir, "bundle.json"); + + const certs = options.entries.map((entry) => { + const obj: Record = { + name: entry.name, + kind: entry.kind, + thumbprint: entry.thumbprint, + pemPath: containerize(entry.hostPemPath, options), + trustInContainer: entry.trustInContainer, + }; + if (entry.hostPfxPath) { + obj.pfxPath = containerize(entry.hostPfxPath, options); + } + if (entry.hostPemKeyPath) { + obj.pemKeyPath = containerize(entry.hostPemKeyPath, options); + } + return obj; + }); + + const bundle: Record = { + $schema: BUNDLE_SCHEMA_URL, + certs, + }; + if (options.extraDestinations && options.extraDestinations.length > 0) { + bundle.extraDestinations = options.extraDestinations; + } + + fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + "\n", { + mode: 0o644, + }); + return bundlePath; +} + +/** + * Translate a host-filesystem absolute path under `hostOutDir` into the + * equivalent container-mount path. Paths outside the out-dir pass through + * unchanged — the user may have crafted bundle entries that point at + * already-in-container locations. + */ +function containerize(hostPath: string, options: WriteBundleOptions): string { + const resolved = path.resolve(hostPath); + const baseResolved = path.resolve(options.hostOutDir); + if (resolved.startsWith(baseResolved + path.sep) || resolved === baseResolved) { + const rel = path.relative(baseResolved, resolved); + if (!rel) return options.containerMount; + // Forward slashes always — the container is always POSIX even when the + // host is Windows. + const posixRel = rel.split(path.sep).join("/"); + const mount = options.containerMount.replace(/\/+$/, ""); + return `${mount}/${posixRel}`; + } + return hostPath; +} diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts new file mode 100644 index 0000000..60c4a2a --- /dev/null +++ b/src/cli/src/commands/bundle.ts @@ -0,0 +1,95 @@ +import * as fs from "fs"; +import * as path from "path"; +import { loadPfx, loadPemPair } from "@devcontainer-dev-certs/shared"; +import { + writeBundle, + type BundleCertEntry, +} from "../bundle/writer"; + +export interface BundleCommandOptions { + outDir?: string; + containerMount?: string; + name?: string; + kind?: "dotnet-dev" | "user"; + noTrustInContainer?: boolean; +} + +const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; + +/** + * `ddc bundle ` — emit a `bundle.json` referencing an + * already-existing cert file. Useful when the cert was generated by + * something else (e.g. `dotnet dev-certs` invoked manually, or a corporate + * CA bundle) and the user just needs the wrapping bundle for the in-container + * installer to consume. + */ +export async function runBundle( + certPath: string, + options: BundleCommandOptions +): Promise { + if (!fs.existsSync(certPath)) { + throw new Error(`File not found: ${certPath}`); + } + + const resolvedCertPath = path.resolve(certPath); + const outDir = path.resolve(options.outDir ?? path.dirname(resolvedCertPath)); + const containerMount = options.containerMount ?? DEFAULT_CONTAINER_MOUNT; + const name = options.name ?? path.basename(resolvedCertPath, path.extname(resolvedCertPath)); + const kind = options.kind ?? "user"; + + const ext = resolvedCertPath.toLowerCase(); + let hostPfxPath: string | null = null; + let hostPemPath: string; + let hostPemKeyPath: string | null = null; + let thumbprint: string; + + const stem = path.join( + path.dirname(resolvedCertPath), + path.basename(resolvedCertPath, path.extname(resolvedCertPath)) + ); + + if (ext.endsWith(".pfx") || ext.endsWith(".p12")) { + hostPfxPath = resolvedCertPath; + const loaded = await loadPfx(resolvedCertPath); + thumbprint = loaded.cert.thumbprintSha1; + // Look for sibling PEM files using common naming conventions. + const candidatePem = `${stem}.pem`; + const candidateKey = `${stem}.key`; + if (fs.existsSync(candidatePem)) { + hostPemPath = candidatePem; + } else { + throw new Error( + `Bundle requires a PEM cert next to the PFX. Looked for ${candidatePem}.` + ); + } + if (fs.existsSync(candidateKey)) hostPemKeyPath = candidateKey; + } else { + hostPemPath = resolvedCertPath; + const candidateKey = `${stem}.key`; + const keyPath = fs.existsSync(candidateKey) ? candidateKey : null; + const loaded = loadPemPair(resolvedCertPath, keyPath); + thumbprint = loaded.cert.thumbprintSha1; + if (keyPath) hostPemKeyPath = candidateKey; + // Look for sibling PFX too. + if (fs.existsSync(`${stem}.pfx`)) hostPfxPath = `${stem}.pfx`; + } + + const entry: BundleCertEntry = { + name, + kind, + thumbprint, + hostPfxPath, + hostPemPath, + hostPemKeyPath, + trustInContainer: !options.noTrustInContainer, + }; + + fs.mkdirSync(outDir, { recursive: true }); + const bundlePath = writeBundle({ + hostOutDir: outDir, + containerMount, + entries: [entry], + }); + process.stderr.write(`Bundle: ${bundlePath}\n`); + process.stderr.write(`Thumbprint: ${thumbprint}\n`); +} diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts new file mode 100644 index 0000000..cc9c7b3 --- /dev/null +++ b/src/cli/src/commands/doctor.ts @@ -0,0 +1,150 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { + createPlatformStore, + runProcess, +} from "@devcontainer-dev-certs/shared"; +import { DotnetBackend } from "../backends/dotnet"; +import { describeAutoBackend } from "../backends/select"; +import { installCliLogger } from "../logger"; + +export interface DoctorCommandOptions { + outDir?: string; + verbose?: boolean; +} + +const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); + +interface Check { + label: string; + status: "ok" | "warn" | "fail"; + detail: string; +} + +/** + * `ddc doctor` — read-only diagnostics: which backends are available, what + * `--backend auto` would pick, host trust-store state for the cert (if any) + * in the out-dir. Mirrors the in-container `devcontainer-dev-certs-install + * --doctor` ergonomics: every check produces an `[ok]` / `[warn]` / `[fail]` + * line. + */ +export async function runDoctor( + options: DoctorCommandOptions +): Promise { + installCliLogger(Boolean(options.verbose)); + + const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); + const checks: Check[] = []; + + // Backend availability. + const dotnetAvailable = await new DotnetBackend().isAvailable(); + checks.push({ + label: "dotnet CLI on PATH", + status: dotnetAvailable ? "ok" : "warn", + detail: dotnetAvailable + ? "found" + : "not found (the 'dotnet' backend is unavailable; native backend will be used)", + }); + + const auto = await describeAutoBackend(); + checks.push({ + label: "--backend auto would pick", + status: "ok", + detail: auto, + }); + + // Out-dir presence. + if (fs.existsSync(outDir)) { + checks.push({ + label: `out-dir ${outDir}`, + status: "ok", + detail: "exists", + }); + } else { + checks.push({ + label: `out-dir ${outDir}`, + status: "warn", + detail: "does not exist (run `ddc generate` to create it)", + }); + } + + // Bundle.json presence. + const bundlePath = path.join(outDir, "bundle.json"); + if (fs.existsSync(bundlePath)) { + checks.push({ + label: `bundle.json at ${bundlePath}`, + status: "ok", + detail: "found", + }); + } else { + checks.push({ + label: `bundle.json at ${bundlePath}`, + status: "warn", + detail: "not found", + }); + } + + // Platform store state. + try { + const store = await createPlatformStore(); + const status = await store.checkStatus(); + if (status.exists) { + checks.push({ + label: "Host platform store has a valid dev cert", + status: status.isTrusted ? "ok" : "warn", + detail: status.isTrusted + ? `trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})` + : `present but NOT trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})`, + }); + } else { + checks.push({ + label: "Host platform store has a valid dev cert", + status: "warn", + detail: "no dev cert found in host platform store", + }); + } + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + checks.push({ + label: "Host platform store check", + status: "fail", + detail: message, + }); + } + + // Required tools for the native backend on Linux. + if (process.platform === "linux") { + const openssl = await runProcess("which", ["openssl"]); + checks.push({ + label: "openssl on PATH (Linux native trust)", + status: openssl.exitCode === 0 ? "ok" : "warn", + detail: openssl.exitCode === 0 ? openssl.stdout.trim() : "not found", + }); + const certutil = await runProcess("which", ["certutil"]); + checks.push({ + label: "certutil on PATH (Linux NSS browser trust)", + status: certutil.exitCode === 0 ? "ok" : "warn", + detail: + certutil.exitCode === 0 + ? certutil.stdout.trim() + : "not found (Chromium/Firefox won't auto-trust; install libnss3-tools / nss-tools)", + }); + } + + // Print summary. + let failures = 0; + let warnings = 0; + for (const c of checks) { + process.stdout.write(`[${c.status}] ${c.label}: ${c.detail}\n`); + if (c.status === "fail") failures++; + else if (c.status === "warn") warnings++; + } + process.stdout.write( + `\n${checks.length} check(s) total — ${failures} fail, ${warnings} warn.\n` + ); + + if (failures > 0) { + process.exitCode = 1; + } +} diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts new file mode 100644 index 0000000..ac3f9c1 --- /dev/null +++ b/src/cli/src/commands/generate.ts @@ -0,0 +1,69 @@ +import * as os from "os"; +import * as path from "path"; +import { selectBackend } from "../backends/select"; +import type { BackendMode } from "../backends/types"; +import { writeBundle, type BundleCertEntry } from "../bundle/writer"; +import { installCliLogger } from "../logger"; + +export interface GenerateCommandOptions { + outDir?: string; + backend?: BackendMode; + noTrust?: boolean; + containerMount?: string; + noBundle?: boolean; + verbose?: boolean; +} + +const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); +const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; + +/** + * `ddc generate` — produce a fresh dev cert + bundle.json. Picks a backend + * (native by default, dotnet pass-through on macOS when available, with + * `--backend` to override) and runs the host trust step unless `--no-trust` + * is passed. + */ +export async function runGenerate( + options: GenerateCommandOptions +): Promise { + installCliLogger(Boolean(options.verbose)); + + const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); + const backend = await selectBackend(options.backend ?? "auto"); + const containerMount = options.containerMount ?? DEFAULT_CONTAINER_MOUNT; + + process.stderr.write(`Backend: ${backend.kind}\n`); + process.stderr.write(`Out dir: ${outDir}\n`); + + const result = await backend.generate({ + outDir, + noTrust: Boolean(options.noTrust), + containerMount, + }); + + process.stderr.write( + `Thumbprint: ${result.thumbprint}\n` + + `PFX: ${result.pfxPath}\n` + + `PEM: ${result.pemPath}\n` + + (result.pemKeyPath ? `Key: ${result.pemKeyPath}\n` : "") + + `Trusted on host: ${result.trusted ? "yes" : "no (skipped via --no-trust)"}\n` + ); + + if (!options.noBundle) { + const entry: BundleCertEntry = { + name: "aspnetcore-dev", + kind: "dotnet-dev", + thumbprint: result.thumbprint, + hostPfxPath: result.pfxPath, + hostPemPath: result.pemPath, + hostPemKeyPath: result.pemKeyPath, + trustInContainer: true, + }; + const bundlePath = writeBundle({ + hostOutDir: outDir, + containerMount, + entries: [entry], + }); + process.stderr.write(`Bundle: ${bundlePath}\n`); + } +} diff --git a/src/cli/src/commands/inspect.ts b/src/cli/src/commands/inspect.ts new file mode 100644 index 0000000..653774a --- /dev/null +++ b/src/cli/src/commands/inspect.ts @@ -0,0 +1,173 @@ +import * as fs from "fs"; +import { + type DevCert, + ASPNET_HTTPS_OID, + CURRENT_CERTIFICATE_VERSION, + MINIMUM_CERTIFICATE_VERSION, + getCertificateVersion, + isValidDevCert, + loadPfx, + loadPemPair, + validateLocalSans, + collectSanEntries, +} from "@devcontainer-dev-certs/shared"; + +export interface InspectCommandOptions { + json?: boolean; +} + +interface InspectReport { + path: string; + format: "pfx" | "pem"; + subjectCN: string | null; + thumbprintSha1: string; + thumbprintSha256: string; + notBefore: string; + notAfter: string; + expiresInDays: number; + hasPrivateKey: boolean; + devCertOidPresent: boolean; + devCertVersion: number | null; + isValidDevCert: boolean; + sans: Array<{ type: string; value: string }>; + nonLocalSans: Array<{ type: string; value: string }>; + warnings: string[]; +} + +/** + * `ddc inspect ` — load a PFX or PEM (cert-only) and report its + * vital statistics. Text by default; `--json` switches to machine-readable. + */ +export async function runInspect( + certPath: string, + options: InspectCommandOptions +): Promise { + if (!fs.existsSync(certPath)) { + throw new Error(`File not found: ${certPath}`); + } + + const report = await buildReport(certPath); + if (options.json) { + process.stdout.write(JSON.stringify(report, null, 2) + "\n"); + } else { + process.stdout.write(formatTextReport(report)); + } +} + +async function buildReport(certPath: string): Promise { + const ext = certPath.toLowerCase(); + const warnings: string[] = []; + + let cert: DevCert; + let hasPrivateKey: boolean; + let format: "pfx" | "pem"; + + if (ext.endsWith(".pfx") || ext.endsWith(".p12")) { + format = "pfx"; + const loaded = await loadPfx(certPath); + cert = loaded.cert; + hasPrivateKey = loaded.key !== null; + } else { + format = "pem"; + // PEM inspection: opportunistically look for a sibling key by the + // conventional naming (`stem.key` next to `stem.pem`); if absent, treat + // it as a cert-only PEM. + const stem = certPath.replace(/\.[^.]+$/, ""); + const candidateKeyPath = `${stem}.key`; + const keyPath = fs.existsSync(candidateKeyPath) ? candidateKeyPath : null; + const loaded = loadPemPair(certPath, keyPath); + cert = loaded.cert; + hasPrivateKey = loaded.key !== null; + } + + const devCertOidPresent = cert.hasExtension(ASPNET_HTTPS_OID); + const devCertVersion = devCertOidPresent ? getCertificateVersion(cert) : null; + const sansAll = collectSanEntries(cert).map((entry) => ({ + type: entry.type, + value: entry.value, + })); + const localCheck = validateLocalSans(cert); + + if (devCertOidPresent && devCertVersion !== null) { + if (devCertVersion < MINIMUM_CERTIFICATE_VERSION) { + warnings.push( + `Dev-cert version byte ${devCertVersion} is below the minimum (${MINIMUM_CERTIFICATE_VERSION}). Regenerate with a current dotnet SDK.` + ); + } + if (devCertVersion > CURRENT_CERTIFICATE_VERSION) { + warnings.push( + `Dev-cert version byte ${devCertVersion} is newer than this build expects (${CURRENT_CERTIFICATE_VERSION}). The cert may use features we don't know about.` + ); + } + } + if (!hasPrivateKey && format === "pfx") { + warnings.push("PFX contains no private key — Kestrel will not be able to serve TLS from this file."); + } + if (localCheck.nonLocalEntries.length > 0) { + warnings.push( + `${localCheck.nonLocalEntries.length} non-local SAN entr${localCheck.nonLocalEntries.length === 1 ? "y" : "ies"} present — this cert grants TLS to names beyond the developer machine.` + ); + } + + const now = Date.now(); + const expiresInDays = Math.floor( + (cert.notAfter.getTime() - now) / 86400_000 + ); + + return { + path: certPath, + format, + subjectCN: cert.subjectCN ?? null, + thumbprintSha1: cert.thumbprintSha1, + thumbprintSha256: cert.thumbprint, + notBefore: cert.notBefore.toISOString(), + notAfter: cert.notAfter.toISOString(), + expiresInDays, + hasPrivateKey, + devCertOidPresent, + devCertVersion, + isValidDevCert: isValidDevCert(cert), + sans: sansAll, + nonLocalSans: localCheck.nonLocalEntries.map((entry) => ({ + type: entry.type, + value: entry.value, + })), + warnings, + }; +} + +function formatTextReport(report: InspectReport): string { + const lines: string[] = []; + lines.push(`File: ${report.path}`); + lines.push(`Format: ${report.format.toUpperCase()}`); + lines.push(`Subject CN: ${report.subjectCN ?? "(none)"}`); + lines.push(`Thumbprint (SHA-1): ${report.thumbprintSha1}`); + lines.push(`Thumbprint (SHA-256): ${report.thumbprintSha256}`); + lines.push(`Valid from: ${report.notBefore}`); + lines.push(`Valid until: ${report.notAfter}`); + lines.push(`Expires in: ${report.expiresInDays} day(s)`); + lines.push(`Has private key: ${report.hasPrivateKey ? "yes" : "no"}`); + lines.push(`ASP.NET dev-cert OID: ${report.devCertOidPresent ? "yes" : "no"}`); + if (report.devCertVersion !== null) { + lines.push(` Version byte: ${report.devCertVersion}`); + } + lines.push(`Valid as dev cert: ${report.isValidDevCert ? "yes" : "no"}`); + lines.push(`SANs:`); + if (report.sans.length === 0) { + lines.push(` (none)`); + } else { + for (const san of report.sans) { + const flag = report.nonLocalSans.some( + (n) => n.type === san.type && n.value === san.value + ) + ? " [non-local]" + : ""; + lines.push(` ${san.type}:${san.value}${flag}`); + } + } + if (report.warnings.length > 0) { + lines.push(`Warnings:`); + for (const w of report.warnings) lines.push(` - ${w}`); + } + return lines.join("\n") + "\n"; +} diff --git a/src/cli/src/commands/trust.ts b/src/cli/src/commands/trust.ts new file mode 100644 index 0000000..e986ff2 --- /dev/null +++ b/src/cli/src/commands/trust.ts @@ -0,0 +1,55 @@ +import * as fs from "fs"; +import { + createPlatformStore, + loadPfx, + loadPemPair, + type DevCert, +} from "@devcontainer-dev-certs/shared"; +import { installCliLogger } from "../logger"; + +export interface TrustCommandOptions { + verbose?: boolean; +} + +/** + * `ddc trust ` — add an existing cert to the host's OS trust + * store. Useful when the user already has a cert (generated elsewhere) and + * just needs the host trust step. Goes through the shared + * `PlatformCertificateStore.trustCertificate` — same hook the host + * extension uses. + */ +export async function runTrust( + certPath: string, + options: TrustCommandOptions +): Promise { + installCliLogger(Boolean(options.verbose)); + + if (!fs.existsSync(certPath)) { + throw new Error(`File not found: ${certPath}`); + } + + let cert: DevCert; + const lower = certPath.toLowerCase(); + if (lower.endsWith(".pfx") || lower.endsWith(".p12")) { + const loaded = await loadPfx(certPath); + cert = loaded.cert; + } else { + const loaded = loadPemPair(certPath); + cert = loaded.cert; + } + + const store = await createPlatformStore(); + + if (await store.isCertTrusted(cert)) { + process.stderr.write( + `Certificate ${cert.thumbprintSha1} is already trusted on this host; nothing to do.\n` + ); + return; + } + + process.stderr.write( + `Trusting certificate ${cert.thumbprintSha1} on this host...\n` + ); + await store.trustCertificate(cert); + process.stderr.write("Trust step complete.\n"); +} diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts new file mode 100644 index 0000000..218f8f7 --- /dev/null +++ b/src/cli/src/index.ts @@ -0,0 +1,133 @@ +import "reflect-metadata"; +import { Command, Option } from "commander"; +import { runBundle } from "./commands/bundle"; +import { runDoctor } from "./commands/doctor"; +import { runGenerate } from "./commands/generate"; +import { runInspect } from "./commands/inspect"; +import { runTrust } from "./commands/trust"; +import type { BackendMode } from "./backends/types"; + +const program = new Command(); + +program + .name("ddc") + .description( + "Host-side dev-cert toolkit. Generates, inspects, trusts, and bundles " + + "ASP.NET-compatible HTTPS dev certs for use with dev containers — without VS Code." + ); + +program + .command("generate") + .description("Generate a dev cert, optionally trust it, and emit a bundle.json.") + .option("-o, --out-dir ", "Directory to write artifacts to (default ~/.dev-certs).") + .addOption( + new Option("-b, --backend ", "Backend selection.") + .choices(["auto", "native", "dotnet", "aspire"]) + .default("auto") + ) + .option("--no-trust", "Skip the host trust step (PFX / PEM are still emitted).") + .option( + "--container-mount ", + "Container-side mount target for the out-dir, recorded into bundle.json.", + "/host-dev-certs" + ) + .option("--no-bundle", "Skip emitting bundle.json.") + .option("-v, --verbose", "Stream shared-layer log lines to stderr.") + .action( + async (opts: { + outDir?: string; + backend: BackendMode; + trust: boolean; + containerMount: string; + bundle: boolean; + verbose?: boolean; + }) => { + await runGenerate({ + outDir: opts.outDir, + backend: opts.backend, + // commander inverts `--no-trust` into `opts.trust = false`. + noTrust: !opts.trust, + containerMount: opts.containerMount, + noBundle: !opts.bundle, + verbose: opts.verbose, + }); + } + ); + +program + .command("inspect ") + .description("Print details about a PFX or PEM certificate.") + .option("--json", "Emit machine-readable JSON instead of human-readable text.") + .action(async (certPath: string, opts: { json?: boolean }) => { + await runInspect(certPath, { json: opts.json }); + }); + +program + .command("bundle ") + .description("Emit a bundle.json referencing an already-existing cert file.") + .option( + "-o, --out-dir ", + "Directory to write bundle.json to (default: directory of cert-path)." + ) + .option( + "--container-mount ", + "Container-side mount target for the out-dir.", + "/host-dev-certs" + ) + .option( + "--name ", + "Filename stem to use in bundle.json (default: cert-path's basename without extension)." + ) + .addOption( + new Option("--kind ", "Bundle entry kind.") + .choices(["dotnet-dev", "user"]) + .default("user") + ) + .option( + "--no-trust-in-container", + "Mark trustInContainer=false (cert is served only, not added to trust stores)." + ) + .action( + async ( + certPath: string, + opts: { + outDir?: string; + containerMount: string; + name?: string; + kind: "dotnet-dev" | "user"; + trustInContainer: boolean; + } + ) => { + await runBundle(certPath, { + outDir: opts.outDir, + containerMount: opts.containerMount, + name: opts.name, + kind: opts.kind, + noTrustInContainer: !opts.trustInContainer, + }); + } + ); + +program + .command("trust ") + .description("Add an existing PFX or PEM cert to the host's OS trust store.") + .option("-v, --verbose", "Stream shared-layer log lines to stderr.") + .action(async (certPath: string, opts: { verbose?: boolean }) => { + await runTrust(certPath, { verbose: opts.verbose }); + }); + +program + .command("doctor") + .description("Read-only diagnostics: backend availability + host trust state.") + .option("-o, --out-dir ", "Out-dir to inspect (default ~/.dev-certs).") + .option("-v, --verbose", "Stream shared-layer log lines to stderr.") + .action(async (opts: { outDir?: string; verbose?: boolean }) => { + await runDoctor({ outDir: opts.outDir, verbose: opts.verbose }); + }); + +// Run. +program.parseAsync(process.argv).catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write(`ddc: ${message}\n`); + process.exit(1); +}); diff --git a/src/cli/src/logger.ts b/src/cli/src/logger.ts new file mode 100644 index 0000000..6ca8b78 --- /dev/null +++ b/src/cli/src/logger.ts @@ -0,0 +1,20 @@ +import { setLogSink, type LogSink } from "@devcontainer-dev-certs/shared"; + +/** + * Wires a console-backed sink into the shared logger so the platform / cert + * layer's `log()` calls surface during CLI runs. Verbose mode forwards every + * line to stderr (so stdout stays clean for `--json` / scripting use); quiet + * mode swallows them entirely. + */ +export function installCliLogger(verbose: boolean): void { + if (!verbose) { + setLogSink(undefined); + return; + } + const sink: LogSink = { + appendLine(message: string): void { + process.stderr.write(`${message}\n`); + }, + }; + setLogSink(sink); +} diff --git a/src/cli/tests/select.test.ts b/src/cli/tests/select.test.ts new file mode 100644 index 0000000..bc16f07 --- /dev/null +++ b/src/cli/tests/select.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { + selectBackend, + describeAutoBackend, +} from "../src/backends/select"; + +// `selectBackend('dotnet')` calls into the DotnetBackend's `isAvailable` +// which shells out via the shared runProcess. Stub that so the tests don't +// require an actual `dotnet` install. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ + runProcess: vi.fn(), +})); + +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; + +const mockedRunProcess = vi.mocked(runProcess); + +describe("selectBackend", () => { + let originalPlatform: PropertyDescriptor | undefined; + + beforeEach(() => { + vi.clearAllMocks(); + originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); + }); + + afterEach(() => { + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform); + } + }); + + function stubPlatform(value: NodeJS.Platform): void { + Object.defineProperty(process, "platform", { + value, + configurable: true, + }); + } + + it("returns the native backend for --backend native regardless of platform", async () => { + stubPlatform("linux"); + const backend = await selectBackend("native"); + expect(backend.kind).toBe("native"); + }); + + it("returns the dotnet backend for --backend dotnet when dotnet is on PATH", async () => { + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("dotnet"); + expect(backend.kind).toBe("dotnet"); + }); + + it("throws when --backend dotnet is requested but dotnet is unavailable", async () => { + mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); + await expect(selectBackend("dotnet")).rejects.toThrow(/not found on PATH/); + }); + + it("rejects --backend aspire as not implemented in v0", async () => { + await expect(selectBackend("aspire")).rejects.toThrow(/not implemented yet/); + }); + + it("auto-picks dotnet on macOS when dotnet is available", async () => { + stubPlatform("darwin"); + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("dotnet"); + }); + + it("auto-falls-back to native on macOS when dotnet is unavailable", async () => { + stubPlatform("darwin"); + mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + }); + + it("auto-picks native on Linux regardless of dotnet availability", async () => { + stubPlatform("linux"); + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + }); + + it("auto-picks native on Windows", async () => { + stubPlatform("win32"); + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + }); +}); + +describe("describeAutoBackend", () => { + let originalPlatform: PropertyDescriptor | undefined; + + beforeEach(() => { + vi.clearAllMocks(); + originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); + }); + + afterEach(() => { + if (originalPlatform) { + Object.defineProperty(process, "platform", originalPlatform); + } + }); + + function stubPlatform(value: NodeJS.Platform): void { + Object.defineProperty(process, "platform", { + value, + configurable: true, + }); + } + + it("reports 'dotnet' on macOS when dotnet is available", async () => { + stubPlatform("darwin"); + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + expect(await describeAutoBackend()).toBe("dotnet"); + }); + + it("reports 'native' everywhere else", async () => { + stubPlatform("linux"); + expect(await describeAutoBackend()).toBe("native"); + stubPlatform("win32"); + expect(await describeAutoBackend()).toBe("native"); + }); +}); diff --git a/src/cli/tests/writer.test.ts b/src/cli/tests/writer.test.ts new file mode 100644 index 0000000..d5b062b --- /dev/null +++ b/src/cli/tests/writer.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { writeBundle, type BundleCertEntry } from "../src/bundle/writer"; + +let tmpDir: string; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-writer-test-")); +}); + +afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +function makeEntry(overrides: Partial = {}): BundleCertEntry { + return { + name: "aspnetcore-dev", + thumbprint: "ABCDEF1234567890", + kind: "dotnet-dev", + hostPfxPath: path.join(tmpDir, "aspnetcore-dev.pfx"), + hostPemPath: path.join(tmpDir, "aspnetcore-dev.pem"), + hostPemKeyPath: path.join(tmpDir, "aspnetcore-dev.key"), + trustInContainer: true, + ...overrides, + }; +} + +function readBundle(bundlePath: string): Record { + return JSON.parse(fs.readFileSync(bundlePath, "utf-8")) as Record; +} + +describe("writeBundle", () => { + it("writes a bundle.json with the schema URL and a $schema pointer", () => { + const bundlePath = writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [makeEntry()], + }); + expect(bundlePath).toBe(path.join(tmpDir, "bundle.json")); + const bundle = readBundle(bundlePath); + expect(bundle.$schema).toContain("bundle.schema.json"); + expect(Array.isArray(bundle.certs)).toBe(true); + }); + + it("rewrites host paths under the out-dir to container-mount paths", () => { + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [makeEntry()], + }); + const bundle = readBundle(path.join(tmpDir, "bundle.json")); + const cert = (bundle.certs as Record[])[0]; + expect(cert.pemPath).toBe("/host-dev-certs/aspnetcore-dev.pem"); + expect(cert.pfxPath).toBe("/host-dev-certs/aspnetcore-dev.pfx"); + expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.key"); + }); + + it("leaves paths outside the out-dir untouched (no implicit copy assumed)", () => { + const externalPath = path.join(os.tmpdir(), "elsewhere.pem"); + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [ + makeEntry({ + hostPemPath: externalPath, + hostPfxPath: null, + hostPemKeyPath: null, + }), + ], + }); + const bundle = readBundle(path.join(tmpDir, "bundle.json")); + const cert = (bundle.certs as Record[])[0]; + expect(cert.pemPath).toBe(externalPath); + }); + + it("strips trailing slashes from containerMount so paths don't double-slash", () => { + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs/", + entries: [makeEntry()], + }); + const bundle = readBundle(path.join(tmpDir, "bundle.json")); + const cert = (bundle.certs as Record[])[0]; + expect(cert.pemPath).toBe("/host-dev-certs/aspnetcore-dev.pem"); + }); + + it("omits pfxPath / pemKeyPath when they're null (cert-only entries)", () => { + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [ + makeEntry({ + hostPfxPath: null, + hostPemKeyPath: null, + }), + ], + }); + const bundle = readBundle(path.join(tmpDir, "bundle.json")); + const cert = (bundle.certs as Record[])[0]; + expect("pfxPath" in cert).toBe(false); + expect("pemKeyPath" in cert).toBe(false); + expect(cert.pemPath).toBeDefined(); + }); + + it("emits each entry with its declared kind and trustInContainer flag", () => { + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [ + makeEntry({ name: "corp-ca", kind: "user", trustInContainer: false }), + ], + }); + const bundle = readBundle(path.join(tmpDir, "bundle.json")); + const cert = (bundle.certs as Record[])[0]; + expect(cert.name).toBe("corp-ca"); + expect(cert.kind).toBe("user"); + expect(cert.trustInContainer).toBe(false); + }); + + it("includes extraDestinations when provided, omits the key when absent", () => { + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [makeEntry()], + extraDestinations: [{ path: "/etc/nginx/certs", format: "pem" }], + }); + const withExtras = readBundle(path.join(tmpDir, "bundle.json")); + expect(withExtras.extraDestinations).toEqual([ + { path: "/etc/nginx/certs", format: "pem" }, + ]); + + writeBundle({ + hostOutDir: tmpDir, + containerMount: "/host-dev-certs", + entries: [makeEntry()], + }); + const withoutExtras = readBundle(path.join(tmpDir, "bundle.json")); + expect("extraDestinations" in withoutExtras).toBe(false); + }); +}); diff --git a/src/cli/tsconfig.json b/src/cli/tsconfig.json new file mode 100644 index 0000000..cdc3aae --- /dev/null +++ b/src/cli/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Node16", + "types": ["node"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/src/cli/tsconfig.lint.json b/src/cli/tsconfig.lint.json new file mode 100644 index 0000000..0b5e893 --- /dev/null +++ b/src/cli/tsconfig.lint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.setup.ts"] +} diff --git a/src/cli/vitest.config.ts b/src/cli/vitest.config.ts new file mode 100644 index 0000000..1b1cdf6 --- /dev/null +++ b/src/cli/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + setupFiles: ["./vitest.setup.ts"], + }, +}); diff --git a/src/cli/vitest.setup.ts b/src/cli/vitest.setup.ts new file mode 100644 index 0000000..82e4bba --- /dev/null +++ b/src/cli/vitest.setup.ts @@ -0,0 +1,4 @@ +// @peculiar/x509 v2 transitively pulls in tsyringe, which requires the +// reflect-metadata polyfill before any module decorated with @injectable +// is imported. Same setup the extension test suites use. +import "reflect-metadata"; diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index ed053c6..01777ec 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -1,4 +1,5 @@ -export { initLogger, log, revealLogger } from "./logger"; +export { log, revealLogger, setLogSink } from "./logger"; +export type { LogSink } from "./logger"; export { identityLocalizer } from "./localizer"; export type { Localizer } from "./localizer"; export type { diff --git a/src/shared/src/logger.ts b/src/shared/src/logger.ts index f91d242..96e54cd 100644 --- a/src/shared/src/logger.ts +++ b/src/shared/src/logger.ts @@ -1,27 +1,41 @@ -import * as vscode from "vscode"; +/** + * Generic log sink interface — structurally compatible with + * `vscode.OutputChannel` so the VS Code extension hosts can plug their + * channel directly into `setLogSink`. Non-VS-Code consumers (the host CLI, + * scripts) supply their own implementation (typically a console wrapper) so + * the shared cert / platform layer can call `log()` without a `vscode` + * dependency. + */ +export interface LogSink { + appendLine(message: string): void; + /** Optional. Honored by `revealLogger`. */ + show?(preserveFocus: boolean): void; +} -let channel: vscode.OutputChannel | undefined; +let sink: LogSink | undefined; /** - * Initialize the shared logger with an output channel. - * Call once from the extension's activate() function. - * Returns the channel so it can be registered as a disposable. + * Wire up the active log sink. `undefined` disables logging entirely. Safe + * to call repeatedly — the most recent call wins. */ -export function initLogger(channelName: string): vscode.OutputChannel { - channel = vscode.window.createOutputChannel(channelName); - return channel; +export function setLogSink(newSink: LogSink | undefined): void { + sink = newSink; } /** - * Log a timestamped message to the output channel. - * Requires initLogger() to have been called first. + * Log a timestamped message to the active sink. No-op if no sink is set — + * the platform / cert layer calls this freely without coordinating with + * its hosts about whether logging is enabled. */ export function log(message: string): void { - channel?.appendLine(`[${new Date().toISOString()}] ${message}`); + sink?.appendLine(`[${new Date().toISOString()}] ${message}`); } -/** Reveal the shared output channel. `preserveFocus = true` so a - * concurrent prompt doesn't lose focus. No-op if uninitialised. */ +/** + * Reveal the active sink (VS Code OutputChannel.show). `preserveFocus = true` + * so a concurrent prompt doesn't lose focus. No-op if uninitialized or the + * sink doesn't implement `show`. + */ export function revealLogger(): void { - channel?.show(true); + sink?.show?.(true); } diff --git a/src/shared/src/loggerVscode.ts b/src/shared/src/loggerVscode.ts new file mode 100644 index 0000000..b1aa407 --- /dev/null +++ b/src/shared/src/loggerVscode.ts @@ -0,0 +1,17 @@ +import * as vscode from "vscode"; +import { setLogSink } from "./logger"; + +/** + * VS Code-host helper: create a named OutputChannel and wire it up as the + * shared log sink. Returns the channel so the caller can register it with + * `context.subscriptions.push(...)` for automatic disposal. + * + * Kept in a dedicated submodule (not re-exported from the package barrel) so + * the shared cert / platform layer remains importable from non-VS-Code + * contexts (host CLI, scripts) without resolving `vscode` at module load. + */ +export function initLogger(channelName: string): vscode.OutputChannel { + const channel = vscode.window.createOutputChannel(channelName); + setLogSink(channel); + return channel; +} diff --git a/src/vscode-ui-extension/src/extension.ts b/src/vscode-ui-extension/src/extension.ts index c7c2e0b..329b1f2 100644 --- a/src/vscode-ui-extension/src/extension.ts +++ b/src/vscode-ui-extension/src/extension.ts @@ -13,12 +13,12 @@ import { } from "./containerCertAccept"; import { trustInNss } from "./platform/nssTrust"; import { - initLogger, log, getOpenSslTrustDir, getPemFileName, type NonLocalSanEntry, } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import type { CertBundle, CertBundleV3 } from "@devcontainer-dev-certs/shared"; const CONTAINER_CERT_CONSENT_KEY = "containerCertProvisionConsented"; diff --git a/src/vscode-ui-extension/tests/classifyCandidate.test.ts b/src/vscode-ui-extension/tests/classifyCandidate.test.ts index dfc85cd..769d45c 100644 --- a/src/vscode-ui-extension/tests/classifyCandidate.test.ts +++ b/src/vscode-ui-extension/tests/classifyCandidate.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { initLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { logMessages } from "./__mocks__/vscode"; import { classifyCandidate } from "../src/platform/baseStore"; import { generateCertificate } from "../src/cert/generator"; diff --git a/src/vscode-ui-extension/tests/containerCertAccept.test.ts b/src/vscode-ui-extension/tests/containerCertAccept.test.ts index 4946490..efc5cbe 100644 --- a/src/vscode-ui-extension/tests/containerCertAccept.test.ts +++ b/src/vscode-ui-extension/tests/containerCertAccept.test.ts @@ -10,10 +10,10 @@ import { DevCert, ASPNET_HTTPS_OID, CURRENT_CERTIFICATE_VERSION, - initLogger, SAN_DNS_NAMES, SAN_IP_ADDRESSES, } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { acceptContainerDevCert, type AcceptContainerCertDeps, diff --git a/src/vscode-ui-extension/tests/linuxStore.test.ts b/src/vscode-ui-extension/tests/linuxStore.test.ts index 3d31780..a43c8c7 100644 --- a/src/vscode-ui-extension/tests/linuxStore.test.ts +++ b/src/vscode-ui-extension/tests/linuxStore.test.ts @@ -3,7 +3,7 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import type * as Shared from "@devcontainer-dev-certs/shared"; -import { initLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { generateCertificate } from "../src/cert/generator"; import { VALIDITY_DAYS } from "../src/cert/properties"; import { buildPfx, parsePfx } from "../src/cert/pfx"; diff --git a/src/vscode-ui-extension/tests/macStore.test.ts b/src/vscode-ui-extension/tests/macStore.test.ts index 623c730..6d73456 100644 --- a/src/vscode-ui-extension/tests/macStore.test.ts +++ b/src/vscode-ui-extension/tests/macStore.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { initLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { logMessages } from "./__mocks__/vscode"; import { generateCertificate } from "../src/cert/generator"; import { VALIDITY_DAYS } from "../src/cert/properties"; diff --git a/src/vscode-ui-extension/tests/selectBestDevCert.test.ts b/src/vscode-ui-extension/tests/selectBestDevCert.test.ts index 21d9c97..5b75abf 100644 --- a/src/vscode-ui-extension/tests/selectBestDevCert.test.ts +++ b/src/vscode-ui-extension/tests/selectBestDevCert.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { initLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { logMessages } from "./__mocks__/vscode"; import { selectBestDevCert, type UsableDevCert } from "../src/platform/baseStore"; import { generateCertificate } from "../src/cert/generator"; diff --git a/src/vscode-ui-extension/tests/windowsStore.test.ts b/src/vscode-ui-extension/tests/windowsStore.test.ts index 7d61573..ad41aff 100644 --- a/src/vscode-ui-extension/tests/windowsStore.test.ts +++ b/src/vscode-ui-extension/tests/windowsStore.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { initLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { logMessages } from "./__mocks__/vscode"; import { generateCertificate } from "../src/cert/generator"; import { VALIDITY_DAYS } from "../src/cert/properties"; diff --git a/src/vscode-workspace-extension/src/extension.ts b/src/vscode-workspace-extension/src/extension.ts index aae243f..0b0f8b4 100644 --- a/src/vscode-workspace-extension/src/extension.ts +++ b/src/vscode-workspace-extension/src/extension.ts @@ -32,7 +32,8 @@ import { import { parseExtraCertDestinations } from "./util/destinations"; import { ensureSslCertDir } from "./util/sslCertDir"; import { upmapV1ToV3, upmapV2ToV3 } from "./util/upmap"; -import { initLogger, log, revealLogger } from "@devcontainer-dev-certs/shared"; +import { log, revealLogger } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import type { CertBundle, CertBundleV3, diff --git a/src/vscode-workspace-extension/tests/containerCertPush.test.ts b/src/vscode-workspace-extension/tests/containerCertPush.test.ts index 8424603..87514fc 100644 --- a/src/vscode-workspace-extension/tests/containerCertPush.test.ts +++ b/src/vscode-workspace-extension/tests/containerCertPush.test.ts @@ -19,10 +19,10 @@ import { ASPNET_HTTPS_OID, CURRENT_CERTIFICATE_VERSION, buildPfx, - initLogger, SAN_DNS_NAMES, SAN_IP_ADDRESSES, } from "@devcontainer-dev-certs/shared"; +import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import * as vscode from "vscode"; import { findBestContainerDevCert, From c184b7017655202ef3d4e80f48c9f51f609626b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 06:42:32 +0000 Subject: [PATCH 07/41] Add hostCertGenerator setting; promote backends into shared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 of the non-VS-Code carveout. The dotnet-pass-through backend that landed in `ddc` is now also available to the VS Code host extension, controlled by a new `devcontainerDevCerts.hostCertGenerator` setting with three values: - `auto` (default): on macOS, prefer dotnet when the dotnet CLI is on PATH (better keychain-trust UX via a signed binary); fall back to native everywhere else. Identical resolution to `ddc --backend auto`. - `native`: always use the in-tree cert primitives + CertManager. No dotnet SDK required. Identical to the historical extension behavior. - `dotnet`: always shell out to `dotnet dev-certs https`. Requires the dotnet SDK on PATH; trust is fired by the dotnet CLI rather than the extension's own binary. To make this work without duplicating code between `ddc` and the host extension, the backend abstraction (`Backend` / `NativeBackend` / `DotnetBackend` / `selectBackend` / `describeAutoBackend`) was promoted from `src/cli/src/backends/` into `src/shared/src/backends/` and exported from the shared barrel. The CLI now imports the same backends the extension does; the CLI's removed `backends/` directory is gone entirely (the local `generateAndWriteFiles` lower-level helper was not used anywhere yet and was dropped along with it). CertProvider wires the setting through a new `provisionViaConfiguredBackend()` private method. When the resolved backend is native (either explicitly `native` or `auto` falling back on a non-macOS host / macOS without dotnet), CertProvider keeps using the in-process CertManager — preserving its `vscode.l10n.t` locale injection and Linux NSS trust reporter wiring that the shared NativeBackend's bare CertManager construction would lose. When the resolved backend is dotnet, CertProvider creates a per-provisioning tmp dir, delegates `generate({outDir, noTrust: false})` to the backend, and then discards the dir — the side effect we care about is that the cert lands in the OS platform store, which the subsequent existing `certManager.exportCert(...)` flow reads identically regardless of which backend put it there. Five new tests in `tests/hostCertGenerator.test.ts` cover the dispatch logic with a partial-mocked shared module: native short- circuit, auto-resolves-to-native defers to manager, dotnet routes through backend.generate, errors from selectBackend propagate, and the per-provisioning tmp dir is cleaned up on the throw path. Aspire backend remains intentionally out of scope (phase 4 was deferred per the original plan); `selectBackend('aspire')` continues to reject with a clear "not implemented yet" error so the option stays visible without bait-and-switch ergonomics. All checks pass: 212 UI tests (+5 new), 79 workspace tests, 17 CLI tests, type-check across all four workspaces, esbuild bundles correctly, full-repo lint. --- src/cli/src/backends/native.ts | 111 -------- src/cli/src/backends/types.ts | 48 ---- src/cli/src/commands/doctor.ts | 4 +- src/cli/src/commands/generate.ts | 7 +- src/cli/src/index.ts | 2 +- src/cli/tests/select.test.ts | 2 +- src/{cli => shared}/src/backends/dotnet.ts | 39 ++- src/shared/src/backends/native.ts | 61 +++++ src/{cli => shared}/src/backends/select.ts | 3 +- src/shared/src/backends/types.ts | 53 ++++ src/shared/src/index.ts | 16 ++ src/vscode-ui-extension/package.json | 15 ++ src/vscode-ui-extension/src/certProvider.ts | 60 ++++- .../tests/hostCertGenerator.test.ts | 255 ++++++++++++++++++ 14 files changed, 486 insertions(+), 190 deletions(-) delete mode 100644 src/cli/src/backends/native.ts delete mode 100644 src/cli/src/backends/types.ts rename src/{cli => shared}/src/backends/dotnet.ts (65%) create mode 100644 src/shared/src/backends/native.ts rename src/{cli => shared}/src/backends/select.ts (93%) create mode 100644 src/shared/src/backends/types.ts create mode 100644 src/vscode-ui-extension/tests/hostCertGenerator.test.ts diff --git a/src/cli/src/backends/native.ts b/src/cli/src/backends/native.ts deleted file mode 100644 index 4ec4e31..0000000 --- a/src/cli/src/backends/native.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { - CertManager, - VALIDITY_DAYS, - generateCertificate, - exportPfx, - exportPem, - loadPfx, - buildPfx, - type DevCert, - type DevKey, -} from "@devcontainer-dev-certs/shared"; -import type { Backend, GenerateOptions, GenerateResult } from "./types"; - -/** - * Native backend: uses the shared `CertManager` directly. Same code path - * the VS Code host extension uses for generation, host trust, and - * platform-store I/O — no shelling out to other tools. - */ -export class NativeBackend implements Backend { - readonly kind = "native" as const; - - isAvailable(): Promise { - // The native backend is always available — the shared layer ships - // implementations for all three supported platforms (Linux/macOS/Windows) - // and the cert primitives themselves have no external runtime - // dependencies. - return Promise.resolve(true); - } - - async generate(options: GenerateOptions): Promise { - fs.mkdirSync(options.outDir, { recursive: true }); - - const manager = new CertManager(); - - // Drive the manager through the same generate+trust flow the host - // extension uses, then export the live cert to the out-dir. Trust is - // skipped when --no-trust is passed. - if (options.noTrust) { - // Generate-only: produce a cert, save it to the platform store, but - // don't trust. The manager exposes `generate()` separately for this. - await manager.generate(false); - } else { - await manager.trust(); - } - - // Export current cert. The manager doesn't expose the live cert/key - // directly; export it through the manager's `exportCert` which writes - // the canonical filenames. We follow up by reading the PFX back to - // recover the thumbprint — the manager will have loaded the same cert - // from the platform store so the bytes match. - await manager.exportCert("pfx", options.outDir); - await manager.exportCert("pem", options.outDir); - - const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); - const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); - const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.key"); - - const loaded = await loadPfx(pfxPath); - if (!loaded || !loaded.cert) { - throw new Error( - `Native backend export wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` - ); - } - - return { - pfxPath, - pemPath, - pemKeyPath, - thumbprint: loaded.cert.thumbprintSha1, - trusted: !options.noTrust, - backendUsed: "native", - }; - } -} - -/** - * Generate a brand-new cert in memory (no platform-store interaction) and - * write it to the given out-dir. Used by `ddc generate` when the user opts - * out of the manager flow and just wants files on disk; also useful as a - * lower-level building block for tests. - * - * Exposed alongside `NativeBackend` because some flows (e.g. a future - * `ddc bundle --regen`) want the artifacts without trust as a side effect. - */ -export async function generateAndWriteFiles( - outDir: string -): Promise<{ - cert: DevCert; - key: DevKey; - thumbprint: string; - pfxPath: string; - pemPath: string; - pemKeyPath: string; - rootPfxPath: string; -}> { - fs.mkdirSync(outDir, { recursive: true }); - - const now = new Date(); - const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); - const { cert, key, thumbprint } = await generateCertificate(now, expiry); - - const pfxPath = await exportPfx(cert, key, outDir); - const { certPath: pemPath, keyPath: pemKeyPath } = exportPem(cert, key, outDir); - - const rootPfxPath = path.join(outDir, "aspnetcore-dev-root.pfx"); - fs.writeFileSync(rootPfxPath, await buildPfx({ cert }), { mode: 0o644 }); - - return { cert, key, thumbprint, pfxPath, pemPath, pemKeyPath, rootPfxPath }; -} diff --git a/src/cli/src/backends/types.ts b/src/cli/src/backends/types.ts deleted file mode 100644 index 2d403a5..0000000 --- a/src/cli/src/backends/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * One of the three concrete backends `ddc` can use to generate (and trust) - * a dev cert. `auto` is resolved at command-dispatch time into one of the - * concrete kinds. - */ -export type BackendKind = "native" | "dotnet" | "aspire"; - -export type BackendMode = BackendKind | "auto"; - -export interface GenerateOptions { - /** Directory that receives PFX / PEM / bundle.json artifacts. */ - outDir: string; - /** Skip the host trust step. PFX / PEM are still emitted. */ - noTrust: boolean; - /** - * Container-side path the host out-dir maps to via a Docker mount — - * recorded into bundle.json's `pfxPath` / `pemPath` so the in-container - * installer reads from the right place. Defaults to `/host-dev-certs`. - */ - containerMount: string; -} - -export interface GenerateResult { - /** Absolute host path of the PFX. */ - pfxPath: string; - /** Absolute host path of the PEM cert. */ - pemPath: string; - /** Absolute host path of the PEM key (null when backend didn't emit one). */ - pemKeyPath: string | null; - /** SHA-1 thumbprint, uppercase hex. */ - thumbprint: string; - /** Whether the host trust step ran and succeeded. */ - trusted: boolean; - /** Backend that actually produced the cert (auto resolves to one of these). */ - backendUsed: BackendKind; -} - -/** - * Backends implement generation (+ optional trust) and platform-availability - * detection. `auto` selection asks each candidate whether it's available and - * picks per platform preference. - */ -export interface Backend { - readonly kind: BackendKind; - /** Is this backend usable on the current host? */ - isAvailable(): Promise; - generate(options: GenerateOptions): Promise; -} diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts index cc9c7b3..87f7f2d 100644 --- a/src/cli/src/commands/doctor.ts +++ b/src/cli/src/commands/doctor.ts @@ -3,10 +3,10 @@ import * as os from "os"; import * as path from "path"; import { createPlatformStore, + describeAutoBackend, + DotnetBackend, runProcess, } from "@devcontainer-dev-certs/shared"; -import { DotnetBackend } from "../backends/dotnet"; -import { describeAutoBackend } from "../backends/select"; import { installCliLogger } from "../logger"; export interface DoctorCommandOptions { diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts index ac3f9c1..b5f0cea 100644 --- a/src/cli/src/commands/generate.ts +++ b/src/cli/src/commands/generate.ts @@ -1,7 +1,9 @@ import * as os from "os"; import * as path from "path"; -import { selectBackend } from "../backends/select"; -import type { BackendMode } from "../backends/types"; +import { + selectBackend, + type BackendMode, +} from "@devcontainer-dev-certs/shared"; import { writeBundle, type BundleCertEntry } from "../bundle/writer"; import { installCliLogger } from "../logger"; @@ -38,7 +40,6 @@ export async function runGenerate( const result = await backend.generate({ outDir, noTrust: Boolean(options.noTrust), - containerMount, }); process.stderr.write( diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts index 218f8f7..51d3000 100644 --- a/src/cli/src/index.ts +++ b/src/cli/src/index.ts @@ -5,7 +5,7 @@ import { runDoctor } from "./commands/doctor"; import { runGenerate } from "./commands/generate"; import { runInspect } from "./commands/inspect"; import { runTrust } from "./commands/trust"; -import type { BackendMode } from "./backends/types"; +import type { BackendMode } from "@devcontainer-dev-certs/shared"; const program = new Command(); diff --git a/src/cli/tests/select.test.ts b/src/cli/tests/select.test.ts index bc16f07..4440a97 100644 --- a/src/cli/tests/select.test.ts +++ b/src/cli/tests/select.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { selectBackend, describeAutoBackend, -} from "../src/backends/select"; +} from "@devcontainer-dev-certs/shared"; // `selectBackend('dotnet')` calls into the DotnetBackend's `isAvailable` // which shells out via the shared runProcess. Stub that so the tests don't diff --git a/src/cli/src/backends/dotnet.ts b/src/shared/src/backends/dotnet.ts similarity index 65% rename from src/cli/src/backends/dotnet.ts rename to src/shared/src/backends/dotnet.ts index 3dbd81b..daeea92 100644 --- a/src/cli/src/backends/dotnet.ts +++ b/src/shared/src/backends/dotnet.ts @@ -1,22 +1,23 @@ import * as fs from "fs"; import * as path from "path"; -import { - loadPfx, - runProcess, -} from "@devcontainer-dev-certs/shared"; +import { loadPfx } from "../cert/loader"; +import { runProcess } from "../platform/processUtil"; import type { Backend, GenerateOptions, GenerateResult } from "./types"; /** - * Dotnet backend: shells out to `dotnet dev-certs https`. On macOS this is - * the canonical way to get a signed-binary-attributed keychain trust prompt - * — the host extension's `security add-trusted-cert` flow works but has a - * less polished UX because the calling binary isn't a notarized Apple - * cert-management tool. On Windows / Linux the dotnet backend is equivalent - * to the native backend modulo cert format differences. + * Dotnet backend: shells out to `dotnet dev-certs https`. On macOS this + * is the canonical way to get a signed-binary-attributed keychain trust + * prompt — the native backend's `security add-trusted-cert` invocation + * works but has a less polished UX because the calling binary isn't a + * notarized Apple cert-management tool. On Windows / Linux the two + * backends end up writing to the same platform store, so the choice is + * mostly stylistic. * - * Two-pass: one invocation exports PFX, a second exports PEM. We pass - * `--trust` only on the first invocation (the second pass would re-trust - * the same cert and add nothing). + * Two-pass: one invocation to write the PFX (with `--trust` unless + * `noTrust` is set), a second to write the PEM. We can't combine them — + * `dotnet dev-certs --format ...` only accepts one format per call, and + * `--trust` only does anything on the first invocation anyway (it's + * idempotent w.r.t. the OS trust store). */ export class DotnetBackend implements Backend { readonly kind = "dotnet" as const; @@ -31,12 +32,10 @@ export class DotnetBackend implements Backend { const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); - // dotnet dev-certs --format PEM writes both `` (cert) and - // `.key` (key) when invoked without --no-password and without - // `-ep`. We rely on that companion key file to populate pemKeyPath. + // `dotnet dev-certs --format PEM` writes both `` (cert) and + // `.key` (private key in PEM PKCS#8). const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.pem.key"); - // First pass: export PFX, trust (unless --no-trust). const pfxArgs = ["dev-certs", "https"]; if (!options.noTrust) pfxArgs.push("--trust"); pfxArgs.push("--format", "Pfx", "--no-password", "--export-path", pfxPath); @@ -48,7 +47,6 @@ export class DotnetBackend implements Backend { ); } - // Second pass: export PEM. No --trust here — already done above. const pemResult = await runProcess( "dotnet", [ @@ -68,11 +66,10 @@ export class DotnetBackend implements Backend { ); } - // Recover the thumbprint from the PFX we just wrote. const loaded = await loadPfx(pfxPath); - if (!loaded || !loaded.cert) { + if (!loaded.cert) { throw new Error( - `dotnet wrote ${pfxPath} but the resulting PFX could not be parsed.` + `dotnet wrote ${pfxPath} but the resulting PFX could not be parsed for thumbprint recovery.` ); } diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts new file mode 100644 index 0000000..cd98daf --- /dev/null +++ b/src/shared/src/backends/native.ts @@ -0,0 +1,61 @@ +import * as fs from "fs"; +import * as path from "path"; +import { CertManager } from "../cert/manager"; +import { loadPfx } from "../cert/loader"; +import type { Backend, GenerateOptions, GenerateResult } from "./types"; + +/** + * Native backend: uses the in-tree `CertManager` directly. Same code path + * the VS Code host extension uses for generation, host trust, and + * platform-store I/O — no shelling out to other tools, no `dotnet` runtime + * required. + */ +export class NativeBackend implements Backend { + readonly kind = "native" as const; + + isAvailable(): Promise { + // Always available — the shared layer ships implementations for all + // three supported platforms and the cert primitives themselves have + // no external runtime dependencies. + return Promise.resolve(true); + } + + async generate(options: GenerateOptions): Promise { + fs.mkdirSync(options.outDir, { recursive: true }); + + const manager = new CertManager(); + if (options.noTrust) { + // Generate-only: produce a cert, save it to the platform store, but + // don't run the OS trust-prompt path. + await manager.generate(false); + } else { + await manager.trust(); + } + + await manager.exportCert("pfx", options.outDir); + await manager.exportCert("pem", options.outDir); + + const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); + const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); + const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.key"); + + // Recover the thumbprint by re-reading the exported PFX. Cheaper than + // reaching into the manager's private state and keeps the contract + // symmetric with the `dotnet` backend's recovery step. + const loaded = await loadPfx(pfxPath); + if (!loaded.cert) { + throw new Error( + `Native backend wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` + ); + } + + return { + pfxPath, + pemPath, + pemKeyPath, + thumbprint: loaded.cert.thumbprintSha1, + trusted: !options.noTrust, + backendUsed: "native", + }; + } +} diff --git a/src/cli/src/backends/select.ts b/src/shared/src/backends/select.ts similarity index 93% rename from src/cli/src/backends/select.ts rename to src/shared/src/backends/select.ts index 0d9bf8b..ac9ba33 100644 --- a/src/cli/src/backends/select.ts +++ b/src/shared/src/backends/select.ts @@ -38,7 +38,8 @@ async function autoSelect(): Promise { /** * Report which backend `auto` would pick on this host without actually - * constructing it. Useful for `ddc doctor`. + * constructing it. Useful for `ddc doctor` and for status surfaces in the + * VS Code host extension. */ export async function describeAutoBackend(): Promise { if (process.platform === "darwin") { diff --git a/src/shared/src/backends/types.ts b/src/shared/src/backends/types.ts new file mode 100644 index 0000000..b696715 --- /dev/null +++ b/src/shared/src/backends/types.ts @@ -0,0 +1,53 @@ +/** + * Backend abstraction shared by `ddc` (host CLI) and the VS Code host + * extension. Lets both consumers pick between equivalent generators — + * the bundled-in native cert primitives, the `dotnet dev-certs https` CLI + * pass-through, or (future) an Aspire-aware backend — without each having + * to reimplement the selection / availability-detection logic. + * + * The interface is deliberately narrow: each backend exposes + * `isAvailable()` and `generate()`. Trust is bundled into `generate()` so + * backends like `dotnet` (which combines generate + trust into a single + * shell invocation) don't need a separate trust hook. + */ + +export type BackendKind = "native" | "dotnet" | "aspire"; + +export type BackendMode = BackendKind | "auto"; + +export interface GenerateOptions { + /** Directory that receives PFX / PEM / key artifacts. */ + outDir: string; + /** Skip the host trust step. PFX / PEM are still emitted. */ + noTrust: boolean; +} + +export interface GenerateResult { + /** Absolute host path of the PFX. */ + pfxPath: string; + /** Absolute host path of the PEM cert. */ + pemPath: string; + /** Absolute host path of the PEM key (null when backend didn't emit one). */ + pemKeyPath: string | null; + /** SHA-1 thumbprint, uppercase hex. */ + thumbprint: string; + /** Whether the host trust step ran and succeeded. */ + trusted: boolean; + /** + * Which backend actually produced the cert — meaningful when the caller + * selected `auto` and wants to know which concrete kind was picked. + */ + backendUsed: BackendKind; +} + +/** + * Backends implement generation (+ optional trust) and a platform-availability + * probe. `auto` selection asks each candidate whether it's available and + * picks per-platform preference. + */ +export interface Backend { + readonly kind: BackendKind; + /** Is this backend usable on the current host? */ + isAvailable(): Promise; + generate(options: GenerateOptions): Promise; +} diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index 01777ec..07ca9ce 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -122,3 +122,19 @@ export { trustInNss } from "./platform/nssTrust"; export type { NssTrustResult } from "./platform/nssTrust"; export { runProcess } from "./platform/processUtil"; export type { ProcessResult } from "./platform/processUtil"; + +// Backend abstraction — selectable cert-generator backends shared by the +// host CLI (`ddc`) and the VS Code host extension. Lets both consumers +// pick between the bundled native generator, the `dotnet dev-certs https` +// pass-through, and (future) an Aspire-aware variant without each +// having to reimplement availability detection / selection logic. +export { NativeBackend } from "./backends/native"; +export { DotnetBackend } from "./backends/dotnet"; +export { selectBackend, describeAutoBackend } from "./backends/select"; +export type { + Backend, + BackendKind, + BackendMode, + GenerateOptions, + GenerateResult, +} from "./backends/types"; diff --git a/src/vscode-ui-extension/package.json b/src/vscode-ui-extension/package.json index 78a44ae..c557db3 100644 --- a/src/vscode-ui-extension/package.json +++ b/src/vscode-ui-extension/package.json @@ -56,6 +56,21 @@ "default": true, "description": "Auto-generate the ASP.NET Core / Aspire compatible HTTPS development certificate and trust it in the host OS store. When false, user-managed certificates (if any) are still synced but no dev cert is generated." }, + "devcontainerDevCerts.hostCertGenerator": { + "type": "string", + "enum": [ + "auto", + "native", + "dotnet" + ], + "default": "auto", + "enumDescriptions": [ + "On macOS, prefer the 'dotnet' backend when the dotnet CLI is on PATH (better keychain-trust UX via a signed binary); fall back to 'native' everywhere else.", + "Always use the bundled cert primitives. No dotnet SDK required. Same code path the extension has historically used; the OS trust prompt is fired by the extension's own binary.", + "Always shell out to `dotnet dev-certs https`. Requires the dotnet SDK to be installed and on PATH. Generates the cert and runs the OS trust step via the dotnet CLI." + ], + "description": "Which backend to use when this extension generates the host dev certificate. Only affects the auto-generated ASP.NET Core dev cert (gated by devcontainerDevCerts.generateDotNetCert); user-managed certificates are unaffected. Existing certs found in the OS store are not regenerated regardless of this setting — switching backends only affects fresh provisioning." + }, "devcontainerDevCerts.userCertificates": { "type": "array", "default": [], diff --git a/src/vscode-ui-extension/src/certProvider.ts b/src/vscode-ui-extension/src/certProvider.ts index 1d0a780..3543f51 100644 --- a/src/vscode-ui-extension/src/certProvider.ts +++ b/src/vscode-ui-extension/src/certProvider.ts @@ -7,8 +7,13 @@ import { exportLoadedCert } from "./cert/exporter"; import { loadPemPair, loadPfx } from "./cert/loader"; import type { LoadedCert } from "./cert/loader"; import { buildPfx } from "./cert/pfx"; -import { assertValidCertName, log } from "@devcontainer-dev-certs/shared"; +import { + assertValidCertName, + log, + selectBackend, +} from "@devcontainer-dev-certs/shared"; import type { + BackendMode, CertBundle, CertBundleV3, CertMaterial, @@ -183,6 +188,57 @@ export class CertProvider { return certs; } + /** + * Provision the host dev cert via the backend the user has selected + * (`devcontainerDevCerts.hostCertGenerator`). Default `auto` resolves + * to the dotnet backend on macOS (when the dotnet CLI is on PATH) and + * to native everywhere else — both end up writing the cert into the + * same OS platform store that `certManager` reads from, so the + * downstream `exportCert` path works regardless of which backend + * actually performed the provisioning. + */ + private async provisionViaConfiguredBackend(): Promise { + const setting = vscode.workspace + .getConfiguration("devcontainerDevCerts") + .get("hostCertGenerator", "auto"); + + // Native is the historical code path. Use the in-process CertManager + // directly so its l10n + Linux NSS reporter wiring (set up in + // extension.ts) is preserved — the shared NativeBackend constructs a + // bare CertManager without those hooks. + if (setting === "native") { + await this.certManager.trust(); + return; + } + + const backend = await selectBackend(setting); + if (backend.kind === "native") { + // `auto` resolved to native (non-macOS, or macOS without dotnet + // installed). Same reasoning as above — defer to the configured + // CertManager rather than the bare one inside NativeBackend. + await this.certManager.trust(); + return; + } + + // dotnet backend: the cert + trust side effects are what we care + // about; the on-disk PFX/PEM in the tmp dir is a byproduct of the + // backend's contract that we discard. The platform store ends up + // populated identically to the native path, so the subsequent + // `exportCert` calls work without further special-casing. + log(`Provisioning host dev cert via '${backend.kind}' backend.`); + const tmpProvisioningDir = fs.mkdtempSync( + path.join(os.tmpdir(), "devcerts-provision-") + ); + try { + await backend.generate({ + outDir: tmpProvisioningDir, + noTrust: false, + }); + } finally { + fs.rmSync(tmpProvisioningDir, { recursive: true, force: true }); + } + } + private async ensureDotNetDevCert( autoProvision: boolean ): Promise { @@ -215,7 +271,7 @@ export class CertProvider { return null; } log("Ensuring certificate is generated and trusted..."); - await this.certManager.trust(); + await this.provisionViaConfiguredBackend(); } // mkdtempSync gives us a unique 0o700 dir in tmpdir. Combined with diff --git a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts new file mode 100644 index 0000000..52527f0 --- /dev/null +++ b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts @@ -0,0 +1,255 @@ +import { + describe, + it, + expect, + beforeEach, + vi, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import type { Backend } from "@devcontainer-dev-certs/shared"; + +import type * as Shared from "@devcontainer-dev-certs/shared"; + +// Mock `selectBackend` so the test doesn't need a real `dotnet` install on +// PATH. We keep the rest of the shared module intact via `importActual` +// so the manager / cert primitives the SUT uses still work normally. +vi.mock("@devcontainer-dev-certs/shared", async () => { + const actual = await vi.importActual( + "@devcontainer-dev-certs/shared" + ); + return { + ...actual, + selectBackend: vi.fn(), + }; +}); + +import { selectBackend } from "@devcontainer-dev-certs/shared"; +import { CertProvider } from "../src/certProvider"; +import { generateCertificate } from "../src/cert/generator"; +import { VALIDITY_DAYS } from "../src/cert/properties"; +import type { CertManager } from "../src/cert/manager"; +import type { DevCert, DevKey } from "../src/cert/types"; +import { __resetConfig, __setConfig } from "./__mocks__/vscode"; + +const mockedSelectBackend = vi.mocked(selectBackend); + +async function makeValidCert(): ReturnType { + const now = new Date(); + const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); + return generateCertificate(now, expiry); +} + +interface ManagerSpyHandles { + manager: CertManager; + trustSpy: ReturnType; + checkSpy: ReturnType; +} + +/** + * Build a CertManager-shaped mock that initially reports the cert as + * absent / untrusted (so `ensureDotNetDevCert` is forced down the + * provisioning path), then flips to present + trusted on subsequent + * `check()` calls — simulating the side effect of a successful trust + * step. `exportCert` writes real cert files into the export dir so the + * subsequent base64-reading + cache-population step doesn't blow up. + */ +function buildManagerMock(cert: DevCert, key: DevKey, thumbprint: string): ManagerSpyHandles { + let provisioned = false; + const status = () => ({ + exists: provisioned, + isTrusted: provisioned, + thumbprint: provisioned ? thumbprint : null, + notBefore: null, + notAfter: null, + version: 1, + }); + const trustSpy = vi.fn(async () => { + provisioned = true; + }); + const checkSpy = vi.fn(async () => status()); + const manager = { + check: checkSpy, + trust: trustSpy, + exportCert: vi.fn( + async (format: "pfx" | "pem" | "root-pfx", outputDir: string) => { + fs.mkdirSync(outputDir, { recursive: true }); + if (format === "pem") { + fs.writeFileSync(path.join(outputDir, "aspnetcore-dev.pem"), cert.pem); + fs.writeFileSync(path.join(outputDir, "aspnetcore-dev.key"), key.pem); + } else if (format === "pfx") { + fs.writeFileSync( + path.join(outputDir, "aspnetcore-dev.pfx"), + Buffer.from("fake-pfx") + ); + } else { + fs.writeFileSync( + path.join(outputDir, "aspnetcore-dev-root.pfx"), + Buffer.from("fake-root") + ); + } + } + ), + trustExternalCertificate: vi.fn(async () => {}), + } as unknown as CertManager; + return { manager, trustSpy, checkSpy }; +} + +/** + * Construct a fake Backend that records every call to its `generate` and + * flips the supplied "is now trusted" state via the manager mock so the + * downstream export path sees a populated platform store. + */ +function fakeBackend( + kind: "native" | "dotnet", + onGenerate: () => void +): Backend { + return { + kind, + isAvailable: vi.fn(() => Promise.resolve(true)), + generate: vi.fn(async () => { + onGenerate(); + return { + pfxPath: "/dev/null/pfx", + pemPath: "/dev/null/pem", + pemKeyPath: null, + thumbprint: "FAKE", + trusted: true, + backendUsed: kind, + }; + }), + }; +} + +beforeEach(() => { + __resetConfig(); + mockedSelectBackend.mockReset(); +}); + +describe("CertProvider.provisionViaConfiguredBackend", () => { + it("uses the in-process CertManager.trust() when hostCertGenerator is 'native'", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager, trustSpy } = buildManagerMock(cert, key, thumbprint); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + hostCertGenerator: "native", + }); + + const provider = new CertProvider(manager); + await provider.getCertMaterial(true); + + expect(trustSpy).toHaveBeenCalledTimes(1); + // selectBackend is bypassed entirely on the native short-circuit. + expect(mockedSelectBackend).not.toHaveBeenCalled(); + }); + + it("defers to selectBackend('auto') when hostCertGenerator is unset", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager, trustSpy } = buildManagerMock(cert, key, thumbprint); + + // 'auto' resolved to native — selectBackend returned a native backend. + // CertProvider should use the configured manager.trust() rather than + // the bare CertManager inside the shared NativeBackend so the + // extension's l10n + Linux NSS reporter wiring is preserved. + mockedSelectBackend.mockResolvedValue(fakeBackend("native", () => { + throw new Error("native backend.generate should not run for 'auto' → native"); + })); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + // hostCertGenerator intentionally absent — default is 'auto'. + }); + + const provider = new CertProvider(manager); + await provider.getCertMaterial(true); + + expect(mockedSelectBackend).toHaveBeenCalledWith("auto"); + expect(trustSpy).toHaveBeenCalledTimes(1); + }); + + it("dispatches to backend.generate() when selectBackend returns a dotnet backend", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager, trustSpy } = buildManagerMock(cert, key, thumbprint); + + // Simulate dotnet's side effect: after `generate()` runs, the + // platform store has a trusted cert. We bridge to the same state + // flip the native trustSpy would have produced by calling trustSpy + // ourselves from within the fake generate. + let backendCalled = false; + mockedSelectBackend.mockResolvedValue( + fakeBackend("dotnet", () => { + backendCalled = true; + void trustSpy(); + }) + ); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + hostCertGenerator: "dotnet", + }); + + const provider = new CertProvider(manager); + await provider.getCertMaterial(true); + + expect(mockedSelectBackend).toHaveBeenCalledWith("dotnet"); + expect(backendCalled).toBe(true); + // CertManager.trust() must NOT have been called directly by + // CertProvider when the dotnet backend runs — provisioning is the + // backend's job, not the manager's. + expect(trustSpy).toHaveBeenCalledTimes(1); + // (The one call we saw was from the fake generate itself, simulating + // the dotnet side effect.) + }); + + it("propagates selectBackend errors so the user sees the failure", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager } = buildManagerMock(cert, key, thumbprint); + + mockedSelectBackend.mockRejectedValue( + new Error("Requested --backend dotnet but the `dotnet` CLI was not found on PATH.") + ); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + hostCertGenerator: "dotnet", + }); + + const provider = new CertProvider(manager); + await expect(provider.getCertMaterial(true)).rejects.toThrow( + /dotnet.*not found on PATH/ + ); + }); + + it("cleans up the per-provisioning tmp dir even when the backend throws", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager } = buildManagerMock(cert, key, thumbprint); + + let createdDir: string | null = null; + mockedSelectBackend.mockResolvedValue({ + kind: "dotnet", + isAvailable: () => Promise.resolve(true), + generate: vi.fn(async (opts: Shared.GenerateOptions) => { + // The provisioning tmp dir exists at the point generate runs. + // We capture it so we can assert it gets cleaned up on the + // exception path below. + createdDir = opts.outDir; + expect(fs.existsSync(opts.outDir)).toBe(true); + throw new Error("simulated dotnet failure mid-generation"); + }), + }); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + hostCertGenerator: "dotnet", + }); + + const provider = new CertProvider(manager); + await expect(provider.getCertMaterial(true)).rejects.toThrow(/simulated dotnet failure/); + expect(createdDir).not.toBeNull(); + expect(fs.existsSync(createdDir!)).toBe(false); + // Sanity: the tmp dir really did live under os.tmpdir(). + expect(createdDir!.startsWith(os.tmpdir())).toBe(true); + }); +}); From 41ddc01dbbab4e831bb0cb68947d3e631a7b00ec Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 16:20:47 +0000 Subject: [PATCH 08/41] Drop aspire from public backend surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 stays deferred but the placeholder was leaking out of internal notes into user-facing surfaces — `--backend aspire` showed up in `ddc generate --help` and `BackendKind` listed it as a valid choice in the shared type. Both did nothing except throw "not implemented yet," which is a strictly worse UX than not advertising the option at all (users tab-complete it, try it, get an error, file an issue). Removed: - `"aspire"` from the `BackendKind` union in `shared/src/backends/types.ts`. - The rejection branch in `selectBackend()` — unreachable once the type narrows. - `"aspire"` from commander's `--backend` choice list in the CLI. - The CLI test asserting the rejection behavior; with the option no longer in the union, the test wouldn't compile anyway. The VS Code setting's enum never included `aspire` (the host-extension landing in phase 5 was already conservative), so no `package.json` change was needed. When an aspire-aware backend lands, add it back the same way the dotnet backend was wired up — type union entry + `select.ts` branch + commander choice + setting enum + tests. 16 CLI tests (-1), 212 UI tests, type-check + lint clean. --- src/cli/src/index.ts | 2 +- src/cli/tests/select.test.ts | 4 ---- src/shared/src/backends/select.ts | 5 ----- src/shared/src/backends/types.ts | 8 ++++---- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts index 51d3000..42777ce 100644 --- a/src/cli/src/index.ts +++ b/src/cli/src/index.ts @@ -22,7 +22,7 @@ program .option("-o, --out-dir ", "Directory to write artifacts to (default ~/.dev-certs).") .addOption( new Option("-b, --backend ", "Backend selection.") - .choices(["auto", "native", "dotnet", "aspire"]) + .choices(["auto", "native", "dotnet"]) .default("auto") ) .option("--no-trust", "Skip the host trust step (PFX / PEM are still emitted).") diff --git a/src/cli/tests/select.test.ts b/src/cli/tests/select.test.ts index 4440a97..ab34315 100644 --- a/src/cli/tests/select.test.ts +++ b/src/cli/tests/select.test.ts @@ -53,10 +53,6 @@ describe("selectBackend", () => { await expect(selectBackend("dotnet")).rejects.toThrow(/not found on PATH/); }); - it("rejects --backend aspire as not implemented in v0", async () => { - await expect(selectBackend("aspire")).rejects.toThrow(/not implemented yet/); - }); - it("auto-picks dotnet on macOS when dotnet is available", async () => { stubPlatform("darwin"); mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); diff --git a/src/shared/src/backends/select.ts b/src/shared/src/backends/select.ts index ac9ba33..09c9a0b 100644 --- a/src/shared/src/backends/select.ts +++ b/src/shared/src/backends/select.ts @@ -19,11 +19,6 @@ export async function selectBackend(mode: BackendMode): Promise { } return backend; } - if (mode === "aspire") { - throw new Error( - "--backend aspire is not implemented yet. Use 'native' or 'dotnet'." - ); - } if (mode === "auto") return autoSelect(); throw new Error(`Unknown backend mode: ${String(mode)}`); } diff --git a/src/shared/src/backends/types.ts b/src/shared/src/backends/types.ts index b696715..aa2fd86 100644 --- a/src/shared/src/backends/types.ts +++ b/src/shared/src/backends/types.ts @@ -1,9 +1,9 @@ /** * Backend abstraction shared by `ddc` (host CLI) and the VS Code host * extension. Lets both consumers pick between equivalent generators — - * the bundled-in native cert primitives, the `dotnet dev-certs https` CLI - * pass-through, or (future) an Aspire-aware backend — without each having - * to reimplement the selection / availability-detection logic. + * the bundled-in native cert primitives or the `dotnet dev-certs https` + * CLI pass-through — without each having to reimplement the selection / + * availability-detection logic. * * The interface is deliberately narrow: each backend exposes * `isAvailable()` and `generate()`. Trust is bundled into `generate()` so @@ -11,7 +11,7 @@ * shell invocation) don't need a separate trust hook. */ -export type BackendKind = "native" | "dotnet" | "aspire"; +export type BackendKind = "native" | "dotnet"; export type BackendMode = BackendKind | "auto"; From 4809f8e19a480e474e20bdda4cfc16e65e067364 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 16:29:44 +0000 Subject: [PATCH 09/41] Harden runProcess against Windows cwd-first executable lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `child_process.spawn` (and `execFile`, which we use) ultimately calls `CreateProcess` on Windows. When given a bare command name like `"dotnet"`, `CreateProcess` searches the application directory, the current working directory, the system directories, and then PATH — in that order. A malicious `dotnet.exe` dropped into a workspace by a compromised dev-container repo would hijack our shell-outs before the real PATH-resolved binary is ever consulted. The dotnet backend (`runProcess("dotnet", ...)`), the Windows store (`runProcess("certutil.exe", ...)`, `runProcess("pwsh", ...)`), and any future shell-outs all go through `runProcess`. Fixing it in the single chokepoint closes the gap uniformly — there's no longer a spawn site in the codebase that hands a bare name to `execFile` on Windows. The new `resolveSafeExecPath(command, options)` helper: - Returns the command unchanged on non-Windows hosts. `execvp`-style syscalls on Linux / macOS never consult `cwd`, so the cwd-first hijack vector doesn't exist there and we save the disk walk. - Passes through absolute paths and any path containing a separator verbatim — the caller has expressed explicit intent. - On Windows, scans PATH directories (using `path.win32` semantics so tests can drive the Windows branch from a Linux host) and appends each PATHEXT entry to the bare name. Returns the first absolute hit, or `null` if nothing matches. - Filters relative PATH entries (`.`, `bin`, etc.) on Windows. A user who's added them re-introduces the exact hijack vector we're defending against; filtering costs nothing for normal setups and provides defense in depth for hostile ones. - Honors PATHEXT case-insensitively and short-circuits when the caller already supplied a known extension (so `certutil.exe` doesn't get probed as `certutil.exe.cmd`, etc.). `runProcess` wires the resolver into its first step. When the resolver returns `null` on Windows (command not on PATH), we synthesize a `ProcessResult { exitCode: 127, stderr: "command not found on PATH: ..." }` rather than falling through to the unsafe spawn. That gives callers a more actionable error than the raw ENOENT from execFile would have produced, and it's consistent with Unix exit-127 semantics. Ten focused tests in `tests/resolveSafeExecPath.test.ts` cover the matrix: non-Windows no-op, absolute / separator pass-through, PATH + PATHEXT scan, no-match → null, no double-extension, relative entries skipped, case-insensitive PATHEXT, PATH order preserved. All driven synthetically via the `platform` / `searchPath` / `fileExists` options so the suite runs identically on Linux / macOS / Windows. CLI smoke-tested end-to-end (`generate --backend native --no-trust`, `doctor`); existing 222 UI + 79 workspace + 16 CLI tests still green; lint clean. --- src/shared/src/index.ts | 7 +- src/shared/src/platform/processUtil.ts | 132 ++++++++++++++- .../tests/resolveSafeExecPath.test.ts | 151 ++++++++++++++++++ 3 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 src/vscode-ui-extension/tests/resolveSafeExecPath.test.ts diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index 07ca9ce..89d3ca5 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -120,8 +120,11 @@ export type { } from "./platform/windowsStore"; export { trustInNss } from "./platform/nssTrust"; export type { NssTrustResult } from "./platform/nssTrust"; -export { runProcess } from "./platform/processUtil"; -export type { ProcessResult } from "./platform/processUtil"; +export { runProcess, resolveSafeExecPath } from "./platform/processUtil"; +export type { + ProcessResult, + ResolveSafeExecPathOptions, +} from "./platform/processUtil"; // Backend abstraction — selectable cert-generator backends shared by the // host CLI (`ddc`) and the VS Code host extension. Lets both consumers diff --git a/src/shared/src/platform/processUtil.ts b/src/shared/src/platform/processUtil.ts index 0ccdbb9..34e452b 100644 --- a/src/shared/src/platform/processUtil.ts +++ b/src/shared/src/platform/processUtil.ts @@ -1,4 +1,6 @@ import { execFile } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; import { promisify } from "util"; const execFileAsync = promisify(execFile); @@ -9,17 +11,142 @@ export interface ProcessResult { stderr: string; } +export interface ResolveSafeExecPathOptions { + /** + * Override the detected platform. Default is `process.platform`. Tests + * use this to drive the Windows branch from Linux/macOS hosts. + */ + platform?: NodeJS.Platform; + /** + * PATH-like delimiter-separated list of directories to search. Default + * is `process.env.PATH`. + */ + searchPath?: string; + /** + * PATHEXT-style semicolon-separated list of executable suffixes to try + * when the command has no extension. Default is `process.env.PATHEXT`, + * falling back to the conventional Windows set if that's unset. + */ + pathExt?: string; + /** + * File-existence probe. Injectable so tests can drive the resolver + * without writing real fixture files to disk. + */ + fileExists?: (candidate: string) => boolean; +} + +/** + * Resolve a bare command name (`"dotnet"`, `"certutil.exe"`) to an + * absolute path on the executable search PATH, defeating Windows' + * cwd-first `CreateProcess` lookup. By handing `child_process.spawn` + * an absolute path, we skip the application-dir / cwd / system-dirs / + * PATH cascade entirely — a malicious `dotnet.exe` dropped into the + * user's workspace by a compromised dev-container repo can no longer + * hijack our shell-outs. + * + * Behavior: + * + * - Non-Windows hosts: returns the command unchanged. `execvp`-family + * syscalls on Linux / macOS never consult `cwd`, so the cwd-first + * hijack vector doesn't exist there and we save the disk walk. + * - Absolute paths or paths containing a separator: pass through + * verbatim. The caller has expressed explicit intent (e.g. an env + * var override) and we don't second-guess. + * - Bare names on Windows: scan PATH, respecting PATHEXT for implicit + * extensions. Returns `null` if the command isn't found on PATH — + * the caller (typically `runProcess`) translates that into a + * well-defined "command not found" `ProcessResult` rather than + * falling through to the unsafe spawn. + * - Relative PATH entries are skipped on Windows. A user who's added + * `.` (or `bin`, or any other relative directory) to PATH would + * re-introduce the exact hijack we're trying to prevent. Filtering + * them costs nothing for normal setups and provides defense in + * depth for hostile ones. + */ +export function resolveSafeExecPath( + command: string, + options: ResolveSafeExecPathOptions = {} +): string | null { + const platform = options.platform ?? process.platform; + + if (platform !== "win32") { + return command; + } + + // Use Windows path semantics regardless of the host OS — tests drive + // the Windows branch from Linux/macOS via the `platform` override and + // expect `C:\...` to be recognized as absolute, `;` as the PATH + // delimiter, etc. + const winPath = path.win32; + + if ( + winPath.isAbsolute(command) || + command.includes("/") || + command.includes("\\") + ) { + return command; + } + + const searchPath = options.searchPath ?? process.env.PATH ?? ""; + const pathExt = + options.pathExt ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD"; + const fileExists = options.fileExists ?? fs.existsSync; + + const dirs = searchPath + .split(winPath.delimiter) + .map((d) => d.trim()) + .filter((d) => d.length > 0 && winPath.isAbsolute(d)); + const extensions = pathExt + .split(";") + .map((e) => e.trim().toLowerCase()) + .filter((e) => e.length > 0); + + const commandLower = command.toLowerCase(); + const hasExplicitExtension = extensions.some((ext) => + commandLower.endsWith(ext) + ); + + for (const dir of dirs) { + if (hasExplicitExtension) { + const candidate = winPath.join(dir, command); + if (fileExists(candidate)) return candidate; + } else { + for (const ext of extensions) { + const candidate = winPath.join(dir, command + ext); + if (fileExists(candidate)) return candidate; + } + } + } + + return null; +} + /** * Run an external process and return its exit code, stdout, and stderr. * Does not throw on non-zero exit codes. + * + * On Windows, the command is resolved through `resolveSafeExecPath` + * before being handed to `execFile`. This is the single chokepoint for + * all of our shell-outs (dotnet backend, certutil.exe in the Windows + * store, pwsh, openssl, etc.), so the cwd-planting defense applies + * uniformly. Commands that resolve to `null` (not found on PATH) return + * `exitCode: 127` rather than falling through to a `cwd`-first spawn. */ export async function runProcess( command: string, args: string[], timeout: number = 30000 ): Promise { + const resolved = resolveSafeExecPath(command); + if (resolved === null) { + return { + exitCode: 127, + stdout: "", + stderr: `command not found on PATH: ${command}`, + }; + } try { - const result = await execFileAsync(command, args, { timeout }); + const result = await execFileAsync(resolved, args, { timeout }); return { exitCode: 0, stdout: result.stdout, stderr: result.stderr }; } catch (err: unknown) { const error = err as Error & { @@ -28,8 +155,7 @@ export async function runProcess( stderr?: string; }; // If the process ran but returned non-zero, we still have stdout/stderr - const exitCode = - typeof error.code === "number" ? error.code : 1; + const exitCode = typeof error.code === "number" ? error.code : 1; return { exitCode, stdout: error.stdout ?? "", diff --git a/src/vscode-ui-extension/tests/resolveSafeExecPath.test.ts b/src/vscode-ui-extension/tests/resolveSafeExecPath.test.ts new file mode 100644 index 0000000..23edd07 --- /dev/null +++ b/src/vscode-ui-extension/tests/resolveSafeExecPath.test.ts @@ -0,0 +1,151 @@ +import { describe, it, expect } from "vitest"; +import { resolveSafeExecPath } from "@devcontainer-dev-certs/shared"; + +/** + * The resolver is Windows-specific defense against `CreateProcess`'s + * cwd-first executable lookup. These tests drive the Windows branch + * from any host by passing `platform: "win32"` plus an injected + * `fileExists` probe and synthetic PATH / PATHEXT — no fixture files + * on disk, no real environment dependencies. + */ +describe("resolveSafeExecPath", () => { + it("returns the command unchanged on Linux", () => { + const result = resolveSafeExecPath("dotnet", { + platform: "linux", + searchPath: "/usr/bin", + fileExists: () => false, + }); + expect(result).toBe("dotnet"); + }); + + it("returns the command unchanged on macOS", () => { + const result = resolveSafeExecPath("dotnet", { + platform: "darwin", + searchPath: "/usr/local/bin", + fileExists: () => false, + }); + expect(result).toBe("dotnet"); + }); + + it("passes through absolute Windows paths verbatim", () => { + const explicit = "C:\\Program Files\\dotnet\\dotnet.exe"; + const result = resolveSafeExecPath(explicit, { + platform: "win32", + searchPath: "C:\\Windows\\System32", + fileExists: () => true, + }); + expect(result).toBe(explicit); + }); + + it("passes through Windows paths with a separator verbatim", () => { + const result = resolveSafeExecPath("bin\\dotnet.exe", { + platform: "win32", + searchPath: "C:\\Windows\\System32", + fileExists: () => true, + }); + expect(result).toBe("bin\\dotnet.exe"); + }); + + it("scans PATH and appends PATHEXT entries for bare names on Windows", () => { + const fileExists = (candidate: string): boolean => + candidate === "C:\\Program Files\\dotnet\\dotnet.exe"; + + const result = resolveSafeExecPath("dotnet", { + platform: "win32", + searchPath: [ + "C:\\Windows\\System32", + "C:\\Program Files\\dotnet", + ].join(";"), + pathExt: ".COM;.EXE;.BAT;.CMD", + fileExists, + }); + + expect(result).toBe("C:\\Program Files\\dotnet\\dotnet.exe"); + }); + + it("returns null when the command isn't found anywhere on Windows PATH", () => { + const result = resolveSafeExecPath("nonexistent-tool", { + platform: "win32", + searchPath: "C:\\Windows\\System32;C:\\Program Files\\dotnet", + pathExt: ".EXE;.CMD", + fileExists: () => false, + }); + expect(result).toBeNull(); + }); + + it("does not append an extension to a command that already has one", () => { + const seen: string[] = []; + const fileExists = (candidate: string): boolean => { + seen.push(candidate); + return candidate === "C:\\Windows\\System32\\certutil.exe"; + }; + + const result = resolveSafeExecPath("certutil.exe", { + platform: "win32", + searchPath: "C:\\Windows\\System32", + pathExt: ".EXE;.CMD;.BAT", + fileExists, + }); + + expect(result).toBe("C:\\Windows\\System32\\certutil.exe"); + // None of the probed candidates should be `certutil.exe.cmd`, + // `certutil.exe.bat`, etc. — the extension matcher must short-circuit. + expect(seen.every((p) => !/\.exe\.[a-z]+$/i.test(p))).toBe(true); + }); + + it("skips relative PATH entries to defeat cwd-equivalent hijacks", () => { + const fileExists = (candidate: string): boolean => { + // Anything probed from a relative entry would be 'bad-dir\dotnet.exe'. + // Our resolver should never even check it. + if (candidate.startsWith("bad-dir")) { + throw new Error( + `Resolver probed a relative PATH entry: ${candidate} — that's exactly the hijack vector we're defending against.` + ); + } + return candidate === "C:\\Program Files\\dotnet\\dotnet.exe"; + }; + + const result = resolveSafeExecPath("dotnet", { + platform: "win32", + searchPath: ["bad-dir", ".", "C:\\Program Files\\dotnet"].join(";"), + pathExt: ".EXE", + fileExists, + }); + + expect(result).toBe("C:\\Program Files\\dotnet\\dotnet.exe"); + }); + + it("treats PATHEXT case-insensitively (handles upper/mixed case extensions)", () => { + // Real-world Windows defaults to upper-case `.COM;.EXE;.BAT;.CMD`, but the + // matcher must work whether the user has rewritten it or not — and whether + // the command was passed with a `.EXE` or `.exe` suffix. The filesystem + // itself is case-insensitive on Windows, mirrored here in the probe. + const fileExists = (candidate: string): boolean => + candidate.toLowerCase() === "c:\\windows\\system32\\certutil.exe"; + + const result = resolveSafeExecPath("certutil.EXE", { + platform: "win32", + searchPath: "C:\\Windows\\System32", + pathExt: ".COM;.EXE;.BAT;.CMD", + fileExists, + }); + + expect(result).toBe("C:\\Windows\\System32\\certutil.EXE"); + }); + + it("returns the first match wins (PATH order preserved)", () => { + // Two candidate matches; the earlier PATH entry must win. + const fileExists = (candidate: string): boolean => + candidate === "C:\\first\\dotnet.exe" || + candidate === "C:\\second\\dotnet.exe"; + + const result = resolveSafeExecPath("dotnet", { + platform: "win32", + searchPath: ["C:\\first", "C:\\second"].join(";"), + pathExt: ".EXE", + fileExists, + }); + + expect(result).toBe("C:\\first\\dotnet.exe"); + }); +}); From 30a2c4ed680ec3a3bf1a2893bd398f72e6242594 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 17:39:44 +0000 Subject: [PATCH 10/41] Document the ddc CLI in the manual-setup example and a new CLI README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ddc` has been in the tree for a couple of commits but nothing pointed at it from the docs, so non-VS-Code users following `examples/manual-setup/README.md` would still walk through the `dotnet dev-certs https` + hand-compute-thumbprint + hand-edit-JSON ritual that `ddc generate` now collapses into one command. - `src/cli/README.md` (new): the canonical CLI reference. Covers what ddc is, the build-from-source install path (no published binary yet — the README is honest about this), each of the five commands with their full flag tables and short rationales, an explanation of the backend selection, and a Limitations section that calls out three real footguns (no published binary, native backend writes to the platform store even with `--no-trust`, no reverse-sync). - `examples/manual-setup/README.md`: leads with `ddc generate` and keeps the existing `dotnet dev-certs` + `openssl` walkthrough as a fallback path. Cross-references the CLI README for install and command details rather than duplicating them. Adds `ddc doctor` to the verification section. - Root `README.md`: brief subsection under "Manual / non-VS Code use" pointing at `src/cli/README.md` so people who land on the root doc discover the CLI without having to click through to the example first. Cross-checked every technical claim (cert key size, SAN list, default out-dir, default container mount, `--name` default, `ddc trust` already-trusted short-circuit) against the actual source before committing. No code changes; lint / type-check / tests unaffected. --- README.md | 11 +++ examples/manual-setup/README.md | 39 +++++++- src/cli/README.md | 160 ++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 5 deletions(-) create mode 100644 src/cli/README.md diff --git a/README.md b/README.md index a4e9e6c..2df5c45 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,17 @@ The companion-extension pattern is VS Code-specific, but the underlying containe For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference. +### `ddc` — the host-side CLI + +[`ddc`](src/cli/README.md) is the host-side CLI that produces the cert files and `bundle.json` the in-container installer below consumes. One command does generation, host trust, and bundle emission: + +```bash +mkdir -p ~/.dev-certs +ddc generate --out-dir ~/.dev-certs +``` + +It also exposes `ddc inspect` (cert details), `ddc bundle` (wrap an existing cert into a bundle.json), `ddc trust` (host-trust an existing cert), and `ddc doctor` (read-only diagnostics). See [`src/cli/README.md`](src/cli/README.md) for the full reference. Doing the steps by hand is documented in [`examples/manual-setup/`](examples/manual-setup/) for situations where the CLI isn't available. + ### The fallback installer The feature delivers `devcontainer-dev-certs-install` to `/usr/local/bin/` during install. It writes to the same canonical paths the VS Code workspace extension uses — `~/.dotnet/corefx/cryptography/x509stores/{my,root}` and `~/.aspnet/dev-certs/trust/` (with c_rehash symlinks) — so Kestrel's `X509Store` fallback and OpenSSL clients discover certs installed this way exactly as they would extension-installed ones. diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md index 53976cb..9296e9c 100644 --- a/examples/manual-setup/README.md +++ b/examples/manual-setup/README.md @@ -11,9 +11,30 @@ The same canonical trust state the VS Code workspace extension produces — `~/. - The `devcontainer-dev-certs` feature in your `devcontainer.json` with `installFallbackTools: true` (or `openssl` and `jq` already present in your base image). - A directory on the host containing the cert files you want installed, plus a `bundle.json` describing them. -## One-time host setup +The host-side cert + `bundle.json` can be produced two ways. **The `ddc` CLI is the simpler path** — one command does generation, host trust, and `bundle.json` emission. The manual path (still documented below) is what you'd reach for when `ddc` isn't available, when you need a cert from a different source, or when you're sharing a specific cert with the VS Code host extension. -Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`). On Windows / macOS / Linux: +## One-time host setup (with `ddc`) + +`ddc` is the host-side CLI included in this repository. See [`src/cli/README.md`](../../src/cli/README.md) for install instructions; the short version while no published binary exists is "clone the repo and `cd src/cli && npm install && node esbuild.mjs`". + +Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`) and generate everything in one shot: + +```bash +mkdir -p ~/.dev-certs +ddc generate --out-dir ~/.dev-certs +``` + +This: + +1. Generates an ASP.NET-compatible dev cert (RSA-2048, the standard `localhost` + `*.dev.localhost` + docker SANs, the ASP.NET dev-cert OID marker so Kestrel finds it). +2. Trusts it on the host (Linux/macOS/Windows — same backend the VS Code host extension uses, or `dotnet dev-certs --trust` on macOS when `dotnet` is on PATH). +3. Writes `aspnetcore-dev.pfx`, `aspnetcore-dev.pem`, `aspnetcore-dev.key`, and `bundle.json` into the out-dir, with `bundle.json` already wired to the container-mount path (`/host-dev-certs` by default). + +Skip to "[Project setup](#project-setup)" — no other host steps required. + +## One-time host setup (manually) + +Pick a host directory to hold your certs and bundle file: ```bash mkdir -p ~/.dev-certs @@ -38,7 +59,7 @@ openssl x509 -in ~/.dev-certs/aspnetcore-dev.pem -noout -fingerprint -sha1 \ Drop a copy of [`bundle.json`](./bundle.json) into `~/.dev-certs/` and replace `REPLACE_WITH_SHA1_FINGERPRINT_NO_COLONS` with the fingerprint you just computed. -> **Note on `dotnet dev-certs`-generated certs vs the host extension's certs.** This example assumes you're using `dotnet dev-certs https` for cert generation. The host extension produces functionally equivalent certs with the same OID marker and SAN entries — either source works against the same fallback installer in the container. If you need to share a *specific* cert with the host extension (e.g. a Windows developer also runs the extension), generate it once and have both flows consume the same PFX. +> **Note on `dotnet dev-certs`-generated certs vs the host extension's certs.** This manual path uses `dotnet dev-certs https` for cert generation. The host extension produces functionally equivalent certs with the same OID marker and SAN entries — either source works against the same fallback installer in the container. If you need to share a *specific* cert with the host extension (e.g. a Windows developer also runs the extension), generate it once and have both flows consume the same PFX. ## Project setup @@ -50,7 +71,7 @@ Copy the bits of [`devcontainer.json`](./devcontainer.json) you want into your o The fallback installer is delivered to `/usr/local/bin/devcontainer-dev-certs-install` by the feature. -Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`dotnet dev-certs https --clean && dotnet dev-certs https --trust && …re-export…`) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. +Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`ddc generate` again, or the manual `dotnet dev-certs https --clean && …re-export…` ritual) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. ## Verifying @@ -62,6 +83,12 @@ devcontainer-dev-certs-install --doctor You should see `[ok]` for every check. If you see `[fail]` or `[warn]`, the message tells you what to fix. +On the host, `ddc doctor` gives equivalent diagnostics for the host side (which backends are available, whether the cert is in the host platform store and trusted): + +```bash +ddc doctor +``` + You can also sanity-check from inside the container: ```bash @@ -94,10 +121,12 @@ The bundle is a list — add corporate CAs, wildcard certs, etc. as additional e CA-only entries (no `pfxPath`, no `pemKeyPath`) are valid — they get planted in the trust store but no private key is synced. +`ddc bundle ` emits a single-cert `bundle.json` for an arbitrary cert file (auto-discovers sibling `.pem` / `.key` / `.pfx`, fills in the SHA-1 thumbprint, rewrites paths to the container mount). Merge its output into your existing bundle by hand to add a cert. + See the [bundle schema](../../schema/bundle.schema.json) for the full field reference. ## Limitations -- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `dotnet dev-certs https --trust` (the example above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. +- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `ddc generate` (the example above does this — it runs the host trust step), `dotnet dev-certs https --trust` (the manual path above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. - **No `defaultKestrelCertificate` equivalent.** The VS Code-only `defaultKestrelCertificate` setting writes `ASPNETCORE_Kestrel__Certificates__Default__Path/__Password` via VS Code's `EnvironmentVariableCollection`. To pin a custom Kestrel default outside VS Code, set those env vars yourself in `devcontainer.json` `containerEnv`. - **No reverse sync (container → host).** The `syncContainerCert` flow needs a privileged host-side process to add the cert to the host OS trust store; without the host extension's UI there's nowhere to surface the consent prompt. diff --git a/src/cli/README.md b/src/cli/README.md new file mode 100644 index 0000000..764d641 --- /dev/null +++ b/src/cli/README.md @@ -0,0 +1,160 @@ +# ddc + +`ddc` is the host-side CLI for [devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs). It generates ASP.NET-compatible HTTPS development certificates, trusts them on the host, inspects existing cert files, and emits the `bundle.json` the in-container installer reads — without VS Code being involved. + +## Why this exists + +The host extension automates everything when you use VS Code. Outside of VS Code (JetBrains, Vim, raw CLI, CI), the canonical path to the same trust state is: + +1. Generate a cert. +2. Trust it on the host OS. +3. Hand-write a `bundle.json` referencing the cert files inside the container's bind-mount. +4. Hand-compute the SHA-1 thumbprint and paste it into the bundle. + +`ddc generate` is one command that does all four. The other commands (`inspect`, `bundle`, `trust`, `doctor`) cover the rest of the lifecycle. + +`ddc` uses the same shared cert + platform code the VS Code host extension uses — same trust paths, same SAN list, same OID marker — so a cert produced by `ddc` is interchangeable with one produced by the extension. + +## Install + +There is no published binary yet. To use `ddc` today, build it from this repository: + +```bash +git clone https://github.com/dnegstad/devcontainer-dev-certs.git +cd devcontainer-dev-certs +npm install +cd src/cli && node esbuild.mjs +``` + +That produces `dist/ddc.js`, an executable Node script. Invoke it directly: + +```bash +node dist/ddc.js --help +``` + +Or symlink it onto your PATH: + +```bash +chmod +x dist/ddc.js +ln -s "$(pwd)/dist/ddc.js" ~/.local/bin/ddc +ddc --help +``` + +Node 18 or newer is required. + +## Quick start + +```bash +mkdir -p ~/.dev-certs +ddc generate --out-dir ~/.dev-certs +``` + +That produces in `~/.dev-certs`: + +- `aspnetcore-dev.pfx` — cert + private key in PKCS#12, passwordless +- `aspnetcore-dev.pem` — cert in PEM +- `aspnetcore-dev.key` — private key in PEM +- `bundle.json` — manifest the in-container installer reads, with paths rewritten to the container's bind-mount target (`/host-dev-certs` by default) + +The cert is also added to your host OS trust store (Linux NSS DB / macOS keychain / Windows cert store) so browsers accept forwarded ports. + +Bind-mount the directory into the container and have the in-container installer consume the bundle — see [`examples/manual-setup/`](../../examples/manual-setup/) for the full devcontainer.json. + +## Commands + +### `ddc generate` + +Generate a dev cert, trust it on the host, and emit `bundle.json`. + +``` +ddc generate [--out-dir ] [--backend auto|native|dotnet] + [--no-trust] [--container-mount ] [--no-bundle] [--verbose] +``` + +| Flag | Default | Notes | +|------|---------|-------| +| `--out-dir ` | `~/.dev-certs` | Directory to write artifacts to. | +| `--backend ` | `auto` | Cert generator backend. `auto` prefers `dotnet` on macOS when the `dotnet` CLI is on PATH (better keychain-trust UX via a signed binary); `native` everywhere else. | +| `--no-trust` | off | Skip the host OS trust step. PFX / PEM / `bundle.json` are still emitted. | +| `--container-mount ` | `/host-dev-certs` | Container-side mount target the out-dir bind-mounts to. Recorded into `bundle.json` so the in-container installer reads from the right path. | +| `--no-bundle` | off | Skip emitting `bundle.json`. | +| `--verbose` | off | Stream shared-layer log lines to stderr. | + +### `ddc inspect ` + +Print details about a PFX or PEM certificate. + +``` +ddc inspect path/to/aspnetcore-dev.pfx +``` + +Reports the subject CN, both SHA-1 and SHA-256 thumbprints, validity window, ASP.NET dev-cert OID and version byte (so you can tell whether the cert is fresh enough for the current installer), every SAN entry (with `[non-local]` flags on any that aren't on the standard developer-cert allowlist), and warnings (cert without key, expiring soon, non-local SANs present). + +Pass `--json` for machine-readable output: + +``` +ddc inspect --json path/to/cert.pfx +``` + +### `ddc bundle ` + +Emit a single-cert `bundle.json` referencing an already-existing cert file. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by naming convention so a single PFX argument is usually enough. + +``` +ddc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] + [--name ] [--kind dotnet-dev|user] + [--no-trust-in-container] +``` + +| Flag | Default | Notes | +|------|---------|-------| +| `--out-dir ` | directory of cert-path | Where to write `bundle.json`. | +| `--container-mount ` | `/host-dev-certs` | Container-side mount target. | +| `--name ` | basename of cert-path | Filename stem used in the bundle (`{name}.pem`, etc.). | +| `--kind ` | `user` | `dotnet-dev` uses the historic `aspnetcore-localhost-{thumbprint}.pem` filename in the OpenSSL trust dir; `user` uses `{name}.pem`. | +| `--no-trust-in-container` | trust-in-container is on by default | Mark the entry as `trustInContainer: false` — cert is served only, not added to trust stores inside the container. | + +Useful for wrapping a cert produced by something else (a corporate CA, a manual `dotnet dev-certs` invocation, a wildcard cert generated by another tool) into the bundle format the in-container installer expects. + +### `ddc trust ` + +Add an existing cert to the host OS trust store via the same shared platform layer the VS Code host extension uses. + +``` +ddc trust path/to/cert.pfx +``` + +Short-circuits with an "already trusted" message when the cert is already in the trust store — repeated invocations don't re-prompt. + +### `ddc doctor` + +Read-only diagnostics: which backends are available, what `--backend auto` would pick, host platform-store state, and (on Linux) `openssl` / `certutil` presence on PATH. + +``` +ddc doctor [--out-dir ] +``` + +Exits non-zero if any check reports `[fail]`. `[warn]` is informational and exits zero. + +## Bundle JSON + +The `bundle.json` written by `ddc generate` and `ddc bundle` conforms to the schema at [`schema/bundle.schema.json`](../../schema/bundle.schema.json) and is the same format the in-container `devcontainer-dev-certs-install --bundle-json` installer accepts. + +For multi-cert setups (auto-generated dev cert + corporate CA + extra wildcard), the simplest workflow is `ddc generate` to seed the bundle and then hand-edit additional entries in. The bundle is a list — see the [root README](../../README.md#manual--non-vs-code-use) for the field reference. + +## Backends + +Two backends today; both produce certs the in-container installer accepts. + +- **`native`** uses the bundled cert primitives (the same Node + `@peculiar/x509` + `pkijs` code path the VS Code host extension uses). No external runtime dependencies. Works on every platform. +- **`dotnet`** shells out to `dotnet dev-certs https`. Requires the dotnet SDK on PATH. On macOS this gives a more polished keychain trust prompt (the calling binary is Apple-notarized); on Windows / Linux it's functionally equivalent to native. + +`--backend auto` (the default) picks dotnet on macOS when available, native everywhere else. The VS Code host extension has the same selection logic, controlled by the `devcontainerDevCerts.hostCertGenerator` setting. + +On Windows, the dotnet backend (and any other shell-out in this tool) resolves its command through PATH before spawning, so a malicious binary planted in the working directory can't hijack the lookup. + +## Limitations + +- **No published binary yet.** Build from source as described above. +- **Native backend writes to the platform store even with `--no-trust`.** `--no-trust` skips the OS trust prompt but the cert still lands in `~/.dotnet/corefx/cryptography/x509stores/my/` (or the Windows store) because that's where the manager generates into. The flag controls the trust step, not the generation step. +- **No reverse sync.** The VS Code workspace extension's `syncContainerCert` flow (pushing a container-side cert back to the host) needs the host extension's consent UI; there's no equivalent in the CLI. From 08fb2a1a92c9b81408f3b961d490b9f573ab651d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 17:47:22 +0000 Subject: [PATCH 11/41] Native backend: skip the platform store entirely on --no-trust MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit, `ddc generate --no-trust` still wrote the cert into `~/.dotnet/corefx/cryptography/x509stores/my/` (the .NET X509 store) because the native backend always drove `CertManager`, and `CertManager.generate(false)` persists to the store as part of its contract. That was a footgun: the typical `--no-trust` caller is asking for "cert files I can bind-mount into a container" and doesn't expect their host's .NET dev cert state to be mutated. The native backend now has two code paths: - `noTrust: true` generates the cert purely in memory via the shared `generateCertificate` primitive and writes only to `--out-dir`. `CertManager` is never instantiated, so the platform store stays untouched. The trade-off is that `--no-trust` invocations are no longer idempotent w.r.t. an existing store cert — but with no store residency that's the right semantics: the user has opted out of host state. - `noTrust: false` keeps the existing `CertManager.trust()` flow. Writing to the .NET store on this path is the host-trust contract, not a side effect — it's where `dotnet dev-certs --check`, host- running Kestrel, and the VS Code host extension all look. The dotnet backend's `--no-trust` semantics are necessarily different. `dotnet dev-certs https` always persists the cert into the .NET store regardless of `--trust`; we can't paper over that without reimplementing what the dotnet binary does, which is the entire point of using that backend. A comment on `DotnetBackend` calls this out so callers with strict-isolation requirements know to pick `native`. Four new tests in `tests/nativeBackend.test.ts` redirect `$HOME` to a fresh tmpdir, run `generate({noTrust: true})`, and assert (a) the cert files land in `outDir`, (b) the thumbprint round-trips through the PFX reparse, (c) nothing was created under `$HOME` — `fakeHome/.dotnet/...` doesn't exist and `fakeHome` itself stays empty — and (d) successive invocations produce distinct certs (confirming the path doesn't accidentally fall through to a store-based cache). Smoke-tested end-to-end: `HOME=$tmp ddc generate --no-trust` writes four files to `--out-dir` and leaves the redirected home empty. CLI README's Limitations section was outdated by the fix; replaced the bullet with a `--no-trust semantics` subsection explaining the per-backend behavior so users know which backend honors strict isolation. 226 UI tests (+4 new), 79 workspace, 16 CLI; lint + type-check clean. --- src/cli/README.md | 10 +- src/shared/src/backends/dotnet.ts | 10 ++ src/shared/src/backends/native.ts | 111 ++++++++++++------ .../tests/nativeBackend.test.ts | 111 ++++++++++++++++++ 4 files changed, 206 insertions(+), 36 deletions(-) create mode 100644 src/vscode-ui-extension/tests/nativeBackend.test.ts diff --git a/src/cli/README.md b/src/cli/README.md index 764d641..04b5e9a 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -153,8 +153,16 @@ Two backends today; both produce certs the in-container installer accepts. On Windows, the dotnet backend (and any other shell-out in this tool) resolves its command through PATH before spawning, so a malicious binary planted in the working directory can't hijack the lookup. +## `--no-trust` semantics + +The two backends honor `--no-trust` differently: + +- **`--backend native --no-trust`** generates the cert purely in memory and writes only to `--out-dir`. The host's `.NET` X509 store and OS trust store are not touched. This is the right choice for "give me cert files to bind-mount into a container, don't install anything on my host." +- **`--backend dotnet --no-trust`** skips the OS trust prompt, but `dotnet dev-certs https` still persists the cert into the `.NET` X509 store as a side effect — that's how `dotnet dev-certs` itself works, regardless of `--trust`. If you want strict file-only output, use the native backend. + +Without `--no-trust`, both backends write to the `.NET` X509 store and trust the cert in the OS — that's the host-trust contract (it's where `dotnet dev-certs --check`, host-running Kestrel, and the VS Code host extension all look for the cert). + ## Limitations - **No published binary yet.** Build from source as described above. -- **Native backend writes to the platform store even with `--no-trust`.** `--no-trust` skips the OS trust prompt but the cert still lands in `~/.dotnet/corefx/cryptography/x509stores/my/` (or the Windows store) because that's where the manager generates into. The flag controls the trust step, not the generation step. - **No reverse sync.** The VS Code workspace extension's `syncContainerCert` flow (pushing a container-side cert back to the host) needs the host extension's consent UI; there's no equivalent in the CLI. diff --git a/src/shared/src/backends/dotnet.ts b/src/shared/src/backends/dotnet.ts index daeea92..e8c9d85 100644 --- a/src/shared/src/backends/dotnet.ts +++ b/src/shared/src/backends/dotnet.ts @@ -18,6 +18,16 @@ import type { Backend, GenerateOptions, GenerateResult } from "./types"; * `dotnet dev-certs --format ...` only accepts one format per call, and * `--trust` only does anything on the first invocation anyway (it's * idempotent w.r.t. the OS trust store). + * + * `noTrust` only suppresses the OS-trust step here; it does NOT + * suppress the .NET store side effect. `dotnet dev-certs https` + * always persists the generated cert into the .NET X509Store + * regardless of `--trust`. If a caller needs strict isolation — + * cert files in `outDir` and nothing else — they should use the + * native backend, which honors `noTrust` by skipping the store + * entirely. We can't paper over this here without re-implementing + * what `dotnet dev-certs` does, which is the entire point of using + * the dotnet backend in the first place. */ export class DotnetBackend implements Backend { readonly kind = "dotnet" as const; diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts index cd98daf..3d6c49f 100644 --- a/src/shared/src/backends/native.ts +++ b/src/shared/src/backends/native.ts @@ -1,14 +1,33 @@ import * as fs from "fs"; import * as path from "path"; -import { CertManager } from "../cert/manager"; +import { exportPem, exportPfx } from "../cert/exporter"; +import { generateCertificate } from "../cert/generator"; import { loadPfx } from "../cert/loader"; +import { CertManager } from "../cert/manager"; +import { VALIDITY_DAYS } from "../cert/properties"; import type { Backend, GenerateOptions, GenerateResult } from "./types"; /** - * Native backend: uses the in-tree `CertManager` directly. Same code path - * the VS Code host extension uses for generation, host trust, and - * platform-store I/O — no shelling out to other tools, no `dotnet` runtime - * required. + * Native backend: uses the in-tree cert primitives directly — no + * shelling out to other tools, no `dotnet` runtime required. + * + * Two code paths, picked by `noTrust`: + * + * - `noTrust: false` (default): drive `CertManager` end-to-end. The + * generated cert lands in the host's OS platform store + * (`~/.dotnet/corefx/cryptography/x509stores/my/` on Linux/macOS, + * `CurrentUser\My` on Windows) and is added to the OS trust store. + * Living in the .NET store is part of the host-trust contract — it's + * where `dotnet dev-certs --check`, host-running Kestrel, and the + * VS Code host extension all look — so writing there is the point of + * the operation, not a side effect. + * + * - `noTrust: true`: generate purely in memory and write only to + * `outDir`. The platform store is NOT touched. The typical caller + * here is `ddc generate --no-trust` for "give me cert files to + * bind-mount into a container" — that user has explicitly opted out + * of host-side cert installation, so we honor it by keeping our + * side effects bounded to `outDir`. */ export class NativeBackend implements Backend { readonly kind = "native" as const; @@ -23,39 +42,61 @@ export class NativeBackend implements Backend { async generate(options: GenerateOptions): Promise { fs.mkdirSync(options.outDir, { recursive: true }); - const manager = new CertManager(); if (options.noTrust) { - // Generate-only: produce a cert, save it to the platform store, but - // don't run the OS trust-prompt path. - await manager.generate(false); - } else { - await manager.trust(); + return generateFilesOnly(options.outDir); } + return generateAndTrust(options.outDir); + } +} - await manager.exportCert("pfx", options.outDir); - await manager.exportCert("pem", options.outDir); - - const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); - const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); - const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.key"); - - // Recover the thumbprint by re-reading the exported PFX. Cheaper than - // reaching into the manager's private state and keeps the contract - // symmetric with the `dotnet` backend's recovery step. - const loaded = await loadPfx(pfxPath); - if (!loaded.cert) { - throw new Error( - `Native backend wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` - ); - } +async function generateFilesOnly(outDir: string): Promise { + const now = new Date(); + const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); + const { cert, key, thumbprint } = await generateCertificate(now, expiry); + + const pfxPath = await exportPfx(cert, key, outDir); + const { certPath: pemPath, keyPath: pemKeyPath } = exportPem( + cert, + key, + outDir + ); - return { - pfxPath, - pemPath, - pemKeyPath, - thumbprint: loaded.cert.thumbprintSha1, - trusted: !options.noTrust, - backendUsed: "native", - }; + return { + pfxPath, + pemPath, + pemKeyPath, + thumbprint, + trusted: false, + backendUsed: "native", + }; +} + +async function generateAndTrust(outDir: string): Promise { + const manager = new CertManager(); + await manager.trust(); + await manager.exportCert("pfx", outDir); + await manager.exportCert("pem", outDir); + + const pfxPath = path.join(outDir, "aspnetcore-dev.pfx"); + const pemPath = path.join(outDir, "aspnetcore-dev.pem"); + const pemKeyPath = path.join(outDir, "aspnetcore-dev.key"); + + // Recover the thumbprint by re-reading the exported PFX. Cheaper than + // reaching into the manager's private state and keeps the contract + // symmetric with the `dotnet` backend's recovery step. + const loaded = await loadPfx(pfxPath); + if (!loaded.cert) { + throw new Error( + `Native backend wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` + ); } + + return { + pfxPath, + pemPath, + pemKeyPath, + thumbprint: loaded.cert.thumbprintSha1, + trusted: true, + backendUsed: "native", + }; } diff --git a/src/vscode-ui-extension/tests/nativeBackend.test.ts b/src/vscode-ui-extension/tests/nativeBackend.test.ts new file mode 100644 index 0000000..db49348 --- /dev/null +++ b/src/vscode-ui-extension/tests/nativeBackend.test.ts @@ -0,0 +1,111 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { NativeBackend } from "@devcontainer-dev-certs/shared"; +import { loadPfx } from "@devcontainer-dev-certs/shared"; + +/** + * NativeBackend has two distinct code paths picked by `noTrust`. + * + * - `noTrust: true`: generate purely in memory, write only to `outDir`. + * The platform store (`~/.dotnet/corefx/cryptography/x509stores/my/` + * on Linux/macOS) must NOT be touched — the caller has explicitly + * opted out of host-side cert installation. + * - `noTrust: false`: drive `CertManager` end-to-end, including writing + * to the platform store. That's the host-trust contract; we don't + * test that path here because it requires interactive trust prompts + * on macOS/Windows and root NSS hooks on Linux. + * + * Both paths are exercised on a redirected `$HOME` so any accidental + * store write would land in our tmpdir where we can audit it. + */ +describe("NativeBackend.generate with --no-trust", () => { + let originalHome: string | undefined; + let fakeHome: string; + let outDir: string; + let backend: NativeBackend; + + beforeEach(() => { + originalHome = process.env.HOME; + fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-nativebackend-home-")); + process.env.HOME = fakeHome; + outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-nativebackend-out-")); + backend = new NativeBackend(); + }); + + afterEach(() => { + fs.rmSync(fakeHome, { recursive: true, force: true }); + fs.rmSync(outDir, { recursive: true, force: true }); + if (originalHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = originalHome; + } + }); + + it("writes pfx + pem + key into outDir and reports trusted=false", async () => { + const result = await backend.generate({ outDir, noTrust: true }); + + expect(result.trusted).toBe(false); + expect(result.backendUsed).toBe("native"); + expect(fs.existsSync(result.pfxPath)).toBe(true); + expect(fs.existsSync(result.pemPath)).toBe(true); + expect(result.pemKeyPath).not.toBeNull(); + expect(fs.existsSync(result.pemKeyPath!)).toBe(true); + expect(result.pfxPath).toBe(path.join(outDir, "aspnetcore-dev.pfx")); + expect(result.pemPath).toBe(path.join(outDir, "aspnetcore-dev.pem")); + expect(result.pemKeyPath).toBe(path.join(outDir, "aspnetcore-dev.key")); + }); + + it("reports a SHA-1 thumbprint that matches the reparsed PFX", async () => { + const result = await backend.generate({ outDir, noTrust: true }); + + const loaded = await loadPfx(result.pfxPath); + expect(loaded.cert.thumbprintSha1).toBe(result.thumbprint); + // SHA-1 is 40 hex chars; uppercase per shared cert primitives. + expect(result.thumbprint).toMatch(/^[0-9A-F]{40}$/); + }); + + it("does NOT write into the platform store directory under HOME", async () => { + await backend.generate({ outDir, noTrust: true }); + + // The .NET X509Store on Linux/macOS lives under + // ~/.dotnet/corefx/cryptography/x509stores/my/. With our redirected + // $HOME, that translates to fakeHome/.dotnet/... — and since the + // noTrust path bypasses CertManager entirely, nothing under fakeHome + // should have been created. + const storeDir = path.join( + fakeHome, + ".dotnet", + "corefx", + "cryptography", + "x509stores", + "my" + ); + expect(fs.existsSync(storeDir)).toBe(false); + + // Belt-and-suspenders: nothing at all under fakeHome. + const homeEntries = fs.readdirSync(fakeHome); + expect(homeEntries).toEqual([]); + }); + + it("produces a fresh cert on every invocation (no store-based reuse)", async () => { + const first = await backend.generate({ outDir, noTrust: true }); + fs.rmSync(outDir, { recursive: true }); + fs.mkdirSync(outDir); + const second = await backend.generate({ outDir, noTrust: true }); + + // Two separate generations → two distinct serial numbers / thumbprints. + // The noTrust path is intentionally not idempotent: with no store + // residency, there's no "existing cert" for the backend to find and + // reuse. + expect(second.thumbprint).not.toBe(first.thumbprint); + }); +}); From a7227a748c02df67f3e59a9d67e1c593db2bf45b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 17:55:46 +0000 Subject: [PATCH 12/41] ddc bundle: warn on cross-dir paths; ddc doctor: per-OS tool parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two distinct issues, one commit because the surface area is small and they share the per-OS tool-check infrastructure from the resolver hardening commit. # ddc bundle — silent-broken-bundle warning `ddc bundle ~/Downloads/foo.pfx --out-dir ~/.dev-certs` would produce a `bundle.json` referencing `~/Downloads/foo.pfx` verbatim, because the writer's containerize step only rewrites paths under `--out-dir`. With only `~/.dev-certs` bind-mounted to `/host-dev-certs` in the container, the in-container installer can't read the cert and the bundle is silently broken. The fix: after we've assembled the bundle entry but before writing it, walk the cert-file paths and warn (stderr) on any that don't live under `--out-dir`. The warning names the offending field and path on every line so a user can act on it without `--verbose`, and points them at the two ways to fix it (copy into `--out-dir`, or arrange additional mounts). No erroring or auto-copying — both are legitimate setups in unusual layouts and a warning is the right default. # ddc doctor — macOS and Windows parity Before this commit only the Linux branch had platform-specific tool checks (`openssl`, `certutil`). macOS and Windows users running `ddc doctor` got the universal checks (dotnet, auto backend, store state) but nothing about whether the tools their native backend actually depends on were on PATH. The new structure factors the per-OS checks into `checkLinuxTools()` / `checkMacosTools()` / `checkWindowsTools()`, dispatched by `process.platform`: - Linux (unchanged): `openssl`, `certutil` (NSS browser trust) - macOS (new): `security` (the keychain CLI macStore drives) - Windows (new): `pwsh` *or* `powershell` (PowerShell 7+ preferred, 5.1 accepted as a fallback with a note) and `certutil.exe` (Windows trust store) Windows uses `resolveSafeExecPath` rather than shelling to `where.exe` — same lookup `runProcess` uses internally, no spawn overhead, no risk of `where.exe` itself being hijacked from cwd. Linux / macOS keep `which` since `resolveSafeExecPath` is a no-op there. # Tests - `tests/bundle.test.ts` (4 tests): asserts no-warn when paths are under `--out-dir`, warn-with-paths when they aren't, every out-of-dir field gets listed, and the exact-out-dir-base case doesn't trip the check (regression guard). - `tests/doctor.test.ts` (10 tests): all three OS branches driven from a Linux host via `stubPlatform` + a vi.mocked shared module. Covers the happy path on each OS, each missing-tool warning, the PowerShell 7-vs-5.1 fallback note, and a regression guard that Windows never calls `which`. # Sweep Bundle warning smoke-tested end-to-end: `ddc bundle` against a cert in one tmpdir with --out-dir in another emits the warning to stderr naming all three artifact fields and both candidate fix paths. Doctor smoke-tested on Linux (existing checks unchanged). 226 UI tests, 79 workspace tests, 30 CLI tests (+14 new); lint + type-check clean. # CLI README Updated the `ddc bundle` and `ddc doctor` sections to describe the new behaviors — the warning condition and what to do about it, the per-OS tool matrix. --- src/cli/README.md | 10 +- src/cli/src/commands/bundle.ts | 47 +++++ src/cli/src/commands/doctor.ts | 110 ++++++++++-- src/cli/tests/bundle.test.ts | 125 +++++++++++++ src/cli/tests/doctor.test.ts | 312 +++++++++++++++++++++++++++++++++ 5 files changed, 585 insertions(+), 19 deletions(-) create mode 100644 src/cli/tests/bundle.test.ts create mode 100644 src/cli/tests/doctor.test.ts diff --git a/src/cli/README.md b/src/cli/README.md index 04b5e9a..3ca1d40 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -116,6 +116,8 @@ ddc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] Useful for wrapping a cert produced by something else (a corporate CA, a manual `dotnet dev-certs` invocation, a wildcard cert generated by another tool) into the bundle format the in-container installer expects. +If any cert file referenced by the bundle lives outside `--out-dir`, `ddc bundle` emits a stderr warning. The writer only rewrites paths under `--out-dir` to the container-mount target; paths outside are left verbatim, which means the in-container installer will try to read them at their host-filesystem location — something it can only do if you've also bind-mounted that location. Either copy the cert files into `--out-dir` and re-run, or arrange additional mounts so the referenced paths exist container-side. + ### `ddc trust ` Add an existing cert to the host OS trust store via the same shared platform layer the VS Code host extension uses. @@ -128,12 +130,18 @@ Short-circuits with an "already trusted" message when the cert is already in the ### `ddc doctor` -Read-only diagnostics: which backends are available, what `--backend auto` would pick, host platform-store state, and (on Linux) `openssl` / `certutil` presence on PATH. +Read-only diagnostics: which backends are available, what `--backend auto` would pick, host platform-store state, and per-OS tool presence. ``` ddc doctor [--out-dir ] ``` +Per-OS tool checks: + +- **Linux**: `openssl` (native trust step) and `certutil` (NSS browser-trust step; missing means Firefox / Chromium won't auto-trust). +- **macOS**: `security` (the keychain CLI the native backend drives). +- **Windows**: `pwsh` *or* `powershell` (Windows store enumeration; PowerShell 7+ preferred, 5.1 accepted as fallback) and `certutil.exe` (Windows trust store). + Exits non-zero if any check reports `[fail]`. `[warn]` is informational and exits zero. ## Bundle JSON diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts index 60c4a2a..e75cb69 100644 --- a/src/cli/src/commands/bundle.ts +++ b/src/cli/src/commands/bundle.ts @@ -85,6 +85,7 @@ export async function runBundle( }; fs.mkdirSync(outDir, { recursive: true }); + warnOnOutOfBundleDirPaths(entry, outDir, containerMount); const bundlePath = writeBundle({ hostOutDir: outDir, containerMount, @@ -93,3 +94,49 @@ export async function runBundle( process.stderr.write(`Bundle: ${bundlePath}\n`); process.stderr.write(`Thumbprint: ${thumbprint}\n`); } + +/** + * Warn when a cert file referenced by the bundle is NOT under `outDir`. + * The writer only rewrites paths under `outDir` to the container mount; + * paths outside are left verbatim, which means the in-container + * installer will try to read them at their host-filesystem location — + * something it can only do if the user has also bind-mounted that + * location into the container. The vast majority of the time they + * haven't, and a silently-broken bundle is worse than a noisy one. + */ +function warnOnOutOfBundleDirPaths( + entry: BundleCertEntry, + outDir: string, + containerMount: string +): void { + const candidates: Array<[string, string]> = []; + if (entry.hostPfxPath) candidates.push(["pfxPath", entry.hostPfxPath]); + candidates.push(["pemPath", entry.hostPemPath]); + if (entry.hostPemKeyPath) + candidates.push(["pemKeyPath", entry.hostPemKeyPath]); + + const resolvedOutDir = path.resolve(outDir); + const outsideEntries = candidates.filter(([, p]) => { + const resolved = path.resolve(p); + return !( + resolved === resolvedOutDir || + resolved.startsWith(resolvedOutDir + path.sep) + ); + }); + + if (outsideEntries.length === 0) return; + + process.stderr.write( + `[warn] Cert files reference paths outside --out-dir (${resolvedOutDir}):\n` + ); + for (const [field, p] of outsideEntries) { + process.stderr.write(` ${field}: ${p}\n`); + } + process.stderr.write( + ` The bundle references absolute host paths that the in-container\n` + + ` installer will read literally. If you only bind-mount ${resolvedOutDir}\n` + + ` to ${containerMount}, those paths won't resolve inside the container.\n` + + ` Either copy the cert files into --out-dir and re-run, or arrange\n` + + ` additional mounts so the referenced paths exist container-side.\n` + ); +} diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts index 87f7f2d..00bd8df 100644 --- a/src/cli/src/commands/doctor.ts +++ b/src/cli/src/commands/doctor.ts @@ -5,6 +5,7 @@ import { createPlatformStore, describeAutoBackend, DotnetBackend, + resolveSafeExecPath, runProcess, } from "@devcontainer-dev-certs/shared"; import { installCliLogger } from "../logger"; @@ -113,24 +114,7 @@ export async function runDoctor( }); } - // Required tools for the native backend on Linux. - if (process.platform === "linux") { - const openssl = await runProcess("which", ["openssl"]); - checks.push({ - label: "openssl on PATH (Linux native trust)", - status: openssl.exitCode === 0 ? "ok" : "warn", - detail: openssl.exitCode === 0 ? openssl.stdout.trim() : "not found", - }); - const certutil = await runProcess("which", ["certutil"]); - checks.push({ - label: "certutil on PATH (Linux NSS browser trust)", - status: certutil.exitCode === 0 ? "ok" : "warn", - detail: - certutil.exitCode === 0 - ? certutil.stdout.trim() - : "not found (Chromium/Firefox won't auto-trust; install libnss3-tools / nss-tools)", - }); - } + for (const c of await checkPlatformTools()) checks.push(c); // Print summary. let failures = 0; @@ -148,3 +132,93 @@ export async function runDoctor( process.exitCode = 1; } } + +/** + * Per-OS tool presence checks. Each backend / trust path depends on a + * different set of external commands, so the checks branch by + * `process.platform`. Linux has the most fan-out (separate tools for + * the OpenSSL trust dir and the NSS browser DB); macOS and Windows + * each have a small canonical set. + * + * On Windows we resolve via `resolveSafeExecPath` rather than shelling + * to `where.exe` — same lookup as `runProcess`, no spawn overhead, no + * risk of `where.exe` itself being hijacked. + */ +async function checkPlatformTools(): Promise { + if (process.platform === "linux") return checkLinuxTools(); + if (process.platform === "darwin") return checkMacosTools(); + if (process.platform === "win32") return checkWindowsTools(); + return []; +} + +async function checkLinuxTools(): Promise { + const checks: Check[] = []; + + const openssl = await runProcess("which", ["openssl"]); + checks.push({ + label: "openssl on PATH (Linux native trust)", + status: openssl.exitCode === 0 ? "ok" : "warn", + detail: openssl.exitCode === 0 ? openssl.stdout.trim() : "not found", + }); + + const certutil = await runProcess("which", ["certutil"]); + checks.push({ + label: "certutil on PATH (Linux NSS browser trust)", + status: certutil.exitCode === 0 ? "ok" : "warn", + detail: + certutil.exitCode === 0 + ? certutil.stdout.trim() + : "not found (Chromium/Firefox won't auto-trust; install libnss3-tools / nss-tools)", + }); + + return checks; +} + +async function checkMacosTools(): Promise { + const checks: Check[] = []; + + // `security` is the keychain CLI. macStore uses it for trust and + // enumeration; without it the native backend can't run on macOS. + // It's part of the base OS install at /usr/bin/security, so a + // missing entry usually means PATH has been pruned aggressively. + const security = await runProcess("which", ["security"]); + checks.push({ + label: "security on PATH (macOS keychain trust)", + status: security.exitCode === 0 ? "ok" : "warn", + detail: + security.exitCode === 0 + ? security.stdout.trim() + : "not found (native backend cannot drive the keychain — usually means PATH was stripped)", + }); + + return checks; +} + +function checkWindowsTools(): Check[] { + const checks: Check[] = []; + + // windowsStore prefers `pwsh` (PowerShell 7+) but falls back to + // `powershell` (PowerShell 5.1). At least one must be findable. + const pwsh = resolveSafeExecPath("pwsh"); + const powershell = resolveSafeExecPath("powershell"); + const psFound = pwsh ?? powershell; + checks.push({ + label: "pwsh or powershell on PATH (Windows store enumeration)", + status: psFound !== null ? "ok" : "warn", + detail: + psFound !== null + ? `${psFound}${pwsh === null ? " (PowerShell 5.1; pwsh 7+ preferred but not required)" : ""}` + : "not found (Windows store enumeration / cleanup will fail)", + }); + + const certutilExe = resolveSafeExecPath("certutil.exe"); + checks.push({ + label: "certutil.exe on PATH (Windows trust store)", + status: certutilExe !== null ? "ok" : "warn", + detail: + certutilExe ?? + "not found (native trust step will fail — usually means PATH was stripped)", + }); + + return checks; +} diff --git a/src/cli/tests/bundle.test.ts b/src/cli/tests/bundle.test.ts new file mode 100644 index 0000000..a08da8d --- /dev/null +++ b/src/cli/tests/bundle.test.ts @@ -0,0 +1,125 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { generateCertificate, exportPfx, exportPem, VALIDITY_DAYS } from "@devcontainer-dev-certs/shared"; +import { runBundle } from "../src/commands/bundle"; + +/** + * `ddc bundle` is supposed to flag the silent-broken-bundle case: cert + * files referenced by the bundle live outside the `--out-dir`, so the + * containerize step in the writer leaves their absolute host paths + * verbatim — and the in-container installer (which only ever sees the + * mount target, not the host filesystem) will fail to read them. + * + * These tests drive the warning by spying on stderr and asserting on + * what the bundle command wrote there. + */ + +async function makeCertFilesIn(dir: string): Promise { + const now = new Date(); + const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); + const { cert, key } = await generateCertificate(now, expiry); + await exportPfx(cert, key, dir); + exportPem(cert, key, dir); +} + +const cleanupDirs: string[] = []; + +beforeEach(() => { + vi.spyOn(process.stderr, "write").mockImplementation(() => true); +}); + +afterEach(() => { + vi.restoreAllMocks(); + for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); + cleanupDirs.length = 0; +}); + +function collectStderr(): string { + const writeMock = vi.mocked(process.stderr.write); + return writeMock.mock.calls.map((c) => String(c[0])).join(""); +} + +describe("ddc bundle out-of-dir warning", () => { + it("does not warn when cert files live inside --out-dir", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-in-")); + cleanupDirs.push(dir); + await makeCertFilesIn(dir); + + await runBundle(path.join(dir, "aspnetcore-dev.pfx"), { + outDir: dir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const stderr = collectStderr(); + expect(stderr).not.toContain("[warn]"); + expect(stderr).not.toContain("outside --out-dir"); + }); + + it("warns when cert files are NOT under --out-dir", async () => { + const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-cert-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-out-")); + cleanupDirs.push(certDir, outDir); + await makeCertFilesIn(certDir); + + await runBundle(path.join(certDir, "aspnetcore-dev.pfx"), { + outDir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const stderr = collectStderr(); + expect(stderr).toContain("[warn]"); + expect(stderr).toContain("outside --out-dir"); + // The warning should name the actual offending paths so the user + // can act on it without re-running with --verbose. + expect(stderr).toContain(path.join(certDir, "aspnetcore-dev.pfx")); + expect(stderr).toContain(path.join(certDir, "aspnetcore-dev.pem")); + }); + + it("flags every out-of-dir file, not just the first one", async () => { + const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-cert-multi-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-out-multi-")); + cleanupDirs.push(certDir, outDir); + await makeCertFilesIn(certDir); + + await runBundle(path.join(certDir, "aspnetcore-dev.pfx"), { + outDir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const stderr = collectStderr(); + // All three artifact fields (pfx, pem, key) live in certDir, so + // each should appear in the warning detail lines. + expect(stderr).toContain("pfxPath:"); + expect(stderr).toContain("pemPath:"); + expect(stderr).toContain("pemKeyPath:"); + }); + + it("does not warn for the cert path itself when it's the same as the out-dir base", async () => { + // Regression guard: a cert at exactly $OUT_DIR/cert.pfx and outDir + // = $OUT_DIR should not trip the "outside" check. + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-exact-")); + cleanupDirs.push(dir); + await makeCertFilesIn(dir); + + await runBundle(path.join(dir, "aspnetcore-dev.pfx"), { + outDir: dir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const stderr = collectStderr(); + expect(stderr).not.toContain("outside --out-dir"); + }); +}); diff --git a/src/cli/tests/doctor.test.ts b/src/cli/tests/doctor.test.ts new file mode 100644 index 0000000..dbc8dac --- /dev/null +++ b/src/cli/tests/doctor.test.ts @@ -0,0 +1,312 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import type * as Shared from "@devcontainer-dev-certs/shared"; + +// `runDoctor` exercises three different shared-module surfaces: +// `runProcess`, `resolveSafeExecPath`, and `createPlatformStore`. Mock +// all three so the tests don't depend on what's actually installed on +// the host machine. `describeAutoBackend` is left real but its dotnet +// probe goes through the same `runProcess` mock. +vi.mock("@devcontainer-dev-certs/shared", async () => { + const actual = await vi.importActual( + "@devcontainer-dev-certs/shared" + ); + return { + ...actual, + runProcess: vi.fn(), + resolveSafeExecPath: vi.fn(), + createPlatformStore: vi.fn(), + describeAutoBackend: vi.fn(), + }; +}); + +import { + createPlatformStore, + describeAutoBackend, + resolveSafeExecPath, + runProcess, +} from "@devcontainer-dev-certs/shared"; +import { runDoctor } from "../src/commands/doctor"; + +const mockedRunProcess = vi.mocked(runProcess); +const mockedResolveSafeExecPath = vi.mocked(resolveSafeExecPath); +const mockedCreatePlatformStore = vi.mocked(createPlatformStore); +const mockedDescribeAutoBackend = vi.mocked(describeAutoBackend); + +const cleanupDirs: string[] = []; + +function stubPlatform(value: NodeJS.Platform): () => void { + const original = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value, configurable: true }); + return () => { + if (original) Object.defineProperty(process, "platform", original); + }; +} + +function collectStdout(): string { + const writeMock = vi.mocked(process.stdout.write); + return writeMock.mock.calls.map((c) => String(c[0])).join(""); +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(process.stdout, "write").mockImplementation(() => true); + vi.spyOn(process.stderr, "write").mockImplementation(() => true); + + // Default stubs: dotnet not on PATH, platform store empty, auto picks + // native. Each test can override what it cares about. + mockedRunProcess.mockResolvedValue({ exitCode: 1, stdout: "", stderr: "" }); + mockedResolveSafeExecPath.mockReturnValue(null); + mockedDescribeAutoBackend.mockResolvedValue("native"); + mockedCreatePlatformStore.mockResolvedValue({ + checkStatus: vi.fn(async () => ({ + exists: false, + isTrusted: false, + thumbprint: null, + notBefore: null, + notAfter: null, + version: null, + })), + } as never); +}); + +afterEach(() => { + vi.restoreAllMocks(); + for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); + cleanupDirs.length = 0; + process.exitCode = 0; +}); + +describe("ddc doctor — Linux", () => { + it("reports [ok] when openssl and certutil are both on PATH", async () => { + const restore = stubPlatform("linux"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-linux-")); + cleanupDirs.push(outDir); + + mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { + if (cmd === "which" && args[0] === "openssl") { + return { exitCode: 0, stdout: "/usr/bin/openssl\n", stderr: "" }; + } + if (cmd === "which" && args[0] === "certutil") { + return { exitCode: 0, stdout: "/usr/bin/certutil\n", stderr: "" }; + } + return { exitCode: 1, stdout: "", stderr: "" }; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[ok] openssl on PATH"); + expect(stdout).toContain("/usr/bin/openssl"); + expect(stdout).toContain("[ok] certutil on PATH"); + }); + + it("reports [warn] when certutil is missing", async () => { + const restore = stubPlatform("linux"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-linux-")); + cleanupDirs.push(outDir); + + mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { + if (cmd === "which" && args[0] === "openssl") { + return { exitCode: 0, stdout: "/usr/bin/openssl\n", stderr: "" }; + } + return { exitCode: 1, stdout: "", stderr: "" }; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[warn] certutil on PATH"); + expect(stdout).toContain("Chromium/Firefox won't auto-trust"); + }); +}); + +describe("ddc doctor — macOS", () => { + it("checks for the `security` keychain CLI", async () => { + const restore = stubPlatform("darwin"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + cleanupDirs.push(outDir); + + mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { + if (cmd === "which" && args[0] === "security") { + return { exitCode: 0, stdout: "/usr/bin/security\n", stderr: "" }; + } + return { exitCode: 1, stdout: "", stderr: "" }; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[ok] security on PATH"); + expect(stdout).toContain("/usr/bin/security"); + }); + + it("does NOT run Linux-only checks on macOS", async () => { + const restore = stubPlatform("darwin"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + cleanupDirs.push(outDir); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).not.toContain("openssl on PATH"); + expect(stdout).not.toContain("Linux NSS"); + }); + + it("warns when `security` isn't on PATH", async () => { + const restore = stubPlatform("darwin"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + cleanupDirs.push(outDir); + + // Default mockedRunProcess returns exit 1 for everything — including + // the `which security` probe. + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[warn] security on PATH"); + expect(stdout).toContain("native backend cannot drive the keychain"); + }); +}); + +describe("ddc doctor — Windows", () => { + it("reports [ok] when both pwsh and certutil.exe resolve", async () => { + const restore = stubPlatform("win32"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + cleanupDirs.push(outDir); + + mockedResolveSafeExecPath.mockImplementation((cmd: string) => { + if (cmd === "pwsh") return "C:\\Program Files\\PowerShell\\7\\pwsh.exe"; + if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; + return null; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[ok] pwsh or powershell on PATH"); + expect(stdout).toContain("pwsh.exe"); + expect(stdout).toContain("[ok] certutil.exe on PATH"); + }); + + it("accepts powershell as a fallback when pwsh is absent, with a note", async () => { + const restore = stubPlatform("win32"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + cleanupDirs.push(outDir); + + mockedResolveSafeExecPath.mockImplementation((cmd: string) => { + if (cmd === "pwsh") return null; + if (cmd === "powershell") + return "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; + if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; + return null; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[ok] pwsh or powershell on PATH"); + expect(stdout).toContain("powershell.exe"); + expect(stdout).toContain("PowerShell 5.1"); + }); + + it("warns when neither pwsh nor powershell is found", async () => { + const restore = stubPlatform("win32"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + cleanupDirs.push(outDir); + + mockedResolveSafeExecPath.mockImplementation((cmd: string) => { + if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; + return null; // both pwsh and powershell missing + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[warn] pwsh or powershell on PATH"); + expect(stdout).toContain("Windows store enumeration / cleanup will fail"); + }); + + it("warns when certutil.exe is missing", async () => { + const restore = stubPlatform("win32"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + cleanupDirs.push(outDir); + + mockedResolveSafeExecPath.mockImplementation((cmd: string) => { + if (cmd === "pwsh") return "C:\\Program Files\\PowerShell\\7\\pwsh.exe"; + return null; + }); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + const stdout = collectStdout(); + expect(stdout).toContain("[warn] certutil.exe on PATH"); + expect(stdout).toContain("native trust step will fail"); + }); + + it("does NOT shell out to `which` on Windows (uses resolveSafeExecPath)", async () => { + const restore = stubPlatform("win32"); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + cleanupDirs.push(outDir); + + try { + await runDoctor({ outDir }); + } finally { + restore(); + } + + // `which` is not the right tool on Windows (`where.exe` is), and we + // explicitly skip it in favor of resolveSafeExecPath because the + // latter is the same lookup runProcess uses and avoids a redundant + // shell-out. Regression-guard the path: no `which` invocations. + const whichCalls = mockedRunProcess.mock.calls.filter( + ([cmd]) => cmd === "which" + ); + expect(whichCalls).toHaveLength(0); + }); +}); From 15f33c5e8b797d66e9d1e22b06de72c4b08f7d1b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:16:10 +0000 Subject: [PATCH 13/41] =?UTF-8?q?Rename=20CLI=20binary=20ddc=20=E2=86=92?= =?UTF-8?q?=20dcdc;=20ready=20@devcontainer-dev-certs/cli=20for=20npm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unrelated `ddc` package already exists on npm (PowerShell / Vim ecosystem), and even a scoped publish wouldn't sidestep the bin-name collision in users' `node_modules/.bin/`. Renaming the binary now is strictly easier than reckoning with the collision after the first release. # Binary rename `ddc` → `dcdc` (DevContainer Dev Certs — short, pronounceable, unlikely to collide). Touched every callsite: command name in commander, error prefix, all five command docstrings, the shared layer's references to `dcdc doctor` / `dcdc generate --no-trust`, tmp-dir prefixes in tests, every code block in the three READMEs. # Package metadata for npm publish `src/cli/package.json` is now publishable: - Dropped `private: true`. - `bin: { "dcdc": "./dist/dcdc.js" }`. - `files: ["dist/dcdc.js", "LICENSE", "README.md"]` — only the bundled binary ships, not the source. `npm pack --dry-run` reports a 184 kB tarball (788 kB unpacked) with the production build. - All runtime deps moved to `devDependencies` because esbuild bundles everything into `dist/dcdc.js`. The published package has zero install-time dependencies; users pull the binary and that's it. - `publishConfig: { access: "public", provenance: true }` — provenance attestations match the SLSA pattern already used for VSIXes / OCI artifacts elsewhere in this repo (they're emitted automatically by npm publish when running under GitHub Actions OIDC; locally they're silently skipped, so the field is safe to set unconditionally). - `engines.node: ">=18"`, `keywords`, full `repository.directory` pointer so npm's "GitHub" link lands on the right subtree. - `prepublishOnly: "node esbuild.mjs --production"` so any publish attempt — local or CI — always produces a minified bundle, never ships a dev build by accident. LICENSE copied from the repo root into `src/cli/` so the published tarball carries its own copy (npm doesn't follow workspace links). # CI `build-extensions.yml` now also builds the CLI, runs its test suite, and runs `npm pack --dry-run -w src/cli` on every PR and main push. The pack dry-run catches metadata regressions (missing files, bad bin paths, broken LICENSE/README references) before a release would discover them. The CLI has no Windows-specific integration tests — `resolveSafeExecPath` mocks the platform — so we don't need a separate Windows runner for it. # README updates - `src/cli/README.md`: install section rewritten to `npm install -g @devcontainer-dev-certs/cli`, with `npx` one-off as an alternative and "build from source" as a development fallback. Calls out the `ddc` collision and explains the rename. - `examples/manual-setup/README.md`: install instructions for `dcdc` now point at the npm package instead of "build from source." - Root `README.md`: every `ddc` mention renamed. # Sweep 226 UI tests, 79 workspace tests, 30 CLI tests; type-check + lint clean across all four workspaces; `npm pack --dry-run` happy; end-to-end smoke (`dcdc generate --no-trust` + `dcdc doctor`) green under the new name. --- .github/workflows/build-extensions.yml | 17 +++++ README.md | 8 +-- examples/manual-setup/README.md | 24 ++++--- package-lock.json | 21 +++--- src/cli/LICENSE | 21 ++++++ src/cli/README.md | 66 +++++++++---------- src/cli/esbuild.mjs | 2 +- src/cli/package.json | 55 ++++++++++++---- src/cli/src/commands/bundle.ts | 2 +- src/cli/src/commands/doctor.ts | 4 +- src/cli/src/commands/generate.ts | 2 +- src/cli/src/commands/inspect.ts | 2 +- src/cli/src/commands/trust.ts | 2 +- src/cli/src/index.ts | 4 +- src/cli/tests/bundle.test.ts | 16 ++--- src/cli/tests/doctor.test.ts | 26 ++++---- src/cli/tests/writer.test.ts | 2 +- src/shared/src/backends/native.ts | 2 +- src/shared/src/backends/select.ts | 2 +- src/shared/src/backends/types.ts | 2 +- src/shared/src/index.ts | 2 +- .../tests/nativeBackend.test.ts | 4 +- 22 files changed, 183 insertions(+), 103 deletions(-) create mode 100644 src/cli/LICENSE diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index 285e2bf..d4f7ee0 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -57,6 +57,23 @@ jobs: - name: Package Workspace Extension VSIX run: npm run package -w src/vscode-workspace-extension + - name: Build CLI + # Same workspace, same shared layer — `dcdc` is the host-side + # surface of the same code the extensions ship. Production build + # in release contexts so the published bundle matches what the + # extensions ship. + run: npm run ${{ inputs.production && 'build:prod' || 'build' }} -w src/cli + + - name: Test CLI + run: npm test -w src/cli + + - name: Verify CLI package metadata + # Guardrail: confirms `npm publish` would produce a valid + # tarball (correct `files`, `bin` resolution, no missing + # LICENSE/README) without actually publishing. Catches metadata + # regressions before they hit a real release. + run: npm pack --dry-run -w src/cli + - name: Upload VSIX artifacts if: inputs.upload-vsix uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/README.md b/README.md index 2df5c45..17f1a64 100644 --- a/README.md +++ b/README.md @@ -331,16 +331,16 @@ The companion-extension pattern is VS Code-specific, but the underlying containe For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference. -### `ddc` — the host-side CLI +### `dcdc` — the host-side CLI -[`ddc`](src/cli/README.md) is the host-side CLI that produces the cert files and `bundle.json` the in-container installer below consumes. One command does generation, host trust, and bundle emission: +[`dcdc`](src/cli/README.md) is the host-side CLI that produces the cert files and `bundle.json` the in-container installer below consumes. One command does generation, host trust, and bundle emission: ```bash mkdir -p ~/.dev-certs -ddc generate --out-dir ~/.dev-certs +dcdc generate --out-dir ~/.dev-certs ``` -It also exposes `ddc inspect` (cert details), `ddc bundle` (wrap an existing cert into a bundle.json), `ddc trust` (host-trust an existing cert), and `ddc doctor` (read-only diagnostics). See [`src/cli/README.md`](src/cli/README.md) for the full reference. Doing the steps by hand is documented in [`examples/manual-setup/`](examples/manual-setup/) for situations where the CLI isn't available. +It also exposes `dcdc inspect` (cert details), `dcdc bundle` (wrap an existing cert into a bundle.json), `dcdc trust` (host-trust an existing cert), and `dcdc doctor` (read-only diagnostics). See [`src/cli/README.md`](src/cli/README.md) for the full reference. Doing the steps by hand is documented in [`examples/manual-setup/`](examples/manual-setup/) for situations where the CLI isn't available. ### The fallback installer diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md index 9296e9c..be0d103 100644 --- a/examples/manual-setup/README.md +++ b/examples/manual-setup/README.md @@ -11,17 +11,23 @@ The same canonical trust state the VS Code workspace extension produces — `~/. - The `devcontainer-dev-certs` feature in your `devcontainer.json` with `installFallbackTools: true` (or `openssl` and `jq` already present in your base image). - A directory on the host containing the cert files you want installed, plus a `bundle.json` describing them. -The host-side cert + `bundle.json` can be produced two ways. **The `ddc` CLI is the simpler path** — one command does generation, host trust, and `bundle.json` emission. The manual path (still documented below) is what you'd reach for when `ddc` isn't available, when you need a cert from a different source, or when you're sharing a specific cert with the VS Code host extension. +The host-side cert + `bundle.json` can be produced two ways. **The `dcdc` CLI is the simpler path** — one command does generation, host trust, and `bundle.json` emission. The manual path (still documented below) is what you'd reach for when `dcdc` isn't available, when you need a cert from a different source, or when you're sharing a specific cert with the VS Code host extension. -## One-time host setup (with `ddc`) +## One-time host setup (with `dcdc`) -`ddc` is the host-side CLI included in this repository. See [`src/cli/README.md`](../../src/cli/README.md) for install instructions; the short version while no published binary exists is "clone the repo and `cd src/cli && npm install && node esbuild.mjs`". +`dcdc` is the host-side CLI shipped as [`@devcontainer-dev-certs/cli`](https://www.npmjs.com/package/@devcontainer-dev-certs/cli) on npm: + +```bash +npm install -g @devcontainer-dev-certs/cli +``` + +Node 18 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`) and generate everything in one shot: ```bash mkdir -p ~/.dev-certs -ddc generate --out-dir ~/.dev-certs +dcdc generate --out-dir ~/.dev-certs ``` This: @@ -71,7 +77,7 @@ Copy the bits of [`devcontainer.json`](./devcontainer.json) you want into your o The fallback installer is delivered to `/usr/local/bin/devcontainer-dev-certs-install` by the feature. -Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`ddc generate` again, or the manual `dotnet dev-certs https --clean && …re-export…` ritual) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. +Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`dcdc generate` again, or the manual `dotnet dev-certs https --clean && …re-export…` ritual) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. ## Verifying @@ -83,10 +89,10 @@ devcontainer-dev-certs-install --doctor You should see `[ok]` for every check. If you see `[fail]` or `[warn]`, the message tells you what to fix. -On the host, `ddc doctor` gives equivalent diagnostics for the host side (which backends are available, whether the cert is in the host platform store and trusted): +On the host, `dcdc doctor` gives equivalent diagnostics for the host side (which backends are available, whether the cert is in the host platform store and trusted): ```bash -ddc doctor +dcdc doctor ``` You can also sanity-check from inside the container: @@ -121,12 +127,12 @@ The bundle is a list — add corporate CAs, wildcard certs, etc. as additional e CA-only entries (no `pfxPath`, no `pemKeyPath`) are valid — they get planted in the trust store but no private key is synced. -`ddc bundle ` emits a single-cert `bundle.json` for an arbitrary cert file (auto-discovers sibling `.pem` / `.key` / `.pfx`, fills in the SHA-1 thumbprint, rewrites paths to the container mount). Merge its output into your existing bundle by hand to add a cert. +`dcdc bundle ` emits a single-cert `bundle.json` for an arbitrary cert file (auto-discovers sibling `.pem` / `.key` / `.pfx`, fills in the SHA-1 thumbprint, rewrites paths to the container mount). Merge its output into your existing bundle by hand to add a cert. See the [bundle schema](../../schema/bundle.schema.json) for the full field reference. ## Limitations -- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `ddc generate` (the example above does this — it runs the host trust step), `dotnet dev-certs https --trust` (the manual path above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. +- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `dcdc generate` (the example above does this — it runs the host trust step), `dotnet dev-certs https --trust` (the manual path above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. - **No `defaultKestrelCertificate` equivalent.** The VS Code-only `defaultKestrelCertificate` setting writes `ASPNETCORE_Kestrel__Certificates__Default__Path/__Password` via VS Code's `EnvironmentVariableCollection`. To pin a custom Kestrel default outside VS Code, set those env vars yourself in `devcontainer.json` `containerEnv`. - **No reverse sync (container → host).** The `syncContainerCert` flow needs a privileged host-side process to add the cert to the host OS trust store; without the host extension's UI there's nowhere to surface the consent prompt. diff --git a/package-lock.json b/package-lock.json index 55c39c3..cd2242e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7070,28 +7070,31 @@ "src/cli": { "name": "@devcontainer-dev-certs/cli", "version": "1.3.1-pre", - "dependencies": { - "@devcontainer-dev-certs/shared": "*", - "@peculiar/x509": "^2.0.0", - "asn1js": "^3.0.10", - "commander": "^14.0.1", - "pkijs": "^3.4.0", - "reflect-metadata": "^0.2.2" - }, + "license": "MIT", "bin": { - "ddc": "dist/ddc.js" + "dcdc": "dist/dcdc.js" }, "devDependencies": { + "@devcontainer-dev-certs/shared": "*", + "@peculiar/x509": "^2.0.0", "@types/node": "^22.0.0", + "asn1js": "^3.0.10", + "commander": "^14.0.1", "esbuild": "^0.28.0", + "pkijs": "^3.4.0", + "reflect-metadata": "^0.2.2", "typescript": "^6.0.3", "vitest": "^4.1.5" + }, + "engines": { + "node": ">=18" } }, "src/cli/node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" diff --git a/src/cli/LICENSE b/src/cli/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/src/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/cli/README.md b/src/cli/README.md index 3ca1d40..c1071f1 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -1,6 +1,6 @@ -# ddc +# dcdc -`ddc` is the host-side CLI for [devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs). It generates ASP.NET-compatible HTTPS development certificates, trusts them on the host, inspects existing cert files, and emits the `bundle.json` the in-container installer reads — without VS Code being involved. +`dcdc` is the host-side CLI for [devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs). It generates ASP.NET-compatible HTTPS development certificates, trusts them on the host, inspects existing cert files, and emits the `bundle.json` the in-container installer reads — without VS Code being involved. ## Why this exists @@ -11,42 +11,42 @@ The host extension automates everything when you use VS Code. Outside of VS Code 3. Hand-write a `bundle.json` referencing the cert files inside the container's bind-mount. 4. Hand-compute the SHA-1 thumbprint and paste it into the bundle. -`ddc generate` is one command that does all four. The other commands (`inspect`, `bundle`, `trust`, `doctor`) cover the rest of the lifecycle. +`dcdc generate` is one command that does all four. The other commands (`inspect`, `bundle`, `trust`, `doctor`) cover the rest of the lifecycle. -`ddc` uses the same shared cert + platform code the VS Code host extension uses — same trust paths, same SAN list, same OID marker — so a cert produced by `ddc` is interchangeable with one produced by the extension. +`dcdc` uses the same shared cert + platform code the VS Code host extension uses — same trust paths, same SAN list, same OID marker — so a cert produced by `dcdc` is interchangeable with one produced by the extension. ## Install -There is no published binary yet. To use `ddc` today, build it from this repository: - ```bash -git clone https://github.com/dnegstad/devcontainer-dev-certs.git -cd devcontainer-dev-certs -npm install -cd src/cli && node esbuild.mjs +npm install -g @devcontainer-dev-certs/cli +dcdc --help ``` -That produces `dist/ddc.js`, an executable Node script. Invoke it directly: +Node 18 or newer is required. The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. + +`dcdc` is the binary name. The package name is `@devcontainer-dev-certs/cli` — the rename sidesteps a collision with the unrelated [`ddc`](https://www.npmjs.com/package/ddc) package on npm. If you want a one-off invocation without a global install: ```bash -node dist/ddc.js --help +npx -p @devcontainer-dev-certs/cli dcdc --help ``` -Or symlink it onto your PATH: +### Building from source + +For development against an unreleased version, or to inspect the bundle: ```bash -chmod +x dist/ddc.js -ln -s "$(pwd)/dist/ddc.js" ~/.local/bin/ddc -ddc --help +git clone https://github.com/dnegstad/devcontainer-dev-certs.git +cd devcontainer-dev-certs +npm install +cd src/cli && node esbuild.mjs +node dist/dcdc.js --help ``` -Node 18 or newer is required. - ## Quick start ```bash mkdir -p ~/.dev-certs -ddc generate --out-dir ~/.dev-certs +dcdc generate --out-dir ~/.dev-certs ``` That produces in `~/.dev-certs`: @@ -62,12 +62,12 @@ Bind-mount the directory into the container and have the in-container installer ## Commands -### `ddc generate` +### `dcdc generate` Generate a dev cert, trust it on the host, and emit `bundle.json`. ``` -ddc generate [--out-dir ] [--backend auto|native|dotnet] +dcdc generate [--out-dir ] [--backend auto|native|dotnet] [--no-trust] [--container-mount ] [--no-bundle] [--verbose] ``` @@ -80,12 +80,12 @@ ddc generate [--out-dir ] [--backend auto|native|dotnet] | `--no-bundle` | off | Skip emitting `bundle.json`. | | `--verbose` | off | Stream shared-layer log lines to stderr. | -### `ddc inspect ` +### `dcdc inspect ` Print details about a PFX or PEM certificate. ``` -ddc inspect path/to/aspnetcore-dev.pfx +dcdc inspect path/to/aspnetcore-dev.pfx ``` Reports the subject CN, both SHA-1 and SHA-256 thumbprints, validity window, ASP.NET dev-cert OID and version byte (so you can tell whether the cert is fresh enough for the current installer), every SAN entry (with `[non-local]` flags on any that aren't on the standard developer-cert allowlist), and warnings (cert without key, expiring soon, non-local SANs present). @@ -93,15 +93,15 @@ Reports the subject CN, both SHA-1 and SHA-256 thumbprints, validity window, ASP Pass `--json` for machine-readable output: ``` -ddc inspect --json path/to/cert.pfx +dcdc inspect --json path/to/cert.pfx ``` -### `ddc bundle ` +### `dcdc bundle ` Emit a single-cert `bundle.json` referencing an already-existing cert file. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by naming convention so a single PFX argument is usually enough. ``` -ddc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] +dcdc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] [--name ] [--kind dotnet-dev|user] [--no-trust-in-container] ``` @@ -116,24 +116,24 @@ ddc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] Useful for wrapping a cert produced by something else (a corporate CA, a manual `dotnet dev-certs` invocation, a wildcard cert generated by another tool) into the bundle format the in-container installer expects. -If any cert file referenced by the bundle lives outside `--out-dir`, `ddc bundle` emits a stderr warning. The writer only rewrites paths under `--out-dir` to the container-mount target; paths outside are left verbatim, which means the in-container installer will try to read them at their host-filesystem location — something it can only do if you've also bind-mounted that location. Either copy the cert files into `--out-dir` and re-run, or arrange additional mounts so the referenced paths exist container-side. +If any cert file referenced by the bundle lives outside `--out-dir`, `dcdc bundle` emits a stderr warning. The writer only rewrites paths under `--out-dir` to the container-mount target; paths outside are left verbatim, which means the in-container installer will try to read them at their host-filesystem location — something it can only do if you've also bind-mounted that location. Either copy the cert files into `--out-dir` and re-run, or arrange additional mounts so the referenced paths exist container-side. -### `ddc trust ` +### `dcdc trust ` Add an existing cert to the host OS trust store via the same shared platform layer the VS Code host extension uses. ``` -ddc trust path/to/cert.pfx +dcdc trust path/to/cert.pfx ``` Short-circuits with an "already trusted" message when the cert is already in the trust store — repeated invocations don't re-prompt. -### `ddc doctor` +### `dcdc doctor` Read-only diagnostics: which backends are available, what `--backend auto` would pick, host platform-store state, and per-OS tool presence. ``` -ddc doctor [--out-dir ] +dcdc doctor [--out-dir ] ``` Per-OS tool checks: @@ -146,9 +146,9 @@ Exits non-zero if any check reports `[fail]`. `[warn]` is informational and exit ## Bundle JSON -The `bundle.json` written by `ddc generate` and `ddc bundle` conforms to the schema at [`schema/bundle.schema.json`](../../schema/bundle.schema.json) and is the same format the in-container `devcontainer-dev-certs-install --bundle-json` installer accepts. +The `bundle.json` written by `dcdc generate` and `dcdc bundle` conforms to the schema at [`schema/bundle.schema.json`](../../schema/bundle.schema.json) and is the same format the in-container `devcontainer-dev-certs-install --bundle-json` installer accepts. -For multi-cert setups (auto-generated dev cert + corporate CA + extra wildcard), the simplest workflow is `ddc generate` to seed the bundle and then hand-edit additional entries in. The bundle is a list — see the [root README](../../README.md#manual--non-vs-code-use) for the field reference. +For multi-cert setups (auto-generated dev cert + corporate CA + extra wildcard), the simplest workflow is `dcdc generate` to seed the bundle and then hand-edit additional entries in. The bundle is a list — see the [root README](../../README.md#manual--non-vs-code-use) for the field reference. ## Backends diff --git a/src/cli/esbuild.mjs b/src/cli/esbuild.mjs index 526a9da..7a1b99a 100644 --- a/src/cli/esbuild.mjs +++ b/src/cli/esbuild.mjs @@ -5,7 +5,7 @@ const production = process.argv.includes("--production"); await esbuild.build({ entryPoints: ["src/index.ts"], bundle: true, - outfile: "dist/ddc.js", + outfile: "dist/dcdc.js", // `vscode` is a build-time stub used only by the shared package's // `loggerVscode.ts` helper, which the CLI never imports. Marking it // external prevents esbuild from trying to resolve a module that diff --git a/src/cli/package.json b/src/cli/package.json index 0c6c544..aa35718 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -1,29 +1,62 @@ { "name": "@devcontainer-dev-certs/cli", "version": "1.3.1-pre", - "private": true, - "description": "ddc — host-side CLI for generating, inspecting, and trusting dev certs without VS Code.", - "main": "./dist/ddc.js", + "description": "dcdc — host-side CLI for generating, inspecting, and trusting ASP.NET-compatible dev certs for use with dev containers, outside of VS Code.", + "license": "MIT", + "author": "Daniel Negstad", + "homepage": "https://github.com/dnegstad/devcontainer-dev-certs#readme", + "repository": { + "type": "git", + "url": "https://github.com/dnegstad/devcontainer-dev-certs.git", + "directory": "src/cli" + }, + "bugs": { + "url": "https://github.com/dnegstad/devcontainer-dev-certs/issues" + }, + "keywords": [ + "devcontainer", + "dev-certs", + "https", + "aspnet", + "aspire", + "kestrel", + "tls", + "ssl", + "certificate", + "x509" + ], + "main": "./dist/dcdc.js", "bin": { - "ddc": "./dist/ddc.js" + "dcdc": "./dist/dcdc.js" + }, + "files": [ + "dist/dcdc.js", + "LICENSE", + "README.md" + ], + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public", + "provenance": true }, "scripts": { "build": "node esbuild.mjs", "build:prod": "node esbuild.mjs --production", "test": "vitest run", - "lint": "eslint src tests" + "lint": "eslint src tests", + "prepublishOnly": "node esbuild.mjs --production" }, - "dependencies": { + "devDependencies": { "@devcontainer-dev-certs/shared": "*", "@peculiar/x509": "^2.0.0", + "@types/node": "^22.0.0", "asn1js": "^3.0.10", "commander": "^14.0.1", - "pkijs": "^3.4.0", - "reflect-metadata": "^0.2.2" - }, - "devDependencies": { - "@types/node": "^22.0.0", "esbuild": "^0.28.0", + "pkijs": "^3.4.0", + "reflect-metadata": "^0.2.2", "typescript": "^6.0.3", "vitest": "^4.1.5" } diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts index e75cb69..5aa59fd 100644 --- a/src/cli/src/commands/bundle.ts +++ b/src/cli/src/commands/bundle.ts @@ -17,7 +17,7 @@ export interface BundleCommandOptions { const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; /** - * `ddc bundle ` — emit a `bundle.json` referencing an + * `dcdc bundle ` — emit a `bundle.json` referencing an * already-existing cert file. Useful when the cert was generated by * something else (e.g. `dotnet dev-certs` invoked manually, or a corporate * CA bundle) and the user just needs the wrapping bundle for the in-container diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts index 00bd8df..18aa0bc 100644 --- a/src/cli/src/commands/doctor.ts +++ b/src/cli/src/commands/doctor.ts @@ -24,7 +24,7 @@ interface Check { } /** - * `ddc doctor` — read-only diagnostics: which backends are available, what + * `dcdc doctor` — read-only diagnostics: which backends are available, what * `--backend auto` would pick, host trust-store state for the cert (if any) * in the out-dir. Mirrors the in-container `devcontainer-dev-certs-install * --doctor` ergonomics: every check produces an `[ok]` / `[warn]` / `[fail]` @@ -66,7 +66,7 @@ export async function runDoctor( checks.push({ label: `out-dir ${outDir}`, status: "warn", - detail: "does not exist (run `ddc generate` to create it)", + detail: "does not exist (run `dcdc generate` to create it)", }); } diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts index b5f0cea..34c1384 100644 --- a/src/cli/src/commands/generate.ts +++ b/src/cli/src/commands/generate.ts @@ -20,7 +20,7 @@ const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; /** - * `ddc generate` — produce a fresh dev cert + bundle.json. Picks a backend + * `dcdc generate` — produce a fresh dev cert + bundle.json. Picks a backend * (native by default, dotnet pass-through on macOS when available, with * `--backend` to override) and runs the host trust step unless `--no-trust` * is passed. diff --git a/src/cli/src/commands/inspect.ts b/src/cli/src/commands/inspect.ts index 653774a..47d69e4 100644 --- a/src/cli/src/commands/inspect.ts +++ b/src/cli/src/commands/inspect.ts @@ -35,7 +35,7 @@ interface InspectReport { } /** - * `ddc inspect ` — load a PFX or PEM (cert-only) and report its + * `dcdc inspect ` — load a PFX or PEM (cert-only) and report its * vital statistics. Text by default; `--json` switches to machine-readable. */ export async function runInspect( diff --git a/src/cli/src/commands/trust.ts b/src/cli/src/commands/trust.ts index e986ff2..a7c2ffb 100644 --- a/src/cli/src/commands/trust.ts +++ b/src/cli/src/commands/trust.ts @@ -12,7 +12,7 @@ export interface TrustCommandOptions { } /** - * `ddc trust ` — add an existing cert to the host's OS trust + * `dcdc trust ` — add an existing cert to the host's OS trust * store. Useful when the user already has a cert (generated elsewhere) and * just needs the host trust step. Goes through the shared * `PlatformCertificateStore.trustCertificate` — same hook the host diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts index 42777ce..ac49381 100644 --- a/src/cli/src/index.ts +++ b/src/cli/src/index.ts @@ -10,7 +10,7 @@ import type { BackendMode } from "@devcontainer-dev-certs/shared"; const program = new Command(); program - .name("ddc") + .name("dcdc") .description( "Host-side dev-cert toolkit. Generates, inspects, trusts, and bundles " + "ASP.NET-compatible HTTPS dev certs for use with dev containers — without VS Code." @@ -128,6 +128,6 @@ program // Run. program.parseAsync(process.argv).catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err); - process.stderr.write(`ddc: ${message}\n`); + process.stderr.write(`dcdc: ${message}\n`); process.exit(1); }); diff --git a/src/cli/tests/bundle.test.ts b/src/cli/tests/bundle.test.ts index a08da8d..9e74c3c 100644 --- a/src/cli/tests/bundle.test.ts +++ b/src/cli/tests/bundle.test.ts @@ -13,7 +13,7 @@ import { generateCertificate, exportPfx, exportPem, VALIDITY_DAYS } from "@devco import { runBundle } from "../src/commands/bundle"; /** - * `ddc bundle` is supposed to flag the silent-broken-bundle case: cert + * `dcdc bundle` is supposed to flag the silent-broken-bundle case: cert * files referenced by the bundle live outside the `--out-dir`, so the * containerize step in the writer leaves their absolute host paths * verbatim — and the in-container installer (which only ever sees the @@ -48,9 +48,9 @@ function collectStderr(): string { return writeMock.mock.calls.map((c) => String(c[0])).join(""); } -describe("ddc bundle out-of-dir warning", () => { +describe("dcdc bundle out-of-dir warning", () => { it("does not warn when cert files live inside --out-dir", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-in-")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-in-")); cleanupDirs.push(dir); await makeCertFilesIn(dir); @@ -66,8 +66,8 @@ describe("ddc bundle out-of-dir warning", () => { }); it("warns when cert files are NOT under --out-dir", async () => { - const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-cert-")); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-out-")); + const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-cert-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-out-")); cleanupDirs.push(certDir, outDir); await makeCertFilesIn(certDir); @@ -87,8 +87,8 @@ describe("ddc bundle out-of-dir warning", () => { }); it("flags every out-of-dir file, not just the first one", async () => { - const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-cert-multi-")); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-out-multi-")); + const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-cert-multi-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-out-multi-")); cleanupDirs.push(certDir, outDir); await makeCertFilesIn(certDir); @@ -109,7 +109,7 @@ describe("ddc bundle out-of-dir warning", () => { it("does not warn for the cert path itself when it's the same as the out-dir base", async () => { // Regression guard: a cert at exactly $OUT_DIR/cert.pfx and outDir // = $OUT_DIR should not trip the "outside" check. - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-bundle-test-exact-")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-exact-")); cleanupDirs.push(dir); await makeCertFilesIn(dir); diff --git a/src/cli/tests/doctor.test.ts b/src/cli/tests/doctor.test.ts index dbc8dac..2390b6d 100644 --- a/src/cli/tests/doctor.test.ts +++ b/src/cli/tests/doctor.test.ts @@ -87,10 +87,10 @@ afterEach(() => { process.exitCode = 0; }); -describe("ddc doctor — Linux", () => { +describe("dcdc doctor — Linux", () => { it("reports [ok] when openssl and certutil are both on PATH", async () => { const restore = stubPlatform("linux"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-linux-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-linux-")); cleanupDirs.push(outDir); mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { @@ -117,7 +117,7 @@ describe("ddc doctor — Linux", () => { it("reports [warn] when certutil is missing", async () => { const restore = stubPlatform("linux"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-linux-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-linux-")); cleanupDirs.push(outDir); mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { @@ -139,10 +139,10 @@ describe("ddc doctor — Linux", () => { }); }); -describe("ddc doctor — macOS", () => { +describe("dcdc doctor — macOS", () => { it("checks for the `security` keychain CLI", async () => { const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); cleanupDirs.push(outDir); mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { @@ -165,7 +165,7 @@ describe("ddc doctor — macOS", () => { it("does NOT run Linux-only checks on macOS", async () => { const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); cleanupDirs.push(outDir); try { @@ -181,7 +181,7 @@ describe("ddc doctor — macOS", () => { it("warns when `security` isn't on PATH", async () => { const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-mac-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); cleanupDirs.push(outDir); // Default mockedRunProcess returns exit 1 for everything — including @@ -198,10 +198,10 @@ describe("ddc doctor — macOS", () => { }); }); -describe("ddc doctor — Windows", () => { +describe("dcdc doctor — Windows", () => { it("reports [ok] when both pwsh and certutil.exe resolve", async () => { const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); cleanupDirs.push(outDir); mockedResolveSafeExecPath.mockImplementation((cmd: string) => { @@ -224,7 +224,7 @@ describe("ddc doctor — Windows", () => { it("accepts powershell as a fallback when pwsh is absent, with a note", async () => { const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); cleanupDirs.push(outDir); mockedResolveSafeExecPath.mockImplementation((cmd: string) => { @@ -249,7 +249,7 @@ describe("ddc doctor — Windows", () => { it("warns when neither pwsh nor powershell is found", async () => { const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); cleanupDirs.push(outDir); mockedResolveSafeExecPath.mockImplementation((cmd: string) => { @@ -270,7 +270,7 @@ describe("ddc doctor — Windows", () => { it("warns when certutil.exe is missing", async () => { const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); cleanupDirs.push(outDir); mockedResolveSafeExecPath.mockImplementation((cmd: string) => { @@ -291,7 +291,7 @@ describe("ddc doctor — Windows", () => { it("does NOT shell out to `which` on Windows (uses resolveSafeExecPath)", async () => { const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-doctor-win-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); cleanupDirs.push(outDir); try { diff --git a/src/cli/tests/writer.test.ts b/src/cli/tests/writer.test.ts index d5b062b..ed0a749 100644 --- a/src/cli/tests/writer.test.ts +++ b/src/cli/tests/writer.test.ts @@ -7,7 +7,7 @@ import { writeBundle, type BundleCertEntry } from "../src/bundle/writer"; let tmpDir: string; beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-writer-test-")); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-writer-test-")); }); afterEach(() => { diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts index 3d6c49f..182afcf 100644 --- a/src/shared/src/backends/native.ts +++ b/src/shared/src/backends/native.ts @@ -24,7 +24,7 @@ import type { Backend, GenerateOptions, GenerateResult } from "./types"; * * - `noTrust: true`: generate purely in memory and write only to * `outDir`. The platform store is NOT touched. The typical caller - * here is `ddc generate --no-trust` for "give me cert files to + * here is `dcdc generate --no-trust` for "give me cert files to * bind-mount into a container" — that user has explicitly opted out * of host-side cert installation, so we honor it by keeping our * side effects bounded to `outDir`. diff --git a/src/shared/src/backends/select.ts b/src/shared/src/backends/select.ts index 09c9a0b..02e5312 100644 --- a/src/shared/src/backends/select.ts +++ b/src/shared/src/backends/select.ts @@ -33,7 +33,7 @@ async function autoSelect(): Promise { /** * Report which backend `auto` would pick on this host without actually - * constructing it. Useful for `ddc doctor` and for status surfaces in the + * constructing it. Useful for `dcdc doctor` and for status surfaces in the * VS Code host extension. */ export async function describeAutoBackend(): Promise { diff --git a/src/shared/src/backends/types.ts b/src/shared/src/backends/types.ts index aa2fd86..5f64869 100644 --- a/src/shared/src/backends/types.ts +++ b/src/shared/src/backends/types.ts @@ -1,5 +1,5 @@ /** - * Backend abstraction shared by `ddc` (host CLI) and the VS Code host + * Backend abstraction shared by `dcdc` (host CLI) and the VS Code host * extension. Lets both consumers pick between equivalent generators — * the bundled-in native cert primitives or the `dotnet dev-certs https` * CLI pass-through — without each having to reimplement the selection / diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index 89d3ca5..a6d7876 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -127,7 +127,7 @@ export type { } from "./platform/processUtil"; // Backend abstraction — selectable cert-generator backends shared by the -// host CLI (`ddc`) and the VS Code host extension. Lets both consumers +// host CLI (`dcdc`) and the VS Code host extension. Lets both consumers // pick between the bundled native generator, the `dotnet dev-certs https` // pass-through, and (future) an Aspire-aware variant without each // having to reimplement availability detection / selection logic. diff --git a/src/vscode-ui-extension/tests/nativeBackend.test.ts b/src/vscode-ui-extension/tests/nativeBackend.test.ts index db49348..dd26f92 100644 --- a/src/vscode-ui-extension/tests/nativeBackend.test.ts +++ b/src/vscode-ui-extension/tests/nativeBackend.test.ts @@ -34,9 +34,9 @@ describe("NativeBackend.generate with --no-trust", () => { beforeEach(() => { originalHome = process.env.HOME; - fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-nativebackend-home-")); + fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-nativebackend-home-")); process.env.HOME = fakeHome; - outDir = fs.mkdtempSync(path.join(os.tmpdir(), "ddc-nativebackend-out-")); + outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-nativebackend-out-")); backend = new NativeBackend(); }); From aa1294c06cddb40f2384ead06e90c1a3dfb12409 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:23:43 +0000 Subject: [PATCH 14/41] CLI README: drop the ddc-collision backstory There was never a `ddc` release; mentioning the rename in user-facing docs gives weight to a name that never shipped. Replaced the collision paragraph with a one-line lead-in for the `npx` example that immediately follows. --- src/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/README.md b/src/cli/README.md index c1071f1..48d9c8f 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -24,7 +24,7 @@ dcdc --help Node 18 or newer is required. The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. -`dcdc` is the binary name. The package name is `@devcontainer-dev-certs/cli` — the rename sidesteps a collision with the unrelated [`ddc`](https://www.npmjs.com/package/ddc) package on npm. If you want a one-off invocation without a global install: +For a one-off invocation without a global install: ```bash npx -p @devcontainer-dev-certs/cli dcdc --help From 5275537f6446cebb402c1150a5a46b2a4c963386 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 18:39:46 +0000 Subject: [PATCH 15/41] Wire the CLI into the existing release pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI releases now ride the same `release: [released]` trigger that already publishes the feature to GHCR and uploads VSIXes to the GitHub Release. Same version across every component, single source of truth for "what shipped when," matching the convention `release- feature.yml` already established. # `bump-version.yml` Adds `src/cli/package.json` to the bumped-in-lockstep list so the post-release dev-version PR keeps every component at the same prerelease tag. # `release-feature.yml` - `validate-release` now also asserts `src/cli/package.json`'s version matches the GitHub Release tag. A mismatch (e.g. forgot to merge the bump PR) fails the release before any publish runs. - New `publish-cli` job, runs in parallel with `attest-vsix` after the shared build. Steps: 1. setup-node with `registry-url: https://registry.npmjs.org` so `npm publish` knows where to talk. 2. Pin npm to ^11.5.0. Node 22 ships npm 10.x; OIDC trusted publishing landed as a stable feature in npm 11.5, so we upgrade explicitly rather than depend on Node-bundled drift. 3. Download the `cli-tarball` artifact produced by the build reusable workflow. 4. `npm publish --provenance --access public`. Authentication is via npm's OIDC trusted publisher policy — no `NPM_TOKEN` is stored anywhere. The runner's OIDC token (minted from `id-token: write`) is exchanged for a short- lived publish credential, and `--provenance` causes the same token to sign a SLSA attestation that gets stored on npm's registry alongside the package. 5. `actions/attest-build-provenance` against the tarball file — parallel to the VSIX attestation step, stores a sigstore bundle on GitHub's attestation store. This is independent of the npm-side provenance from step 4, so users can verify the tarball with either `gh attestation verify` or npm's provenance UI. 6. `gh release upload` attaches the tarball to the GitHub Release alongside the VSIXes. Job permissions: `contents: write` (release upload), `id-token: write` (OIDC for both npm publish AND attest-build-provenance), `attestations: write` (sigstore bundle storage). Runs in the `release` environment so it inherits the same protections that gate `publish-feature` and `attest-vsix`. # `build-extensions.yml` - Input renamed: `upload-vsix` → `upload-artifacts`. The flag now controls both VSIX and CLI tarball uploads, and the old name would lie about that. - New step: `Package CLI tarball` runs `npm pack -w src/cli --pack-destination src/cli/dist` after the prod build. The tarball it produces is the exact bytes the `publish-cli` job hands to npm — no rebuild between build and publish, so what was tested in CI is what ships to users. - New step: `Upload CLI tarball` adds it as an artifact named `cli-tarball` for downstream jobs to download. # `ci.yml` Input rename. CI runs (which produce VSIX + CLI tarball as build artifacts on every main push) get the same upload behavior as before plus the new CLI tarball. # One-time setup required before the first release npmjs.com trusted publisher config: Org settings → Packages → Trusted publishers → Add publisher - Publisher: GitHub Actions - Org: dnegstad - Repo: devcontainer-dev-certs - Workflow path: .github/workflows/release-feature.yml - Environment: release Without this entry, the `npm publish` step will fail with an auth error on the first release attempt. After it's configured, all future releases authenticate automatically. # Tests YAML parses cleanly for all 5 workflow files. CLI tests + lint unaffected (no code changes). --- .github/workflows/build-extensions.yml | 20 +++++- .github/workflows/bump-version.yml | 5 +- .github/workflows/ci.yml | 2 +- .github/workflows/release-feature.yml | 84 +++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index d4f7ee0..6a4f26b 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -7,8 +7,8 @@ on: description: Use production build (minified, no sourcemaps) type: boolean default: false - upload-vsix: - description: Package and upload VSIX artifacts + upload-artifacts: + description: Package and upload release artifacts (VSIXes and CLI tarball) for downstream publish jobs. type: boolean default: false @@ -74,13 +74,27 @@ jobs: # regressions before they hit a real release. run: npm pack --dry-run -w src/cli + - name: Package CLI tarball + if: inputs.upload-artifacts + # Produces the actual tarball the publish job will hand to + # `npm publish`. Pinned to `src/cli/dist/` so the upload-artifact + # step has a stable, single-file path. + run: npm pack -w src/cli --pack-destination src/cli/dist + - name: Upload VSIX artifacts - if: inputs.upload-vsix + if: inputs.upload-artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: vsix path: src/vscode-*-extension/*.vsix + - name: Upload CLI tarball + if: inputs.upload-artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: cli-tarball + path: src/cli/dist/*.tgz + windows-certificate-validation: name: Windows Certificate Validation runs-on: windows-latest diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 9343669..35afd6b 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -37,11 +37,12 @@ jobs: run: | NEXT="${{ steps.version.outputs.next }}" - # Extension package.json files + # Extension + CLI package.json files for f in \ src/vscode-ui-extension/package.json \ src/vscode-workspace-extension/package.json \ - src/shared/package.json; do + src/shared/package.json \ + src/cli/package.json; do jq --arg v "$NEXT" '.version = $v' "$f" > "$f.tmp" && mv "$f.tmp" "$f" done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ae234e..507f6d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,4 +12,4 @@ jobs: uses: ./.github/workflows/build-extensions.yml with: production: true - upload-vsix: true + upload-artifacts: true diff --git a/.github/workflows/release-feature.yml b/.github/workflows/release-feature.yml index 6fde874..c86306c 100644 --- a/.github/workflows/release-feature.yml +++ b/.github/workflows/release-feature.yml @@ -39,7 +39,8 @@ jobs: src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json \ src/vscode-ui-extension/package.json \ src/vscode-workspace-extension/package.json \ - src/shared/package.json; do + src/shared/package.json \ + src/cli/package.json; do v=$(jq -r '.version' "$f") if [[ "$v" != "$EXPECTED" ]]; then echo "::error file=$f::version '$v' does not match release tag '$EXPECTED'" @@ -55,7 +56,7 @@ jobs: uses: ./.github/workflows/build-extensions.yml with: production: true - upload-vsix: true + upload-artifacts: true publish-feature: name: Publish feature to GHCR @@ -161,3 +162,82 @@ jobs: set -euo pipefail mapfile -t ARGS <<< "$FILES" gh release upload "$TAG" "${ARGS[@]}" --repo "${{ github.repository }}" + + publish-cli: + name: Publish CLI to npm + needs: [validate-release, build] + runs-on: ubuntu-latest + environment: + name: release + url: https://www.npmjs.com/package/@devcontainer-dev-certs/cli + permissions: + # `contents: write` so we can attach the tarball to the GitHub Release. + # `id-token: write` is the load-bearing one — both npm OIDC trusted + # publishing AND `actions/attest-build-provenance` consume the OIDC + # token GitHub mints from this permission. Without it, npm publish + # would fall through to anonymous mode (failing) and the attestation + # step would error before producing a sigstore bundle. + contents: write + id-token: write + attestations: write + steps: + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + # Configures `.npmrc` so `npm publish` targets the public + # registry. No `NODE_AUTH_TOKEN` is needed — the npm CLI + # exchanges the runner's OIDC token for a short-lived publish + # credential via npmjs.com's trusted publisher policy (set up + # one-time at https://www.npmjs.com/settings/devcontainer-dev-certs/packages). + registry-url: https://registry.npmjs.org + + - name: Pin npm to a version that supports trusted publishing + # Node 22 ships npm 10.x; OIDC trusted publishing landed as a + # stable feature in npm 11.5. Upgrade explicitly so we don't + # depend on whatever Node-bundled npm version drifts in next. + run: npm install -g npm@^11.5.0 + + - name: Download CLI tarball + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: cli-tarball + path: cli-tarball + + - name: Resolve tarball path + id: tarball + run: | + set -euo pipefail + mapfile -t FILES < <(find cli-tarball -type f -name '*.tgz' | sort) + if [[ ${#FILES[@]} -ne 1 ]]; then + echo "::error::Expected exactly one CLI tarball, found ${#FILES[@]}" + printf '%s\n' "${FILES[@]}" + exit 1 + fi + echo "path=${FILES[0]}" >> "$GITHUB_OUTPUT" + echo "Resolved CLI tarball: ${FILES[0]}" + + - name: Publish to npm + # `--provenance` + the `publishConfig.provenance: true` in + # package.json + the trusted publisher policy on npmjs.com is + # the SLSA story for the CLI. The published package on npm + # carries a verifiable link back to this workflow run. + run: npm publish "${{ steps.tarball.outputs.path }}" --provenance --access public + + - name: Attest CLI tarball provenance + # Parallel to the VSIX attestation step: stores a sigstore + # bundle on GitHub's attestation store, verifiable with + # `gh attestation verify --repo dnegstad/devcontainer-dev-certs`. + # This is independent of (and complementary to) the npm-side + # provenance the publish step emits. + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: ${{ steps.tarball.outputs.path }} + + - name: Attach CLI tarball to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.event.release.tag_name }} + FILE: ${{ steps.tarball.outputs.path }} + run: | + set -euo pipefail + gh release upload "$TAG" "$FILE" --repo "${{ github.repository }}" From 1d1f8a1d01b17c4ae718d1464aa7338a28c9ac3f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 20:36:46 +0000 Subject: [PATCH 16/41] Add RELEASING.md covering the normal flow and the one-time CLI bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release pipeline is repo-wide (feature + two VSIXes + CLI on one trigger), but the CLI piece has a chicken-and-egg bootstrap because npm trusted publishing requires the package to exist before the trust policy can be configured. Documenting the procedure in chat history isn't durable — future-maintainer-me (or a successor) will hit this when the next greenfield package is added to the org. Two sections: - **Normal release procedure**: cut a GitHub Release with the right tag, the workflow does everything. Concise — the workflow is the source of truth, the doc just explains the human inputs. - **One-time bootstrap**: publish a content-stub `0.0.0-bootstrap` prerelease from a local terminal, deprecate it immediately, configure the trust policy now that the package exists, then let CI publish the first real version end-to-end with full provenance. The stub is invisible to default range queries (npm semver excludes prereleases by default) and the deprecate message points any explicit pinners at `@latest`. Uses `npm version 0.0.0-bootstrap --no-git-tag-version` instead of the original jq-based version-mangling — the npm CLI has the right tool built in and reaches for fewer external utilities. Navigation path for the trust policy config corrected from my earlier write-up: it's per-package (npm.com/package/ → Settings → Trusted Publisher), not org-wide. Also calls out the post-May-2026 requirement to explicitly select allowed actions when adding the policy. --- RELEASING.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..db9c936 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,98 @@ +# Releasing + +This repository ships four components together on a single release trigger: + +- The devcontainer feature (`ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs`) +- The host VS Code extension (`dnegstad.devcontainer-dev-certs-host`) +- The remote VS Code extension (`dnegstad.devcontainer-dev-certs-remote`) +- The host CLI (`@devcontainer-dev-certs/cli`) + +All four share one repo-wide version. A release is one GitHub Release; everything downstream runs from `.github/workflows/release-feature.yml`. + +## Normal release procedure + +1. Verify every component is at the version you intend to release. `bump-version.yml` keeps them in lockstep automatically after the previous release, so this usually means "merge the open `chore/bump-X.Y.Z-pre` PR and then promote `X.Y.Z-pre` to `X.Y.Z`" via a separate bump PR. +2. Create a GitHub Release with tag `vX.Y.Z` (must match the component versions exactly — `validate-release` will fail the workflow otherwise). +3. Publish the GitHub Release. The `release: [released]` trigger starts the pipeline. + +The workflow: + +- Validates that the tag matches every component's `package.json` / `devcontainer-feature.json` version. +- Builds and tests everything via `build-extensions.yml` in production mode, producing VSIX files and the CLI tarball as workflow artifacts. +- Publishes the devcontainer feature to GHCR with build provenance. +- Attests VSIX provenance via `actions/attest-build-provenance` and attaches both VSIXes to the GitHub Release. +- Publishes the CLI to npm via OIDC trusted publishing (with `--provenance`), attests the tarball via `actions/attest-build-provenance`, and attaches the tarball to the GitHub Release. + +No manual publish commands. The GitHub Release is the trigger; CI does the rest. + +## One-time bootstrap: CLI npm trusted publishing + +npm's trusted publishing requires the package to exist before the trust policy can be configured. The first time we publish `@devcontainer-dev-certs/cli` we have a chicken-and-egg problem: the CI workflow can't authenticate to npm yet (no trust policy), but the trust policy can't be created yet (no package). + +We resolve this by publishing a content-stub version manually under a prerelease tag that no consumer's range query will satisfy, then configuring the trust policy, then letting CI publish all real versions from then on. + +This procedure happens **once** in the lifetime of the package. After the trust policy is configured the workflow takes over and no maintainer needs to touch npm credentials again. + +### Steps + +1. Make sure you have `npm` configured with an account that's a member of the `@devcontainer-dev-certs` org. + + ```bash + npm login + npm whoami + ``` + +2. From a clean checkout, on a throwaway branch (we never push this — the version mangling is local only): + + ```bash + git checkout -b chore/bootstrap-npm-stub + cd src/cli + npm version 0.0.0-bootstrap --no-git-tag-version + npm publish --access public + ``` + + `prepublishOnly` produces the minified bundle. `npm publish` uploads it under the prerelease tag `0.0.0-bootstrap`. Because it's a prerelease, it doesn't match `^1.0.0`, `*`, or any other default range query — installers asking for the package won't get the stub. + +3. Mark the stub as deprecated so anyone who explicitly pins to it sees a warning: + + ```bash + npm deprecate @devcontainer-dev-certs/cli@0.0.0-bootstrap \ + "Stub version published to bootstrap trusted publishing. Install @devcontainer-dev-certs/cli@latest." + ``` + +4. Discard the throwaway branch: + + ```bash + cd ../.. + git checkout main + git branch -D chore/bootstrap-npm-stub + ``` + +5. Configure the trusted publisher on npmjs.com: + + - Navigate to + - Open the **Settings** tab on the package page + - Scroll to the **Trusted Publisher** section + - Add a publisher with: + - Publisher type: **GitHub Actions** + - Organization / user: `dnegstad` + - Repository: `devcontainer-dev-certs` + - Workflow filename: `release-feature.yml` + - Environment: `release` + - Allowed actions: **npm publish** (required for configs created after May 20, 2026) + - Save. + +6. Cut the first real release (`v1.4.0` or whatever the next coordinated repo-wide version is) through the normal procedure above. CI authenticates via OIDC, publishes with `--provenance`, attaches the tarball to the Release. + +7. Verify the first real release end-to-end: + + ```bash + # Tarball attestation via GitHub + gh release download v1.4.0 --pattern '*.tgz' --repo dnegstad/devcontainer-dev-certs + gh attestation verify devcontainer-dev-certs-cli-*.tgz --repo dnegstad/devcontainer-dev-certs + + # Provenance on the npm registry + npm view @devcontainer-dev-certs/cli@1.4.0 + ``` + + Both should report successful verification with the workflow run that produced the artifact. From b99048441ac1db7c1ebe8dbc5e303bfe168cc50d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:56:18 +0000 Subject: [PATCH 17/41] Fix dotnet backend: --no-password is PFX-invalid; bypass dotnet export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dotnet dev-certs https --format Pfx --no-password` errors out on every currently-shipping .NET SDK (6/7/8/9) with "Unrecognized command or argument '--no-password'" — the flag was always PEM-only and the open feature request to extend it to PFX (dotnet/sdk#48482) is in Backlog. The dotnet backend has therefore been broken since it landed: any caller picking `--backend dotnet` (or `auto` on macOS with dotnet available, or `hostCertGenerator: "dotnet"` in the extension) would trip the PFX-export pass on the first invocation, with no PFX written and no host trust performed. The fix is structural rather than tactical — we stop using dotnet's `--export-path` entirely: 1. `dotnet dev-certs https [--trust]` — no export, just generate (if absent) and trust. The cert lands in the .NET store at the conventional location for the platform. 2. `createPlatformStore().findExistingDevCert()` — the same hook the rest of the codebase uses to discover the cert dotnet just deposited. 3. `exportPfx` + `exportPem` from our own primitives — write the files into `outDir` under our naming conventions. This also fixes a secondary issue: `dotnet dev-certs --format PEM --export-path foo.pem` writes `foo.pem` + `foo.pem.key`, but our bundle / inspect sibling-discovery probes `${stem}.key` (i.e. `foo.key`). With the rework, the dotnet backend emits `aspnetcore-dev.key` like the native backend does, so the in-container installer + downstream `dcdc bundle` / `dcdc inspect` all see the key where they expect it. NSS on Linux is supplemented through the same backend. `dotnet dev-certs --trust` on Linux only writes the OpenSSL trust dir + the .NET Root store; the Firefox / Chromium NSS DBs are missed. The dotnet backend now invokes `trustInNss(pemPath)` after a successful trust step on Linux, with the new optional `linuxNssTrustReporter` flowing through `GenerateOptions` so callers (CLI, extension) can surface the outcome. `NativeBackend.generateAndTrust` now accepts and threads the same reporter into `CertManager`'s options — previously it constructed a bare manager that silently dropped NSS reporting. CLI `dcdc generate` defaults to a stderr-logging reporter via the new `src/cli/src/nssReporter.ts`. The host extension already had a toast reporter wired to `CertManager`; this change just lets the CLI's direct-NativeBackend path inherit equivalent surfacing. Eight new tests in `tests/dotnetBackend.test.ts` lock in the contract: no `--no-password` / `--format` / `--export-path` flags ever, naming matches the native backend, NSS is invoked on Linux only when trust is requested, non-zero dotnet exits and empty-store outcomes both produce actionable errors. --- src/shared/src/backends/dotnet.ts | 121 ++++---- src/shared/src/backends/native.ts | 15 +- src/shared/src/backends/types.ts | 13 + .../tests/dotnetBackend.test.ts | 258 ++++++++++++++++++ 4 files changed, 347 insertions(+), 60 deletions(-) create mode 100644 src/vscode-ui-extension/tests/dotnetBackend.test.ts diff --git a/src/shared/src/backends/dotnet.ts b/src/shared/src/backends/dotnet.ts index e8c9d85..282a337 100644 --- a/src/shared/src/backends/dotnet.ts +++ b/src/shared/src/backends/dotnet.ts @@ -1,33 +1,46 @@ import * as fs from "fs"; -import * as path from "path"; -import { loadPfx } from "../cert/loader"; +import { exportPem, exportPfx } from "../cert/exporter"; +import { trustInNss } from "../platform/nssTrust"; import { runProcess } from "../platform/processUtil"; +import { + createPlatformStore, + type LinuxNssTrustReporter, +} from "../platform/types"; import type { Backend, GenerateOptions, GenerateResult } from "./types"; /** - * Dotnet backend: shells out to `dotnet dev-certs https`. On macOS this - * is the canonical way to get a signed-binary-attributed keychain trust - * prompt — the native backend's `security add-trusted-cert` invocation - * works but has a less polished UX because the calling binary isn't a - * notarized Apple cert-management tool. On Windows / Linux the two - * backends end up writing to the same platform store, so the choice is - * mostly stylistic. + * Dotnet backend: shells out to `dotnet dev-certs https` for the + * generate-and-trust step on macOS — where the dotnet binary is + * Apple-notarized and produces a cleaner keychain prompt than our + * `security add-trusted-cert` invocation does — and then re-exports + * the resulting cert from the platform store using our own primitives. * - * Two-pass: one invocation to write the PFX (with `--trust` unless - * `noTrust` is set), a second to write the PEM. We can't combine them — - * `dotnet dev-certs --format ...` only accepts one format per call, and - * `--trust` only does anything on the first invocation anyway (it's - * idempotent w.r.t. the OS trust store). + * Why we don't use dotnet's `--export-path`: * - * `noTrust` only suppresses the OS-trust step here; it does NOT - * suppress the .NET store side effect. `dotnet dev-certs https` - * always persists the generated cert into the .NET X509Store - * regardless of `--trust`. If a caller needs strict isolation — - * cert files in `outDir` and nothing else — they should use the - * native backend, which honors `noTrust` by skipping the store - * entirely. We can't paper over this here without re-implementing - * what `dotnet dev-certs` does, which is the entire point of using - * the dotnet backend in the first place. + * - `--no-password` is PEM-only in every currently-shipping .NET SDK + * (6/7/8/9). PFX export requires a password, which we'd then have + * to strip locally to match our passwordless convention — at which + * point we're already loading and re-exporting, so we may as well + * skip the broken flag combination. + * - `--format PEM --export-path foo.pem` writes `foo.pem` + `foo.pem.key`, + * not `foo.pem` + `foo.key`. The latter is what our `bundle.ts` / + * `inspect.ts` sibling-discovery probes for, what the in-container + * installer expects, and what the native backend produces. Going + * through our own exporters keeps naming uniform across backends. + * + * So the flow is: + * 1. `dotnet dev-certs https [--trust]` — generates (if absent) and + * trusts. No file export. + * 2. `findExistingDevCert()` against the platform store — same path + * the rest of the codebase uses to discover dotnet-installed certs. + * 3. `exportPfx` + `exportPem` — write the files to outDir under our + * conventional names with our conventional permissions. + * + * On Linux, `dotnet dev-certs --trust` only populates the OpenSSL trust + * dir and the .NET Root store; it doesn't touch the NSS DBs that + * Firefox / Chromium read. We supplement that with our own `trustInNss` + * step so the dotnet backend's trust outcome matches the native + * backend's on Linux. */ export class DotnetBackend implements Backend { readonly kind = "dotnet" as const; @@ -40,56 +53,50 @@ export class DotnetBackend implements Backend { async generate(options: GenerateOptions): Promise { fs.mkdirSync(options.outDir, { recursive: true }); - const pfxPath = path.join(options.outDir, "aspnetcore-dev.pfx"); - const pemPath = path.join(options.outDir, "aspnetcore-dev.pem"); - // `dotnet dev-certs --format PEM` writes both `` (cert) and - // `.key` (private key in PEM PKCS#8). - const pemKeyPath = path.join(options.outDir, "aspnetcore-dev.pem.key"); - - const pfxArgs = ["dev-certs", "https"]; - if (!options.noTrust) pfxArgs.push("--trust"); - pfxArgs.push("--format", "Pfx", "--no-password", "--export-path", pfxPath); + const args = ["dev-certs", "https"]; + if (!options.noTrust) args.push("--trust"); - const pfxResult = await runProcess("dotnet", pfxArgs, 60_000); - if (pfxResult.exitCode !== 0) { + const result = await runProcess("dotnet", args, 60_000); + if (result.exitCode !== 0) { throw new Error( - `dotnet dev-certs (PFX pass) failed (exit ${pfxResult.exitCode}): ${pfxResult.stderr || pfxResult.stdout}` + `dotnet dev-certs failed (exit ${result.exitCode}): ${result.stderr || result.stdout}` ); } - const pemResult = await runProcess( - "dotnet", - [ - "dev-certs", - "https", - "--format", - "PEM", - "--no-password", - "--export-path", - pemPath, - ], - 60_000 - ); - if (pemResult.exitCode !== 0) { + const store = await createPlatformStore(); + const found = await store.findExistingDevCert(); + if (!found) { throw new Error( - `dotnet dev-certs (PEM pass) failed (exit ${pemResult.exitCode}): ${pemResult.stderr || pemResult.stdout}` + "dotnet dev-certs completed but no dev cert was found in the platform store afterwards." ); } - const loaded = await loadPfx(pfxPath); - if (!loaded.cert) { - throw new Error( - `dotnet wrote ${pfxPath} but the resulting PFX could not be parsed for thumbprint recovery.` - ); + const pfxPath = await exportPfx(found.cert, found.key, options.outDir); + const { certPath: pemPath, keyPath: pemKeyPath } = exportPem( + found.cert, + found.key, + options.outDir + ); + + if (!options.noTrust && process.platform === "linux") { + await runNssTrust(pemPath, options.linuxNssTrustReporter); } return { pfxPath, pemPath, - pemKeyPath: fs.existsSync(pemKeyPath) ? pemKeyPath : null, - thumbprint: loaded.cert.thumbprintSha1, + pemKeyPath, + thumbprint: found.thumbprint, trusted: !options.noTrust, backendUsed: "dotnet", }; } } + +async function runNssTrust( + pemPath: string, + reporter: LinuxNssTrustReporter | undefined +): Promise { + const result = await trustInNss(pemPath); + reporter?.(result, pemPath); +} diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts index 182afcf..fac0b29 100644 --- a/src/shared/src/backends/native.ts +++ b/src/shared/src/backends/native.ts @@ -5,6 +5,7 @@ import { generateCertificate } from "../cert/generator"; import { loadPfx } from "../cert/loader"; import { CertManager } from "../cert/manager"; import { VALIDITY_DAYS } from "../cert/properties"; +import type { LinuxNssTrustReporter } from "../platform/types"; import type { Backend, GenerateOptions, GenerateResult } from "./types"; /** @@ -45,7 +46,7 @@ export class NativeBackend implements Backend { if (options.noTrust) { return generateFilesOnly(options.outDir); } - return generateAndTrust(options.outDir); + return generateAndTrust(options.outDir, options.linuxNssTrustReporter); } } @@ -71,8 +72,16 @@ async function generateFilesOnly(outDir: string): Promise { }; } -async function generateAndTrust(outDir: string): Promise { - const manager = new CertManager(); +async function generateAndTrust( + outDir: string, + linuxNssTrustReporter: LinuxNssTrustReporter | undefined +): Promise { + // Without a reporter the Linux NSS trust step would still run but its + // outcome would be discarded. The CLI's `dcdc generate` always wires a + // stderr-logging reporter; CertProvider in the host extension wires its + // toast reporter. Both surfaces are responsible for telling the user + // when NSS trust didn't take. + const manager = new CertManager({ linuxNssTrustReporter }); await manager.trust(); await manager.exportCert("pfx", outDir); await manager.exportCert("pem", outDir); diff --git a/src/shared/src/backends/types.ts b/src/shared/src/backends/types.ts index 5f64869..fe0c9d1 100644 --- a/src/shared/src/backends/types.ts +++ b/src/shared/src/backends/types.ts @@ -15,11 +15,24 @@ export type BackendKind = "native" | "dotnet"; export type BackendMode = BackendKind | "auto"; +import type { LinuxNssTrustReporter } from "../platform/types"; + export interface GenerateOptions { /** Directory that receives PFX / PEM / key artifacts. */ outDir: string; /** Skip the host trust step. PFX / PEM are still emitted. */ noTrust: boolean; + /** + * Optional callback invoked after the Linux NSS browser-trust step + * with success/failure status and the PEM path. No-op on other + * platforms. The native backend forwards it to `CertManager`; the + * dotnet backend invokes `trustInNss` directly with it (because + * `dotnet dev-certs --trust` itself doesn't touch the NSS DBs that + * Firefox / Chromium read). Without a reporter, NSS failures are + * silent — surfacing them in a CLI stderr line or a VS Code toast + * is the consumer's job. + */ + linuxNssTrustReporter?: LinuxNssTrustReporter; } export interface GenerateResult { diff --git a/src/vscode-ui-extension/tests/dotnetBackend.test.ts b/src/vscode-ui-extension/tests/dotnetBackend.test.ts new file mode 100644 index 0000000..1fac12c --- /dev/null +++ b/src/vscode-ui-extension/tests/dotnetBackend.test.ts @@ -0,0 +1,258 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import type * as Shared from "@devcontainer-dev-certs/shared"; +import type * as PlatformTypes from "@devcontainer-dev-certs/shared/src/platform/types"; + +// DotnetBackend shells out to the real `dotnet` CLI (which isn't on the +// test runner). Mock the shell-out chokepoint and the platform-store +// constructor so the test drives a synthetic outcome end-to-end. +vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ + runProcess: vi.fn(), + resolveSafeExecPath: vi.fn((c: string) => c), +})); + +vi.mock("@devcontainer-dev-certs/shared/src/platform/types", async () => { + const actual = await vi.importActual( + "@devcontainer-dev-certs/shared/src/platform/types" + ); + return { ...actual, createPlatformStore: vi.fn() }; +}); + +vi.mock("@devcontainer-dev-certs/shared/src/platform/nssTrust", () => ({ + trustInNss: vi.fn(), +})); + +import { DotnetBackend } from "@devcontainer-dev-certs/shared"; +import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; +import { createPlatformStore } from "@devcontainer-dev-certs/shared/src/platform/types"; +import { trustInNss } from "@devcontainer-dev-certs/shared/src/platform/nssTrust"; +import { + generateCertificate, + VALIDITY_DAYS, +} from "@devcontainer-dev-certs/shared"; + +const mockedRunProcess = vi.mocked(runProcess); +const mockedCreatePlatformStore = vi.mocked(createPlatformStore); +const mockedTrustInNss = vi.mocked(trustInNss); + +const cleanupDirs: string[] = []; + +function stubPlatform(value: NodeJS.Platform): () => void { + const original = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value, configurable: true }); + return () => { + if (original) Object.defineProperty(process, "platform", original); + }; +} + +async function makeCert(): ReturnType { + const now = new Date(); + const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); + return generateCertificate(now, expiry); +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +afterEach(() => { + for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); + cleanupDirs.length = 0; +}); + +describe("DotnetBackend.generate", () => { + it("invokes `dotnet dev-certs https --trust` (no --no-password, no --format)", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + + const result = await new DotnetBackend().generate({ + outDir, + noTrust: false, + }); + + // Regression guard for the broken `--no-password` + `--format Pfx` + // combo: we should never invoke dotnet with either flag. + expect(mockedRunProcess).toHaveBeenCalledWith( + "dotnet", + ["dev-certs", "https", "--trust"], + expect.any(Number) + ); + for (const call of mockedRunProcess.mock.calls) { + const args = call[1]; + expect(args).not.toContain("--no-password"); + expect(args).not.toContain("--format"); + expect(args).not.toContain("--export-path"); + } + expect(result.thumbprint).toBe(generated.thumbprint); + }); + + it("omits --trust when noTrust is set", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + + await new DotnetBackend().generate({ outDir, noTrust: true }); + + expect(mockedRunProcess).toHaveBeenCalledWith( + "dotnet", + ["dev-certs", "https"], + expect.any(Number) + ); + }); + + it("writes PFX, PEM, and key into outDir using our naming (foo.key, not foo.pem.key)", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + + const result = await new DotnetBackend().generate({ + outDir, + noTrust: false, + }); + + // Naming matches the native backend so bundle.ts and inspect.ts + // sibling-discovery work uniformly. The dotnet-native convention + // `aspnetcore-dev.pem.key` must NOT leak through here. + expect(result.pfxPath).toBe(path.join(outDir, "aspnetcore-dev.pfx")); + expect(result.pemPath).toBe(path.join(outDir, "aspnetcore-dev.pem")); + expect(result.pemKeyPath).toBe(path.join(outDir, "aspnetcore-dev.key")); + expect(fs.existsSync(result.pfxPath)).toBe(true); + expect(fs.existsSync(result.pemPath)).toBe(true); + expect(fs.existsSync(result.pemKeyPath!)).toBe(true); + // Never write the dotnet-style naming as a byproduct. + expect(fs.existsSync(path.join(outDir, "aspnetcore-dev.pem.key"))).toBe(false); + }); + + it("supplements the trust step with NSS on Linux", async () => { + const restore = stubPlatform("linux"); + try { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + mockedTrustInNss.mockResolvedValue({ + success: true, + message: "ok", + }); + + const reporter = vi.fn(); + await new DotnetBackend().generate({ + outDir, + noTrust: false, + linuxNssTrustReporter: reporter, + }); + + // Verifies the gap-fill: dotnet --trust on Linux misses NSS + // browser DBs, so the backend invokes trustInNss separately. + expect(mockedTrustInNss).toHaveBeenCalledTimes(1); + expect(mockedTrustInNss).toHaveBeenCalledWith( + path.join(outDir, "aspnetcore-dev.pem") + ); + expect(reporter).toHaveBeenCalledTimes(1); + expect(reporter).toHaveBeenCalledWith( + { success: true, message: "ok" }, + path.join(outDir, "aspnetcore-dev.pem") + ); + } finally { + restore(); + } + }); + + it("does NOT run the NSS step on macOS (keychain handles browser trust)", async () => { + const restore = stubPlatform("darwin"); + try { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + + await new DotnetBackend().generate({ outDir, noTrust: false }); + + expect(mockedTrustInNss).not.toHaveBeenCalled(); + } finally { + restore(); + } + }); + + it("does NOT run the NSS step on Linux when noTrust is set", async () => { + const restore = stubPlatform("linux"); + try { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + const generated = await makeCert(); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => generated), + } as unknown as Shared.PlatformCertificateStore); + + await new DotnetBackend().generate({ outDir, noTrust: true }); + + expect(mockedTrustInNss).not.toHaveBeenCalled(); + } finally { + restore(); + } + }); + + it("throws when dotnet exits non-zero", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + + mockedRunProcess.mockResolvedValue({ + exitCode: 1, + stdout: "", + stderr: "Unrecognized command or argument 'whatever'", + }); + + await expect( + new DotnetBackend().generate({ outDir, noTrust: false }) + ).rejects.toThrow(/dotnet dev-certs failed.*Unrecognized command/); + }); + + it("throws with a clear message when the store has no cert post-trust", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + cleanupDirs.push(outDir); + + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); + mockedCreatePlatformStore.mockResolvedValue({ + findExistingDevCert: vi.fn(async () => null), + } as unknown as Shared.PlatformCertificateStore); + + await expect( + new DotnetBackend().generate({ outDir, noTrust: false }) + ).rejects.toThrow(/no dev cert was found in the platform store/); + }); +}); From e3f90dd74157d820b9dd4ac47e5e4b8af5164203 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:56:35 +0000 Subject: [PATCH 18/41] Wire Linux NSS reporter through every trust-step entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux NSS browser-trust failures were passing silently in three places, all variations on the same dropped-callback bug: - `dcdc trust ` constructed `createPlatformStore()` with no `linuxNssTrustReporter`, so when the user's `certutil` was missing or the NSS DB was locked, trust completed quietly without surfacing the gap. Users saw "Trust step complete." and then wondered why Firefox / Chromium still rejected the cert. - `CertProvider.provisionViaConfiguredBackend`'s dotnet branch called `backend.generate({ noTrust: false })` without forwarding any NSS hook. The native branch went through `this.certManager.trust()` which had the host extension's toast reporter wired in; the dotnet branch silently lost it. Linux users with `hostCertGenerator: "dotnet"` got the same gap as the CLI surface above plus no toast-based recovery guidance. - `NativeBackend.generateAndTrust` (the prior commit added the reporter to its options surface but the wiring also needed CLI callers to actually pass one). Single-source-of-truth fix: the reporter is an explicit constructor arg on `CertProvider`, the same one wired into the manager, so the extension's `showBrowserTrustFailureGuidance` toast fires uniformly regardless of which backend the user picked. On the CLI side, `stderrNssTrustReporter` (new, `src/cli/src/nssReporter.ts`) prints a clearly-flagged warning to stderr naming the cert and recommending the libnss3-tools / nss-tools package — wired into both `dcdc generate` and `dcdc trust`. One new test in `tests/hostCertGenerator.test.ts` round-trips a sentinel reporter through CertProvider → dotnet backend so a future refactor that silently drops the wiring fails the suite instead of silently breaking the Linux UX again. --- src/cli/src/commands/trust.ts | 8 +++- src/cli/src/nssReporter.ts | 26 ++++++++++++ src/vscode-ui-extension/src/certProvider.ts | 24 ++++++++++- src/vscode-ui-extension/src/extension.ts | 31 +++++++++----- .../tests/hostCertGenerator.test.ts | 40 +++++++++++++++++++ 5 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 src/cli/src/nssReporter.ts diff --git a/src/cli/src/commands/trust.ts b/src/cli/src/commands/trust.ts index a7c2ffb..e8b04d3 100644 --- a/src/cli/src/commands/trust.ts +++ b/src/cli/src/commands/trust.ts @@ -6,6 +6,7 @@ import { type DevCert, } from "@devcontainer-dev-certs/shared"; import { installCliLogger } from "../logger"; +import { stderrNssTrustReporter } from "../nssReporter"; export interface TrustCommandOptions { verbose?: boolean; @@ -16,7 +17,8 @@ export interface TrustCommandOptions { * store. Useful when the user already has a cert (generated elsewhere) and * just needs the host trust step. Goes through the shared * `PlatformCertificateStore.trustCertificate` — same hook the host - * extension uses. + * extension uses, including the Linux NSS browser-trust step (whose + * outcome is reported on stderr so failures don't pass silently). */ export async function runTrust( certPath: string, @@ -38,7 +40,9 @@ export async function runTrust( cert = loaded.cert; } - const store = await createPlatformStore(); + const store = await createPlatformStore({ + linuxNssTrustReporter: stderrNssTrustReporter, + }); if (await store.isCertTrusted(cert)) { process.stderr.write( diff --git a/src/cli/src/nssReporter.ts b/src/cli/src/nssReporter.ts new file mode 100644 index 0000000..5705019 --- /dev/null +++ b/src/cli/src/nssReporter.ts @@ -0,0 +1,26 @@ +import type { LinuxNssTrustReporter } from "@devcontainer-dev-certs/shared"; + +/** + * Stderr-backed NSS trust reporter for the CLI surfaces. Wire this into + * any backend call that runs the trust step (`dcdc generate`, + * `dcdc trust`) so Linux NSS browser-trust failures don't pass silently — + * the user gets one line that names the trust dir / NSS DB and tells + * them what to install if `certutil` was missing. + * + * No-op on macOS and Windows (the shared layer never calls this on + * non-Linux platforms; this is a defensive belt-and-suspenders). + */ +export const stderrNssTrustReporter: LinuxNssTrustReporter = (result, pemPath) => { + if (result.success) { + process.stderr.write( + `Linux NSS browser trust: ok (${result.message}; cert at ${pemPath})\n` + ); + return; + } + process.stderr.write( + `Linux NSS browser trust: WARN (${result.message}; cert at ${pemPath})\n` + + ` Firefox / Chromium may not trust the cert until this is resolved.\n` + + ` If certutil is missing, install libnss3-tools (Debian / Ubuntu)\n` + + ` or nss-tools (Fedora / RHEL) and re-run.\n` + ); +}; diff --git a/src/vscode-ui-extension/src/certProvider.ts b/src/vscode-ui-extension/src/certProvider.ts index 3543f51..1d37f15 100644 --- a/src/vscode-ui-extension/src/certProvider.ts +++ b/src/vscode-ui-extension/src/certProvider.ts @@ -20,6 +20,7 @@ import type { CertMaterialV2, CertMaterialV3, DefaultKestrelCertSelection, + LinuxNssTrustReporter, } from "@devcontainer-dev-certs/shared"; import { DOTNET_DEV_CERT_NAME } from "@devcontainer-dev-certs/shared"; @@ -54,7 +55,21 @@ export class CertProvider { private cachedUser = new Map(); private warnedExpiredCerts = new Set(); - constructor(private readonly certManager: CertManager) {} + /** + * @param certManager Drives the native provisioning path. + * @param linuxNssTrustReporter Forwarded to the dotnet backend's + * `generate()` so the dotnet path on Linux supplements + * `dotnet dev-certs --trust` (which only writes the OpenSSL trust + * dir + .NET root) with our NSS browser-trust step. Without this + * wiring, the host extension's toast guidance for NSS failures + * never fires under `hostCertGenerator: "dotnet"`. The native / + * auto-resolved-to-native paths get the reporter via `certManager`'s + * own wiring. + */ + constructor( + private readonly certManager: CertManager, + private readonly linuxNssTrustReporter?: LinuxNssTrustReporter + ) {} /** * Legacy single-cert entry point. Returns the dotnet-dev cert material in @@ -225,6 +240,12 @@ export class CertProvider { // backend's contract that we discard. The platform store ends up // populated identically to the native path, so the subsequent // `exportCert` calls work without further special-casing. + // + // On Linux we forward `linuxNssTrustReporter` so the dotnet backend + // supplements `dotnet dev-certs --trust` (which doesn't touch NSS + // browser DBs) with our own `trustInNss` step — wired through the + // same reporter the native path uses so the manual-guidance toast + // fires uniformly regardless of which backend the user picked. log(`Provisioning host dev cert via '${backend.kind}' backend.`); const tmpProvisioningDir = fs.mkdtempSync( path.join(os.tmpdir(), "devcerts-provision-") @@ -233,6 +254,7 @@ export class CertProvider { await backend.generate({ outDir: tmpProvisioningDir, noTrust: false, + linuxNssTrustReporter: this.linuxNssTrustReporter, }); } finally { fs.rmSync(tmpProvisioningDir, { recursive: true, force: true }); diff --git a/src/vscode-ui-extension/src/extension.ts b/src/vscode-ui-extension/src/extension.ts index 329b1f2..f7e4a5c 100644 --- a/src/vscode-ui-extension/src/extension.ts +++ b/src/vscode-ui-extension/src/extension.ts @@ -19,25 +19,36 @@ import { type NonLocalSanEntry, } from "@devcontainer-dev-certs/shared"; import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; -import type { CertBundle, CertBundleV3 } from "@devcontainer-dev-certs/shared"; +import type { + CertBundle, + CertBundleV3, + LinuxNssTrustReporter, +} from "@devcontainer-dev-certs/shared"; const CONTAINER_CERT_CONSENT_KEY = "containerCertProvisionConsented"; export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push(initLogger("Dev Container Dev Certs")); + // Shared NSS reporter: the native path picks it up via `CertManager`'s + // construction; the dotnet path picks it up via `CertProvider`'s + // `provisionViaConfiguredBackend`, which forwards it into the dotnet + // backend's `generate()` so the trust outcome surfaces identically + // regardless of `hostCertGenerator` setting. + const linuxNssTrustReporter: LinuxNssTrustReporter = (result, pemPath) => { + if (result.success) { + log(`Linux NSS trust: ${result.message}`); + return; + } + log(`Linux NSS trust did not fully succeed: ${result.message}`); + void showBrowserTrustFailureGuidance(pemPath, result.message); + }; + const certManager = new CertManager({ localize: vscode.l10n.t, - linuxNssTrustReporter: (result, pemPath) => { - if (result.success) { - log(`Linux NSS trust: ${result.message}`); - return; - } - log(`Linux NSS trust did not fully succeed: ${result.message}`); - void showBrowserTrustFailureGuidance(pemPath, result.message); - }, + linuxNssTrustReporter, }); - const certProvider = new CertProvider(certManager); + const certProvider = new CertProvider(certManager, linuxNssTrustReporter); log("UI extension activated (managed certificate provider)."); diff --git a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts index 52527f0..b523323 100644 --- a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts +++ b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts @@ -222,6 +222,46 @@ describe("CertProvider.provisionViaConfiguredBackend", () => { ); }); + it("forwards the linuxNssTrustReporter to the dotnet backend", async () => { + const { cert, key, thumbprint } = await makeValidCert(); + const { manager, trustSpy } = buildManagerMock(cert, key, thumbprint); + + let receivedReporter: unknown = "uncalled"; + mockedSelectBackend.mockResolvedValue({ + kind: "dotnet", + isAvailable: () => Promise.resolve(true), + generate: vi.fn(async (opts: Shared.GenerateOptions) => { + receivedReporter = opts.linuxNssTrustReporter; + await trustSpy(); + return { + pfxPath: "/dev/null/pfx", + pemPath: "/dev/null/pem", + pemKeyPath: null, + thumbprint: "FAKE", + trusted: true, + backendUsed: "dotnet" as const, + }; + }), + }); + + __setConfig("devcontainerDevCerts", { + generateDotNetCert: true, + hostCertGenerator: "dotnet", + }); + + const sentinelReporter: Shared.LinuxNssTrustReporter = () => { + /* test-supplied reporter, identity matters for the assertion */ + }; + const provider = new CertProvider(manager, sentinelReporter); + await provider.getCertMaterial(true); + + // Regression guard for the gap where the dotnet branch silently + // dropped the reporter on Linux. The exact identity must round-trip + // — substituting a different no-op reporter would silently break + // the host extension's NSS-failure toast. + expect(receivedReporter).toBe(sentinelReporter); + }); + it("cleans up the per-provisioning tmp dir even when the backend throws", async () => { const { cert, key, thumbprint } = await makeValidCert(); const { manager } = buildManagerMock(cert, key, thumbprint); From 27c965e537a3de68ecd20e5d20e123fee43af665 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:56:52 +0000 Subject: [PATCH 19/41] dcdc generate / bundle: trust symmetry + sibling-file probe parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two unrelated CLI ergonomics issues, batched because they share the same test scaffolding. # generate: --no-trust didn't propagate to trustInContainer `dcdc generate --no-trust` suppressed the host-side trust step but emitted `trustInContainer: true` in the bundle.json regardless. A user passing `--no-trust` to get files-only output, then committing the bundle to a repo or copying it to a CI box, would have the host opt-out honored AND silently reversed inside the container — directly contradicting the user's intent at exactly the moment they'd assume the configuration was symmetric. Fix: bundle `trustInContainer` now mirrors `!noTrust`. A user who genuinely wants host-untrust + container-trust can still construct that bundle via `dcdc bundle` after the fact, which is explicit about it. # bundle / inspect: sibling-file probes missed valid combinations Two asymmetries: - For a `.pem` cert path, the sibling-PFX probe only checked `${stem}.pfx`. Openssl writes `.p12` by default (`openssl pkcs12 -export -out cert.p12`), so a user with a cert.pem + cert.p12 pair got `pfxPath: null` in the bundle and the in-container installer had no PFX for Kestrel. - The sibling-key probe only checked `${stem}.key`. `dotnet dev-certs --format PEM --export-path foo.pem` writes `foo.pem.key` (filename-suffixed, not stem-based). A user running `dcdc bundle foo.pem` against dotnet's native naming got `pemKeyPath: null` despite a valid sibling key existing, and `dcdc inspect` reported `hasPrivateKey: false` for the same inputs. Both fixes use a shared `findSiblingKey` helper that probes both conventions in order, and bundle's PFX probe now iterates both `.pfx` and `.p12`. Stem-based naming wins when both exist (matches the more common case + our own exporter). Six new tests cover the trustInContainer propagation (both directions) and the new sibling-file discovery (p12 acceptance, dotnet `.pem.key` discovery, stem-form preference when both exist). --- src/cli/src/commands/bundle.ts | 46 +++++++++++--- src/cli/src/commands/generate.ts | 15 ++++- src/cli/src/commands/inspect.ts | 29 +++++++-- src/cli/tests/bundle.test.ts | 75 ++++++++++++++++++++++ src/cli/tests/generate.test.ts | 104 +++++++++++++++++++++++++++++++ 5 files changed, 251 insertions(+), 18 deletions(-) create mode 100644 src/cli/tests/generate.test.ts diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts index 5aa59fd..a315b31 100644 --- a/src/cli/src/commands/bundle.ts +++ b/src/cli/src/commands/bundle.ts @@ -40,7 +40,7 @@ export async function runBundle( const ext = resolvedCertPath.toLowerCase(); let hostPfxPath: string | null = null; let hostPemPath: string; - let hostPemKeyPath: string | null = null; + let hostPemKeyPath: string | null; let thumbprint: string; const stem = path.join( @@ -52,9 +52,11 @@ export async function runBundle( hostPfxPath = resolvedCertPath; const loaded = await loadPfx(resolvedCertPath); thumbprint = loaded.cert.thumbprintSha1; - // Look for sibling PEM files using common naming conventions. + // Look for sibling PEM cert + key. PEM cert is required (the + // in-container installer plants `{name}.pem` into the trust dir); + // PEM key is optional but needed for `pem-bundle` / `key` extra + // destination formats. const candidatePem = `${stem}.pem`; - const candidateKey = `${stem}.key`; if (fs.existsSync(candidatePem)) { hostPemPath = candidatePem; } else { @@ -62,16 +64,23 @@ export async function runBundle( `Bundle requires a PEM cert next to the PFX. Looked for ${candidatePem}.` ); } - if (fs.existsSync(candidateKey)) hostPemKeyPath = candidateKey; + hostPemKeyPath = findSiblingKey(candidatePem); } else { hostPemPath = resolvedCertPath; - const candidateKey = `${stem}.key`; - const keyPath = fs.existsSync(candidateKey) ? candidateKey : null; - const loaded = loadPemPair(resolvedCertPath, keyPath); + hostPemKeyPath = findSiblingKey(resolvedCertPath); + const loaded = loadPemPair(resolvedCertPath, hostPemKeyPath); thumbprint = loaded.cert.thumbprintSha1; - if (keyPath) hostPemKeyPath = candidateKey; - // Look for sibling PFX too. - if (fs.existsSync(`${stem}.pfx`)) hostPfxPath = `${stem}.pfx`; + // Sibling PKCS#12: accept either extension. Openssl writes `.p12` + // by default (`openssl pkcs12 -export -out ...`); the .NET tooling + // and our own exporter write `.pfx`. Probing only one would silently + // miss the other. + for (const pfxExt of [".pfx", ".p12"]) { + const candidate = `${stem}${pfxExt}`; + if (fs.existsSync(candidate)) { + hostPfxPath = candidate; + break; + } + } } const entry: BundleCertEntry = { @@ -95,6 +104,23 @@ export async function runBundle( process.stderr.write(`Thumbprint: ${thumbprint}\n`); } +/** + * Locate a sibling PEM private key next to a cert PEM, returning the + * first match or null. Probes both naming conventions: + * + * - `.key` — what our exporter writes, what `openssl` writes by + * convention. + * - `.key` — what `dotnet dev-certs --format PEM + * --export-path foo.pem` writes (`foo.pem.key`). + */ +function findSiblingKey(certPath: string): string | null { + const stem = certPath.replace(/\.[^.]+$/, ""); + for (const candidate of [`${stem}.key`, `${certPath}.key`]) { + if (fs.existsSync(candidate)) return candidate; + } + return null; +} + /** * Warn when a cert file referenced by the bundle is NOT under `outDir`. * The writer only rewrites paths under `outDir` to the container mount; diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts index 34c1384..1f8363b 100644 --- a/src/cli/src/commands/generate.ts +++ b/src/cli/src/commands/generate.ts @@ -6,6 +6,7 @@ import { } from "@devcontainer-dev-certs/shared"; import { writeBundle, type BundleCertEntry } from "../bundle/writer"; import { installCliLogger } from "../logger"; +import { stderrNssTrustReporter } from "../nssReporter"; export interface GenerateCommandOptions { outDir?: string; @@ -33,13 +34,18 @@ export async function runGenerate( const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); const backend = await selectBackend(options.backend ?? "auto"); const containerMount = options.containerMount ?? DEFAULT_CONTAINER_MOUNT; + const noTrust = Boolean(options.noTrust); process.stderr.write(`Backend: ${backend.kind}\n`); process.stderr.write(`Out dir: ${outDir}\n`); const result = await backend.generate({ outDir, - noTrust: Boolean(options.noTrust), + noTrust, + // Surface NSS trust outcomes (Linux only) on stderr so the user + // isn't left thinking browser trust succeeded when it silently + // didn't. No-op on macOS / Windows. + linuxNssTrustReporter: stderrNssTrustReporter, }); process.stderr.write( @@ -58,7 +64,12 @@ export async function runGenerate( hostPfxPath: result.pfxPath, hostPemPath: result.pemPath, hostPemKeyPath: result.pemKeyPath, - trustInContainer: true, + // Mirror the host-trust opt-out: if the user passed `--no-trust`, + // they want files-only on both sides of the host/container + // boundary. Forcing `trustInContainer: true` here would honor the + // host opt-out and silently reverse it inside the container — an + // asymmetry that bites anyone who commits the bundle to a repo. + trustInContainer: !noTrust, }; const bundlePath = writeBundle({ hostOutDir: outDir, diff --git a/src/cli/src/commands/inspect.ts b/src/cli/src/commands/inspect.ts index 47d69e4..ad7f9a7 100644 --- a/src/cli/src/commands/inspect.ts +++ b/src/cli/src/commands/inspect.ts @@ -54,6 +54,23 @@ export async function runInspect( } } +/** + * Locate a sibling PEM private key next to a cert PEM, returning the + * first match or null. Probes both naming conventions: + * + * - `.key` — what our exporter writes, what `openssl` writes by + * convention. + * - `.key` — what `dotnet dev-certs --format PEM + * --export-path foo.pem` writes (`foo.pem.key`). + */ +function findSiblingKey(certPath: string): string | null { + const stem = certPath.replace(/\.[^.]+$/, ""); + for (const candidate of [`${stem}.key`, `${certPath}.key`]) { + if (fs.existsSync(candidate)) return candidate; + } + return null; +} + async function buildReport(certPath: string): Promise { const ext = certPath.toLowerCase(); const warnings: string[] = []; @@ -69,12 +86,12 @@ async function buildReport(certPath: string): Promise { hasPrivateKey = loaded.key !== null; } else { format = "pem"; - // PEM inspection: opportunistically look for a sibling key by the - // conventional naming (`stem.key` next to `stem.pem`); if absent, treat - // it as a cert-only PEM. - const stem = certPath.replace(/\.[^.]+$/, ""); - const candidateKeyPath = `${stem}.key`; - const keyPath = fs.existsSync(candidateKeyPath) ? candidateKeyPath : null; + // PEM inspection: opportunistically look for a sibling key. Two + // conventions are in the wild — `stem.key` (our exporter + openssl) + // and `filename.pem.key` (`dotnet dev-certs --format PEM + // --export-path foo.pem` writes `foo.pem.key`). Probe both so + // dotnet-generated key pairs aren't misreported as cert-only. + const keyPath = findSiblingKey(certPath); const loaded = loadPemPair(certPath, keyPath); cert = loaded.cert; hasPrivateKey = loaded.key !== null; diff --git a/src/cli/tests/bundle.test.ts b/src/cli/tests/bundle.test.ts index 9e74c3c..b516b25 100644 --- a/src/cli/tests/bundle.test.ts +++ b/src/cli/tests/bundle.test.ts @@ -123,3 +123,78 @@ describe("dcdc bundle out-of-dir warning", () => { expect(stderr).not.toContain("outside --out-dir"); }); }); + +describe("ddc bundle sibling-file discovery", () => { + it("finds a sibling .p12 (openssl convention) next to a .pem", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-p12-")); + cleanupDirs.push(dir); + await makeCertFilesIn(dir); + // Rename the .pfx → .p12 to simulate the openssl naming convention. + fs.renameSync( + path.join(dir, "aspnetcore-dev.pfx"), + path.join(dir, "aspnetcore-dev.p12") + ); + + await runBundle(path.join(dir, "aspnetcore-dev.pem"), { + outDir: dir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const bundle = JSON.parse( + fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") + ) as Record; + const cert = (bundle.certs as Array>)[0]; + expect(cert.pfxPath).toBe("/host-dev-certs/aspnetcore-dev.p12"); + }); + + it("finds a sibling key with the dotnet `.pem.key` naming", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-pemkey-")); + cleanupDirs.push(dir); + await makeCertFilesIn(dir); + // Simulate `dotnet dev-certs --format PEM --export-path foo.pem` + // by renaming aspnetcore-dev.key → aspnetcore-dev.pem.key. The + // .pfx is removed so the key naming is what the test exercises. + fs.renameSync( + path.join(dir, "aspnetcore-dev.key"), + path.join(dir, "aspnetcore-dev.pem.key") + ); + fs.unlinkSync(path.join(dir, "aspnetcore-dev.pfx")); + + await runBundle(path.join(dir, "aspnetcore-dev.pem"), { + outDir: dir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const bundle = JSON.parse( + fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") + ) as Record; + const cert = (bundle.certs as Array>)[0]; + expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.pem.key"); + }); + + it("prefers the stem-based key naming when both conventions exist", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-bothkeys-")); + cleanupDirs.push(dir); + await makeCertFilesIn(dir); + // Add a dotnet-style key alongside the our-convention one. Probe + // order favors the stem form so an our-exporter cert wins. + fs.copyFileSync( + path.join(dir, "aspnetcore-dev.key"), + path.join(dir, "aspnetcore-dev.pem.key") + ); + + await runBundle(path.join(dir, "aspnetcore-dev.pem"), { + outDir: dir, + containerMount: "/host-dev-certs", + kind: "user", + }); + + const bundle = JSON.parse( + fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") + ) as Record; + const cert = (bundle.certs as Array>)[0]; + expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.key"); + }); +}); diff --git a/src/cli/tests/generate.test.ts b/src/cli/tests/generate.test.ts new file mode 100644 index 0000000..4be7ae3 --- /dev/null +++ b/src/cli/tests/generate.test.ts @@ -0,0 +1,104 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, + vi, +} from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import type * as Shared from "@devcontainer-dev-certs/shared"; + +vi.mock("@devcontainer-dev-certs/shared", async () => { + const actual = await vi.importActual( + "@devcontainer-dev-certs/shared" + ); + return { ...actual, selectBackend: vi.fn() }; +}); + +import { selectBackend } from "@devcontainer-dev-certs/shared"; +import { runGenerate } from "../src/commands/generate"; + +const mockedSelectBackend = vi.mocked(selectBackend); +const cleanupDirs: string[] = []; + +function fakeBackend(): Shared.Backend { + return { + kind: "native", + isAvailable: () => Promise.resolve(true), + generate: vi.fn(async (opts: Shared.GenerateOptions) => ({ + pfxPath: path.join(opts.outDir, "aspnetcore-dev.pfx"), + pemPath: path.join(opts.outDir, "aspnetcore-dev.pem"), + pemKeyPath: path.join(opts.outDir, "aspnetcore-dev.key"), + thumbprint: "ABCDEF1234567890", + trusted: !opts.noTrust, + backendUsed: "native", + })), + }; +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(process.stderr, "write").mockImplementation(() => true); + mockedSelectBackend.mockResolvedValue(fakeBackend()); +}); + +afterEach(() => { + vi.restoreAllMocks(); + for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); + cleanupDirs.length = 0; +}); + +describe("dcdc generate --no-trust propagation to bundle.json", () => { + it("emits trustInContainer:false when --no-trust is passed", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-notrust-")); + cleanupDirs.push(outDir); + + await runGenerate({ + outDir, + backend: "native", + noTrust: true, + }); + + const bundle = JSON.parse( + fs.readFileSync(path.join(outDir, "bundle.json"), "utf-8") + ) as Record; + const cert = (bundle.certs as Array>)[0]; + expect(cert.trustInContainer).toBe(false); + }); + + it("emits trustInContainer:true when --no-trust is NOT passed", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-trust-")); + cleanupDirs.push(outDir); + + await runGenerate({ + outDir, + backend: "native", + }); + + const bundle = JSON.parse( + fs.readFileSync(path.join(outDir, "bundle.json"), "utf-8") + ) as Record; + const cert = (bundle.certs as Array>)[0]; + expect(cert.trustInContainer).toBe(true); + }); + + it("forwards a stderr NSS reporter to the backend (so failures don't pass silently)", async () => { + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-reporter-")); + cleanupDirs.push(outDir); + const backend = fakeBackend(); + mockedSelectBackend.mockResolvedValueOnce(backend); + + await runGenerate({ + outDir, + backend: "native", + }); + + const generateMock = vi.mocked(backend.generate); + expect(generateMock).toHaveBeenCalledTimes(1); + const opts = generateMock.mock.calls[0][0]; + expect(opts.linuxNssTrustReporter).toBeInstanceOf(Function); + }); +}); From 54406f74fa318cd6c1d56910c306987bf5a7ceba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:57:05 +0000 Subject: [PATCH 20/41] setup-cert.sh: honor DEVCONTAINER_DEV_CERTS_*_DIR env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `install.sh` painstakingly exports `DEVCONTAINER_DEV_CERTS_*_DIR` into login shells (via `/etc/profile.d`) and PAM sessions (via `/etc/environment`) so non-VS-Code users get the canonical paths without having to know where the feature decided to chown things. `setup-cert.sh` then completely ignored those env vars and re-derived the paths from `_REMOTE_USER`, which is only set during the feature-build step — NOT in the runtime shell where the fallback installer actually runs. The net effect: a JetBrains / SSH user `developer` running `devcontainer-dev-certs-install --doctor` would have the script probe `/home/vscode/.dotnet/...` (the build-time _REMOTE_USER default) instead of `/home/developer/.dotnet/...` (where install.sh actually chowned things to). Doctor reported failures that didn't exist; `--bundle-json` mode wrote certs the developer's session couldn't read. The fallback installer was effectively broken for any image whose remote user isn't vscode — i.e. exactly the JetBrains / Vim / raw-CLI audience this branch's manual-setup example was meant to serve. Fix is three lines of shell — the env vars take precedence; the `REMOTE_USER_HOME` fallback now also honors `$HOME` (the current shell's user) before falling back to `/home/${REMOTE_USER}`. Legacy path resolution stays available for anyone invoking the script outside a login shell or PAM session. --- .../scripts/setup-cert.sh | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh index 1aa0757..2b5aad2 100755 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh @@ -37,11 +37,18 @@ set -e REMOTE_USER="${_REMOTE_USER:-vscode}" -REMOTE_USER_HOME="${_REMOTE_USER_HOME:-/home/${REMOTE_USER}}" - -DOTNET_STORE_DIR="${REMOTE_USER_HOME}/.dotnet/corefx/cryptography/x509stores/my" -DOTNET_ROOT_STORE_DIR="${REMOTE_USER_HOME}/.dotnet/corefx/cryptography/x509stores/root" -TRUST_DIR="${REMOTE_USER_HOME}/.aspnet/dev-certs/trust" +REMOTE_USER_HOME="${_REMOTE_USER_HOME:-${HOME:-/home/${REMOTE_USER}}}" + +# Honor the DEVCONTAINER_DEV_CERTS_*_DIR vars exported by install.sh +# (via /etc/profile.d and /etc/environment) so non-VS-Code users — +# whose shell session doesn't set _REMOTE_USER — get the same paths +# install.sh chowned at feature-build time. Falling back to the +# REMOTE_USER_HOME computation keeps the legacy path working when +# the env vars aren't present (e.g. running this script directly +# without a login shell or PAM session). +DOTNET_STORE_DIR="${DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR:-${REMOTE_USER_HOME}/.dotnet/corefx/cryptography/x509stores/my}" +DOTNET_ROOT_STORE_DIR="${DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR:-${REMOTE_USER_HOME}/.dotnet/corefx/cryptography/x509stores/root}" +TRUST_DIR="${DEVCONTAINER_DEV_CERTS_TRUST_DIR:-${REMOTE_USER_HOME}/.aspnet/dev-certs/trust}" ensure_openssl() { if ! command -v openssl &>/dev/null; then From 376e6c62242c5aadded522b0245dd6ad8ec6933f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:57:22 +0000 Subject: [PATCH 21/41] macStore.removeCertificates: untrust before delete, with cert file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cleanup path was calling security remove-trusted-cert -d which is wrong in three independent ways: 1. The positional must be a CERT FILE (DER or PEM), not a keychain path. Passing the keychain DB caused `security` to either error out silently (exit code never checked) or treat the keychain as if it were a cert. 2. `-d` is the admin-trust-domain flag. Our `add-trusted-cert` call uses USER trust domain (no `-d`), so removing with `-d` looks in the wrong TrustSettings.plist and our entries are never matched even if the rest of the syntax were right. 3. No thumbprint / cert-identifier was supplied, so even if the positional and domain were both fixed the call would be ambiguous. The result: every time a user ran "Remove dev certificates" on macOS, the `.pfx` files and keychain entries got deleted but trust-settings entries for the now-deleted certs were left dangling. A subsequent regenerate could then trip macOS's duplicate-trust-settings handling in confusing ways. Fix is structural: each PFX we manage gets handled top-to-bottom in the correct order — untrust (with the cert file as the positional, no `-d`), then `delete-certificate -Z `, then `unlinkSync` the PFX. Unlink is last so a mid-cleanup interruption leaves the file in place and the cleanup is restartable. Five new tests in `tests/macStore.test.ts`: - Untrust receives a temp cert file path (under os.tmpdir), no `-d`, no keychain positional. - Untrust runs BEFORE delete-certificate. - PFX is unlinked after keychain teardown. - Regression guard: `remove-trusted-cert -d ` is never invoked. - Multiple dev cert PFXes each get the full untrust + delete + unlink sequence independently. - No-op cleanup when devCertsDir doesn't exist. --- src/shared/src/platform/macStore.ts | 115 +++++++++------- .../tests/macStore.test.ts | 128 ++++++++++++++++++ 2 files changed, 196 insertions(+), 47 deletions(-) diff --git a/src/shared/src/platform/macStore.ts b/src/shared/src/platform/macStore.ts index 8698e03..ca8cea7 100644 --- a/src/shared/src/platform/macStore.ts +++ b/src/shared/src/platform/macStore.ts @@ -222,60 +222,81 @@ export class MacCertificateStore extends BaseCertificateStore { } async removeCertificates(): Promise { - // Collect SHA-1 thumbprints of every dev cert we have on disk so we - // can delete keychain entries by hash. Matching on `-c localhost` is - // too broad — the user may have unrelated `localhost` certs added - // for other tools and we don't want to nuke those. - const thumbprints = new Set(); - if (fs.existsSync(this.devCertsDir)) { - const pfxFiles = fs - .readdirSync(this.devCertsDir) - .filter( - (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + // For each PFX we manage on disk, load it, run untrust + delete-from- + // keychain by thumbprint, then unlink the PFX. Three-step structure + // because trust settings are stored separately from the cert (in + // TrustSettings.plist) and reference it by hash — if we delete the + // cert from the keychain first, the trust settings become orphaned + // dangling entries that the next `add-trusted-cert` may flag as + // duplicates. + // + // Matching dev certs by filename (`aspnetcore-localhost-*.pfx`) + + // the dev-cert OID is narrower than matching keychain entries by + // `-c localhost`: the user may have unrelated `localhost` certs + // added for other tools, and bulk-untrusting by keychain or by CN + // would nuke those too. + if (!fs.existsSync(this.devCertsDir)) return; + + const pfxFiles = fs + .readdirSync(this.devCertsDir) + .filter( + (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + ); + + for (const pfxFile of pfxFiles) { + const pfxPath = path.join(this.devCertsDir, pfxFile); + let parsed: Awaited>; + try { + parsed = await this.loadPfx(pfxPath); + } catch { + // Unparseable — skip the untrust step but still unlink below + // so we don't leave stale files around. + parsed = null; + } + + if (parsed && parsed.cert.hasExtension(ASPNET_HTTPS_OID)) { + // Step 1: untrust. `security remove-trusted-cert` takes a + // cert file (DER / PEM) as its positional, NOT a keychain + // path. Trust settings were added without `-d` (user domain, + // matching `add-trusted-cert` above), so we remove without + // `-d` too. Non-zero exit just means there was no trust + // settings entry to remove — not an error in cleanup. + const tmpCert = path.join( + os.tmpdir(), + `devcert-untrust-${randomUUID()}.cer` ); - for (const pfxFile of pfxFiles) { + fs.writeFileSync(tmpCert, certToDer(parsed.cert)); try { - const result = await this.loadPfx(path.join(this.devCertsDir, pfxFile)); - if (result && result.cert.hasExtension(ASPNET_HTTPS_OID)) { - thumbprints.add(result.thumbprint); + await runProcess("security", ["remove-trusted-cert", tmpCert]); + } finally { + try { + fs.unlinkSync(tmpCert); + } catch { + /* ignore */ } - } catch { - // Skip unparseable files; they're not ours to delete by hash. } - } - } - for (const thumbprint of thumbprints) { - // delete-certificate exits non-zero once there are no more entries - // matching the hash; loop with a generous bound to drain any - // duplicates left by past regenerations. - for (let i = 0; i < 100; i++) { - const result = await runProcess("security", [ - "delete-certificate", - "-Z", - thumbprint, - this.keychainPath, - ]); - if (result.exitCode !== 0) break; + // Step 2: delete the keychain entries. delete-certificate exits + // non-zero once there are no more entries matching the hash; + // loop with a generous bound to drain any duplicates left by + // past regenerations. + for (let i = 0; i < 100; i++) { + const result = await runProcess("security", [ + "delete-certificate", + "-Z", + parsed.thumbprint, + this.keychainPath, + ]); + if (result.exitCode !== 0) break; + } } - } - // Remove trust settings entries that pointed at any of those certs. - await runProcess("security", [ - "remove-trusted-cert", - "-d", - this.keychainPath, - ]); - - // Remove PFX files from disk - if (fs.existsSync(this.devCertsDir)) { - const pfxFiles = fs - .readdirSync(this.devCertsDir) - .filter( - (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") - ); - for (const pfxFile of pfxFiles) { - fs.unlinkSync(path.join(this.devCertsDir, pfxFile)); + // Step 3: unlink the PFX. Done last so a mid-cleanup interruption + // leaves the file in place and the cleanup is restartable. + try { + fs.unlinkSync(pfxPath); + } catch { + /* ignore */ } } } diff --git a/src/vscode-ui-extension/tests/macStore.test.ts b/src/vscode-ui-extension/tests/macStore.test.ts index 6d73456..9b948b7 100644 --- a/src/vscode-ui-extension/tests/macStore.test.ts +++ b/src/vscode-ui-extension/tests/macStore.test.ts @@ -231,3 +231,131 @@ describe("MacCertificateStore.findExistingDevCert", () => { expect(warn).toBeDefined(); }); }); + +describe("MacCertificateStore.removeCertificates", () => { + let store: MacCertificateStore; + + beforeEach(() => { + vi.clearAllMocks(); + logMessages.length = 0; + testHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-mac-rm-")); + fs.mkdirSync(devCertsDir(), { recursive: true }); + store = new MacCertificateStore(); + }); + + afterEach(() => { + fs.rmSync(testHomeDir, { recursive: true, force: true }); + }); + + it("calls `security remove-trusted-cert ` for each dev cert (no `-d`, no keychain positional)", async () => { + const { cert, key, thumbprint } = await makeTestCert(); + const pfxBytes = await buildPfx({ cert, key }); + fs.writeFileSync( + path.join(devCertsDir(), `aspnetcore-localhost-${thumbprint}.pfx`), + pfxBytes + ); + + const sec = setupSecurityMock(); + await store.removeCertificates(); + + const untrust = sec.calls.filter( + (c) => c.cmd === "security" && c.args[0] === "remove-trusted-cert" + ); + expect(untrust).toHaveLength(1); + // No `-d` (we trusted to user domain, not admin) — using -d here + // would look in the wrong trust-settings file and silently miss + // our entry. + expect(untrust[0].args).not.toContain("-d"); + // The positional must be a cert file path under os.tmpdir() — NOT + // the keychain path. Past bug: we were passing the keychain path + // here, which made the command a no-op (or worse, errored). + const tmpDir = os.tmpdir(); + const positional = untrust[0].args[untrust[0].args.length - 1]; + expect(positional.startsWith(tmpDir)).toBe(true); + expect(positional).toMatch(/devcert-untrust-.*\.cer$/); + }); + + it("calls untrust BEFORE delete-certificate (so trust-settings entries aren't orphaned)", async () => { + const { cert, key, thumbprint } = await makeTestCert(); + const pfxBytes = await buildPfx({ cert, key }); + fs.writeFileSync( + path.join(devCertsDir(), `aspnetcore-localhost-${thumbprint}.pfx`), + pfxBytes + ); + + const sec = setupSecurityMock(); + await store.removeCertificates(); + + const untrustIdx = sec.calls.findIndex( + (c) => c.cmd === "security" && c.args[0] === "remove-trusted-cert" + ); + const deleteIdx = sec.calls.findIndex( + (c) => c.cmd === "security" && c.args[0] === "delete-certificate" + ); + expect(untrustIdx).toBeGreaterThanOrEqual(0); + expect(deleteIdx).toBeGreaterThan(untrustIdx); + }); + + it("unlinks the PFX from disk after the keychain teardown", async () => { + const { cert, key, thumbprint } = await makeTestCert(); + const pfxBytes = await buildPfx({ cert, key }); + const pfxPath = path.join( + devCertsDir(), + `aspnetcore-localhost-${thumbprint}.pfx` + ); + fs.writeFileSync(pfxPath, pfxBytes); + + setupSecurityMock(); + await store.removeCertificates(); + + expect(fs.existsSync(pfxPath)).toBe(false); + }); + + it("regression: never calls `security remove-trusted-cert -d `", async () => { + const { cert, key, thumbprint } = await makeTestCert(); + const pfxBytes = await buildPfx({ cert, key }); + fs.writeFileSync( + path.join(devCertsDir(), `aspnetcore-localhost-${thumbprint}.pfx`), + pfxBytes + ); + + const sec = setupSecurityMock(); + await store.removeCertificates(); + + const bad = sec.calls.find( + (c) => + c.cmd === "security" && + c.args[0] === "remove-trusted-cert" && + c.args.includes("-d") + ); + expect(bad).toBeUndefined(); + }); + + it("processes multiple dev cert PFXes independently", async () => { + const a = await makeTestCert(); + const b = await makeTestCert(); + fs.writeFileSync( + path.join(devCertsDir(), `aspnetcore-localhost-${a.thumbprint}.pfx`), + await buildPfx({ cert: a.cert, key: a.key }) + ); + fs.writeFileSync( + path.join(devCertsDir(), `aspnetcore-localhost-${b.thumbprint}.pfx`), + await buildPfx({ cert: b.cert, key: b.key }) + ); + + const sec = setupSecurityMock(); + await store.removeCertificates(); + + const untrustCount = sec.calls.filter( + (c) => c.cmd === "security" && c.args[0] === "remove-trusted-cert" + ).length; + expect(untrustCount).toBe(2); + }); + + it("no-ops cleanly when the devCertsDir doesn't exist", async () => { + fs.rmSync(devCertsDir(), { recursive: true, force: true }); + const sec = setupSecurityMock(); + await store.removeCertificates(); + expect(sec.calls).toHaveLength(0); + }); +}); From a140e40dca5c9fb983f4e1906f020112385ee8bd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 21:57:32 +0000 Subject: [PATCH 22/41] windowsStore: re-probe pwsh on each call if not yet found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `resolvedPwsh` cached the negative result permanently. The first time `getPowerShell()` couldn't find pwsh on PATH — which on Windows can happen for a perfectly mundane reason: the user installed pwsh AFTER the VS Code extension host started, and Windows installers commonly defer PATH propagation to existing processes — the cache pinned the running extension host to PowerShell 5.1 (`powershell`) for the rest of the session. Every subsequent cert store operation then ran the older, slower PS 5.1 instead of pwsh, with no indication that anything was off until the user happened to reload the window. Fix is minimal: only cache the POSITIVE result (pwsh confirmed working). When the probe doesn't find pwsh, return the powershell fallback THIS time but don't pin — next call re-probes pwsh in case it became available in the meantime. The probe cost is ~50ms on Windows; happening once per cert op (a handful of times per session) is well below where it would matter. --- src/shared/src/platform/windowsStore.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/shared/src/platform/windowsStore.ts b/src/shared/src/platform/windowsStore.ts index 01f5eed..34bbddd 100644 --- a/src/shared/src/platform/windowsStore.ts +++ b/src/shared/src/platform/windowsStore.ts @@ -12,21 +12,30 @@ import { ASPNET_HTTPS_OID } from "../cert/properties"; import { certToDer } from "../cert/exporter"; import { type DevCert, type DevKey } from "../cert/types"; -/** Cached PowerShell executable name — prefers pwsh (PowerShell 7+) over powershell (5.1). */ -let resolvedPwsh: string | null = null; +/** + * Cached PowerShell executable name — prefers pwsh (PowerShell 7+) over + * powershell (5.1). Only the POSITIVE result is cached: if the first + * probe finds pwsh, every subsequent call returns it without re-probing. + * If the probe DOESN'T find pwsh, we re-probe on each call rather than + * pinning to powershell forever — Windows installers commonly defer + * `PATH` propagation to existing processes, so a freshly-installed pwsh + * may only become discoverable after the first few cert ops. + */ +let resolvedPwsh: "pwsh" | null = null; export type WindowsStoreLocation = "CurrentUser" | "LocalMachine"; async function getPowerShell(): Promise { - if (resolvedPwsh) return resolvedPwsh; + if (resolvedPwsh === "pwsh") return resolvedPwsh; const pwshResult = await runProcess("pwsh", ["-NoProfile", "-Command", "echo ok"]); if (pwshResult.exitCode === 0) { resolvedPwsh = "pwsh"; - } else { - resolvedPwsh = "powershell"; + return "pwsh"; } - return resolvedPwsh; + // Don't cache the fallback — re-probe next call so a freshly-installed + // pwsh becomes available without needing to restart the extension host. + return "powershell"; } /** From 4a0405216a211ac636da9136a019fa79264081d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:54:30 +0000 Subject: [PATCH 23/41] Backend cleanup: drop PFX reparse; collapse certProvider's double native check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanup-grade fixes from the code review, batched because they touch the same native-backend path. # native.ts: thumbprint from manager.check(), not a PFX reparse `NativeBackend.generateAndTrust` was re-reading the just-exported PFX via `loadPfx` solely to recover a thumbprint that `manager.trust()` had already put into the platform store. The comment justifying the reparse ("symmetric with the dotnet backend's recovery step") went stale when the dotnet backend rework stopped reparsing, leaving a 30-line round-trip — PKCS#12 parse + disk read + null-check error path — for state derivable from `(await manager.check()).thumbprint`. The fix uses `check()` directly. `manager.trust()` guarantees a trusted cert in the store on return, so the thumbprint is guaranteed non-null; we keep one defensive check to surface a broken contract rather than crash on a `.thumbprint` access. loadPfx is no longer imported here. # certProvider.ts: collapse the double native short-circuit `provisionViaConfiguredBackend` checked `setting === "native"` AND then `backend.kind === "native"` six lines apart, both routing to the same `this.certManager.trust()` call. Since `selectBackend("native")` always returns `kind: "native"`, the first check was fully subsumed by the second; the comment claiming "selectBackend is bypassed entirely" was the same staleness pattern as native.ts above — true when it was written, false after the dotnet rework normalized the dispatcher. Now `selectBackend(setting)` runs unconditionally and the single `backend.kind === "native"` arm covers explicit-native + auto-resolved- to-native uniformly. The updated comment is honest about WHY we still defer to `this.certManager` instead of `NativeBackend.generate`: the bare manager in NativeBackend doesn't carry the extension's `localize` (vscode.l10n.t) wiring. NSS reporter is no longer a reason — that's threadable through GenerateOptions now. The "uses certManager.trust() when 'native'" test had to update its assertion (selectBackend is now called even on the native path) and now also pins that backend.generate() does NOT run on the native arm — same guarantee the old `not.toHaveBeenCalled` gave, expressed through the new flow. --- src/shared/src/backends/native.ts | 27 +++++++++---------- src/vscode-ui-extension/src/certProvider.ts | 20 ++++++-------- .../tests/hostCertGenerator.test.ts | 10 +++++-- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts index fac0b29..2a57b0c 100644 --- a/src/shared/src/backends/native.ts +++ b/src/shared/src/backends/native.ts @@ -2,7 +2,6 @@ import * as fs from "fs"; import * as path from "path"; import { exportPem, exportPfx } from "../cert/exporter"; import { generateCertificate } from "../cert/generator"; -import { loadPfx } from "../cert/loader"; import { CertManager } from "../cert/manager"; import { VALIDITY_DAYS } from "../cert/properties"; import type { LinuxNssTrustReporter } from "../platform/types"; @@ -86,25 +85,23 @@ async function generateAndTrust( await manager.exportCert("pfx", outDir); await manager.exportCert("pem", outDir); - const pfxPath = path.join(outDir, "aspnetcore-dev.pfx"); - const pemPath = path.join(outDir, "aspnetcore-dev.pem"); - const pemKeyPath = path.join(outDir, "aspnetcore-dev.key"); - - // Recover the thumbprint by re-reading the exported PFX. Cheaper than - // reaching into the manager's private state and keeps the contract - // symmetric with the `dotnet` backend's recovery step. - const loaded = await loadPfx(pfxPath); - if (!loaded.cert) { + // `manager.trust()` guarantees the cert is present in the platform + // store, so a follow-up `check()` reads the thumbprint without + // touching disk again. (Previously this re-parsed the exported PFX — + // a holdover from when the dotnet backend did the same dance; the + // dotnet rework no longer reparses, so this can read its own state.) + const status = await manager.check(); + if (!status.thumbprint) { throw new Error( - `Native backend wrote ${pfxPath} but it could not be reparsed for thumbprint recovery.` + "Native backend trust() succeeded but check() reports no thumbprint." ); } return { - pfxPath, - pemPath, - pemKeyPath, - thumbprint: loaded.cert.thumbprintSha1, + pfxPath: path.join(outDir, "aspnetcore-dev.pfx"), + pemPath: path.join(outDir, "aspnetcore-dev.pem"), + pemKeyPath: path.join(outDir, "aspnetcore-dev.key"), + thumbprint: status.thumbprint, trusted: true, backendUsed: "native", }; diff --git a/src/vscode-ui-extension/src/certProvider.ts b/src/vscode-ui-extension/src/certProvider.ts index 1d37f15..0b582a6 100644 --- a/src/vscode-ui-extension/src/certProvider.ts +++ b/src/vscode-ui-extension/src/certProvider.ts @@ -217,20 +217,16 @@ export class CertProvider { .getConfiguration("devcontainerDevCerts") .get("hostCertGenerator", "auto"); - // Native is the historical code path. Use the in-process CertManager - // directly so its l10n + Linux NSS reporter wiring (set up in - // extension.ts) is preserved — the shared NativeBackend constructs a - // bare CertManager without those hooks. - if (setting === "native") { - await this.certManager.trust(); - return; - } - const backend = await selectBackend(setting); + + // Native — whether the user explicitly picked it or `auto` resolved + // to it (non-macOS, or macOS without dotnet) — defers to the + // pre-configured CertManager on `this`. NativeBackend.generate would + // produce equivalent OS-store state, but its bare manager misses the + // extension-only `localize` wiring (vscode.l10n.t). Until that's + // also threadable through GenerateOptions, this is the one knob + // worth keeping the bypass for. if (backend.kind === "native") { - // `auto` resolved to native (non-macOS, or macOS without dotnet - // installed). Same reasoning as above — defer to the configured - // CertManager rather than the bare one inside NativeBackend. await this.certManager.trust(); return; } diff --git a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts index b523323..98e65d8 100644 --- a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts +++ b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts @@ -132,6 +132,13 @@ describe("CertProvider.provisionViaConfiguredBackend", () => { const { cert, key, thumbprint } = await makeValidCert(); const { manager, trustSpy } = buildManagerMock(cert, key, thumbprint); + // `selectBackend("native")` returns a native backend; the provider + // sees `kind: "native"` and routes to the pre-configured + // CertManager rather than calling `backend.generate()`. + mockedSelectBackend.mockResolvedValue(fakeBackend("native", () => { + throw new Error("native backend.generate should not run; native path defers to certManager.trust()"); + })); + __setConfig("devcontainerDevCerts", { generateDotNetCert: true, hostCertGenerator: "native", @@ -140,9 +147,8 @@ describe("CertProvider.provisionViaConfiguredBackend", () => { const provider = new CertProvider(manager); await provider.getCertMaterial(true); + expect(mockedSelectBackend).toHaveBeenCalledWith("native"); expect(trustSpy).toHaveBeenCalledTimes(1); - // selectBackend is bypassed entirely on the native short-circuit. - expect(mockedSelectBackend).not.toHaveBeenCalled(); }); it("defers to selectBackend('auto') when hostCertGenerator is unset", async () => { From 8a19d459c95784bedb2de9d2ab3c1eb531eba2fb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:57:03 +0000 Subject: [PATCH 24/41] Move findSiblingKey to shared; consolidate CLI defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both findings flagged duplication that would silently drift if sibling-discovery or default paths change. # findSiblingKey → shared/cert/loader Identical copies lived in `src/cli/src/commands/bundle.ts` and `src/cli/src/commands/inspect.ts`, probing `.key` (our exporter + openssl) and `.key` (dotnet) in order. A future change — new ed25519 key naming, an SDK convention shift — would have required edits in two places, with `dcdc bundle` and `dcdc inspect` reporting silently-different views of the same cert directory if one were missed. Promoted to `src/shared/src/cert/loader.ts` next to `loadPemPair`, re-exported from the package barrel. Both CLI commands now import the shared helper. # CLI defaults → src/cli/src/defaults.ts `DEFAULT_CONTAINER_MOUNT` ("/host-dev-certs") appeared in `bundle.ts` and `generate.ts`; `DEFAULT_OUT_DIR` (~/.dev-certs) in `generate.ts` and `doctor.ts`. Changing either default to support a new mount layout or a different out-dir convention required 2-3 synchronized edits; missing one would surface as `dcdc generate` writing to one location while `dcdc bundle` or `dcdc doctor` inspected another — the kind of inconsistency that surfaces as a user-visible misdiagnosis (doctor reports "out-dir does not exist" because it's looking somewhere generate doesn't write). Both defaults now live in `src/cli/src/defaults.ts` and are imported by every command that needs them. No behavior changes; lint + type-check + 36 CLI / 240 UI tests all green. --- src/cli/src/commands/bundle.ts | 26 ++++++-------------------- src/cli/src/commands/doctor.ts | 4 +--- src/cli/src/commands/generate.ts | 5 +---- src/cli/src/commands/inspect.ts | 18 +----------------- src/cli/src/defaults.ts | 21 +++++++++++++++++++++ src/shared/src/cert/loader.ts | 21 +++++++++++++++++++++ src/shared/src/index.ts | 2 +- 7 files changed, 52 insertions(+), 45 deletions(-) create mode 100644 src/cli/src/defaults.ts diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts index a315b31..2582f07 100644 --- a/src/cli/src/commands/bundle.ts +++ b/src/cli/src/commands/bundle.ts @@ -1,10 +1,15 @@ import * as fs from "fs"; import * as path from "path"; -import { loadPfx, loadPemPair } from "@devcontainer-dev-certs/shared"; +import { + findSiblingKey, + loadPemPair, + loadPfx, +} from "@devcontainer-dev-certs/shared"; import { writeBundle, type BundleCertEntry, } from "../bundle/writer"; +import { DEFAULT_CONTAINER_MOUNT } from "../defaults"; export interface BundleCommandOptions { outDir?: string; @@ -14,8 +19,6 @@ export interface BundleCommandOptions { noTrustInContainer?: boolean; } -const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; - /** * `dcdc bundle ` — emit a `bundle.json` referencing an * already-existing cert file. Useful when the cert was generated by @@ -104,23 +107,6 @@ export async function runBundle( process.stderr.write(`Thumbprint: ${thumbprint}\n`); } -/** - * Locate a sibling PEM private key next to a cert PEM, returning the - * first match or null. Probes both naming conventions: - * - * - `.key` — what our exporter writes, what `openssl` writes by - * convention. - * - `.key` — what `dotnet dev-certs --format PEM - * --export-path foo.pem` writes (`foo.pem.key`). - */ -function findSiblingKey(certPath: string): string | null { - const stem = certPath.replace(/\.[^.]+$/, ""); - for (const candidate of [`${stem}.key`, `${certPath}.key`]) { - if (fs.existsSync(candidate)) return candidate; - } - return null; -} - /** * Warn when a cert file referenced by the bundle is NOT under `outDir`. * The writer only rewrites paths under `outDir` to the container mount; diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts index 18aa0bc..d0d7b1e 100644 --- a/src/cli/src/commands/doctor.ts +++ b/src/cli/src/commands/doctor.ts @@ -1,5 +1,4 @@ import * as fs from "fs"; -import * as os from "os"; import * as path from "path"; import { createPlatformStore, @@ -8,6 +7,7 @@ import { resolveSafeExecPath, runProcess, } from "@devcontainer-dev-certs/shared"; +import { DEFAULT_OUT_DIR } from "../defaults"; import { installCliLogger } from "../logger"; export interface DoctorCommandOptions { @@ -15,8 +15,6 @@ export interface DoctorCommandOptions { verbose?: boolean; } -const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); - interface Check { label: string; status: "ok" | "warn" | "fail"; diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts index 1f8363b..12a1684 100644 --- a/src/cli/src/commands/generate.ts +++ b/src/cli/src/commands/generate.ts @@ -1,10 +1,10 @@ -import * as os from "os"; import * as path from "path"; import { selectBackend, type BackendMode, } from "@devcontainer-dev-certs/shared"; import { writeBundle, type BundleCertEntry } from "../bundle/writer"; +import { DEFAULT_CONTAINER_MOUNT, DEFAULT_OUT_DIR } from "../defaults"; import { installCliLogger } from "../logger"; import { stderrNssTrustReporter } from "../nssReporter"; @@ -17,9 +17,6 @@ export interface GenerateCommandOptions { verbose?: boolean; } -const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); -const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; - /** * `dcdc generate` — produce a fresh dev cert + bundle.json. Picks a backend * (native by default, dotnet pass-through on macOS when available, with diff --git a/src/cli/src/commands/inspect.ts b/src/cli/src/commands/inspect.ts index ad7f9a7..13f3eba 100644 --- a/src/cli/src/commands/inspect.ts +++ b/src/cli/src/commands/inspect.ts @@ -4,6 +4,7 @@ import { ASPNET_HTTPS_OID, CURRENT_CERTIFICATE_VERSION, MINIMUM_CERTIFICATE_VERSION, + findSiblingKey, getCertificateVersion, isValidDevCert, loadPfx, @@ -54,23 +55,6 @@ export async function runInspect( } } -/** - * Locate a sibling PEM private key next to a cert PEM, returning the - * first match or null. Probes both naming conventions: - * - * - `.key` — what our exporter writes, what `openssl` writes by - * convention. - * - `.key` — what `dotnet dev-certs --format PEM - * --export-path foo.pem` writes (`foo.pem.key`). - */ -function findSiblingKey(certPath: string): string | null { - const stem = certPath.replace(/\.[^.]+$/, ""); - for (const candidate of [`${stem}.key`, `${certPath}.key`]) { - if (fs.existsSync(candidate)) return candidate; - } - return null; -} - async function buildReport(certPath: string): Promise { const ext = certPath.toLowerCase(); const warnings: string[] = []; diff --git a/src/cli/src/defaults.ts b/src/cli/src/defaults.ts new file mode 100644 index 0000000..a4cd357 --- /dev/null +++ b/src/cli/src/defaults.ts @@ -0,0 +1,21 @@ +import * as os from "os"; +import * as path from "path"; + +/** + * CLI-wide defaults. Consolidated here so a future change to either + * value lands in one place — previously the out-dir and container-mount + * defaults were duplicated across `generate`, `bundle`, and `doctor`, + * with no compiler help to keep them in sync. + */ + +/** Host directory the CLI writes cert artifacts + `bundle.json` into. */ +export const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); + +/** + * Container-side mount target the host out-dir is expected to be + * bind-mounted to. Recorded into `bundle.json`'s `pfxPath` / `pemPath` + * so the in-container installer reads from the right place. Users with + * a custom mount layout can override per-invocation via + * `--container-mount`. + */ +export const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; diff --git a/src/shared/src/cert/loader.ts b/src/shared/src/cert/loader.ts index d72d644..453fc83 100644 --- a/src/shared/src/cert/loader.ts +++ b/src/shared/src/cert/loader.ts @@ -55,3 +55,24 @@ export function loadPemPair( return buildLoadedCert(cert, key); } + +/** + * Locate a sibling PEM private key next to a cert PEM, returning the + * first match or null. Probes both naming conventions in the wild: + * + * - `.key` — what our exporter writes, what `openssl` writes by + * convention. + * - `.key` — what `dotnet dev-certs --format PEM + * --export-path foo.pem` writes (`foo.pem.key`). + * + * Stem-form wins when both exist; that's the path `loadPemPair` + * documents and the more common case for openssl- / our-exporter- + * produced pairs. + */ +export function findSiblingKey(certPath: string): string | null { + const stem = certPath.replace(/\.[^.]+$/, ""); + for (const candidate of [`${stem}.key`, `${certPath}.key`]) { + if (fs.existsSync(candidate)) return candidate; + } + return null; +} diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index a6d7876..ced89b4 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -42,7 +42,7 @@ export { } from "./cert/properties"; export { buildPfx, parsePfx } from "./cert/pfx"; export type { BuildPfxOptions, ParsedPfx } from "./cert/pfx"; -export { loadPfx, loadPemPair } from "./cert/loader"; +export { loadPfx, loadPemPair, findSiblingKey } from "./cert/loader"; export type { LoadedCert } from "./cert/loader"; export { isValidDevCert, From d3f5e07fb875efceed63fccdc47785450c3ecfda Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 17:58:47 +0000 Subject: [PATCH 25/41] dcdc doctor: probe dotnet once and run check groups concurrently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `runDoctor` was calling `new DotnetBackend().isAvailable()` (spawn 1) and then `describeAutoBackend()` (which on macOS spawned `dotnet --version` again as spawn 2) in series — two ~200ms shellouts where one suffices. The remaining checks (out-dir / bundle.json existence, platform-store enumeration, per-platform tool presence) were also awaited serially despite being data-independent: the platform-store check needed PowerShell on Windows and could take a few hundred milliseconds while the cheap fs.existsSync calls sat behind it. Two fixes, one refactor: 1. `describeAutoBackend` now accepts an optional `dotnetAvailable` hint so callers that have already probed dotnet can skip the re-probe. Default behavior (no hint) is unchanged — the auto- backend resolution path used by `selectBackend("auto")` still probes lazily. 2. `runDoctor` probes dotnet once at the top, then runs four independent check groups (`checkBackends`, `checkOutDir`, `checkPlatformStore`, `checkPlatformTools`) under `Promise.all`. Each returns `Check[]`; the final composition preserves stable output order regardless of which Promise settles first. The check-group split also pulled apart the inline body of `runDoctor`, which had grown to ~100 lines of mixed concerns. Each group is now ~15 lines, named after what it inspects, and testable on its own (existing tests still pass — they assert on the rendered output, not the call orchestration). 10 doctor tests + 36 CLI tests + 241 UI tests all green. --- src/cli/src/commands/doctor.ts | 173 +++++++++++++++++------------- src/shared/src/backends/select.ts | 17 +-- 2 files changed, 108 insertions(+), 82 deletions(-) diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts index d0d7b1e..07580ae 100644 --- a/src/cli/src/commands/doctor.ts +++ b/src/cli/src/commands/doctor.ts @@ -34,100 +34,121 @@ export async function runDoctor( installCliLogger(Boolean(options.verbose)); const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); - const checks: Check[] = []; - // Backend availability. + // Probe dotnet ONCE. The backend-availability and auto-resolution + // lines both depend on this, and `describeAutoBackend()` used to + // spawn its own `dotnet --version` — two spawns per doctor run on + // macOS where one suffices. const dotnetAvailable = await new DotnetBackend().isAvailable(); - checks.push({ - label: "dotnet CLI on PATH", - status: dotnetAvailable ? "ok" : "warn", - detail: dotnetAvailable - ? "found" - : "not found (the 'dotnet' backend is unavailable; native backend will be used)", - }); - const auto = await describeAutoBackend(); - checks.push({ - label: "--backend auto would pick", - status: "ok", - detail: auto, - }); + // All remaining check groups are independent — no group reads state + // another group writes — so they run concurrently. Each returns + // `Check[]` so the final output preserves a stable order regardless + // of which Promise settles first. + const [backendChecks, outDirChecks, storeChecks, toolChecks] = + await Promise.all([ + checkBackends(dotnetAvailable), + Promise.resolve(checkOutDir(outDir)), + checkPlatformStore(), + checkPlatformTools(), + ]); + + const checks: Check[] = [ + ...backendChecks, + ...outDirChecks, + ...storeChecks, + ...toolChecks, + ]; - // Out-dir presence. - if (fs.existsSync(outDir)) { - checks.push({ - label: `out-dir ${outDir}`, - status: "ok", - detail: "exists", - }); - } else { - checks.push({ - label: `out-dir ${outDir}`, - status: "warn", - detail: "does not exist (run `dcdc generate` to create it)", - }); + let failures = 0; + let warnings = 0; + for (const c of checks) { + process.stdout.write(`[${c.status}] ${c.label}: ${c.detail}\n`); + if (c.status === "fail") failures++; + else if (c.status === "warn") warnings++; } + process.stdout.write( + `\n${checks.length} check(s) total — ${failures} fail, ${warnings} warn.\n` + ); - // Bundle.json presence. - const bundlePath = path.join(outDir, "bundle.json"); - if (fs.existsSync(bundlePath)) { - checks.push({ - label: `bundle.json at ${bundlePath}`, - status: "ok", - detail: "found", - }); - } else { - checks.push({ - label: `bundle.json at ${bundlePath}`, - status: "warn", - detail: "not found", - }); + if (failures > 0) { + process.exitCode = 1; } +} + +async function checkBackends(dotnetAvailable: boolean): Promise { + const auto = await describeAutoBackend(dotnetAvailable); + return [ + { + label: "dotnet CLI on PATH", + status: dotnetAvailable ? "ok" : "warn", + detail: dotnetAvailable + ? "found" + : "not found (the 'dotnet' backend is unavailable; native backend will be used)", + }, + { + label: "--backend auto would pick", + status: "ok", + detail: auto, + }, + ]; +} - // Platform store state. +function checkOutDir(outDir: string): Check[] { + const bundlePath = path.join(outDir, "bundle.json"); + return [ + fs.existsSync(outDir) + ? { label: `out-dir ${outDir}`, status: "ok", detail: "exists" } + : { + label: `out-dir ${outDir}`, + status: "warn", + detail: "does not exist (run `dcdc generate` to create it)", + }, + fs.existsSync(bundlePath) + ? { + label: `bundle.json at ${bundlePath}`, + status: "ok", + detail: "found", + } + : { + label: `bundle.json at ${bundlePath}`, + status: "warn", + detail: "not found", + }, + ]; +} + +async function checkPlatformStore(): Promise { try { const store = await createPlatformStore(); const status = await store.checkStatus(); - if (status.exists) { - checks.push({ + if (!status.exists) { + return [ + { + label: "Host platform store has a valid dev cert", + status: "warn", + detail: "no dev cert found in host platform store", + }, + ]; + } + return [ + { label: "Host platform store has a valid dev cert", status: status.isTrusted ? "ok" : "warn", detail: status.isTrusted ? `trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})` : `present but NOT trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})`, - }); - } else { - checks.push({ - label: "Host platform store has a valid dev cert", - status: "warn", - detail: "no dev cert found in host platform store", - }); - } + }, + ]; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); - checks.push({ - label: "Host platform store check", - status: "fail", - detail: message, - }); - } - - for (const c of await checkPlatformTools()) checks.push(c); - - // Print summary. - let failures = 0; - let warnings = 0; - for (const c of checks) { - process.stdout.write(`[${c.status}] ${c.label}: ${c.detail}\n`); - if (c.status === "fail") failures++; - else if (c.status === "warn") warnings++; - } - process.stdout.write( - `\n${checks.length} check(s) total — ${failures} fail, ${warnings} warn.\n` - ); - - if (failures > 0) { - process.exitCode = 1; + return [ + { + label: "Host platform store check", + status: "fail", + detail: message, + }, + ]; } } diff --git a/src/shared/src/backends/select.ts b/src/shared/src/backends/select.ts index 02e5312..18ef60e 100644 --- a/src/shared/src/backends/select.ts +++ b/src/shared/src/backends/select.ts @@ -35,11 +35,16 @@ async function autoSelect(): Promise { * Report which backend `auto` would pick on this host without actually * constructing it. Useful for `dcdc doctor` and for status surfaces in the * VS Code host extension. + * + * Callers that have already probed dotnet (e.g. `dcdc doctor` checks + * dotnet availability for its own line of output) can pass the result + * via `dotnetAvailable` to avoid a second `dotnet --version` spawn. */ -export async function describeAutoBackend(): Promise { - if (process.platform === "darwin") { - const dotnet = new DotnetBackend(); - if (await dotnet.isAvailable()) return "dotnet"; - } - return "native"; +export async function describeAutoBackend( + dotnetAvailable?: boolean +): Promise { + if (process.platform !== "darwin") return "native"; + const available = + dotnetAvailable ?? (await new DotnetBackend().isAvailable()); + return available ? "dotnet" : "native"; } From 9707fb09a81c1398f804b52ce566f28e09cd0d34 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:01:41 +0000 Subject: [PATCH 26/41] Extract stubPlatform into a per-workspace test helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same `stubPlatform` body lived in `cli/tests/select.test.ts`, `cli/tests/doctor.test.ts`, and `ui-extension/tests/dotnetBackend.test.ts`, with two of the three using a self-contained restore-callback pattern and select.test.ts using a divergent outer-state pattern (originalPlatform let-binding + before/afterEach hooks). Any change to how platform-stubbing must work (a future Node tightening process.platform's descriptor, or a new platform we want to detect) would have needed three edits with the risk of one being missed — and the existing divergence in select.test.ts was already a smaller version of that problem. Two per-workspace helper files (`tests/_helpers.ts` in CLI and in the UI extension) export a single `stubPlatform` that returns its restore callback. The three test files import it; select.test.ts is also converted to the restore-callback pattern so all three callsites look identical now. Cross-workspace duplication remains (no test-helpers package exists) but in-workspace duplication is gone, which was the reviewer's specific complaint. Adding such a package wouldn't pay for itself for one ~10-line function. 36 CLI + 241 UI tests still green; lint clean. --- src/cli/tests/_helpers.ts | 24 ++++ src/cli/tests/doctor.test.ts | 10 +- src/cli/tests/select.test.ts | 119 +++++++++--------- src/vscode-ui-extension/tests/_helpers.ts | 20 +++ .../tests/dotnetBackend.test.ts | 10 +- 5 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 src/cli/tests/_helpers.ts create mode 100644 src/vscode-ui-extension/tests/_helpers.ts diff --git a/src/cli/tests/_helpers.ts b/src/cli/tests/_helpers.ts new file mode 100644 index 0000000..e2cd971 --- /dev/null +++ b/src/cli/tests/_helpers.ts @@ -0,0 +1,24 @@ +/** + * Shared test helpers for the CLI test suite. Keep this file small — + * anything that grows beyond a few utilities should move into a per- + * concern helper module to keep imports honest. + */ + +/** + * Override `process.platform` for the duration of a test. Returns a + * restore callback to call from `finally` (or `afterEach`) so the + * stub doesn't leak into sibling tests. Tolerant of platforms where + * the original descriptor is undefined. + * + * Note: Node makes `process.platform` non-writable but `configurable`, + * so `Object.defineProperty` is the supported route. If a future Node + * tightens this, the failure surfaces at the first test that calls + * `stubPlatform` rather than hiding in a side channel. + */ +export function stubPlatform(value: NodeJS.Platform): () => void { + const original = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value, configurable: true }); + return () => { + if (original) Object.defineProperty(process, "platform", original); + }; +} diff --git a/src/cli/tests/doctor.test.ts b/src/cli/tests/doctor.test.ts index 2390b6d..599304e 100644 --- a/src/cli/tests/doctor.test.ts +++ b/src/cli/tests/doctor.test.ts @@ -43,15 +43,9 @@ const mockedResolveSafeExecPath = vi.mocked(resolveSafeExecPath); const mockedCreatePlatformStore = vi.mocked(createPlatformStore); const mockedDescribeAutoBackend = vi.mocked(describeAutoBackend); -const cleanupDirs: string[] = []; +import { stubPlatform } from "./_helpers"; -function stubPlatform(value: NodeJS.Platform): () => void { - const original = Object.getOwnPropertyDescriptor(process, "platform"); - Object.defineProperty(process, "platform", { value, configurable: true }); - return () => { - if (original) Object.defineProperty(process, "platform", original); - }; -} +const cleanupDirs: string[] = []; function collectStdout(): string { const writeMock = vi.mocked(process.stdout.write); diff --git a/src/cli/tests/select.test.ts b/src/cli/tests/select.test.ts index ab34315..db56357 100644 --- a/src/cli/tests/select.test.ts +++ b/src/cli/tests/select.test.ts @@ -1,8 +1,9 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { selectBackend, describeAutoBackend, } from "@devcontainer-dev-certs/shared"; +import { stubPlatform } from "./_helpers"; // `selectBackend('dotnet')` calls into the DotnetBackend's `isAvailable` // which shells out via the shared runProcess. Stub that so the tests don't @@ -16,30 +17,18 @@ import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processU const mockedRunProcess = vi.mocked(runProcess); describe("selectBackend", () => { - let originalPlatform: PropertyDescriptor | undefined; - beforeEach(() => { vi.clearAllMocks(); - originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); - }); - - afterEach(() => { - if (originalPlatform) { - Object.defineProperty(process, "platform", originalPlatform); - } }); - function stubPlatform(value: NodeJS.Platform): void { - Object.defineProperty(process, "platform", { - value, - configurable: true, - }); - } - it("returns the native backend for --backend native regardless of platform", async () => { - stubPlatform("linux"); - const backend = await selectBackend("native"); - expect(backend.kind).toBe("native"); + const restore = stubPlatform("linux"); + try { + const backend = await selectBackend("native"); + expect(backend.kind).toBe("native"); + } finally { + restore(); + } }); it("returns the dotnet backend for --backend dotnet when dotnet is on PATH", async () => { @@ -54,65 +43,77 @@ describe("selectBackend", () => { }); it("auto-picks dotnet on macOS when dotnet is available", async () => { - stubPlatform("darwin"); - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("dotnet"); + const restore = stubPlatform("darwin"); + try { + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("dotnet"); + } finally { + restore(); + } }); it("auto-falls-back to native on macOS when dotnet is unavailable", async () => { - stubPlatform("darwin"); - mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); + const restore = stubPlatform("darwin"); + try { + mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + } finally { + restore(); + } }); it("auto-picks native on Linux regardless of dotnet availability", async () => { - stubPlatform("linux"); - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); + const restore = stubPlatform("linux"); + try { + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + } finally { + restore(); + } }); it("auto-picks native on Windows", async () => { - stubPlatform("win32"); - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); + const restore = stubPlatform("win32"); + try { + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + const backend = await selectBackend("auto"); + expect(backend.kind).toBe("native"); + } finally { + restore(); + } }); }); describe("describeAutoBackend", () => { - let originalPlatform: PropertyDescriptor | undefined; - beforeEach(() => { vi.clearAllMocks(); - originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); - }); - - afterEach(() => { - if (originalPlatform) { - Object.defineProperty(process, "platform", originalPlatform); - } }); - function stubPlatform(value: NodeJS.Platform): void { - Object.defineProperty(process, "platform", { - value, - configurable: true, - }); - } - it("reports 'dotnet' on macOS when dotnet is available", async () => { - stubPlatform("darwin"); - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - expect(await describeAutoBackend()).toBe("dotnet"); + const restore = stubPlatform("darwin"); + try { + mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); + expect(await describeAutoBackend()).toBe("dotnet"); + } finally { + restore(); + } }); it("reports 'native' everywhere else", async () => { - stubPlatform("linux"); - expect(await describeAutoBackend()).toBe("native"); - stubPlatform("win32"); - expect(await describeAutoBackend()).toBe("native"); + const restoreLinux = stubPlatform("linux"); + try { + expect(await describeAutoBackend()).toBe("native"); + } finally { + restoreLinux(); + } + const restoreWin = stubPlatform("win32"); + try { + expect(await describeAutoBackend()).toBe("native"); + } finally { + restoreWin(); + } }); }); diff --git a/src/vscode-ui-extension/tests/_helpers.ts b/src/vscode-ui-extension/tests/_helpers.ts new file mode 100644 index 0000000..64b58ac --- /dev/null +++ b/src/vscode-ui-extension/tests/_helpers.ts @@ -0,0 +1,20 @@ +/** + * Shared test helpers for the UI extension test suite. Mirrors the + * CLI workspace's `tests/_helpers.ts` — same contract, different + * workspace, kept in sync by hand because no cross-workspace test- + * helpers package exists. + */ + +/** + * Override `process.platform` for the duration of a test. Returns a + * restore callback to call from `finally` (or `afterEach`) so the + * stub doesn't leak into sibling tests. Tolerant of platforms where + * the original descriptor is undefined. + */ +export function stubPlatform(value: NodeJS.Platform): () => void { + const original = Object.getOwnPropertyDescriptor(process, "platform"); + Object.defineProperty(process, "platform", { value, configurable: true }); + return () => { + if (original) Object.defineProperty(process, "platform", original); + }; +} diff --git a/src/vscode-ui-extension/tests/dotnetBackend.test.ts b/src/vscode-ui-extension/tests/dotnetBackend.test.ts index 1fac12c..a728a65 100644 --- a/src/vscode-ui-extension/tests/dotnetBackend.test.ts +++ b/src/vscode-ui-extension/tests/dotnetBackend.test.ts @@ -44,15 +44,9 @@ const mockedRunProcess = vi.mocked(runProcess); const mockedCreatePlatformStore = vi.mocked(createPlatformStore); const mockedTrustInNss = vi.mocked(trustInNss); -const cleanupDirs: string[] = []; +import { stubPlatform } from "./_helpers"; -function stubPlatform(value: NodeJS.Platform): () => void { - const original = Object.getOwnPropertyDescriptor(process, "platform"); - Object.defineProperty(process, "platform", { value, configurable: true }); - return () => { - if (original) Object.defineProperty(process, "platform", original); - }; -} +const cleanupDirs: string[] = []; async function makeCert(): ReturnType { const now = new Date(); From 8e259957574d3e1a7a5c1df47a1f2d25c2f66320 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:30:11 +0000 Subject: [PATCH 27/41] Empirically verify macOS dotnet dev-certs disk-cache compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The code review flagged that `DotnetBackend.generate` on macOS reads the cert dotnet writes to ~/.aspnet/dev-certs/https/aspnetcore-localhost- {thumb}.pfx via our parsePfx, and parsePfx strictly rejects non-PBES2 PKCS#12 encryption. The verifier's chain concluded CONFIRMED based on a "Export(X509ContentType.Pfx) → UnixExportProvider → Windows3desPbe" claim, but on closer reading that chain conflated the `--export-path` user-facing export with the macOS disk-cache writer, and the no-password leg of the runtime-internal path wasn't independently verified. The byte-level outcome of `certificate.Export(X509ContentType.Pfx)` (no password) on macOS in current SDKs is what actually decides whether DotnetBackend works on its default-preferred platform. Rather than hunting more remote citations, settle the question empirically. New integration test: - Self-skips unless `process.platform === "darwin"` AND dotnet >= 6 on PATH, so Linux CI and local dev see it skipped. - Sets HOME to a tmpdir so the disk cache lands somewhere we can audit without touching the developer's real ~/.aspnet/dev-certs/. - Runs `dotnet dev-certs https` (no --trust — avoids the keychain trust-settings prompt that would block headless CI). - Locates the aspnetcore-localhost-*.pfx in the redirected cache dir and feeds it to `loadPfx`. - Asserts the load succeeds + returns a SHA-1 thumbprint + carries a private key. On failure, parsePfx's error message names the offending OID, so CI logs will tell us exactly which algorithm to handle if the assumption breaks. Diagnostic line is written to stderr regardless of pass/fail so the result is durable in workflow logs. CI: new `macos-dotnet-dev-certs-cache` job on `macos-latest` parallel to `windows-certificate-validation`. Same shape — checkout, Node 22, npm ci, build UI extension, setup-dotnet 10.0.x, run the one integration test. Runs on every PR and CI push so an aspnetcore algorithm change would surface immediately. Outcomes: - test green → finding 1 REFUTED; DotnetBackend's macOS path works as written and we close that review item. - test red → finding 1 CONFIRMED with the exact OID; we then pick the fix (accept the algorithm in parsePfx, or switch the backend to dotnet's PEM export which sidesteps the question entirely) with real data instead of assumed behavior. --- .github/workflows/build-extensions.yml | 33 ++++ .../dotnetMacosCache.integration.test.ts | 145 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index 6a4f26b..34f7659 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -126,6 +126,39 @@ jobs: - name: Validate PFX with .NET run: npm test -w src/vscode-ui-extension -- tests/dotnetPfx.integration.test.ts + macos-dotnet-dev-certs-cache: + name: macOS dotnet dev-certs disk-cache compatibility + runs-on: macos-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + + - name: Install dependencies + run: npm ci + + - name: Build UI Extension + run: npm run ${{ inputs.production && 'build:prod' || 'build' }} -w src/vscode-ui-extension + + - name: Set up .NET 10 SDK + # Test invokes `dotnet dev-certs https` to produce the actual + # on-disk cache file aspnetcore's MacOSCertificateManager + # writes. We need a real SDK on PATH; the exact major version + # is logged so we can correlate the result with SDK behavior + # if aspnetcore changes its export defaults. + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: "10.0.x" + + - name: Validate dotnet dev-certs disk cache is loadable + # If this fails, `DotnetBackend.generate` on macOS cannot + # discover the cert dotnet just created — the rework that + # replaced `dotnet --export-path` with platform-store + # discovery depends on `parsePfx` accepting whatever + # `certificate.Export(X509ContentType.Pfx)` writes on macOS. + run: npm test -w src/vscode-ui-extension -- tests/dotnetMacosCache.integration.test.ts + feature: name: Validate Devcontainer Feature runs-on: ubuntu-latest diff --git a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts new file mode 100644 index 0000000..fce5ed0 --- /dev/null +++ b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import { execFileSync } from "child_process"; +import { loadPfx } from "../src/cert/loader"; + +/** + * macOS-only integration test that answers a specific code-review + * question: when `DotnetBackend.generate` runs `dotnet dev-certs https + * --trust` and then calls `findExistingDevCert`, can our `parsePfx` + * actually read the on-disk cache file aspnetcore writes? + * + * The cache file path is `~/.aspnet/dev-certs/https/aspnetcore-localhost- + * {thumbprint}.pfx`. The writer in `MacOSCertificateManager.SaveCertificate + * Core` calls `certificate.Export(X509ContentType.Pfx)` with no password + * — but the byte-level PBE algorithm that produces on macOS is what + * determines whether our parser (which strictly accepts PBES2 and + * rejects every legacy PKCS#12 PBE-with-SHA OID) can read it back. + * + * Test isolates the cache file via `HOME=` so it never touches + * the developer's real dev cert state, and it deliberately omits + * `--trust` so there's no keychain trust-settings prompt in CI. + * + * Outcomes: + * - parsePfx loads the file → finding REFUTED, DotnetBackend works on + * macOS as written. + * - parsePfx throws → finding CONFIRMED. The error message names the + * offending OID; we use that to pick the right fix. + * + * The test asserts a successful load. If aspnetcore changes the + * algorithm in a future SDK, we want this to fail loudly rather than + * pass with a stale assumption. + */ + +const isMacOS = process.platform === "darwin"; + +let dotnetMajor = 0; +try { + const v = execFileSync("dotnet", ["--version"], { + timeout: 5000, + stdio: ["ignore", "pipe", "pipe"], + }) + .toString() + .trim(); + dotnetMajor = Number.parseInt(v.split(".")[0] ?? "", 10) || 0; +} catch { + // dotnet not on PATH; the describe.skipIf below skips the suite. +} + +const ready = isMacOS && dotnetMajor >= 6; + +let tmpHome: string; +let cachePfxPath: string; +let loadResult: + | { kind: "ok"; thumbprint: string; hasKey: boolean } + | { kind: "err"; message: string }; + +beforeAll(async () => { + if (!ready) return; + + tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-macos-")); + + // `dotnet dev-certs https` (no --trust): generates a cert if absent, + // saves to the macOS keychain, AND writes the disk cache file at + // $HOME/.aspnet/dev-certs/https/. Setting HOME to a tmpdir redirects + // only the disk cache — the keychain still lives at the user's real + // login keychain. That's intentional: we're testing the disk cache, + // not the keychain. + // + // 60s timeout because first-run cert generation can be slow on cold + // CI runners. + execFileSync("dotnet", ["dev-certs", "https"], { + timeout: 60_000, + env: { ...process.env, HOME: tmpHome }, + stdio: "pipe", + }); + + const cacheDir = path.join(tmpHome, ".aspnet", "dev-certs", "https"); + if (!fs.existsSync(cacheDir)) { + // Surface this loudly — if the cache dir doesn't appear, the test + // premise has changed (aspnetcore moved or skipped the disk cache) + // and any further assertions are meaningless. + throw new Error( + `Expected aspnetcore disk cache at ${cacheDir}; directory does not exist. ` + + `dotnet dev-certs may have changed its on-disk layout.` + ); + } + const pfxes = fs + .readdirSync(cacheDir) + .filter( + (f) => f.startsWith("aspnetcore-localhost-") && f.endsWith(".pfx") + ); + if (pfxes.length === 0) { + throw new Error( + `aspnetcore disk cache dir ${cacheDir} contains no aspnetcore-localhost-*.pfx files.` + ); + } + cachePfxPath = path.join(cacheDir, pfxes[0]); + + try { + const loaded = await loadPfx(cachePfxPath); + loadResult = { + kind: "ok", + thumbprint: loaded.thumbprint, + hasKey: loaded.key !== null, + }; + } catch (err) { + loadResult = { + kind: "err", + message: err instanceof Error ? err.message : String(err), + }; + } +}, 120_000); + +afterAll(() => { + if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true }); +}); + +describe.skipIf(!ready)( + "dotnet dev-certs macOS disk cache → parsePfx", + () => { + it("loads the disk-cache PFX without throwing", () => { + // Diagnostic: write the resolved path AND outcome to stderr so + // CI logs carry an actionable signal even when the assertion + // passes. (stderr instead of console.log to dodge the codebase's + // no-console rule.) + process.stderr.write( + `[macos-cache] dotnet major: ${dotnetMajor}, cache: ${cachePfxPath}, ` + + `result: ${JSON.stringify(loadResult)}\n` + ); + expect(loadResult.kind).toBe("ok"); + }); + + it("recovers a 40-char SHA-1 thumbprint from the cache PFX", () => { + if (loadResult.kind !== "ok") return; + expect(loadResult.thumbprint).toMatch(/^[0-9A-F]{40}$/); + }); + + it("includes the private key (otherwise Kestrel couldn't serve TLS)", () => { + if (loadResult.kind !== "ok") return; + expect(loadResult.hasKey).toBe(true); + }); + } +); From 546b03ed5877c06facf52b3dbe82cb2a286acb3f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 18:57:31 +0000 Subject: [PATCH 28/41] Fix two CI failures from the main merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 1. hostCertGenerator.test.ts: type the manager spies explicitly Main added a `typecheck` step that runs `tsc --noEmit -p tsconfig.lint.json` — and the lint tsconfig includes test files that the per-workspace `tsconfig.json` excludes, so test-only typecheck errors slip past local `npm run test` and surface in CI. `ManagerSpyHandles.trustSpy: ReturnType` widens to `Mock` (callable OR newable). TS won't let you invoke that without first narrowing which side of the union you mean — both `void trustSpy()` at line 190 and `await trustSpy()` at line 241 trip TS2348. Type the spies as `Mock<() => Promise>` / `Mock<() => Promise>` — concrete callable signatures match the call sites' shape. Same pattern the surrounding tests (containerCertAccept, containerCertPush) already use. # 2. macOS workflow: unlock the login keychain before dotnet dev-certs The `macos-dotnet-dev-certs-cache` job failed at the `dotnet dev-certs https` step with `"There was an error saving the HTTPS developer certificate to the current user personal certificate store."`. The error is keychain-side: macOS GitHub Actions runners ship with the login keychain locked, and `dotnet dev-certs` delegates the save to SecurityFramework which requires an unlocked target keychain. Add a step that runs `security unlock-keychain -p ''` against the runner's login keychain before invoking dotnet. The empty password is the documented GH Actions convention for the runner user. `security set-keychain-settings` (no args) disables the auto-lock timeout so a slow dotnet run can't have the keychain re-lock mid-write. This doesn't change the test's contract — we're still observing what `dotnet dev-certs https` writes to the on-disk cache. It just lets dotnet actually get that far. If the test STILL fails after this, the failure mode will be on the parse side (our parsePfx vs the cache file's bytes), which is the question we set out to answer in the first place. --- .github/workflows/build-extensions.yml | 15 +++++++++++++++ .../tests/hostCertGenerator.test.ts | 9 +++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index 5ce80c7..2fdffee 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -163,6 +163,21 @@ jobs: with: dotnet-version: "10.0.x" + - name: Unlock login keychain for dotnet dev-certs + # `dotnet dev-certs https` saves the generated cert into + # CurrentUser\My, which on macOS is the login keychain. GitHub + # Actions macos runners ship with the login keychain locked + # by default — without an unlock, the save call fails with + # "There was an error saving the HTTPS developer certificate + # to the current user personal certificate store." and the + # disk cache we're trying to test never gets written. + # + # The empty password is the documented GH Actions convention + # for the runner user's login keychain. + run: | + security unlock-keychain -p '' "$HOME/Library/Keychains/login.keychain-db" + security set-keychain-settings "$HOME/Library/Keychains/login.keychain-db" + - name: Validate dotnet dev-certs disk cache is loadable # If this fails, `DotnetBackend.generate` on macOS cannot # discover the cert dotnet just created — the rework that diff --git a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts index 98e65d8..adb1073 100644 --- a/src/vscode-ui-extension/tests/hostCertGenerator.test.ts +++ b/src/vscode-ui-extension/tests/hostCertGenerator.test.ts @@ -4,6 +4,7 @@ import { expect, beforeEach, vi, + type Mock, } from "vitest"; import * as fs from "fs"; import * as os from "os"; @@ -43,8 +44,12 @@ async function makeValidCert(): ReturnType { interface ManagerSpyHandles { manager: CertManager; - trustSpy: ReturnType; - checkSpy: ReturnType; + // Concrete callable signatures so call sites (`trustSpy()`, `trustSpy.mock.calls`) + // narrow correctly; `ReturnType` widens to Mock which TS won't let you invoke without first telling it + // which side of the union you mean. + trustSpy: Mock<() => Promise>; + checkSpy: Mock<() => Promise>; } /** From d33c91430cc26f47f3fd43973167fcafe3a2095b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 19:00:15 +0000 Subject: [PATCH 29/41] Fix two more CI failures: install.sh shellcheck + macOS keychain HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # install.sh SC2129 (shellcheck) Three consecutive `echo "..." >> "${PROFILE_SCRIPT}"` lines I added for the per-user $HOME-expanded path hints trip SC2129 in CI's shellcheck step ("Consider using { cmd1; cmd2; } >> file instead of individual redirects"). Collapsed into a single block redirect. Same content emitted, same ordering preserved, one open of PROFILE_SCRIPT instead of three. # dotnetMacosCache test: stop redirecting $HOME Previous attempt unlocked the runner's login keychain (`$HOME/Library/Keychains/login.keychain-db`) in the workflow step, but the test then ran `dotnet dev-certs https` with `env: { HOME: tmpHome }`. macOS keychain APIs resolve the login keychain relative to `$HOME` — with HOME redirected to a tmpdir where no keychain file exists, dotnet's save call failed with the exact same "There was an error saving the HTTPS developer certificate to the current user personal certificate store." error as before, and the workflow unlock did nothing useful because it unlocked a different file. The HOME override was overcautious for the CI context anyway. The runner is ephemeral, so writes to `~/.aspnet/dev-certs/https/` and the login keychain disappear with the runner VM. Drop the override: the test now reads the cache from `os.homedir()` and trusts the workflow's keychain unlock to apply where dotnet actually looks. Local self-skip on Linux still passes. The next macOS run will either succeed (parsing the dotnet-authored cache file → finding 1 REFUTED) or fail at the parse step with parsePfx's specific OID error message (→ finding 1 CONFIRMED with a definitive answer). --- .../src/devcontainer-dev-certs/install.sh | 8 +++-- .../dotnetMacosCache.integration.test.ts | 31 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh b/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh index f7cf494..5d479d4 100755 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/install.sh @@ -340,9 +340,11 @@ append_profile "DEVCONTAINER_DEV_CERTS_EXTRA_DESTINATIONS" "${EXTRA_CERT_DESTINA # $HOME-expanded form into profile.d so each user gets their own at login. append_profile "DEVCONTAINER_DEV_CERTS_INSTALL_BIN" "${FALLBACK_BIN_PATH}" # Profile.d entries that need per-user $HOME expansion at login time. -echo "export DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/my\"" >> "${PROFILE_SCRIPT}" -echo "export DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/root\"" >> "${PROFILE_SCRIPT}" -echo "export DEVCONTAINER_DEV_CERTS_TRUST_DIR=\"\$HOME/.aspnet/dev-certs/trust\"" >> "${PROFILE_SCRIPT}" +{ + echo "export DEVCONTAINER_DEV_CERTS_DOTNET_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/my\"" + echo "export DEVCONTAINER_DEV_CERTS_DOTNET_ROOT_STORE_DIR=\"\$HOME/.dotnet/corefx/cryptography/x509stores/root\"" + echo "export DEVCONTAINER_DEV_CERTS_TRUST_DIR=\"\$HOME/.aspnet/dev-certs/trust\"" +} >> "${PROFILE_SCRIPT}" # Suppress dotnet's first-run HTTPS dev cert provisioning ONLY when the # host is the source. diff --git a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts index fce5ed0..b776f87 100644 --- a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts +++ b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { describe, it, expect, beforeAll } from "vitest"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; @@ -50,7 +50,6 @@ try { const ready = isMacOS && dotnetMajor >= 6; -let tmpHome: string; let cachePfxPath: string; let loadResult: | { kind: "ok"; thumbprint: string; hasKey: boolean } @@ -59,24 +58,25 @@ let loadResult: beforeAll(async () => { if (!ready) return; - tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-macos-")); - - // `dotnet dev-certs https` (no --trust): generates a cert if absent, - // saves to the macOS keychain, AND writes the disk cache file at - // $HOME/.aspnet/dev-certs/https/. Setting HOME to a tmpdir redirects - // only the disk cache — the keychain still lives at the user's real - // login keychain. That's intentional: we're testing the disk cache, - // not the keychain. + // Use the real `$HOME`. Earlier iterations of this test redirected HOME + // to a tmpdir to isolate the disk cache, but the macOS keychain APIs + // dotnet calls into resolve the login keychain from `$HOME/Library/ + // Keychains/login.keychain-db`. A redirected HOME points at a path + // where no keychain exists, and `dotnet dev-certs` fails with + // "There was an error saving the HTTPS developer certificate to the + // current user personal certificate store." before we ever get to + // observe what it would have written to the disk cache. The CI runner + // is ephemeral, so the isolation isn't load-bearing here. // // 60s timeout because first-run cert generation can be slow on cold // CI runners. execFileSync("dotnet", ["dev-certs", "https"], { timeout: 60_000, - env: { ...process.env, HOME: tmpHome }, stdio: "pipe", }); - const cacheDir = path.join(tmpHome, ".aspnet", "dev-certs", "https"); + const home = os.homedir(); + const cacheDir = path.join(home, ".aspnet", "dev-certs", "https"); if (!fs.existsSync(cacheDir)) { // Surface this loudly — if the cache dir doesn't appear, the test // premise has changed (aspnetcore moved or skipped the disk cache) @@ -113,9 +113,10 @@ beforeAll(async () => { } }, 120_000); -afterAll(() => { - if (tmpHome) fs.rmSync(tmpHome, { recursive: true, force: true }); -}); +// No afterAll cleanup — we wrote to the runner's real `~/.aspnet/dev-certs/ +// https/` and login keychain. Both are owned by the ephemeral CI runner +// VM; trying to scrub them would mostly just hide our footprint from a +// follow-up investigation if the next run sees stale state. describe.skipIf(!ready)( "dotnet dev-certs macOS disk cache → parsePfx", From 9b35bd980ec4d1235fb673bec842a5afd67ca59c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 19:02:45 +0000 Subject: [PATCH 30/41] setup-cert.sh: fix two shellcheck findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pre-existed but were only flagged after the merge surfaced them in CI: # SC2181 — direct exit-status check (line 261) `openssl x509 ... -checkend 0` followed by `[ $? -ne 0 ]` is the indirect form shellcheck recommends against. Inverted with `!` inline: ! printf '%s' "${pem}" | openssl x509 -noout -checkend 0 &>/dev/null Same semantics (function returns 0 / true when the cert IS expired), one fewer subshell, no `$?` dance. # SC2006 / SC2086 — backticks in error message text (line 320) The original used Markdown-style backticks around an `openssl rehash` suggestion: `` `openssl rehash ${TRUST_DIR}` ``. Inside a double-quoted bash string those are command substitution — bash WOULD actually invoke `openssl rehash $TRUST_DIR` at message-build time when `fail()` runs (and the unquoted `${TRUST_DIR}` would word-split into separate args). Latent bug masquerading as a style warning. Swapped to single quotes around the suggestion: clearer formatting for the user, no command execution, no word-split. The error message now reads as the user would expect: a literal suggestion in plain quotes, not a phantom command. --- .../src/devcontainer-dev-certs/scripts/setup-cert.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh index 2b5aad2..bd7425c 100755 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/scripts/setup-cert.sh @@ -255,10 +255,10 @@ if [ "${1:-}" = "--doctor" ]; then local path="$1" local pem pem=$(openssl pkcs12 -in "${path}" -nokeys -passin pass: 2>/dev/null) || return 1 - printf '%s' "${pem}" | openssl x509 -noout -checkend 0 &>/dev/null # checkend exits 0 when cert is still valid for at least N seconds, - # 1 when expired. Invert. - [ $? -ne 0 ] + # 1 when expired. Invert so this function returns 0 (true) when + # the cert IS expired. + ! printf '%s' "${pem}" | openssl x509 -noout -checkend 0 &>/dev/null } section ".NET CurrentUser/My contents:" @@ -317,7 +317,7 @@ if [ "${1:-}" = "--doctor" ]; then if [ -n "${found_link}" ]; then info "${pem_name} → $(basename "${found_link}") (hash ${hash})" else - fail "${pem_name}: no c_rehash symlink (${hash}.N) pointing back — OpenSSL won't find this cert; re-run the installer or `openssl rehash ${TRUST_DIR}`" + fail "${pem_name}: no c_rehash symlink (${hash}.N) pointing back — OpenSSL won't find this cert; re-run the installer or 'openssl rehash ${TRUST_DIR}'" fi else fail "${pem_name}: openssl could not compute a subject hash" From 09115d8a04b7be8e7700a992a7e615fc78ef1b84 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 20:58:50 +0000 Subject: [PATCH 31/41] Accept the one legacy PBE algorithm aspnetcore writes on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What `parsePfx` now accepts PKCS#12 files where the cert bag and/or the PKCS#8 shrouded key bag are encrypted with `pbeWithSHA1And3-KeyTripleDES-CBC` (OID 1.2.840.113549.1.12.1.3). Every other legacy PBE-with-SHA OID stays rejected with the existing "re-export with PBES2/AES" error message. The relax-and-decrypt logic lives in a new, narrowly scoped module: `src/shared/src/cert/pkcs12LegacyPbe.ts`. # Why `aspnetcore`'s `MacOSCertificateManager.SaveCertificateCore` calls `certificate.Export(X509ContentType.Pfx)` with no password to write the disk cache at `~/.aspnet/dev-certs/https/aspnetcore-localhost-* .pfx`. dotnet/runtime's managed PKCS#12 writer's default for that call on Unix is 3DES + SHA-1 + 2000 iterations. The Linux dev-cert disk cache, in contrast, goes through `OpenSslDirectoryBasedStore Provider` which calls `ExportPkcs12(PbeParameters(Aes256Cbc, SHA256, …))` explicitly — PBES2/AES. Confirmed empirically against .NET 10.0.301 / runtime 10.0.9 in two environments: - GH Actions macos-latest runner (tests/dotnetMacosCache.integration .test.ts produced the disk cache and we inspected the OID in the rejection message) - A maintainer's local Mac (`openssl pkcs12 -info` reported `pbeWithSHA1And3-KeyTripleDES-CBC` for both bags) Without this change, `DotnetBackend.generate` on macOS — the platform `--backend auto` PREFERS — fails because `findExistingDevCert` tries to load the disk cache and `parsePfx` rejects it. The misleading error is "no dev cert was found in the platform store afterwards" even though dotnet succeeded. The same blocker hits `dcdc inspect`, `dcdc bundle`, and `dcdc trust` against any user-supplied dotnet-on-macOS PFX. # How New file `src/shared/src/cert/pkcs12LegacyPbe.ts`: - `pkcs12Kdf`: RFC 7292 Appendix B key derivation function. SHA-1 based diversifier KDF (NOT PBKDF2). About 40 LOC of careful Buffer arithmetic with explicit comments at the subtle steps (block padding, big-endian I_l + B + 1 carry propagation). - `decryptLegacyPbe`: derive 24-byte 3DES key (diversifier 1) and 8-byte IV (diversifier 2), then `crypto.createDecipheriv( 'des-ede3-cbc', key, iv)`. - `parseLegacyPbeParams`: decode the `pkcs-12PbeParams` SEQUENCE { salt OCTET STRING, iterations INTEGER }. - `SUPPORTED_LEGACY_PBE_OID` / `isSupportedLegacyPbe`: type-guard around the single OID we currently accept. Empty-password handling: UTF-16BE + trailing null terminator unconditionally — including for the empty string. The RFC's literal wording says "empty P for empty password" but every implementation that matters (OpenSSL, dotnet/runtime's managed PKCS#12 writer, Bouncy Castle) always appends the terminator. The aspnetcore disk cache decrypts under that convention; following the RFC literally would fail to read our own target. `src/shared/src/cert/pfx.ts` changes are scoped to the two decrypt points (`decryptSafeContents` and `decryptShroudedKeyBag`): - PBES2 path is unchanged — first branch, calls pkijs the same way it always did. - Legacy 3DES branch is new — extracts the pkcs-12PbeParams + ciphertext, hands them to the new module, parses the cleartext as `SafeContents` (cert bag) or `PrivateKeyInfo` (key bag). - Everything else still throws `unsupportedAlgorithmError`. The `REJECTED_LEGACY_PBE_NAMES` table no longer lists 1.2.840.113549.1.12.1.3; the comment above it spells out why. # Removal criteria The new module's docstring carries a full removal checklist for the day aspnetcore upstream switches macOS to PBES2 (one-line fix on their side: pass `PbeParameters` to `ExportPkcs12`). When that happens AND we've raised our floor SDK to a version that includes the fix, the steps are: 1. Delete `src/shared/src/cert/pkcs12LegacyPbe.ts`. 2. Delete `tests/pkcs12LegacyPbe.test.ts` and `test/fixtures/pkcs12-legacy-3des.pfx`. 3. Re-add 1.2.840.113549.1.12.1.3 to `REJECTED_LEGACY_PBE_NAMES`. 4. Revert the two `decryptSafeContents` / `decryptShroudedKeyBag` branches to "reject anything not PBES2". # Tests `tests/pkcs12LegacyPbe.test.ts` (9 tests): - `SUPPORTED_LEGACY_PBE_OID` names exactly 1.2.840.113549.1.12.1.3. - `isSupportedLegacyPbe` accepts the one OID, rejects the other five legacy PBE-with-SHA OIDs and PBES2. - `decryptLegacyPbe` defensively refuses unsupported OIDs. - `pkcs12Kdf` distinct-key-and-IV / determinism / iteration- sensitivity / non-empty-password sanity checks. - `parsePfx` end-to-end load of `test/fixtures/pkcs12-legacy-3des.pfx` (built via openssl with `PBE-SHA1-3DES` for both cert and key bags + empty password, same shape aspnetcore produces): cert subjectCN == "localhost", key present, thumbprint round-trips. The fixture is a one-shot ~2KB file committed under `test/fixtures/`; the commands to regenerate it are in the test file's docstring. # Sweep 250 UI tests (+9), 36 CLI tests, 79 workspace tests — all green. Lint + type-check clean. Existing PBES2 round-trips unaffected. The CI macOS test on this branch should now PASS (it asserts `loadResult.kind === "ok"` against the aspnetcore disk cache; with this commit, the cache becomes loadable). --- src/shared/src/cert/pfx.ts | 106 +++++-- src/shared/src/cert/pkcs12LegacyPbe.ts | 258 ++++++++++++++++++ .../tests/pkcs12LegacyPbe.test.ts | 118 ++++++++ test/fixtures/pkcs12-legacy-3des.pfx | Bin 0 -> 2381 bytes 4 files changed, 460 insertions(+), 22 deletions(-) create mode 100644 src/shared/src/cert/pkcs12LegacyPbe.ts create mode 100644 src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts create mode 100644 test/fixtures/pkcs12-legacy-3des.pfx diff --git a/src/shared/src/cert/pfx.ts b/src/shared/src/cert/pfx.ts index db94ed2..4fd4b22 100644 --- a/src/shared/src/cert/pfx.ts +++ b/src/shared/src/cert/pfx.ts @@ -24,6 +24,11 @@ import { randomBytes, webcrypto, } from "node:crypto"; +import { + decryptLegacyPbe, + isSupportedLegacyPbe, + parseLegacyPbeParams, +} from "./pkcs12LegacyPbe"; import { DevCert } from "./types"; import { DevKey } from "./types"; @@ -69,14 +74,20 @@ const AES_KEY_LENGTH = 32; // AES-256 const AES_IV_LENGTH = 16; /** - * Well-known PKCS#12 PBE-with-SHA OIDs that we deliberately don't accept on - * the read path. Used purely to render an actionable error message — the - * actual rejection is "anything that isn't PBES2". + * Well-known PKCS#12 PBE-with-SHA OIDs that we deliberately don't accept + * on the read path. Used purely to render an actionable error message — + * the actual rejection is "anything that isn't PBES2, except the one + * legacy OID `pkcs12LegacyPbe.ts` is currently scoped to handle". See + * that module's docstring for context and removal criteria. */ const REJECTED_LEGACY_PBE_NAMES: Record = { "1.2.840.113549.1.12.1.1": "pbeWithSHAAnd128BitRC4", "1.2.840.113549.1.12.1.2": "pbeWithSHAAnd40BitRC4", - "1.2.840.113549.1.12.1.3": "pbeWithSHAAnd3-KeyTripleDES-CBC", + // 1.2.840.113549.1.12.1.3 = pbeWithSHAAnd3-KeyTripleDES-CBC is the + // one legacy algorithm we DO accept (via pkcs12LegacyPbe.ts) because + // aspnetcore writes it on macOS. Keeping it out of this rejection + // list rather than papering over with a special-case in the error + // path; the read flow branches before this table is consulted. "1.2.840.113549.1.12.1.4": "pbeWithSHAAnd2-KeyTripleDES-CBC", "1.2.840.113549.1.12.1.5": "pbeWithSHAAnd128BitRC2-CBC", "1.2.840.113549.1.12.1.6": "pbeWithSHAAnd40BitRC2-CBC", @@ -821,14 +832,30 @@ async function decryptSafeContents( const encryptedData = new pkijs.EncryptedData({ schema: contentInfo.content, }); - const algoOid = - encryptedData.encryptedContentInfo.contentEncryptionAlgorithm - .algorithmId; - if (algoOid !== OID_PBES2) { - throw unsupportedAlgorithmError("cert bag", algoOid); + const algoId = encryptedData.encryptedContentInfo.contentEncryptionAlgorithm; + const algoOid = algoId.algorithmId; + if (algoOid === OID_PBES2) { + const decrypted = await encryptedData.decrypt({ password: passwordBuf }); + return pkijs.SafeContents.fromBER(decrypted); + } + if (isSupportedLegacyPbe(algoOid)) { + // Legacy 3DES path — see pkcs12LegacyPbe.ts for context and the + // removal checklist. Restricted to the one OID aspnetcore writes + // on macOS; every other legacy PBE algorithm still falls through + // to the rejection below. + const params = parseLegacyPbeParams(algoId.algorithmParams); + const ciphertext = bufferFromAsn1OctetString( + encryptedData.encryptedContentInfo.encryptedContent + ); + const decrypted = decryptLegacyPbe( + algoOid, + params, + ciphertext, + Buffer.from(passwordBuf).toString("utf-8") + ); + return pkijs.SafeContents.fromBER(bufferToArrayBuffer(decrypted)); } - const decrypted = await encryptedData.decrypt({ password: passwordBuf }); - return pkijs.SafeContents.fromBER(decrypted); + throw unsupportedAlgorithmError("cert bag", algoOid); } throw new Error( @@ -841,18 +868,33 @@ async function decryptShroudedKeyBag( passwordBuf: ArrayBuffer ): Promise { const algoOid = bag.encryptionAlgorithm.algorithmId; - if (algoOid !== OID_PBES2) { - throw unsupportedAlgorithmError("private key", algoOid); + if (algoOid === OID_PBES2) { + await ( + bag as unknown as { + parseInternalValues: (p: { password: ArrayBuffer }) => Promise; + } + ).parseInternalValues({ password: passwordBuf }); + const pki = bag.parsedValue; + if (!pki) throw new Error("PKCS#8 shrouded key bag has no key."); + const der = pki.toSchema().toBER(false); + return DevKey.fromPkcs8Der(Buffer.from(der)); } - await ( - bag as unknown as { - parseInternalValues: (p: { password: ArrayBuffer }) => Promise; - } - ).parseInternalValues({ password: passwordBuf }); - const pki = bag.parsedValue; - if (!pki) throw new Error("PKCS#8 shrouded key bag has no key."); - const der = pki.toSchema().toBER(false); - return DevKey.fromPkcs8Der(Buffer.from(der)); + if (isSupportedLegacyPbe(algoOid)) { + // Legacy 3DES path — see pkcs12LegacyPbe.ts for context and the + // removal checklist. + const params = parseLegacyPbeParams(bag.encryptionAlgorithm.algorithmParams); + const ciphertext = bufferFromAsn1OctetString(bag.encryptedData); + const decrypted = decryptLegacyPbe( + algoOid, + params, + ciphertext, + Buffer.from(passwordBuf).toString("utf-8") + ); + const pki = pkijs.PrivateKeyInfo.fromBER(bufferToArrayBuffer(decrypted)); + const der = pki.toSchema().toBER(false); + return DevKey.fromPkcs8Der(Buffer.from(der)); + } + throw unsupportedAlgorithmError("private key", algoOid); } function passwordToArrayBuffer(password: string): ArrayBuffer { @@ -870,3 +912,23 @@ function bufferToArrayBuffer(buf: Buffer | Uint8Array): ArrayBuffer { new Uint8Array(ab).set(buf); return ab; } + +/** + * Pull the raw bytes out of an asn1js OctetString — handles both + * primitive (`valueBlock.valueHex`) and constructed (`getValue()`) + * encodings. Used by the legacy-PBE decrypt path to feed ciphertext + * into Node crypto without going through pkijs's internal decryptor. + */ +function bufferFromAsn1OctetString(node: unknown): Buffer { + const os = node as asn1js.OctetString; + // Constructed octet string: concatenated child segments. + const inner = os?.valueBlock?.value; + if (Array.isArray(inner) && inner.length > 0) { + const pieces = inner.map((child) => + Buffer.from((child as asn1js.OctetString).valueBlock.valueHex) + ); + return Buffer.concat(pieces); + } + // Primitive octet string: raw bytes in valueHex. + return Buffer.from(os.valueBlock.valueHex); +} diff --git a/src/shared/src/cert/pkcs12LegacyPbe.ts b/src/shared/src/cert/pkcs12LegacyPbe.ts new file mode 100644 index 0000000..a18ba09 --- /dev/null +++ b/src/shared/src/cert/pkcs12LegacyPbe.ts @@ -0,0 +1,258 @@ +/** + * Legacy PKCS#12 PBE decryptor — narrowly scoped to one algorithm. + * + * # Why this exists + * + * `parsePfx` strictly accepts only modern PBES2/AES encryption for cert + * and key bags. We added that policy because every cert WE produce uses + * PBES2 and every legacy PBE algorithm in the PKCS#12 family is + * cryptographically weak. The lone exception is the cert that + * `aspnetcore`'s `MacOSCertificateManager.SaveCertificateCore` writes + * into `~/.aspnet/dev-certs/https/aspnetcore-localhost-*.pfx`: it calls + * `certificate.Export(X509ContentType.Pfx)` with no password, and + * dotnet/runtime's managed Pkcs12 writer's no-password default on Unix + * is `pbeWithSHA1And3-KeyTripleDES-CBC` (OID 1.2.840.113549.1.12.1.3). + * Confirmed empirically on .NET 10.0.301 / runtime 10.0.9, both in CI + * (macos-latest) and on a maintainer's local Mac. + * + * Without this module, `DotnetBackend.generate` on macOS — the platform + * `--backend auto` PREFERS — fails because `findExistingDevCert` can't + * read aspnetcore's disk cache. Same blocker hits `dcdc inspect`, + * `dcdc bundle`, and `dcdc trust` against any user-supplied + * dotnet-on-macOS-produced PFX. Scoping is per-OID: only the algorithm + * we observed in practice is accepted; the other five RC2/RC4/2-key-3DES + * OIDs in the PKCS#12 family remain rejected with the original error. + * + * # Removal criteria + * + * Delete this module (and its companion test file) when ALL of the + * following hold: + * + * 1. Our floor-supported .NET SDK no longer produces 3DES-encrypted + * disk-cache PFXes on macOS — i.e. aspnetcore has switched + * `MacOSCertificateManager.SaveCertificateCore` from + * `Export(X509ContentType.Pfx)` to `ExportPkcs12(PbeParameters(...))` + * with explicit PBES2 parameters, matching the + * `OpenSslDirectoryBasedStoreProvider` Linux path. Verify via + * `tests/dotnetMacosCache.integration.test.ts` against the new + * minimum SDK version. + * 2. No user-facing report has surfaced legacy-PBE PFXes from any + * other source that we care about loading. + * + * Removal steps: + * + * 1. Delete this file. + * 2. Delete `tests/pkcs12LegacyPbe.test.ts` (in the UI extension test + * tree). + * 3. Delete `test/fixtures/pkcs12-legacy-3des.pfx`. + * 4. In `src/shared/src/cert/pfx.ts`: + * - Add `"1.2.840.113549.1.12.1.3": "pbeWithSHAAnd3-KeyTripleDES-CBC"` + * back to `REJECTED_LEGACY_PBE_NAMES`. + * - Revert `decryptSafeContents` and `decryptShroudedKeyBag` to + * their original "reject anything not PBES2" form (drop the + * `isSupportedLegacyPbe` branches and the imports from this + * module). + * + * # References + * + * - RFC 7292 §4.2: PKCS#12 PBE-with-SHA OIDs. + * - RFC 7292 Appendix B: PKCS#12 v1.0 key derivation function. + * - RFC 7292 Appendix C: `pkcs-12PbeParams` ASN.1 definition. + * - aspnetcore source: src/Shared/CertificateGeneration/ + * MacOSCertificateManager.cs `SaveCertificateCore` + * (`certificate.Export(X509ContentType.Pfx)` with no password). + * - dotnet/runtime behavior: managed Pkcs12 writer's no-password + * default on Unix is 3DES-with-SHA1, 2000 iterations. + */ + +import { createDecipheriv, createHash } from "crypto"; +import type * as asn1js from "asn1js"; + +/** + * The one legacy PBE OID this module decodes. Every other legacy + * PKCS#12 PBE OID stays in `REJECTED_LEGACY_PBE_NAMES` in pfx.ts. + */ +export const SUPPORTED_LEGACY_PBE_OID = "1.2.840.113549.1.12.1.3"; + +/** Type-guard: is `oid` the one legacy OID we support? */ +export function isSupportedLegacyPbe(oid: string): boolean { + return oid === SUPPORTED_LEGACY_PBE_OID; +} + +export interface LegacyPbeParams { + /** Salt bytes from the pkcs-12PbeParams ASN.1 sequence. */ + salt: Buffer; + /** Iteration count from the pkcs-12PbeParams ASN.1 sequence. */ + iterations: number; +} + +/** + * Parse the AlgorithmIdentifier.parameters field as `pkcs-12PbeParams` + * (RFC 7292 Appendix C): + * + * pkcs-12PbeParams ::= SEQUENCE { + * salt OCTET STRING, + * iterations INTEGER + * } + * + * The same format is shared by all six legacy OIDs in the + * `1.2.840.113549.1.12.1.*` family. + */ +export function parseLegacyPbeParams( + algorithmParameters: unknown +): LegacyPbeParams { + const seq = algorithmParameters as asn1js.Sequence | undefined; + const items = seq?.valueBlock?.value; + if (!items || items.length < 2) { + throw new Error( + "Malformed pkcs-12PbeParams: expected SEQUENCE { salt, iterations }." + ); + } + const saltAsn1 = items[0] as asn1js.OctetString; + const iterAsn1 = items[1] as asn1js.Integer; + return { + salt: Buffer.from(saltAsn1.valueBlock.valueHex), + iterations: iterAsn1.valueBlock.valueDec, + }; +} + +/** + * Decrypt a body protected by `pbeWithSHA1And3-KeyTripleDES-CBC`. + * + * 1. Derive the 24-byte 3DES key via PKCS#12 v1.0 KDF (diversifier 1). + * 2. Derive the 8-byte IV via PKCS#12 v1.0 KDF (diversifier 2). + * 3. Decrypt with `des-ede3-cbc` (Node's built-in crypto). + * + * The PKCS#1 padding the cipher leaves on the plaintext is stripped by + * Node's `decipher.final()`. + */ +export function decryptLegacyPbe( + oid: string, + params: LegacyPbeParams, + ciphertext: Buffer, + password: string +): Buffer { + if (oid !== SUPPORTED_LEGACY_PBE_OID) { + throw new Error( + `decryptLegacyPbe called with unsupported OID ${oid}. ` + + `Call sites should check isSupportedLegacyPbe(oid) first.` + ); + } + const key = pkcs12Kdf(password, params.salt, params.iterations, 1, 24); + const iv = pkcs12Kdf(password, params.salt, params.iterations, 2, 8); + const decipher = createDecipheriv("des-ede3-cbc", key, iv); + return Buffer.concat([decipher.update(ciphertext), decipher.final()]); +} + +/** + * PKCS#12 v1.0 key derivation function (RFC 7292 Appendix B). NOT + * PBKDF2 — this is the legacy SHA-1-based diversifier KDF specifically + * defined for PKCS#12. Used here for both encryption key (diversifier + * 1) and IV (diversifier 2) derivation against the same salt and + * iteration count. + * + * Empty-password handling: we treat the password as UTF-16BE plus a + * trailing 16-bit null terminator UNCONDITIONALLY — including for the + * empty string, where `P` is the 2-byte `00 00` null terminator. The + * RFC's literal wording says "empty password → empty P", but every + * implementation that matters in practice (OpenSSL, Bouncy Castle, + * dotnet/runtime's managed PKCS#12 writer) appends the null terminator + * regardless. The aspnetcore disk cache we're trying to read was + * produced under the always-include-terminator convention; following + * the RFC literally would make us fail to decrypt our own target. + * + * Exported for testing only — call sites should use `decryptLegacyPbe`. + */ +export function pkcs12Kdf( + password: string, + salt: Buffer, + iterations: number, + diversifier: 1 | 2 | 3, + outputLength: number +): Buffer { + const u = 20; // SHA-1 output size (bytes) + const v = 64; // SHA-1 input block size (bytes) + + // D: v bytes, each equal to the diversifier byte. + const D = Buffer.alloc(v, diversifier); + + // S: salt repeated to a multiple of v bytes (empty if salt is empty). + const S = repeatToMultiple(salt, v); + + // P: password as UTF-16BE with a trailing null character, repeated to + // a multiple of v bytes. We append the terminator unconditionally — + // see the docstring for the empty-string rationale. + const pwBytes = utf16BeWithNul(password); + const P = repeatToMultiple(pwBytes, v); + + // I = S || P. + let I = Buffer.concat([S, P]); + + // Number of u-byte rounds needed to fill the output. + const rounds = Math.ceil(outputLength / u); + const out = Buffer.alloc(outputLength); + + for (let i = 1; i <= rounds; i++) { + // A_i = H^c(D || I), with H = SHA-1 and c = iterations. + let A = createHash("sha1").update(D).update(I).digest(); + for (let k = 1; k < iterations; k++) { + A = createHash("sha1").update(A).digest(); + } + + // Copy A_i into the output (truncated on the last round if needed). + const offset = (i - 1) * u; + const take = Math.min(u, outputLength - offset); + A.copy(out, offset, 0, take); + + if (i < rounds) { + // B = A repeated to v bytes. + const B = Buffer.alloc(v); + for (let k = 0; k < v; k++) B[k] = A[k % u]; + + // For each v-byte block I_l of I, set I_l = (I_l + B + 1) mod 2^(8v). + // Big-endian arithmetic with carry propagation. + const blockCount = I.length / v; + const newI = Buffer.alloc(I.length); + for (let l = 0; l < blockCount; l++) { + let carry = 1; // the +1 in (I_l + B + 1) + for (let m = v - 1; m >= 0; m--) { + const sum = I[l * v + m] + B[m] + carry; + newI[l * v + m] = sum & 0xff; + carry = sum >>> 8; + } + // Overflow above 2^(8v) is dropped per the mod 2^(8v) in the spec. + } + I = newI; + } + } + + return out; +} + +/** + * Encode a string as UTF-16BE plus a trailing 16-bit null character — + * the password representation PKCS#12 v1.0 KDF expects. Surrogate pairs + * are preserved (the `String.prototype.charCodeAt` iteration produces + * the UTF-16 code units directly). + */ +function utf16BeWithNul(s: string): Buffer { + const buf = Buffer.alloc((s.length + 1) * 2); + for (let i = 0; i < s.length; i++) { + buf.writeUInt16BE(s.charCodeAt(i), i * 2); + } + buf.writeUInt16BE(0, s.length * 2); + return buf; +} + +/** + * Repeat `src` (cycling) into a buffer whose length is the smallest + * multiple of `block` >= src.length. An empty `src` yields an empty + * result (matches the RFC 7292 note for empty salt / password). + */ +function repeatToMultiple(src: Buffer, block: number): Buffer { + if (src.length === 0) return Buffer.alloc(0); + const len = block * Math.ceil(src.length / block); + const out = Buffer.alloc(len); + for (let i = 0; i < len; i++) out[i] = src[i % src.length]; + return out; +} diff --git a/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts new file mode 100644 index 0000000..d5f8d81 --- /dev/null +++ b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; +import * as fs from "fs"; +import * as path from "path"; +import { loadPfx } from "@devcontainer-dev-certs/shared"; +import { + SUPPORTED_LEGACY_PBE_OID, + decryptLegacyPbe, + isSupportedLegacyPbe, + pkcs12Kdf, +} from "@devcontainer-dev-certs/shared/src/cert/pkcs12LegacyPbe"; + +/** + * Lifecycle: delete this file when the parent module is removed. See + * `src/shared/src/cert/pkcs12LegacyPbe.ts` for the removal checklist. + */ + +// Pre-generated 3DES-encrypted PKCS#12 fixture. Built via: +// openssl req -newkey rsa:2048 -nodes -keyout key -x509 \ +// -days 30 -subj /CN=localhost -out crt +// openssl pkcs12 -legacy -export -inkey key -in crt -passout pass: \ +// -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 \ +// -iter 2000 -out test/fixtures/pkcs12-legacy-3des.pfx +// +// `openssl pkcs12 -info` reports the same algorithm aspnetcore's +// MacOSCertificateManager.SaveCertificateCore writes (`pbeWithSHA1And3- +// KeyTripleDES-CBC`), so loading this fixture exercises the same path +// the macOS dev cert disk cache flows through. +const FIXTURE = path.resolve( + __dirname, + "../../../test/fixtures/pkcs12-legacy-3des.pfx" +); + +describe("pkcs12LegacyPbe", () => { + it("SUPPORTED_LEGACY_PBE_OID names exactly the 3DES algorithm dotnet emits", () => { + expect(SUPPORTED_LEGACY_PBE_OID).toBe("1.2.840.113549.1.12.1.3"); + }); + + it("isSupportedLegacyPbe accepts the 3DES OID and rejects the others in the legacy family", () => { + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.3")).toBe(true); + // Rest of the legacy PBE family stays rejected at the parsePfx layer. + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.1")).toBe(false); + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.2")).toBe(false); + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.4")).toBe(false); + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.5")).toBe(false); + expect(isSupportedLegacyPbe("1.2.840.113549.1.12.1.6")).toBe(false); + // PBES2 is not handled here — it goes through pkijs. + expect(isSupportedLegacyPbe("1.2.840.113549.1.5.13")).toBe(false); + }); + + it("decryptLegacyPbe refuses OIDs it doesn't support (defensive)", () => { + expect(() => + decryptLegacyPbe( + "1.2.840.113549.1.12.1.1", + { salt: Buffer.alloc(8), iterations: 1 }, + Buffer.alloc(0), + "" + ) + ).toThrow(/unsupported OID/); + }); +}); + +describe("pkcs12Kdf — empty-password convention", () => { + // For empty password, the bytes fed into I are the UTF-16BE null + // terminator: two zero bytes. Two distinct diversifiers + same + // salt/iterations must produce distinct outputs (sanity: the + // diversifier is what makes the key and IV derivations different). + it("derives distinct key and IV from the same salt with empty password", () => { + const salt = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + const key = pkcs12Kdf("", salt, 2000, 1, 24); + const iv = pkcs12Kdf("", salt, 2000, 2, 8); + expect(key.length).toBe(24); + expect(iv.length).toBe(8); + // 64 bits of overlap shouldn't be zero — vanishingly unlikely + // unless the diversifier byte was ignored. + expect(key.slice(0, 8).equals(iv)).toBe(false); + }); + + it("produces deterministic output for the same inputs", () => { + const salt = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + const a = pkcs12Kdf("", salt, 2000, 1, 24); + const b = pkcs12Kdf("", salt, 2000, 1, 24); + expect(a.equals(b)).toBe(true); + }); + + it("changes output when the iteration count changes", () => { + const salt = Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]); + const a = pkcs12Kdf("", salt, 2000, 1, 24); + const b = pkcs12Kdf("", salt, 1, 1, 24); + expect(a.equals(b)).toBe(false); + }); + + it("handles non-empty passwords (UTF-16BE encoding) without throwing", () => { + // We don't have a published RFC 7292 test vector with the + // always-null-terminator convention to assert exact bytes, but + // the function must accept a non-empty input without error and + // produce a different output than the empty-password case. + const salt = Buffer.from([0xaa, 0xbb, 0xcc, 0xdd]); + const emptyPw = pkcs12Kdf("", salt, 100, 1, 24); + const realPw = pkcs12Kdf("password", salt, 100, 1, 24); + expect(emptyPw.equals(realPw)).toBe(false); + }); +}); + +describe("parsePfx — legacy 3DES PFX round-trip", () => { + it("loads the empty-password 3DES fixture and recovers cert + key", async () => { + expect(fs.existsSync(FIXTURE)).toBe(true); + const loaded = await loadPfx(FIXTURE); + expect(loaded.thumbprint).toMatch(/^[0-9A-F]{40}$/); + expect(loaded.key).not.toBeNull(); + // Cert subject CN was set to localhost when the fixture was built. + expect(loaded.cert.subjectCN).toBe("localhost"); + }); + + it("reports `hasKey: true` so consumers don't treat it as a CA-only PFX", async () => { + const loaded = await loadPfx(FIXTURE); + expect(loaded.key).not.toBeNull(); + }); +}); diff --git a/test/fixtures/pkcs12-legacy-3des.pfx b/test/fixtures/pkcs12-legacy-3des.pfx new file mode 100644 index 0000000000000000000000000000000000000000..86518c88809014ec8c2209b6722bbf44723b8534 GIT binary patch literal 2381 zcmV-T39|Muf(c0i0Ru3C2@eJdDuzgg_YDCD0ic2j00e>v{4jzD_%MP4uLcP!hDe6@ z4FLxRpn?OSFoFY|0s#Opf&-NX2`Yw2hW8Bt2LUh~1_~;MNQUPlyJ4fP2rpN?>W|}Uoy_-C({`|`B7b08GuFZ>UoRxnyQ z`6oXS4b1|H;8U4pq_;{sCk@`9jX!YOaajk0d%uZQwE*Wxp=_-%z>soqb%F~$#EYdA z^_%kS;=;g1RuUYrLUVMG+DA~0=2CD7g6M7ZM{eEP1z;1VZkk;f!m-Tz6sK;Hiyu=b zOWg|D`J(NBjYQGjMQK>cQJz96T8ycyJ>}snFF(j{nXzqlv4cyYO<-hoN@iGAO{lhq zA{N7){!us@OsrR5G*@aX|G81TU9v9WVi3Hyvo+8CeB*@Baw`|YvpU7fGt40wkj{X^kEuXM5hoJaR zW7pmXeyV^E1&bKCB>s~8b}OBUHJQ+0dtfw_wah|Ze>6|-HFX9!k>DZjBp*`$dZYV% z#Hph5wY5b9O1;7Si$TY_tx^yKygO{5P}URv-3((>nwj9es*GCNueze`Ybddo6>3__P0Do?TyzXJZO_;bT8HcSpzHkxuut2P)zWbB>mf^@9UDQ z%#qXNU?&zX5f`rD4@`L`A zAPKV2SW5o^@ILkz^e*~$J(WA(gmpa>@^E%!P5)Tgph#f5>7XG=6-UKhT^dJKFJ+pg zpV$$ZHt@kooDYL@yhIKTfQq*0YNR$hZW6%V+u@&vSwvwSTBmNd*j@d}R7~#wp8&LNQU0;_nT?<@;*=Ftg#jL$qgNjouh2ZxOy`ZCqr|4LI?-tS!#?R zP8E+|IIENZ8-p+`hYPCWKpkr9pGMnS!XF; zm?qMR2XQ^$jq+G?)Uc}Dxs?AHfK=iO?&wl!ov==Cd15!$${d$9u}2JoJXszSGr}%W z;zVbTL3^Hj;b1hZk8L-B6G<))?b7Y!od_k zRCLbzN2(?RkkenoJHPP7KH>7HHFW;B(XMqe({^&8^CPND$p05jKC-Ea=N;Nc>VQFO z1Ey*Ije-qnwi;wIw+GDFcc|@=0Q_Cf67=8aWr+ZSE#+#22s8-OYdTKJqZfYb5SyOE zT3?3#?tkQI$qz4_{u~JLA%ZATq2h1+6^^&{^Is?mQQoPC3#B)))Fk{)wkTTt3zk_N zB#uwFn??pVjybq`xk>){+i{*gYw}Es3xe3AHLd6ypN+MeaInJNu6b74-(f%+KmaUw zLL{YA`7R+a+*;Ipx8?V$N>I&f1R76N92vFXkMR}J#w#LF|DRL^@Il-b@Mvi7tg}?V zB0n&?G2nTwfni~uheguP(l9kK$Dvx%lWd&kN7#$hpPpo6E|YB;ml#i)vciYI%9atB zUBNXdtaKTB*y=xVb?!U0^}tbAF9oAj@<%F^OsRw7U9lhqwTt``McpI-L-ata68Cgf zjgua^X}g4*f4^5M3NhQ_S~o)O3vn|L`*>_kp7D={6q#P_NRwSfYPO>sUWOIwq#lGnf@)=?Jd7bN=*AG-meeWgS`%Q9S-Q+%~ zF<8;XatZtWBPx$>N|mfwkNlm0@(7r`9!P_v-^s)5U_m6%tY_f^dAf4nLN^pnfL=%M z>2is%fbq90gcWf>>O_Df9#FJ+94PN#Z41-b@`+}qfiE{)@D7J;p66H6QSc7vHgyMc zmio~$5q4bVt%ONeWZ_Z7utKwUqI!2qbIk#$NM|+dqCc?CyUpbEA8He!CIPhAB7FMe;T^nMpNE;1;$!RccB!%Z z%kNWej{Lsx@l`P;Fe3&DDuzgg_YDCF6)_eB6u4E|jmy9kT_r*O0EN{AOLM~_;V>~U zAutIB1uG5%0vZJX1Qa&q?&*)4hbRQ;d9$R_OfNz`Jm>@nb_V}rr$5Vl0s;rn6KPmq literal 0 HcmV?d00001 From d40d0b73860aa24a73e8ba7cb4931d7c25a6816f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 21:02:54 +0000 Subject: [PATCH 32/41] Try both empty-password conventions in legacy 3DES decrypt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI macOS run on `09115d8` got past the OID-rejection layer but failed inside the cipher: `error:1C800064:Provider routines::bad decrypt`. PKCS#7 padding check failure means the derived 3DES key didn't match what aspnetcore used to encrypt. The only variable: empty-password encoding. Our local fixture (openssl-generated) decrypts cleanly because openssl encodes empty-password as the UTF-16BE null terminator (`\x00\x00`). dotnet/runtime's managed PKCS#12 writer evidently uses the RFC-literal interpretation — empty password → empty bytes. Both interpretations are valid; the spec is famously ambiguous on this point and implementations have disagreed for two decades. Fix is contained in the existing legacy module: - `pkcs12Kdf` gains an `includeNullTerminator` parameter (default `true`). For non-empty passwords every implementation agrees, so the flag is only meaningful for the empty case. - `decryptLegacyPbe` iterates the two conventions for empty passwords: openssl-style first (matches our test fixture and most legacy PFXes in the wild), then RFC-literal (matches aspnetcore's macOS disk cache). Non-empty passwords still use the single canonical encoding. - PKCS#7 padding failure under both conventions propagates the last error outward — the password's wrong, the file's corrupt, or aspnetcore changed its export convention again. Two new tests pin the flag's behavior: - empty-password output differs between the two conventions (otherwise the fallback in `decryptLegacyPbe` would be a no-op) - non-empty-password output also differs between the two — confirms we're not silently always-including the terminator Local fixture round-trip still passes; this commit doesn't change the OpenSSL path's behavior, only adds the .NET path as a fallback. --- src/shared/src/cert/pkcs12LegacyPbe.ts | 123 ++++++++++++++---- .../tests/pkcs12LegacyPbe.test.ts | 25 ++++ 2 files changed, 122 insertions(+), 26 deletions(-) diff --git a/src/shared/src/cert/pkcs12LegacyPbe.ts b/src/shared/src/cert/pkcs12LegacyPbe.ts index a18ba09..0a3d51f 100644 --- a/src/shared/src/cert/pkcs12LegacyPbe.ts +++ b/src/shared/src/cert/pkcs12LegacyPbe.ts @@ -123,8 +123,27 @@ export function parseLegacyPbeParams( * 2. Derive the 8-byte IV via PKCS#12 v1.0 KDF (diversifier 2). * 3. Decrypt with `des-ede3-cbc` (Node's built-in crypto). * - * The PKCS#1 padding the cipher leaves on the plaintext is stripped by - * Node's `decipher.final()`. + * For an EMPTY password we try two derivation conventions and return + * whichever decrypts cleanly: + * + * - "with null terminator" — encode `""` as the 2-byte UTF-16BE null + * terminator and derive from that. OpenSSL, Bouncy Castle, and our + * openssl-generated test fixture all use this convention. + * - "literal empty bytes" — RFC 7292's literal wording ("if password + * is the empty string, then so is P"). dotnet/runtime's managed + * PKCS#12 writer takes this branch; its on-disk macOS dev-cert + * cache decrypts ONLY under this convention. Confirmed empirically + * on .NET 10.0.301 — CI hit "bad decrypt" with the OpenSSL + * convention applied uniformly. + * + * For non-empty passwords there's no ambiguity: implementations agree + * the password is UTF-16BE bytes plus a trailing null terminator, and + * only that path is tried. + * + * PKCS#7 padding the cipher leaves on the plaintext is stripped by + * Node's `decipher.final()`. A `final()` throw under both conventions + * propagates outward — the password supplied is wrong, the file is + * corrupt, or aspnetcore changed its export convention again. */ export function decryptLegacyPbe( oid: string, @@ -138,8 +157,42 @@ export function decryptLegacyPbe( `Call sites should check isSupportedLegacyPbe(oid) first.` ); } - const key = pkcs12Kdf(password, params.salt, params.iterations, 1, 24); - const iv = pkcs12Kdf(password, params.salt, params.iterations, 2, 8); + // Non-empty password: single canonical encoding. + if (password.length > 0) { + return tryDecrypt3DesPbe(params, ciphertext, password, true); + } + // Empty password: try OpenSSL convention first (matches our test + // fixture + most real-world legacy PFXes), fall back to RFC-literal + // (matches dotnet's macOS disk cache). + try { + return tryDecrypt3DesPbe(params, ciphertext, "", true); + } catch { + return tryDecrypt3DesPbe(params, ciphertext, "", false); + } +} + +function tryDecrypt3DesPbe( + params: LegacyPbeParams, + ciphertext: Buffer, + password: string, + includeNullTerminator: boolean +): Buffer { + const key = pkcs12Kdf( + password, + params.salt, + params.iterations, + 1, + 24, + includeNullTerminator + ); + const iv = pkcs12Kdf( + password, + params.salt, + params.iterations, + 2, + 8, + includeNullTerminator + ); const decipher = createDecipheriv("des-ede3-cbc", key, iv); return Buffer.concat([decipher.update(ciphertext), decipher.final()]); } @@ -151,24 +204,21 @@ export function decryptLegacyPbe( * 1) and IV (diversifier 2) derivation against the same salt and * iteration count. * - * Empty-password handling: we treat the password as UTF-16BE plus a - * trailing 16-bit null terminator UNCONDITIONALLY — including for the - * empty string, where `P` is the 2-byte `00 00` null terminator. The - * RFC's literal wording says "empty password → empty P", but every - * implementation that matters in practice (OpenSSL, Bouncy Castle, - * dotnet/runtime's managed PKCS#12 writer) appends the null terminator - * regardless. The aspnetcore disk cache we're trying to read was - * produced under the always-include-terminator convention; following - * the RFC literally would make us fail to decrypt our own target. + * `includeNullTerminator` controls the well-known empty-password + * disagreement (see `decryptLegacyPbe`). For non-empty passwords every + * relevant implementation appends the UTF-16BE null terminator, so the + * flag has no observable effect there. * - * Exported for testing only — call sites should use `decryptLegacyPbe`. + * Exported for testing only — call sites should use `decryptLegacyPbe`, + * which handles the empty-password convention fallback. */ export function pkcs12Kdf( password: string, salt: Buffer, iterations: number, diversifier: 1 | 2 | 3, - outputLength: number + outputLength: number, + includeNullTerminator = true ): Buffer { const u = 20; // SHA-1 output size (bytes) const v = 64; // SHA-1 input block size (bytes) @@ -179,10 +229,20 @@ export function pkcs12Kdf( // S: salt repeated to a multiple of v bytes (empty if salt is empty). const S = repeatToMultiple(salt, v); - // P: password as UTF-16BE with a trailing null character, repeated to - // a multiple of v bytes. We append the terminator unconditionally — - // see the docstring for the empty-string rationale. - const pwBytes = utf16BeWithNul(password); + // P: password as UTF-16BE bytes, optionally with a trailing null + // terminator, repeated to a multiple of v bytes. For non-empty + // passwords both conventions match; for empty, the terminator-or-not + // choice is what callers iterate over to handle .NET vs OpenSSL. + let pwBytes: Buffer; + if (password.length === 0) { + pwBytes = includeNullTerminator + ? Buffer.from([0x00, 0x00]) + : Buffer.alloc(0); + } else { + pwBytes = includeNullTerminator + ? utf16BeWithNul(password) + : utf16Be(password); + } const P = repeatToMultiple(pwBytes, v); // I = S || P. @@ -230,17 +290,28 @@ export function pkcs12Kdf( } /** - * Encode a string as UTF-16BE plus a trailing 16-bit null character — - * the password representation PKCS#12 v1.0 KDF expects. Surrogate pairs - * are preserved (the `String.prototype.charCodeAt` iteration produces - * the UTF-16 code units directly). + * Encode a string as UTF-16BE — no terminator. Surrogate pairs are + * preserved (the `String.prototype.charCodeAt` iteration produces the + * UTF-16 code units directly). */ -function utf16BeWithNul(s: string): Buffer { - const buf = Buffer.alloc((s.length + 1) * 2); +function utf16Be(s: string): Buffer { + const buf = Buffer.alloc(s.length * 2); for (let i = 0; i < s.length; i++) { buf.writeUInt16BE(s.charCodeAt(i), i * 2); } - buf.writeUInt16BE(0, s.length * 2); + return buf; +} + +/** + * Encode a string as UTF-16BE plus a trailing 16-bit null character — + * the password representation PKCS#12 v1.0 KDF expects for non-empty + * strings under every convention we care about. + */ +function utf16BeWithNul(s: string): Buffer { + const body = utf16Be(s); + const buf = Buffer.alloc(body.length + 2); + body.copy(buf, 0); + // The trailing 2 bytes are already zero (Buffer.alloc zero-fills). return buf; } diff --git a/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts index d5f8d81..d40bc40 100644 --- a/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts +++ b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts @@ -99,6 +99,31 @@ describe("pkcs12Kdf — empty-password convention", () => { const realPw = pkcs12Kdf("password", salt, 100, 1, 24); expect(emptyPw.equals(realPw)).toBe(false); }); + + it("produces different output for empty password under the two terminator conventions", () => { + // The whole point of the includeNullTerminator flag: when password + // is empty, the two conventions feed different bytes (\x00\x00 vs + // nothing) into the KDF. Output bytes MUST differ — otherwise the + // .NET-vs-OpenSSL fallback in decryptLegacyPbe is a no-op. + const salt = Buffer.from([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]); + const withNul = pkcs12Kdf("", salt, 2000, 1, 24, true); + const noNul = pkcs12Kdf("", salt, 2000, 1, 24, false); + expect(withNul.equals(noNul)).toBe(false); + }); + + it("ignores includeNullTerminator for non-empty passwords (both conventions agree)", () => { + const salt = Buffer.from([0xab, 0xcd, 0xef, 0x01]); + const withNul = pkcs12Kdf("password", salt, 100, 1, 24, true); + const noNul = pkcs12Kdf("password", salt, 100, 1, 24, false); + // Non-empty passwords always include the terminator under both + // conventions per the spec; only the empty case is contested. + // (This locks in the current behavior so a future "always omit" + // experiment would fail the test loudly.) + expect(withNul.equals(noNul)).toBe(false); + // Note: they differ because withNul appends \x00\x00 and noNul + // doesn't. Both are valid encodings in different traditions; we + // pick null-terminator as the canonical default. + }); }); describe("parsePfx — legacy 3DES PFX round-trip", () => { From 29d32043865b16495a77bfb2da00bf2273957990 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:13:17 +0000 Subject: [PATCH 33/41] Address four Copilot review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 1. pkcs12Kdf: includeNullTerminator now strictly empty-only Reviewer caught a contract violation. The docstring said the flag had "no observable effect for non-empty passwords," but the implementation WAS threading it through the non-empty branch (`utf16BeWithNul` vs `utf16Be`). Either the contract was wrong or the code was — the code was wrong. Real-world PKCS#12 implementations all append the null terminator for non-empty passwords; the ambiguity lives only at the empty case. Fix: non-empty passwords always go through `utf16BeWithNul`; the flag is only consulted for `password.length === 0`. The now-unused `utf16Be` helper is deleted. Docstring clarified to spell out that non-empty passwords IGNORE the flag. # 2. pkcs12Kdf test: invert the non-empty assertion Same reviewer thread. The test "ignores includeNullTerminator for non-empty passwords" asserted the two outputs differ — which directly contradicted the test's stated contract. Under the corrected implementation, both calls produce identical bytes for non-empty passwords; the test now asserts that, and the comment explains the test is a regression guard against accidentally threading the flag through the non-empty branch. # 3. linuxStore.test.ts: correct importOriginal type parameter `vi.mock("@devcontainer-dev-certs/shared/src/paths", …)` was typed as `importOriginal()` — `Shared` is the package barrel, not the `paths` submodule that's actually being mocked. The types lied about what the original module exports, masking any future mistake where the `paths` module's shape changes. Replaced with `importOriginal()` and added a `type * as SharedPaths from "@devcontainer-dev-certs/shared/src/paths"` import. The `Shared` import is no longer used in this file and is dropped. The same pattern in three vscode-workspace-extension test files is NOT affected — those mock the package barrel and correctly type against `typeof Shared`. Only linuxStore had the mismatch. # 4. CLI engines.node: bump to >=20 Reviewer flagged the inconsistency between `engines.node: >=18` and commander@^14's dependency tree. Independent of commander's specific requirements: Node 18 reached end-of-life in April 2025; advertising support for an EOL runtime in a CLI shipped in 2026 is misleading. Node 20 LTS is the lowest currently-supported version. Updated: - `src/cli/package.json` engines.node to `>=20` - `src/cli/esbuild.mjs` target to `node20` (matches the engines declaration so any Node 20+ syntax esbuild emits is honest) - `src/cli/README.md` and `examples/manual-setup/README.md` Node version requirements (calls out Node 18's EOL date in the CLI README so the version bump is grounded) Local CLI tarball still 1.78MB unpacked, no observable change in bundle size from the esbuild target bump. # Sweep 252 UI tests (+1 — non-empty test now asserts equality after the contract correction), 36 CLI tests, 79 workspace tests. Lint + type-check clean. CLI builds with the new node20 target. --- examples/manual-setup/README.md | 2 +- src/cli/README.md | 2 +- src/cli/esbuild.mjs | 2 +- src/cli/package.json | 2 +- src/shared/src/cert/pkcs12LegacyPbe.ts | 38 +++++++------------ .../tests/linuxStore.test.ts | 4 +- .../tests/pkcs12LegacyPbe.test.ts | 13 +++---- 7 files changed, 25 insertions(+), 38 deletions(-) diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md index be0d103..69772ce 100644 --- a/examples/manual-setup/README.md +++ b/examples/manual-setup/README.md @@ -21,7 +21,7 @@ The host-side cert + `bundle.json` can be produced two ways. **The `dcdc` CLI is npm install -g @devcontainer-dev-certs/cli ``` -Node 18 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. +Node 20 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`) and generate everything in one shot: diff --git a/src/cli/README.md b/src/cli/README.md index 48d9c8f..cd4b168 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -22,7 +22,7 @@ npm install -g @devcontainer-dev-certs/cli dcdc --help ``` -Node 18 or newer is required. The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. +Node 20 or newer is required (Node 18 reached end-of-life in April 2025). The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. For a one-off invocation without a global install: diff --git a/src/cli/esbuild.mjs b/src/cli/esbuild.mjs index 7a1b99a..533aca3 100644 --- a/src/cli/esbuild.mjs +++ b/src/cli/esbuild.mjs @@ -13,7 +13,7 @@ await esbuild.build({ external: ["vscode"], format: "cjs", platform: "node", - target: "node18", + target: "node20", sourcemap: !production, minify: production, banner: { diff --git a/src/cli/package.json b/src/cli/package.json index aa35718..fad4a6a 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -35,7 +35,7 @@ "README.md" ], "engines": { - "node": ">=18" + "node": ">=20" }, "publishConfig": { "access": "public", diff --git a/src/shared/src/cert/pkcs12LegacyPbe.ts b/src/shared/src/cert/pkcs12LegacyPbe.ts index 0a3d51f..136658c 100644 --- a/src/shared/src/cert/pkcs12LegacyPbe.ts +++ b/src/shared/src/cert/pkcs12LegacyPbe.ts @@ -207,7 +207,8 @@ function tryDecrypt3DesPbe( * `includeNullTerminator` controls the well-known empty-password * disagreement (see `decryptLegacyPbe`). For non-empty passwords every * relevant implementation appends the UTF-16BE null terminator, so the - * flag has no observable effect there. + * flag is IGNORED for them — the function always uses the + * null-terminated encoding to match real-world interop. * * Exported for testing only — call sites should use `decryptLegacyPbe`, * which handles the empty-password convention fallback. @@ -229,19 +230,18 @@ export function pkcs12Kdf( // S: salt repeated to a multiple of v bytes (empty if salt is empty). const S = repeatToMultiple(salt, v); - // P: password as UTF-16BE bytes, optionally with a trailing null - // terminator, repeated to a multiple of v bytes. For non-empty - // passwords both conventions match; for empty, the terminator-or-not - // choice is what callers iterate over to handle .NET vs OpenSSL. + // P: password as UTF-16BE bytes, repeated to a multiple of v bytes. + // Non-empty passwords always include the trailing null terminator + // (every implementation we care about agrees there); for the empty + // password, the `includeNullTerminator` flag picks between the two + // contested conventions that callers iterate over. let pwBytes: Buffer; if (password.length === 0) { pwBytes = includeNullTerminator ? Buffer.from([0x00, 0x00]) : Buffer.alloc(0); } else { - pwBytes = includeNullTerminator - ? utf16BeWithNul(password) - : utf16Be(password); + pwBytes = utf16BeWithNul(password); } const P = repeatToMultiple(pwBytes, v); @@ -290,28 +290,18 @@ export function pkcs12Kdf( } /** - * Encode a string as UTF-16BE — no terminator. Surrogate pairs are + * Encode a string as UTF-16BE plus a trailing 16-bit null character — + * the password representation PKCS#12 v1.0 KDF expects for non-empty + * strings under every convention we care about. Surrogate pairs are * preserved (the `String.prototype.charCodeAt` iteration produces the * UTF-16 code units directly). */ -function utf16Be(s: string): Buffer { - const buf = Buffer.alloc(s.length * 2); +function utf16BeWithNul(s: string): Buffer { + const buf = Buffer.alloc((s.length + 1) * 2); for (let i = 0; i < s.length; i++) { buf.writeUInt16BE(s.charCodeAt(i), i * 2); } - return buf; -} - -/** - * Encode a string as UTF-16BE plus a trailing 16-bit null character — - * the password representation PKCS#12 v1.0 KDF expects for non-empty - * strings under every convention we care about. - */ -function utf16BeWithNul(s: string): Buffer { - const body = utf16Be(s); - const buf = Buffer.alloc(body.length + 2); - body.copy(buf, 0); - // The trailing 2 bytes are already zero (Buffer.alloc zero-fills). + // Trailing 2 bytes are already zero (Buffer.alloc zero-fills). return buf; } diff --git a/src/vscode-ui-extension/tests/linuxStore.test.ts b/src/vscode-ui-extension/tests/linuxStore.test.ts index a43c8c7..5ccdd76 100644 --- a/src/vscode-ui-extension/tests/linuxStore.test.ts +++ b/src/vscode-ui-extension/tests/linuxStore.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; -import type * as Shared from "@devcontainer-dev-certs/shared"; +import type * as SharedPaths from "@devcontainer-dev-certs/shared/src/paths"; import { initLogger } from "@devcontainer-dev-certs/shared/src/loggerVscode"; import { generateCertificate } from "../src/cert/generator"; import { VALIDITY_DAYS } from "../src/cert/properties"; @@ -41,7 +41,7 @@ let testRootStoreDir: string; let testTrustDir: string; vi.mock("@devcontainer-dev-certs/shared/src/paths", async (importOriginal) => { - const original = await importOriginal(); + const original = await importOriginal(); return { ...original, getDotNetStorePath: () => testStoreDir, diff --git a/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts index d40bc40..73574c8 100644 --- a/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts +++ b/src/vscode-ui-extension/tests/pkcs12LegacyPbe.test.ts @@ -115,14 +115,11 @@ describe("pkcs12Kdf — empty-password convention", () => { const salt = Buffer.from([0xab, 0xcd, 0xef, 0x01]); const withNul = pkcs12Kdf("password", salt, 100, 1, 24, true); const noNul = pkcs12Kdf("password", salt, 100, 1, 24, false); - // Non-empty passwords always include the terminator under both - // conventions per the spec; only the empty case is contested. - // (This locks in the current behavior so a future "always omit" - // experiment would fail the test loudly.) - expect(withNul.equals(noNul)).toBe(false); - // Note: they differ because withNul appends \x00\x00 and noNul - // doesn't. Both are valid encodings in different traditions; we - // pick null-terminator as the canonical default. + // Non-empty passwords always include the terminator under every + // real-world PKCS#12 implementation; the flag is honored only for + // empty passwords. If a future refactor accidentally threads the + // flag through the non-empty branch, this test catches it. + expect(withNul.equals(noNul)).toBe(true); }); }); From a6747fea3105421c4aa9733c9ab93e2fe7b682f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:16:55 +0000 Subject: [PATCH 34/41] Bump Node floor from 20 to 22 (Node 20 went EOL April 2026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous bump in 29d3204 traded one EOL runtime for another — Node 20 reached end-of-life two months ago. Node 22 is the lowest LTS still receiving security updates, and it's also the runtime VS Code currently bundles (22.22.1 as of VS Code 1.121.0), which makes it a natural floor for everything in this repo. Updated: - `src/cli/package.json` engines.node: ">=22" - `src/cli/esbuild.mjs` target: "node22" - `src/cli/README.md` calls out Node 20's EOL date AND the VS Code runtime alignment, so the floor is grounded in two reasons rather than one - `examples/manual-setup/README.md` updated to match No code changes; bundle size unchanged at 1.78 MB unpacked. --- examples/manual-setup/README.md | 2 +- src/cli/README.md | 2 +- src/cli/esbuild.mjs | 2 +- src/cli/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md index 69772ce..628f93e 100644 --- a/examples/manual-setup/README.md +++ b/examples/manual-setup/README.md @@ -21,7 +21,7 @@ The host-side cert + `bundle.json` can be produced two ways. **The `dcdc` CLI is npm install -g @devcontainer-dev-certs/cli ``` -Node 20 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. +Node 22 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`) and generate everything in one shot: diff --git a/src/cli/README.md b/src/cli/README.md index cd4b168..268ef63 100644 --- a/src/cli/README.md +++ b/src/cli/README.md @@ -22,7 +22,7 @@ npm install -g @devcontainer-dev-certs/cli dcdc --help ``` -Node 20 or newer is required (Node 18 reached end-of-life in April 2025). The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. +Node 22 or newer is required (Node 20 reached end-of-life in April 2026; Node 22 is the lowest LTS still receiving security updates and is the version VS Code currently bundles). The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. For a one-off invocation without a global install: diff --git a/src/cli/esbuild.mjs b/src/cli/esbuild.mjs index 533aca3..5401d60 100644 --- a/src/cli/esbuild.mjs +++ b/src/cli/esbuild.mjs @@ -13,7 +13,7 @@ await esbuild.build({ external: ["vscode"], format: "cjs", platform: "node", - target: "node20", + target: "node22", sourcemap: !production, minify: production, banner: { diff --git a/src/cli/package.json b/src/cli/package.json index fad4a6a..da23b6e 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -35,7 +35,7 @@ "README.md" ], "engines": { - "node": ">=20" + "node": ">=22" }, "publishConfig": { "access": "public", From 7b92c6bfc4e7df16521aca638a0a49d8634e3cca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:30:40 +0000 Subject: [PATCH 35/41] Bump CLI version to 1.3.2-pre to match the rest of the repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main's `bump-version.yml` ran for the 1.3.1 release and pushed every manifest to 1.3.2-pre — but the CLI package didn't exist on main yet, so it was skipped. This branch added `src/cli/package.json` to the bump list going forward, but the catch-up for the existing skew has to happen by hand. If a release tag landed before this fix, `validate-release` in release-feature.yml (which this branch wired to also check `src/cli/package.json`) would fail with a clear "version 1.3.1-pre does not match release tag 1.3.x" — caught at the gate, but only after kicking off a release. Bump applied via `npm version 1.3.2-pre --no-git-tag-version` so the shape matches what the bump workflow produces; package-lock.json re-synced. No code changes. --- package-lock.json | 4 ++-- src/cli/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8d9a758..1d1b5a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7069,7 +7069,7 @@ }, "src/cli": { "name": "@devcontainer-dev-certs/cli", - "version": "1.3.1-pre", + "version": "1.3.2-pre", "license": "MIT", "bin": { "dcdc": "dist/dcdc.js" @@ -7087,7 +7087,7 @@ "vitest": "^4.1.5" }, "engines": { - "node": ">=18" + "node": ">=22" } }, "src/cli/node_modules/commander": { diff --git a/src/cli/package.json b/src/cli/package.json index da23b6e..28edad6 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -1,6 +1,6 @@ { "name": "@devcontainer-dev-certs/cli", - "version": "1.3.1-pre", + "version": "1.3.2-pre", "description": "dcdc — host-side CLI for generating, inspecting, and trusting ASP.NET-compatible dev certs for use with dev containers, outside of VS Code.", "license": "MIT", "author": "Daniel Negstad", From 66eb59123fd848c7b5ea70eee23c887dbcc2623f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:54:28 +0000 Subject: [PATCH 36/41] Bump all components to 1.4.0-pre.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns every manifest validate-release watches to the same version in preparation for the first coordinated release that includes the CLI: - src/shared/package.json - src/cli/package.json - src/vscode-ui-extension/package.json - src/vscode-workspace-extension/package.json - src/devcontainer-feature/src/devcontainer-dev-certs/ devcontainer-feature.json The `.1` suffix on the prerelease tag is intentional. After the RELEASING.md bootstrap procedure (stub publish + trusted-publisher config) is run for `@devcontainer-dev-certs/cli`, this commit's version is a publishable prerelease — `npm publish --tag next` will land 1.4.0-pre.1 on npm so we can exercise the OIDC publish path end-to-end before cutting the real 1.4.0 release. Subsequent prerelease spins can iterate the trailing integer (`-pre.2`, `-pre.3`, …) without colliding. Bumped via `jq --arg v 1.4.0-pre.1 '.version = $v'` for each manifest (matches the shape `bump-version.yml` produces) + `npm install --package-lock-only` to resync the lockfile. No code changes; 252 UI / 79 workspace / 36 CLI tests green; lint clean. --- package-lock.json | 8 ++++---- src/cli/package.json | 2 +- .../src/devcontainer-dev-certs/devcontainer-feature.json | 2 +- src/shared/package.json | 2 +- src/vscode-ui-extension/package.json | 2 +- src/vscode-workspace-extension/package.json | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d1b5a4..31cabef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7069,7 +7069,7 @@ }, "src/cli": { "name": "@devcontainer-dev-certs/cli", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "license": "MIT", "bin": { "dcdc": "dist/dcdc.js" @@ -7102,7 +7102,7 @@ }, "src/shared": { "name": "@devcontainer-dev-certs/shared", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "dependencies": { "@peculiar/x509": "^2.0.0", "asn1js": "^3.0.10", @@ -7115,7 +7115,7 @@ }, "src/vscode-ui-extension": { "name": "devcontainer-dev-certs-host", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "license": "MIT", "dependencies": { "@devcontainer-dev-certs/shared": "*", @@ -7138,7 +7138,7 @@ }, "src/vscode-workspace-extension": { "name": "devcontainer-dev-certs-remote", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "license": "MIT", "dependencies": { "@devcontainer-dev-certs/shared": "*", diff --git a/src/cli/package.json b/src/cli/package.json index 28edad6..4a3bb79 100644 --- a/src/cli/package.json +++ b/src/cli/package.json @@ -1,6 +1,6 @@ { "name": "@devcontainer-dev-certs/cli", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "description": "dcdc — host-side CLI for generating, inspecting, and trusting ASP.NET-compatible dev certs for use with dev containers, outside of VS Code.", "license": "MIT", "author": "Daniel Negstad", diff --git a/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json b/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json index 7629409..64cd410 100644 --- a/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json +++ b/src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "devcontainer-dev-certs", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "name": "Dev Container Development Certificates", "description": "Enables trusted HTTPS in Dev Containers by preparing certificate trust infrastructure and installing companion VS Code extensions that automatically generate, trust, and transfer ASP.NET and Aspire compatible development certificates from the host machine. Add to your devcontainer.json: \"features\": { \"ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs:1\": {} }", "documentationURL": "https://github.com/dnegstad/devcontainer-dev-certs", diff --git a/src/shared/package.json b/src/shared/package.json index c7a3c93..5a08352 100644 --- a/src/shared/package.json +++ b/src/shared/package.json @@ -1,6 +1,6 @@ { "name": "@devcontainer-dev-certs/shared", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "private": true, "main": "./src/index.ts", "types": "./src/index.ts", diff --git a/src/vscode-ui-extension/package.json b/src/vscode-ui-extension/package.json index 253463f..0ba6ee5 100644 --- a/src/vscode-ui-extension/package.json +++ b/src/vscode-ui-extension/package.json @@ -3,7 +3,7 @@ "displayName": "Dev Container Dev Certificates (Host)", "description": "Generates and trusts ASP.NET and Aspire compatible HTTPS development certificates on the host machine for use in Dev Containers and remote environments.", "icon": "images/dn_ui_extension_icon_256.png", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "publisher": "dnegstad", "license": "MIT", "repository": { diff --git a/src/vscode-workspace-extension/package.json b/src/vscode-workspace-extension/package.json index 5c81333..cd3e1e8 100644 --- a/src/vscode-workspace-extension/package.json +++ b/src/vscode-workspace-extension/package.json @@ -3,7 +3,7 @@ "displayName": "Dev Container Dev Certificates (Remote)", "description": "Receives and installs ASP.NET and Aspire compatible HTTPS development certificates inside Dev Containers and remote environments.", "icon": "images/dn_workspace_extension_icon_256.png", - "version": "1.3.2-pre", + "version": "1.4.0-pre.1", "publisher": "dnegstad", "license": "MIT", "repository": { From d4861f32d602cc442f2d126fc481ef88380410a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 23:59:42 +0000 Subject: [PATCH 37/41] nativeBackend.test.ts: consolidate the two shared imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Style nit from Copilot review — both imports were from `@devcontainer-dev-certs/shared`, no reason to split them across two statements. Matches the convention everywhere else in the test tree (one import line per source module). --- src/vscode-ui-extension/tests/nativeBackend.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vscode-ui-extension/tests/nativeBackend.test.ts b/src/vscode-ui-extension/tests/nativeBackend.test.ts index dd26f92..6da0e97 100644 --- a/src/vscode-ui-extension/tests/nativeBackend.test.ts +++ b/src/vscode-ui-extension/tests/nativeBackend.test.ts @@ -8,8 +8,7 @@ import { import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { NativeBackend } from "@devcontainer-dev-certs/shared"; -import { loadPfx } from "@devcontainer-dev-certs/shared"; +import { NativeBackend, loadPfx } from "@devcontainer-dev-certs/shared"; /** * NativeBackend has two distinct code paths picked by `noTrust`. From 33032baacd3a28d790a4b3f5e40b1f38a6c7b631 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 00:12:18 +0000 Subject: [PATCH 38/41] dcdc generate / --help: clearer reuse + interaction semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two UX gaps surfaced during local testing: # generate output didn't say it reused `dcdc generate` silently reuses an existing trusted dev cert when one's already in the platform store. The output reported the resulting thumbprint and "Trusted on host: yes" — but didn't distinguish "fresh generation" from "we just observed the cert that was already there." Indistinguishable from outside, which made it look like a no-op had side effects. Fix: snapshot the platform store BEFORE the backend runs, compare the pre-existing thumbprint to the result thumbprint afterwards, and add a `Cert source:` line that says one of three things: - "reused (existing trusted cert already in the host platform store)" - "newly generated (added to the host platform store)" - "newly generated (in memory; --no-trust skips platform-store write)" The pre-check is best-effort: if `createPlatformStore` or `checkStatus` throws (corrupt state, permission issue, etc.) we skip the diagnostic and report "newly generated" by default rather than blocking the generate. `--no-trust` skips the pre-check entirely because the native backend bypasses the platform store on that path. # Command descriptions didn't say what each command DOES vs DOESN'T DO Reviewer pointed out the relationships between `generate` / `trust` / `bundle` aren't obvious from `--help`, and they diverge from the familiar `dotnet dev-certs https` convention in ways that look arbitrary. Concrete example: `dcdc trust X` looks superficially like `dotnet dev-certs https --import X --trust`, but the dotnet form imports the cert into the .NET dev cert store AND adds it to OS trust, while dcdc's only does the OS trust step. Fix: rewrote every command's description to say: - what it DOES (action) - what state it touches (side effects) - what it explicitly DOES NOT do, when there's a plausible mis-mapping (`dcdc trust` says it's trust-only, not import) - rough equivalence to a `dotnet dev-certs https …` invocation where one exists Plus a workflow guide + dotnet-mapping table in the program-level help (rendered by `dcdc --help`). That's the document users see when they first run the CLI looking for "what does each of these do." No behavioral change beyond the new `Cert source:` line on generate. All 36 CLI tests green; help text renders cleanly. --- src/cli/src/commands/generate.ts | 51 +++++++++++++++++++++++--- src/cli/src/index.ts | 62 ++++++++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts index 12a1684..84a3817 100644 --- a/src/cli/src/commands/generate.ts +++ b/src/cli/src/commands/generate.ts @@ -1,5 +1,6 @@ import * as path from "path"; import { + createPlatformStore, selectBackend, type BackendMode, } from "@devcontainer-dev-certs/shared"; @@ -18,10 +19,13 @@ export interface GenerateCommandOptions { } /** - * `dcdc generate` — produce a fresh dev cert + bundle.json. Picks a backend - * (native by default, dotnet pass-through on macOS when available, with - * `--backend` to override) and runs the host trust step unless `--no-trust` - * is passed. + * `dcdc generate` — ensure a dev cert exists on the host and emit its + * artifacts + a `bundle.json` for the in-container installer. If the + * host platform store already has a valid trusted dev cert, the backend + * reuses it (no fresh generation, no re-trust prompt) and we say so on + * stderr so the user knows what happened. With `--no-trust`, the native + * backend bypasses the platform store entirely and always produces a + * fresh in-memory cert. */ export async function runGenerate( options: GenerateCommandOptions @@ -36,6 +40,16 @@ export async function runGenerate( process.stderr.write(`Backend: ${backend.kind}\n`); process.stderr.write(`Out dir: ${outDir}\n`); + // Snapshot the platform store BEFORE the backend runs so we can + // tell the user whether the backend reused an existing cert or + // generated a fresh one. Best-effort: if the store check throws + // (corrupt state, permission issue), we skip the diagnostic rather + // than fail the generate. Skipped entirely for `--no-trust` since + // the native backend doesn't touch the store on that path. + const preExistingThumbprint = noTrust + ? null + : await safelyReadStoreThumbprint(); + const result = await backend.generate({ outDir, noTrust, @@ -45,8 +59,13 @@ export async function runGenerate( linuxNssTrustReporter: stderrNssTrustReporter, }); + const reused = + preExistingThumbprint !== null && + preExistingThumbprint === result.thumbprint; + process.stderr.write( - `Thumbprint: ${result.thumbprint}\n` + + `Cert source: ${certSourceLabel(reused, noTrust)}\n` + + `Thumbprint: ${result.thumbprint}\n` + `PFX: ${result.pfxPath}\n` + `PEM: ${result.pemPath}\n` + (result.pemKeyPath ? `Key: ${result.pemKeyPath}\n` : "") + @@ -76,3 +95,25 @@ export async function runGenerate( process.stderr.write(`Bundle: ${bundlePath}\n`); } } + +async function safelyReadStoreThumbprint(): Promise { + try { + const store = await createPlatformStore(); + const status = await store.checkStatus(); + return status.exists ? status.thumbprint : null; + } catch { + // Store reads aren't load-bearing for the generate flow — we're + // only using them to decorate the output. Don't block. + return null; + } +} + +function certSourceLabel(reused: boolean, noTrust: boolean): string { + if (reused) { + return "reused (existing trusted cert already in the host platform store)"; + } + if (noTrust) { + return "newly generated (in memory; --no-trust skips platform-store write)"; + } + return "newly generated (added to the host platform store)"; +} diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts index ac49381..c4bc414 100644 --- a/src/cli/src/index.ts +++ b/src/cli/src/index.ts @@ -13,12 +13,41 @@ program .name("dcdc") .description( "Host-side dev-cert toolkit. Generates, inspects, trusts, and bundles " + - "ASP.NET-compatible HTTPS dev certs for use with dev containers — without VS Code." + "ASP.NET-compatible HTTPS dev certs for use with dev containers — without " + + "VS Code.\n\n" + + "Typical workflows:\n" + + " dcdc generate Ensure the host dev cert exists and is\n" + + " trusted; emit files + bundle.json.\n" + + " dcdc inspect ./cert.pfx Read a cert file (subject, thumbprint,\n" + + " SANs, validity).\n" + + " dcdc bundle ./cert.pfx Wrap an existing cert file in a\n" + + " bundle.json for the in-container installer.\n" + + " dcdc trust ./cert.pem Add an existing cert to the host's OS\n" + + " trust store ONLY (does not import to the\n" + + " .NET dev cert store — see notes below).\n" + + " dcdc doctor Read-only diagnostics.\n\n" + + "Mapping to `dotnet dev-certs https` (for reference):\n" + + " dotnet dev-certs https --trust ≈ dcdc generate\n" + + " dotnet dev-certs https ≈ dcdc generate --no-trust\n" + + " dotnet dev-certs https --check ≈ dcdc doctor\n" + + " dotnet dev-certs https --import F --trust no exact equivalent — `dcdc\n" + + " trust F` does the OS trust\n" + + " step only, NOT the .NET store\n" + + " import. Use the dotnet CLI\n" + + " directly if you need both.\n" + + " dotnet dev-certs https --export-path F ≈ dcdc generate --out-dir \n" + + " --no-trust" ); program .command("generate") - .description("Generate a dev cert, optionally trust it, and emit a bundle.json.") + .description( + "Ensure the host dev cert exists and is trusted, then write its files " + + "(PFX/PEM/key) + bundle.json. Reuses an existing trusted cert in the host " + + "platform store when one is present (no re-prompt, no fresh key generation). " + + "Roughly equivalent to `dotnet dev-certs https --trust` plus our bundle.json " + + "write." + ) .option("-o, --out-dir ", "Directory to write artifacts to (default ~/.dev-certs).") .addOption( new Option("-b, --backend ", "Backend selection.") @@ -56,7 +85,11 @@ program program .command("inspect ") - .description("Print details about a PFX or PEM certificate.") + .description( + "Print details about a PFX or PEM certificate (subject CN, thumbprints, " + + "validity, SANs, dev-cert OID + version byte, warnings). Read-only — " + + "doesn't touch the platform store, doesn't add trust." + ) .option("--json", "Emit machine-readable JSON instead of human-readable text.") .action(async (certPath: string, opts: { json?: boolean }) => { await runInspect(certPath, { json: opts.json }); @@ -64,7 +97,12 @@ program program .command("bundle ") - .description("Emit a bundle.json referencing an already-existing cert file.") + .description( + "Wrap an already-existing cert file in a bundle.json that the in-container " + + "installer reads. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by " + + "naming convention. Doesn't touch the cert or the platform store — only " + + "emits the JSON manifest." + ) .option( "-o, --out-dir ", "Directory to write bundle.json to (default: directory of cert-path)." @@ -110,7 +148,13 @@ program program .command("trust ") - .description("Add an existing PFX or PEM cert to the host's OS trust store.") + .description( + "Add an existing PFX or PEM cert to the host's OS trust store (macOS " + + "keychain / Windows CurrentUser\\Root / Linux OpenSSL trust dir + NSS DBs). " + + "ONLY adds trust — does NOT register the cert as the host .NET dev cert " + + "(that's what `dotnet dev-certs --import` does; if you need both, run the " + + "dotnet CLI). Short-circuits if the cert is already trusted." + ) .option("-v, --verbose", "Stream shared-layer log lines to stderr.") .action(async (certPath: string, opts: { verbose?: boolean }) => { await runTrust(certPath, { verbose: opts.verbose }); @@ -118,7 +162,13 @@ program program .command("doctor") - .description("Read-only diagnostics: backend availability + host trust state.") + .description( + "Read-only diagnostics: which backends are available (and which `--backend " + + "auto` would pick), out-dir / bundle.json presence, host platform-store " + + "cert state (present? trusted? thumbprint?), and per-OS tool presence " + + "(openssl/certutil on Linux, security on macOS, pwsh/powershell + " + + "certutil.exe on Windows). Doesn't modify anything." + ) .option("-o, --out-dir ", "Out-dir to inspect (default ~/.dev-certs).") .option("-v, --verbose", "Stream shared-layer log lines to stderr.") .action(async (opts: { outDir?: string; verbose?: boolean }) => { From 93311f850d6329c719a9939a61a18e04f940e7e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 05:06:03 +0000 Subject: [PATCH 39/41] Drop dcdc CLI from PR scope; keep shared layer + macOS support The dcdc CLI carved an own-the-config surface that conflicts with VS Code's per-user `hostCertGenerator` setting and offers little value without IDE-specific extensions. Drop it from this PR and keep: - The shared workspace extraction and backend abstraction (NativeBackend, DotnetBackend, selectBackend) used by the VS Code extension's `hostCertGenerator` setting. - macOS dotnet dev-certs disk-cache PFX compatibility (3DES legacy PBE decoder in `pkcs12LegacyPbe.ts`). - Platform-store hardening (`resolveSafeExecPath`, Windows pwsh re-probe, macStore untrust-before-delete). - The fallback installer + bundle JSON delivered by the devcontainer feature for non-VS Code use, now framed as minimally supported rather than first-class. The macOS integration test now also pins the on-disk PBE algorithm OID: when aspnetcore eventually switches the macOS writer to PBES2, the assertion fires with an actionable error telling the next maintainer to remove the legacy 3DES handler. Removals: - `src/cli/` workspace (commands, tests, README, esbuild config, tsconfigs, package.json). - CLI build/test/pack steps in `build-extensions.yml`. - `publish-cli` job and CLI tarball download in `release-feature.yml`. - `src/cli/package.json` from the bump and validate lists. - `findSiblingKey` from shared (CLI-only consumer). - "dcdc" references throughout shared comments and the manual-setup example; the example now opens with a "minimally supported" callout and walks through the `dotnet dev-certs` host path explicitly. --- .github/workflows/build-extensions.yml | 43 +- .github/workflows/bump-version.yml | 5 +- .github/workflows/release-feature.yml | 82 +- README.md | 18 +- RELEASING.md | 80 +- eslint.config.mjs | 1 - examples/manual-setup/README.md | 50 +- package-lock.json | 2199 +---------------- package.json | 1 - src/cli/LICENSE | 21 - src/cli/README.md | 176 -- src/cli/esbuild.mjs | 22 - src/cli/package.json | 63 - src/cli/src/bundle/writer.ts | 111 - src/cli/src/commands/bundle.ts | 154 -- src/cli/src/commands/doctor.ts | 243 -- src/cli/src/commands/generate.ts | 119 - src/cli/src/commands/inspect.ts | 174 -- src/cli/src/commands/trust.ts | 59 - src/cli/src/defaults.ts | 21 - src/cli/src/index.ts | 183 -- src/cli/src/logger.ts | 20 - src/cli/src/nssReporter.ts | 26 - src/cli/tests/_helpers.ts | 24 - src/cli/tests/bundle.test.ts | 200 -- src/cli/tests/doctor.test.ts | 306 --- src/cli/tests/generate.test.ts | 104 - src/cli/tests/select.test.ts | 119 - src/cli/tests/writer.test.ts | 142 -- src/cli/tsconfig.json | 19 - src/cli/tsconfig.lint.json | 7 - src/cli/vitest.config.ts | 8 - src/cli/vitest.setup.ts | 4 - src/shared/src/backends/native.ts | 14 +- src/shared/src/backends/select.ts | 18 +- src/shared/src/backends/types.ts | 11 +- src/shared/src/cert/loader.ts | 20 - src/shared/src/cert/pkcs12LegacyPbe.ts | 12 +- src/shared/src/index.ts | 12 +- .../tests/dotnetBackend.test.ts | 16 +- .../dotnetMacosCache.integration.test.ts | 150 +- .../tests/nativeBackend.test.ts | 4 +- 42 files changed, 296 insertions(+), 4765 deletions(-) delete mode 100644 src/cli/LICENSE delete mode 100644 src/cli/README.md delete mode 100644 src/cli/esbuild.mjs delete mode 100644 src/cli/package.json delete mode 100644 src/cli/src/bundle/writer.ts delete mode 100644 src/cli/src/commands/bundle.ts delete mode 100644 src/cli/src/commands/doctor.ts delete mode 100644 src/cli/src/commands/generate.ts delete mode 100644 src/cli/src/commands/inspect.ts delete mode 100644 src/cli/src/commands/trust.ts delete mode 100644 src/cli/src/defaults.ts delete mode 100644 src/cli/src/index.ts delete mode 100644 src/cli/src/logger.ts delete mode 100644 src/cli/src/nssReporter.ts delete mode 100644 src/cli/tests/_helpers.ts delete mode 100644 src/cli/tests/bundle.test.ts delete mode 100644 src/cli/tests/doctor.test.ts delete mode 100644 src/cli/tests/generate.test.ts delete mode 100644 src/cli/tests/select.test.ts delete mode 100644 src/cli/tests/writer.test.ts delete mode 100644 src/cli/tsconfig.json delete mode 100644 src/cli/tsconfig.lint.json delete mode 100644 src/cli/vitest.config.ts delete mode 100644 src/cli/vitest.setup.ts diff --git a/.github/workflows/build-extensions.yml b/.github/workflows/build-extensions.yml index 2fdffee..5e7833d 100644 --- a/.github/workflows/build-extensions.yml +++ b/.github/workflows/build-extensions.yml @@ -8,7 +8,7 @@ on: type: boolean default: false upload-artifacts: - description: Package and upload release artifacts (VSIXes and CLI tarball) for downstream publish jobs. + description: Package and upload release artifacts (VSIXes) for downstream publish jobs. type: boolean default: false @@ -69,30 +69,6 @@ jobs: - name: Package Workspace Extension VSIX run: npm run package -w src/vscode-workspace-extension - - name: Build CLI - # Same workspace, same shared layer — `dcdc` is the host-side - # surface of the same code the extensions ship. Production build - # in release contexts so the published bundle matches what the - # extensions ship. - run: npm run ${{ inputs.production && 'build:prod' || 'build' }} -w src/cli - - - name: Test CLI - run: npm test -w src/cli - - - name: Verify CLI package metadata - # Guardrail: confirms `npm publish` would produce a valid - # tarball (correct `files`, `bin` resolution, no missing - # LICENSE/README) without actually publishing. Catches metadata - # regressions before they hit a real release. - run: npm pack --dry-run -w src/cli - - - name: Package CLI tarball - if: inputs.upload-artifacts - # Produces the actual tarball the publish job will hand to - # `npm publish`. Pinned to `src/cli/dist/` so the upload-artifact - # step has a stable, single-file path. - run: npm pack -w src/cli --pack-destination src/cli/dist - - name: Upload VSIX artifacts if: inputs.upload-artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -100,13 +76,6 @@ jobs: name: vsix path: src/vscode-*-extension/*.vsix - - name: Upload CLI tarball - if: inputs.upload-artifacts - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: cli-tarball - path: src/cli/dist/*.tgz - windows-certificate-validation: name: Windows Certificate Validation runs-on: windows-latest @@ -179,11 +148,13 @@ jobs: security set-keychain-settings "$HOME/Library/Keychains/login.keychain-db" - name: Validate dotnet dev-certs disk cache is loadable - # If this fails, `DotnetBackend.generate` on macOS cannot - # discover the cert dotnet just created — the rework that - # replaced `dotnet --export-path` with platform-store - # discovery depends on `parsePfx` accepting whatever + # If this fails, the dotnet backend on macOS cannot discover + # the cert dotnet just created — platform-store discovery + # depends on `parsePfx` accepting whatever # `certificate.Export(X509ContentType.Pfx)` writes on macOS. + # The same test pins the on-disk PBE algorithm OID so we get + # a loud signal the day aspnetcore switches to PBES2 and the + # legacy 3DES handler in `pkcs12LegacyPbe.ts` can be removed. run: npm test -w src/vscode-ui-extension -- tests/dotnetMacosCache.integration.test.ts feature: diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml index 35afd6b..9343669 100644 --- a/.github/workflows/bump-version.yml +++ b/.github/workflows/bump-version.yml @@ -37,12 +37,11 @@ jobs: run: | NEXT="${{ steps.version.outputs.next }}" - # Extension + CLI package.json files + # Extension package.json files for f in \ src/vscode-ui-extension/package.json \ src/vscode-workspace-extension/package.json \ - src/shared/package.json \ - src/cli/package.json; do + src/shared/package.json; do jq --arg v "$NEXT" '.version = $v' "$f" > "$f.tmp" && mv "$f.tmp" "$f" done diff --git a/.github/workflows/release-feature.yml b/.github/workflows/release-feature.yml index c86306c..19aa1c7 100644 --- a/.github/workflows/release-feature.yml +++ b/.github/workflows/release-feature.yml @@ -39,8 +39,7 @@ jobs: src/devcontainer-feature/src/devcontainer-dev-certs/devcontainer-feature.json \ src/vscode-ui-extension/package.json \ src/vscode-workspace-extension/package.json \ - src/shared/package.json \ - src/cli/package.json; do + src/shared/package.json; do v=$(jq -r '.version' "$f") if [[ "$v" != "$EXPECTED" ]]; then echo "::error file=$f::version '$v' does not match release tag '$EXPECTED'" @@ -162,82 +161,3 @@ jobs: set -euo pipefail mapfile -t ARGS <<< "$FILES" gh release upload "$TAG" "${ARGS[@]}" --repo "${{ github.repository }}" - - publish-cli: - name: Publish CLI to npm - needs: [validate-release, build] - runs-on: ubuntu-latest - environment: - name: release - url: https://www.npmjs.com/package/@devcontainer-dev-certs/cli - permissions: - # `contents: write` so we can attach the tarball to the GitHub Release. - # `id-token: write` is the load-bearing one — both npm OIDC trusted - # publishing AND `actions/attest-build-provenance` consume the OIDC - # token GitHub mints from this permission. Without it, npm publish - # would fall through to anonymous mode (failing) and the attestation - # step would error before producing a sigstore bundle. - contents: write - id-token: write - attestations: write - steps: - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: "22" - # Configures `.npmrc` so `npm publish` targets the public - # registry. No `NODE_AUTH_TOKEN` is needed — the npm CLI - # exchanges the runner's OIDC token for a short-lived publish - # credential via npmjs.com's trusted publisher policy (set up - # one-time at https://www.npmjs.com/settings/devcontainer-dev-certs/packages). - registry-url: https://registry.npmjs.org - - - name: Pin npm to a version that supports trusted publishing - # Node 22 ships npm 10.x; OIDC trusted publishing landed as a - # stable feature in npm 11.5. Upgrade explicitly so we don't - # depend on whatever Node-bundled npm version drifts in next. - run: npm install -g npm@^11.5.0 - - - name: Download CLI tarball - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: cli-tarball - path: cli-tarball - - - name: Resolve tarball path - id: tarball - run: | - set -euo pipefail - mapfile -t FILES < <(find cli-tarball -type f -name '*.tgz' | sort) - if [[ ${#FILES[@]} -ne 1 ]]; then - echo "::error::Expected exactly one CLI tarball, found ${#FILES[@]}" - printf '%s\n' "${FILES[@]}" - exit 1 - fi - echo "path=${FILES[0]}" >> "$GITHUB_OUTPUT" - echo "Resolved CLI tarball: ${FILES[0]}" - - - name: Publish to npm - # `--provenance` + the `publishConfig.provenance: true` in - # package.json + the trusted publisher policy on npmjs.com is - # the SLSA story for the CLI. The published package on npm - # carries a verifiable link back to this workflow run. - run: npm publish "${{ steps.tarball.outputs.path }}" --provenance --access public - - - name: Attest CLI tarball provenance - # Parallel to the VSIX attestation step: stores a sigstore - # bundle on GitHub's attestation store, verifiable with - # `gh attestation verify --repo dnegstad/devcontainer-dev-certs`. - # This is independent of (and complementary to) the npm-side - # provenance the publish step emits. - uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 - with: - subject-path: ${{ steps.tarball.outputs.path }} - - - name: Attach CLI tarball to release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ github.event.release.tag_name }} - FILE: ${{ steps.tarball.outputs.path }} - run: | - set -euo pipefail - gh release upload "$TAG" "$FILE" --repo "${{ github.repository }}" diff --git a/README.md b/README.md index 6e2f494..a887f39 100644 --- a/README.md +++ b/README.md @@ -325,20 +325,12 @@ Pushes from a Dev Container without the matching feature option are ignored — ## Manual / non-VS Code use -The companion-extension pattern is VS Code-specific, but the underlying container-side machinery isn't. JetBrains, Vim, raw CLI, and CI users can drive the same trust state through a small shell tool the feature installs into the container. +> [!NOTE] +> This is a **minimally supported** path. The first-class experience this project ships is the VS Code host + remote extension pair; everything below exists so a user without VS Code isn't left with no escape hatch, but it's hand-wired and there's no host-side automation for cert generation or OS trust. Treat it as a "you're on your own for the host half" workaround, not a primary supported scenario. -For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference. - -### `dcdc` — the host-side CLI +The companion-extension pattern is VS Code-specific, but the underlying container-side machinery isn't. JetBrains, Vim, raw CLI, and CI users can drive the in-container half of the trust state through a small shell tool the feature installs into the container. Host-side cert generation and OS trust remain the user's problem (typically `dotnet dev-certs https --trust`). -[`dcdc`](src/cli/README.md) is the host-side CLI that produces the cert files and `bundle.json` the in-container installer below consumes. One command does generation, host trust, and bundle emission: - -```bash -mkdir -p ~/.dev-certs -dcdc generate --out-dir ~/.dev-certs -``` - -It also exposes `dcdc inspect` (cert details), `dcdc bundle` (wrap an existing cert into a bundle.json), `dcdc trust` (host-trust an existing cert), and `dcdc doctor` (read-only diagnostics). See [`src/cli/README.md`](src/cli/README.md) for the full reference. Doing the steps by hand is documented in [`examples/manual-setup/`](examples/manual-setup/) for situations where the CLI isn't available. +For an end-to-end walkthrough — generating the cert on the host, mounting it in, wiring `postStartCommand` — see [`examples/manual-setup/`](examples/manual-setup/). The summary here is the reference. ### The fallback installer @@ -444,7 +436,7 @@ devcontainer-dev-certs-install --bundle-json /host-dev-certs/bundle.json \ ### What's still VS Code-only -- **Host-side cert generation and trust.** Use `dotnet dev-certs https --trust` (or your platform's equivalent) and export the PFX / PEM into your mounted host directory. The host extension's editor-agnostic generator is on the roadmap. +- **Host-side cert generation and trust.** Use `dotnet dev-certs https --trust` (or your platform's equivalent) and export the PFX / PEM into your mounted host directory. - **`defaultKestrelCertificate`.** Lives in a VS Code setting and is delivered via `EnvironmentVariableCollection`. Set `ASPNETCORE_Kestrel__Certificates__Default__Path`/`__Password` yourself in `devcontainer.json` `containerEnv` if you need this outside VS Code. - **Reverse sync (`syncContainerCert`).** Needs a privileged host-side process with a consent UI; the script can't perform host trust. diff --git a/RELEASING.md b/RELEASING.md index db9c936..f991ac2 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,13 +1,12 @@ # Releasing -This repository ships four components together on a single release trigger: +This repository ships three components together on a single release trigger: - The devcontainer feature (`ghcr.io/dnegstad/devcontainer-dev-certs/devcontainer-dev-certs`) - The host VS Code extension (`dnegstad.devcontainer-dev-certs-host`) - The remote VS Code extension (`dnegstad.devcontainer-dev-certs-remote`) -- The host CLI (`@devcontainer-dev-certs/cli`) -All four share one repo-wide version. A release is one GitHub Release; everything downstream runs from `.github/workflows/release-feature.yml`. +All three share one repo-wide version. A release is one GitHub Release; everything downstream runs from `.github/workflows/release-feature.yml`. ## Normal release procedure @@ -18,81 +17,8 @@ All four share one repo-wide version. A release is one GitHub Release; everythin The workflow: - Validates that the tag matches every component's `package.json` / `devcontainer-feature.json` version. -- Builds and tests everything via `build-extensions.yml` in production mode, producing VSIX files and the CLI tarball as workflow artifacts. +- Builds and tests everything via `build-extensions.yml` in production mode, producing VSIX files as workflow artifacts. - Publishes the devcontainer feature to GHCR with build provenance. - Attests VSIX provenance via `actions/attest-build-provenance` and attaches both VSIXes to the GitHub Release. -- Publishes the CLI to npm via OIDC trusted publishing (with `--provenance`), attests the tarball via `actions/attest-build-provenance`, and attaches the tarball to the GitHub Release. No manual publish commands. The GitHub Release is the trigger; CI does the rest. - -## One-time bootstrap: CLI npm trusted publishing - -npm's trusted publishing requires the package to exist before the trust policy can be configured. The first time we publish `@devcontainer-dev-certs/cli` we have a chicken-and-egg problem: the CI workflow can't authenticate to npm yet (no trust policy), but the trust policy can't be created yet (no package). - -We resolve this by publishing a content-stub version manually under a prerelease tag that no consumer's range query will satisfy, then configuring the trust policy, then letting CI publish all real versions from then on. - -This procedure happens **once** in the lifetime of the package. After the trust policy is configured the workflow takes over and no maintainer needs to touch npm credentials again. - -### Steps - -1. Make sure you have `npm` configured with an account that's a member of the `@devcontainer-dev-certs` org. - - ```bash - npm login - npm whoami - ``` - -2. From a clean checkout, on a throwaway branch (we never push this — the version mangling is local only): - - ```bash - git checkout -b chore/bootstrap-npm-stub - cd src/cli - npm version 0.0.0-bootstrap --no-git-tag-version - npm publish --access public - ``` - - `prepublishOnly` produces the minified bundle. `npm publish` uploads it under the prerelease tag `0.0.0-bootstrap`. Because it's a prerelease, it doesn't match `^1.0.0`, `*`, or any other default range query — installers asking for the package won't get the stub. - -3. Mark the stub as deprecated so anyone who explicitly pins to it sees a warning: - - ```bash - npm deprecate @devcontainer-dev-certs/cli@0.0.0-bootstrap \ - "Stub version published to bootstrap trusted publishing. Install @devcontainer-dev-certs/cli@latest." - ``` - -4. Discard the throwaway branch: - - ```bash - cd ../.. - git checkout main - git branch -D chore/bootstrap-npm-stub - ``` - -5. Configure the trusted publisher on npmjs.com: - - - Navigate to - - Open the **Settings** tab on the package page - - Scroll to the **Trusted Publisher** section - - Add a publisher with: - - Publisher type: **GitHub Actions** - - Organization / user: `dnegstad` - - Repository: `devcontainer-dev-certs` - - Workflow filename: `release-feature.yml` - - Environment: `release` - - Allowed actions: **npm publish** (required for configs created after May 20, 2026) - - Save. - -6. Cut the first real release (`v1.4.0` or whatever the next coordinated repo-wide version is) through the normal procedure above. CI authenticates via OIDC, publishes with `--provenance`, attaches the tarball to the Release. - -7. Verify the first real release end-to-end: - - ```bash - # Tarball attestation via GitHub - gh release download v1.4.0 --pattern '*.tgz' --repo dnegstad/devcontainer-dev-certs - gh attestation verify devcontainer-dev-certs-cli-*.tgz --repo dnegstad/devcontainer-dev-certs - - # Provenance on the npm registry - npm view @devcontainer-dev-certs/cli@1.4.0 - ``` - - Both should report successful verification with the workflow run that produced the artifact. diff --git a/eslint.config.mjs b/eslint.config.mjs index c1d4c81..387ec5d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,7 +41,6 @@ export default tseslint.config( parserOptions: { project: [ "./src/shared/tsconfig.json", - "./src/cli/tsconfig.lint.json", "./src/vscode-ui-extension/tsconfig.lint.json", "./src/vscode-workspace-extension/tsconfig.lint.json", ], diff --git a/examples/manual-setup/README.md b/examples/manual-setup/README.md index 628f93e..e53324e 100644 --- a/examples/manual-setup/README.md +++ b/examples/manual-setup/README.md @@ -1,44 +1,22 @@ # manual-setup -End-to-end example for using `devcontainer-dev-certs` outside of VS Code (JetBrains, Vim, raw CLI, CI). +End-to-end example for using `devcontainer-dev-certs` outside of VS Code. + +> [!IMPORTANT] +> Usage outside of VS Code is **minimally supported** and the host-side half of the workflow is hand-wired. The host extension that ships with this project does cert generation, OS trust, and routing automatically; without it you're driving `dotnet dev-certs` (or a comparable tool) yourself and only the in-container installer is automated. If you're choosing between this and the VS Code extensions, pick the extensions. ## What this gives you The same canonical trust state the VS Code workspace extension produces — `~/.dotnet/corefx/cryptography/x509stores/{my,root}` populated with your dev cert and `~/.aspnet/dev-certs/trust/` populated with hash-symlinked PEMs — without VS Code being involved. Kestrel discovers the cert via its `X509Store` fallback; `curl`, `wget`, and other OpenSSL clients trust it via `SSL_CERT_DIR`. +What this does **not** give you: automated host-side cert generation, automated OS trust on the host, the reverse-sync flow (`syncContainerCert`), or `defaultKestrelCertificate`. Those all require the VS Code host extension. + ## Prerequisites - The `devcontainer-dev-certs` feature in your `devcontainer.json` with `installFallbackTools: true` (or `openssl` and `jq` already present in your base image). - A directory on the host containing the cert files you want installed, plus a `bundle.json` describing them. -The host-side cert + `bundle.json` can be produced two ways. **The `dcdc` CLI is the simpler path** — one command does generation, host trust, and `bundle.json` emission. The manual path (still documented below) is what you'd reach for when `dcdc` isn't available, when you need a cert from a different source, or when you're sharing a specific cert with the VS Code host extension. - -## One-time host setup (with `dcdc`) - -`dcdc` is the host-side CLI shipped as [`@devcontainer-dev-certs/cli`](https://www.npmjs.com/package/@devcontainer-dev-certs/cli) on npm: - -```bash -npm install -g @devcontainer-dev-certs/cli -``` - -Node 22 or newer is required. See [`src/cli/README.md`](../../src/cli/README.md) for the full command reference. - -Pick a host directory to hold your certs and bundle file (the example below uses `~/.dev-certs`) and generate everything in one shot: - -```bash -mkdir -p ~/.dev-certs -dcdc generate --out-dir ~/.dev-certs -``` - -This: - -1. Generates an ASP.NET-compatible dev cert (RSA-2048, the standard `localhost` + `*.dev.localhost` + docker SANs, the ASP.NET dev-cert OID marker so Kestrel finds it). -2. Trusts it on the host (Linux/macOS/Windows — same backend the VS Code host extension uses, or `dotnet dev-certs --trust` on macOS when `dotnet` is on PATH). -3. Writes `aspnetcore-dev.pfx`, `aspnetcore-dev.pem`, `aspnetcore-dev.key`, and `bundle.json` into the out-dir, with `bundle.json` already wired to the container-mount path (`/host-dev-certs` by default). - -Skip to "[Project setup](#project-setup)" — no other host steps required. - -## One-time host setup (manually) +## One-time host setup Pick a host directory to hold your certs and bundle file: @@ -65,7 +43,7 @@ openssl x509 -in ~/.dev-certs/aspnetcore-dev.pem -noout -fingerprint -sha1 \ Drop a copy of [`bundle.json`](./bundle.json) into `~/.dev-certs/` and replace `REPLACE_WITH_SHA1_FINGERPRINT_NO_COLONS` with the fingerprint you just computed. -> **Note on `dotnet dev-certs`-generated certs vs the host extension's certs.** This manual path uses `dotnet dev-certs https` for cert generation. The host extension produces functionally equivalent certs with the same OID marker and SAN entries — either source works against the same fallback installer in the container. If you need to share a *specific* cert with the host extension (e.g. a Windows developer also runs the extension), generate it once and have both flows consume the same PFX. +> **Note on `dotnet dev-certs`-generated certs vs the host extension's certs.** This path uses `dotnet dev-certs https` for cert generation. The VS Code host extension produces functionally equivalent certs with the same OID marker and SAN entries — either source works against the same fallback installer in the container. If you need to share a *specific* cert with the host extension (e.g. a Windows developer also runs the extension), generate it once and have both flows consume the same PFX. ## Project setup @@ -77,7 +55,7 @@ Copy the bits of [`devcontainer.json`](./devcontainer.json) you want into your o The fallback installer is delivered to `/usr/local/bin/devcontainer-dev-certs-install` by the feature. -Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`dcdc generate` again, or the manual `dotnet dev-certs https --clean && …re-export…` ritual) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. +Use `postStartCommand` (not `postCreateCommand`) so the install re-runs on every container start. That way regenerating the cert on the host (`dotnet dev-certs https --clean && …re-export…`) takes effect the next time you start the container — no rebuild required. The `|| true` keeps container startup from blocking if the bundle is missing or malformed. ## Verifying @@ -89,12 +67,6 @@ devcontainer-dev-certs-install --doctor You should see `[ok]` for every check. If you see `[fail]` or `[warn]`, the message tells you what to fix. -On the host, `dcdc doctor` gives equivalent diagnostics for the host side (which backends are available, whether the cert is in the host platform store and trusted): - -```bash -dcdc doctor -``` - You can also sanity-check from inside the container: ```bash @@ -127,12 +99,10 @@ The bundle is a list — add corporate CAs, wildcard certs, etc. as additional e CA-only entries (no `pfxPath`, no `pemKeyPath`) are valid — they get planted in the trust store but no private key is synced. -`dcdc bundle ` emits a single-cert `bundle.json` for an arbitrary cert file (auto-discovers sibling `.pem` / `.key` / `.pfx`, fills in the SHA-1 thumbprint, rewrites paths to the container mount). Merge its output into your existing bundle by hand to add a cert. - See the [bundle schema](../../schema/bundle.schema.json) for the full field reference. ## Limitations -- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `dcdc generate` (the example above does this — it runs the host trust step), `dotnet dev-certs https --trust` (the manual path above does this), an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. +- **Host trust is on you.** This script only handles the *container side*. Trusting the cert on your host so browsers accept forwarded ports requires `dotnet dev-certs https --trust`, an OS-specific dance (`security` on macOS, PowerShell on Windows, NSS / OpenSSL on Linux), or running the VS Code host extension once even if you don't use VS Code day-to-day. - **No `defaultKestrelCertificate` equivalent.** The VS Code-only `defaultKestrelCertificate` setting writes `ASPNETCORE_Kestrel__Certificates__Default__Path/__Password` via VS Code's `EnvironmentVariableCollection`. To pin a custom Kestrel default outside VS Code, set those env vars yourself in `devcontainer.json` `containerEnv`. - **No reverse sync (container → host).** The `syncContainerCert` flow needs a privileged host-side process to add the cert to the host OS trust store; without the host extension's UI there's nowhere to surface the consent prompt. diff --git a/package-lock.json b/package-lock.json index 31cabef..5a3ffaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,6 @@ "name": "devcontainer-dev-certs", "workspaces": [ "src/shared", - "src/cli", "src/vscode-ui-extension", "src/vscode-workspace-extension" ], @@ -21,15 +20,11 @@ }, "node_modules/@azu/format-text": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", - "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@azu/style-format": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", - "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", "dev": true, "license": "WTFPL", "dependencies": { @@ -38,8 +33,6 @@ }, "node_modules/@azure/abort-controller": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", "dependencies": { @@ -51,8 +44,6 @@ }, "node_modules/@azure/core-auth": { "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", - "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, "license": "MIT", "dependencies": { @@ -66,8 +57,6 @@ }, "node_modules/@azure/core-client": { "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", - "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "dev": true, "license": "MIT", "dependencies": { @@ -85,8 +74,6 @@ }, "node_modules/@azure/core-rest-pipeline": { "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", - "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -104,8 +91,6 @@ }, "node_modules/@azure/core-tracing": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", - "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -117,8 +102,6 @@ }, "node_modules/@azure/core-util": { "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", - "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "license": "MIT", "dependencies": { @@ -132,8 +115,6 @@ }, "node_modules/@azure/identity": { "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", - "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, "license": "MIT", "dependencies": { @@ -155,8 +136,6 @@ }, "node_modules/@azure/logger": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", - "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "license": "MIT", "dependencies": { @@ -169,8 +148,6 @@ }, "node_modules/@azure/msal-browser": { "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.6.3.tgz", - "integrity": "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w==", "dev": true, "license": "MIT", "dependencies": { @@ -182,8 +159,6 @@ }, "node_modules/@azure/msal-common": { "version": "16.4.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.4.1.tgz", - "integrity": "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw==", "dev": true, "license": "MIT", "engines": { @@ -192,8 +167,6 @@ }, "node_modules/@azure/msal-node": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", - "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -206,8 +179,6 @@ }, "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { "version": "16.6.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", - "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", "dev": true, "license": "MIT", "engines": { @@ -216,8 +187,6 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -231,332 +200,18 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@devcontainer-dev-certs/cli": { - "resolved": "src/cli", - "link": true - }, "node_modules/@devcontainer-dev-certs/shared": { "resolved": "src/shared", "link": true }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -570,163 +225,8 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -744,8 +244,6 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -757,8 +255,6 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -767,8 +263,6 @@ }, "node_modules/@eslint/config-array": { "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -782,8 +276,6 @@ }, "node_modules/@eslint/config-array/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -792,8 +284,6 @@ }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -805,8 +295,6 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -821,8 +309,6 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -834,8 +320,6 @@ }, "node_modules/@eslint/core": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -847,8 +331,6 @@ }, "node_modules/@eslint/js": { "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { @@ -868,8 +350,6 @@ }, "node_modules/@eslint/object-schema": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -878,8 +358,6 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -892,8 +370,6 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -905,8 +381,6 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -920,8 +394,6 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -930,8 +402,6 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -944,8 +414,6 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -958,8 +426,6 @@ }, "node_modules/@isaacs/cliui": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -968,34 +434,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@noble/hashes": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "license": "MIT", "engines": { "node": ">= 16" @@ -1006,8 +449,6 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -1020,8 +461,6 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -1030,8 +469,6 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1044,8 +481,6 @@ }, "node_modules/@oxc-project/types": { "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -1054,8 +489,6 @@ }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -1067,8 +500,6 @@ }, "node_modules/@peculiar/asn1-csr": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -1079,62 +510,6 @@ }, "node_modules/@peculiar/asn1-ecc": { "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -1143,306 +518,117 @@ "tslib": "^2.8.1" } }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", - "license": "MIT", - "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.0.0.tgz", - "integrity": "sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA==", - "license": "MIT", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", - "cpu": [ - "s390x" - ], - "dev": true, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/@peculiar/x509": { + "version": "2.0.0", "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { + "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { + "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -1450,7 +636,7 @@ "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" @@ -1458,15 +644,11 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", - "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1478,8 +660,6 @@ }, "node_modules/@secretlint/config-loader": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", - "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1496,8 +676,6 @@ }, "node_modules/@secretlint/core": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", - "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "license": "MIT", "dependencies": { @@ -1512,8 +690,6 @@ }, "node_modules/@secretlint/formatter": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", - "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { @@ -1535,8 +711,6 @@ }, "node_modules/@secretlint/formatter/node_modules/chalk": { "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -1548,8 +722,6 @@ }, "node_modules/@secretlint/node": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", - "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1568,22 +740,16 @@ }, "node_modules/@secretlint/profiler": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", - "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/resolver": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", - "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/secretlint-formatter-sarif": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", - "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1592,8 +758,6 @@ }, "node_modules/@secretlint/secretlint-rule-no-dotenv": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", - "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", "dependencies": { @@ -1605,8 +769,6 @@ }, "node_modules/@secretlint/secretlint-rule-preset-recommend": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", - "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, "license": "MIT", "engines": { @@ -1615,8 +777,6 @@ }, "node_modules/@secretlint/source-creator": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", - "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "license": "MIT", "dependencies": { @@ -1629,8 +789,6 @@ }, "node_modules/@secretlint/types": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", - "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", "engines": { @@ -1639,8 +797,6 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, "license": "MIT", "engines": { @@ -1652,22 +808,16 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@textlint/ast-node-types": { "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.5.4.tgz", - "integrity": "sha512-bVtB6VEy9U9DpW8cTt25k5T+lz86zV5w6ImePZqY1AXzSuPhqQNT77lkMPxonXzUducEIlSvUu3o7sKw3y9+Sw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.5.4.tgz", - "integrity": "sha512-D9qJedKBLmAo+kiudop4UKgSxXMi4O8U86KrCidVXZ9RsK0NSVIw6+r2rlMUOExq79iEY81FRENyzmNVRxDBsg==", "dev": true, "license": "MIT", "dependencies": { @@ -1689,8 +839,6 @@ }, "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -1699,15 +847,11 @@ }, "node_modules/@textlint/linter-formatter/node_modules/pluralize": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", - "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -1719,43 +863,24 @@ }, "node_modules/@textlint/module-interop": { "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.5.4.tgz", - "integrity": "sha512-JyAUd26ll3IFF87LP0uGoa8Tzw5ZKiYvGs6v8jLlzyND1lUYCI4+2oIAslrODLkf0qwoCaJrBQWM3wsw+asVGQ==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.5.4.tgz", - "integrity": "sha512-5GUagtpQuYcmhlOzBGdmVBvDu5lKgVTjwbxtdfoidN4OIqblIxThJHHjazU+ic+/bCIIzI2JcOjHGSaRmE8Gcg==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.5.4.tgz", - "integrity": "sha512-mY28j2U7nrWmZbxyKnRvB8vJxJab4AxqOobLfb6iozrLelJbqxcOTvBQednadWPfAk9XWaZVMqUr9Nird3mutg==", "dev": true, "license": "MIT", "dependencies": { "@textlint/ast-node-types": "15.5.4" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -1765,36 +890,26 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/esrecurse": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1803,29 +918,21 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, "license": "MIT" }, "node_modules/@types/sarif": { "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", - "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", "dev": true, "license": "MIT" }, "node_modules/@types/vscode": { "version": "1.110.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", - "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", - "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1853,8 +960,6 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", - "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1878,8 +983,6 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", - "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1900,8 +1003,6 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", - "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", "dev": true, "license": "MIT", "dependencies": { @@ -1918,8 +1019,6 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", - "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", "dev": true, "license": "MIT", "engines": { @@ -1935,8 +1034,6 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", - "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1960,8 +1057,6 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", - "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", "dev": true, "license": "MIT", "engines": { @@ -1974,8 +1069,6 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", - "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", "dev": true, "license": "MIT", "dependencies": { @@ -2002,8 +1095,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -2012,8 +1103,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2025,8 +1114,6 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2041,8 +1128,6 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", - "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2065,8 +1150,6 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", - "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2083,8 +1166,6 @@ }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", - "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", "dev": true, "license": "MIT", "dependencies": { @@ -2098,8 +1179,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -2116,8 +1195,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { @@ -2143,8 +1220,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { @@ -2156,8 +1231,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2170,8 +1243,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2186,8 +1257,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, "license": "MIT", "funding": { @@ -2196,8 +1265,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { @@ -2211,8 +1278,6 @@ }, "node_modules/@vscode/vsce": { "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz", - "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==", "dev": true, "license": "MIT", "dependencies": { @@ -2221,148 +1286,60 @@ "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", - "@vscode/vsce-sign": "^2.0.0", - "azure-devops-node-api": "^12.5.0", - "chalk": "^4.1.2", - "cheerio": "^1.0.0-rc.9", - "cockatiel": "^3.1.2", - "commander": "^12.1.0", - "form-data": "^4.0.0", - "glob": "^11.0.0", - "hosted-git-info": "^4.0.2", - "jsonc-parser": "^3.2.0", - "leven": "^3.1.0", - "markdown-it": "^14.1.0", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "secretlint": "^10.1.2", - "semver": "^7.5.2", - "tmp": "^0.2.3", - "typed-rest-client": "^1.8.4", - "url-join": "^4.0.1", - "xml2js": "^0.5.0", - "yauzl": "^3.2.1", - "yazl": "^2.2.2" - }, - "bin": { - "vsce": "vsce" - }, - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "keytar": "^7.7.0" - } - }, - "node_modules/@vscode/vsce-sign": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", - "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", - "dev": true, - "hasInstallScript": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optionalDependencies": { - "@vscode/vsce-sign-alpine-arm64": "2.0.6", - "@vscode/vsce-sign-alpine-x64": "2.0.6", - "@vscode/vsce-sign-darwin-arm64": "2.0.6", - "@vscode/vsce-sign-darwin-x64": "2.0.6", - "@vscode/vsce-sign-linux-arm": "2.0.6", - "@vscode/vsce-sign-linux-arm64": "2.0.6", - "@vscode/vsce-sign-linux-x64": "2.0.6", - "@vscode/vsce-sign-win32-arm64": "2.0.6", - "@vscode/vsce-sign-win32-x64": "2.0.6" - } - }, - "node_modules/@vscode/vsce-sign-alpine-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", - "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-alpine-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", - "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "alpine" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", - "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-darwin-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", - "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@vscode/vsce-sign-linux-arm": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", - "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^3.2.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } }, - "node_modules/@vscode/vsce-sign-linux-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", - "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", - "cpu": [ - "arm64" - ], + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", "dev": true, + "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "linux" - ] + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } }, "node_modules/@vscode/vsce-sign-linux-x64": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", - "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", "cpu": [ "x64" ], @@ -2373,38 +1350,8 @@ "linux" ] }, - "node_modules/@vscode/vsce-sign-win32-arm64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", - "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/vsce-sign-win32-x64": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", - "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/acorn": { "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2416,8 +1363,6 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2426,8 +1371,6 @@ }, "node_modules/agent-base": { "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -2436,8 +1379,6 @@ }, "node_modules/ajv": { "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -2453,8 +1394,6 @@ }, "node_modules/ansi-escapes": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", - "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -2469,8 +1408,6 @@ }, "node_modules/ansi-regex": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -2482,8 +1419,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -2498,15 +1433,11 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/asn1js": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", @@ -2519,8 +1450,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -2529,8 +1458,6 @@ }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "engines": { @@ -2539,15 +1466,11 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/azure-devops-node-api": { "version": "12.5.0", - "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", - "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "license": "MIT", "dependencies": { @@ -2557,15 +1480,11 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -2586,8 +1505,6 @@ }, "node_modules/binaryextensions": { "version": "6.11.0", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", - "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -2602,8 +1519,6 @@ }, "node_modules/bl": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", "optional": true, @@ -2615,22 +1530,16 @@ }, "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/boundary": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", - "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2640,8 +1549,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2653,8 +1560,6 @@ }, "node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -2679,8 +1584,6 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -2689,15 +1592,11 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2712,8 +1611,6 @@ }, "node_modules/bytestreamjs": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "license": "BSD-3-Clause", "engines": { "node": ">=6.0.0" @@ -2721,8 +1618,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2735,8 +1630,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -2752,8 +1645,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -2762,8 +1653,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -2779,8 +1668,6 @@ }, "node_modules/cheerio": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, "license": "MIT", "dependencies": { @@ -2805,8 +1692,6 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2823,16 +1708,12 @@ }, "node_modules/chownr": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "license": "ISC", "optional": true }, "node_modules/cockatiel": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", - "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", "dev": true, "license": "MIT", "engines": { @@ -2841,8 +1722,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2854,15 +1733,11 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -2874,8 +1749,6 @@ }, "node_modules/commander": { "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -2884,22 +1757,16 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -2913,8 +1780,6 @@ }, "node_modules/css-select": { "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2930,8 +1795,6 @@ }, "node_modules/css-what": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2943,8 +1806,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2961,8 +1822,6 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "optional": true, @@ -2978,8 +1837,6 @@ }, "node_modules/deep-extend": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", "optional": true, @@ -2989,15 +1846,11 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/default-browser": { "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -3013,8 +1866,6 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -3026,8 +1877,6 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { @@ -3039,8 +1888,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -3049,8 +1896,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3067,8 +1912,6 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { @@ -3082,8 +1925,6 @@ }, "node_modules/domelementtype": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -3095,8 +1936,6 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3111,8 +1950,6 @@ }, "node_modules/domutils": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3126,8 +1963,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -3141,8 +1976,6 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3151,8 +1984,6 @@ }, "node_modules/editions": { "version": "6.22.0", - "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", - "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -3168,15 +1999,11 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/encoding-sniffer": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -3189,8 +2016,6 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "optional": true, @@ -3200,8 +2025,6 @@ }, "node_modules/entities": { "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3213,8 +2036,6 @@ }, "node_modules/environment": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", - "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", "engines": { @@ -3226,8 +2047,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -3236,8 +2055,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -3246,15 +2063,11 @@ }, "node_modules/es-module-lexer": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -3266,8 +2079,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -3282,8 +2093,6 @@ }, "node_modules/esbuild": { "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3324,8 +2133,6 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -3337,8 +2144,6 @@ }, "node_modules/eslint": { "version": "10.3.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", "dependencies": { @@ -3393,8 +2198,6 @@ }, "node_modules/eslint-scope": { "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3412,8 +2215,6 @@ }, "node_modules/eslint-visitor-keys": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3425,8 +2226,6 @@ }, "node_modules/eslint/node_modules/ajv": { "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3442,8 +2241,6 @@ }, "node_modules/eslint/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -3452,8 +2249,6 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3465,8 +2260,6 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -3478,8 +2271,6 @@ }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -3488,15 +2279,11 @@ }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3511,8 +2298,6 @@ }, "node_modules/espree": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3529,8 +2314,6 @@ }, "node_modules/esquery": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3542,8 +2325,6 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3555,8 +2336,6 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3565,8 +2344,6 @@ }, "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -3575,8 +2352,6 @@ }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3585,8 +2360,6 @@ }, "node_modules/expand-template": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", "optional": true, @@ -3596,8 +2369,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3606,15 +2377,11 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3630,22 +2397,16 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -3661,8 +2422,6 @@ }, "node_modules/fastq": { "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3671,8 +2430,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -3689,8 +2446,6 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3702,8 +2457,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -3715,8 +2468,6 @@ }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -3732,8 +2483,6 @@ }, "node_modules/flat-cache": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -3746,15 +2495,11 @@ }, "node_modules/flatted": { "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/foreground-child": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -3770,8 +2515,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -3787,16 +2530,12 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/fs-extra": { "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -3808,25 +2547,8 @@ "node": ">=14.14" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -3835,8 +2557,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3860,8 +2580,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3874,17 +2592,12 @@ }, "node_modules/github-from-package": { "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/glob": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3907,8 +2620,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -3920,8 +2631,6 @@ }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -3930,8 +2639,6 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -3943,8 +2650,6 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3959,8 +2664,6 @@ }, "node_modules/globals": { "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -3972,8 +2675,6 @@ }, "node_modules/globby": { "version": "14.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", - "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, "license": "MIT", "dependencies": { @@ -3993,8 +2694,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -4006,15 +2705,11 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -4023,8 +2718,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -4036,8 +2729,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4052,8 +2743,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4065,8 +2754,6 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -4078,8 +2765,6 @@ }, "node_modules/htmlparser2": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -4098,8 +2783,6 @@ }, "node_modules/htmlparser2/node_modules/entities": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -4111,8 +2794,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -4125,8 +2806,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -4139,8 +2818,6 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4152,8 +2829,6 @@ }, "node_modules/ieee754": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -4174,8 +2849,6 @@ }, "node_modules/ignore": { "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -4184,8 +2857,6 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -4194,8 +2865,6 @@ }, "node_modules/index-to-position": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", - "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", "engines": { @@ -4207,24 +2876,18 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC", "optional": true }, "node_modules/ini": { "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, "license": "ISC", "optional": true }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { @@ -4239,8 +2902,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -4249,8 +2910,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -4259,8 +2918,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -4272,8 +2929,6 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { @@ -4291,8 +2946,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -4301,8 +2954,6 @@ }, "node_modules/is-wsl": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -4317,15 +2968,11 @@ }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/istextorbinary": { "version": "9.5.0", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", - "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -4342,8 +2989,6 @@ }, "node_modules/jackspeak": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -4358,15 +3003,11 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -4378,29 +3019,21 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -4412,15 +3045,11 @@ }, "node_modules/jsonc-parser": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -4432,8 +3061,6 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "license": "MIT", "dependencies": { @@ -4455,8 +3082,6 @@ }, "node_modules/jwa": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "license": "MIT", "dependencies": { @@ -4467,8 +3092,6 @@ }, "node_modules/jws": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", "dependencies": { @@ -4478,8 +3101,6 @@ }, "node_modules/keytar": { "version": "7.9.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", - "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4491,8 +3112,6 @@ }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -4501,8 +3120,6 @@ }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", "engines": { @@ -4511,8 +3128,6 @@ }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4525,8 +3140,6 @@ }, "node_modules/lightningcss": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -4553,157 +3166,8 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -4723,8 +3187,6 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -4742,52 +3204,8 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/linkify-it": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4796,8 +3214,6 @@ }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -4812,71 +3228,51 @@ }, "node_modules/lodash": { "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true, "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true, "license": "MIT" }, "node_modules/lodash.truncate": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -4888,8 +3284,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4898,8 +3292,6 @@ }, "node_modules/markdown-it": { "version": "14.1.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", - "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -4916,8 +3308,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -4926,15 +3316,11 @@ }, "node_modules/mdurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -4943,8 +3329,6 @@ }, "node_modules/micromatch": { "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -4957,8 +3341,6 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4970,8 +3352,6 @@ }, "node_modules/mime": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", "bin": { @@ -4983,8 +3363,6 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -4993,8 +3371,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -5006,8 +3382,6 @@ }, "node_modules/mimic-response": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "optional": true, @@ -5020,8 +3394,6 @@ }, "node_modules/minimatch": { "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5033,8 +3405,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "optional": true, @@ -5044,8 +3414,6 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5054,30 +3422,22 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, "license": "MIT", "optional": true }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, "node_modules/nanoid": { "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -5095,23 +3455,17 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/node-abi": { "version": "3.89.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", - "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", "dev": true, "license": "MIT", "optional": true, @@ -5124,16 +3478,12 @@ }, "node_modules/node-addon-api": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-sarif-builder": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", - "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", "dev": true, "license": "MIT", "dependencies": { @@ -5146,8 +3496,6 @@ }, "node_modules/normalize-package-data": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5161,8 +3509,6 @@ }, "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "license": "ISC", "dependencies": { @@ -5174,15 +3520,11 @@ }, "node_modules/normalize-package-data/node_modules/lru-cache": { "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/nth-check": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -5194,8 +3536,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -5207,8 +3547,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -5218,8 +3556,6 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "optional": true, @@ -5229,8 +3565,6 @@ }, "node_modules/open": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { @@ -5248,8 +3582,6 @@ }, "node_modules/optionator": { "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -5266,8 +3598,6 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5282,8 +3612,6 @@ }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -5298,8 +3626,6 @@ }, "node_modules/p-map": { "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -5311,15 +3637,11 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parse-json": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5336,8 +3658,6 @@ }, "node_modules/parse-semver": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5346,8 +3666,6 @@ }, "node_modules/parse-semver/node_modules/semver": { "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -5356,8 +3674,6 @@ }, "node_modules/parse5": { "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -5369,8 +3685,6 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { @@ -5383,8 +3697,6 @@ }, "node_modules/parse5-parser-stream": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { @@ -5396,8 +3708,6 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -5409,8 +3719,6 @@ }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -5419,8 +3727,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -5429,8 +3735,6 @@ }, "node_modules/path-scurry": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -5446,8 +3750,6 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5456,8 +3758,6 @@ }, "node_modules/path-type": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", - "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, "license": "MIT", "engines": { @@ -5469,29 +3769,21 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -5503,8 +3795,6 @@ }, "node_modules/pkijs": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", - "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "license": "BSD-3-Clause", "dependencies": { "@noble/hashes": "1.4.0", @@ -5520,8 +3810,6 @@ }, "node_modules/pluralize": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", "engines": { @@ -5530,8 +3818,6 @@ }, "node_modules/postcss": { "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -5559,9 +3845,6 @@ }, "node_modules/prebuild-install": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", "optional": true, @@ -5588,8 +3871,6 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -5598,8 +3879,6 @@ }, "node_modules/pump": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "optional": true, @@ -5610,8 +3889,6 @@ }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -5620,8 +3897,6 @@ }, "node_modules/punycode.js": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, "license": "MIT", "engines": { @@ -5630,8 +3905,6 @@ }, "node_modules/pvtsutils": { "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -5639,8 +3912,6 @@ }, "node_modules/pvutils": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "license": "MIT", "engines": { "node": ">=16.0.0" @@ -5648,8 +3919,6 @@ }, "node_modules/qs": { "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5664,8 +3933,6 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -5685,8 +3952,6 @@ }, "node_modules/rc": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, @@ -5702,8 +3967,6 @@ }, "node_modules/rc-config-loader": { "version": "4.1.4", - "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", - "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5715,8 +3978,6 @@ }, "node_modules/read": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "license": "ISC", "dependencies": { @@ -5728,8 +3989,6 @@ }, "node_modules/read-pkg": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, "license": "MIT", "dependencies": { @@ -5748,8 +4007,6 @@ }, "node_modules/read-pkg/node_modules/unicorn-magic": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", "engines": { @@ -5761,8 +4018,6 @@ }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "optional": true, @@ -5777,14 +4032,10 @@ }, "node_modules/reflect-metadata": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, "node_modules/require-from-string": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -5793,8 +4044,6 @@ }, "node_modules/reusify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -5804,8 +4053,6 @@ }, "node_modules/rolldown": { "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { @@ -5838,8 +4085,6 @@ }, "node_modules/run-applescript": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -5851,8 +4096,6 @@ }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -5875,8 +4118,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -5896,15 +4137,11 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -5913,8 +4150,6 @@ }, "node_modules/secretlint": { "version": "10.2.2", - "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", - "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", "dev": true, "license": "MIT", "dependencies": { @@ -5935,8 +4170,6 @@ }, "node_modules/semver": { "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -5948,8 +4181,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -5961,8 +4192,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -5971,8 +4200,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -5991,8 +4218,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { @@ -6008,8 +4233,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -6027,8 +4250,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -6047,15 +4268,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -6067,8 +4284,6 @@ }, "node_modules/simple-concat": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ { @@ -6089,8 +4304,6 @@ }, "node_modules/simple-get": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "funding": [ { @@ -6116,8 +4329,6 @@ }, "node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", "engines": { @@ -6129,8 +4340,6 @@ }, "node_modules/slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6147,8 +4356,6 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -6157,8 +4364,6 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6168,15 +4373,11 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -6186,29 +4387,21 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", - "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/stackback": { "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", - "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", "optional": true, @@ -6218,8 +4411,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -6233,8 +4424,6 @@ }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -6243,8 +4432,6 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -6256,8 +4443,6 @@ }, "node_modules/strip-ansi": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -6272,8 +4457,6 @@ }, "node_modules/strip-json-comments": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", "optional": true, @@ -6283,8 +4466,6 @@ }, "node_modules/structured-source": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", - "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6293,8 +4474,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -6306,8 +4485,6 @@ }, "node_modules/supports-hyperlinks": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", - "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", "dependencies": { @@ -6323,8 +4500,6 @@ }, "node_modules/table": { "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6340,8 +4515,6 @@ }, "node_modules/table/node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -6350,8 +4523,6 @@ }, "node_modules/table/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -6363,8 +4534,6 @@ }, "node_modules/tar-fs": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "optional": true, @@ -6377,8 +4546,6 @@ }, "node_modules/tar-stream": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", "optional": true, @@ -6395,8 +4562,6 @@ }, "node_modules/terminal-link": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", - "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", "dependencies": { @@ -6412,15 +4577,11 @@ }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/textextensions": { "version": "6.11.0", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", - "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -6435,15 +4596,11 @@ }, "node_modules/tinybench": { "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { @@ -6452,8 +4609,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -6469,8 +4624,6 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -6479,8 +4632,6 @@ }, "node_modules/tmp": { "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -6489,8 +4640,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6502,8 +4651,6 @@ }, "node_modules/ts-api-utils": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -6515,14 +4662,10 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", - "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", - "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", "license": "MIT", "dependencies": { "tslib": "^1.9.3" @@ -6533,14 +4676,10 @@ }, "node_modules/tsyringe/node_modules/tslib": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, "license": "MIT", "engines": { @@ -6549,8 +4688,6 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -6563,8 +4700,6 @@ }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -6576,8 +4711,6 @@ }, "node_modules/type-fest": { "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -6589,8 +4722,6 @@ }, "node_modules/typed-rest-client": { "version": "1.8.11", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", - "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "license": "MIT", "dependencies": { @@ -6601,8 +4732,6 @@ }, "node_modules/typescript": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6615,8 +4744,6 @@ }, "node_modules/typescript-eslint": { "version": "8.59.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", - "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6639,22 +4766,16 @@ }, "node_modules/uc.micro": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, "license": "MIT" }, "node_modules/underscore": { "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, "license": "MIT" }, "node_modules/undici": { "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -6663,15 +4784,11 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", - "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -6683,8 +4800,6 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -6693,8 +4808,6 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -6703,23 +4816,17 @@ }, "node_modules/url-join": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true, "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -6729,8 +4836,6 @@ }, "node_modules/version-range": { "version": "4.15.0", - "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", - "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", "dev": true, "license": "Artistic-2.0", "engines": { @@ -6742,8 +4847,6 @@ }, "node_modules/vite": { "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { @@ -6820,8 +4923,6 @@ }, "node_modules/vitest": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", "dependencies": { @@ -6910,9 +5011,6 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -6924,8 +5022,6 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -6934,8 +5030,6 @@ }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -6950,8 +5044,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -6967,8 +5059,6 @@ }, "node_modules/word-wrap": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -6977,16 +5067,12 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC", "optional": true }, "node_modules/wsl-utils": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", "dependencies": { @@ -7001,8 +5087,6 @@ }, "node_modules/xml2js": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", "dependencies": { @@ -7015,8 +5099,6 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "license": "MIT", "engines": { @@ -7025,15 +5107,11 @@ }, "node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yauzl": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", - "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7046,8 +5124,6 @@ }, "node_modules/yazl": { "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "dev": true, "license": "MIT", "dependencies": { @@ -7056,8 +5132,6 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { @@ -7067,39 +5141,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "src/cli": { - "name": "@devcontainer-dev-certs/cli", - "version": "1.4.0-pre.1", - "license": "MIT", - "bin": { - "dcdc": "dist/dcdc.js" - }, - "devDependencies": { - "@devcontainer-dev-certs/shared": "*", - "@peculiar/x509": "^2.0.0", - "@types/node": "^22.0.0", - "asn1js": "^3.0.10", - "commander": "^14.0.1", - "esbuild": "^0.28.0", - "pkijs": "^3.4.0", - "reflect-metadata": "^0.2.2", - "typescript": "^6.0.3", - "vitest": "^4.1.5" - }, - "engines": { - "node": ">=22" - } - }, - "src/cli/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "src/shared": { "name": "@devcontainer-dev-certs/shared", "version": "1.4.0-pre.1", diff --git a/package.json b/package.json index 274bd42..daa3c22 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "private": true, "workspaces": [ "src/shared", - "src/cli", "src/vscode-ui-extension", "src/vscode-workspace-extension" ], diff --git a/src/cli/LICENSE b/src/cli/LICENSE deleted file mode 100644 index 14fac91..0000000 --- a/src/cli/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/src/cli/README.md b/src/cli/README.md deleted file mode 100644 index 268ef63..0000000 --- a/src/cli/README.md +++ /dev/null @@ -1,176 +0,0 @@ -# dcdc - -`dcdc` is the host-side CLI for [devcontainer-dev-certs](https://github.com/dnegstad/devcontainer-dev-certs). It generates ASP.NET-compatible HTTPS development certificates, trusts them on the host, inspects existing cert files, and emits the `bundle.json` the in-container installer reads — without VS Code being involved. - -## Why this exists - -The host extension automates everything when you use VS Code. Outside of VS Code (JetBrains, Vim, raw CLI, CI), the canonical path to the same trust state is: - -1. Generate a cert. -2. Trust it on the host OS. -3. Hand-write a `bundle.json` referencing the cert files inside the container's bind-mount. -4. Hand-compute the SHA-1 thumbprint and paste it into the bundle. - -`dcdc generate` is one command that does all four. The other commands (`inspect`, `bundle`, `trust`, `doctor`) cover the rest of the lifecycle. - -`dcdc` uses the same shared cert + platform code the VS Code host extension uses — same trust paths, same SAN list, same OID marker — so a cert produced by `dcdc` is interchangeable with one produced by the extension. - -## Install - -```bash -npm install -g @devcontainer-dev-certs/cli -dcdc --help -``` - -Node 22 or newer is required (Node 20 reached end-of-life in April 2026; Node 22 is the lowest LTS still receiving security updates and is the version VS Code currently bundles). The package ships a single bundled binary (no per-install dependency resolution), so the install footprint is small. - -For a one-off invocation without a global install: - -```bash -npx -p @devcontainer-dev-certs/cli dcdc --help -``` - -### Building from source - -For development against an unreleased version, or to inspect the bundle: - -```bash -git clone https://github.com/dnegstad/devcontainer-dev-certs.git -cd devcontainer-dev-certs -npm install -cd src/cli && node esbuild.mjs -node dist/dcdc.js --help -``` - -## Quick start - -```bash -mkdir -p ~/.dev-certs -dcdc generate --out-dir ~/.dev-certs -``` - -That produces in `~/.dev-certs`: - -- `aspnetcore-dev.pfx` — cert + private key in PKCS#12, passwordless -- `aspnetcore-dev.pem` — cert in PEM -- `aspnetcore-dev.key` — private key in PEM -- `bundle.json` — manifest the in-container installer reads, with paths rewritten to the container's bind-mount target (`/host-dev-certs` by default) - -The cert is also added to your host OS trust store (Linux NSS DB / macOS keychain / Windows cert store) so browsers accept forwarded ports. - -Bind-mount the directory into the container and have the in-container installer consume the bundle — see [`examples/manual-setup/`](../../examples/manual-setup/) for the full devcontainer.json. - -## Commands - -### `dcdc generate` - -Generate a dev cert, trust it on the host, and emit `bundle.json`. - -``` -dcdc generate [--out-dir ] [--backend auto|native|dotnet] - [--no-trust] [--container-mount ] [--no-bundle] [--verbose] -``` - -| Flag | Default | Notes | -|------|---------|-------| -| `--out-dir ` | `~/.dev-certs` | Directory to write artifacts to. | -| `--backend ` | `auto` | Cert generator backend. `auto` prefers `dotnet` on macOS when the `dotnet` CLI is on PATH (better keychain-trust UX via a signed binary); `native` everywhere else. | -| `--no-trust` | off | Skip the host OS trust step. PFX / PEM / `bundle.json` are still emitted. | -| `--container-mount ` | `/host-dev-certs` | Container-side mount target the out-dir bind-mounts to. Recorded into `bundle.json` so the in-container installer reads from the right path. | -| `--no-bundle` | off | Skip emitting `bundle.json`. | -| `--verbose` | off | Stream shared-layer log lines to stderr. | - -### `dcdc inspect ` - -Print details about a PFX or PEM certificate. - -``` -dcdc inspect path/to/aspnetcore-dev.pfx -``` - -Reports the subject CN, both SHA-1 and SHA-256 thumbprints, validity window, ASP.NET dev-cert OID and version byte (so you can tell whether the cert is fresh enough for the current installer), every SAN entry (with `[non-local]` flags on any that aren't on the standard developer-cert allowlist), and warnings (cert without key, expiring soon, non-local SANs present). - -Pass `--json` for machine-readable output: - -``` -dcdc inspect --json path/to/cert.pfx -``` - -### `dcdc bundle ` - -Emit a single-cert `bundle.json` referencing an already-existing cert file. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by naming convention so a single PFX argument is usually enough. - -``` -dcdc bundle path/to/cert.pfx [--out-dir ] [--container-mount ] - [--name ] [--kind dotnet-dev|user] - [--no-trust-in-container] -``` - -| Flag | Default | Notes | -|------|---------|-------| -| `--out-dir ` | directory of cert-path | Where to write `bundle.json`. | -| `--container-mount ` | `/host-dev-certs` | Container-side mount target. | -| `--name ` | basename of cert-path | Filename stem used in the bundle (`{name}.pem`, etc.). | -| `--kind ` | `user` | `dotnet-dev` uses the historic `aspnetcore-localhost-{thumbprint}.pem` filename in the OpenSSL trust dir; `user` uses `{name}.pem`. | -| `--no-trust-in-container` | trust-in-container is on by default | Mark the entry as `trustInContainer: false` — cert is served only, not added to trust stores inside the container. | - -Useful for wrapping a cert produced by something else (a corporate CA, a manual `dotnet dev-certs` invocation, a wildcard cert generated by another tool) into the bundle format the in-container installer expects. - -If any cert file referenced by the bundle lives outside `--out-dir`, `dcdc bundle` emits a stderr warning. The writer only rewrites paths under `--out-dir` to the container-mount target; paths outside are left verbatim, which means the in-container installer will try to read them at their host-filesystem location — something it can only do if you've also bind-mounted that location. Either copy the cert files into `--out-dir` and re-run, or arrange additional mounts so the referenced paths exist container-side. - -### `dcdc trust ` - -Add an existing cert to the host OS trust store via the same shared platform layer the VS Code host extension uses. - -``` -dcdc trust path/to/cert.pfx -``` - -Short-circuits with an "already trusted" message when the cert is already in the trust store — repeated invocations don't re-prompt. - -### `dcdc doctor` - -Read-only diagnostics: which backends are available, what `--backend auto` would pick, host platform-store state, and per-OS tool presence. - -``` -dcdc doctor [--out-dir ] -``` - -Per-OS tool checks: - -- **Linux**: `openssl` (native trust step) and `certutil` (NSS browser-trust step; missing means Firefox / Chromium won't auto-trust). -- **macOS**: `security` (the keychain CLI the native backend drives). -- **Windows**: `pwsh` *or* `powershell` (Windows store enumeration; PowerShell 7+ preferred, 5.1 accepted as fallback) and `certutil.exe` (Windows trust store). - -Exits non-zero if any check reports `[fail]`. `[warn]` is informational and exits zero. - -## Bundle JSON - -The `bundle.json` written by `dcdc generate` and `dcdc bundle` conforms to the schema at [`schema/bundle.schema.json`](../../schema/bundle.schema.json) and is the same format the in-container `devcontainer-dev-certs-install --bundle-json` installer accepts. - -For multi-cert setups (auto-generated dev cert + corporate CA + extra wildcard), the simplest workflow is `dcdc generate` to seed the bundle and then hand-edit additional entries in. The bundle is a list — see the [root README](../../README.md#manual--non-vs-code-use) for the field reference. - -## Backends - -Two backends today; both produce certs the in-container installer accepts. - -- **`native`** uses the bundled cert primitives (the same Node + `@peculiar/x509` + `pkijs` code path the VS Code host extension uses). No external runtime dependencies. Works on every platform. -- **`dotnet`** shells out to `dotnet dev-certs https`. Requires the dotnet SDK on PATH. On macOS this gives a more polished keychain trust prompt (the calling binary is Apple-notarized); on Windows / Linux it's functionally equivalent to native. - -`--backend auto` (the default) picks dotnet on macOS when available, native everywhere else. The VS Code host extension has the same selection logic, controlled by the `devcontainerDevCerts.hostCertGenerator` setting. - -On Windows, the dotnet backend (and any other shell-out in this tool) resolves its command through PATH before spawning, so a malicious binary planted in the working directory can't hijack the lookup. - -## `--no-trust` semantics - -The two backends honor `--no-trust` differently: - -- **`--backend native --no-trust`** generates the cert purely in memory and writes only to `--out-dir`. The host's `.NET` X509 store and OS trust store are not touched. This is the right choice for "give me cert files to bind-mount into a container, don't install anything on my host." -- **`--backend dotnet --no-trust`** skips the OS trust prompt, but `dotnet dev-certs https` still persists the cert into the `.NET` X509 store as a side effect — that's how `dotnet dev-certs` itself works, regardless of `--trust`. If you want strict file-only output, use the native backend. - -Without `--no-trust`, both backends write to the `.NET` X509 store and trust the cert in the OS — that's the host-trust contract (it's where `dotnet dev-certs --check`, host-running Kestrel, and the VS Code host extension all look for the cert). - -## Limitations - -- **No published binary yet.** Build from source as described above. -- **No reverse sync.** The VS Code workspace extension's `syncContainerCert` flow (pushing a container-side cert back to the host) needs the host extension's consent UI; there's no equivalent in the CLI. diff --git a/src/cli/esbuild.mjs b/src/cli/esbuild.mjs deleted file mode 100644 index 5401d60..0000000 --- a/src/cli/esbuild.mjs +++ /dev/null @@ -1,22 +0,0 @@ -import * as esbuild from "esbuild"; - -const production = process.argv.includes("--production"); - -await esbuild.build({ - entryPoints: ["src/index.ts"], - bundle: true, - outfile: "dist/dcdc.js", - // `vscode` is a build-time stub used only by the shared package's - // `loggerVscode.ts` helper, which the CLI never imports. Marking it - // external prevents esbuild from trying to resolve a module that - // doesn't exist outside the VS Code extension host. - external: ["vscode"], - format: "cjs", - platform: "node", - target: "node22", - sourcemap: !production, - minify: production, - banner: { - js: "#!/usr/bin/env node\n", - }, -}); diff --git a/src/cli/package.json b/src/cli/package.json deleted file mode 100644 index 4a3bb79..0000000 --- a/src/cli/package.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "name": "@devcontainer-dev-certs/cli", - "version": "1.4.0-pre.1", - "description": "dcdc — host-side CLI for generating, inspecting, and trusting ASP.NET-compatible dev certs for use with dev containers, outside of VS Code.", - "license": "MIT", - "author": "Daniel Negstad", - "homepage": "https://github.com/dnegstad/devcontainer-dev-certs#readme", - "repository": { - "type": "git", - "url": "https://github.com/dnegstad/devcontainer-dev-certs.git", - "directory": "src/cli" - }, - "bugs": { - "url": "https://github.com/dnegstad/devcontainer-dev-certs/issues" - }, - "keywords": [ - "devcontainer", - "dev-certs", - "https", - "aspnet", - "aspire", - "kestrel", - "tls", - "ssl", - "certificate", - "x509" - ], - "main": "./dist/dcdc.js", - "bin": { - "dcdc": "./dist/dcdc.js" - }, - "files": [ - "dist/dcdc.js", - "LICENSE", - "README.md" - ], - "engines": { - "node": ">=22" - }, - "publishConfig": { - "access": "public", - "provenance": true - }, - "scripts": { - "build": "node esbuild.mjs", - "build:prod": "node esbuild.mjs --production", - "test": "vitest run", - "lint": "eslint src tests", - "prepublishOnly": "node esbuild.mjs --production" - }, - "devDependencies": { - "@devcontainer-dev-certs/shared": "*", - "@peculiar/x509": "^2.0.0", - "@types/node": "^22.0.0", - "asn1js": "^3.0.10", - "commander": "^14.0.1", - "esbuild": "^0.28.0", - "pkijs": "^3.4.0", - "reflect-metadata": "^0.2.2", - "typescript": "^6.0.3", - "vitest": "^4.1.5" - } -} diff --git a/src/cli/src/bundle/writer.ts b/src/cli/src/bundle/writer.ts deleted file mode 100644 index 2f32933..0000000 --- a/src/cli/src/bundle/writer.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; - -/** - * Schema URL the manual-setup example references. Including `$schema` in the - * generated bundle.json lets editors like VS Code (and language servers like - * jsonls / coc-jsonls) pick up validation + autocomplete automatically. - */ -export const BUNDLE_SCHEMA_URL = - "https://raw.githubusercontent.com/dnegstad/devcontainer-dev-certs/main/schema/bundle.schema.json"; - -export interface BundleCertEntry { - /** Filename stem (e.g. `aspnetcore-dev`). */ - name: string; - /** SHA-1 thumbprint, uppercase hex, no separators. */ - thumbprint: string; - /** `dotnet-dev` (auto-generated) or `user` (user-supplied). */ - kind: "dotnet-dev" | "user"; - /** Host filesystem absolute path to the PFX, or null if not produced. */ - hostPfxPath: string | null; - /** Host filesystem absolute path to the PEM cert. */ - hostPemPath: string; - /** Host filesystem absolute path to the PEM key, or null if not produced. */ - hostPemKeyPath: string | null; - /** - * Whether the in-container installer should plant this cert into the OS - * trust store (CA bundle + .NET root) inside the container. `true` for - * default `dotnet-dev` certs. - */ - trustInContainer: boolean; -} - -export interface WriteBundleOptions { - /** Absolute path to the host out-dir holding the cert files. */ - hostOutDir: string; - /** - * Container-side path the host out-dir bind-mounts to (e.g. - * `/host-dev-certs`). Bundle paths are rewritten to this prefix because the - * in-container installer is what reads bundle.json — not anything on the - * host. - */ - containerMount: string; - entries: BundleCertEntry[]; - /** - * Extra destinations to write into bundle.json — directories inside the - * container that the installer will additionally drop artifacts into - * (e.g. `/etc/nginx/certs`). Optional; mirrors the existing schema field. - */ - extraDestinations?: Array<{ path: string; format?: string }>; -} - -/** - * Write `bundle.json` into the host out-dir. The on-disk JSON references - * the *container-side* paths (mount target + filename), because that file is - * consumed by the in-container `devcontainer-dev-certs-install` script — not - * by anything that sees the host filesystem. - */ -export function writeBundle(options: WriteBundleOptions): string { - const bundlePath = path.join(options.hostOutDir, "bundle.json"); - - const certs = options.entries.map((entry) => { - const obj: Record = { - name: entry.name, - kind: entry.kind, - thumbprint: entry.thumbprint, - pemPath: containerize(entry.hostPemPath, options), - trustInContainer: entry.trustInContainer, - }; - if (entry.hostPfxPath) { - obj.pfxPath = containerize(entry.hostPfxPath, options); - } - if (entry.hostPemKeyPath) { - obj.pemKeyPath = containerize(entry.hostPemKeyPath, options); - } - return obj; - }); - - const bundle: Record = { - $schema: BUNDLE_SCHEMA_URL, - certs, - }; - if (options.extraDestinations && options.extraDestinations.length > 0) { - bundle.extraDestinations = options.extraDestinations; - } - - fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + "\n", { - mode: 0o644, - }); - return bundlePath; -} - -/** - * Translate a host-filesystem absolute path under `hostOutDir` into the - * equivalent container-mount path. Paths outside the out-dir pass through - * unchanged — the user may have crafted bundle entries that point at - * already-in-container locations. - */ -function containerize(hostPath: string, options: WriteBundleOptions): string { - const resolved = path.resolve(hostPath); - const baseResolved = path.resolve(options.hostOutDir); - if (resolved.startsWith(baseResolved + path.sep) || resolved === baseResolved) { - const rel = path.relative(baseResolved, resolved); - if (!rel) return options.containerMount; - // Forward slashes always — the container is always POSIX even when the - // host is Windows. - const posixRel = rel.split(path.sep).join("/"); - const mount = options.containerMount.replace(/\/+$/, ""); - return `${mount}/${posixRel}`; - } - return hostPath; -} diff --git a/src/cli/src/commands/bundle.ts b/src/cli/src/commands/bundle.ts deleted file mode 100644 index 2582f07..0000000 --- a/src/cli/src/commands/bundle.ts +++ /dev/null @@ -1,154 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { - findSiblingKey, - loadPemPair, - loadPfx, -} from "@devcontainer-dev-certs/shared"; -import { - writeBundle, - type BundleCertEntry, -} from "../bundle/writer"; -import { DEFAULT_CONTAINER_MOUNT } from "../defaults"; - -export interface BundleCommandOptions { - outDir?: string; - containerMount?: string; - name?: string; - kind?: "dotnet-dev" | "user"; - noTrustInContainer?: boolean; -} - -/** - * `dcdc bundle ` — emit a `bundle.json` referencing an - * already-existing cert file. Useful when the cert was generated by - * something else (e.g. `dotnet dev-certs` invoked manually, or a corporate - * CA bundle) and the user just needs the wrapping bundle for the in-container - * installer to consume. - */ -export async function runBundle( - certPath: string, - options: BundleCommandOptions -): Promise { - if (!fs.existsSync(certPath)) { - throw new Error(`File not found: ${certPath}`); - } - - const resolvedCertPath = path.resolve(certPath); - const outDir = path.resolve(options.outDir ?? path.dirname(resolvedCertPath)); - const containerMount = options.containerMount ?? DEFAULT_CONTAINER_MOUNT; - const name = options.name ?? path.basename(resolvedCertPath, path.extname(resolvedCertPath)); - const kind = options.kind ?? "user"; - - const ext = resolvedCertPath.toLowerCase(); - let hostPfxPath: string | null = null; - let hostPemPath: string; - let hostPemKeyPath: string | null; - let thumbprint: string; - - const stem = path.join( - path.dirname(resolvedCertPath), - path.basename(resolvedCertPath, path.extname(resolvedCertPath)) - ); - - if (ext.endsWith(".pfx") || ext.endsWith(".p12")) { - hostPfxPath = resolvedCertPath; - const loaded = await loadPfx(resolvedCertPath); - thumbprint = loaded.cert.thumbprintSha1; - // Look for sibling PEM cert + key. PEM cert is required (the - // in-container installer plants `{name}.pem` into the trust dir); - // PEM key is optional but needed for `pem-bundle` / `key` extra - // destination formats. - const candidatePem = `${stem}.pem`; - if (fs.existsSync(candidatePem)) { - hostPemPath = candidatePem; - } else { - throw new Error( - `Bundle requires a PEM cert next to the PFX. Looked for ${candidatePem}.` - ); - } - hostPemKeyPath = findSiblingKey(candidatePem); - } else { - hostPemPath = resolvedCertPath; - hostPemKeyPath = findSiblingKey(resolvedCertPath); - const loaded = loadPemPair(resolvedCertPath, hostPemKeyPath); - thumbprint = loaded.cert.thumbprintSha1; - // Sibling PKCS#12: accept either extension. Openssl writes `.p12` - // by default (`openssl pkcs12 -export -out ...`); the .NET tooling - // and our own exporter write `.pfx`. Probing only one would silently - // miss the other. - for (const pfxExt of [".pfx", ".p12"]) { - const candidate = `${stem}${pfxExt}`; - if (fs.existsSync(candidate)) { - hostPfxPath = candidate; - break; - } - } - } - - const entry: BundleCertEntry = { - name, - kind, - thumbprint, - hostPfxPath, - hostPemPath, - hostPemKeyPath, - trustInContainer: !options.noTrustInContainer, - }; - - fs.mkdirSync(outDir, { recursive: true }); - warnOnOutOfBundleDirPaths(entry, outDir, containerMount); - const bundlePath = writeBundle({ - hostOutDir: outDir, - containerMount, - entries: [entry], - }); - process.stderr.write(`Bundle: ${bundlePath}\n`); - process.stderr.write(`Thumbprint: ${thumbprint}\n`); -} - -/** - * Warn when a cert file referenced by the bundle is NOT under `outDir`. - * The writer only rewrites paths under `outDir` to the container mount; - * paths outside are left verbatim, which means the in-container - * installer will try to read them at their host-filesystem location — - * something it can only do if the user has also bind-mounted that - * location into the container. The vast majority of the time they - * haven't, and a silently-broken bundle is worse than a noisy one. - */ -function warnOnOutOfBundleDirPaths( - entry: BundleCertEntry, - outDir: string, - containerMount: string -): void { - const candidates: Array<[string, string]> = []; - if (entry.hostPfxPath) candidates.push(["pfxPath", entry.hostPfxPath]); - candidates.push(["pemPath", entry.hostPemPath]); - if (entry.hostPemKeyPath) - candidates.push(["pemKeyPath", entry.hostPemKeyPath]); - - const resolvedOutDir = path.resolve(outDir); - const outsideEntries = candidates.filter(([, p]) => { - const resolved = path.resolve(p); - return !( - resolved === resolvedOutDir || - resolved.startsWith(resolvedOutDir + path.sep) - ); - }); - - if (outsideEntries.length === 0) return; - - process.stderr.write( - `[warn] Cert files reference paths outside --out-dir (${resolvedOutDir}):\n` - ); - for (const [field, p] of outsideEntries) { - process.stderr.write(` ${field}: ${p}\n`); - } - process.stderr.write( - ` The bundle references absolute host paths that the in-container\n` + - ` installer will read literally. If you only bind-mount ${resolvedOutDir}\n` + - ` to ${containerMount}, those paths won't resolve inside the container.\n` + - ` Either copy the cert files into --out-dir and re-run, or arrange\n` + - ` additional mounts so the referenced paths exist container-side.\n` - ); -} diff --git a/src/cli/src/commands/doctor.ts b/src/cli/src/commands/doctor.ts deleted file mode 100644 index 07580ae..0000000 --- a/src/cli/src/commands/doctor.ts +++ /dev/null @@ -1,243 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { - createPlatformStore, - describeAutoBackend, - DotnetBackend, - resolveSafeExecPath, - runProcess, -} from "@devcontainer-dev-certs/shared"; -import { DEFAULT_OUT_DIR } from "../defaults"; -import { installCliLogger } from "../logger"; - -export interface DoctorCommandOptions { - outDir?: string; - verbose?: boolean; -} - -interface Check { - label: string; - status: "ok" | "warn" | "fail"; - detail: string; -} - -/** - * `dcdc doctor` — read-only diagnostics: which backends are available, what - * `--backend auto` would pick, host trust-store state for the cert (if any) - * in the out-dir. Mirrors the in-container `devcontainer-dev-certs-install - * --doctor` ergonomics: every check produces an `[ok]` / `[warn]` / `[fail]` - * line. - */ -export async function runDoctor( - options: DoctorCommandOptions -): Promise { - installCliLogger(Boolean(options.verbose)); - - const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); - - // Probe dotnet ONCE. The backend-availability and auto-resolution - // lines both depend on this, and `describeAutoBackend()` used to - // spawn its own `dotnet --version` — two spawns per doctor run on - // macOS where one suffices. - const dotnetAvailable = await new DotnetBackend().isAvailable(); - - // All remaining check groups are independent — no group reads state - // another group writes — so they run concurrently. Each returns - // `Check[]` so the final output preserves a stable order regardless - // of which Promise settles first. - const [backendChecks, outDirChecks, storeChecks, toolChecks] = - await Promise.all([ - checkBackends(dotnetAvailable), - Promise.resolve(checkOutDir(outDir)), - checkPlatformStore(), - checkPlatformTools(), - ]); - - const checks: Check[] = [ - ...backendChecks, - ...outDirChecks, - ...storeChecks, - ...toolChecks, - ]; - - let failures = 0; - let warnings = 0; - for (const c of checks) { - process.stdout.write(`[${c.status}] ${c.label}: ${c.detail}\n`); - if (c.status === "fail") failures++; - else if (c.status === "warn") warnings++; - } - process.stdout.write( - `\n${checks.length} check(s) total — ${failures} fail, ${warnings} warn.\n` - ); - - if (failures > 0) { - process.exitCode = 1; - } -} - -async function checkBackends(dotnetAvailable: boolean): Promise { - const auto = await describeAutoBackend(dotnetAvailable); - return [ - { - label: "dotnet CLI on PATH", - status: dotnetAvailable ? "ok" : "warn", - detail: dotnetAvailable - ? "found" - : "not found (the 'dotnet' backend is unavailable; native backend will be used)", - }, - { - label: "--backend auto would pick", - status: "ok", - detail: auto, - }, - ]; -} - -function checkOutDir(outDir: string): Check[] { - const bundlePath = path.join(outDir, "bundle.json"); - return [ - fs.existsSync(outDir) - ? { label: `out-dir ${outDir}`, status: "ok", detail: "exists" } - : { - label: `out-dir ${outDir}`, - status: "warn", - detail: "does not exist (run `dcdc generate` to create it)", - }, - fs.existsSync(bundlePath) - ? { - label: `bundle.json at ${bundlePath}`, - status: "ok", - detail: "found", - } - : { - label: `bundle.json at ${bundlePath}`, - status: "warn", - detail: "not found", - }, - ]; -} - -async function checkPlatformStore(): Promise { - try { - const store = await createPlatformStore(); - const status = await store.checkStatus(); - if (!status.exists) { - return [ - { - label: "Host platform store has a valid dev cert", - status: "warn", - detail: "no dev cert found in host platform store", - }, - ]; - } - return [ - { - label: "Host platform store has a valid dev cert", - status: status.isTrusted ? "ok" : "warn", - detail: status.isTrusted - ? `trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})` - : `present but NOT trusted (thumbprint ${status.thumbprint}, expires ${status.notAfter})`, - }, - ]; - } catch (err: unknown) { - const message = err instanceof Error ? err.message : String(err); - return [ - { - label: "Host platform store check", - status: "fail", - detail: message, - }, - ]; - } -} - -/** - * Per-OS tool presence checks. Each backend / trust path depends on a - * different set of external commands, so the checks branch by - * `process.platform`. Linux has the most fan-out (separate tools for - * the OpenSSL trust dir and the NSS browser DB); macOS and Windows - * each have a small canonical set. - * - * On Windows we resolve via `resolveSafeExecPath` rather than shelling - * to `where.exe` — same lookup as `runProcess`, no spawn overhead, no - * risk of `where.exe` itself being hijacked. - */ -async function checkPlatformTools(): Promise { - if (process.platform === "linux") return checkLinuxTools(); - if (process.platform === "darwin") return checkMacosTools(); - if (process.platform === "win32") return checkWindowsTools(); - return []; -} - -async function checkLinuxTools(): Promise { - const checks: Check[] = []; - - const openssl = await runProcess("which", ["openssl"]); - checks.push({ - label: "openssl on PATH (Linux native trust)", - status: openssl.exitCode === 0 ? "ok" : "warn", - detail: openssl.exitCode === 0 ? openssl.stdout.trim() : "not found", - }); - - const certutil = await runProcess("which", ["certutil"]); - checks.push({ - label: "certutil on PATH (Linux NSS browser trust)", - status: certutil.exitCode === 0 ? "ok" : "warn", - detail: - certutil.exitCode === 0 - ? certutil.stdout.trim() - : "not found (Chromium/Firefox won't auto-trust; install libnss3-tools / nss-tools)", - }); - - return checks; -} - -async function checkMacosTools(): Promise { - const checks: Check[] = []; - - // `security` is the keychain CLI. macStore uses it for trust and - // enumeration; without it the native backend can't run on macOS. - // It's part of the base OS install at /usr/bin/security, so a - // missing entry usually means PATH has been pruned aggressively. - const security = await runProcess("which", ["security"]); - checks.push({ - label: "security on PATH (macOS keychain trust)", - status: security.exitCode === 0 ? "ok" : "warn", - detail: - security.exitCode === 0 - ? security.stdout.trim() - : "not found (native backend cannot drive the keychain — usually means PATH was stripped)", - }); - - return checks; -} - -function checkWindowsTools(): Check[] { - const checks: Check[] = []; - - // windowsStore prefers `pwsh` (PowerShell 7+) but falls back to - // `powershell` (PowerShell 5.1). At least one must be findable. - const pwsh = resolveSafeExecPath("pwsh"); - const powershell = resolveSafeExecPath("powershell"); - const psFound = pwsh ?? powershell; - checks.push({ - label: "pwsh or powershell on PATH (Windows store enumeration)", - status: psFound !== null ? "ok" : "warn", - detail: - psFound !== null - ? `${psFound}${pwsh === null ? " (PowerShell 5.1; pwsh 7+ preferred but not required)" : ""}` - : "not found (Windows store enumeration / cleanup will fail)", - }); - - const certutilExe = resolveSafeExecPath("certutil.exe"); - checks.push({ - label: "certutil.exe on PATH (Windows trust store)", - status: certutilExe !== null ? "ok" : "warn", - detail: - certutilExe ?? - "not found (native trust step will fail — usually means PATH was stripped)", - }); - - return checks; -} diff --git a/src/cli/src/commands/generate.ts b/src/cli/src/commands/generate.ts deleted file mode 100644 index 84a3817..0000000 --- a/src/cli/src/commands/generate.ts +++ /dev/null @@ -1,119 +0,0 @@ -import * as path from "path"; -import { - createPlatformStore, - selectBackend, - type BackendMode, -} from "@devcontainer-dev-certs/shared"; -import { writeBundle, type BundleCertEntry } from "../bundle/writer"; -import { DEFAULT_CONTAINER_MOUNT, DEFAULT_OUT_DIR } from "../defaults"; -import { installCliLogger } from "../logger"; -import { stderrNssTrustReporter } from "../nssReporter"; - -export interface GenerateCommandOptions { - outDir?: string; - backend?: BackendMode; - noTrust?: boolean; - containerMount?: string; - noBundle?: boolean; - verbose?: boolean; -} - -/** - * `dcdc generate` — ensure a dev cert exists on the host and emit its - * artifacts + a `bundle.json` for the in-container installer. If the - * host platform store already has a valid trusted dev cert, the backend - * reuses it (no fresh generation, no re-trust prompt) and we say so on - * stderr so the user knows what happened. With `--no-trust`, the native - * backend bypasses the platform store entirely and always produces a - * fresh in-memory cert. - */ -export async function runGenerate( - options: GenerateCommandOptions -): Promise { - installCliLogger(Boolean(options.verbose)); - - const outDir = path.resolve(options.outDir ?? DEFAULT_OUT_DIR); - const backend = await selectBackend(options.backend ?? "auto"); - const containerMount = options.containerMount ?? DEFAULT_CONTAINER_MOUNT; - const noTrust = Boolean(options.noTrust); - - process.stderr.write(`Backend: ${backend.kind}\n`); - process.stderr.write(`Out dir: ${outDir}\n`); - - // Snapshot the platform store BEFORE the backend runs so we can - // tell the user whether the backend reused an existing cert or - // generated a fresh one. Best-effort: if the store check throws - // (corrupt state, permission issue), we skip the diagnostic rather - // than fail the generate. Skipped entirely for `--no-trust` since - // the native backend doesn't touch the store on that path. - const preExistingThumbprint = noTrust - ? null - : await safelyReadStoreThumbprint(); - - const result = await backend.generate({ - outDir, - noTrust, - // Surface NSS trust outcomes (Linux only) on stderr so the user - // isn't left thinking browser trust succeeded when it silently - // didn't. No-op on macOS / Windows. - linuxNssTrustReporter: stderrNssTrustReporter, - }); - - const reused = - preExistingThumbprint !== null && - preExistingThumbprint === result.thumbprint; - - process.stderr.write( - `Cert source: ${certSourceLabel(reused, noTrust)}\n` + - `Thumbprint: ${result.thumbprint}\n` + - `PFX: ${result.pfxPath}\n` + - `PEM: ${result.pemPath}\n` + - (result.pemKeyPath ? `Key: ${result.pemKeyPath}\n` : "") + - `Trusted on host: ${result.trusted ? "yes" : "no (skipped via --no-trust)"}\n` - ); - - if (!options.noBundle) { - const entry: BundleCertEntry = { - name: "aspnetcore-dev", - kind: "dotnet-dev", - thumbprint: result.thumbprint, - hostPfxPath: result.pfxPath, - hostPemPath: result.pemPath, - hostPemKeyPath: result.pemKeyPath, - // Mirror the host-trust opt-out: if the user passed `--no-trust`, - // they want files-only on both sides of the host/container - // boundary. Forcing `trustInContainer: true` here would honor the - // host opt-out and silently reverse it inside the container — an - // asymmetry that bites anyone who commits the bundle to a repo. - trustInContainer: !noTrust, - }; - const bundlePath = writeBundle({ - hostOutDir: outDir, - containerMount, - entries: [entry], - }); - process.stderr.write(`Bundle: ${bundlePath}\n`); - } -} - -async function safelyReadStoreThumbprint(): Promise { - try { - const store = await createPlatformStore(); - const status = await store.checkStatus(); - return status.exists ? status.thumbprint : null; - } catch { - // Store reads aren't load-bearing for the generate flow — we're - // only using them to decorate the output. Don't block. - return null; - } -} - -function certSourceLabel(reused: boolean, noTrust: boolean): string { - if (reused) { - return "reused (existing trusted cert already in the host platform store)"; - } - if (noTrust) { - return "newly generated (in memory; --no-trust skips platform-store write)"; - } - return "newly generated (added to the host platform store)"; -} diff --git a/src/cli/src/commands/inspect.ts b/src/cli/src/commands/inspect.ts deleted file mode 100644 index 13f3eba..0000000 --- a/src/cli/src/commands/inspect.ts +++ /dev/null @@ -1,174 +0,0 @@ -import * as fs from "fs"; -import { - type DevCert, - ASPNET_HTTPS_OID, - CURRENT_CERTIFICATE_VERSION, - MINIMUM_CERTIFICATE_VERSION, - findSiblingKey, - getCertificateVersion, - isValidDevCert, - loadPfx, - loadPemPair, - validateLocalSans, - collectSanEntries, -} from "@devcontainer-dev-certs/shared"; - -export interface InspectCommandOptions { - json?: boolean; -} - -interface InspectReport { - path: string; - format: "pfx" | "pem"; - subjectCN: string | null; - thumbprintSha1: string; - thumbprintSha256: string; - notBefore: string; - notAfter: string; - expiresInDays: number; - hasPrivateKey: boolean; - devCertOidPresent: boolean; - devCertVersion: number | null; - isValidDevCert: boolean; - sans: Array<{ type: string; value: string }>; - nonLocalSans: Array<{ type: string; value: string }>; - warnings: string[]; -} - -/** - * `dcdc inspect ` — load a PFX or PEM (cert-only) and report its - * vital statistics. Text by default; `--json` switches to machine-readable. - */ -export async function runInspect( - certPath: string, - options: InspectCommandOptions -): Promise { - if (!fs.existsSync(certPath)) { - throw new Error(`File not found: ${certPath}`); - } - - const report = await buildReport(certPath); - if (options.json) { - process.stdout.write(JSON.stringify(report, null, 2) + "\n"); - } else { - process.stdout.write(formatTextReport(report)); - } -} - -async function buildReport(certPath: string): Promise { - const ext = certPath.toLowerCase(); - const warnings: string[] = []; - - let cert: DevCert; - let hasPrivateKey: boolean; - let format: "pfx" | "pem"; - - if (ext.endsWith(".pfx") || ext.endsWith(".p12")) { - format = "pfx"; - const loaded = await loadPfx(certPath); - cert = loaded.cert; - hasPrivateKey = loaded.key !== null; - } else { - format = "pem"; - // PEM inspection: opportunistically look for a sibling key. Two - // conventions are in the wild — `stem.key` (our exporter + openssl) - // and `filename.pem.key` (`dotnet dev-certs --format PEM - // --export-path foo.pem` writes `foo.pem.key`). Probe both so - // dotnet-generated key pairs aren't misreported as cert-only. - const keyPath = findSiblingKey(certPath); - const loaded = loadPemPair(certPath, keyPath); - cert = loaded.cert; - hasPrivateKey = loaded.key !== null; - } - - const devCertOidPresent = cert.hasExtension(ASPNET_HTTPS_OID); - const devCertVersion = devCertOidPresent ? getCertificateVersion(cert) : null; - const sansAll = collectSanEntries(cert).map((entry) => ({ - type: entry.type, - value: entry.value, - })); - const localCheck = validateLocalSans(cert); - - if (devCertOidPresent && devCertVersion !== null) { - if (devCertVersion < MINIMUM_CERTIFICATE_VERSION) { - warnings.push( - `Dev-cert version byte ${devCertVersion} is below the minimum (${MINIMUM_CERTIFICATE_VERSION}). Regenerate with a current dotnet SDK.` - ); - } - if (devCertVersion > CURRENT_CERTIFICATE_VERSION) { - warnings.push( - `Dev-cert version byte ${devCertVersion} is newer than this build expects (${CURRENT_CERTIFICATE_VERSION}). The cert may use features we don't know about.` - ); - } - } - if (!hasPrivateKey && format === "pfx") { - warnings.push("PFX contains no private key — Kestrel will not be able to serve TLS from this file."); - } - if (localCheck.nonLocalEntries.length > 0) { - warnings.push( - `${localCheck.nonLocalEntries.length} non-local SAN entr${localCheck.nonLocalEntries.length === 1 ? "y" : "ies"} present — this cert grants TLS to names beyond the developer machine.` - ); - } - - const now = Date.now(); - const expiresInDays = Math.floor( - (cert.notAfter.getTime() - now) / 86400_000 - ); - - return { - path: certPath, - format, - subjectCN: cert.subjectCN ?? null, - thumbprintSha1: cert.thumbprintSha1, - thumbprintSha256: cert.thumbprint, - notBefore: cert.notBefore.toISOString(), - notAfter: cert.notAfter.toISOString(), - expiresInDays, - hasPrivateKey, - devCertOidPresent, - devCertVersion, - isValidDevCert: isValidDevCert(cert), - sans: sansAll, - nonLocalSans: localCheck.nonLocalEntries.map((entry) => ({ - type: entry.type, - value: entry.value, - })), - warnings, - }; -} - -function formatTextReport(report: InspectReport): string { - const lines: string[] = []; - lines.push(`File: ${report.path}`); - lines.push(`Format: ${report.format.toUpperCase()}`); - lines.push(`Subject CN: ${report.subjectCN ?? "(none)"}`); - lines.push(`Thumbprint (SHA-1): ${report.thumbprintSha1}`); - lines.push(`Thumbprint (SHA-256): ${report.thumbprintSha256}`); - lines.push(`Valid from: ${report.notBefore}`); - lines.push(`Valid until: ${report.notAfter}`); - lines.push(`Expires in: ${report.expiresInDays} day(s)`); - lines.push(`Has private key: ${report.hasPrivateKey ? "yes" : "no"}`); - lines.push(`ASP.NET dev-cert OID: ${report.devCertOidPresent ? "yes" : "no"}`); - if (report.devCertVersion !== null) { - lines.push(` Version byte: ${report.devCertVersion}`); - } - lines.push(`Valid as dev cert: ${report.isValidDevCert ? "yes" : "no"}`); - lines.push(`SANs:`); - if (report.sans.length === 0) { - lines.push(` (none)`); - } else { - for (const san of report.sans) { - const flag = report.nonLocalSans.some( - (n) => n.type === san.type && n.value === san.value - ) - ? " [non-local]" - : ""; - lines.push(` ${san.type}:${san.value}${flag}`); - } - } - if (report.warnings.length > 0) { - lines.push(`Warnings:`); - for (const w of report.warnings) lines.push(` - ${w}`); - } - return lines.join("\n") + "\n"; -} diff --git a/src/cli/src/commands/trust.ts b/src/cli/src/commands/trust.ts deleted file mode 100644 index e8b04d3..0000000 --- a/src/cli/src/commands/trust.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as fs from "fs"; -import { - createPlatformStore, - loadPfx, - loadPemPair, - type DevCert, -} from "@devcontainer-dev-certs/shared"; -import { installCliLogger } from "../logger"; -import { stderrNssTrustReporter } from "../nssReporter"; - -export interface TrustCommandOptions { - verbose?: boolean; -} - -/** - * `dcdc trust ` — add an existing cert to the host's OS trust - * store. Useful when the user already has a cert (generated elsewhere) and - * just needs the host trust step. Goes through the shared - * `PlatformCertificateStore.trustCertificate` — same hook the host - * extension uses, including the Linux NSS browser-trust step (whose - * outcome is reported on stderr so failures don't pass silently). - */ -export async function runTrust( - certPath: string, - options: TrustCommandOptions -): Promise { - installCliLogger(Boolean(options.verbose)); - - if (!fs.existsSync(certPath)) { - throw new Error(`File not found: ${certPath}`); - } - - let cert: DevCert; - const lower = certPath.toLowerCase(); - if (lower.endsWith(".pfx") || lower.endsWith(".p12")) { - const loaded = await loadPfx(certPath); - cert = loaded.cert; - } else { - const loaded = loadPemPair(certPath); - cert = loaded.cert; - } - - const store = await createPlatformStore({ - linuxNssTrustReporter: stderrNssTrustReporter, - }); - - if (await store.isCertTrusted(cert)) { - process.stderr.write( - `Certificate ${cert.thumbprintSha1} is already trusted on this host; nothing to do.\n` - ); - return; - } - - process.stderr.write( - `Trusting certificate ${cert.thumbprintSha1} on this host...\n` - ); - await store.trustCertificate(cert); - process.stderr.write("Trust step complete.\n"); -} diff --git a/src/cli/src/defaults.ts b/src/cli/src/defaults.ts deleted file mode 100644 index a4cd357..0000000 --- a/src/cli/src/defaults.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as os from "os"; -import * as path from "path"; - -/** - * CLI-wide defaults. Consolidated here so a future change to either - * value lands in one place — previously the out-dir and container-mount - * defaults were duplicated across `generate`, `bundle`, and `doctor`, - * with no compiler help to keep them in sync. - */ - -/** Host directory the CLI writes cert artifacts + `bundle.json` into. */ -export const DEFAULT_OUT_DIR = path.join(os.homedir(), ".dev-certs"); - -/** - * Container-side mount target the host out-dir is expected to be - * bind-mounted to. Recorded into `bundle.json`'s `pfxPath` / `pemPath` - * so the in-container installer reads from the right place. Users with - * a custom mount layout can override per-invocation via - * `--container-mount`. - */ -export const DEFAULT_CONTAINER_MOUNT = "/host-dev-certs"; diff --git a/src/cli/src/index.ts b/src/cli/src/index.ts deleted file mode 100644 index c4bc414..0000000 --- a/src/cli/src/index.ts +++ /dev/null @@ -1,183 +0,0 @@ -import "reflect-metadata"; -import { Command, Option } from "commander"; -import { runBundle } from "./commands/bundle"; -import { runDoctor } from "./commands/doctor"; -import { runGenerate } from "./commands/generate"; -import { runInspect } from "./commands/inspect"; -import { runTrust } from "./commands/trust"; -import type { BackendMode } from "@devcontainer-dev-certs/shared"; - -const program = new Command(); - -program - .name("dcdc") - .description( - "Host-side dev-cert toolkit. Generates, inspects, trusts, and bundles " + - "ASP.NET-compatible HTTPS dev certs for use with dev containers — without " + - "VS Code.\n\n" + - "Typical workflows:\n" + - " dcdc generate Ensure the host dev cert exists and is\n" + - " trusted; emit files + bundle.json.\n" + - " dcdc inspect ./cert.pfx Read a cert file (subject, thumbprint,\n" + - " SANs, validity).\n" + - " dcdc bundle ./cert.pfx Wrap an existing cert file in a\n" + - " bundle.json for the in-container installer.\n" + - " dcdc trust ./cert.pem Add an existing cert to the host's OS\n" + - " trust store ONLY (does not import to the\n" + - " .NET dev cert store — see notes below).\n" + - " dcdc doctor Read-only diagnostics.\n\n" + - "Mapping to `dotnet dev-certs https` (for reference):\n" + - " dotnet dev-certs https --trust ≈ dcdc generate\n" + - " dotnet dev-certs https ≈ dcdc generate --no-trust\n" + - " dotnet dev-certs https --check ≈ dcdc doctor\n" + - " dotnet dev-certs https --import F --trust no exact equivalent — `dcdc\n" + - " trust F` does the OS trust\n" + - " step only, NOT the .NET store\n" + - " import. Use the dotnet CLI\n" + - " directly if you need both.\n" + - " dotnet dev-certs https --export-path F ≈ dcdc generate --out-dir \n" + - " --no-trust" - ); - -program - .command("generate") - .description( - "Ensure the host dev cert exists and is trusted, then write its files " + - "(PFX/PEM/key) + bundle.json. Reuses an existing trusted cert in the host " + - "platform store when one is present (no re-prompt, no fresh key generation). " + - "Roughly equivalent to `dotnet dev-certs https --trust` plus our bundle.json " + - "write." - ) - .option("-o, --out-dir ", "Directory to write artifacts to (default ~/.dev-certs).") - .addOption( - new Option("-b, --backend ", "Backend selection.") - .choices(["auto", "native", "dotnet"]) - .default("auto") - ) - .option("--no-trust", "Skip the host trust step (PFX / PEM are still emitted).") - .option( - "--container-mount ", - "Container-side mount target for the out-dir, recorded into bundle.json.", - "/host-dev-certs" - ) - .option("--no-bundle", "Skip emitting bundle.json.") - .option("-v, --verbose", "Stream shared-layer log lines to stderr.") - .action( - async (opts: { - outDir?: string; - backend: BackendMode; - trust: boolean; - containerMount: string; - bundle: boolean; - verbose?: boolean; - }) => { - await runGenerate({ - outDir: opts.outDir, - backend: opts.backend, - // commander inverts `--no-trust` into `opts.trust = false`. - noTrust: !opts.trust, - containerMount: opts.containerMount, - noBundle: !opts.bundle, - verbose: opts.verbose, - }); - } - ); - -program - .command("inspect ") - .description( - "Print details about a PFX or PEM certificate (subject CN, thumbprints, " + - "validity, SANs, dev-cert OID + version byte, warnings). Read-only — " + - "doesn't touch the platform store, doesn't add trust." - ) - .option("--json", "Emit machine-readable JSON instead of human-readable text.") - .action(async (certPath: string, opts: { json?: boolean }) => { - await runInspect(certPath, { json: opts.json }); - }); - -program - .command("bundle ") - .description( - "Wrap an already-existing cert file in a bundle.json that the in-container " + - "installer reads. Auto-discovers sibling `.pem` / `.key` / `.pfx` files by " + - "naming convention. Doesn't touch the cert or the platform store — only " + - "emits the JSON manifest." - ) - .option( - "-o, --out-dir ", - "Directory to write bundle.json to (default: directory of cert-path)." - ) - .option( - "--container-mount ", - "Container-side mount target for the out-dir.", - "/host-dev-certs" - ) - .option( - "--name ", - "Filename stem to use in bundle.json (default: cert-path's basename without extension)." - ) - .addOption( - new Option("--kind ", "Bundle entry kind.") - .choices(["dotnet-dev", "user"]) - .default("user") - ) - .option( - "--no-trust-in-container", - "Mark trustInContainer=false (cert is served only, not added to trust stores)." - ) - .action( - async ( - certPath: string, - opts: { - outDir?: string; - containerMount: string; - name?: string; - kind: "dotnet-dev" | "user"; - trustInContainer: boolean; - } - ) => { - await runBundle(certPath, { - outDir: opts.outDir, - containerMount: opts.containerMount, - name: opts.name, - kind: opts.kind, - noTrustInContainer: !opts.trustInContainer, - }); - } - ); - -program - .command("trust ") - .description( - "Add an existing PFX or PEM cert to the host's OS trust store (macOS " + - "keychain / Windows CurrentUser\\Root / Linux OpenSSL trust dir + NSS DBs). " + - "ONLY adds trust — does NOT register the cert as the host .NET dev cert " + - "(that's what `dotnet dev-certs --import` does; if you need both, run the " + - "dotnet CLI). Short-circuits if the cert is already trusted." - ) - .option("-v, --verbose", "Stream shared-layer log lines to stderr.") - .action(async (certPath: string, opts: { verbose?: boolean }) => { - await runTrust(certPath, { verbose: opts.verbose }); - }); - -program - .command("doctor") - .description( - "Read-only diagnostics: which backends are available (and which `--backend " + - "auto` would pick), out-dir / bundle.json presence, host platform-store " + - "cert state (present? trusted? thumbprint?), and per-OS tool presence " + - "(openssl/certutil on Linux, security on macOS, pwsh/powershell + " + - "certutil.exe on Windows). Doesn't modify anything." - ) - .option("-o, --out-dir ", "Out-dir to inspect (default ~/.dev-certs).") - .option("-v, --verbose", "Stream shared-layer log lines to stderr.") - .action(async (opts: { outDir?: string; verbose?: boolean }) => { - await runDoctor({ outDir: opts.outDir, verbose: opts.verbose }); - }); - -// Run. -program.parseAsync(process.argv).catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err); - process.stderr.write(`dcdc: ${message}\n`); - process.exit(1); -}); diff --git a/src/cli/src/logger.ts b/src/cli/src/logger.ts deleted file mode 100644 index 6ca8b78..0000000 --- a/src/cli/src/logger.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { setLogSink, type LogSink } from "@devcontainer-dev-certs/shared"; - -/** - * Wires a console-backed sink into the shared logger so the platform / cert - * layer's `log()` calls surface during CLI runs. Verbose mode forwards every - * line to stderr (so stdout stays clean for `--json` / scripting use); quiet - * mode swallows them entirely. - */ -export function installCliLogger(verbose: boolean): void { - if (!verbose) { - setLogSink(undefined); - return; - } - const sink: LogSink = { - appendLine(message: string): void { - process.stderr.write(`${message}\n`); - }, - }; - setLogSink(sink); -} diff --git a/src/cli/src/nssReporter.ts b/src/cli/src/nssReporter.ts deleted file mode 100644 index 5705019..0000000 --- a/src/cli/src/nssReporter.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { LinuxNssTrustReporter } from "@devcontainer-dev-certs/shared"; - -/** - * Stderr-backed NSS trust reporter for the CLI surfaces. Wire this into - * any backend call that runs the trust step (`dcdc generate`, - * `dcdc trust`) so Linux NSS browser-trust failures don't pass silently — - * the user gets one line that names the trust dir / NSS DB and tells - * them what to install if `certutil` was missing. - * - * No-op on macOS and Windows (the shared layer never calls this on - * non-Linux platforms; this is a defensive belt-and-suspenders). - */ -export const stderrNssTrustReporter: LinuxNssTrustReporter = (result, pemPath) => { - if (result.success) { - process.stderr.write( - `Linux NSS browser trust: ok (${result.message}; cert at ${pemPath})\n` - ); - return; - } - process.stderr.write( - `Linux NSS browser trust: WARN (${result.message}; cert at ${pemPath})\n` + - ` Firefox / Chromium may not trust the cert until this is resolved.\n` + - ` If certutil is missing, install libnss3-tools (Debian / Ubuntu)\n` + - ` or nss-tools (Fedora / RHEL) and re-run.\n` - ); -}; diff --git a/src/cli/tests/_helpers.ts b/src/cli/tests/_helpers.ts deleted file mode 100644 index e2cd971..0000000 --- a/src/cli/tests/_helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Shared test helpers for the CLI test suite. Keep this file small — - * anything that grows beyond a few utilities should move into a per- - * concern helper module to keep imports honest. - */ - -/** - * Override `process.platform` for the duration of a test. Returns a - * restore callback to call from `finally` (or `afterEach`) so the - * stub doesn't leak into sibling tests. Tolerant of platforms where - * the original descriptor is undefined. - * - * Note: Node makes `process.platform` non-writable but `configurable`, - * so `Object.defineProperty` is the supported route. If a future Node - * tightens this, the failure surfaces at the first test that calls - * `stubPlatform` rather than hiding in a side channel. - */ -export function stubPlatform(value: NodeJS.Platform): () => void { - const original = Object.getOwnPropertyDescriptor(process, "platform"); - Object.defineProperty(process, "platform", { value, configurable: true }); - return () => { - if (original) Object.defineProperty(process, "platform", original); - }; -} diff --git a/src/cli/tests/bundle.test.ts b/src/cli/tests/bundle.test.ts deleted file mode 100644 index b516b25..0000000 --- a/src/cli/tests/bundle.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - afterEach, - vi, -} from "vitest"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import { generateCertificate, exportPfx, exportPem, VALIDITY_DAYS } from "@devcontainer-dev-certs/shared"; -import { runBundle } from "../src/commands/bundle"; - -/** - * `dcdc bundle` is supposed to flag the silent-broken-bundle case: cert - * files referenced by the bundle live outside the `--out-dir`, so the - * containerize step in the writer leaves their absolute host paths - * verbatim — and the in-container installer (which only ever sees the - * mount target, not the host filesystem) will fail to read them. - * - * These tests drive the warning by spying on stderr and asserting on - * what the bundle command wrote there. - */ - -async function makeCertFilesIn(dir: string): Promise { - const now = new Date(); - const expiry = new Date(now.getTime() + VALIDITY_DAYS * 86400_000); - const { cert, key } = await generateCertificate(now, expiry); - await exportPfx(cert, key, dir); - exportPem(cert, key, dir); -} - -const cleanupDirs: string[] = []; - -beforeEach(() => { - vi.spyOn(process.stderr, "write").mockImplementation(() => true); -}); - -afterEach(() => { - vi.restoreAllMocks(); - for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); - cleanupDirs.length = 0; -}); - -function collectStderr(): string { - const writeMock = vi.mocked(process.stderr.write); - return writeMock.mock.calls.map((c) => String(c[0])).join(""); -} - -describe("dcdc bundle out-of-dir warning", () => { - it("does not warn when cert files live inside --out-dir", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-in-")); - cleanupDirs.push(dir); - await makeCertFilesIn(dir); - - await runBundle(path.join(dir, "aspnetcore-dev.pfx"), { - outDir: dir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const stderr = collectStderr(); - expect(stderr).not.toContain("[warn]"); - expect(stderr).not.toContain("outside --out-dir"); - }); - - it("warns when cert files are NOT under --out-dir", async () => { - const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-cert-")); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-out-")); - cleanupDirs.push(certDir, outDir); - await makeCertFilesIn(certDir); - - await runBundle(path.join(certDir, "aspnetcore-dev.pfx"), { - outDir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const stderr = collectStderr(); - expect(stderr).toContain("[warn]"); - expect(stderr).toContain("outside --out-dir"); - // The warning should name the actual offending paths so the user - // can act on it without re-running with --verbose. - expect(stderr).toContain(path.join(certDir, "aspnetcore-dev.pfx")); - expect(stderr).toContain(path.join(certDir, "aspnetcore-dev.pem")); - }); - - it("flags every out-of-dir file, not just the first one", async () => { - const certDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-cert-multi-")); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-out-multi-")); - cleanupDirs.push(certDir, outDir); - await makeCertFilesIn(certDir); - - await runBundle(path.join(certDir, "aspnetcore-dev.pfx"), { - outDir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const stderr = collectStderr(); - // All three artifact fields (pfx, pem, key) live in certDir, so - // each should appear in the warning detail lines. - expect(stderr).toContain("pfxPath:"); - expect(stderr).toContain("pemPath:"); - expect(stderr).toContain("pemKeyPath:"); - }); - - it("does not warn for the cert path itself when it's the same as the out-dir base", async () => { - // Regression guard: a cert at exactly $OUT_DIR/cert.pfx and outDir - // = $OUT_DIR should not trip the "outside" check. - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-test-exact-")); - cleanupDirs.push(dir); - await makeCertFilesIn(dir); - - await runBundle(path.join(dir, "aspnetcore-dev.pfx"), { - outDir: dir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const stderr = collectStderr(); - expect(stderr).not.toContain("outside --out-dir"); - }); -}); - -describe("ddc bundle sibling-file discovery", () => { - it("finds a sibling .p12 (openssl convention) next to a .pem", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-p12-")); - cleanupDirs.push(dir); - await makeCertFilesIn(dir); - // Rename the .pfx → .p12 to simulate the openssl naming convention. - fs.renameSync( - path.join(dir, "aspnetcore-dev.pfx"), - path.join(dir, "aspnetcore-dev.p12") - ); - - await runBundle(path.join(dir, "aspnetcore-dev.pem"), { - outDir: dir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const bundle = JSON.parse( - fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") - ) as Record; - const cert = (bundle.certs as Array>)[0]; - expect(cert.pfxPath).toBe("/host-dev-certs/aspnetcore-dev.p12"); - }); - - it("finds a sibling key with the dotnet `.pem.key` naming", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-pemkey-")); - cleanupDirs.push(dir); - await makeCertFilesIn(dir); - // Simulate `dotnet dev-certs --format PEM --export-path foo.pem` - // by renaming aspnetcore-dev.key → aspnetcore-dev.pem.key. The - // .pfx is removed so the key naming is what the test exercises. - fs.renameSync( - path.join(dir, "aspnetcore-dev.key"), - path.join(dir, "aspnetcore-dev.pem.key") - ); - fs.unlinkSync(path.join(dir, "aspnetcore-dev.pfx")); - - await runBundle(path.join(dir, "aspnetcore-dev.pem"), { - outDir: dir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const bundle = JSON.parse( - fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") - ) as Record; - const cert = (bundle.certs as Array>)[0]; - expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.pem.key"); - }); - - it("prefers the stem-based key naming when both conventions exist", async () => { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-bundle-bothkeys-")); - cleanupDirs.push(dir); - await makeCertFilesIn(dir); - // Add a dotnet-style key alongside the our-convention one. Probe - // order favors the stem form so an our-exporter cert wins. - fs.copyFileSync( - path.join(dir, "aspnetcore-dev.key"), - path.join(dir, "aspnetcore-dev.pem.key") - ); - - await runBundle(path.join(dir, "aspnetcore-dev.pem"), { - outDir: dir, - containerMount: "/host-dev-certs", - kind: "user", - }); - - const bundle = JSON.parse( - fs.readFileSync(path.join(dir, "bundle.json"), "utf-8") - ) as Record; - const cert = (bundle.certs as Array>)[0]; - expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.key"); - }); -}); diff --git a/src/cli/tests/doctor.test.ts b/src/cli/tests/doctor.test.ts deleted file mode 100644 index 599304e..0000000 --- a/src/cli/tests/doctor.test.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - afterEach, - vi, -} from "vitest"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; - -import type * as Shared from "@devcontainer-dev-certs/shared"; - -// `runDoctor` exercises three different shared-module surfaces: -// `runProcess`, `resolveSafeExecPath`, and `createPlatformStore`. Mock -// all three so the tests don't depend on what's actually installed on -// the host machine. `describeAutoBackend` is left real but its dotnet -// probe goes through the same `runProcess` mock. -vi.mock("@devcontainer-dev-certs/shared", async () => { - const actual = await vi.importActual( - "@devcontainer-dev-certs/shared" - ); - return { - ...actual, - runProcess: vi.fn(), - resolveSafeExecPath: vi.fn(), - createPlatformStore: vi.fn(), - describeAutoBackend: vi.fn(), - }; -}); - -import { - createPlatformStore, - describeAutoBackend, - resolveSafeExecPath, - runProcess, -} from "@devcontainer-dev-certs/shared"; -import { runDoctor } from "../src/commands/doctor"; - -const mockedRunProcess = vi.mocked(runProcess); -const mockedResolveSafeExecPath = vi.mocked(resolveSafeExecPath); -const mockedCreatePlatformStore = vi.mocked(createPlatformStore); -const mockedDescribeAutoBackend = vi.mocked(describeAutoBackend); - -import { stubPlatform } from "./_helpers"; - -const cleanupDirs: string[] = []; - -function collectStdout(): string { - const writeMock = vi.mocked(process.stdout.write); - return writeMock.mock.calls.map((c) => String(c[0])).join(""); -} - -beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(process.stdout, "write").mockImplementation(() => true); - vi.spyOn(process.stderr, "write").mockImplementation(() => true); - - // Default stubs: dotnet not on PATH, platform store empty, auto picks - // native. Each test can override what it cares about. - mockedRunProcess.mockResolvedValue({ exitCode: 1, stdout: "", stderr: "" }); - mockedResolveSafeExecPath.mockReturnValue(null); - mockedDescribeAutoBackend.mockResolvedValue("native"); - mockedCreatePlatformStore.mockResolvedValue({ - checkStatus: vi.fn(async () => ({ - exists: false, - isTrusted: false, - thumbprint: null, - notBefore: null, - notAfter: null, - version: null, - })), - } as never); -}); - -afterEach(() => { - vi.restoreAllMocks(); - for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); - cleanupDirs.length = 0; - process.exitCode = 0; -}); - -describe("dcdc doctor — Linux", () => { - it("reports [ok] when openssl and certutil are both on PATH", async () => { - const restore = stubPlatform("linux"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-linux-")); - cleanupDirs.push(outDir); - - mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { - if (cmd === "which" && args[0] === "openssl") { - return { exitCode: 0, stdout: "/usr/bin/openssl\n", stderr: "" }; - } - if (cmd === "which" && args[0] === "certutil") { - return { exitCode: 0, stdout: "/usr/bin/certutil\n", stderr: "" }; - } - return { exitCode: 1, stdout: "", stderr: "" }; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[ok] openssl on PATH"); - expect(stdout).toContain("/usr/bin/openssl"); - expect(stdout).toContain("[ok] certutil on PATH"); - }); - - it("reports [warn] when certutil is missing", async () => { - const restore = stubPlatform("linux"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-linux-")); - cleanupDirs.push(outDir); - - mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { - if (cmd === "which" && args[0] === "openssl") { - return { exitCode: 0, stdout: "/usr/bin/openssl\n", stderr: "" }; - } - return { exitCode: 1, stdout: "", stderr: "" }; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[warn] certutil on PATH"); - expect(stdout).toContain("Chromium/Firefox won't auto-trust"); - }); -}); - -describe("dcdc doctor — macOS", () => { - it("checks for the `security` keychain CLI", async () => { - const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); - cleanupDirs.push(outDir); - - mockedRunProcess.mockImplementation(async (cmd: string, args: string[]) => { - if (cmd === "which" && args[0] === "security") { - return { exitCode: 0, stdout: "/usr/bin/security\n", stderr: "" }; - } - return { exitCode: 1, stdout: "", stderr: "" }; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[ok] security on PATH"); - expect(stdout).toContain("/usr/bin/security"); - }); - - it("does NOT run Linux-only checks on macOS", async () => { - const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); - cleanupDirs.push(outDir); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).not.toContain("openssl on PATH"); - expect(stdout).not.toContain("Linux NSS"); - }); - - it("warns when `security` isn't on PATH", async () => { - const restore = stubPlatform("darwin"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-mac-")); - cleanupDirs.push(outDir); - - // Default mockedRunProcess returns exit 1 for everything — including - // the `which security` probe. - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[warn] security on PATH"); - expect(stdout).toContain("native backend cannot drive the keychain"); - }); -}); - -describe("dcdc doctor — Windows", () => { - it("reports [ok] when both pwsh and certutil.exe resolve", async () => { - const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); - cleanupDirs.push(outDir); - - mockedResolveSafeExecPath.mockImplementation((cmd: string) => { - if (cmd === "pwsh") return "C:\\Program Files\\PowerShell\\7\\pwsh.exe"; - if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; - return null; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[ok] pwsh or powershell on PATH"); - expect(stdout).toContain("pwsh.exe"); - expect(stdout).toContain("[ok] certutil.exe on PATH"); - }); - - it("accepts powershell as a fallback when pwsh is absent, with a note", async () => { - const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); - cleanupDirs.push(outDir); - - mockedResolveSafeExecPath.mockImplementation((cmd: string) => { - if (cmd === "pwsh") return null; - if (cmd === "powershell") - return "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; - if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; - return null; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[ok] pwsh or powershell on PATH"); - expect(stdout).toContain("powershell.exe"); - expect(stdout).toContain("PowerShell 5.1"); - }); - - it("warns when neither pwsh nor powershell is found", async () => { - const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); - cleanupDirs.push(outDir); - - mockedResolveSafeExecPath.mockImplementation((cmd: string) => { - if (cmd === "certutil.exe") return "C:\\Windows\\System32\\certutil.exe"; - return null; // both pwsh and powershell missing - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[warn] pwsh or powershell on PATH"); - expect(stdout).toContain("Windows store enumeration / cleanup will fail"); - }); - - it("warns when certutil.exe is missing", async () => { - const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); - cleanupDirs.push(outDir); - - mockedResolveSafeExecPath.mockImplementation((cmd: string) => { - if (cmd === "pwsh") return "C:\\Program Files\\PowerShell\\7\\pwsh.exe"; - return null; - }); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - const stdout = collectStdout(); - expect(stdout).toContain("[warn] certutil.exe on PATH"); - expect(stdout).toContain("native trust step will fail"); - }); - - it("does NOT shell out to `which` on Windows (uses resolveSafeExecPath)", async () => { - const restore = stubPlatform("win32"); - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-doctor-win-")); - cleanupDirs.push(outDir); - - try { - await runDoctor({ outDir }); - } finally { - restore(); - } - - // `which` is not the right tool on Windows (`where.exe` is), and we - // explicitly skip it in favor of resolveSafeExecPath because the - // latter is the same lookup runProcess uses and avoids a redundant - // shell-out. Regression-guard the path: no `which` invocations. - const whichCalls = mockedRunProcess.mock.calls.filter( - ([cmd]) => cmd === "which" - ); - expect(whichCalls).toHaveLength(0); - }); -}); diff --git a/src/cli/tests/generate.test.ts b/src/cli/tests/generate.test.ts deleted file mode 100644 index 4be7ae3..0000000 --- a/src/cli/tests/generate.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - describe, - it, - expect, - beforeEach, - afterEach, - vi, -} from "vitest"; -import * as fs from "fs"; -import * as os from "os"; -import * as path from "path"; -import type * as Shared from "@devcontainer-dev-certs/shared"; - -vi.mock("@devcontainer-dev-certs/shared", async () => { - const actual = await vi.importActual( - "@devcontainer-dev-certs/shared" - ); - return { ...actual, selectBackend: vi.fn() }; -}); - -import { selectBackend } from "@devcontainer-dev-certs/shared"; -import { runGenerate } from "../src/commands/generate"; - -const mockedSelectBackend = vi.mocked(selectBackend); -const cleanupDirs: string[] = []; - -function fakeBackend(): Shared.Backend { - return { - kind: "native", - isAvailable: () => Promise.resolve(true), - generate: vi.fn(async (opts: Shared.GenerateOptions) => ({ - pfxPath: path.join(opts.outDir, "aspnetcore-dev.pfx"), - pemPath: path.join(opts.outDir, "aspnetcore-dev.pem"), - pemKeyPath: path.join(opts.outDir, "aspnetcore-dev.key"), - thumbprint: "ABCDEF1234567890", - trusted: !opts.noTrust, - backendUsed: "native", - })), - }; -} - -beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(process.stderr, "write").mockImplementation(() => true); - mockedSelectBackend.mockResolvedValue(fakeBackend()); -}); - -afterEach(() => { - vi.restoreAllMocks(); - for (const dir of cleanupDirs) fs.rmSync(dir, { recursive: true, force: true }); - cleanupDirs.length = 0; -}); - -describe("dcdc generate --no-trust propagation to bundle.json", () => { - it("emits trustInContainer:false when --no-trust is passed", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-notrust-")); - cleanupDirs.push(outDir); - - await runGenerate({ - outDir, - backend: "native", - noTrust: true, - }); - - const bundle = JSON.parse( - fs.readFileSync(path.join(outDir, "bundle.json"), "utf-8") - ) as Record; - const cert = (bundle.certs as Array>)[0]; - expect(cert.trustInContainer).toBe(false); - }); - - it("emits trustInContainer:true when --no-trust is NOT passed", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-trust-")); - cleanupDirs.push(outDir); - - await runGenerate({ - outDir, - backend: "native", - }); - - const bundle = JSON.parse( - fs.readFileSync(path.join(outDir, "bundle.json"), "utf-8") - ) as Record; - const cert = (bundle.certs as Array>)[0]; - expect(cert.trustInContainer).toBe(true); - }); - - it("forwards a stderr NSS reporter to the backend (so failures don't pass silently)", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-gen-reporter-")); - cleanupDirs.push(outDir); - const backend = fakeBackend(); - mockedSelectBackend.mockResolvedValueOnce(backend); - - await runGenerate({ - outDir, - backend: "native", - }); - - const generateMock = vi.mocked(backend.generate); - expect(generateMock).toHaveBeenCalledTimes(1); - const opts = generateMock.mock.calls[0][0]; - expect(opts.linuxNssTrustReporter).toBeInstanceOf(Function); - }); -}); diff --git a/src/cli/tests/select.test.ts b/src/cli/tests/select.test.ts deleted file mode 100644 index db56357..0000000 --- a/src/cli/tests/select.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - selectBackend, - describeAutoBackend, -} from "@devcontainer-dev-certs/shared"; -import { stubPlatform } from "./_helpers"; - -// `selectBackend('dotnet')` calls into the DotnetBackend's `isAvailable` -// which shells out via the shared runProcess. Stub that so the tests don't -// require an actual `dotnet` install. -vi.mock("@devcontainer-dev-certs/shared/src/platform/processUtil", () => ({ - runProcess: vi.fn(), -})); - -import { runProcess } from "@devcontainer-dev-certs/shared/src/platform/processUtil"; - -const mockedRunProcess = vi.mocked(runProcess); - -describe("selectBackend", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("returns the native backend for --backend native regardless of platform", async () => { - const restore = stubPlatform("linux"); - try { - const backend = await selectBackend("native"); - expect(backend.kind).toBe("native"); - } finally { - restore(); - } - }); - - it("returns the dotnet backend for --backend dotnet when dotnet is on PATH", async () => { - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("dotnet"); - expect(backend.kind).toBe("dotnet"); - }); - - it("throws when --backend dotnet is requested but dotnet is unavailable", async () => { - mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); - await expect(selectBackend("dotnet")).rejects.toThrow(/not found on PATH/); - }); - - it("auto-picks dotnet on macOS when dotnet is available", async () => { - const restore = stubPlatform("darwin"); - try { - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("dotnet"); - } finally { - restore(); - } - }); - - it("auto-falls-back to native on macOS when dotnet is unavailable", async () => { - const restore = stubPlatform("darwin"); - try { - mockedRunProcess.mockResolvedValue({ exitCode: 127, stdout: "", stderr: "not found" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); - } finally { - restore(); - } - }); - - it("auto-picks native on Linux regardless of dotnet availability", async () => { - const restore = stubPlatform("linux"); - try { - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); - } finally { - restore(); - } - }); - - it("auto-picks native on Windows", async () => { - const restore = stubPlatform("win32"); - try { - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - const backend = await selectBackend("auto"); - expect(backend.kind).toBe("native"); - } finally { - restore(); - } - }); -}); - -describe("describeAutoBackend", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("reports 'dotnet' on macOS when dotnet is available", async () => { - const restore = stubPlatform("darwin"); - try { - mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "8.0.100", stderr: "" }); - expect(await describeAutoBackend()).toBe("dotnet"); - } finally { - restore(); - } - }); - - it("reports 'native' everywhere else", async () => { - const restoreLinux = stubPlatform("linux"); - try { - expect(await describeAutoBackend()).toBe("native"); - } finally { - restoreLinux(); - } - const restoreWin = stubPlatform("win32"); - try { - expect(await describeAutoBackend()).toBe("native"); - } finally { - restoreWin(); - } - }); -}); diff --git a/src/cli/tests/writer.test.ts b/src/cli/tests/writer.test.ts deleted file mode 100644 index ed0a749..0000000 --- a/src/cli/tests/writer.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import { writeBundle, type BundleCertEntry } from "../src/bundle/writer"; - -let tmpDir: string; - -beforeEach(() => { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-writer-test-")); -}); - -afterEach(() => { - fs.rmSync(tmpDir, { recursive: true, force: true }); -}); - -function makeEntry(overrides: Partial = {}): BundleCertEntry { - return { - name: "aspnetcore-dev", - thumbprint: "ABCDEF1234567890", - kind: "dotnet-dev", - hostPfxPath: path.join(tmpDir, "aspnetcore-dev.pfx"), - hostPemPath: path.join(tmpDir, "aspnetcore-dev.pem"), - hostPemKeyPath: path.join(tmpDir, "aspnetcore-dev.key"), - trustInContainer: true, - ...overrides, - }; -} - -function readBundle(bundlePath: string): Record { - return JSON.parse(fs.readFileSync(bundlePath, "utf-8")) as Record; -} - -describe("writeBundle", () => { - it("writes a bundle.json with the schema URL and a $schema pointer", () => { - const bundlePath = writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [makeEntry()], - }); - expect(bundlePath).toBe(path.join(tmpDir, "bundle.json")); - const bundle = readBundle(bundlePath); - expect(bundle.$schema).toContain("bundle.schema.json"); - expect(Array.isArray(bundle.certs)).toBe(true); - }); - - it("rewrites host paths under the out-dir to container-mount paths", () => { - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [makeEntry()], - }); - const bundle = readBundle(path.join(tmpDir, "bundle.json")); - const cert = (bundle.certs as Record[])[0]; - expect(cert.pemPath).toBe("/host-dev-certs/aspnetcore-dev.pem"); - expect(cert.pfxPath).toBe("/host-dev-certs/aspnetcore-dev.pfx"); - expect(cert.pemKeyPath).toBe("/host-dev-certs/aspnetcore-dev.key"); - }); - - it("leaves paths outside the out-dir untouched (no implicit copy assumed)", () => { - const externalPath = path.join(os.tmpdir(), "elsewhere.pem"); - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [ - makeEntry({ - hostPemPath: externalPath, - hostPfxPath: null, - hostPemKeyPath: null, - }), - ], - }); - const bundle = readBundle(path.join(tmpDir, "bundle.json")); - const cert = (bundle.certs as Record[])[0]; - expect(cert.pemPath).toBe(externalPath); - }); - - it("strips trailing slashes from containerMount so paths don't double-slash", () => { - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs/", - entries: [makeEntry()], - }); - const bundle = readBundle(path.join(tmpDir, "bundle.json")); - const cert = (bundle.certs as Record[])[0]; - expect(cert.pemPath).toBe("/host-dev-certs/aspnetcore-dev.pem"); - }); - - it("omits pfxPath / pemKeyPath when they're null (cert-only entries)", () => { - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [ - makeEntry({ - hostPfxPath: null, - hostPemKeyPath: null, - }), - ], - }); - const bundle = readBundle(path.join(tmpDir, "bundle.json")); - const cert = (bundle.certs as Record[])[0]; - expect("pfxPath" in cert).toBe(false); - expect("pemKeyPath" in cert).toBe(false); - expect(cert.pemPath).toBeDefined(); - }); - - it("emits each entry with its declared kind and trustInContainer flag", () => { - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [ - makeEntry({ name: "corp-ca", kind: "user", trustInContainer: false }), - ], - }); - const bundle = readBundle(path.join(tmpDir, "bundle.json")); - const cert = (bundle.certs as Record[])[0]; - expect(cert.name).toBe("corp-ca"); - expect(cert.kind).toBe("user"); - expect(cert.trustInContainer).toBe(false); - }); - - it("includes extraDestinations when provided, omits the key when absent", () => { - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [makeEntry()], - extraDestinations: [{ path: "/etc/nginx/certs", format: "pem" }], - }); - const withExtras = readBundle(path.join(tmpDir, "bundle.json")); - expect(withExtras.extraDestinations).toEqual([ - { path: "/etc/nginx/certs", format: "pem" }, - ]); - - writeBundle({ - hostOutDir: tmpDir, - containerMount: "/host-dev-certs", - entries: [makeEntry()], - }); - const withoutExtras = readBundle(path.join(tmpDir, "bundle.json")); - expect("extraDestinations" in withoutExtras).toBe(false); - }); -}); diff --git a/src/cli/tsconfig.json b/src/cli/tsconfig.json deleted file mode 100644 index cdc3aae..0000000 --- a/src/cli/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "compilerOptions": { - "module": "Node16", - "target": "ES2022", - "lib": ["ES2022", "DOM"], - "moduleResolution": "Node16", - "types": ["node"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "sourceMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/src/cli/tsconfig.lint.json b/src/cli/tsconfig.lint.json deleted file mode 100644 index 0b5e893..0000000 --- a/src/cli/tsconfig.lint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "rootDir": "." - }, - "include": ["src/**/*.ts", "tests/**/*.ts", "vitest.setup.ts"] -} diff --git a/src/cli/vitest.config.ts b/src/cli/vitest.config.ts deleted file mode 100644 index 1b1cdf6..0000000 --- a/src/cli/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - include: ["tests/**/*.test.ts"], - setupFiles: ["./vitest.setup.ts"], - }, -}); diff --git a/src/cli/vitest.setup.ts b/src/cli/vitest.setup.ts deleted file mode 100644 index 82e4bba..0000000 --- a/src/cli/vitest.setup.ts +++ /dev/null @@ -1,4 +0,0 @@ -// @peculiar/x509 v2 transitively pulls in tsyringe, which requires the -// reflect-metadata polyfill before any module decorated with @injectable -// is imported. Same setup the extension test suites use. -import "reflect-metadata"; diff --git a/src/shared/src/backends/native.ts b/src/shared/src/backends/native.ts index 2a57b0c..7bad5ca 100644 --- a/src/shared/src/backends/native.ts +++ b/src/shared/src/backends/native.ts @@ -23,11 +23,9 @@ import type { Backend, GenerateOptions, GenerateResult } from "./types"; * the operation, not a side effect. * * - `noTrust: true`: generate purely in memory and write only to - * `outDir`. The platform store is NOT touched. The typical caller - * here is `dcdc generate --no-trust` for "give me cert files to - * bind-mount into a container" — that user has explicitly opted out - * of host-side cert installation, so we honor it by keeping our - * side effects bounded to `outDir`. + * `outDir`. The platform store is NOT touched. Reserved for callers + * that have explicitly opted out of host-side cert installation — we + * honor that by keeping our side effects bounded to `outDir`. */ export class NativeBackend implements Backend { readonly kind = "native" as const; @@ -76,10 +74,8 @@ async function generateAndTrust( linuxNssTrustReporter: LinuxNssTrustReporter | undefined ): Promise { // Without a reporter the Linux NSS trust step would still run but its - // outcome would be discarded. The CLI's `dcdc generate` always wires a - // stderr-logging reporter; CertProvider in the host extension wires its - // toast reporter. Both surfaces are responsible for telling the user - // when NSS trust didn't take. + // outcome would be discarded. CertProvider in the host extension wires + // a toast reporter so the user sees NSS trust failures. const manager = new CertManager({ linuxNssTrustReporter }); await manager.trust(); await manager.exportCert("pfx", outDir); diff --git a/src/shared/src/backends/select.ts b/src/shared/src/backends/select.ts index 18ef60e..3c90f13 100644 --- a/src/shared/src/backends/select.ts +++ b/src/shared/src/backends/select.ts @@ -3,10 +3,10 @@ import { NativeBackend } from "./native"; import type { Backend, BackendKind, BackendMode } from "./types"; /** - * Resolve a `--backend` choice (possibly `auto`) into a concrete backend - * instance. `auto` prefers `dotnet` on macOS when the `dotnet` CLI is on - * PATH (better keychain-trust UX via a signed binary), `native` everywhere - * else. + * Resolve a `hostCertGenerator` choice (possibly `auto`) into a concrete + * backend instance. `auto` prefers `dotnet` on macOS when the `dotnet` CLI + * is on PATH (better keychain-trust UX via a signed binary), `native` + * everywhere else. */ export async function selectBackend(mode: BackendMode): Promise { if (mode === "native") return new NativeBackend(); @@ -14,7 +14,7 @@ export async function selectBackend(mode: BackendMode): Promise { const backend = new DotnetBackend(); if (!(await backend.isAvailable())) { throw new Error( - "Requested --backend dotnet but the `dotnet` CLI was not found on PATH." + "Requested hostCertGenerator=dotnet but the `dotnet` CLI was not found on PATH." ); } return backend; @@ -33,12 +33,10 @@ async function autoSelect(): Promise { /** * Report which backend `auto` would pick on this host without actually - * constructing it. Useful for `dcdc doctor` and for status surfaces in the - * VS Code host extension. + * constructing it. Useful for status surfaces in the VS Code host extension. * - * Callers that have already probed dotnet (e.g. `dcdc doctor` checks - * dotnet availability for its own line of output) can pass the result - * via `dotnetAvailable` to avoid a second `dotnet --version` spawn. + * Callers that have already probed dotnet can pass the result via + * `dotnetAvailable` to avoid a second `dotnet --version` spawn. */ export async function describeAutoBackend( dotnetAvailable?: boolean diff --git a/src/shared/src/backends/types.ts b/src/shared/src/backends/types.ts index fe0c9d1..73fd0c5 100644 --- a/src/shared/src/backends/types.ts +++ b/src/shared/src/backends/types.ts @@ -1,8 +1,8 @@ /** - * Backend abstraction shared by `dcdc` (host CLI) and the VS Code host - * extension. Lets both consumers pick between equivalent generators — - * the bundled-in native cert primitives or the `dotnet dev-certs https` - * CLI pass-through — without each having to reimplement the selection / + * Backend abstraction for the VS Code host extension's `hostCertGenerator` + * setting. Lets the extension pick between equivalent generators — the + * bundled-in native cert primitives or the `dotnet dev-certs https` CLI + * pass-through — without the call site reimplementing the selection / * availability-detection logic. * * The interface is deliberately narrow: each backend exposes @@ -29,8 +29,7 @@ export interface GenerateOptions { * dotnet backend invokes `trustInNss` directly with it (because * `dotnet dev-certs --trust` itself doesn't touch the NSS DBs that * Firefox / Chromium read). Without a reporter, NSS failures are - * silent — surfacing them in a CLI stderr line or a VS Code toast - * is the consumer's job. + * silent — surfacing them in a VS Code toast is the consumer's job. */ linuxNssTrustReporter?: LinuxNssTrustReporter; } diff --git a/src/shared/src/cert/loader.ts b/src/shared/src/cert/loader.ts index 453fc83..8ccaa03 100644 --- a/src/shared/src/cert/loader.ts +++ b/src/shared/src/cert/loader.ts @@ -56,23 +56,3 @@ export function loadPemPair( return buildLoadedCert(cert, key); } -/** - * Locate a sibling PEM private key next to a cert PEM, returning the - * first match or null. Probes both naming conventions in the wild: - * - * - `.key` — what our exporter writes, what `openssl` writes by - * convention. - * - `.key` — what `dotnet dev-certs --format PEM - * --export-path foo.pem` writes (`foo.pem.key`). - * - * Stem-form wins when both exist; that's the path `loadPemPair` - * documents and the more common case for openssl- / our-exporter- - * produced pairs. - */ -export function findSiblingKey(certPath: string): string | null { - const stem = certPath.replace(/\.[^.]+$/, ""); - for (const candidate of [`${stem}.key`, `${certPath}.key`]) { - if (fs.existsSync(candidate)) return candidate; - } - return null; -} diff --git a/src/shared/src/cert/pkcs12LegacyPbe.ts b/src/shared/src/cert/pkcs12LegacyPbe.ts index 136658c..3afe2e0 100644 --- a/src/shared/src/cert/pkcs12LegacyPbe.ts +++ b/src/shared/src/cert/pkcs12LegacyPbe.ts @@ -16,12 +16,12 @@ * (macos-latest) and on a maintainer's local Mac. * * Without this module, `DotnetBackend.generate` on macOS — the platform - * `--backend auto` PREFERS — fails because `findExistingDevCert` can't - * read aspnetcore's disk cache. Same blocker hits `dcdc inspect`, - * `dcdc bundle`, and `dcdc trust` against any user-supplied - * dotnet-on-macOS-produced PFX. Scoping is per-OID: only the algorithm - * we observed in practice is accepted; the other five RC2/RC4/2-key-3DES - * OIDs in the PKCS#12 family remain rejected with the original error. + * `hostCertGenerator=auto` PREFERS — fails because `findExistingDevCert` + * can't read aspnetcore's disk cache. Same blocker hits any user-supplied + * dotnet-on-macOS-produced PFX that flows through `parsePfx`. Scoping is + * per-OID: only the algorithm we observed in practice is accepted; the + * other five RC2/RC4/2-key-3DES OIDs in the PKCS#12 family remain + * rejected with the original error. * * # Removal criteria * diff --git a/src/shared/src/index.ts b/src/shared/src/index.ts index ced89b4..be77796 100644 --- a/src/shared/src/index.ts +++ b/src/shared/src/index.ts @@ -42,7 +42,7 @@ export { } from "./cert/properties"; export { buildPfx, parsePfx } from "./cert/pfx"; export type { BuildPfxOptions, ParsedPfx } from "./cert/pfx"; -export { loadPfx, loadPemPair, findSiblingKey } from "./cert/loader"; +export { loadPfx, loadPemPair } from "./cert/loader"; export type { LoadedCert } from "./cert/loader"; export { isValidDevCert, @@ -126,11 +126,11 @@ export type { ResolveSafeExecPathOptions, } from "./platform/processUtil"; -// Backend abstraction — selectable cert-generator backends shared by the -// host CLI (`dcdc`) and the VS Code host extension. Lets both consumers -// pick between the bundled native generator, the `dotnet dev-certs https` -// pass-through, and (future) an Aspire-aware variant without each -// having to reimplement availability detection / selection logic. +// Backend abstraction — selectable cert-generator backends for the +// VS Code host extension's `hostCertGenerator` setting. Lets the +// extension pick between the bundled native generator and the +// `dotnet dev-certs https` pass-through without the call site +// reimplementing availability detection / selection logic. export { NativeBackend } from "./backends/native"; export { DotnetBackend } from "./backends/dotnet"; export { selectBackend, describeAutoBackend } from "./backends/select"; diff --git a/src/vscode-ui-extension/tests/dotnetBackend.test.ts b/src/vscode-ui-extension/tests/dotnetBackend.test.ts index a728a65..4d38fc0 100644 --- a/src/vscode-ui-extension/tests/dotnetBackend.test.ts +++ b/src/vscode-ui-extension/tests/dotnetBackend.test.ts @@ -65,7 +65,7 @@ afterEach(() => { describe("DotnetBackend.generate", () => { it("invokes `dotnet dev-certs https --trust` (no --no-password, no --format)", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -96,7 +96,7 @@ describe("DotnetBackend.generate", () => { }); it("omits --trust when noTrust is set", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -115,7 +115,7 @@ describe("DotnetBackend.generate", () => { }); it("writes PFX, PEM, and key into outDir using our naming (foo.key, not foo.pem.key)", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -145,7 +145,7 @@ describe("DotnetBackend.generate", () => { it("supplements the trust step with NSS on Linux", async () => { const restore = stubPlatform("linux"); try { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -184,7 +184,7 @@ describe("DotnetBackend.generate", () => { it("does NOT run the NSS step on macOS (keychain handles browser trust)", async () => { const restore = stubPlatform("darwin"); try { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -204,7 +204,7 @@ describe("DotnetBackend.generate", () => { it("does NOT run the NSS step on Linux when noTrust is set", async () => { const restore = stubPlatform("linux"); try { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); const generated = await makeCert(); @@ -222,7 +222,7 @@ describe("DotnetBackend.generate", () => { }); it("throws when dotnet exits non-zero", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); mockedRunProcess.mockResolvedValue({ @@ -237,7 +237,7 @@ describe("DotnetBackend.generate", () => { }); it("throws with a clear message when the store has no cert post-trust", async () => { - const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-dotnet-")); + const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-dotnet-")); cleanupDirs.push(outDir); mockedRunProcess.mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" }); diff --git a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts index b776f87..0e76aee 100644 --- a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts +++ b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts @@ -3,34 +3,40 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import { execFileSync } from "child_process"; +import * as pkijs from "pkijs"; import { loadPfx } from "../src/cert/loader"; /** - * macOS-only integration test that answers a specific code-review - * question: when `DotnetBackend.generate` runs `dotnet dev-certs https - * --trust` and then calls `findExistingDevCert`, can our `parsePfx` - * actually read the on-disk cache file aspnetcore writes? + * macOS-only integration test guarding the read-side compatibility + * between `parsePfx` and the PFX `aspnetcore`'s `MacOSCertificateManager. + * SaveCertificateCore` writes to disk at + * `~/.aspnet/dev-certs/https/aspnetcore-localhost-{thumbprint}.pfx`. * - * The cache file path is `~/.aspnet/dev-certs/https/aspnetcore-localhost- - * {thumbprint}.pfx`. The writer in `MacOSCertificateManager.SaveCertificate - * Core` calls `certificate.Export(X509ContentType.Pfx)` with no password - * — but the byte-level PBE algorithm that produces on macOS is what - * determines whether our parser (which strictly accepts PBES2 and - * rejects every legacy PKCS#12 PBE-with-SHA OID) can read it back. + * The cache file is produced by `dotnet dev-certs https` (no flags) — + * the writer calls `certificate.Export(X509ContentType.Pfx)` with no + * password. On every currently-supported .NET SDK that path emits + * legacy `pbeWithSHA1And3-KeyTripleDES-CBC` (OID 1.2.840.113549.1.12.1.3) + * + SHA-1, 2000 iterations. We need our parser to read this; the + * narrow legacy-PBE handler in `pkcs12LegacyPbe.ts` exists exactly for + * this case. * - * Test isolates the cache file via `HOME=` so it never touches - * the developer's real dev cert state, and it deliberately omits - * `--trust` so there's no keychain trust-settings prompt in CI. + * Two things this test pins: * - * Outcomes: - * - parsePfx loads the file → finding REFUTED, DotnetBackend works on - * macOS as written. - * - parsePfx throws → finding CONFIRMED. The error message names the - * offending OID; we use that to pick the right fix. + * 1. The cache file loads via `parsePfx` — Kestrel discovery, the + * VS Code host extension's read path, the workspace-extension's + * cert push, and anything else that goes through `parsePfx` keeps + * working as long as this passes. + * 2. The on-disk PBE algorithm OID is the one our legacy handler is + * designed for. When aspnetcore eventually switches the macOS + * writer to `ExportPkcs12(PbeParameters(Aes256Cbc, …))` and ships + * PBES2 instead — the day this OID flips — the second assertion + * fails loudly and tells the next maintainer "the legacy 3DES + * handler in `pkcs12LegacyPbe.ts` can now be removed; see its + * removal checklist." That's the only way we'll know the + * workaround has aged out without monitoring upstream by hand. * - * The test asserts a successful load. If aspnetcore changes the - * algorithm in a future SDK, we want this to fail loudly rather than - * pass with a stale assumption. + * Both observations require running against a real dotnet on a real + * macOS host; the test self-skips elsewhere. */ const isMacOS = process.platform === "darwin"; @@ -50,26 +56,26 @@ try { const ready = isMacOS && dotnetMajor >= 6; +// OIDs we recognize at the EncryptedData layer of the PKCS#12. Any +// other value means the writer started emitting a format we haven't +// catalogued; the test fails so the discrepancy is investigated. +const OID_PBE_3DES_SHA1 = "1.2.840.113549.1.12.1.3"; +const OID_PBES2 = "1.2.840.113549.1.5.13"; + let cachePfxPath: string; let loadResult: | { kind: "ok"; thumbprint: string; hasKey: boolean } | { kind: "err"; message: string }; +let observedPbeOid: string | null = null; beforeAll(async () => { if (!ready) return; - // Use the real `$HOME`. Earlier iterations of this test redirected HOME - // to a tmpdir to isolate the disk cache, but the macOS keychain APIs - // dotnet calls into resolve the login keychain from `$HOME/Library/ - // Keychains/login.keychain-db`. A redirected HOME points at a path - // where no keychain exists, and `dotnet dev-certs` fails with - // "There was an error saving the HTTPS developer certificate to the - // current user personal certificate store." before we ever get to - // observe what it would have written to the disk cache. The CI runner - // is ephemeral, so the isolation isn't load-bearing here. - // // 60s timeout because first-run cert generation can be slow on cold - // CI runners. + // CI runners. We use the real `$HOME` deliberately: macOS keychain + // APIs resolve the login keychain via `$HOME/Library/Keychains/` + // and `dotnet dev-certs` fails before it ever writes the disk + // cache if HOME points at a tmpdir with no keychain. execFileSync("dotnet", ["dev-certs", "https"], { timeout: 60_000, stdio: "pipe", @@ -78,9 +84,6 @@ beforeAll(async () => { const home = os.homedir(); const cacheDir = path.join(home, ".aspnet", "dev-certs", "https"); if (!fs.existsSync(cacheDir)) { - // Surface this loudly — if the cache dir doesn't appear, the test - // premise has changed (aspnetcore moved or skipped the disk cache) - // and any further assertions are meaningless. throw new Error( `Expected aspnetcore disk cache at ${cacheDir}; directory does not exist. ` + `dotnet dev-certs may have changed its on-disk layout.` @@ -98,6 +101,8 @@ beforeAll(async () => { } cachePfxPath = path.join(cacheDir, pfxes[0]); + observedPbeOid = inspectFirstEncryptedDataOid(cachePfxPath); + try { const loaded = await loadPfx(cachePfxPath); loadResult = { @@ -119,15 +124,16 @@ beforeAll(async () => { // follow-up investigation if the next run sees stale state. describe.skipIf(!ready)( - "dotnet dev-certs macOS disk cache → parsePfx", + "aspnetcore macOS PFX format compatibility", () => { - it("loads the disk-cache PFX without throwing", () => { - // Diagnostic: write the resolved path AND outcome to stderr so - // CI logs carry an actionable signal even when the assertion - // passes. (stderr instead of console.log to dodge the codebase's - // no-console rule.) + it("parsePfx loads aspnetcore's macOS disk-cache PFX", () => { + // Diagnostic line carries the resolved path AND the observed PBE + // OID so CI logs always show the discrepancy at-a-glance even + // when the assertion passes. Stderr (not console.log) keeps the + // codebase's no-console lint rule happy. process.stderr.write( - `[macos-cache] dotnet major: ${dotnetMajor}, cache: ${cachePfxPath}, ` + + `[macos-pfx] dotnet major: ${dotnetMajor}, cache: ${cachePfxPath}, ` + + `observedPbeOid: ${observedPbeOid ?? "(unknown)"}, ` + `result: ${JSON.stringify(loadResult)}\n` ); expect(loadResult.kind).toBe("ok"); @@ -142,5 +148,65 @@ describe.skipIf(!ready)( if (loadResult.kind !== "ok") return; expect(loadResult.hasKey).toBe(true); }); + + it("aspnetcore is still emitting the legacy 3DES PBE algorithm we expect", () => { + // This assertion exists to fire on a SUCCESS event from + // aspnetcore's perspective — when they swap the macOS writer + // over to `ExportPkcs12(PbeParameters(Aes256Cbc, ...))` and the + // disk cache starts coming out as PBES2. When that day arrives, + // this test goes red; the failure message is the next + // maintainer's signal to clean up the legacy code: + // + // - `src/shared/src/cert/pkcs12LegacyPbe.ts` can be deleted + // (follow its removal checklist). + // - The OID can return to `REJECTED_LEGACY_PBE_NAMES` in + // `pfx.ts`. + // - This very assertion gets flipped to expect PBES2 (or just + // deleted, depending on whether the codebase still needs + // to know). + if (observedPbeOid === OID_PBES2) { + throw new Error( + "aspnetcore's macOS PFX writer appears to have switched to PBES2 " + + "(OID 1.2.840.113549.1.5.13). The legacy 3DES handler in " + + "src/shared/src/cert/pkcs12LegacyPbe.ts is no longer needed and " + + "can be removed — see its docstring for the removal checklist." + ); + } + expect(observedPbeOid).toBe(OID_PBE_3DES_SHA1); + }); } ); + +/** + * Pull the encryption algorithm OID off the first `EncryptedData` + * `ContentInfo` inside the PFX's `authenticatedSafe`. That's the + * algorithm protecting the cert bag, which is what `pkcs12LegacyPbe.ts` + * has to know how to decrypt. + * + * Walks pkijs's parsed structure directly rather than going through + * `parsePfx` so it works regardless of whether the legacy handler can + * decode the file — the OID is observable from the headers alone, no + * password needed. + * + * Returns null if the structure doesn't contain an `EncryptedData` + * (`Data`-typed contents are unencrypted, no OID to report). + */ +function inspectFirstEncryptedDataOid(pfxPath: string): string | null { + const bytes = fs.readFileSync(pfxPath); + const ab = new ArrayBuffer(bytes.byteLength); + new Uint8Array(ab).set(bytes); + const pfx = pkijs.PFX.fromBER(ab); + const outerContent = pfx.parsedValue?.authenticatedSafe?.parsedValue + ?.safeContents as ReadonlyArray | undefined; + if (!outerContent) return null; + for (const contentInfo of outerContent) { + // 1.2.840.113549.1.7.6 = pkcs-7-encryptedData + if (contentInfo.contentType !== "1.2.840.113549.1.7.6") continue; + const encryptedData = new pkijs.EncryptedData({ + schema: contentInfo.content, + }); + return encryptedData.encryptedContentInfo.contentEncryptionAlgorithm + .algorithmId; + } + return null; +} diff --git a/src/vscode-ui-extension/tests/nativeBackend.test.ts b/src/vscode-ui-extension/tests/nativeBackend.test.ts index 6da0e97..b041c87 100644 --- a/src/vscode-ui-extension/tests/nativeBackend.test.ts +++ b/src/vscode-ui-extension/tests/nativeBackend.test.ts @@ -33,9 +33,9 @@ describe("NativeBackend.generate with --no-trust", () => { beforeEach(() => { originalHome = process.env.HOME; - fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-nativebackend-home-")); + fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-nativebackend-home-")); process.env.HOME = fakeHome; - outDir = fs.mkdtempSync(path.join(os.tmpdir(), "dcdc-nativebackend-out-")); + outDir = fs.mkdtempSync(path.join(os.tmpdir(), "devcerts-nativebackend-out-")); backend = new NativeBackend(); }); From 17def4ae970b775d76a24c6cfae881e4d74b6c69 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 05:08:54 +0000 Subject: [PATCH 40/41] Restore cross-platform optional deps in package-lock.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous scope-reduction commit regenerated package-lock.json from scratch on Linux to drop the deleted `src/cli` workspace entry. npm has a known bug (npm/cli#4828) where regenerating the lock on one platform strips the optional native-binding entries for every other platform, which broke `npm ci` on macOS and Windows runners — vitest's transitive `rolldown` dep failed to load its native binding. Restore the previous lock and remove just the three `src/cli` entries (workspace list, symlink, package definition + nested commander) by hand. Cross-platform `@rolldown/binding-*` entries are now preserved. --- package-lock.json | 1933 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1927 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5a3ffaf..4e783da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,11 +20,15 @@ }, "node_modules/@azu/format-text": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@azu/style-format": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", "dev": true, "license": "WTFPL", "dependencies": { @@ -33,6 +37,8 @@ }, "node_modules/@azure/abort-controller": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", "dev": true, "license": "MIT", "dependencies": { @@ -44,6 +50,8 @@ }, "node_modules/@azure/core-auth": { "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", "dev": true, "license": "MIT", "dependencies": { @@ -57,6 +65,8 @@ }, "node_modules/@azure/core-client": { "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", "dev": true, "license": "MIT", "dependencies": { @@ -74,6 +84,8 @@ }, "node_modules/@azure/core-rest-pipeline": { "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -91,6 +103,8 @@ }, "node_modules/@azure/core-tracing": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -102,6 +116,8 @@ }, "node_modules/@azure/core-util": { "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", "dev": true, "license": "MIT", "dependencies": { @@ -115,6 +131,8 @@ }, "node_modules/@azure/identity": { "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", "dev": true, "license": "MIT", "dependencies": { @@ -136,6 +154,8 @@ }, "node_modules/@azure/logger": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", "dev": true, "license": "MIT", "dependencies": { @@ -148,6 +168,8 @@ }, "node_modules/@azure/msal-browser": { "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.6.3.tgz", + "integrity": "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w==", "dev": true, "license": "MIT", "dependencies": { @@ -159,6 +181,8 @@ }, "node_modules/@azure/msal-common": { "version": "16.4.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.4.1.tgz", + "integrity": "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw==", "dev": true, "license": "MIT", "engines": { @@ -167,6 +191,8 @@ }, "node_modules/@azure/msal-node": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.2.2.tgz", + "integrity": "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -179,6 +205,8 @@ }, "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { "version": "16.6.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.6.2.tgz", + "integrity": "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA==", "dev": true, "license": "MIT", "engines": { @@ -187,6 +215,8 @@ }, "node_modules/@babel/code-frame": { "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { @@ -200,6 +230,8 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -210,8 +242,316 @@ "resolved": "src/shared", "link": true }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -225,8 +565,163 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -244,6 +739,8 @@ }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -255,6 +752,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -263,6 +762,8 @@ }, "node_modules/@eslint/config-array": { "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -276,6 +777,8 @@ }, "node_modules/@eslint/config-array/node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -284,6 +787,8 @@ }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -295,6 +800,8 @@ }, "node_modules/@eslint/config-array/node_modules/minimatch": { "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -309,6 +816,8 @@ }, "node_modules/@eslint/config-helpers": { "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -320,6 +829,8 @@ }, "node_modules/@eslint/core": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -331,6 +842,8 @@ }, "node_modules/@eslint/js": { "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { @@ -350,6 +863,8 @@ }, "node_modules/@eslint/object-schema": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -358,6 +873,8 @@ }, "node_modules/@eslint/plugin-kit": { "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -370,6 +887,8 @@ }, "node_modules/@humanfs/core": { "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -381,6 +900,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -394,6 +915,8 @@ }, "node_modules/@humanfs/types": { "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -402,6 +925,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -414,6 +939,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -426,6 +953,8 @@ }, "node_modules/@isaacs/cliui": { "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -434,11 +963,34 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@noble/hashes": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "license": "MIT", "engines": { "node": ">= 16" @@ -449,6 +1001,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -461,6 +1015,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -469,6 +1025,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -481,6 +1039,8 @@ }, "node_modules/@oxc-project/types": { "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -489,6 +1049,8 @@ }, "node_modules/@peculiar/asn1-cms": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -500,6 +1062,8 @@ }, "node_modules/@peculiar/asn1-csr": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -510,6 +1074,8 @@ }, "node_modules/@peculiar/asn1-ecc": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -520,6 +1086,8 @@ }, "node_modules/@peculiar/asn1-pfx": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.1", @@ -532,6 +1100,8 @@ }, "node_modules/@peculiar/asn1-pkcs8": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -542,6 +1112,8 @@ }, "node_modules/@peculiar/asn1-pkcs9": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.1", @@ -556,6 +1128,8 @@ }, "node_modules/@peculiar/asn1-rsa": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -566,6 +1140,8 @@ }, "node_modules/@peculiar/asn1-schema": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", "license": "MIT", "dependencies": { "asn1js": "^3.0.6", @@ -575,6 +1151,8 @@ }, "node_modules/@peculiar/asn1-x509": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -585,6 +1163,8 @@ }, "node_modules/@peculiar/asn1-x509-attr": { "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -595,6 +1175,8 @@ }, "node_modules/@peculiar/x509": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.0.0.tgz", + "integrity": "sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA==", "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.0", @@ -609,26 +1191,253 @@ "tsyringe": "^4.10.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { + "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { + "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -636,7 +1445,7 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" @@ -644,11 +1453,15 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", "dev": true, "license": "MIT", "dependencies": { @@ -660,6 +1473,8 @@ }, "node_modules/@secretlint/config-loader": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", "dev": true, "license": "MIT", "dependencies": { @@ -676,6 +1491,8 @@ }, "node_modules/@secretlint/core": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", "dev": true, "license": "MIT", "dependencies": { @@ -690,6 +1507,8 @@ }, "node_modules/@secretlint/formatter": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", "dev": true, "license": "MIT", "dependencies": { @@ -711,6 +1530,8 @@ }, "node_modules/@secretlint/formatter/node_modules/chalk": { "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { @@ -722,6 +1543,8 @@ }, "node_modules/@secretlint/node": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -740,16 +1563,22 @@ }, "node_modules/@secretlint/profiler": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/resolver": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/secretlint-formatter-sarif": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", "dev": true, "license": "MIT", "dependencies": { @@ -758,6 +1587,8 @@ }, "node_modules/@secretlint/secretlint-rule-no-dotenv": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", "dev": true, "license": "MIT", "dependencies": { @@ -769,6 +1600,8 @@ }, "node_modules/@secretlint/secretlint-rule-preset-recommend": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", "dev": true, "license": "MIT", "engines": { @@ -777,6 +1610,8 @@ }, "node_modules/@secretlint/source-creator": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", "dev": true, "license": "MIT", "dependencies": { @@ -789,6 +1624,8 @@ }, "node_modules/@secretlint/types": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", "dev": true, "license": "MIT", "engines": { @@ -797,6 +1634,8 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, "license": "MIT", "engines": { @@ -808,16 +1647,22 @@ }, "node_modules/@standard-schema/spec": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@textlint/ast-node-types": { "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.5.4.tgz", + "integrity": "sha512-bVtB6VEy9U9DpW8cTt25k5T+lz86zV5w6ImePZqY1AXzSuPhqQNT77lkMPxonXzUducEIlSvUu3o7sKw3y9+Sw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.5.4.tgz", + "integrity": "sha512-D9qJedKBLmAo+kiudop4UKgSxXMi4O8U86KrCidVXZ9RsK0NSVIw6+r2rlMUOExq79iEY81FRENyzmNVRxDBsg==", "dev": true, "license": "MIT", "dependencies": { @@ -839,6 +1684,8 @@ }, "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -847,11 +1694,15 @@ }, "node_modules/@textlint/linter-formatter/node_modules/pluralize": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -863,24 +1714,43 @@ }, "node_modules/@textlint/module-interop": { "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.5.4.tgz", + "integrity": "sha512-JyAUd26ll3IFF87LP0uGoa8Tzw5ZKiYvGs6v8jLlzyND1lUYCI4+2oIAslrODLkf0qwoCaJrBQWM3wsw+asVGQ==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.5.4.tgz", + "integrity": "sha512-5GUagtpQuYcmhlOzBGdmVBvDu5lKgVTjwbxtdfoidN4OIqblIxThJHHjazU+ic+/bCIIzI2JcOjHGSaRmE8Gcg==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { "version": "15.5.4", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.5.4.tgz", + "integrity": "sha512-mY28j2U7nrWmZbxyKnRvB8vJxJab4AxqOobLfb6iozrLelJbqxcOTvBQednadWPfAk9XWaZVMqUr9Nird3mutg==", "dev": true, "license": "MIT", "dependencies": { "@textlint/ast-node-types": "15.5.4" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -890,26 +1760,36 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/esrecurse": { "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -918,21 +1798,29 @@ }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true, "license": "MIT" }, "node_modules/@types/sarif": { "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", "dev": true, "license": "MIT" }, "node_modules/@types/vscode": { "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", + "integrity": "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==", "dev": true, "license": "MIT", "dependencies": { @@ -960,6 +1848,8 @@ }, "node_modules/@typescript-eslint/parser": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.2.tgz", + "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -983,6 +1873,8 @@ }, "node_modules/@typescript-eslint/project-service": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", + "integrity": "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,6 +1895,8 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", + "integrity": "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==", "dev": true, "license": "MIT", "dependencies": { @@ -1019,6 +1913,8 @@ }, "node_modules/@typescript-eslint/tsconfig-utils": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", + "integrity": "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==", "dev": true, "license": "MIT", "engines": { @@ -1034,6 +1930,8 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", + "integrity": "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1057,6 +1955,8 @@ }, "node_modules/@typescript-eslint/types": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.2.tgz", + "integrity": "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==", "dev": true, "license": "MIT", "engines": { @@ -1069,6 +1969,8 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", + "integrity": "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==", "dev": true, "license": "MIT", "dependencies": { @@ -1095,6 +1997,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -1103,6 +2007,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1114,6 +2020,8 @@ }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1128,6 +2036,8 @@ }, "node_modules/@typescript-eslint/utils": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.2.tgz", + "integrity": "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1150,6 +2060,8 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", + "integrity": "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==", "dev": true, "license": "MIT", "dependencies": { @@ -1166,6 +2078,8 @@ }, "node_modules/@typespec/ts-http-runtime": { "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", "dev": true, "license": "MIT", "dependencies": { @@ -1179,6 +2093,8 @@ }, "node_modules/@vitest/expect": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", "dependencies": { @@ -1195,6 +2111,8 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { @@ -1220,6 +2138,8 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { @@ -1231,6 +2151,8 @@ }, "node_modules/@vitest/runner": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1243,6 +2165,8 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1257,6 +2181,8 @@ }, "node_modules/@vitest/spy": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, "license": "MIT", "funding": { @@ -1265,6 +2191,8 @@ }, "node_modules/@vitest/utils": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { @@ -1278,6 +2206,8 @@ }, "node_modules/@vscode/vsce": { "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.9.1.tgz", + "integrity": "sha512-MPn5p+DoudI+3GfJSpAZZraE1lgLv0LcwbH3+xy7RgEhty3UIkmUMUA+5jPTDaxXae00AnX5u77FxGM8FhfKKA==", "dev": true, "license": "MIT", "dependencies": { @@ -1323,6 +2253,8 @@ }, "node_modules/@vscode/vsce-sign": { "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", "dev": true, "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.txt", @@ -1338,8 +2270,94 @@ "@vscode/vsce-sign-win32-x64": "2.0.6" } }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@vscode/vsce-sign-linux-x64": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", "cpu": [ "x64" ], @@ -1350,8 +2368,38 @@ "linux" ] }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/acorn": { "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1363,6 +2411,8 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1371,6 +2421,8 @@ }, "node_modules/agent-base": { "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", "engines": { @@ -1379,6 +2431,8 @@ }, "node_modules/ajv": { "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -1394,6 +2448,8 @@ }, "node_modules/ansi-escapes": { "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -1408,6 +2464,8 @@ }, "node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -1419,6 +2477,8 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { @@ -1433,11 +2493,15 @@ }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, "license": "Python-2.0" }, "node_modules/asn1js": { "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", @@ -1450,6 +2514,8 @@ }, "node_modules/assertion-error": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -1458,6 +2524,8 @@ }, "node_modules/astral-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "license": "MIT", "engines": { @@ -1466,11 +2534,15 @@ }, "node_modules/asynckit": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, "license": "MIT" }, "node_modules/azure-devops-node-api": { "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", "dev": true, "license": "MIT", "dependencies": { @@ -1480,11 +2552,15 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -1505,6 +2581,8 @@ }, "node_modules/binaryextensions": { "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -1519,6 +2597,8 @@ }, "node_modules/bl": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "license": "MIT", "optional": true, @@ -1530,16 +2610,22 @@ }, "node_modules/boolbase": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, "node_modules/boundary": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/brace-expansion": { "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1549,6 +2635,8 @@ }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -1560,6 +2648,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -1584,6 +2674,8 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, "license": "MIT", "engines": { @@ -1592,11 +2684,15 @@ }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/bundle-name": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1611,6 +2707,8 @@ }, "node_modules/bytestreamjs": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", "license": "BSD-3-Clause", "engines": { "node": ">=6.0.0" @@ -1618,6 +2716,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1630,6 +2730,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -1645,6 +2747,8 @@ }, "node_modules/chai": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -1653,6 +2757,8 @@ }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", "dependencies": { @@ -1668,6 +2774,8 @@ }, "node_modules/cheerio": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", "dev": true, "license": "MIT", "dependencies": { @@ -1692,6 +2800,8 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1708,12 +2818,16 @@ }, "node_modules/chownr": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true, "license": "ISC", "optional": true }, "node_modules/cockatiel": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", "dev": true, "license": "MIT", "engines": { @@ -1722,6 +2836,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1733,11 +2849,15 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", "dependencies": { @@ -1749,6 +2869,8 @@ }, "node_modules/commander": { "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, "license": "MIT", "engines": { @@ -1757,16 +2879,22 @@ }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1780,6 +2908,8 @@ }, "node_modules/css-select": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1795,6 +2925,8 @@ }, "node_modules/css-what": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -1806,6 +2938,8 @@ }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1822,6 +2956,8 @@ }, "node_modules/decompress-response": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dev": true, "license": "MIT", "optional": true, @@ -1837,6 +2973,8 @@ }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, "license": "MIT", "optional": true, @@ -1846,11 +2984,15 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, "node_modules/default-browser": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -1866,6 +3008,8 @@ }, "node_modules/default-browser-id": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", "dev": true, "license": "MIT", "engines": { @@ -1877,6 +3021,8 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, "license": "MIT", "engines": { @@ -1888,6 +3034,8 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { @@ -1896,6 +3044,8 @@ }, "node_modules/detect-libc": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1912,6 +3062,8 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { @@ -1925,6 +3077,8 @@ }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -1936,6 +3090,8 @@ }, "node_modules/domhandler": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1950,6 +3106,8 @@ }, "node_modules/domutils": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -1963,6 +3121,8 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -1976,6 +3136,8 @@ }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1984,6 +3146,8 @@ }, "node_modules/editions": { "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -1999,11 +3163,15 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/encoding-sniffer": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -2016,6 +3184,8 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "dev": true, "license": "MIT", "optional": true, @@ -2025,6 +3195,8 @@ }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2036,6 +3208,8 @@ }, "node_modules/environment": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", "engines": { @@ -2047,6 +3221,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -2055,6 +3231,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -2063,11 +3241,15 @@ }, "node_modules/es-module-lexer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -2079,6 +3261,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -2093,6 +3277,8 @@ }, "node_modules/esbuild": { "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2133,6 +3319,8 @@ }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -2144,6 +3332,8 @@ }, "node_modules/eslint": { "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", "dependencies": { @@ -2198,6 +3388,8 @@ }, "node_modules/eslint-scope": { "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2215,6 +3407,8 @@ }, "node_modules/eslint-visitor-keys": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2226,6 +3420,8 @@ }, "node_modules/eslint/node_modules/ajv": { "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2241,6 +3437,8 @@ }, "node_modules/eslint/node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -2249,6 +3447,8 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2260,6 +3460,8 @@ }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -2271,6 +3473,8 @@ }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -2279,11 +3483,15 @@ }, "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2298,6 +3506,8 @@ }, "node_modules/espree": { "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2314,6 +3524,8 @@ }, "node_modules/esquery": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2325,6 +3537,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2336,6 +3550,8 @@ }, "node_modules/estraverse": { "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2344,6 +3560,8 @@ }, "node_modules/estree-walker": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -2352,6 +3570,8 @@ }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2360,6 +3580,8 @@ }, "node_modules/expand-template": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "dev": true, "license": "(MIT OR WTFPL)", "optional": true, @@ -2369,6 +3591,8 @@ }, "node_modules/expect-type": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2377,11 +3601,15 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -2397,16 +3625,22 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -2422,6 +3656,8 @@ }, "node_modules/fastq": { "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -2430,6 +3666,8 @@ }, "node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -2446,6 +3684,8 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2457,6 +3697,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2468,6 +3710,8 @@ }, "node_modules/find-up": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", "dependencies": { @@ -2483,6 +3727,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -2495,11 +3741,15 @@ }, "node_modules/flatted": { "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -2515,6 +3765,8 @@ }, "node_modules/form-data": { "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, "license": "MIT", "dependencies": { @@ -2530,12 +3782,16 @@ }, "node_modules/fs-constants": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, "license": "MIT", "optional": true }, "node_modules/fs-extra": { "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -2547,8 +3803,25 @@ "node": ">=14.14" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -2557,6 +3830,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2580,6 +3855,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -2592,12 +3869,17 @@ }, "node_modules/github-from-package": { "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/glob": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2620,6 +3902,8 @@ }, "node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -2631,6 +3915,8 @@ }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { @@ -2639,6 +3925,8 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -2650,6 +3938,8 @@ }, "node_modules/glob/node_modules/minimatch": { "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2664,6 +3954,8 @@ }, "node_modules/globals": { "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { @@ -2675,6 +3967,8 @@ }, "node_modules/globby": { "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", "dev": true, "license": "MIT", "dependencies": { @@ -2694,6 +3988,8 @@ }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -2705,11 +4001,15 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, "license": "ISC" }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -2718,6 +4018,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -2729,6 +4031,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { @@ -2743,6 +4047,8 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2754,6 +4060,8 @@ }, "node_modules/hosted-git-info": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", "dev": true, "license": "ISC", "dependencies": { @@ -2765,6 +4073,8 @@ }, "node_modules/htmlparser2": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -2783,6 +4093,8 @@ }, "node_modules/htmlparser2/node_modules/entities": { "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2794,6 +4106,8 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { @@ -2806,6 +4120,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { @@ -2818,6 +4134,8 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2829,6 +4147,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "dev": true, "funding": [ { @@ -2849,6 +4169,8 @@ }, "node_modules/ignore": { "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -2857,6 +4179,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", "engines": { @@ -2865,6 +4189,8 @@ }, "node_modules/index-to-position": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", "dev": true, "license": "MIT", "engines": { @@ -2876,18 +4202,24 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC", "optional": true }, "node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true, "license": "ISC", "optional": true }, "node_modules/is-docker": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, "license": "MIT", "bin": { @@ -2902,6 +4234,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -2910,6 +4244,8 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -2918,6 +4254,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -2929,6 +4267,8 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2946,6 +4286,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -2954,6 +4296,8 @@ }, "node_modules/is-wsl": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -2968,11 +4312,15 @@ }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/istextorbinary": { "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -2989,6 +4337,8 @@ }, "node_modules/jackspeak": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3003,11 +4353,15 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3019,21 +4373,29 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -3045,11 +4407,15 @@ }, "node_modules/jsonc-parser": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -3061,6 +4427,8 @@ }, "node_modules/jsonwebtoken": { "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "dev": true, "license": "MIT", "dependencies": { @@ -3082,6 +4450,8 @@ }, "node_modules/jwa": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, "license": "MIT", "dependencies": { @@ -3092,6 +4462,8 @@ }, "node_modules/jws": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", "dependencies": { @@ -3101,6 +4473,8 @@ }, "node_modules/keytar": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3112,6 +4486,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -3120,6 +4496,8 @@ }, "node_modules/leven": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, "license": "MIT", "engines": { @@ -3128,6 +4506,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3140,6 +4520,8 @@ }, "node_modules/lightningcss": { "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -3166,8 +4548,157 @@ "lightningcss-win32-x64-msvc": "1.32.0" } }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -3187,6 +4718,8 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -3204,8 +4737,52 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3214,6 +4791,8 @@ }, "node_modules/locate-path": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", "dependencies": { @@ -3228,51 +4807,71 @@ }, "node_modules/lodash": { "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, "node_modules/lodash.includes": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "dev": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "dev": true, "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "dev": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true, "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true, "license": "MIT" }, "node_modules/lodash.truncate": { "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true, "license": "MIT" }, "node_modules/lru-cache": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "license": "ISC", "dependencies": { @@ -3284,6 +4883,8 @@ }, "node_modules/magic-string": { "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3292,6 +4893,8 @@ }, "node_modules/markdown-it": { "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -3308,6 +4911,8 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -3316,11 +4921,15 @@ }, "node_modules/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -3329,6 +4938,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -3341,6 +4952,8 @@ }, "node_modules/micromatch/node_modules/picomatch": { "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3352,6 +4965,8 @@ }, "node_modules/mime": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "license": "MIT", "bin": { @@ -3363,6 +4978,8 @@ }, "node_modules/mime-db": { "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", "engines": { @@ -3371,6 +4988,8 @@ }, "node_modules/mime-types": { "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", "dependencies": { @@ -3382,6 +5001,8 @@ }, "node_modules/mimic-response": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "dev": true, "license": "MIT", "optional": true, @@ -3394,6 +5015,8 @@ }, "node_modules/minimatch": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3405,6 +5028,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", "optional": true, @@ -3414,6 +5039,8 @@ }, "node_modules/minipass": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3422,22 +5049,30 @@ }, "node_modules/mkdirp-classic": { "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true, "license": "MIT", "optional": true }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true, "license": "ISC" }, "node_modules/nanoid": { "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3455,17 +5090,23 @@ }, "node_modules/napi-build-utils": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "dev": true, "license": "MIT", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, "node_modules/node-abi": { "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", "dev": true, "license": "MIT", "optional": true, @@ -3478,12 +5119,16 @@ }, "node_modules/node-addon-api": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", "dev": true, "license": "MIT", "optional": true }, "node_modules/node-sarif-builder": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", "dev": true, "license": "MIT", "dependencies": { @@ -3496,6 +5141,8 @@ }, "node_modules/normalize-package-data": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3509,6 +5156,8 @@ }, "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, "license": "ISC", "dependencies": { @@ -3520,11 +5169,15 @@ }, "node_modules/normalize-package-data/node_modules/lru-cache": { "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, "license": "ISC" }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3536,6 +5189,8 @@ }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -3547,6 +5202,8 @@ }, "node_modules/obug": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -3556,6 +5213,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "optional": true, @@ -3565,6 +5224,8 @@ }, "node_modules/open": { "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", "dev": true, "license": "MIT", "dependencies": { @@ -3582,6 +5243,8 @@ }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -3598,6 +5261,8 @@ }, "node_modules/p-limit": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3612,6 +5277,8 @@ }, "node_modules/p-locate": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -3626,6 +5293,8 @@ }, "node_modules/p-map": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", "dev": true, "license": "MIT", "engines": { @@ -3637,11 +5306,15 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parse-json": { "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3658,6 +5331,8 @@ }, "node_modules/parse-semver": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3666,6 +5341,8 @@ }, "node_modules/parse-semver/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "license": "ISC", "bin": { @@ -3674,6 +5351,8 @@ }, "node_modules/parse5": { "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -3685,6 +5364,8 @@ }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { @@ -3697,6 +5378,8 @@ }, "node_modules/parse5-parser-stream": { "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { @@ -3708,6 +5391,8 @@ }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -3719,6 +5404,8 @@ }, "node_modules/path-exists": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { @@ -3727,6 +5414,8 @@ }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -3735,6 +5424,8 @@ }, "node_modules/path-scurry": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -3750,6 +5441,8 @@ }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3758,6 +5451,8 @@ }, "node_modules/path-type": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", "dev": true, "license": "MIT", "engines": { @@ -3769,21 +5464,29 @@ }, "node_modules/pathe": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/pend": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3795,6 +5498,8 @@ }, "node_modules/pkijs": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", "license": "BSD-3-Clause", "dependencies": { "@noble/hashes": "1.4.0", @@ -3810,6 +5515,8 @@ }, "node_modules/pluralize": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true, "license": "MIT", "engines": { @@ -3818,6 +5525,8 @@ }, "node_modules/postcss": { "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -3845,6 +5554,9 @@ }, "node_modules/prebuild-install": { "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "dev": true, "license": "MIT", "optional": true, @@ -3871,6 +5583,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -3879,6 +5593,8 @@ }, "node_modules/pump": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "optional": true, @@ -3889,6 +5605,8 @@ }, "node_modules/punycode": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", "engines": { @@ -3897,6 +5615,8 @@ }, "node_modules/punycode.js": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "dev": true, "license": "MIT", "engines": { @@ -3905,6 +5625,8 @@ }, "node_modules/pvtsutils": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", "license": "MIT", "dependencies": { "tslib": "^2.8.1" @@ -3912,6 +5634,8 @@ }, "node_modules/pvutils": { "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", "license": "MIT", "engines": { "node": ">=16.0.0" @@ -3919,6 +5643,8 @@ }, "node_modules/qs": { "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3933,6 +5659,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -3952,6 +5680,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "optional": true, @@ -3967,6 +5697,8 @@ }, "node_modules/rc-config-loader": { "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3978,6 +5710,8 @@ }, "node_modules/read": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3989,6 +5723,8 @@ }, "node_modules/read-pkg": { "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, "license": "MIT", "dependencies": { @@ -4007,6 +5743,8 @@ }, "node_modules/read-pkg/node_modules/unicorn-magic": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, "license": "MIT", "engines": { @@ -4018,6 +5756,8 @@ }, "node_modules/readable-stream": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "license": "MIT", "optional": true, @@ -4032,10 +5772,14 @@ }, "node_modules/reflect-metadata": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "license": "Apache-2.0" }, "node_modules/require-from-string": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -4044,6 +5788,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -4053,6 +5799,8 @@ }, "node_modules/rolldown": { "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { @@ -4085,6 +5833,8 @@ }, "node_modules/run-applescript": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", "dev": true, "license": "MIT", "engines": { @@ -4096,6 +5846,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -4118,6 +5870,8 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -4137,11 +5891,15 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/sax": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -4150,6 +5908,8 @@ }, "node_modules/secretlint": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", "dev": true, "license": "MIT", "dependencies": { @@ -4170,6 +5930,8 @@ }, "node_modules/semver": { "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4181,6 +5943,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -4192,6 +5956,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -4200,6 +5966,8 @@ }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -4218,6 +5986,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { @@ -4233,6 +6003,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -4250,6 +6022,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -4268,11 +6042,15 @@ }, "node_modules/siginfo": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -4284,6 +6062,8 @@ }, "node_modules/simple-concat": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", "dev": true, "funding": [ { @@ -4304,6 +6084,8 @@ }, "node_modules/simple-get": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", "dev": true, "funding": [ { @@ -4329,6 +6111,8 @@ }, "node_modules/slash": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "license": "MIT", "engines": { @@ -4340,6 +6124,8 @@ }, "node_modules/slice-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4356,6 +6142,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4364,6 +6152,8 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4373,11 +6163,15 @@ }, "node_modules/spdx-exceptions": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4387,21 +6181,29 @@ }, "node_modules/spdx-license-ids": { "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "dev": true, "license": "CC0-1.0" }, "node_modules/stackback": { "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/std-env": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, "node_modules/string_decoder": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dev": true, "license": "MIT", "optional": true, @@ -4411,6 +6213,8 @@ }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -4424,6 +6228,8 @@ }, "node_modules/string-width/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -4432,6 +6238,8 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -4443,6 +6251,8 @@ }, "node_modules/strip-ansi": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { @@ -4457,6 +6267,8 @@ }, "node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, "license": "MIT", "optional": true, @@ -4466,6 +6278,8 @@ }, "node_modules/structured-source": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4474,6 +6288,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -4485,6 +6301,8 @@ }, "node_modules/supports-hyperlinks": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", "dev": true, "license": "MIT", "dependencies": { @@ -4500,6 +6318,8 @@ }, "node_modules/table": { "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4515,6 +6335,8 @@ }, "node_modules/table/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -4523,6 +6345,8 @@ }, "node_modules/table/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -4534,6 +6358,8 @@ }, "node_modules/tar-fs": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "license": "MIT", "optional": true, @@ -4546,6 +6372,8 @@ }, "node_modules/tar-stream": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", "optional": true, @@ -4562,6 +6390,8 @@ }, "node_modules/terminal-link": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", "dev": true, "license": "MIT", "dependencies": { @@ -4577,11 +6407,15 @@ }, "node_modules/text-table": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, "license": "MIT" }, "node_modules/textextensions": { "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", "dev": true, "license": "Artistic-2.0", "dependencies": { @@ -4596,11 +6430,15 @@ }, "node_modules/tinybench": { "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", "dev": true, "license": "MIT", "engines": { @@ -4609,6 +6447,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -4624,6 +6464,8 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -4632,6 +6474,8 @@ }, "node_modules/tmp": { "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -4640,6 +6484,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4651,6 +6497,8 @@ }, "node_modules/ts-api-utils": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -4662,10 +6510,14 @@ }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", "license": "MIT", "dependencies": { "tslib": "^1.9.3" @@ -4676,10 +6528,14 @@ }, "node_modules/tsyringe/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/tunnel": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "dev": true, "license": "MIT", "engines": { @@ -4688,6 +6544,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, "license": "Apache-2.0", "optional": true, @@ -4700,6 +6558,8 @@ }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -4711,6 +6571,8 @@ }, "node_modules/type-fest": { "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -4722,6 +6584,8 @@ }, "node_modules/typed-rest-client": { "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", "dev": true, "license": "MIT", "dependencies": { @@ -4732,6 +6596,8 @@ }, "node_modules/typescript": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4744,6 +6610,8 @@ }, "node_modules/typescript-eslint": { "version": "8.59.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.2.tgz", + "integrity": "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4766,16 +6634,22 @@ }, "node_modules/uc.micro": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true, "license": "MIT" }, "node_modules/underscore": { "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", "dev": true, "license": "MIT" }, "node_modules/undici": { "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -4784,11 +6658,15 @@ }, "node_modules/undici-types": { "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", "dev": true, "license": "MIT", "engines": { @@ -4800,6 +6678,8 @@ }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -4808,6 +6688,8 @@ }, "node_modules/uri-js": { "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4816,17 +6698,23 @@ }, "node_modules/url-join": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true, "license": "MIT" }, "node_modules/util-deprecate": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true, "license": "MIT", "optional": true }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4836,6 +6724,8 @@ }, "node_modules/version-range": { "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", "dev": true, "license": "Artistic-2.0", "engines": { @@ -4847,6 +6737,8 @@ }, "node_modules/vite": { "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { @@ -4923,6 +6815,8 @@ }, "node_modules/vitest": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", "dependencies": { @@ -5011,6 +6905,9 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -5022,6 +6919,8 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -5030,6 +6929,8 @@ }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -5044,6 +6945,8 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -5059,6 +6962,8 @@ }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -5067,12 +6972,16 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC", "optional": true }, "node_modules/wsl-utils": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", "dev": true, "license": "MIT", "dependencies": { @@ -5087,6 +6996,8 @@ }, "node_modules/xml2js": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "license": "MIT", "dependencies": { @@ -5099,6 +7010,8 @@ }, "node_modules/xmlbuilder": { "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "license": "MIT", "engines": { @@ -5107,11 +7020,15 @@ }, "node_modules/yallist": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true, "license": "ISC" }, "node_modules/yauzl": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5124,6 +7041,8 @@ }, "node_modules/yazl": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", "dev": true, "license": "MIT", "dependencies": { @@ -5132,6 +7051,8 @@ }, "node_modules/yocto-queue": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", "engines": { From 6bf93f8fc6bd16df903e5845c35707c2d52f3dd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 05:11:07 +0000 Subject: [PATCH 41/41] Fix OID inspector in macOS PFX format compatibility test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pkijs.PFX.fromBER` populates the outer schema but not `parsedValue` — that requires `parseInternalValues`. My inspector skipped that step and also had an extra `.parsedValue` layer in the walk path, so it always returned null on CI even though the PFX itself loaded fine. The diagnostic line in the previous run made this visible: observedPbeOid: (unknown), result: {"kind":"ok","thumbprint":"…"} Mirror `parsePfx`'s walk: call `parseInternalValues` with an empty password and `checkIntegrity: false` (we're reading headers, not verifying or decrypting), then iterate `pfx.parsedValue.authenticatedSafe .safeContents`. Verified locally against `test/fixtures/pkcs12-legacy- 3des.pfx`, which now correctly reports `1.2.840.113549.1.12.1.3`. --- .../dotnetMacosCache.integration.test.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts index 0e76aee..61b36a6 100644 --- a/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts +++ b/src/vscode-ui-extension/tests/dotnetMacosCache.integration.test.ts @@ -101,7 +101,7 @@ beforeAll(async () => { } cachePfxPath = path.join(cacheDir, pfxes[0]); - observedPbeOid = inspectFirstEncryptedDataOid(cachePfxPath); + observedPbeOid = await inspectFirstEncryptedDataOid(cachePfxPath); try { const loaded = await loadPfx(cachePfxPath); @@ -186,20 +186,28 @@ describe.skipIf(!ready)( * Walks pkijs's parsed structure directly rather than going through * `parsePfx` so it works regardless of whether the legacy handler can * decode the file — the OID is observable from the headers alone, no - * password needed. + * password needed. `parseInternalValues` populates `pfx.parsedValue`; + * `checkIntegrity: false` skips the HMAC step (which `parsePfx` itself + * skips for the same reason — pkijs's MAC verification disagrees with + * .NET's empty-password convention). * * Returns null if the structure doesn't contain an `EncryptedData` * (`Data`-typed contents are unencrypted, no OID to report). */ -function inspectFirstEncryptedDataOid(pfxPath: string): string | null { +async function inspectFirstEncryptedDataOid( + pfxPath: string +): Promise { const bytes = fs.readFileSync(pfxPath); const ab = new ArrayBuffer(bytes.byteLength); new Uint8Array(ab).set(bytes); const pfx = pkijs.PFX.fromBER(ab); - const outerContent = pfx.parsedValue?.authenticatedSafe?.parsedValue - ?.safeContents as ReadonlyArray | undefined; - if (!outerContent) return null; - for (const contentInfo of outerContent) { + await pfx.parseInternalValues({ + password: new ArrayBuffer(0), + checkIntegrity: false, + }); + const authSafe = pfx.parsedValue?.authenticatedSafe; + if (!authSafe) return null; + for (const contentInfo of authSafe.safeContents) { // 1.2.840.113549.1.7.6 = pkcs-7-encryptedData if (contentInfo.contentType !== "1.2.840.113549.1.7.6") continue; const encryptedData = new pkijs.EncryptedData({