mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
Compare commits
14 Commits
dev
...
fix/contai
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a68b31234b | ||
|
|
1c6163149e | ||
|
|
178cd95839 | ||
|
|
3a8a0f8a7f | ||
|
|
e590b313fe | ||
|
|
6a03275b2a | ||
|
|
d54a2f3980 | ||
|
|
0279c0708b | ||
|
|
695c775b87 | ||
|
|
aedb6634e5 | ||
|
|
67153772a0 | ||
|
|
29f8cc718f | ||
|
|
5adf119c3b | ||
|
|
ab82f4576a |
170
.github/workflows/build-agent-container.yml
vendored
170
.github/workflows/build-agent-container.yml
vendored
@@ -1,170 +0,0 @@
|
||||
name: Build Agent Container
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
agent:
|
||||
description: Optional agent filter from recipe/agents.json
|
||||
required: false
|
||||
type: string
|
||||
version:
|
||||
description: Optional dry-run version override for the selected agent
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: Publish artifacts to R2
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-agent-container.yml
|
||||
- packages/browseros-agent/packages/agent-container/**
|
||||
schedule:
|
||||
- cron: '0 7 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
list-matrix:
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/browseros-agent
|
||||
outputs:
|
||||
matrix: ${{ steps.matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun ci
|
||||
|
||||
- name: Typecheck package
|
||||
run: bun run --filter @browseros/agent-container typecheck
|
||||
|
||||
- name: Run package tests
|
||||
run: bun run --filter @browseros/agent-container test
|
||||
|
||||
- name: Compute matrix
|
||||
id: matrix
|
||||
env:
|
||||
INPUT_AGENT: ${{ inputs.agent }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=()
|
||||
if [ -n "$INPUT_AGENT" ]; then
|
||||
args+=(--agent "$INPUT_AGENT")
|
||||
fi
|
||||
matrix="$(bun run packages/agent-container/scripts/list-matrix.ts "${args[@]}")"
|
||||
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
needs: list-matrix
|
||||
runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/browseros-agent
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.list-matrix.outputs.matrix) }}
|
||||
env:
|
||||
ARTIFACT_ROOT: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container
|
||||
OUTPUT_DIR: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container/${{ matrix.agent }}/${{ matrix.arch }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun ci
|
||||
|
||||
- name: Install podman
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y podman
|
||||
|
||||
- name: Build tarball
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cmd=(
|
||||
bun run --filter @browseros/agent-container build --
|
||||
--agent "${{ matrix.agent }}"
|
||||
--arch "${{ matrix.arch }}"
|
||||
--output-dir "$OUTPUT_DIR"
|
||||
)
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "$INPUT_VERSION" ]; then
|
||||
cmd+=(--version "$INPUT_VERSION")
|
||||
fi
|
||||
"${cmd[@]}"
|
||||
|
||||
- name: Smoke test archive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
result_json="$OUTPUT_DIR/build-result.json"
|
||||
tarball="$(bun -e "const result = await Bun.file(process.argv[1]).json(); console.log(result.tarballPath)" "$result_json")"
|
||||
expected_image="$(bun -e 'const result = await Bun.file(process.argv[1]).json(); console.log(result.image + ":" + result.version)' "$result_json")"
|
||||
expected_fingerprint="$(bun -e "const result = await Bun.file(process.argv[1]).json(); console.log(result.smokeFingerprint)" "$result_json")"
|
||||
bun run --filter @browseros/agent-container smoke -- \
|
||||
--tarball "$tarball" \
|
||||
--expected-image "$expected_image" \
|
||||
--expected-fingerprint "$expected_fingerprint"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-container-${{ matrix.agent }}-${{ matrix.arch }}
|
||||
path: ${{ env.ARTIFACT_ROOT }}
|
||||
|
||||
publish:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }}
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: packages/browseros-agent
|
||||
env:
|
||||
ARTIFACT_ROOT: ${{ github.workspace }}/packages/browseros-agent/packages/agent-container/dist/agent-container
|
||||
steps:
|
||||
- name: Guard recipe source of truth
|
||||
if: ${{ inputs.version != '' }}
|
||||
run: |
|
||||
echo "Refusing to publish a workflow_dispatch version override. Update recipe/agents.json instead." >&2
|
||||
exit 1
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun ci
|
||||
|
||||
- name: Download matrix artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: agent-container-*
|
||||
merge-multiple: true
|
||||
path: ${{ env.ARTIFACT_ROOT }}
|
||||
|
||||
- name: Publish to R2
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bun run --filter @browseros/agent-container upload -- \
|
||||
--artifact-dir "$ARTIFACT_ROOT" \
|
||||
--update-aggregate
|
||||
158
.github/workflows/build-agent.yml
vendored
Normal file
158
.github/workflows/build-agent.yml
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
name: build-agent
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
agent:
|
||||
description: "Agent name from bundle.json"
|
||||
required: true
|
||||
type: string
|
||||
default: openclaw
|
||||
publish:
|
||||
description: "Upload to R2 and merge manifest slice"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/browseros-agent/packages/build-tools/**"
|
||||
- "!packages/browseros-agent/packages/build-tools/scripts/build-disk.ts"
|
||||
- ".github/workflows/build-agent.yml"
|
||||
|
||||
env:
|
||||
BUN_VERSION: "1.3.6"
|
||||
PKG_DIR: packages/browseros-agent/packages/build-tools
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/build-tools typecheck
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/build-tools test
|
||||
|
||||
build:
|
||||
needs: check
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- name: Install podman
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y podman
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Build tarball
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
AGENT: ${{ inputs.agent || 'openclaw' }}
|
||||
OUT: ${{ github.workspace }}/dist/images
|
||||
run: bun run build:tarball -- --agent "$AGENT" --arch "${{ matrix.arch }}" --output-dir "$OUT"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tarball-${{ inputs.agent || 'openclaw' }}-${{ matrix.arch }}
|
||||
path: dist/images/
|
||||
retention-days: 7
|
||||
|
||||
smoke:
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tarball-${{ inputs.agent || 'openclaw' }}-arm64
|
||||
path: dist/images
|
||||
- name: Install podman
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y podman
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Smoke test tarball
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
AGENT: ${{ inputs.agent || 'openclaw' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tarball="$(find "$GITHUB_WORKSPACE/dist/images" -name "${AGENT}-*-arm64.tar.gz" -print -quit)"
|
||||
if [ -z "$tarball" ]; then
|
||||
echo "missing arm64 tarball artifact for ${AGENT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
bun run smoke:tarball -- --agent "$AGENT" --arch arm64 --tarball "$tarball"
|
||||
|
||||
publish:
|
||||
needs: [build, smoke]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: release
|
||||
concurrency:
|
||||
group: r2-manifest-publish
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: tarball-*
|
||||
path: dist/images
|
||||
merge-multiple: true
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Upload tarballs to R2
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for file in "$GITHUB_WORKSPACE"/dist/images/*.tar.gz; do
|
||||
base="$(basename "$file")"
|
||||
bun run upload -- --file "$file" --key "vm/images/$base" --content-type "application/gzip" --sidecar-sha
|
||||
done
|
||||
- name: Merge agent slice into manifest
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
AGENT: ${{ inputs.agent || 'openclaw' }}
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist/images
|
||||
cp -R "$GITHUB_WORKSPACE"/dist/images/* dist/images/
|
||||
bun run download -- --key vm/manifest.json --out dist/baseline-manifest.json
|
||||
bun run emit-manifest -- \
|
||||
--slice "agents:${AGENT}" \
|
||||
--dist-dir dist \
|
||||
--merge-from dist/baseline-manifest.json \
|
||||
--out dist/manifest.json
|
||||
bun run upload -- --file dist/manifest.json --key vm/manifest.json --content-type "application/json"
|
||||
184
.github/workflows/build-vm-container.yml
vendored
184
.github/workflows/build-vm-container.yml
vendored
@@ -1,184 +0,0 @@
|
||||
name: build-vm-container
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "CalVer identifier (e.g., 2026.04.22-1)"
|
||||
required: true
|
||||
type: string
|
||||
base_image_sha256:
|
||||
description: "Verify base image locally against this sha256 (debugging only)"
|
||||
required: false
|
||||
type: string
|
||||
publish:
|
||||
description: "Upload to R2 and update latest.json"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/browseros-agent/packages/vm-container/**"
|
||||
- ".github/workflows/build-vm-container.yml"
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
|
||||
env:
|
||||
BUN_VERSION: "1.3.6"
|
||||
PKG_DIR: packages/browseros-agent/packages/vm-container
|
||||
ARTIFACT_RETENTION_DAYS: 7
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# PRs run the x64 lane only until this repo has dependable arm64 runner
|
||||
# capacity. Manual/scheduled runs still request the native arm64 lane.
|
||||
matrix: ${{ fromJSON(github.event_name == 'pull_request' && '{"include":[{"arch":"x64","runner":"ubuntu-24.04"}]}' || '{"include":[{"arch":"arm64","runner":"ubuntu-24.04-arm64"},{"arch":"x64","runner":"ubuntu-24.04"}]}') }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- name: Install libguestfs + qemu-utils + zstd
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libguestfs-tools qemu-utils zstd
|
||||
echo "LIBGUESTFS_BACKEND=direct" >> "$GITHUB_ENV"
|
||||
# Ubuntu 24.04 ships /boot/vmlinuz-* as mode 0600, which breaks
|
||||
# libguestfs's supermin appliance build when virt-customize runs
|
||||
# as a non-root user. Loosen to 0644 so supermin can read them.
|
||||
sudo chmod 0644 /boot/vmlinuz-* || true
|
||||
sudo chmod 0644 /boot/initrd.img-* || true
|
||||
# Give the runner user access to /dev/kvm so libguestfs can use
|
||||
# hardware acceleration instead of slow TCG emulation.
|
||||
if [ -e /dev/kvm ]; then
|
||||
sudo chmod 0666 /dev/kvm
|
||||
fi
|
||||
# libguestfs 1.54+ prefers `passt` for guest networking, but the
|
||||
# passt build on GHA ubuntu-24.04 fails under AppArmor when spawned
|
||||
# from the appliance (exits with status 1, no useful diagnostics).
|
||||
# Move the binary aside so libguestfs falls back to QEMU's built-in
|
||||
# user-mode SLIRP networking, which works fine.
|
||||
if [ -x /usr/bin/passt ]; then
|
||||
sudo mv /usr/bin/passt /usr/bin/passt.disabled
|
||||
fi
|
||||
- name: Install deps
|
||||
working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Typecheck
|
||||
working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/vm-container typecheck
|
||||
- name: Unit tests
|
||||
working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/vm-container test
|
||||
- name: Determine version
|
||||
id: version
|
||||
env:
|
||||
VERSION_INPUT: ${{ inputs.version || '' }}
|
||||
run: |
|
||||
if [ -n "$VERSION_INPUT" ]; then
|
||||
echo "value=$VERSION_INPUT" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "value=$(date -u +%Y.%m.%d)-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- name: Build
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
VERSION_VALUE: ${{ steps.version.outputs.value }}
|
||||
ARCH_VALUE: ${{ matrix.arch }}
|
||||
OUTPUT_DIR: ${{ github.workspace }}/dist/vm/${{ matrix.arch }}
|
||||
BASE_IMAGE_SHA256_INPUT: ${{ inputs.base_image_sha256 || '' }}
|
||||
run: |
|
||||
cmd=(
|
||||
bun run build --
|
||||
--version "$VERSION_VALUE"
|
||||
--arch "$ARCH_VALUE"
|
||||
--output-dir "$OUTPUT_DIR"
|
||||
)
|
||||
if [ -n "$BASE_IMAGE_SHA256_INPUT" ]; then
|
||||
cmd+=(--base-image-sha256 "$BASE_IMAGE_SHA256_INPUT")
|
||||
fi
|
||||
"${cmd[@]}"
|
||||
- name: Dump build log on failure
|
||||
if: ${{ failure() }}
|
||||
run: |
|
||||
LOG_PATH="$GITHUB_WORKSPACE/dist/vm/${{ matrix.arch }}/build-${{ matrix.arch }}.log"
|
||||
if [ -f "$LOG_PATH" ]; then
|
||||
echo "::group::$(basename "$LOG_PATH")"
|
||||
tail -n 200 "$LOG_PATH"
|
||||
echo "::endgroup::"
|
||||
else
|
||||
echo "missing build log: $LOG_PATH"
|
||||
fi
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vm-disk-${{ matrix.arch }}
|
||||
path: dist/vm/${{ matrix.arch }}/
|
||||
retention-days: ${{ env.ARTIFACT_RETENTION_DAYS }}
|
||||
|
||||
smoke:
|
||||
needs: [build]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vm-disk-x64
|
||||
path: dist/vm/x64/
|
||||
- name: Install qemu + zstd + Lima
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y qemu-system-x86 qemu-utils zstd curl
|
||||
LIMA_VERSION=$(cat "${PKG_DIR}/recipe/LIMA_VERSION.pin")
|
||||
LIMA_SHA256=$(cat "${PKG_DIR}/recipe/LIMA_LINUX_X86_64_SHA256.pin")
|
||||
curl -fsSL -o /tmp/lima.tar.gz \
|
||||
"https://github.com/lima-vm/lima/releases/download/${LIMA_VERSION}/lima-${LIMA_VERSION#v}-Linux-x86_64.tar.gz"
|
||||
echo "${LIMA_SHA256} /tmp/lima.tar.gz" | sha256sum --check --strict
|
||||
sudo tar -C /usr/local -xzf /tmp/lima.tar.gz
|
||||
- name: Install deps
|
||||
working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Smoke test
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
run: |
|
||||
QCOW=$(ls "$GITHUB_WORKSPACE/dist/vm/x64/"browseros-vm-*-x64.qcow2.zst | head -n1)
|
||||
bun run smoke -- --qcow "$QCOW" --limactl /usr/local/bin/limactl
|
||||
|
||||
publish:
|
||||
needs: [build, smoke]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: release
|
||||
concurrency:
|
||||
group: vm-publish-${{ inputs.version }}
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: vm-disk-*
|
||||
path: dist/vm/
|
||||
- name: Install deps
|
||||
working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Upload to R2
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
VERSION_INPUT: ${{ inputs.version }}
|
||||
run: |
|
||||
bun run upload -- \
|
||||
--version "$VERSION_INPUT" \
|
||||
--artifact-dir "$GITHUB_WORKSPACE/dist/vm"
|
||||
179
.github/workflows/build-vm.yml
vendored
Normal file
179
.github/workflows/build-vm.yml
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
name: build-vm
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "VM version (e.g. 2026.04.22)"
|
||||
required: true
|
||||
type: string
|
||||
publish:
|
||||
description: "Upload to R2 and merge manifest slice"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/browseros-agent/packages/build-tools/**"
|
||||
- "!packages/browseros-agent/packages/build-tools/scripts/build-tarball.ts"
|
||||
- ".github/workflows/build-vm.yml"
|
||||
|
||||
env:
|
||||
BUN_VERSION: "1.3.6"
|
||||
PKG_DIR: packages/browseros-agent/packages/build-tools
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/build-tools typecheck
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun run --filter @browseros/build-tools test
|
||||
|
||||
build:
|
||||
needs: check
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- name: Install libguestfs, qemu, and zstd
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libguestfs-tools qemu-utils zstd
|
||||
sudo chmod 0644 /boot/vmlinuz-* /boot/initrd.img-* || true
|
||||
[ -e /dev/kvm ] && sudo chmod 0666 /dev/kvm || true
|
||||
[ -x /usr/bin/passt ] && sudo mv /usr/bin/passt /usr/bin/passt.disabled || true
|
||||
echo "LIBGUESTFS_BACKEND=direct" >> "$GITHUB_ENV"
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Validate VM version input
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bundle_version="$(bun -e "const b = await Bun.file('bundle.json').json(); console.log(b.vmVersion)")"
|
||||
if [ "$VERSION" != "$bundle_version" ]; then
|
||||
echo "inputs.version ($VERSION) must match bundle.json vmVersion ($bundle_version)" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Build disk
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
VERSION: ${{ inputs.version || format('pr-{0}', github.event.pull_request.number) }}
|
||||
OUT: ${{ github.workspace }}/dist/vm
|
||||
run: bun run build:disk -- --version "$VERSION" --arch "${{ matrix.arch }}" --output-dir "$OUT"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vm-disk-${{ matrix.arch }}
|
||||
path: dist/vm/
|
||||
retention-days: 7
|
||||
|
||||
smoke:
|
||||
needs: build
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vm-disk-arm64
|
||||
path: dist/vm
|
||||
- name: Install qemu, zstd, curl, and Lima
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y qemu-system-arm qemu-utils zstd curl
|
||||
lima_version="$(cat "${PKG_DIR}/recipe/LIMA_VERSION.pin")"
|
||||
lima_sha256="$(cat "${PKG_DIR}/recipe/LIMA_LINUX_AARCH64_SHA256.pin")"
|
||||
curl -fsSL -o /tmp/lima.tar.gz \
|
||||
"https://github.com/lima-vm/lima/releases/download/${lima_version}/lima-${lima_version#v}-Linux-aarch64.tar.gz"
|
||||
echo "${lima_sha256} /tmp/lima.tar.gz" | sha256sum --check --strict
|
||||
sudo tar -C /usr/local -xzf /tmp/lima.tar.gz
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Smoke test VM disk
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
qcow="$(find "$GITHUB_WORKSPACE/dist/vm" -name 'browseros-vm-*-arm64.qcow2.zst' -print -quit)"
|
||||
if [ -z "$qcow" ]; then
|
||||
echo "missing arm64 VM disk artifact" >&2
|
||||
exit 1
|
||||
fi
|
||||
bun run smoke:vm -- --arch arm64 --qcow "$qcow" --limactl /usr/local/bin/limactl
|
||||
|
||||
publish:
|
||||
needs: [build, smoke]
|
||||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: release
|
||||
concurrency:
|
||||
group: r2-manifest-publish
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: vm-disk-*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
- working-directory: packages/browseros-agent
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Upload VM disks to R2
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for file in "$GITHUB_WORKSPACE"/dist/*.qcow2.zst; do
|
||||
base="$(basename "$file")"
|
||||
bun run upload -- --file "$file" --key "vm/$base" --content-type "application/zstd" --sidecar-sha
|
||||
done
|
||||
- name: Merge VM slice into manifest
|
||||
working-directory: ${{ env.PKG_DIR }}
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist
|
||||
cp -R "$GITHUB_WORKSPACE"/dist/* dist/
|
||||
bun run download -- --key vm/manifest.json --out dist/baseline-manifest.json
|
||||
bun run emit-manifest -- \
|
||||
--slice vm \
|
||||
--dist-dir dist \
|
||||
--merge-from dist/baseline-manifest.json \
|
||||
--out dist/manifest.json
|
||||
bun run upload -- --file dist/manifest.json --key vm/manifest.json --content-type "application/json"
|
||||
@@ -6,12 +6,10 @@ import { PATHS } from '@browseros/shared/constants/paths'
|
||||
import type { ServerDiscoveryConfig } from '@browseros/shared/types/server-config'
|
||||
import { logger } from './logger'
|
||||
|
||||
const DEV_BROWSEROS_DIR_NAME = '.browseros-dev'
|
||||
|
||||
export function getBrowserosDir(): string {
|
||||
const dirName =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? DEV_BROWSEROS_DIR_NAME
|
||||
? PATHS.DEV_BROWSEROS_DIR_NAME
|
||||
: PATHS.BROWSEROS_DIR_NAME
|
||||
return join(homedir(), dirName)
|
||||
}
|
||||
@@ -49,6 +47,18 @@ export function getOpenClawDir(): string {
|
||||
return join(getBrowserosDir(), PATHS.OPENCLAW_DIR_NAME)
|
||||
}
|
||||
|
||||
export function getCacheDir(): string {
|
||||
return join(getBrowserosDir(), PATHS.CACHE_DIR_NAME)
|
||||
}
|
||||
|
||||
export function getVmCacheDir(): string {
|
||||
return join(getCacheDir(), 'vm')
|
||||
}
|
||||
|
||||
export function getAgentCacheDir(): string {
|
||||
return join(getVmCacheDir(), 'images')
|
||||
}
|
||||
|
||||
export function getLazyMonitoringDir(): string {
|
||||
return join(getBrowserosDir(), 'lazy-monitoring')
|
||||
}
|
||||
@@ -86,6 +96,7 @@ export async function ensureBrowserosDir(): Promise<void> {
|
||||
await mkdir(getBuiltinSkillsDir(), { recursive: true })
|
||||
await mkdir(getSessionsDir(), { recursive: true })
|
||||
await mkdir(getLazyMonitoringRunsDir(), { recursive: true })
|
||||
await mkdir(getAgentCacheDir(), { recursive: true })
|
||||
}
|
||||
|
||||
export async function cleanOldSessions(): Promise<void> {
|
||||
|
||||
@@ -8,7 +8,10 @@ import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { PATHS } from '@browseros/shared/constants/paths'
|
||||
import {
|
||||
getAgentCacheDir,
|
||||
getBrowserosDir,
|
||||
getCacheDir,
|
||||
getVmCacheDir,
|
||||
logDevelopmentBrowserosDir,
|
||||
} from '../src/lib/browseros-dir'
|
||||
import { logger } from '../src/lib/logger'
|
||||
@@ -72,4 +75,34 @@ describe('getBrowserosDir', () => {
|
||||
logger.info = originalInfo
|
||||
}
|
||||
})
|
||||
|
||||
it('uses the development cache directory in development', () => {
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
expect(getCacheDir()).toBe(join(homedir(), '.browseros-dev', 'cache'))
|
||||
})
|
||||
|
||||
it('uses the standard cache directory outside development', () => {
|
||||
process.env.NODE_ENV = 'test'
|
||||
|
||||
expect(getCacheDir()).toBe(
|
||||
join(homedir(), PATHS.BROWSEROS_DIR_NAME, 'cache'),
|
||||
)
|
||||
})
|
||||
|
||||
it('uses a vm cache directory below cache', () => {
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
expect(getVmCacheDir()).toBe(
|
||||
join(homedir(), '.browseros-dev', 'cache', 'vm'),
|
||||
)
|
||||
})
|
||||
|
||||
it('uses an agent image cache directory below vm cache', () => {
|
||||
process.env.NODE_ENV = 'development'
|
||||
|
||||
expect(getAgentCacheDir()).toBe(
|
||||
join(homedir(), '.browseros-dev', 'cache', 'vm', 'images'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -216,17 +216,6 @@
|
||||
"chrome-devtools-mcp": "latest",
|
||||
},
|
||||
},
|
||||
"packages/agent-container": {
|
||||
"name": "@browseros/agent-container",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.3",
|
||||
},
|
||||
},
|
||||
"packages/agent-sdk": {
|
||||
"name": "@browseros-ai/agent-sdk",
|
||||
"version": "0.0.7",
|
||||
@@ -243,6 +232,17 @@
|
||||
"zod": "^3.x",
|
||||
},
|
||||
},
|
||||
"packages/build-tools": {
|
||||
"name": "@browseros/build-tools",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"@browseros/shared": "workspace:*",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.3",
|
||||
},
|
||||
},
|
||||
"packages/cdp-protocol": {
|
||||
"name": "@browseros/cdp-protocol",
|
||||
"version": "0.0.1",
|
||||
@@ -254,18 +254,6 @@
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
},
|
||||
"packages/vm-container": {
|
||||
"name": "@browseros/vm-container",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"@browseros/shared": "workspace:*",
|
||||
"zod": "^3.24.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-sdk-client-mock": "^4.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"esbuild",
|
||||
@@ -486,7 +474,7 @@
|
||||
|
||||
"@browseros/agent": ["@browseros/agent@workspace:apps/agent"],
|
||||
|
||||
"@browseros/agent-container": ["@browseros/agent-container@workspace:packages/agent-container"],
|
||||
"@browseros/build-tools": ["@browseros/build-tools@workspace:packages/build-tools"],
|
||||
|
||||
"@browseros/cdp-protocol": ["@browseros/cdp-protocol@workspace:packages/cdp-protocol"],
|
||||
|
||||
@@ -496,8 +484,6 @@
|
||||
|
||||
"@browseros/shared": ["@browseros/shared@workspace:packages/shared"],
|
||||
|
||||
"@browseros/vm-container": ["@browseros/vm-container@workspace:packages/vm-container"],
|
||||
|
||||
"@bunup/dts": ["@bunup/dts@0.14.49", "", { "dependencies": { "@babel/parser": "^7.28.6", "coffi": "^0.1.37", "oxc-minify": "^0.93.0", "oxc-resolver": "^11.16.2", "oxc-transform": "^0.93.0", "picocolors": "^1.1.1", "std-env": "^3.10.0", "ts-import-resolver": "^0.1.23" }, "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-mAni3Zpf1MLxFDNCNStcxjbgNaSZpygc1qqF66WHMoUs3j+NSNKZPKwex9uLGXazJdE8g9MqQ//1QQcETdwY6Q=="],
|
||||
|
||||
"@bunup/shared": ["@bunup/shared@0.16.19", "", { "peerDependencies": { "typescript": "latest" }, "optionalPeers": ["typescript"] }, "sha512-gk0ALbDgEifVtZYRZ/ivvwF53pvOdpfqtLIIK67Q417nvtn/XPGE9NZLbdsowTKy2befmT8eiTxMXeoLyZPibA=="],
|
||||
@@ -2126,8 +2112,6 @@
|
||||
|
||||
"await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="],
|
||||
|
||||
"aws-sdk-client-mock": ["aws-sdk-client-mock@4.1.0", "", { "dependencies": { "@types/sinon": "^17.0.3", "sinon": "^18.0.1", "tslib": "^2.1.0" } }, "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw=="],
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
|
||||
@@ -3108,8 +3092,6 @@
|
||||
|
||||
"jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
|
||||
|
||||
"just-extend": ["just-extend@6.2.0", "", {}, "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
@@ -3468,8 +3450,6 @@
|
||||
|
||||
"next-tick": ["next-tick@1.1.0", "", {}, "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="],
|
||||
|
||||
"nise": ["nise@6.1.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^15.1.1", "just-extend": "^6.2.0", "path-to-regexp": "^8.3.0" } }, "sha512-SnRDPDBjxZZoU2n0+gzzLtSvo1OZo7j6jnbXsoh3AFxEGhaFU7ZF0TmefuKERq79wxR2U+MPn7ArW+Tl+clC3A=="],
|
||||
|
||||
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
@@ -4522,7 +4502,7 @@
|
||||
|
||||
"@browseros/agent/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
|
||||
|
||||
@@ -4530,8 +4510,6 @@
|
||||
|
||||
"@browseros/server/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.1014.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-node": "^3.972.24", "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", "@aws-sdk/middleware-expect-continue": "^3.972.8", "@aws-sdk/middleware-flexible-checksums": "^3.974.3", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-location-constraint": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/middleware-ssec": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/signature-v4-multi-region": "^3.996.11", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-blob-browser": "^4.2.13", "@smithy/hash-node": "^4.2.12", "@smithy/hash-stream-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/md5-js": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow=="],
|
||||
|
||||
"@codesandbox/sandpack-react/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="],
|
||||
@@ -4942,10 +4920,6 @@
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"aws-sdk-client-mock/@types/sinon": ["@types/sinon@17.0.4", "", { "dependencies": { "@types/sinonjs__fake-timers": "*" } }, "sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew=="],
|
||||
|
||||
"aws-sdk-client-mock/sinon": ["sinon@18.0.1", "", { "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "11.2.2", "@sinonjs/samsam": "^8.0.0", "diff": "^5.2.0", "nise": "^6.0.0", "supports-color": "^7" } }, "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw=="],
|
||||
|
||||
"babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
|
||||
|
||||
"better-auth/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||
@@ -5092,8 +5066,6 @@
|
||||
|
||||
"multimatch/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"nise/@sinonjs/fake-timers": ["@sinonjs/fake-timers@15.3.2", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw=="],
|
||||
|
||||
"node-notifier/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"node-notifier/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
@@ -5254,102 +5226,102 @@
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.24", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-ini": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.11", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.10", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="],
|
||||
|
||||
"@browseros/agent/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.24", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-ini": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.11", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.10", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="],
|
||||
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.24", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-ini": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw=="],
|
||||
@@ -5448,100 +5420,6 @@
|
||||
|
||||
"@browseros/server/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core": ["@aws-sdk/core@3.973.23", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.15", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-aoJncvD1XvloZ9JLnKqTRL9dBy+Szkryoag9VT+V1TqsuUgIxV9cnBVM/hrDi2vE8bDqLiDR8nirdRcCdtJu0w=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.24", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-ini": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-9Jwi7aps3AfUicJyF5udYadPypPpCwUZ6BSKr/QjRbVCpRVS1wc+1Q6AEZ/qz8J4JraeRd247pSzyMQSIHVebw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.974.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", "@smithy/is-array-buffer": "^4.2.2", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-fB7FNLH1+VPUs0QL3PLrHW+DD4gKu6daFgWtyq3R0Y0Lx8DLZPvyGAxCZNFBxH+M2xt9KvBJX6USwjuqvitmCQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-50QgHGPQAb2veqFOmTF1A3GsAklLHZXL47KbY35khIkfbXH5PLvqpEc/gOAEBPj/yFxrlgxz/8mqWcWTNxBkwQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.24", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-dLTWy6IfAMhNiSEvMr07g/qZ54be6pLqlxVblbF6AzafmmGAzMMj8qMoY9B4+YgT+gY9IcuxZslNh03L6PyMCQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eQ+dFU05ZRC/lC2XpYlYSPlXtX3VT8sn5toxN2Fv7EXlMoA2p9V7vUBKqHunfD4TRLpxUq8Y8Ol/nCqiv327Ng=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.11", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.23", "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-SKgZY7x6AloLUXO20FJGnkKJ3a6CXzNDt6PYs2yqoPzgU0xKWcUoGGJGEBTsfM5eihKW42lbwp+sXzACLbSsaA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/types": ["@aws-sdk/types@3.973.6", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.10", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-E99zeTscCc+pTMfsvnfi6foPpKmdD1cZfOC7/P8UUrjsoQdg9VEWPRD+xdFduKnfPXwcvby58AlO9jwwF6U96g=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/core": ["@smithy/core@3.23.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.20", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.2.13", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.2.2", "@smithy/chunked-blob-reader-native": "^4.2.3", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/hash-node": ["@smithy/hash-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/md5-js": ["@smithy/md5-js@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.27", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-serde": "^4.2.15", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-T3TFfUgXQlpcg+UdzcAISdZpj4Z+XECZ/cefgA6wLBd6V4lRi0svN2hBouN/be9dXQ31X4sLWz3fAQDf+nt6BA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.44", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-Y1Rav7m5CFRPQyM4CI0koD/bXjyjJu3EQxZZhtLGD88WIrBrQ7kqXM96ncd6rYnojwOo/u9MXu57JrEvu/nLrA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.15", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.0", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/smithy-client": ["@smithy/smithy-client@4.12.7", "", { "dependencies": { "@smithy/core": "^3.23.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-q3gqnwml60G44FECaEEsdQMplYhDMZYCtYhMCzadCnRnnHIobZJjegmdoUo6ieLQlPUzvrMdIJUpx6DoPmzANQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.43", "", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Qd/0wCKMaXxev/z00TvNzGCH2jlKKKxXP1aDxB6oKwSQthe3Og2dMhSayGCnsma1bK/kQX1+X7SMP99t6FgiiQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.47", "", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qSRbYp1EQ7th+sPFuVcVO05AE0QH635hycdEXlpzIahqHHf2Fyd/Zl+8v0XYMJ3cgDVPa0lkMefU7oNUjAP+DQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-retry": ["@smithy/util-retry@4.2.12", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-stream": ["@smithy/util-stream@4.5.20", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-waiter": ["@smithy/util-waiter@4.2.13", "", { "dependencies": { "@smithy/abort-controller": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ=="],
|
||||
|
||||
"@google/gemini-cli-core/@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||
|
||||
"@google/gemini-cli-core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ=="],
|
||||
@@ -5742,10 +5620,6 @@
|
||||
|
||||
"@types/request/form-data/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"aws-sdk-client-mock/sinon/@sinonjs/fake-timers": ["@sinonjs/fake-timers@11.2.2", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw=="],
|
||||
|
||||
"aws-sdk-client-mock/sinon/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="],
|
||||
|
||||
"babel-plugin-macros/cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
|
||||
|
||||
"boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
@@ -5832,69 +5706,69 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-login": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-login": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums/@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums/@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
|
||||
|
||||
@@ -5960,70 +5834,6 @@
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.15", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-BkAfKq8Bd4shCtec1usNz//urPJF/SZy14qJyxkSaRJQ/Vv1gVh0VZSTmS7aE6aLMELkFV5wHHrS9ZcdG8Kxsg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.0", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" } }, "sha512-4XZ3+Gu5DY8/n8zQFHBgcKTF7hWQl42G6CY9xfXVo2d25FM/lYkpmuzhYopYoPL1ITWkJ2OSBQfYEu5JRfHOhA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/credential-provider-env": "^3.972.21", "@aws-sdk/credential-provider-http": "^3.972.23", "@aws-sdk/credential-provider-login": "^3.972.23", "@aws-sdk/credential-provider-process": "^3.972.21", "@aws-sdk/credential-provider-sso": "^3.972.23", "@aws-sdk/credential-provider-web-identity": "^3.972.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-PZLSmU0JFpNCDFReidBezsgL5ji9jOBry8CnZdw4Jj6d0K2z3Ftnp44NXgADqYx5BLMu/ZHujfeJReaDoV+IwQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.21", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-nRxbeOJ1E1gVA0lNQezuMVndx+ZcuyaW/RB05pUsznN5BxykSlH6KkZ/7Ca/ubJf3i5N3p0gwNO5zgPSCzj+ww=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/token-providers": "3.1014.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-APUccADuYPLL0f2htpM8Z4czabSmHOdo4r41W6lKEZdy++cNJ42Radqy6x4TopENzr3hR6WYMyhiuiqtbf/nAA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-H5JNqtIwOu/feInmMMWcK0dL5r897ReEn7n2m16Dd0DPD9gA2Hg8Cq4UDzZ/9OzaLh/uqBM6seixz0U6Fi2Eag=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-bucket-endpoint/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-flexible-checksums/@aws-sdk/crc64-nvme": ["@aws-sdk/crc64-nvme@3.972.5", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.972.3", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/middleware-sdk-s3/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/middleware-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-config-provider/@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-browser/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-defaults-mode-node/@smithy/property-provider": ["@smithy/property-provider@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-retry/@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/util-waiter/@smithy/abort-controller": ["@smithy/abort-controller@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q=="],
|
||||
|
||||
"@google/gemini-cli-core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw=="],
|
||||
|
||||
"@google/gemini-cli-core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g=="],
|
||||
@@ -6068,21 +5878,21 @@
|
||||
|
||||
"wxt/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1014.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1014.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
@@ -6100,22 +5910,6 @@
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.5.8", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-OmE/pSkbMM3dCj1HdOnZ5kXnKK+R/Yz+kbBugraBecp0pGAs21eEURfQRz+1N2gzIHLVyGIP1MEjk/uSrFsngg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1014.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.23", "@aws-sdk/nested-clients": "^3.996.13", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.23", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.8", "@aws-sdk/middleware-user-agent": "^3.972.24", "@aws-sdk/region-config-resolver": "^3.972.9", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.10", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.27", "@smithy/middleware-retry": "^4.4.44", "@smithy/middleware-serde": "^4.2.15", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.0", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.43", "@smithy/util-defaults-mode-node": "^4.2.47", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-ptZ1HF4yYHNJX8cgFF+8NdYO69XJKZn7ft0/ynV3c0hCbN+89fAbrLS+fqniU2tW8o9Kfqhj8FUh+IPXb2Qsuw=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-browser/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@smithy/eventstream-serde-node/@smithy/eventstream-serde-universal/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
|
||||
|
||||
"@google/genai/google-auth-library/gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"@google/genai/google-auth-library/gaxios/rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
@@ -6128,12 +5922,10 @@
|
||||
|
||||
"publish-browser-extension/listr2/cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"@browseros/agent-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
"@browseros/build-tools/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"@browseros/eval/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"@browseros/vm-container/@aws-sdk/client-s3/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/fast-xml-builder": ["fast-xml-builder@1.1.4", "", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
|
||||
|
||||
"@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"@google/genai/google-auth-library/gaxios/rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Required for `bun run --filter @browseros/agent-container upload`
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=
|
||||
|
||||
# Optional overrides
|
||||
R2_PUBLIC_BASE_URL=https://cdn.browseros.com
|
||||
PODMAN_BIN=podman
|
||||
|
||||
# Optional recipe-driven registry auth
|
||||
# If an agent entry in recipe/agents.json sets `requires_auth.secret`,
|
||||
# define that env var before running `build`.
|
||||
# EXAMPLE_REGISTRY_TOKEN=
|
||||
@@ -1,52 +0,0 @@
|
||||
# @browseros/agent-container
|
||||
|
||||
OCI tarball producer for BrowserOS-bundled agent containers.
|
||||
|
||||
This package owns the WS2 pipeline:
|
||||
|
||||
- Read the active agent set from `recipe/agents.json`
|
||||
- Pull the upstream image with `podman`
|
||||
- Save it as an OCI archive and gzip it
|
||||
- Smoke-test the archive with `podman load`
|
||||
- Publish tarballs, checksum sidecars, and manifests to R2
|
||||
|
||||
Package env requirements are documented in [.env.sample](./.env.sample).
|
||||
|
||||
## Local usage
|
||||
|
||||
```bash
|
||||
cd packages/browseros-agent
|
||||
|
||||
# Print the GitHub Actions matrix JSON
|
||||
bun run --filter @browseros/agent-container list-matrix
|
||||
|
||||
# Build one artifact locally
|
||||
bun run --filter @browseros/agent-container build -- \
|
||||
--agent openclaw \
|
||||
--arch arm64 \
|
||||
--output-dir packages/agent-container/dist/agent-container/openclaw/arm64
|
||||
|
||||
# Smoke-test a built tarball
|
||||
bun run --filter @browseros/agent-container smoke -- \
|
||||
--tarball packages/agent-container/dist/agent-container/openclaw/arm64/openclaw-2026.4.12-arm64.tar.gz \
|
||||
--expected-image ghcr.io/openclaw/openclaw:2026.4.12 \
|
||||
--expected-fingerprint ...
|
||||
|
||||
# Upload pre-built artifacts
|
||||
# Fill these from packages/agent-container/.env.sample
|
||||
R2_ACCOUNT_ID=... \
|
||||
R2_ACCESS_KEY_ID=... \
|
||||
R2_SECRET_ACCESS_KEY=... \
|
||||
R2_BUCKET=... \
|
||||
bun run --filter @browseros/agent-container upload -- \
|
||||
--artifact-dir packages/agent-container/dist/agent-container \
|
||||
--update-aggregate
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- `recipe/agents.json` is the source of truth for the active set.
|
||||
- `workflow_dispatch` version overrides are intended for dry runs. Publishing still needs the recipe to be authoritative.
|
||||
- `src/load.ts` is intentionally stubbed. WS6 fills in the runtime consumer path.
|
||||
- Private registry auth is recipe-driven: if `requires_auth.secret` is set for an agent, export that env var before running `build`.
|
||||
- When invoking package scripts with `bun run --filter @browseros/agent-container`, pass artifact paths that are explicit from `packages/browseros-agent`. Filtered scripts run inside the package directory, so bare `dist/...` paths are ambiguous.
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@browseros/agent-container",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "BrowserOS agent container OCI tarball producer",
|
||||
"exports": {
|
||||
"./schema": {
|
||||
"types": "./src/schema/index.ts",
|
||||
"default": "./src/schema/index.ts"
|
||||
},
|
||||
"./load": {
|
||||
"types": "./src/load.ts",
|
||||
"default": "./src/load.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build.ts",
|
||||
"upload": "bun run scripts/upload.ts",
|
||||
"smoke": "bun run scripts/smoke.ts",
|
||||
"list-matrix": "bun run scripts/list-matrix.ts",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.3"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"schema": "v1",
|
||||
"agents": [
|
||||
{
|
||||
"name": "openclaw",
|
||||
"image": "ghcr.io/openclaw/openclaw",
|
||||
"version": "2026.4.12",
|
||||
"arches": ["amd64", "arm64"],
|
||||
"publishAs": "openclaw"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
import { buildTarball } from '../src/build'
|
||||
import { readAgentsConfig } from '../src/catalog'
|
||||
import { parseArch } from '../src/schema/arch'
|
||||
|
||||
const packageRoot = resolve(import.meta.dir, '..')
|
||||
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
agent: { type: 'string' },
|
||||
version: { type: 'string' },
|
||||
arch: { type: 'string' },
|
||||
'output-dir': { type: 'string' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(
|
||||
'Usage: bun run build -- --agent <name> --arch <amd64|arm64> --output-dir <path> [--version <override>]',
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!values.agent || !values.arch || !values['output-dir']) {
|
||||
throw new Error('--agent, --arch, and --output-dir are required')
|
||||
}
|
||||
|
||||
const config = await readAgentsConfig(recipePath)
|
||||
const selected = config.agents.find((agent) => agent.name === values.agent)
|
||||
if (!selected) {
|
||||
throw new Error(`unknown agent: ${values.agent}`)
|
||||
}
|
||||
|
||||
const result = await buildTarball({
|
||||
agent: {
|
||||
...selected,
|
||||
version: values.version ?? selected.version,
|
||||
},
|
||||
arch: parseArch(values.arch),
|
||||
outputDir: values['output-dir'],
|
||||
recipePath,
|
||||
})
|
||||
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
import { expandMatrix, readAgentsConfig } from '../src/catalog'
|
||||
|
||||
const packageRoot = resolve(import.meta.dir, '..')
|
||||
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
agent: { type: 'string' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log('Usage: bun run list-matrix [--agent <name>]')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const config = await readAgentsConfig(recipePath)
|
||||
const include = expandMatrix(config, { agent: values.agent })
|
||||
|
||||
if (include.length === 0) {
|
||||
throw new Error(
|
||||
values.agent
|
||||
? `no agents matched filter: ${values.agent}`
|
||||
: 'recipe/agents.json produced an empty matrix',
|
||||
)
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ include }))
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
import { roundTripPodmanLoad } from '../src/smoke'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
tarball: { type: 'string' },
|
||||
'expected-image': { type: 'string' },
|
||||
'expected-image-id': { type: 'string' },
|
||||
'expected-fingerprint': { type: 'string' },
|
||||
'expected-digest': { type: 'string' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(
|
||||
'Usage: bun run smoke -- --tarball <path> --expected-image <ref> [--expected-fingerprint <sha256-hex>] [--expected-image-id <sha256:...> | --expected-digest <sha256:...>]',
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const expectedImageId = values['expected-image-id'] ?? values['expected-digest']
|
||||
if (
|
||||
!values.tarball ||
|
||||
!values['expected-image'] ||
|
||||
(!expectedImageId && !values['expected-fingerprint'])
|
||||
) {
|
||||
throw new Error(
|
||||
'--tarball, --expected-image, and one verification flag are required',
|
||||
)
|
||||
}
|
||||
|
||||
await roundTripPodmanLoad({
|
||||
tarballPath: values.tarball,
|
||||
expectedImage: values['expected-image'],
|
||||
expectedImageId,
|
||||
expectedSmokeFingerprint: values['expected-fingerprint'],
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
import { loadBuildResult } from '../src/build'
|
||||
import { publishAgents } from '../src/publish'
|
||||
|
||||
async function findBuildResultPaths(root: string): Promise<string[]> {
|
||||
const entries = await readdir(root, { withFileTypes: true })
|
||||
const paths: string[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
const path = join(root, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
paths.push(...(await findBuildResultPaths(path)))
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name === 'build-result.json') {
|
||||
paths.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
return paths.sort()
|
||||
}
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
'artifact-dir': { type: 'string' },
|
||||
'update-aggregate': { type: 'boolean' },
|
||||
help: { type: 'boolean', short: 'h' },
|
||||
},
|
||||
})
|
||||
|
||||
if (values.help) {
|
||||
console.log(
|
||||
'Usage: bun run upload -- --artifact-dir <path> [--update-aggregate]',
|
||||
)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!values['artifact-dir']) {
|
||||
throw new Error('--artifact-dir is required')
|
||||
}
|
||||
|
||||
const artifactDir = resolve(values['artifact-dir'])
|
||||
const buildResultPaths = await findBuildResultPaths(artifactDir)
|
||||
if (buildResultPaths.length === 0) {
|
||||
throw new Error(`no build-result.json files found under ${artifactDir}`)
|
||||
}
|
||||
|
||||
const buildResults = await Promise.all(
|
||||
buildResultPaths.map((path) => loadBuildResult(path)),
|
||||
)
|
||||
|
||||
await publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: Boolean(values['update-aggregate']),
|
||||
})
|
||||
@@ -1,470 +0,0 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { access, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
|
||||
import { type AgentEntry, publishNameForAgent } from './catalog'
|
||||
import type { ContainerArch } from './schema/arch'
|
||||
|
||||
const PODMAN_BIN = process.env.PODMAN_BIN ?? 'podman'
|
||||
|
||||
interface PodmanCommandResult {
|
||||
stdout: string
|
||||
stderr: string
|
||||
}
|
||||
|
||||
interface PodmanInspectShape {
|
||||
Id?: string
|
||||
Digest?: string
|
||||
RepoDigests?: string[]
|
||||
Architecture?: string
|
||||
Os?: string
|
||||
Config?: unknown
|
||||
RootFS?: unknown
|
||||
}
|
||||
|
||||
interface PodmanImageMetadata {
|
||||
imageId: string
|
||||
sourceOciDigest: string
|
||||
smokeFingerprint: string
|
||||
}
|
||||
|
||||
interface RepoDigestCount {
|
||||
digest: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface BuildOptions {
|
||||
agent: AgentEntry
|
||||
arch: ContainerArch
|
||||
outputDir: string
|
||||
recipePath?: string
|
||||
builtBy?: string
|
||||
}
|
||||
|
||||
export interface BuildResult {
|
||||
name: string
|
||||
publishAs: string
|
||||
image: string
|
||||
version: string
|
||||
arch: ContainerArch
|
||||
sourceOciDigest: string
|
||||
imageId: string
|
||||
smokeFingerprint: string
|
||||
filename: string
|
||||
tarballPath: string
|
||||
tarballShaPath: string
|
||||
compressedSha256: string
|
||||
compressedSizeBytes: number
|
||||
uncompressedSha256: string
|
||||
uncompressedSizeBytes: number
|
||||
podmanVersion: string
|
||||
builtAt: string
|
||||
builtBy: string
|
||||
gitSha: string
|
||||
gitDirty: boolean
|
||||
configSha256: string
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((entry) => stableJson(entry)).join(',')}]`
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
const entries = Object.entries(value as Record<string, unknown>).sort(
|
||||
([left], [right]) => left.localeCompare(right),
|
||||
)
|
||||
return `{${entries
|
||||
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
||||
.join(',')}}`
|
||||
}
|
||||
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function smokeFingerprintForInspect(inspected: PodmanInspectShape): string {
|
||||
const payload = stableJson({
|
||||
Architecture: inspected.Architecture ?? '',
|
||||
Os: inspected.Os ?? '',
|
||||
Config: inspected.Config ?? null,
|
||||
RootFS: inspected.RootFS ?? null,
|
||||
})
|
||||
return createHash('sha256').update(payload).digest('hex')
|
||||
}
|
||||
|
||||
function normalizeSha256Like(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (/^sha256:[a-f0-9]{64}$/.test(trimmed)) {
|
||||
return trimmed
|
||||
}
|
||||
if (/^[a-f0-9]{64}$/.test(trimmed)) {
|
||||
return `sha256:${trimmed}`
|
||||
}
|
||||
|
||||
throw new Error(`unexpected sha256-like value: ${value}`)
|
||||
}
|
||||
|
||||
function selectSourceOciDigest(
|
||||
platformDigest: string,
|
||||
repoDigests: string[],
|
||||
): string {
|
||||
const counts = new Map<string, number>()
|
||||
for (const digest of repoDigests) {
|
||||
counts.set(digest, (counts.get(digest) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const candidates: RepoDigestCount[] = [...counts.entries()]
|
||||
.filter(([digest]) => digest !== platformDigest)
|
||||
.map(([digest, count]) => ({ digest, count }))
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.count - left.count || left.digest.localeCompare(right.digest),
|
||||
)
|
||||
|
||||
const [firstCandidate, secondCandidate] = candidates
|
||||
if (!firstCandidate) {
|
||||
return platformDigest
|
||||
}
|
||||
if (!secondCandidate || firstCandidate.count > secondCandidate.count) {
|
||||
return firstCandidate.digest
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`ambiguous source OCI digest for ${platformDigest}: ${candidates
|
||||
.map((candidate) => `${candidate.digest} (${candidate.count})`)
|
||||
.join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
async function runPodman(
|
||||
args: string[],
|
||||
options: { stdin?: string } = {},
|
||||
): Promise<PodmanCommandResult> {
|
||||
const proc = Bun.spawn([PODMAN_BIN, ...args], {
|
||||
stdin: options.stdin ? Buffer.from(`${options.stdin}\n`) : undefined,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
|
||||
const [stdout, stderr] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
])
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`podman ${args.join(' ')} exited ${exitCode}\n${stderr.trim() || stdout.trim()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return { stdout, stderr }
|
||||
}
|
||||
|
||||
async function runCommand(command: string[]): Promise<string> {
|
||||
const proc = Bun.spawn(command, {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
const [stdout, stderr] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
])
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(
|
||||
`${command.join(' ')} exited ${exitCode}\n${stderr.trim() || stdout.trim()}`,
|
||||
)
|
||||
}
|
||||
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function sha256OfFile(path: string): Promise<string> {
|
||||
const hash = createHash('sha256')
|
||||
const stream = createReadStream(path)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
hash.update(chunk)
|
||||
}
|
||||
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
async function gzipArchive(tarPath: string): Promise<void> {
|
||||
const proc = Bun.spawn(['gzip', '-9', '-f', '-k', tarPath], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`gzip exited ${exitCode}\n${stderr.trim()}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function gitSha(): Promise<string> {
|
||||
return runCommand(['git', 'rev-parse', 'HEAD'])
|
||||
}
|
||||
|
||||
async function gitDirty(): Promise<boolean> {
|
||||
const stdout = await runCommand(['git', 'status', '--short'])
|
||||
return stdout.length > 0
|
||||
}
|
||||
|
||||
function recipePathForPackage(): string {
|
||||
return resolve(import.meta.dir, '..', 'recipe', 'agents.json')
|
||||
}
|
||||
|
||||
function imageRefForBuild(options: BuildOptions): string {
|
||||
return `${options.agent.image}:${options.agent.version}`
|
||||
}
|
||||
|
||||
function builtByForBuild(explicitBuiltBy?: string): string {
|
||||
if (explicitBuiltBy) {
|
||||
return explicitBuiltBy
|
||||
}
|
||||
|
||||
const workflowRef = process.env.GITHUB_WORKFLOW_REF?.trim()
|
||||
if (workflowRef) {
|
||||
return workflowRef
|
||||
}
|
||||
|
||||
const workflow = process.env.GITHUB_WORKFLOW?.trim()
|
||||
const ref = process.env.GITHUB_REF?.trim()
|
||||
if (workflow && ref) {
|
||||
return `${workflow}@${ref}`
|
||||
}
|
||||
|
||||
const user = process.env.USER ?? process.env.LOGNAME ?? 'unknown'
|
||||
return `local:${user}`
|
||||
}
|
||||
|
||||
export function registryForImage(image: string): string {
|
||||
const firstSegment = image.split('/')[0]
|
||||
if (
|
||||
!firstSegment ||
|
||||
(!firstSegment.includes('.') &&
|
||||
!firstSegment.includes(':') &&
|
||||
firstSegment !== 'localhost')
|
||||
) {
|
||||
return 'docker.io'
|
||||
}
|
||||
|
||||
return firstSegment
|
||||
}
|
||||
|
||||
async function podmanVersion(): Promise<string> {
|
||||
const { stdout } = await runPodman(['--version'])
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function podmanLogin(options: {
|
||||
registry: string
|
||||
username: string
|
||||
password: string
|
||||
}): Promise<void> {
|
||||
await runPodman(
|
||||
[
|
||||
'login',
|
||||
'--username',
|
||||
options.username,
|
||||
'--password-stdin',
|
||||
options.registry,
|
||||
],
|
||||
{ stdin: options.password },
|
||||
)
|
||||
}
|
||||
|
||||
async function podmanPull(
|
||||
imageRef: string,
|
||||
arch: ContainerArch,
|
||||
): Promise<void> {
|
||||
await runPodman([
|
||||
'pull',
|
||||
'--quiet',
|
||||
'--os',
|
||||
'linux',
|
||||
'--arch',
|
||||
arch,
|
||||
imageRef,
|
||||
])
|
||||
}
|
||||
|
||||
export async function podmanInspectImage(
|
||||
imageRef: string,
|
||||
): Promise<PodmanImageMetadata> {
|
||||
const { stdout } = await runPodman([
|
||||
'inspect',
|
||||
'--type',
|
||||
'image',
|
||||
'--format',
|
||||
'{{json .}}',
|
||||
imageRef,
|
||||
])
|
||||
const inspected = JSON.parse(stdout.trim()) as PodmanInspectShape
|
||||
const imageId = normalizeSha256Like(inspected.Id ?? '')
|
||||
const platformDigest = normalizeSha256Like(inspected.Digest ?? imageId)
|
||||
const repoDigests = (inspected.RepoDigests ?? [])
|
||||
.map((entry) => entry.split('@')[1] ?? '')
|
||||
.filter(Boolean)
|
||||
.map((entry) => normalizeSha256Like(entry))
|
||||
const sourceOciDigest = selectSourceOciDigest(platformDigest, repoDigests)
|
||||
|
||||
return {
|
||||
imageId,
|
||||
sourceOciDigest,
|
||||
smokeFingerprint: smokeFingerprintForInspect(inspected),
|
||||
}
|
||||
}
|
||||
|
||||
async function podmanSaveOci(options: {
|
||||
imageRef: string
|
||||
outPath: string
|
||||
}): Promise<void> {
|
||||
await runPodman([
|
||||
'save',
|
||||
'--format',
|
||||
'oci-archive',
|
||||
'--output',
|
||||
options.outPath,
|
||||
options.imageRef,
|
||||
])
|
||||
}
|
||||
|
||||
export async function podmanLoadArchive(tarballPath: string): Promise<void> {
|
||||
await runPodman(['load', '--input', tarballPath])
|
||||
}
|
||||
|
||||
export async function podmanRemoveImage(imageRef: string): Promise<void> {
|
||||
await runPodman(['rmi', '-f', imageRef])
|
||||
}
|
||||
|
||||
async function maybeLoginForAgent(options: BuildOptions): Promise<void> {
|
||||
const auth = options.agent.requires_auth
|
||||
if (!auth) {
|
||||
return
|
||||
}
|
||||
|
||||
const password = process.env[auth.secret]?.trim()
|
||||
if (!password) {
|
||||
throw new Error(`missing registry credential env var: ${auth.secret}`)
|
||||
}
|
||||
|
||||
await podmanLogin({
|
||||
registry: registryForImage(options.agent.image),
|
||||
username: auth.username ?? 'oauth2accesstoken',
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
export async function buildTarball(
|
||||
options: BuildOptions,
|
||||
): Promise<BuildResult> {
|
||||
const imageRef = imageRefForBuild(options)
|
||||
const publishAs = publishNameForAgent(options.agent)
|
||||
const outputDir = resolve(options.outputDir)
|
||||
const recipePath = resolve(options.recipePath ?? recipePathForPackage())
|
||||
const baseName = `${publishAs}-${options.agent.version}-${options.arch}.tar`
|
||||
const tarPath = join(outputDir, baseName)
|
||||
const tarballPath = `${tarPath}.gz`
|
||||
const tarballShaPath = `${tarballPath}.sha256`
|
||||
const buildResultPath = join(outputDir, 'build-result.json')
|
||||
|
||||
await mkdir(outputDir, { recursive: true })
|
||||
await Promise.all([
|
||||
rm(tarPath, { force: true }),
|
||||
rm(tarballPath, { force: true }),
|
||||
rm(tarballShaPath, { force: true }),
|
||||
rm(buildResultPath, { force: true }),
|
||||
])
|
||||
|
||||
const [gitShaValue, gitDirtyValue, configSha256, podmanVersionValue] =
|
||||
await Promise.all([
|
||||
gitSha(),
|
||||
gitDirty(),
|
||||
sha256OfFile(recipePath),
|
||||
podmanVersion(),
|
||||
])
|
||||
const builtAt = new Date().toISOString()
|
||||
const builtBy = builtByForBuild(options.builtBy)
|
||||
|
||||
await maybeLoginForAgent(options)
|
||||
await podmanPull(imageRef, options.arch)
|
||||
const inspection = await podmanInspectImage(imageRef)
|
||||
await podmanSaveOci({ imageRef, outPath: tarPath })
|
||||
await gzipArchive(tarPath)
|
||||
|
||||
const [
|
||||
compressedSha256,
|
||||
uncompressedSha256,
|
||||
compressedStats,
|
||||
uncompressedStats,
|
||||
] = await Promise.all([
|
||||
sha256OfFile(tarballPath),
|
||||
sha256OfFile(tarPath),
|
||||
stat(tarballPath),
|
||||
stat(tarPath),
|
||||
])
|
||||
|
||||
const filename = basename(tarballPath)
|
||||
await writeFile(tarballShaPath, `${compressedSha256} ${filename}\n`, 'utf8')
|
||||
await rm(tarPath, { force: true })
|
||||
|
||||
const result: BuildResult = {
|
||||
name: options.agent.name,
|
||||
publishAs,
|
||||
image: options.agent.image,
|
||||
version: options.agent.version,
|
||||
arch: options.arch,
|
||||
sourceOciDigest: inspection.sourceOciDigest,
|
||||
imageId: inspection.imageId,
|
||||
smokeFingerprint: inspection.smokeFingerprint,
|
||||
filename,
|
||||
tarballPath,
|
||||
tarballShaPath,
|
||||
compressedSha256,
|
||||
compressedSizeBytes: compressedStats.size,
|
||||
uncompressedSha256,
|
||||
uncompressedSizeBytes: uncompressedStats.size,
|
||||
podmanVersion: podmanVersionValue,
|
||||
builtAt,
|
||||
builtBy,
|
||||
gitSha: gitShaValue,
|
||||
gitDirty: gitDirtyValue,
|
||||
configSha256,
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
buildResultPath,
|
||||
`${JSON.stringify(result, null, 2)}\n`,
|
||||
'utf8',
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
export async function loadBuildResult(path: string): Promise<BuildResult> {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const result = JSON.parse(raw) as BuildResult
|
||||
const resultDir = dirname(path)
|
||||
const tarballPath = (await pathExists(result.tarballPath))
|
||||
? result.tarballPath
|
||||
: join(resultDir, result.filename)
|
||||
const tarballShaPath = (await pathExists(result.tarballShaPath))
|
||||
? result.tarballShaPath
|
||||
: `${tarballPath}.sha256`
|
||||
|
||||
return {
|
||||
...result,
|
||||
tarballPath,
|
||||
tarballShaPath,
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ARCHES, type ContainerArch } from './schema/arch'
|
||||
|
||||
export const agentEntrySchema = z.object({
|
||||
name: z.string().regex(/^[a-z0-9-]+$/),
|
||||
image: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
arches: z.array(z.enum(ARCHES)).min(1),
|
||||
publishAs: z
|
||||
.string()
|
||||
.regex(/^[a-z0-9-]+$/)
|
||||
.optional(),
|
||||
requires_auth: z
|
||||
.object({
|
||||
secret: z.string().min(1),
|
||||
username: z.string().min(1).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const agentsConfigSchema = z
|
||||
.object({
|
||||
schema: z.literal('v1'),
|
||||
agents: z.array(agentEntrySchema).min(1),
|
||||
})
|
||||
.superRefine((config, ctx) => {
|
||||
const seen = new Set<string>()
|
||||
for (const [index, agent] of config.agents.entries()) {
|
||||
if (seen.has(agent.name)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['agents', index, 'name'],
|
||||
message: `duplicate agent name: ${agent.name}`,
|
||||
})
|
||||
}
|
||||
seen.add(agent.name)
|
||||
}
|
||||
})
|
||||
|
||||
export type AgentEntry = z.infer<typeof agentEntrySchema>
|
||||
export type AgentsConfig = z.infer<typeof agentsConfigSchema>
|
||||
|
||||
export interface MatrixEntry {
|
||||
agent: string
|
||||
image: string
|
||||
version: string
|
||||
arch: ContainerArch
|
||||
publishAs: string
|
||||
}
|
||||
|
||||
export function publishNameForAgent(agent: AgentEntry): string {
|
||||
return agent.publishAs ?? agent.name
|
||||
}
|
||||
|
||||
export async function readAgentsConfig(path: string): Promise<AgentsConfig> {
|
||||
const raw = await readFile(path, 'utf8')
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return agentsConfigSchema.parse(parsed)
|
||||
}
|
||||
|
||||
export function expandMatrix(
|
||||
config: AgentsConfig,
|
||||
filter: { agent?: string } = {},
|
||||
): MatrixEntry[] {
|
||||
const entries: MatrixEntry[] = []
|
||||
|
||||
for (const agent of config.agents) {
|
||||
if (filter.agent && agent.name !== filter.agent) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const arch of agent.arches) {
|
||||
entries.push({
|
||||
agent: agent.name,
|
||||
image: agent.image,
|
||||
version: agent.version,
|
||||
arch,
|
||||
publishAs: publishNameForAgent(agent),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return entries.sort((left, right) => {
|
||||
const byAgent = left.agent.localeCompare(right.agent)
|
||||
if (byAgent !== 0) {
|
||||
return byAgent
|
||||
}
|
||||
|
||||
return left.arch.localeCompare(right.arch)
|
||||
})
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type {
|
||||
AgentArtifact,
|
||||
AgentManifest,
|
||||
AggregateManifest,
|
||||
ContainerArch,
|
||||
} from './schema'
|
||||
|
||||
export async function fetchAggregateManifest(): Promise<AggregateManifest> {
|
||||
throw new Error('fetchAggregateManifest: implemented in WS6')
|
||||
}
|
||||
|
||||
export async function fetchAgentManifest(
|
||||
_agent: string,
|
||||
_version: string,
|
||||
): Promise<AgentManifest> {
|
||||
throw new Error('fetchAgentManifest: implemented in WS6')
|
||||
}
|
||||
|
||||
export async function verifySha256(
|
||||
_path: string,
|
||||
_expectedSha256: string,
|
||||
): Promise<void> {
|
||||
throw new Error('verifySha256: implemented in WS6')
|
||||
}
|
||||
|
||||
export async function findStagedTarball(
|
||||
_name: string,
|
||||
_version: string,
|
||||
_arch: ContainerArch,
|
||||
): Promise<string> {
|
||||
throw new Error('findStagedTarball: implemented in WS6')
|
||||
}
|
||||
|
||||
export async function loadTarball(
|
||||
_artifact: AgentArtifact,
|
||||
_destinationPath: string,
|
||||
): Promise<void> {
|
||||
throw new Error('loadTarball: implemented in WS6')
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
type S3ClientConfig,
|
||||
} from '@aws-sdk/client-s3'
|
||||
|
||||
import type { BuildResult } from './build'
|
||||
import { ARCHES } from './schema/arch'
|
||||
import {
|
||||
type AgentManifest,
|
||||
type AggregateEntry,
|
||||
type AggregateManifest,
|
||||
agentManifestSchema,
|
||||
aggregateManifestSchema,
|
||||
} from './schema/manifest'
|
||||
import {
|
||||
keyForAggregateManifest,
|
||||
keyForSha,
|
||||
keyForTarball,
|
||||
keyForVersionManifest,
|
||||
} from './schema/r2-keys'
|
||||
|
||||
const CDN_BASE_URL =
|
||||
process.env.R2_PUBLIC_BASE_URL ?? 'https://cdn.browseros.com'
|
||||
const JSON_CONTENT_TYPE = 'application/json; charset=utf-8'
|
||||
const SHA_CONTENT_TYPE = 'text/plain; charset=utf-8'
|
||||
|
||||
export interface PublishOptions {
|
||||
buildResults: BuildResult[]
|
||||
updateAggregate: boolean
|
||||
bucket?: string
|
||||
cdnBaseURL?: string
|
||||
client?: S3Client
|
||||
now?: () => Date
|
||||
}
|
||||
|
||||
interface ResultGroup {
|
||||
name: string
|
||||
publishAs: string
|
||||
image: string
|
||||
version: string
|
||||
sourceOciDigest: string
|
||||
podmanVersions: string[]
|
||||
gitSha: string
|
||||
gitDirty: boolean
|
||||
configSha256: string
|
||||
builtBy: string
|
||||
results: BuildResult[]
|
||||
}
|
||||
|
||||
function requiredEnv(name: string): string {
|
||||
const value = process.env[name]?.trim()
|
||||
if (!value) {
|
||||
throw new Error(`missing required env var: ${name}`)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function createR2Client(): S3Client {
|
||||
const config: S3ClientConfig = {
|
||||
region: 'auto',
|
||||
endpoint: `https://${requiredEnv('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: requiredEnv('R2_ACCESS_KEY_ID'),
|
||||
secretAccessKey: requiredEnv('R2_SECRET_ACCESS_KEY'),
|
||||
},
|
||||
}
|
||||
|
||||
return new S3Client(config)
|
||||
}
|
||||
|
||||
function getBucket(): string {
|
||||
return requiredEnv('R2_BUCKET')
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
path: string,
|
||||
contentType = 'application/gzip',
|
||||
): Promise<void> {
|
||||
const { size } = await stat(path)
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: createReadStream(path),
|
||||
ContentLength: size,
|
||||
ContentType: contentType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadBody(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
body: string | Uint8Array,
|
||||
contentType = JSON_CONTENT_TYPE,
|
||||
): Promise<void> {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function deleteObject(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
await client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function keyForGroup(name: string, version: string): string {
|
||||
return `${name}:${version}`
|
||||
}
|
||||
|
||||
function compareByArch(left: BuildResult, right: BuildResult): number {
|
||||
return ARCHES.indexOf(left.arch) - ARCHES.indexOf(right.arch)
|
||||
}
|
||||
|
||||
function cdnUrl(baseUrl: string, key: string): string {
|
||||
return `${baseUrl.replace(/\/+$/, '')}/${key}`
|
||||
}
|
||||
|
||||
function createManifestForGroup(
|
||||
group: ResultGroup,
|
||||
builtAt: string,
|
||||
cdnBaseURL: string,
|
||||
): AgentManifest {
|
||||
return agentManifestSchema.parse({
|
||||
name: group.name,
|
||||
schema: 'v1',
|
||||
build: {
|
||||
git_sha: group.gitSha,
|
||||
git_dirty: group.gitDirty,
|
||||
built_at: builtAt,
|
||||
built_by: group.builtBy,
|
||||
config_sha256: group.configSha256,
|
||||
podman_versions: group.podmanVersions,
|
||||
},
|
||||
source: {
|
||||
image: group.image,
|
||||
version: group.version,
|
||||
oci_digest: group.sourceOciDigest,
|
||||
},
|
||||
artifacts: [...group.results].sort(compareByArch).map((result) => {
|
||||
const key = keyForTarball(
|
||||
result.name,
|
||||
result.version,
|
||||
result.arch,
|
||||
result.publishAs,
|
||||
)
|
||||
return {
|
||||
arch: result.arch,
|
||||
filename: result.filename,
|
||||
format: 'oci-archive+gzip',
|
||||
compressed_sha256: result.compressedSha256,
|
||||
compressed_size_bytes: result.compressedSizeBytes,
|
||||
uncompressed_sha256: result.uncompressedSha256,
|
||||
uncompressed_size_bytes: result.uncompressedSizeBytes,
|
||||
url: cdnUrl(cdnBaseURL, key),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
function mergeAggregateEntries(
|
||||
existing: AggregateEntry[],
|
||||
nextEntries: AggregateEntry[],
|
||||
builtAt: string,
|
||||
builtBy: string,
|
||||
): AggregateManifest {
|
||||
const merged = new Map<string, AggregateEntry>()
|
||||
|
||||
for (const entry of existing) {
|
||||
merged.set(entry.name, entry)
|
||||
}
|
||||
for (const entry of nextEntries) {
|
||||
merged.set(entry.name, entry)
|
||||
}
|
||||
|
||||
return aggregateManifestSchema.parse({
|
||||
schema: 'v1',
|
||||
built_at: builtAt,
|
||||
built_by: builtBy,
|
||||
agents: [...merged.values()].sort((left, right) =>
|
||||
left.name.localeCompare(right.name),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
function buildAggregateEntries(
|
||||
groups: ResultGroup[],
|
||||
cdnBaseURL: string,
|
||||
): AggregateEntry[] {
|
||||
return groups
|
||||
.map((group) => ({
|
||||
name: group.name,
|
||||
version: group.version,
|
||||
oci_digest: group.sourceOciDigest,
|
||||
manifest_url: cdnUrl(
|
||||
cdnBaseURL,
|
||||
keyForVersionManifest(group.name, group.version),
|
||||
),
|
||||
}))
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
function aggregateBuiltBy(groups: ResultGroup[]): string {
|
||||
const builtByValues = [
|
||||
...new Set(groups.map((group) => group.builtBy)),
|
||||
].sort()
|
||||
return builtByValues.join(', ')
|
||||
}
|
||||
|
||||
function buildGroup(results: BuildResult[]): ResultGroup {
|
||||
const [firstResult, ...rest] = results
|
||||
if (!firstResult) {
|
||||
throw new Error('cannot publish an empty build result group')
|
||||
}
|
||||
|
||||
for (const result of rest) {
|
||||
if (result.name !== firstResult.name) {
|
||||
throw new Error('mixed agent names in publish group')
|
||||
}
|
||||
if (result.publishAs !== firstResult.publishAs) {
|
||||
throw new Error('mixed publishAs values in publish group')
|
||||
}
|
||||
if (result.image !== firstResult.image) {
|
||||
throw new Error('mixed source images in publish group')
|
||||
}
|
||||
if (result.version !== firstResult.version) {
|
||||
throw new Error('mixed versions in publish group')
|
||||
}
|
||||
if (result.sourceOciDigest !== firstResult.sourceOciDigest) {
|
||||
throw new Error('mixed source OCI digests in publish group')
|
||||
}
|
||||
if (
|
||||
result.gitSha !== firstResult.gitSha ||
|
||||
result.gitDirty !== firstResult.gitDirty
|
||||
) {
|
||||
throw new Error('mixed git metadata in publish group')
|
||||
}
|
||||
if (result.configSha256 !== firstResult.configSha256) {
|
||||
throw new Error('mixed recipe config hashes in publish group')
|
||||
}
|
||||
if (result.builtBy !== firstResult.builtBy) {
|
||||
throw new Error('mixed build provenance in publish group')
|
||||
}
|
||||
}
|
||||
|
||||
const podmanVersions = [
|
||||
...new Set(results.map((result) => result.podmanVersion)),
|
||||
].sort()
|
||||
|
||||
return {
|
||||
name: firstResult.name,
|
||||
publishAs: firstResult.publishAs,
|
||||
image: firstResult.image,
|
||||
version: firstResult.version,
|
||||
sourceOciDigest: firstResult.sourceOciDigest,
|
||||
podmanVersions,
|
||||
gitSha: firstResult.gitSha,
|
||||
gitDirty: firstResult.gitDirty,
|
||||
configSha256: firstResult.configSha256,
|
||||
builtBy: firstResult.builtBy,
|
||||
results: [...results].sort(compareByArch),
|
||||
}
|
||||
}
|
||||
|
||||
function groupByAgentVersion(buildResults: BuildResult[]): ResultGroup[] {
|
||||
const grouped = new Map<string, BuildResult[]>()
|
||||
|
||||
for (const result of buildResults) {
|
||||
const key = keyForGroup(result.name, result.version)
|
||||
const existing = grouped.get(key)
|
||||
if (existing) {
|
||||
existing.push(result)
|
||||
continue
|
||||
}
|
||||
grouped.set(key, [result])
|
||||
}
|
||||
|
||||
return [...grouped.values()]
|
||||
.map((results) => buildGroup(results))
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
async function readBodyAsString(body: unknown): Promise<string> {
|
||||
const withTransform = body as {
|
||||
transformToByteArray?: () => Promise<Uint8Array>
|
||||
}
|
||||
if (!withTransform?.transformToByteArray) {
|
||||
throw new Error('R2 response body is not readable')
|
||||
}
|
||||
|
||||
const bytes = await withTransform.transformToByteArray()
|
||||
return new TextDecoder().decode(bytes)
|
||||
}
|
||||
|
||||
async function readExistingAggregateEntries(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
): Promise<AggregateEntry[]> {
|
||||
try {
|
||||
const response = await client.send(
|
||||
new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: keyForAggregateManifest(),
|
||||
}),
|
||||
)
|
||||
const body = await readBodyAsString(response.Body)
|
||||
const parsed = aggregateManifestSchema.parse(JSON.parse(body))
|
||||
return parsed.agents
|
||||
} catch (error) {
|
||||
const maybeError = error as {
|
||||
name?: string
|
||||
$metadata?: { httpStatusCode?: number }
|
||||
}
|
||||
if (
|
||||
maybeError?.name === 'NoSuchKey' ||
|
||||
maybeError?.$metadata?.httpStatusCode === 404
|
||||
) {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function rollbackKeys(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
uploadedKeys: string[],
|
||||
): Promise<void> {
|
||||
await Promise.allSettled(
|
||||
[...uploadedKeys].reverse().map((key) => deleteObject(client, bucket, key)),
|
||||
)
|
||||
}
|
||||
|
||||
export async function publishAgents(options: PublishOptions): Promise<void> {
|
||||
if (options.buildResults.length === 0) {
|
||||
throw new Error('buildResults must not be empty')
|
||||
}
|
||||
|
||||
const client = options.client ?? createR2Client()
|
||||
const bucket = options.bucket ?? getBucket()
|
||||
const cdnBaseURL = options.cdnBaseURL ?? CDN_BASE_URL
|
||||
const now = options.now ?? (() => new Date())
|
||||
const uploadedKeys: string[] = []
|
||||
|
||||
try {
|
||||
const groups = groupByAgentVersion(options.buildResults)
|
||||
const builtAt = now().toISOString()
|
||||
|
||||
for (const group of groups) {
|
||||
for (const result of group.results) {
|
||||
const tarKey = keyForTarball(
|
||||
result.name,
|
||||
result.version,
|
||||
result.arch,
|
||||
result.publishAs,
|
||||
)
|
||||
const shaKey = keyForSha(
|
||||
result.name,
|
||||
result.version,
|
||||
result.arch,
|
||||
result.publishAs,
|
||||
)
|
||||
|
||||
await uploadFile(client, bucket, tarKey, result.tarballPath)
|
||||
uploadedKeys.push(tarKey)
|
||||
|
||||
await uploadFile(
|
||||
client,
|
||||
bucket,
|
||||
shaKey,
|
||||
result.tarballShaPath,
|
||||
SHA_CONTENT_TYPE,
|
||||
)
|
||||
uploadedKeys.push(shaKey)
|
||||
}
|
||||
|
||||
const manifest = createManifestForGroup(group, builtAt, cdnBaseURL)
|
||||
const manifestKey = keyForVersionManifest(group.name, group.version)
|
||||
await uploadBody(
|
||||
client,
|
||||
bucket,
|
||||
manifestKey,
|
||||
`${JSON.stringify(manifest, null, 2)}\n`,
|
||||
JSON_CONTENT_TYPE,
|
||||
)
|
||||
uploadedKeys.push(manifestKey)
|
||||
}
|
||||
|
||||
if (options.updateAggregate) {
|
||||
const existingEntries = await readExistingAggregateEntries(client, bucket)
|
||||
const aggregate = mergeAggregateEntries(
|
||||
existingEntries,
|
||||
buildAggregateEntries(groups, cdnBaseURL),
|
||||
builtAt,
|
||||
aggregateBuiltBy(groups),
|
||||
)
|
||||
const aggregateKey = keyForAggregateManifest()
|
||||
await uploadBody(
|
||||
client,
|
||||
bucket,
|
||||
aggregateKey,
|
||||
`${JSON.stringify(aggregate, null, 2)}\n`,
|
||||
JSON_CONTENT_TYPE,
|
||||
)
|
||||
uploadedKeys.push(aggregateKey)
|
||||
}
|
||||
} catch (error) {
|
||||
await rollbackKeys(client, bucket, uploadedKeys)
|
||||
throw error
|
||||
} finally {
|
||||
if (!options.client) {
|
||||
client.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export const ARCHES = ['amd64', 'arm64'] as const
|
||||
|
||||
export type ContainerArch = (typeof ARCHES)[number]
|
||||
|
||||
export function parseArch(value: string): ContainerArch {
|
||||
if (value === 'amd64' || value === 'arm64') {
|
||||
return value
|
||||
}
|
||||
|
||||
throw new Error(`invalid container arch: ${value}`)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
export type { ContainerArch } from './arch'
|
||||
export { ARCHES, parseArch } from './arch'
|
||||
export type {
|
||||
AgentArtifact,
|
||||
AgentManifest,
|
||||
AggregateEntry,
|
||||
AggregateManifest,
|
||||
} from './manifest'
|
||||
export {
|
||||
agentArtifactSchema,
|
||||
agentManifestSchema,
|
||||
aggregateEntrySchema,
|
||||
aggregateManifestSchema,
|
||||
MANIFEST_SCHEMA_VERSION,
|
||||
ociDigestSchema,
|
||||
parseAgentManifest,
|
||||
parseAggregateManifest,
|
||||
sha256HexSchema,
|
||||
} from './manifest'
|
||||
export {
|
||||
keyForAggregateManifest,
|
||||
keyForSha,
|
||||
keyForTarball,
|
||||
keyForVersionManifest,
|
||||
R2_AGENTS_PREFIX,
|
||||
} from './r2-keys'
|
||||
@@ -1,65 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ARCHES } from './arch'
|
||||
|
||||
export const MANIFEST_SCHEMA_VERSION = 'v1' as const
|
||||
|
||||
export const sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
export const ociDigestSchema = z.string().regex(/^sha256:[a-f0-9]{64}$/)
|
||||
|
||||
export const agentArtifactSchema = z.object({
|
||||
arch: z.enum(ARCHES),
|
||||
filename: z.string().min(1),
|
||||
format: z.literal('oci-archive+gzip'),
|
||||
compressed_sha256: sha256HexSchema,
|
||||
compressed_size_bytes: z.number().int().positive(),
|
||||
uncompressed_sha256: sha256HexSchema,
|
||||
uncompressed_size_bytes: z.number().int().positive(),
|
||||
url: z.string().url(),
|
||||
})
|
||||
|
||||
export const agentManifestSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
schema: z.literal(MANIFEST_SCHEMA_VERSION),
|
||||
build: z.object({
|
||||
git_sha: z.string().min(1),
|
||||
git_dirty: z.boolean(),
|
||||
built_at: z.string().datetime(),
|
||||
built_by: z.string().min(1),
|
||||
config_sha256: sha256HexSchema,
|
||||
podman_versions: z.array(z.string().min(1)).min(1),
|
||||
}),
|
||||
source: z.object({
|
||||
image: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
oci_digest: ociDigestSchema,
|
||||
}),
|
||||
artifacts: z.array(agentArtifactSchema).min(1),
|
||||
})
|
||||
|
||||
export const aggregateEntrySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
oci_digest: ociDigestSchema,
|
||||
manifest_url: z.string().url(),
|
||||
})
|
||||
|
||||
export const aggregateManifestSchema = z.object({
|
||||
schema: z.literal(MANIFEST_SCHEMA_VERSION),
|
||||
built_at: z.string().datetime(),
|
||||
built_by: z.string().min(1),
|
||||
agents: z.array(aggregateEntrySchema).min(1),
|
||||
})
|
||||
|
||||
export type AgentArtifact = z.infer<typeof agentArtifactSchema>
|
||||
export type AgentManifest = z.infer<typeof agentManifestSchema>
|
||||
export type AggregateEntry = z.infer<typeof aggregateEntrySchema>
|
||||
export type AggregateManifest = z.infer<typeof aggregateManifestSchema>
|
||||
|
||||
export function parseAgentManifest(raw: unknown): AgentManifest {
|
||||
return agentManifestSchema.parse(raw)
|
||||
}
|
||||
|
||||
export function parseAggregateManifest(raw: unknown): AggregateManifest {
|
||||
return aggregateManifestSchema.parse(raw)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { ContainerArch } from './arch'
|
||||
|
||||
export const R2_AGENTS_PREFIX = 'agents'
|
||||
|
||||
export function keyForTarball(
|
||||
agent: string,
|
||||
version: string,
|
||||
arch: ContainerArch,
|
||||
publishAs = agent,
|
||||
): string {
|
||||
return `${R2_AGENTS_PREFIX}/${agent}/${version}/${publishAs}-${version}-${arch}.tar.gz`
|
||||
}
|
||||
|
||||
export function keyForSha(
|
||||
agent: string,
|
||||
version: string,
|
||||
arch: ContainerArch,
|
||||
publishAs = agent,
|
||||
): string {
|
||||
return `${keyForTarball(agent, version, arch, publishAs)}.sha256`
|
||||
}
|
||||
|
||||
export function keyForVersionManifest(agent: string, version: string): string {
|
||||
return `${R2_AGENTS_PREFIX}/${agent}/${version}/manifest.json`
|
||||
}
|
||||
|
||||
export function keyForAggregateManifest(): string {
|
||||
return `${R2_AGENTS_PREFIX}/manifest.json`
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { createReadStream, createWriteStream } from 'node:fs'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { createGunzip } from 'node:zlib'
|
||||
|
||||
import {
|
||||
podmanInspectImage,
|
||||
podmanLoadArchive,
|
||||
podmanRemoveImage,
|
||||
} from './build'
|
||||
|
||||
export interface RoundTripPodmanLoadOptions {
|
||||
tarballPath: string
|
||||
expectedImage: string
|
||||
expectedImageId?: string
|
||||
expectedSmokeFingerprint?: string
|
||||
}
|
||||
|
||||
async function maybeDecompressTarball(tarballPath: string): Promise<{
|
||||
tarPath: string
|
||||
cleanupDir?: string
|
||||
}> {
|
||||
if (!tarballPath.endsWith('.gz')) {
|
||||
return { tarPath: tarballPath }
|
||||
}
|
||||
|
||||
const tempDir = await mkdtemp(join(tmpdir(), 'agent-container-smoke-'))
|
||||
const tarPath = join(tempDir, 'image.tar')
|
||||
await pipeline(
|
||||
createReadStream(tarballPath),
|
||||
createGunzip(),
|
||||
createWriteStream(tarPath),
|
||||
)
|
||||
|
||||
return { tarPath, cleanupDir: tempDir }
|
||||
}
|
||||
|
||||
export async function roundTripPodmanLoad(
|
||||
options: RoundTripPodmanLoadOptions,
|
||||
): Promise<void> {
|
||||
if (!options.expectedImageId && !options.expectedSmokeFingerprint) {
|
||||
throw new Error(
|
||||
'expectedImageId or expectedSmokeFingerprint is required for smoke verification',
|
||||
)
|
||||
}
|
||||
|
||||
const decompressed = await maybeDecompressTarball(options.tarballPath)
|
||||
|
||||
try {
|
||||
await podmanRemoveImage(options.expectedImage).catch(() => {})
|
||||
await podmanLoadArchive(decompressed.tarPath)
|
||||
|
||||
const inspection = await podmanInspectImage(options.expectedImage)
|
||||
if (
|
||||
options.expectedSmokeFingerprint &&
|
||||
inspection.smokeFingerprint !== options.expectedSmokeFingerprint
|
||||
) {
|
||||
throw new Error(
|
||||
`loaded image fingerprint mismatch: expected ${options.expectedSmokeFingerprint}, got ${inspection.smokeFingerprint}`,
|
||||
)
|
||||
}
|
||||
if (
|
||||
options.expectedImageId &&
|
||||
inspection.imageId !== options.expectedImageId
|
||||
) {
|
||||
throw new Error(
|
||||
`loaded image ID mismatch: expected ${options.expectedImageId}, got ${inspection.imageId}`,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
await podmanRemoveImage(options.expectedImage).catch(() => {})
|
||||
if (decompressed.cleanupDir) {
|
||||
await rm(decompressed.cleanupDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import { ARCHES, parseArch } from '../src/schema/arch'
|
||||
|
||||
describe('schema/arch', () => {
|
||||
it('exports the supported arches', () => {
|
||||
expect(ARCHES).toEqual(['amd64', 'arm64'])
|
||||
})
|
||||
|
||||
it('parses valid arches', () => {
|
||||
expect(parseArch('amd64')).toBe('amd64')
|
||||
expect(parseArch('arm64')).toBe('arm64')
|
||||
})
|
||||
|
||||
it('rejects invalid arches', () => {
|
||||
expect(() => parseArch('x64')).toThrow('invalid container arch')
|
||||
})
|
||||
})
|
||||
@@ -1,112 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { expandMatrix, readAgentsConfig } from '../src/catalog'
|
||||
|
||||
const tempPaths: string[] = []
|
||||
|
||||
async function writeTempConfig(contents: unknown): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'agent-container-catalog-'))
|
||||
const filePath = join(dir, 'agents.json')
|
||||
tempPaths.push(dir)
|
||||
await writeFile(filePath, `${JSON.stringify(contents, null, 2)}\n`, 'utf8')
|
||||
return filePath
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempPaths
|
||||
.splice(0)
|
||||
.map((path) => rm(path, { recursive: true, force: true })),
|
||||
)
|
||||
})
|
||||
|
||||
describe('catalog', () => {
|
||||
it('reads and expands the agent matrix', async () => {
|
||||
const path = await writeTempConfig({
|
||||
schema: 'v1',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arches: ['amd64', 'arm64'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const config = await readAgentsConfig(path)
|
||||
expect(expandMatrix(config)).toEqual([
|
||||
{
|
||||
agent: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arch: 'amd64',
|
||||
publishAs: 'openclaw',
|
||||
},
|
||||
{
|
||||
agent: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arch: 'arm64',
|
||||
publishAs: 'openclaw',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('filters the matrix by agent name', async () => {
|
||||
const path = await writeTempConfig({
|
||||
schema: 'v1',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arches: ['amd64'],
|
||||
},
|
||||
{
|
||||
name: 'claude-code',
|
||||
image: 'ghcr.io/example/claude-code',
|
||||
version: '1.2.3',
|
||||
arches: ['arm64'],
|
||||
publishAs: 'claude',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const config = await readAgentsConfig(path)
|
||||
expect(expandMatrix(config, { agent: 'claude-code' })).toEqual([
|
||||
{
|
||||
agent: 'claude-code',
|
||||
image: 'ghcr.io/example/claude-code',
|
||||
version: '1.2.3',
|
||||
arch: 'arm64',
|
||||
publishAs: 'claude',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects duplicate agent names', async () => {
|
||||
const path = await writeTempConfig({
|
||||
schema: 'v1',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arches: ['amd64'],
|
||||
},
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/example/openclaw',
|
||||
version: '2026.4.13',
|
||||
arches: ['arm64'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect(readAgentsConfig(path)).rejects.toThrow('duplicate agent name')
|
||||
})
|
||||
})
|
||||
@@ -1,47 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { readAgentsConfig } from '../src/catalog'
|
||||
|
||||
const packageRoot = resolve(import.meta.dir, '..')
|
||||
const recipePath = resolve(packageRoot, 'recipe', 'agents.json')
|
||||
const runtimePath = resolve(
|
||||
import.meta.dir,
|
||||
'..',
|
||||
'..',
|
||||
'..',
|
||||
'apps',
|
||||
'server',
|
||||
'src',
|
||||
'api',
|
||||
'services',
|
||||
'openclaw',
|
||||
'openclaw-service.ts',
|
||||
)
|
||||
|
||||
describe('OpenClaw drift guard', () => {
|
||||
it('keeps recipe/agents.json in sync with the runtime image pin', async () => {
|
||||
const [config, runtimeSource] = await Promise.all([
|
||||
readAgentsConfig(recipePath),
|
||||
readFile(runtimePath, 'utf8'),
|
||||
])
|
||||
|
||||
const openclaw = config.agents.find((agent) => agent.name === 'openclaw')
|
||||
expect(openclaw).toBeDefined()
|
||||
|
||||
const match = runtimeSource.match(
|
||||
/return process\.env\.OPENCLAW_IMAGE \|\| ['"]([^'"]+)['"]/,
|
||||
)
|
||||
if (!match?.[1]) {
|
||||
throw new Error(
|
||||
`failed to extract OpenClaw image fallback from ${runtimePath}`,
|
||||
)
|
||||
}
|
||||
|
||||
const recipeImage = `${openclaw?.image}:${openclaw?.version}`
|
||||
expect(recipeImage).toBe(match[1], {
|
||||
message: `OpenClaw image drifted between ${recipePath} and ${runtimePath}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,223 +0,0 @@
|
||||
import { afterEach, describe, expect, it, mock, spyOn } from 'bun:test'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import {
|
||||
buildTarball,
|
||||
podmanInspectImage,
|
||||
registryForImage,
|
||||
} from '../src/build'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
|
||||
function processResult(
|
||||
stdout: string,
|
||||
stderr = '',
|
||||
exitCode = 0,
|
||||
): Bun.Subprocess {
|
||||
return {
|
||||
stdout: new Response(stdout).body,
|
||||
stderr: new Response(stderr).body,
|
||||
exited: Promise.resolve(exitCode),
|
||||
} as Bun.Subprocess
|
||||
}
|
||||
|
||||
async function createTempDir(): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'agent-container-build-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
mock.restore()
|
||||
await Promise.all(
|
||||
tempDirs
|
||||
.splice(0)
|
||||
.map((path) => rm(path, { recursive: true, force: true })),
|
||||
)
|
||||
})
|
||||
|
||||
describe('build', () => {
|
||||
it('resolves registry hosts correctly', () => {
|
||||
expect(registryForImage('ghcr.io/openclaw/openclaw')).toBe('ghcr.io')
|
||||
expect(registryForImage('localhost:5000/example/image')).toBe(
|
||||
'localhost:5000',
|
||||
)
|
||||
expect(registryForImage('busybox')).toBe('docker.io')
|
||||
})
|
||||
|
||||
it('builds a tarball result and writes the sidecar files', async () => {
|
||||
const dir = await createTempDir()
|
||||
const outputDir = join(dir, 'dist')
|
||||
const recipePath = join(dir, 'agents.json')
|
||||
await writeFile(
|
||||
recipePath,
|
||||
JSON.stringify({
|
||||
schema: 'v1',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arches: ['arm64'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
const originalSpawn = Bun.spawn
|
||||
const podmanCommands: string[][] = []
|
||||
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
|
||||
if (Array.isArray(command) && command[0] === 'podman') {
|
||||
podmanCommands.push(command)
|
||||
|
||||
if (command[1] === '--version') {
|
||||
return processResult('podman version 5.8.1\n')
|
||||
}
|
||||
if (command[1] === 'pull') {
|
||||
return processResult('')
|
||||
}
|
||||
if (command[1] === 'inspect') {
|
||||
return processResult(
|
||||
JSON.stringify({
|
||||
Id: 'f'.repeat(64),
|
||||
Digest: `sha256:${'1'.repeat(64)}`,
|
||||
RepoDigests: [
|
||||
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
|
||||
`ghcr.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
|
||||
],
|
||||
Architecture: 'arm64',
|
||||
Os: 'linux',
|
||||
Config: {
|
||||
Entrypoint: ['/entrypoint.sh'],
|
||||
Env: ['NODE_ENV=production'],
|
||||
},
|
||||
RootFS: {
|
||||
Type: 'layers',
|
||||
Layers: ['sha256:abc'],
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
if (command[1] === 'save') {
|
||||
const outPath = String(command[5])
|
||||
void writeFile(outPath, 'oci archive payload', 'utf8')
|
||||
return processResult('')
|
||||
}
|
||||
}
|
||||
|
||||
return originalSpawn(
|
||||
command as string[],
|
||||
options as SpawnOptions.OptionsObject<string[]>,
|
||||
)
|
||||
})
|
||||
|
||||
const result = await buildTarball({
|
||||
agent: {
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arches: ['arm64'],
|
||||
},
|
||||
arch: 'arm64',
|
||||
outputDir,
|
||||
recipePath,
|
||||
builtBy: 'test-run',
|
||||
})
|
||||
|
||||
expect(result.filename).toBe('openclaw-2026.4.12-arm64.tar.gz')
|
||||
expect(result.sourceOciDigest).toBe(`sha256:${'2'.repeat(64)}`)
|
||||
expect(result.imageId).toBe(`sha256:${'f'.repeat(64)}`)
|
||||
expect(result.smokeFingerprint).toHaveLength(64)
|
||||
expect(existsSync(result.tarballPath)).toBe(true)
|
||||
expect(existsSync(result.tarballShaPath)).toBe(true)
|
||||
expect(existsSync(join(outputDir, 'openclaw-2026.4.12-arm64.tar'))).toBe(
|
||||
false,
|
||||
)
|
||||
expect(existsSync(join(outputDir, 'build-result.json'))).toBe(true)
|
||||
expect(
|
||||
podmanCommands.some(
|
||||
(command) =>
|
||||
command[1] === 'pull' &&
|
||||
command.includes('--arch') &&
|
||||
command.includes('arm64'),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('prefers the repeated non-platform repo digest as the source OCI digest', async () => {
|
||||
const originalSpawn = Bun.spawn
|
||||
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
|
||||
if (
|
||||
Array.isArray(command) &&
|
||||
command[0] === 'podman' &&
|
||||
command[1] === 'inspect'
|
||||
) {
|
||||
return processResult(
|
||||
JSON.stringify({
|
||||
Id: 'f'.repeat(64),
|
||||
Digest: `sha256:${'1'.repeat(64)}`,
|
||||
RepoDigests: [
|
||||
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
|
||||
`mirror.example/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
|
||||
`docker.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
|
||||
],
|
||||
Architecture: 'arm64',
|
||||
Os: 'linux',
|
||||
Config: {},
|
||||
RootFS: {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return originalSpawn(
|
||||
command as string[],
|
||||
options as SpawnOptions.OptionsObject<string[]>,
|
||||
)
|
||||
})
|
||||
|
||||
const inspection = await podmanInspectImage(
|
||||
'ghcr.io/openclaw/openclaw:2026.4.12',
|
||||
)
|
||||
expect(inspection.sourceOciDigest).toBe(`sha256:${'2'.repeat(64)}`)
|
||||
})
|
||||
|
||||
it('fails when repo digests disagree without a clear winner', async () => {
|
||||
const originalSpawn = Bun.spawn
|
||||
spyOn(Bun, 'spawn').mockImplementation((command, options) => {
|
||||
if (
|
||||
Array.isArray(command) &&
|
||||
command[0] === 'podman' &&
|
||||
command[1] === 'inspect'
|
||||
) {
|
||||
return processResult(
|
||||
JSON.stringify({
|
||||
Id: 'f'.repeat(64),
|
||||
Digest: `sha256:${'1'.repeat(64)}`,
|
||||
RepoDigests: [
|
||||
`ghcr.io/openclaw/openclaw@sha256:${'2'.repeat(64)}`,
|
||||
`mirror.example/openclaw/openclaw@sha256:${'3'.repeat(64)}`,
|
||||
`docker.io/openclaw/openclaw@sha256:${'1'.repeat(64)}`,
|
||||
],
|
||||
Architecture: 'arm64',
|
||||
Os: 'linux',
|
||||
Config: {},
|
||||
RootFS: {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return originalSpawn(
|
||||
command as string[],
|
||||
options as SpawnOptions.OptionsObject<string[]>,
|
||||
)
|
||||
})
|
||||
|
||||
await expect(
|
||||
podmanInspectImage('ghcr.io/openclaw/openclaw:2026.4.12'),
|
||||
).rejects.toThrow('ambiguous source OCI digest')
|
||||
})
|
||||
})
|
||||
@@ -1,365 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
type S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
|
||||
import type { BuildResult } from '../src/build'
|
||||
import { publishAgents } from '../src/publish'
|
||||
|
||||
const tempDirs: string[] = []
|
||||
|
||||
function sha(char: string): string {
|
||||
return char.repeat(64)
|
||||
}
|
||||
|
||||
async function createBuildResult(
|
||||
root: string,
|
||||
arch: 'amd64' | 'arm64',
|
||||
overrides: Partial<BuildResult> = {},
|
||||
): Promise<BuildResult> {
|
||||
const dir = join(root, arch)
|
||||
await mkdir(dir, { recursive: true })
|
||||
const tarballPath = join(dir, `openclaw-2026.4.12-${arch}.tar.gz`)
|
||||
const tarballShaPath = `${tarballPath}.sha256`
|
||||
await writeFile(tarballPath, `${arch}-tarball`, 'utf8')
|
||||
await writeFile(
|
||||
tarballShaPath,
|
||||
`${sha(arch === 'amd64' ? 'a' : 'b')} file\n`,
|
||||
'utf8',
|
||||
)
|
||||
|
||||
return {
|
||||
name: 'openclaw',
|
||||
publishAs: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
arch,
|
||||
sourceOciDigest: `sha256:${sha('c')}`,
|
||||
imageId: `sha256:${sha(arch === 'amd64' ? 'd' : 'e')}`,
|
||||
smokeFingerprint: sha(arch === 'amd64' ? '6' : '7'),
|
||||
filename: `openclaw-2026.4.12-${arch}.tar.gz`,
|
||||
tarballPath,
|
||||
tarballShaPath,
|
||||
compressedSha256: sha(arch === 'amd64' ? '1' : '2'),
|
||||
compressedSizeBytes: 100,
|
||||
uncompressedSha256: sha(arch === 'amd64' ? '3' : '4'),
|
||||
uncompressedSizeBytes: 200,
|
||||
podmanVersion: 'podman version 5.8.1',
|
||||
builtAt: '2026-04-22T17:30:00.000Z',
|
||||
builtBy: 'workflow@refs/heads/dev',
|
||||
gitSha: 'abc123',
|
||||
gitDirty: false,
|
||||
configSha256: sha('5'),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
async function createTempDir(): Promise<string> {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'agent-container-publish-'))
|
||||
tempDirs.push(dir)
|
||||
return dir
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempDirs
|
||||
.splice(0)
|
||||
.map((path) => rm(path, { recursive: true, force: true })),
|
||||
)
|
||||
})
|
||||
|
||||
describe('publish', () => {
|
||||
it('uploads version manifests and updates aggregate last', async () => {
|
||||
const root = await createTempDir()
|
||||
const buildResults = await Promise.all([
|
||||
createBuildResult(root, 'amd64'),
|
||||
createBuildResult(root, 'arm64'),
|
||||
])
|
||||
const puts: Array<{ key: string; body: unknown }> = []
|
||||
|
||||
const client = {
|
||||
send: async (command: unknown) => {
|
||||
if (command instanceof GetObjectCommand) {
|
||||
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
|
||||
}
|
||||
if (command instanceof PutObjectCommand) {
|
||||
puts.push({
|
||||
key: String(command.input.Key),
|
||||
body: command.input.Body,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
if (command instanceof DeleteObjectCommand) {
|
||||
return {}
|
||||
}
|
||||
throw new Error('unexpected command')
|
||||
},
|
||||
destroy: () => {},
|
||||
} as unknown as S3Client
|
||||
|
||||
await publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: true,
|
||||
bucket: 'test-bucket',
|
||||
cdnBaseURL: 'https://cdn.example.com',
|
||||
client,
|
||||
now: () => new Date('2026-04-22T18:00:00.000Z'),
|
||||
})
|
||||
|
||||
expect(puts.map((entry) => entry.key)).toEqual([
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz.sha256',
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-arm64.tar.gz',
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-arm64.tar.gz.sha256',
|
||||
'agents/openclaw/2026.4.12/manifest.json',
|
||||
'agents/manifest.json',
|
||||
])
|
||||
|
||||
const versionManifest = JSON.parse(
|
||||
String(puts.find((entry) => entry.key.endsWith('/manifest.json'))?.body),
|
||||
)
|
||||
expect(versionManifest.source.oci_digest).toBe(`sha256:${sha('c')}`)
|
||||
expect(versionManifest.artifacts[0].url).toBe(
|
||||
'https://cdn.example.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
)
|
||||
|
||||
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
|
||||
expect(aggregateManifest.agents).toEqual([
|
||||
{
|
||||
name: 'openclaw',
|
||||
version: '2026.4.12',
|
||||
oci_digest: `sha256:${sha('c')}`,
|
||||
manifest_url:
|
||||
'https://cdn.example.com/agents/openclaw/2026.4.12/manifest.json',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('rolls back uploaded keys when a later upload fails', async () => {
|
||||
const root = await createTempDir()
|
||||
const buildResults = [await createBuildResult(root, 'amd64')]
|
||||
const deleted: string[] = []
|
||||
|
||||
const client = {
|
||||
send: async (command: unknown) => {
|
||||
if (command instanceof GetObjectCommand) {
|
||||
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
|
||||
}
|
||||
if (command instanceof PutObjectCommand) {
|
||||
if (String(command.input.Key).endsWith('/manifest.json')) {
|
||||
throw new Error('manifest upload failed')
|
||||
}
|
||||
return {}
|
||||
}
|
||||
if (command instanceof DeleteObjectCommand) {
|
||||
deleted.push(String(command.input.Key))
|
||||
return {}
|
||||
}
|
||||
throw new Error('unexpected command')
|
||||
},
|
||||
destroy: () => {},
|
||||
} as unknown as S3Client
|
||||
|
||||
await expect(
|
||||
publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: true,
|
||||
bucket: 'test-bucket',
|
||||
client,
|
||||
}),
|
||||
).rejects.toThrow('manifest upload failed')
|
||||
|
||||
expect(deleted).toEqual([
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz.sha256',
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
])
|
||||
})
|
||||
|
||||
it('merges new entries into an existing aggregate manifest', async () => {
|
||||
const root = await createTempDir()
|
||||
const buildResults = [await createBuildResult(root, 'amd64')]
|
||||
const puts: Array<{ key: string; body: unknown }> = []
|
||||
|
||||
const client = {
|
||||
send: async (command: unknown) => {
|
||||
if (command instanceof GetObjectCommand) {
|
||||
return {
|
||||
Body: {
|
||||
transformToByteArray: async () =>
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
schema: 'v1',
|
||||
built_at: '2026-04-21T00:00:00.000Z',
|
||||
built_by: 'previous',
|
||||
agents: [
|
||||
{
|
||||
name: 'claude-code',
|
||||
version: '1.0.0',
|
||||
oci_digest: `sha256:${sha('9')}`,
|
||||
manifest_url:
|
||||
'https://cdn.example.com/agents/claude-code/1.0.0/manifest.json',
|
||||
},
|
||||
{
|
||||
name: 'openclaw',
|
||||
version: '2026.4.11',
|
||||
oci_digest: `sha256:${sha('8')}`,
|
||||
manifest_url:
|
||||
'https://cdn.example.com/agents/openclaw/2026.4.11/manifest.json',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (command instanceof PutObjectCommand) {
|
||||
puts.push({
|
||||
key: String(command.input.Key),
|
||||
body: command.input.Body,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
if (command instanceof DeleteObjectCommand) {
|
||||
return {}
|
||||
}
|
||||
throw new Error('unexpected command')
|
||||
},
|
||||
destroy: () => {},
|
||||
} as unknown as S3Client
|
||||
|
||||
await publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: true,
|
||||
bucket: 'test-bucket',
|
||||
cdnBaseURL: 'https://cdn.example.com',
|
||||
client,
|
||||
now: () => new Date('2026-04-22T18:00:00.000Z'),
|
||||
})
|
||||
|
||||
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
|
||||
expect(aggregateManifest.agents).toEqual([
|
||||
{
|
||||
name: 'claude-code',
|
||||
version: '1.0.0',
|
||||
oci_digest: `sha256:${sha('9')}`,
|
||||
manifest_url:
|
||||
'https://cdn.example.com/agents/claude-code/1.0.0/manifest.json',
|
||||
},
|
||||
{
|
||||
name: 'openclaw',
|
||||
version: '2026.4.12',
|
||||
oci_digest: `sha256:${sha('c')}`,
|
||||
manifest_url:
|
||||
'https://cdn.example.com/agents/openclaw/2026.4.12/manifest.json',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('records distinct podman versions across arches', async () => {
|
||||
const root = await createTempDir()
|
||||
const buildResults = await Promise.all([
|
||||
createBuildResult(root, 'amd64', {
|
||||
podmanVersion: 'podman version 5.8.1',
|
||||
}),
|
||||
createBuildResult(root, 'arm64', {
|
||||
podmanVersion: 'podman version 5.9.0',
|
||||
}),
|
||||
])
|
||||
const puts: Array<{ key: string; body: unknown }> = []
|
||||
|
||||
const client = {
|
||||
send: async (command: unknown) => {
|
||||
if (command instanceof GetObjectCommand) {
|
||||
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
|
||||
}
|
||||
if (command instanceof PutObjectCommand) {
|
||||
puts.push({
|
||||
key: String(command.input.Key),
|
||||
body: command.input.Body,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
if (command instanceof DeleteObjectCommand) {
|
||||
return {}
|
||||
}
|
||||
throw new Error('unexpected command')
|
||||
},
|
||||
destroy: () => {},
|
||||
} as unknown as S3Client
|
||||
|
||||
await publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: false,
|
||||
bucket: 'test-bucket',
|
||||
client,
|
||||
})
|
||||
|
||||
const versionManifest = JSON.parse(
|
||||
String(puts.find((entry) => entry.key.endsWith('/manifest.json'))?.body),
|
||||
)
|
||||
expect(versionManifest.build.podman_versions).toEqual([
|
||||
'podman version 5.8.1',
|
||||
'podman version 5.9.0',
|
||||
])
|
||||
})
|
||||
|
||||
it('records all build provenance values in the aggregate manifest', async () => {
|
||||
const root = await createTempDir()
|
||||
const buildResults = [
|
||||
await createBuildResult(root, 'amd64', {
|
||||
name: 'claude-code',
|
||||
publishAs: 'claude-code',
|
||||
image: 'ghcr.io/example/claude-code',
|
||||
version: '1.0.0',
|
||||
builtBy: 'workflow-a@refs/heads/dev',
|
||||
}),
|
||||
await createBuildResult(root, 'arm64', {
|
||||
name: 'openclaw',
|
||||
publishAs: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
builtBy: 'workflow-b@refs/heads/dev',
|
||||
}),
|
||||
]
|
||||
const puts: Array<{ key: string; body: unknown }> = []
|
||||
|
||||
const client = {
|
||||
send: async (command: unknown) => {
|
||||
if (command instanceof GetObjectCommand) {
|
||||
throw { name: 'NoSuchKey', $metadata: { httpStatusCode: 404 } }
|
||||
}
|
||||
if (command instanceof PutObjectCommand) {
|
||||
puts.push({
|
||||
key: String(command.input.Key),
|
||||
body: command.input.Body,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
if (command instanceof DeleteObjectCommand) {
|
||||
return {}
|
||||
}
|
||||
throw new Error('unexpected command')
|
||||
},
|
||||
destroy: () => {},
|
||||
} as unknown as S3Client
|
||||
|
||||
await publishAgents({
|
||||
buildResults,
|
||||
updateAggregate: true,
|
||||
bucket: 'test-bucket',
|
||||
client,
|
||||
now: () => new Date('2026-04-22T18:00:00.000Z'),
|
||||
})
|
||||
|
||||
const aggregateManifest = JSON.parse(String(puts.at(-1)?.body))
|
||||
expect(aggregateManifest.built_by).toBe(
|
||||
'workflow-a@refs/heads/dev, workflow-b@refs/heads/dev',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import {
|
||||
keyForAggregateManifest,
|
||||
keyForSha,
|
||||
keyForTarball,
|
||||
keyForVersionManifest,
|
||||
} from '../src/schema/r2-keys'
|
||||
|
||||
describe('schema/r2-keys', () => {
|
||||
it('builds tarball keys', () => {
|
||||
expect(keyForTarball('openclaw', '2026.4.12', 'amd64')).toBe(
|
||||
'agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
)
|
||||
})
|
||||
|
||||
it('supports a custom publishAs filename prefix', () => {
|
||||
expect(keyForTarball('claude-code', '1.2.3', 'arm64', 'claude')).toBe(
|
||||
'agents/claude-code/1.2.3/claude-1.2.3-arm64.tar.gz',
|
||||
)
|
||||
expect(keyForSha('claude-code', '1.2.3', 'arm64', 'claude')).toBe(
|
||||
'agents/claude-code/1.2.3/claude-1.2.3-arm64.tar.gz.sha256',
|
||||
)
|
||||
})
|
||||
|
||||
it('builds manifest keys', () => {
|
||||
expect(keyForVersionManifest('openclaw', '2026.4.12')).toBe(
|
||||
'agents/openclaw/2026.4.12/manifest.json',
|
||||
)
|
||||
expect(keyForAggregateManifest()).toBe('agents/manifest.json')
|
||||
})
|
||||
})
|
||||
@@ -1,100 +0,0 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
|
||||
import {
|
||||
parseAgentManifest,
|
||||
parseAggregateManifest,
|
||||
} from '../src/schema/manifest'
|
||||
|
||||
function hex(char: string): string {
|
||||
return char.repeat(64)
|
||||
}
|
||||
|
||||
describe('schema/manifest', () => {
|
||||
it('parses a valid agent manifest', () => {
|
||||
const manifest = parseAgentManifest({
|
||||
name: 'openclaw',
|
||||
schema: 'v1',
|
||||
build: {
|
||||
git_sha: 'abc123',
|
||||
git_dirty: false,
|
||||
built_at: '2026-04-22T17:30:00.000Z',
|
||||
built_by: 'workflow@refs/heads/dev',
|
||||
config_sha256: hex('0'),
|
||||
podman_versions: ['podman version 5.8.1'],
|
||||
},
|
||||
source: {
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
oci_digest: `sha256:${hex('1')}`,
|
||||
},
|
||||
artifacts: [
|
||||
{
|
||||
arch: 'amd64',
|
||||
filename: 'openclaw-2026.4.12-amd64.tar.gz',
|
||||
format: 'oci-archive+gzip',
|
||||
compressed_sha256: hex('2'),
|
||||
compressed_size_bytes: 123,
|
||||
uncompressed_sha256: hex('3'),
|
||||
uncompressed_size_bytes: 456,
|
||||
url: 'https://cdn.browseros.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(manifest.source.version).toBe('2026.4.12')
|
||||
expect(manifest.artifacts).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('rejects invalid artifact hashes', () => {
|
||||
expect(() =>
|
||||
parseAgentManifest({
|
||||
name: 'openclaw',
|
||||
schema: 'v1',
|
||||
build: {
|
||||
git_sha: 'abc123',
|
||||
git_dirty: false,
|
||||
built_at: '2026-04-22T17:30:00.000Z',
|
||||
built_by: 'workflow@refs/heads/dev',
|
||||
config_sha256: hex('0'),
|
||||
podman_versions: ['podman version 5.8.1'],
|
||||
},
|
||||
source: {
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
oci_digest: `sha256:${hex('1')}`,
|
||||
},
|
||||
artifacts: [
|
||||
{
|
||||
arch: 'amd64',
|
||||
filename: 'openclaw-2026.4.12-amd64.tar.gz',
|
||||
format: 'oci-archive+gzip',
|
||||
compressed_sha256: 'bad',
|
||||
compressed_size_bytes: 123,
|
||||
uncompressed_sha256: hex('3'),
|
||||
uncompressed_size_bytes: 456,
|
||||
url: 'https://cdn.browseros.com/agents/openclaw/2026.4.12/openclaw-2026.4.12-amd64.tar.gz',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('parses a valid aggregate manifest', () => {
|
||||
const manifest = parseAggregateManifest({
|
||||
schema: 'v1',
|
||||
built_at: '2026-04-22T17:30:00.000Z',
|
||||
built_by: 'workflow@refs/heads/dev',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
version: '2026.4.12',
|
||||
oci_digest: `sha256:${hex('4')}`,
|
||||
manifest_url:
|
||||
'https://cdn.browseros.com/agents/openclaw/2026.4.12/manifest.json',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
expect(manifest.agents[0]?.name).toBe('openclaw')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
11
packages/browseros-agent/packages/build-tools/.env.sample
Normal file
11
packages/browseros-agent/packages/build-tools/.env.sample
Normal file
@@ -0,0 +1,11 @@
|
||||
# R2 / Cloudflare object storage - required by upload and publish jobs
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=browseros-artifacts
|
||||
|
||||
# Public CDN base - used by cache:sync to GET manifest and artifacts
|
||||
R2_PUBLIC_BASE_URL=https://cdn.browseros.com
|
||||
|
||||
# Dev mode routes cache to ~/.browseros-dev/cache/; unset for ~/.browseros/cache/
|
||||
NODE_ENV=development
|
||||
57
packages/browseros-agent/packages/build-tools/README.md
Normal file
57
packages/browseros-agent/packages/build-tools/README.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# @browseros/build-tools
|
||||
|
||||
Builds BrowserOS VM disks and agent image tarballs, publishes release artifacts to R2, and hydrates the local VM artifact cache for development.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp packages/build-tools/.env.sample packages/build-tools/.env
|
||||
bun install
|
||||
```
|
||||
|
||||
## Build a VM disk
|
||||
|
||||
Requires `libguestfs`, `qemu-img`, and `zstd` in an arm64 Linux environment.
|
||||
On Apple Silicon, run this from an arm64 Lima/Debian VM rather than directly on macOS.
|
||||
|
||||
```bash
|
||||
bun run --filter @browseros/build-tools build:disk -- --version 2026.04.22 --arch arm64
|
||||
```
|
||||
|
||||
## Build an agent tarball
|
||||
|
||||
Requires `podman`.
|
||||
|
||||
```bash
|
||||
bun run --filter @browseros/build-tools build:tarball -- --agent openclaw --arch arm64
|
||||
```
|
||||
|
||||
## Smoke test artifacts
|
||||
|
||||
VM smoke tests require `limactl`, `qemu`, and `zstd`. Agent tarball smoke tests require `podman`.
|
||||
|
||||
```bash
|
||||
bun run --filter @browseros/build-tools smoke:vm -- --arch arm64 --qcow ./dist/browseros-vm-2026.04.22-arm64.qcow2.zst
|
||||
bun run --filter @browseros/build-tools smoke:tarball -- --agent openclaw --arch arm64 --tarball ./dist/images/openclaw-2026.4.12-arm64.tar.gz
|
||||
```
|
||||
|
||||
## Emit a manifest
|
||||
|
||||
```bash
|
||||
bun run --filter @browseros/build-tools emit-manifest -- --dist-dir packages/build-tools/dist
|
||||
```
|
||||
|
||||
Publish workflows can update only one manifest slice at a time. Sliced publishing requires an existing R2 `vm/manifest.json` baseline; bootstrap first releases with `--slice full`.
|
||||
|
||||
```bash
|
||||
bun run --filter @browseros/build-tools emit-manifest -- --slice vm --merge-from https://cdn.browseros.com/vm/manifest.json
|
||||
bun run --filter @browseros/build-tools emit-manifest -- --slice agents:openclaw --merge-from https://cdn.browseros.com/vm/manifest.json
|
||||
```
|
||||
|
||||
## Sync the dev cache
|
||||
|
||||
```bash
|
||||
NODE_ENV=development bun run --filter @browseros/build-tools cache:sync
|
||||
```
|
||||
|
||||
Development cache files land under `~/.browseros-dev/cache/vm/`. Production-mode cache files land under `~/.browseros/cache/vm/`.
|
||||
10
packages/browseros-agent/packages/build-tools/bundle.json
Normal file
10
packages/browseros-agent/packages/build-tools/bundle.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"vmVersion": "2026.04.22",
|
||||
"agents": [
|
||||
{
|
||||
"name": "openclaw",
|
||||
"image": "ghcr.io/openclaw/openclaw",
|
||||
"version": "2026.4.12"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
packages/browseros-agent/packages/build-tools/package.json
Normal file
26
packages/browseros-agent/packages/build-tools/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@browseros/build-tools",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "BrowserOS release artifact producer and dev cache sync",
|
||||
"scripts": {
|
||||
"build:disk": "bun run scripts/build-disk.ts",
|
||||
"build:tarball": "bun run scripts/build-tarball.ts",
|
||||
"emit-manifest": "bun run scripts/emit-manifest.ts",
|
||||
"upload": "bun run scripts/upload-to-r2.ts",
|
||||
"download": "bun run scripts/download-from-r2.ts",
|
||||
"cache:sync": "bun run scripts/cache-sync.ts",
|
||||
"smoke:tarball": "bun run scripts/smoke-tarball.ts",
|
||||
"smoke:vm": "bun run scripts/smoke-vm.ts",
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"@browseros/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
ef5acb5908f6ef1f7ffcf3a63913cdf618da3229ffa3b04e3727959e36bb9de1
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"arm64": {
|
||||
"upstreamVersion": "20260413-2447",
|
||||
"url": "https://cloud.debian.org/images/cloud/bookworm/20260413-2447/debian-12-genericcloud-arm64-20260413-2447.qcow2",
|
||||
"sha512": "15ad6c52e255c84eb0e91001c5907b27199d8a7164d8ac172cfe9c92850dfaf606a6c3161d6af7f0fd5a5fef2aa8dcd9a23c2eb0fedbfcddb38e2bc306cba98f"
|
||||
},
|
||||
"x64": {
|
||||
"upstreamVersion": "20260413-2447",
|
||||
"url": "https://cloud.debian.org/images/cloud/bookworm/20260413-2447/debian-12-genericcloud-amd64-20260413-2447.qcow2",
|
||||
"sha512": "db11b13c4efcc37828ffadae521d101e85079d349e1418074087bb7d306f11caccdc2b0b539d6fd50d623d40a898f83c6137268a048d7700397dc35b7dcbc927"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# BrowserOS VM recipe — Debian 12 (bookworm) genericcloud
|
||||
# Consumed by src/build/orchestrator.ts. One virt-customize primitive per line.
|
||||
# Ops: run-command | copy-in <src>:<dest> | write <path>:<content> | truncate <path>
|
||||
# Consumed by scripts/build-disk.ts. One virt-customize primitive per line.
|
||||
# Ops: run-command | copy-in <src>:<dest-dir> | upload <src>:<dest-file> | write <path>:<content> | truncate <path>
|
||||
# {version} and {manifest_tmp} are substituted at build time.
|
||||
|
||||
run-command apt-get update
|
||||
@@ -12,9 +12,8 @@ run-command usermod -aG sudo browseros
|
||||
copy-in sudoers-browseros:/etc/sudoers.d/
|
||||
run-command chmod 0440 /etc/sudoers.d/sudoers-browseros
|
||||
write /etc/browseros-vm-version:{version}
|
||||
copy-in {manifest_tmp}:/etc/browseros-vm-manifest.json
|
||||
upload {manifest_tmp}:/etc/browseros-vm-manifest.json
|
||||
run-command apt-get clean
|
||||
run-command rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
run-command rm -f /etc/ssh/ssh_host_*
|
||||
run-command rm -f /etc/machine-id
|
||||
truncate /etc/machine-id
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bun
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import {
|
||||
copyFile,
|
||||
mkdir,
|
||||
readFile,
|
||||
rm,
|
||||
stat,
|
||||
writeFile,
|
||||
} from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { $ } from 'bun'
|
||||
import { type Arch, parseArch } from './common/arch'
|
||||
import { fetchWithTimeout } from './common/fetch'
|
||||
import { qcow2Key } from './common/manifest'
|
||||
import { sha256File } from './common/sha256'
|
||||
|
||||
type ChunkSink = ReturnType<ReturnType<typeof Bun.file>['writer']>
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
version: { type: 'string' },
|
||||
arch: { type: 'string' },
|
||||
'output-dir': { type: 'string', default: './dist' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.version || !values.arch) {
|
||||
console.error(
|
||||
'usage: build:disk -- --version <YYYY.MM.DD[-N]> --arch <arm64|x64> [--output-dir ./dist]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const arch = parseArch(values.arch)
|
||||
const version = values.version
|
||||
const outDir = values['output-dir']
|
||||
const pkgRoot = path.resolve(import.meta.dir, '..')
|
||||
|
||||
await mkdir(outDir, { recursive: true })
|
||||
|
||||
const baseImages = JSON.parse(
|
||||
await readFile(path.join(pkgRoot, 'recipe/base-images.json'), 'utf8'),
|
||||
) as Record<Arch, { upstreamVersion: string; url: string; sha512: string }>
|
||||
const base = baseImages[arch]
|
||||
if (!base) throw new Error(`missing base image for arch ${arch}`)
|
||||
|
||||
const basePath = path.join(outDir, `base-${arch}.qcow2`)
|
||||
const workPath = path.join(outDir, `work-${version}-${arch}.qcow2`)
|
||||
const buildMarkerPath = path.join(outDir, `build-marker-${arch}.json`)
|
||||
const recipePath = path.join(pkgRoot, 'recipe/browseros-vm.recipe')
|
||||
const rawOut = path.join(outDir, `browseros-vm-${version}-${arch}.qcow2`)
|
||||
const zstOut = `${rawOut}.zst`
|
||||
|
||||
try {
|
||||
await download(base.url, basePath)
|
||||
await verifySha512(basePath, base.sha512)
|
||||
await copyFile(basePath, workPath)
|
||||
await writeFile(
|
||||
buildMarkerPath,
|
||||
`${JSON.stringify({ name: 'browseros-vm', version, arch, phase: 'build' }, null, 2)}\n`,
|
||||
)
|
||||
|
||||
const recipeText = await readFile(recipePath, 'utf8')
|
||||
const args = composeVirtCustomizeArgs({
|
||||
diskPath: workPath,
|
||||
recipeText,
|
||||
recipeDir: path.dirname(recipePath),
|
||||
substitutions: { version, manifest_tmp: buildMarkerPath },
|
||||
})
|
||||
|
||||
await spawnChecked(['virt-customize', ...args])
|
||||
await $`virt-sparsify --in-place ${workPath}`.quiet()
|
||||
await $`qemu-img convert -O qcow2 -c ${workPath} ${rawOut}`.quiet()
|
||||
await $`zstd -19 --long=30 -T0 -f -o ${zstOut} ${rawOut}`.quiet()
|
||||
|
||||
const sha = await sha256File(zstOut)
|
||||
const size = (await stat(zstOut)).size
|
||||
await writeFile(`${zstOut}.sha256`, `${sha} ${path.basename(zstOut)}\n`)
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
key: qcow2Key(version, arch),
|
||||
path: zstOut,
|
||||
sha256: sha,
|
||||
sizeBytes: size,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
await rm(workPath, { force: true })
|
||||
await rm(basePath, { force: true })
|
||||
await rm(rawOut, { force: true })
|
||||
await rm(buildMarkerPath, { force: true })
|
||||
}
|
||||
|
||||
function composeVirtCustomizeArgs(opts: {
|
||||
diskPath: string
|
||||
recipeText: string
|
||||
recipeDir: string
|
||||
substitutions: Record<string, string>
|
||||
}): string[] {
|
||||
const out = ['-a', opts.diskPath, '--network']
|
||||
for (const rawLine of opts.recipeText.split('\n')) {
|
||||
const line = rawLine.trim()
|
||||
if (!line || line.startsWith('#')) continue
|
||||
|
||||
const spaceAt = line.indexOf(' ')
|
||||
if (spaceAt === -1) throw new Error(`invalid recipe line: ${line}`)
|
||||
|
||||
const op = line.slice(0, spaceAt)
|
||||
const rest = subst(line.slice(spaceAt + 1), opts.substitutions)
|
||||
|
||||
if (op === 'run-command') {
|
||||
out.push('--run-command', rest)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op === 'copy-in') {
|
||||
const colonAt = rest.indexOf(':')
|
||||
if (colonAt === -1) throw new Error(`invalid copy-in line: ${line}`)
|
||||
const source = rest.slice(0, colonAt)
|
||||
const target = rest.slice(colonAt + 1)
|
||||
out.push('--copy-in', `${path.resolve(opts.recipeDir, source)}:${target}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op === 'upload') {
|
||||
const colonAt = rest.indexOf(':')
|
||||
if (colonAt === -1) throw new Error(`invalid upload line: ${line}`)
|
||||
const source = rest.slice(0, colonAt)
|
||||
const target = rest.slice(colonAt + 1)
|
||||
out.push('--upload', `${path.resolve(opts.recipeDir, source)}:${target}`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op === 'write') {
|
||||
out.push('--write', rest)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op === 'truncate') {
|
||||
out.push('--truncate', rest)
|
||||
continue
|
||||
}
|
||||
|
||||
throw new Error(`unknown recipe op: ${op}`)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function subst(value: string, vars: Record<string, string>): string {
|
||||
return value.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||
const replacement = vars[key]
|
||||
if (!replacement) throw new Error(`no substitution for {${key}}`)
|
||||
return replacement
|
||||
})
|
||||
}
|
||||
|
||||
async function download(url: string, dest: string): Promise<void> {
|
||||
const response = await fetchWithTimeout(url)
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`download failed: ${url} (${response.status})`)
|
||||
}
|
||||
|
||||
const sink = Bun.file(dest).writer()
|
||||
const reader = response.body.getReader()
|
||||
try {
|
||||
await pumpStream(reader, sink)
|
||||
} finally {
|
||||
await sink.end()
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySha512(filePath: string, expected: string): Promise<void> {
|
||||
const hash = createHash('sha512')
|
||||
for await (const chunk of createReadStream(filePath)) {
|
||||
hash.update(chunk)
|
||||
}
|
||||
const actual = hash.digest('hex')
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`sha512 mismatch for ${filePath}: expected ${expected}, got ${actual}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnChecked(argv: string[]): Promise<void> {
|
||||
const proc = Bun.spawn(argv, {
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
LIBGUESTFS_BACKEND: process.env.LIBGUESTFS_BACKEND ?? 'direct',
|
||||
},
|
||||
})
|
||||
const code = await proc.exited
|
||||
if (code !== 0) throw new Error(`${argv[0]} exited ${code}`)
|
||||
}
|
||||
|
||||
async function pumpStream(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
sink: ChunkSink,
|
||||
): Promise<void> {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
sink.write(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { parseArch, podmanArch } from './common/arch'
|
||||
import { type Bundle, tarballKey } from './common/manifest'
|
||||
import { sha256File } from './common/sha256'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
agent: { type: 'string' },
|
||||
arch: { type: 'string' },
|
||||
'output-dir': { type: 'string', default: './dist/images' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.agent || !values.arch) {
|
||||
console.error(
|
||||
'usage: build:tarball -- --agent <name> --arch <arm64|x64> [--output-dir ./dist/images]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const arch = parseArch(values.arch)
|
||||
const outDir = values['output-dir']
|
||||
await mkdir(outDir, { recursive: true })
|
||||
|
||||
const pkgRoot = path.resolve(import.meta.dir, '..')
|
||||
const bundle = JSON.parse(
|
||||
await readFile(path.join(pkgRoot, 'bundle.json'), 'utf8'),
|
||||
) as Bundle
|
||||
const agent = bundle.agents.find(({ name }) => name === values.agent)
|
||||
if (!agent) throw new Error(`unknown agent: ${values.agent}`)
|
||||
|
||||
const ref = `${agent.image}:${agent.version}`
|
||||
const tarballPath = path.join(
|
||||
outDir,
|
||||
path.basename(tarballKey(agent.name, agent.version, arch)),
|
||||
)
|
||||
const tarPath = tarballPath.slice(0, -'.gz'.length)
|
||||
|
||||
await rm(tarballPath, { force: true })
|
||||
await rm(`${tarballPath}.sha256`, { force: true })
|
||||
await rm(tarPath, { force: true })
|
||||
await spawnChecked([
|
||||
'podman',
|
||||
'pull',
|
||||
'--os',
|
||||
'linux',
|
||||
'--arch',
|
||||
podmanArch(arch),
|
||||
ref,
|
||||
])
|
||||
await spawnChecked([
|
||||
'podman',
|
||||
'save',
|
||||
'--format=oci-archive',
|
||||
'--output',
|
||||
tarPath,
|
||||
ref,
|
||||
])
|
||||
await spawnChecked(['gzip', '-9', '-f', tarPath])
|
||||
|
||||
const sha = await sha256File(tarballPath)
|
||||
const size = (await stat(tarballPath)).size
|
||||
await writeFile(
|
||||
`${tarballPath}.sha256`,
|
||||
`${sha} ${path.basename(tarballPath)}\n`,
|
||||
)
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
key: tarballKey(agent.name, agent.version, arch),
|
||||
path: tarballPath,
|
||||
sha256: sha,
|
||||
sizeBytes: size,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
async function spawnChecked(argv: string[]): Promise<void> {
|
||||
const proc = Bun.spawn(argv, {
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
})
|
||||
const code = await proc.exited
|
||||
if (code !== 0) throw new Error(`${argv[0]} exited ${code}`)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
|
||||
import { homedir, arch as hostArch } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { PATHS } from '@browseros/shared/constants/paths'
|
||||
import { ARCHES, type Arch } from './common/arch'
|
||||
import { fetchWithTimeout } from './common/fetch'
|
||||
import type { Artifact, VmManifest } from './common/manifest'
|
||||
import { verifySha256 } from './common/sha256'
|
||||
|
||||
type ChunkSink = ReturnType<ReturnType<typeof Bun.file>['writer']>
|
||||
|
||||
export interface PlanItem {
|
||||
key: string
|
||||
destPath: string
|
||||
sha256: string
|
||||
}
|
||||
|
||||
export function planSync(opts: {
|
||||
local: VmManifest | null
|
||||
remote: VmManifest
|
||||
cacheRoot: string
|
||||
arches: Arch[]
|
||||
}): PlanItem[] {
|
||||
const out: PlanItem[] = []
|
||||
for (const arch of opts.arches) {
|
||||
maybeAdd(
|
||||
out,
|
||||
opts.remote.vmDisk[arch],
|
||||
opts.local?.vmDisk[arch],
|
||||
opts.cacheRoot,
|
||||
)
|
||||
|
||||
for (const [name, agent] of Object.entries(opts.remote.agents)) {
|
||||
maybeAdd(
|
||||
out,
|
||||
agent.tarballs[arch],
|
||||
opts.local?.agents[name]?.tarballs[arch],
|
||||
opts.cacheRoot,
|
||||
)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function selectSyncArches(
|
||||
allArches: boolean,
|
||||
rawHostArch = hostArch(),
|
||||
): Arch[] {
|
||||
if (allArches) return [...ARCHES]
|
||||
if (rawHostArch === 'arm64') return ['arm64']
|
||||
if (rawHostArch === 'x64' || rawHostArch === 'ia32') return ['x64']
|
||||
throw new Error(`unsupported host arch: ${rawHostArch}`)
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
'manifest-url': { type: 'string' },
|
||||
'all-arches': { type: 'boolean' },
|
||||
'cache-dir': { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
const cdnBase =
|
||||
process.env.R2_PUBLIC_BASE_URL?.trim() ?? 'https://cdn.browseros.com'
|
||||
const manifestUrl = values['manifest-url'] ?? `${cdnBase}/vm/manifest.json`
|
||||
const cacheRoot = values['cache-dir'] ?? getCacheDir()
|
||||
const arches = selectSyncArches(values['all-arches'] ?? false)
|
||||
|
||||
const response = await fetchWithTimeout(manifestUrl)
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`manifest fetch failed: ${manifestUrl} (${response.status})`,
|
||||
)
|
||||
}
|
||||
const remote = (await response.json()) as VmManifest
|
||||
|
||||
const localManifestPath = path.join(cacheRoot, 'vm', 'manifest.json')
|
||||
const local = await readLocalManifest(localManifestPath)
|
||||
const plan = planSync({ local, remote, cacheRoot, arches })
|
||||
|
||||
if (plan.length === 0) {
|
||||
console.log(`cache up to date at vmVersion ${remote.vmVersion}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log(
|
||||
`syncing ${plan.length} artifact(s) for vmVersion ${remote.vmVersion}`,
|
||||
)
|
||||
for (const item of plan) {
|
||||
await mkdir(path.dirname(item.destPath), { recursive: true })
|
||||
const partial = `${item.destPath}.partial`
|
||||
await downloadToFile(`${cdnBase}/${item.key}`, partial)
|
||||
await verifySha256(partial, item.sha256)
|
||||
await rename(partial, item.destPath)
|
||||
console.log(`synced ${item.key}`)
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(localManifestPath), { recursive: true })
|
||||
await writeFile(localManifestPath, `${JSON.stringify(remote, null, 2)}\n`)
|
||||
console.log(`manifest written to ${localManifestPath}`)
|
||||
}
|
||||
|
||||
function maybeAdd(
|
||||
out: PlanItem[],
|
||||
remote: Artifact,
|
||||
local: Artifact | undefined,
|
||||
cacheRoot: string,
|
||||
): void {
|
||||
if (local?.sha256 === remote.sha256) return
|
||||
out.push({
|
||||
key: remote.key,
|
||||
destPath: path.join(cacheRoot, remote.key),
|
||||
sha256: remote.sha256,
|
||||
})
|
||||
}
|
||||
|
||||
function getCacheDir(): string {
|
||||
const dirName =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? PATHS.DEV_BROWSEROS_DIR_NAME
|
||||
: PATHS.BROWSEROS_DIR_NAME
|
||||
return path.join(homedir(), dirName, PATHS.CACHE_DIR_NAME)
|
||||
}
|
||||
|
||||
export async function readLocalManifest(
|
||||
manifestPath: string,
|
||||
): Promise<VmManifest | null> {
|
||||
try {
|
||||
return JSON.parse(await readFile(manifestPath, 'utf8')) as VmManifest
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadToFile(url: string, dest: string): Promise<void> {
|
||||
const response = await fetchWithTimeout(url)
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`download failed: ${url} (${response.status})`)
|
||||
}
|
||||
|
||||
const sink = Bun.file(dest).writer()
|
||||
const reader = response.body.getReader()
|
||||
try {
|
||||
await pumpStream(reader, sink)
|
||||
} finally {
|
||||
await sink.end()
|
||||
}
|
||||
}
|
||||
|
||||
async function pumpStream(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
sink: ChunkSink,
|
||||
): Promise<void> {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
sink.write(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type Arch = 'arm64' | 'x64'
|
||||
|
||||
export const ARCHES: readonly Arch[] = ['arm64', 'x64']
|
||||
|
||||
export function parseArch(raw: string): Arch {
|
||||
if (raw === 'arm64' || raw === 'x64') return raw
|
||||
throw new Error(`unknown arch: ${raw} (expected arm64|x64)`)
|
||||
}
|
||||
|
||||
export function podmanArch(arch: Arch): 'arm64' | 'amd64' {
|
||||
return arch === 'x64' ? 'amd64' : 'arm64'
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export async function fetchWithTimeout(
|
||||
url: string,
|
||||
init: RequestInit = {},
|
||||
timeoutMs = 30_000,
|
||||
): Promise<Response> {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...init,
|
||||
signal: init.signal ?? controller.signal,
|
||||
})
|
||||
} catch (error) {
|
||||
if ((error as { name?: string }).name === 'AbortError') {
|
||||
throw new Error(`fetch timed out after ${timeoutMs}ms: ${url}`)
|
||||
}
|
||||
throw error
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ARCHES, type Arch } from './arch'
|
||||
|
||||
export interface Artifact {
|
||||
key: string
|
||||
sha256: string
|
||||
sizeBytes: number
|
||||
}
|
||||
|
||||
export interface AgentEntry {
|
||||
image: string
|
||||
version: string
|
||||
tarballs: Record<Arch, Artifact>
|
||||
}
|
||||
|
||||
export interface VmManifest {
|
||||
schemaVersion: 1
|
||||
vmVersion: string
|
||||
updatedAt: string
|
||||
vmDisk: Record<Arch, Artifact>
|
||||
agents: Record<string, AgentEntry>
|
||||
}
|
||||
|
||||
export interface BundleAgent {
|
||||
name: string
|
||||
image: string
|
||||
version: string
|
||||
}
|
||||
|
||||
export interface Bundle {
|
||||
vmVersion: string
|
||||
agents: BundleAgent[]
|
||||
}
|
||||
|
||||
export interface ArtifactInput {
|
||||
sha256: string
|
||||
sizeBytes: number
|
||||
}
|
||||
|
||||
export interface ArtifactInputs {
|
||||
vmDisk: Record<Arch, ArtifactInput>
|
||||
agents: Record<string, Record<Arch, ArtifactInput>>
|
||||
}
|
||||
|
||||
export function qcow2Key(vmVersion: string, arch: Arch): string {
|
||||
return `vm/browseros-vm-${vmVersion}-${arch}.qcow2.zst`
|
||||
}
|
||||
|
||||
export function tarballKey(name: string, version: string, arch: Arch): string {
|
||||
return `vm/images/${name}-${version}-${arch}.tar.gz`
|
||||
}
|
||||
|
||||
export function buildManifest(
|
||||
bundle: Bundle,
|
||||
inputs: ArtifactInputs,
|
||||
now: Date = new Date(),
|
||||
): VmManifest {
|
||||
const vmDisk = {} as Record<Arch, Artifact>
|
||||
for (const arch of ARCHES) {
|
||||
const entry = inputs.vmDisk[arch]
|
||||
if (!entry) throw new Error(`missing vmDisk inputs for arch ${arch}`)
|
||||
vmDisk[arch] = {
|
||||
key: qcow2Key(bundle.vmVersion, arch),
|
||||
sha256: entry.sha256,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
}
|
||||
}
|
||||
|
||||
const agents: Record<string, AgentEntry> = {}
|
||||
for (const agent of bundle.agents) {
|
||||
const tarballs = {} as Record<Arch, Artifact>
|
||||
for (const arch of ARCHES) {
|
||||
const entry = inputs.agents[agent.name]?.[arch]
|
||||
if (!entry) {
|
||||
throw new Error(`missing tarball inputs for ${agent.name}/${arch}`)
|
||||
}
|
||||
tarballs[arch] = {
|
||||
key: tarballKey(agent.name, agent.version, arch),
|
||||
sha256: entry.sha256,
|
||||
sizeBytes: entry.sizeBytes,
|
||||
}
|
||||
}
|
||||
agents[agent.name] = {
|
||||
image: agent.image,
|
||||
version: agent.version,
|
||||
tarballs,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
vmVersion: bundle.vmVersion,
|
||||
updatedAt: now.toISOString(),
|
||||
vmDisk,
|
||||
agents,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import {
|
||||
GetObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
|
||||
function required(name: string): string {
|
||||
const value = process.env[name]?.trim()
|
||||
if (!value) throw new Error(`missing env var: ${name}`)
|
||||
return value
|
||||
}
|
||||
|
||||
export function createR2Client(): S3Client {
|
||||
return new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${required('R2_ACCOUNT_ID')}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: required('R2_ACCESS_KEY_ID'),
|
||||
secretAccessKey: required('R2_SECRET_ACCESS_KEY'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getBucket(): string {
|
||||
return required('R2_BUCKET')
|
||||
}
|
||||
|
||||
export function getCdnBase(): string {
|
||||
return process.env.R2_PUBLIC_BASE_URL?.trim() ?? 'https://cdn.browseros.com'
|
||||
}
|
||||
|
||||
export async function putFile(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
filePath: string,
|
||||
contentType: string,
|
||||
): Promise<void> {
|
||||
const { size } = await stat(filePath)
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: createReadStream(filePath),
|
||||
ContentLength: size,
|
||||
ContentType: contentType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function putBody(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
body: string,
|
||||
contentType: string,
|
||||
): Promise<void> {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentLength: Buffer.byteLength(body),
|
||||
ContentType: contentType,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getBody(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const response = await client.send(
|
||||
new GetObjectCommand({ Bucket: bucket, Key: key }),
|
||||
)
|
||||
const body = response.Body as
|
||||
| { transformToByteArray(): Promise<Uint8Array> }
|
||||
| undefined
|
||||
if (!body) throw new Error(`missing response body for R2 key: ${key}`)
|
||||
const bytes = await body.transformToByteArray()
|
||||
return new TextDecoder().decode(bytes)
|
||||
} catch (error) {
|
||||
const cause = error as {
|
||||
name?: string
|
||||
$metadata?: { httpStatusCode?: number }
|
||||
}
|
||||
if (cause.name === 'NoSuchKey' || cause.$metadata?.httpStatusCode === 404) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
|
||||
export async function sha256File(path: string): Promise<string> {
|
||||
const hash = createHash('sha256')
|
||||
for await (const chunk of createReadStream(path)) {
|
||||
hash.update(chunk)
|
||||
}
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
export async function verifySha256(
|
||||
path: string,
|
||||
expected: string,
|
||||
): Promise<void> {
|
||||
const actual = await sha256File(path)
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`sha256 mismatch for ${path}: expected ${expected}, got ${actual}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { createR2Client, getBody, getBucket } from './common/r2'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
key: { type: 'string' },
|
||||
out: { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.key || !values.out) {
|
||||
console.error('usage: download -- --key <r2-key> --out <path>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const body = await getBody(createR2Client(), getBucket(), values.key)
|
||||
if (body === null) {
|
||||
throw new Error(
|
||||
`R2 key not found: ${values.key}. Publish a full manifest before publishing slices.`,
|
||||
)
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(values.out), { recursive: true })
|
||||
await writeFile(values.out, body)
|
||||
console.log(`downloaded ${values.key} to ${values.out}`)
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { ARCHES, type Arch } from './common/arch'
|
||||
import { fetchWithTimeout } from './common/fetch'
|
||||
import {
|
||||
type AgentEntry,
|
||||
type Artifact,
|
||||
type ArtifactInputs,
|
||||
type Bundle,
|
||||
type BundleAgent,
|
||||
buildManifest,
|
||||
qcow2Key,
|
||||
tarballKey,
|
||||
type VmManifest,
|
||||
} from './common/manifest'
|
||||
import { sha256File } from './common/sha256'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
'dist-dir': { type: 'string', default: './dist' },
|
||||
out: { type: 'string' },
|
||||
slice: { type: 'string', default: 'full' },
|
||||
'merge-from': { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
const distDir = values['dist-dir']
|
||||
const slice = values.slice
|
||||
const pkgRoot = path.resolve(import.meta.dir, '..')
|
||||
const bundle = JSON.parse(
|
||||
await readFile(path.join(pkgRoot, 'bundle.json'), 'utf8'),
|
||||
) as Bundle
|
||||
|
||||
const baseline = values['merge-from']
|
||||
? await loadBaseline(values['merge-from'])
|
||||
: null
|
||||
if (slice !== 'full' && !baseline) {
|
||||
throw new Error(`--slice ${slice} requires --merge-from`)
|
||||
}
|
||||
|
||||
const manifest = await buildSlicedManifest({ bundle, distDir, slice, baseline })
|
||||
const outPath = values.out ?? path.join(distDir, 'manifest.json')
|
||||
await mkdir(path.dirname(outPath), { recursive: true })
|
||||
await writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(`wrote ${outPath} (slice=${slice})`)
|
||||
|
||||
async function buildSlicedManifest(opts: {
|
||||
bundle: Bundle
|
||||
distDir: string
|
||||
slice: string
|
||||
baseline: VmManifest | null
|
||||
}): Promise<VmManifest> {
|
||||
if (opts.slice === 'full') {
|
||||
return buildManifest(
|
||||
opts.bundle,
|
||||
await readAllInputs(opts.bundle, opts.distDir),
|
||||
)
|
||||
}
|
||||
|
||||
const baseline = opts.baseline
|
||||
if (!baseline) throw new Error(`--slice ${opts.slice} requires --merge-from`)
|
||||
const updatedAt = new Date().toISOString()
|
||||
|
||||
if (opts.slice === 'vm') {
|
||||
return {
|
||||
...baseline,
|
||||
schemaVersion: 1,
|
||||
vmVersion: opts.bundle.vmVersion,
|
||||
updatedAt,
|
||||
vmDisk: await readVmDisk(opts.bundle.vmVersion, opts.distDir),
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.slice.startsWith('agents:')) {
|
||||
const name = opts.slice.slice('agents:'.length)
|
||||
const agent = opts.bundle.agents.find((entry) => entry.name === name)
|
||||
if (!agent) throw new Error(`unknown agent: ${name}`)
|
||||
|
||||
return {
|
||||
...baseline,
|
||||
updatedAt,
|
||||
agents: {
|
||||
...baseline.agents,
|
||||
[name]: await readAgentEntry(agent, opts.distDir),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`unknown slice: ${opts.slice}`)
|
||||
}
|
||||
|
||||
async function readAllInputs(
|
||||
bundle: Bundle,
|
||||
distDir: string,
|
||||
): Promise<ArtifactInputs> {
|
||||
const agents: ArtifactInputs['agents'] = {}
|
||||
for (const agent of bundle.agents) {
|
||||
agents[agent.name] = {} as ArtifactInputs['agents'][string]
|
||||
for (const arch of ARCHES) {
|
||||
const artifactPath = path.join(
|
||||
distDir,
|
||||
'images',
|
||||
path.basename(tarballKey(agent.name, agent.version, arch)),
|
||||
)
|
||||
agents[agent.name][arch] = await readArtifactInput(artifactPath)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
vmDisk: await readArtifactInputs((arch) =>
|
||||
path.join(distDir, path.basename(qcow2Key(bundle.vmVersion, arch))),
|
||||
),
|
||||
agents,
|
||||
}
|
||||
}
|
||||
|
||||
async function readVmDisk(
|
||||
vmVersion: string,
|
||||
distDir: string,
|
||||
): Promise<Record<Arch, Artifact>> {
|
||||
const vmDisk = {} as Record<Arch, Artifact>
|
||||
for (const arch of ARCHES) {
|
||||
const key = qcow2Key(vmVersion, arch)
|
||||
const artifactPath = path.join(distDir, path.basename(key))
|
||||
vmDisk[arch] = { key, ...(await readArtifactInput(artifactPath)) }
|
||||
}
|
||||
return vmDisk
|
||||
}
|
||||
|
||||
async function readAgentEntry(
|
||||
agent: BundleAgent,
|
||||
distDir: string,
|
||||
): Promise<AgentEntry> {
|
||||
const tarballs = {} as AgentEntry['tarballs']
|
||||
for (const arch of ARCHES) {
|
||||
const key = tarballKey(agent.name, agent.version, arch)
|
||||
const artifactPath = path.join(distDir, 'images', path.basename(key))
|
||||
tarballs[arch] = { key, ...(await readArtifactInput(artifactPath)) }
|
||||
}
|
||||
return { image: agent.image, version: agent.version, tarballs }
|
||||
}
|
||||
|
||||
async function readArtifactInputs(
|
||||
pathForArch: (arch: Arch) => string,
|
||||
): Promise<Record<Arch, { sha256: string; sizeBytes: number }>> {
|
||||
const out = {} as Record<Arch, { sha256: string; sizeBytes: number }>
|
||||
for (const arch of ARCHES) {
|
||||
out[arch] = await readArtifactInput(pathForArch(arch))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function readArtifactInput(
|
||||
filePath: string,
|
||||
): Promise<{ sha256: string; sizeBytes: number }> {
|
||||
return {
|
||||
sha256: await sha256File(filePath),
|
||||
sizeBytes: (await stat(filePath)).size,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBaseline(src: string): Promise<VmManifest> {
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
const response = await fetchWithTimeout(src)
|
||||
if (!response.ok) {
|
||||
throw new Error(`baseline fetch failed: ${src} (${response.status})`)
|
||||
}
|
||||
return (await response.json()) as VmManifest
|
||||
}
|
||||
|
||||
return JSON.parse(await readFile(src, 'utf8')) as VmManifest
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bun
|
||||
import { createReadStream, createWriteStream } from 'node:fs'
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { createGunzip } from 'node:zlib'
|
||||
import { parseArch, podmanArch } from './common/arch'
|
||||
import type { Bundle } from './common/manifest'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
agent: { type: 'string' },
|
||||
arch: { type: 'string' },
|
||||
tarball: { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.agent || !values.arch || !values.tarball) {
|
||||
console.error(
|
||||
'usage: smoke:tarball -- --agent <name> --arch <arm64|x64> --tarball <path.tar.gz>',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const arch = parseArch(values.arch)
|
||||
const pkgRoot = path.resolve(import.meta.dir, '..')
|
||||
const bundle = JSON.parse(
|
||||
await readFile(path.join(pkgRoot, 'bundle.json'), 'utf8'),
|
||||
) as Bundle
|
||||
const agent = bundle.agents.find(({ name }) => name === values.agent)
|
||||
if (!agent) throw new Error(`unknown agent: ${values.agent}`)
|
||||
|
||||
const ref = `${agent.image}:${agent.version}`
|
||||
const tarball = await maybeDecompress(values.tarball)
|
||||
|
||||
try {
|
||||
await spawnChecked(['podman', 'rmi', '-f', ref]).catch(() => {})
|
||||
await spawnChecked(['podman', 'load', '--input', tarball.path])
|
||||
const inspected = await inspectImage(ref)
|
||||
if (inspected.Os !== 'linux') {
|
||||
throw new Error(`expected linux image, got ${inspected.Os ?? '<missing>'}`)
|
||||
}
|
||||
if (inspected.Architecture !== podmanArch(arch)) {
|
||||
throw new Error(
|
||||
`expected ${podmanArch(arch)} image, got ${inspected.Architecture ?? '<missing>'}`,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
await spawnChecked(['podman', 'rmi', '-f', ref]).catch(() => {})
|
||||
if (tarball.cleanupDir) {
|
||||
await rm(tarball.cleanupDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
console.log('tarball smoke test passed')
|
||||
|
||||
async function maybeDecompress(
|
||||
tarballPath: string,
|
||||
): Promise<{ path: string; cleanupDir?: string }> {
|
||||
if (!tarballPath.endsWith('.gz')) return { path: tarballPath }
|
||||
|
||||
const cleanupDir = await mkdtemp(path.join(tmpdir(), 'browseros-tar-smoke-'))
|
||||
const tarPath = path.join(cleanupDir, 'image.tar')
|
||||
await pipeline(
|
||||
createReadStream(tarballPath),
|
||||
createGunzip(),
|
||||
createWriteStream(tarPath),
|
||||
)
|
||||
return { path: tarPath, cleanupDir }
|
||||
}
|
||||
|
||||
async function inspectImage(ref: string): Promise<{
|
||||
Architecture?: string
|
||||
Os?: string
|
||||
}> {
|
||||
const stdout = await spawnCapture([
|
||||
'podman',
|
||||
'inspect',
|
||||
'--type',
|
||||
'image',
|
||||
'--format',
|
||||
'{{json .}}',
|
||||
ref,
|
||||
])
|
||||
return JSON.parse(stdout) as { Architecture?: string; Os?: string }
|
||||
}
|
||||
|
||||
async function spawnCapture(argv: string[]): Promise<string> {
|
||||
const proc = Bun.spawn(argv, { stdout: 'pipe', stderr: 'pipe' })
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
if (code !== 0) {
|
||||
throw new Error(
|
||||
`${argv[0]} exited ${code}\n${stderr.trim() || stdout.trim()}`,
|
||||
)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
async function spawnChecked(argv: string[]): Promise<void> {
|
||||
await spawnCapture(argv)
|
||||
}
|
||||
@@ -1,37 +1,51 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { $ } from 'bun'
|
||||
import { type Arch, parseArch } from './common/arch'
|
||||
|
||||
const INSTANCE_NAME = 'browseros-vm-smoke'
|
||||
const SOCKET_POLL_INTERVAL_MS = 2000
|
||||
const SOCKET_POLL_TIMEOUT_MS = 120_000
|
||||
|
||||
// Bun's fetch accepts a `unix` socket key at runtime, but the web-standard
|
||||
// `RequestInit` type doesn't include it. Declare the Bun-specific shape
|
||||
// locally so callers don't need to cast inline.
|
||||
type BunRequestInit = RequestInit & { unix?: string }
|
||||
|
||||
export interface BootProbeOptions {
|
||||
limactlPath?: string
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
qcow: { type: 'string' },
|
||||
arch: { type: 'string', default: 'x64' },
|
||||
limactl: { type: 'string', default: 'limactl' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.qcow) {
|
||||
console.error(
|
||||
'usage: smoke:vm -- --qcow <path.qcow2.zst> [--arch arm64|x64] [--limactl limactl]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
export async function bootAndProbe(
|
||||
const arch = parseArch(values.arch ?? 'x64')
|
||||
|
||||
await bootAndProbe(values.qcow, arch, values.limactl ?? 'limactl')
|
||||
console.log('vm smoke test passed')
|
||||
|
||||
async function bootAndProbe(
|
||||
qcowZstPath: string,
|
||||
opts: BootProbeOptions = {},
|
||||
arch: Arch,
|
||||
limactl: string,
|
||||
): Promise<void> {
|
||||
const limactl = (opts.limactlPath ?? 'limactl').trim()
|
||||
if (!limactl) {
|
||||
throw new Error('bootAndProbe: limactlPath cannot be an empty string')
|
||||
}
|
||||
const workDir = await mkdtemp(path.join(tmpdir(), 'vm-smoke-'))
|
||||
const workDir = await mkdtemp(path.join(tmpdir(), 'browseros-vm-smoke-'))
|
||||
const qcowPath = path.join(workDir, 'disk.qcow2')
|
||||
const configPath = path.join(workDir, 'lima.yaml')
|
||||
const sockPath = path.join(workDir, 'podman.sock')
|
||||
|
||||
try {
|
||||
await $`zstd -d -f -o ${qcowPath} ${qcowZstPath}`.quiet()
|
||||
await writeFile(configPath, composeLimaConfig(qcowPath, sockPath))
|
||||
await writeFile(configPath, composeLimaConfig(qcowPath, arch, sockPath))
|
||||
await $`${limactl} start --name=${INSTANCE_NAME} --tty=false ${configPath}`
|
||||
await waitForSocket(sockPath)
|
||||
await probePodmanSocket(sockPath)
|
||||
@@ -42,12 +56,15 @@ export async function bootAndProbe(
|
||||
}
|
||||
}
|
||||
|
||||
export function composeLimaConfig(qcowPath: string, sockPath: string): string {
|
||||
return `# Generated by @browseros/vm-container smoke test
|
||||
vmType: qemu
|
||||
function composeLimaConfig(
|
||||
qcowPath: string,
|
||||
arch: Arch,
|
||||
sockPath: string,
|
||||
): string {
|
||||
return `vmType: qemu
|
||||
images:
|
||||
- location: ${qcowPath}
|
||||
arch: x86_64
|
||||
arch: ${limaArch(arch)}
|
||||
containerd:
|
||||
system: false
|
||||
user: false
|
||||
@@ -60,11 +77,14 @@ portForwards:
|
||||
`
|
||||
}
|
||||
|
||||
function limaArch(arch: Arch): 'aarch64' | 'x86_64' {
|
||||
return arch === 'arm64' ? 'aarch64' : 'x86_64'
|
||||
}
|
||||
|
||||
async function waitForSocket(sockPath: string): Promise<void> {
|
||||
const deadline = Date.now() + SOCKET_POLL_TIMEOUT_MS
|
||||
while (Date.now() < deadline) {
|
||||
const file = Bun.file(sockPath)
|
||||
if (await file.exists()) return
|
||||
if (await Bun.file(sockPath).exists()) return
|
||||
await Bun.sleep(SOCKET_POLL_INTERVAL_MS)
|
||||
}
|
||||
throw new Error(
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bun
|
||||
import { parseArgs } from 'node:util'
|
||||
import { createR2Client, getBucket, putBody, putFile } from './common/r2'
|
||||
import { sha256File } from './common/sha256'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
file: { type: 'string' },
|
||||
key: { type: 'string' },
|
||||
'content-type': { type: 'string' },
|
||||
'sidecar-sha': { type: 'boolean' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.file || !values.key) {
|
||||
throw new Error('--file and --key required')
|
||||
}
|
||||
|
||||
const contentType = values['content-type'] ?? 'application/octet-stream'
|
||||
const client = createR2Client()
|
||||
const bucket = getBucket()
|
||||
|
||||
try {
|
||||
await putFile(client, bucket, values.key, values.file, contentType)
|
||||
console.log(`uploaded ${values.file} to ${bucket}/${values.key}`)
|
||||
|
||||
if (values['sidecar-sha']) {
|
||||
const sha = await sha256File(values.file)
|
||||
const filename = values.file.split('/').pop() ?? values.file
|
||||
await putBody(
|
||||
client,
|
||||
bucket,
|
||||
`${values.key}.sha256`,
|
||||
`${sha} ${filename}\n`,
|
||||
'text/plain; charset=utf-8',
|
||||
)
|
||||
console.log(`uploaded sha256 to ${bucket}/${values.key}.sha256`)
|
||||
}
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { parseArch, podmanArch } from '../scripts/common/arch'
|
||||
|
||||
describe('arch helpers', () => {
|
||||
it('normalizes BrowserOS arches for podman', () => {
|
||||
expect(podmanArch('arm64')).toBe('arm64')
|
||||
expect(podmanArch('x64')).toBe('amd64')
|
||||
})
|
||||
|
||||
it('parses supported release arches', () => {
|
||||
expect(parseArch('arm64')).toBe('arm64')
|
||||
expect(parseArch('x64')).toBe('x64')
|
||||
})
|
||||
|
||||
it('rejects unsupported release arches', () => {
|
||||
expect(() => parseArch('amd64')).toThrow('unknown arch: amd64')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,282 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
type PlanItem,
|
||||
planSync,
|
||||
readLocalManifest,
|
||||
selectSyncArches,
|
||||
} from '../scripts/cache-sync'
|
||||
import type { VmManifest } from '../scripts/common/manifest'
|
||||
import { sha256File } from '../scripts/common/sha256'
|
||||
|
||||
const openclaw = {
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
}
|
||||
|
||||
function manifest(
|
||||
vmVersion: string,
|
||||
diskSha: string,
|
||||
tarSha: string,
|
||||
): VmManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
vmVersion,
|
||||
updatedAt: '2026-04-22T00:00:00.000Z',
|
||||
vmDisk: {
|
||||
arm64: {
|
||||
key: `vm/browseros-vm-${vmVersion}-arm64.qcow2.zst`,
|
||||
sha256: `${diskSha}-arm64`,
|
||||
sizeBytes: 101,
|
||||
},
|
||||
x64: {
|
||||
key: `vm/browseros-vm-${vmVersion}-x64.qcow2.zst`,
|
||||
sha256: `${diskSha}-x64`,
|
||||
sizeBytes: 102,
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
openclaw: {
|
||||
...openclaw,
|
||||
tarballs: {
|
||||
arm64: {
|
||||
key: 'vm/images/openclaw-2026.4.12-arm64.tar.gz',
|
||||
sha256: `${tarSha}-arm64`,
|
||||
sizeBytes: 201,
|
||||
},
|
||||
x64: {
|
||||
key: 'vm/images/openclaw-2026.4.12-x64.tar.gz',
|
||||
sha256: `${tarSha}-x64`,
|
||||
sizeBytes: 202,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function keys(plan: PlanItem[]): string[] {
|
||||
return plan.map((item) => item.key)
|
||||
}
|
||||
|
||||
describe('planSync', () => {
|
||||
it('downloads every selected-arch artifact for a fresh cache', () => {
|
||||
const remote = manifest('2026.04.22', 'd1', 't1')
|
||||
|
||||
expect(
|
||||
keys(planSync({ local: null, remote, cacheRoot: '/c', arches: ['x64'] })),
|
||||
).toEqual([
|
||||
'vm/browseros-vm-2026.04.22-x64.qcow2.zst',
|
||||
'vm/images/openclaw-2026.4.12-x64.tar.gz',
|
||||
])
|
||||
})
|
||||
|
||||
it('does nothing when the local manifest matches the remote manifest', () => {
|
||||
const remote = manifest('2026.04.22', 'd1', 't1')
|
||||
|
||||
expect(
|
||||
planSync({ local: remote, remote, cacheRoot: '/c', arches: ['x64'] }),
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('downloads only artifacts whose sha256 changed', () => {
|
||||
const local = manifest('2026.04.20', 'd-old', 't1')
|
||||
const remote = manifest('2026.04.22', 'd-new', 't1')
|
||||
|
||||
expect(
|
||||
keys(planSync({ local, remote, cacheRoot: '/c', arches: ['x64'] })),
|
||||
).toEqual(['vm/browseros-vm-2026.04.22-x64.qcow2.zst'])
|
||||
})
|
||||
|
||||
it('supports syncing all release arches', () => {
|
||||
const remote = manifest('2026.04.22', 'd1', 't1')
|
||||
|
||||
expect(
|
||||
planSync({
|
||||
local: null,
|
||||
remote,
|
||||
cacheRoot: '/c',
|
||||
arches: ['arm64', 'x64'],
|
||||
}),
|
||||
).toHaveLength(4)
|
||||
})
|
||||
|
||||
it('selects host arch by default and both arches when requested', () => {
|
||||
expect(selectSyncArches(false, 'x64')).toEqual(['x64'])
|
||||
expect(selectSyncArches(true, 'x64')).toEqual(['arm64', 'x64'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('readLocalManifest', () => {
|
||||
let dir: string | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
if (!dir) return
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
dir = null
|
||||
})
|
||||
|
||||
it('returns null only when the local manifest is absent', async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'browseros-cache-manifest-'))
|
||||
|
||||
await expect(
|
||||
readLocalManifest(path.join(dir, 'missing.json')),
|
||||
).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('surfaces corrupt local manifest files', async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'browseros-cache-manifest-'))
|
||||
const manifestPath = path.join(dir, 'manifest.json')
|
||||
await writeFile(manifestPath, '{not json')
|
||||
|
||||
await expect(readLocalManifest(manifestPath)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('emit-manifest', () => {
|
||||
let dir: string | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
if (!dir) return
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
dir = null
|
||||
})
|
||||
|
||||
it('merges a vm slice while preserving agents from the baseline', async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'browseros-emit-vm-'))
|
||||
const distDir = path.join(dir, 'dist')
|
||||
await writeVmFiles(distDir)
|
||||
|
||||
const baseline = manifest('2026.04.20', 'old-disk', 'old-tar')
|
||||
const baselinePath = path.join(dir, 'baseline.json')
|
||||
const outPath = path.join(dir, 'manifest.json')
|
||||
await writeJson(baselinePath, baseline)
|
||||
|
||||
await runEmitManifest([
|
||||
'--slice',
|
||||
'vm',
|
||||
'--dist-dir',
|
||||
distDir,
|
||||
'--merge-from',
|
||||
baselinePath,
|
||||
'--out',
|
||||
outPath,
|
||||
])
|
||||
|
||||
const merged = JSON.parse(await readFile(outPath, 'utf8')) as VmManifest
|
||||
expect(merged.vmVersion).toBe('2026.04.22')
|
||||
expect(merged.agents).toEqual(baseline.agents)
|
||||
expect(merged.vmDisk.x64.sha256).toBe(
|
||||
await sha256File(
|
||||
path.join(distDir, 'browseros-vm-2026.04.22-x64.qcow2.zst'),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('merges an agent slice while preserving vmDisk from the baseline', async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'browseros-emit-agent-'))
|
||||
const distDir = path.join(dir, 'dist')
|
||||
await writeAgentFiles(distDir)
|
||||
|
||||
const baseline = manifest('2026.04.20', 'old-disk', 'old-tar')
|
||||
const baselinePath = path.join(dir, 'baseline.json')
|
||||
const outPath = path.join(dir, 'manifest.json')
|
||||
await writeJson(baselinePath, baseline)
|
||||
|
||||
await runEmitManifest([
|
||||
'--slice',
|
||||
'agents:openclaw',
|
||||
'--dist-dir',
|
||||
distDir,
|
||||
'--merge-from',
|
||||
baselinePath,
|
||||
'--out',
|
||||
outPath,
|
||||
])
|
||||
|
||||
const merged = JSON.parse(await readFile(outPath, 'utf8')) as VmManifest
|
||||
expect(merged.vmVersion).toBe('2026.04.20')
|
||||
expect(merged.vmDisk).toEqual(baseline.vmDisk)
|
||||
expect(merged.agents.openclaw.tarballs.arm64.sha256).toBe(
|
||||
await sha256File(
|
||||
path.join(distDir, 'images/openclaw-2026.4.12-arm64.tar.gz'),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
it('fails slice emission without a merge baseline', async () => {
|
||||
dir = await mkdtemp(path.join(tmpdir(), 'browseros-emit-fail-'))
|
||||
const distDir = path.join(dir, 'dist')
|
||||
await writeVmFiles(distDir)
|
||||
|
||||
const result = await runEmitManifest(
|
||||
[
|
||||
'--slice',
|
||||
'vm',
|
||||
'--dist-dir',
|
||||
distDir,
|
||||
'--out',
|
||||
path.join(dir, 'out.json'),
|
||||
],
|
||||
false,
|
||||
)
|
||||
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.stderr).toContain('--slice vm requires --merge-from')
|
||||
})
|
||||
})
|
||||
|
||||
async function writeVmFiles(distDir: string): Promise<void> {
|
||||
await mkdir(distDir, { recursive: true })
|
||||
await writeFile(
|
||||
path.join(distDir, 'browseros-vm-2026.04.22-arm64.qcow2.zst'),
|
||||
'arm disk',
|
||||
)
|
||||
await writeFile(
|
||||
path.join(distDir, 'browseros-vm-2026.04.22-x64.qcow2.zst'),
|
||||
'x64 disk',
|
||||
)
|
||||
}
|
||||
|
||||
async function writeAgentFiles(distDir: string): Promise<void> {
|
||||
await mkdir(path.join(distDir, 'images'), { recursive: true })
|
||||
await writeFile(
|
||||
path.join(distDir, 'images/openclaw-2026.4.12-arm64.tar.gz'),
|
||||
'arm tarball',
|
||||
)
|
||||
await writeFile(
|
||||
path.join(distDir, 'images/openclaw-2026.4.12-x64.tar.gz'),
|
||||
'x64 tarball',
|
||||
)
|
||||
}
|
||||
|
||||
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
||||
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`)
|
||||
}
|
||||
|
||||
async function runEmitManifest(
|
||||
args: string[],
|
||||
expectSuccess = true,
|
||||
): Promise<{ code: number; stdout: string; stderr: string }> {
|
||||
const proc = Bun.spawn(
|
||||
['bun', 'run', 'scripts/emit-manifest.ts', '--', ...args],
|
||||
{
|
||||
cwd: path.join(import.meta.dir, '..'),
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
},
|
||||
)
|
||||
const [stdout, stderr, code] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
])
|
||||
|
||||
if (expectSuccess && code !== 0) {
|
||||
throw new Error(`emit-manifest failed: ${stderr || stdout}`)
|
||||
}
|
||||
|
||||
return { code, stdout, stderr }
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
type ArtifactInputs,
|
||||
type Bundle,
|
||||
buildManifest,
|
||||
qcow2Key,
|
||||
tarballKey,
|
||||
} from '../scripts/common/manifest'
|
||||
import { verifySha256 } from '../scripts/common/sha256'
|
||||
|
||||
const bundle: Bundle = {
|
||||
vmVersion: '2026.04.22',
|
||||
agents: [
|
||||
{
|
||||
name: 'openclaw',
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const inputs: ArtifactInputs = {
|
||||
vmDisk: {
|
||||
arm64: { sha256: 'disk-arm', sizeBytes: 11 },
|
||||
x64: { sha256: 'disk-x64', sizeBytes: 12 },
|
||||
},
|
||||
agents: {
|
||||
openclaw: {
|
||||
arm64: { sha256: 'tar-arm', sizeBytes: 21 },
|
||||
x64: { sha256: 'tar-x64', sizeBytes: 22 },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('manifest helpers', () => {
|
||||
it('builds release artifact keys', () => {
|
||||
expect(qcow2Key('2026.04.22', 'arm64')).toBe(
|
||||
'vm/browseros-vm-2026.04.22-arm64.qcow2.zst',
|
||||
)
|
||||
expect(tarballKey('openclaw', '2026.4.12', 'x64')).toBe(
|
||||
'vm/images/openclaw-2026.4.12-x64.tar.gz',
|
||||
)
|
||||
})
|
||||
|
||||
it('builds a manifest from bundle metadata and artifact inputs', () => {
|
||||
const manifest = buildManifest(
|
||||
bundle,
|
||||
inputs,
|
||||
new Date('2026-04-22T00:00:00.000Z'),
|
||||
)
|
||||
|
||||
expect(manifest).toMatchObject({
|
||||
schemaVersion: 1,
|
||||
vmVersion: '2026.04.22',
|
||||
updatedAt: '2026-04-22T00:00:00.000Z',
|
||||
vmDisk: {
|
||||
arm64: {
|
||||
key: 'vm/browseros-vm-2026.04.22-arm64.qcow2.zst',
|
||||
sha256: 'disk-arm',
|
||||
sizeBytes: 11,
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
openclaw: {
|
||||
image: 'ghcr.io/openclaw/openclaw',
|
||||
version: '2026.4.12',
|
||||
tarballs: {
|
||||
x64: {
|
||||
key: 'vm/images/openclaw-2026.4.12-x64.tar.gz',
|
||||
sha256: 'tar-x64',
|
||||
sizeBytes: 22,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('fails when required artifact inputs are missing', () => {
|
||||
expect(() =>
|
||||
buildManifest(bundle, {
|
||||
vmDisk: { arm64: inputs.vmDisk.arm64 } as ArtifactInputs['vmDisk'],
|
||||
agents: inputs.agents,
|
||||
}),
|
||||
).toThrow('missing vmDisk inputs for arch x64')
|
||||
|
||||
expect(() =>
|
||||
buildManifest(bundle, {
|
||||
vmDisk: inputs.vmDisk,
|
||||
agents: { openclaw: { arm64: inputs.agents.openclaw.arm64 } },
|
||||
} as unknown as ArtifactInputs),
|
||||
).toThrow('missing tarball inputs for openclaw/x64')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sha256 helpers', () => {
|
||||
let dir: string | null = null
|
||||
|
||||
afterEach(async () => {
|
||||
if (!dir) return
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
dir = null
|
||||
})
|
||||
|
||||
it('verifies matching file content and rejects mismatches', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'browseros-build-tools-'))
|
||||
const filePath = join(dir, 'artifact.txt')
|
||||
await writeFile(filePath, 'browseros\n')
|
||||
|
||||
await expect(
|
||||
verifySha256(
|
||||
filePath,
|
||||
'8e4e07174da39a48ab7aa9a1bebd3adcddff43172c0b19fcbe921cc47c599f62',
|
||||
),
|
||||
).resolves.toBeUndefined()
|
||||
await expect(verifySha256(filePath, 'bad')).rejects.toThrow(
|
||||
'sha256 mismatch',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["scripts/**/*", "tests/**/*", "package.json", "bundle.json"]
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
export const PATHS = {
|
||||
DEFAULT_EXECUTION_DIR: process.cwd(),
|
||||
BROWSEROS_DIR_NAME: '.browseros',
|
||||
DEV_BROWSEROS_DIR_NAME: '.browseros-dev',
|
||||
CACHE_DIR_NAME: 'cache',
|
||||
MEMORY_DIR_NAME: 'memory',
|
||||
SESSIONS_DIR_NAME: 'sessions',
|
||||
TOOL_OUTPUT_DIR_NAME: 'tool-output',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Cloudflare R2 — required for `bun run upload`.
|
||||
# In CI these come from GHA repo secrets; locally put them in `.env` (gitignored)
|
||||
# or symlink to another `.env` that already has them:
|
||||
# ln -s ../../../browseros/.env .env
|
||||
# ln -s ../../apps/server/.env.production .env
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=browseros
|
||||
|
||||
# Optional override — only used if you serve artifacts from a different CDN.
|
||||
# CDN_BASE_URL=https://cdn.browseros.com
|
||||
@@ -1,131 +0,0 @@
|
||||
# @browseros/vm-container
|
||||
|
||||
Produces the Debian + Podman `qcow2` disk image shipped inside BrowserOS. This
|
||||
is the WS1 deliverable from `specs/bundled-vm-runtime-spec.md`.
|
||||
|
||||
The package owns three surfaces:
|
||||
|
||||
- **Build** (`bun run build`) — runs `virt-customize` against a pinned Debian
|
||||
genericcloud image and emits a `.qcow2.zst` plus a `build-result-<arch>.json`
|
||||
metadata file.
|
||||
- **Upload** (`bun run upload`) — publishes pre-built artifacts to R2 under
|
||||
`vm/<version>/` with a per-version `manifest.json` and a top-level
|
||||
`latest.json` pointer.
|
||||
- **Smoke** (`bun run smoke`) — boots a built `.qcow2.zst` in Lima + QEMU and
|
||||
pings `podman.socket`.
|
||||
|
||||
Types under `@browseros/vm-container/schema` are the consumer contract. WS4's
|
||||
runtime code imports `VmManifest`, `parseManifest`, and the R2 key helpers so
|
||||
producer and consumer can't drift.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Linux host** (libguestfs / `virt-customize` is Linux-only).
|
||||
- `libguestfs-tools`, `qemu-utils`, `zstd` on `PATH` (`sudo apt-get install …`
|
||||
on Debian/Ubuntu).
|
||||
- Bun `^1.3.6`. Run from the monorepo root once with `bun install`.
|
||||
|
||||
On macOS, the build step does not run locally — use the GitHub Actions
|
||||
workflow (`build-vm-container.yml`) or a Linux VM.
|
||||
|
||||
## Environment
|
||||
|
||||
The upload step needs Cloudflare R2 credentials. Locally put them in `.env`
|
||||
inside this directory (`.env` is gitignored by the root rule). For CI, the
|
||||
workflow sets them from repo secrets — no `.env` file needed.
|
||||
|
||||
| Variable | Required | Default | Notes |
|
||||
| ----------------------- | -------- | --------------------------- | ------------------------------------------ |
|
||||
| `R2_ACCOUNT_ID` | upload | — | |
|
||||
| `R2_ACCESS_KEY_ID` | upload | — | |
|
||||
| `R2_SECRET_ACCESS_KEY` | upload | — | |
|
||||
| `R2_BUCKET` | upload | — | e.g. `browseros` |
|
||||
| `CDN_BASE_URL` | optional | `https://cdn.browseros.com` | Only affects URLs written into `manifest.json`. An explicit `cdnBaseUrl` option passed to `publishDisks()` overrides this env var. |
|
||||
|
||||
If you already have R2 credentials in another repo `.env`, symlink it to avoid
|
||||
duplicating secrets:
|
||||
|
||||
```bash
|
||||
cd packages/browseros-agent/packages/vm-container
|
||||
ln -s ../../../browseros/.env .env # Chromium build's .env
|
||||
# or
|
||||
ln -s ../../apps/server/.env.production .env
|
||||
```
|
||||
|
||||
## Local build (Linux host)
|
||||
|
||||
```bash
|
||||
cd packages/browseros-agent/packages/vm-container
|
||||
|
||||
# Build one arch. virt-customize inherits LIBGUESTFS_BACKEND from the shell.
|
||||
LIBGUESTFS_BACKEND=direct bun run build -- \
|
||||
--version 2026.04.22-1 \
|
||||
--arch x64 \
|
||||
--output-dir ./dist/x64
|
||||
|
||||
# Smoke test the result (limactl on PATH required).
|
||||
bun run smoke -- --qcow ./dist/x64/browseros-vm-2026.04.22-1-x64.qcow2.zst
|
||||
|
||||
# Upload both arches to R2 (requires R2_* env vars).
|
||||
bun run upload -- --version 2026.04.22-1 --artifact-dir ./dist
|
||||
```
|
||||
|
||||
`--update-latest` is the default on upload; pass `--no-update-latest` to keep
|
||||
`vm/latest.json` pointing at whatever was there before.
|
||||
|
||||
## R2 layout produced
|
||||
|
||||
```
|
||||
r2://$R2_BUCKET/vm/<version>/
|
||||
browseros-vm-<version>-arm64.qcow2.zst
|
||||
browseros-vm-<version>-arm64.qcow2.zst.sha256
|
||||
browseros-vm-<version>-x64.qcow2.zst
|
||||
browseros-vm-<version>-x64.qcow2.zst.sha256
|
||||
manifest.json
|
||||
r2://$R2_BUCKET/vm/latest.json
|
||||
```
|
||||
|
||||
`manifest.json` is the consumer contract — see `src/schema/manifest.ts` for
|
||||
the zod schema. `latest.json` is a human/debug pointer and is **not**
|
||||
consumed by `build:server`; WS3's follow-up pins a specific version in
|
||||
`server-prod-resources.json`.
|
||||
|
||||
## CI
|
||||
|
||||
The `build-vm-container.yml` workflow runs:
|
||||
|
||||
1. Matrix build per arch on `ubuntu-24.04` + `ubuntu-24.04-arm64`.
|
||||
2. x64 Lima boot smoke test.
|
||||
3. Gated publish job (runs only on `workflow_dispatch` with `publish=true`).
|
||||
|
||||
Triggers: `workflow_dispatch` (manual), `pull_request` on
|
||||
`packages/vm-container/**` (dry-run, no publish), and a weekly cron to catch
|
||||
upstream Debian drift.
|
||||
|
||||
## Repro + pinning
|
||||
|
||||
- Base Debian image is pinned by sha512 in `src/build/base-image.ts` because
|
||||
Debian's cloud-image snapshots publish `SHA512SUMS` (not `SHA256SUMS`). The
|
||||
build verifies the download against that pinned sha512, then also computes
|
||||
a local sha256 of the same bytes and records it as `base_image_sha256` in
|
||||
the published `manifest.json` so WS4 can verify using the stable contract.
|
||||
Update both the pin and the upstream version together when bumping to a
|
||||
newer daily.
|
||||
- Recipe file (`recipe/browseros-vm.recipe`) is git-tracked and its sha256
|
||||
lands in `manifest.build.recipe_sha256`.
|
||||
- Installed package versions are captured post-install via `dpkg-query` and
|
||||
land in `manifest.packages`.
|
||||
- `manifest.build.git_sha` / `built_by` record the invocation context.
|
||||
|
||||
## What lives here vs. what's elsewhere
|
||||
|
||||
- **Here:** disk recipe, build orchestrator, R2 uploader, smoke test, types
|
||||
consumed by the runtime.
|
||||
- **Not here:** host-side consumption (`apps/server/.../podman-runtime.ts`),
|
||||
`server-prod-resources.json` entries, `limactl` binary upload (`packages/
|
||||
browseros/build/cli/storage.py`), agent container tarballs (WS2 lives in
|
||||
`packages/browseros-agent/packages/agent-container/` once it lands).
|
||||
|
||||
The file baked into `/etc/browseros-vm-manifest.json` is a build-time marker.
|
||||
The published per-version `manifest.json` is finalized later, after the
|
||||
artifacts have been hashed and uploaded.
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "@browseros/vm-container",
|
||||
"version": "0.0.0",
|
||||
"description": "BrowserOS VM disk image producer (Debian + Podman qcow2)",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./schema": {
|
||||
"types": "./src/schema/index.ts",
|
||||
"default": "./src/schema/index.ts"
|
||||
},
|
||||
"./download": {
|
||||
"types": "./src/download/index.ts",
|
||||
"default": "./src/download/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run scripts/build.ts",
|
||||
"upload": "bun run scripts/upload.ts",
|
||||
"smoke": "bun run scripts/smoke.ts",
|
||||
"test": "bun test tests/",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.933.0",
|
||||
"@browseros/shared": "workspace:*",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-sdk-client-mock": "^4.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { buildDisk } from '../src/build/orchestrator'
|
||||
import { parseArch, todayCalver } from '../src/schema/arch'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
version: { type: 'string' },
|
||||
arch: { type: 'string' },
|
||||
'output-dir': { type: 'string', default: './dist' },
|
||||
'base-image-sha256': { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.arch) {
|
||||
console.error(
|
||||
'usage: bun run build -- --arch <arm64|x64> [--version <YYYY.MM.DD[-N]>] [--output-dir ./dist] [--base-image-sha256 <sha>]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const version = values.version ?? todayCalver(Math.floor(Date.now() / 1000))
|
||||
|
||||
const result = await buildDisk({
|
||||
version,
|
||||
arch: parseArch(values.arch),
|
||||
outputDir: values['output-dir'] ?? './dist',
|
||||
baseImageSha256Override: values['base-image-sha256'],
|
||||
})
|
||||
|
||||
const resultPath = path.join(
|
||||
values['output-dir'] ?? './dist',
|
||||
`build-result-${result.arch}.json`,
|
||||
)
|
||||
await writeFile(resultPath, JSON.stringify(result, null, 2))
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import { parseArgs } from 'node:util'
|
||||
import { bootAndProbe } from '../src/smoke/lima-boot'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
qcow: { type: 'string' },
|
||||
limactl: { type: 'string' },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.qcow) {
|
||||
console.error(
|
||||
'usage: bun run smoke -- --qcow <path.qcow2.zst> [--limactl /usr/local/bin/limactl]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await bootAndProbe(values.qcow, { limactlPath: values.limactl })
|
||||
console.log('smoke test passed')
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import type { BuildResult } from '../src/build/types'
|
||||
import { type Arch, parseArch } from '../src/schema/arch'
|
||||
import { publishDisks } from '../src/upload/publish'
|
||||
|
||||
const { values } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
version: { type: 'string' },
|
||||
'artifact-dir': { type: 'string' },
|
||||
'update-latest': { type: 'boolean', default: true },
|
||||
'no-update-latest': { type: 'boolean', default: false },
|
||||
},
|
||||
})
|
||||
|
||||
if (!values.version || !values['artifact-dir']) {
|
||||
console.error(
|
||||
'usage: bun run upload -- --version <YYYY.MM.DD-N> --artifact-dir <dir> [--no-update-latest]',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const results = await loadResults(values['artifact-dir'])
|
||||
if (Object.keys(results).length === 0) {
|
||||
throw new Error(
|
||||
`no build-result-*.json files found under ${values['artifact-dir']}`,
|
||||
)
|
||||
}
|
||||
|
||||
await publishDisks({
|
||||
version: values.version,
|
||||
results,
|
||||
updateLatest: !values['no-update-latest'],
|
||||
})
|
||||
|
||||
console.log(
|
||||
`published ${Object.keys(results).length} arch(es) for version ${values.version}`,
|
||||
)
|
||||
|
||||
async function loadResults(
|
||||
dir: string,
|
||||
): Promise<Partial<Record<Arch, BuildResult>>> {
|
||||
const out: Partial<Record<Arch, BuildResult>> = {}
|
||||
for (const file of await walkForResults(dir)) {
|
||||
const raw = await readFile(file, 'utf8')
|
||||
const result = JSON.parse(raw) as BuildResult
|
||||
out[parseArch(result.arch)] = resolvePaths(result, path.dirname(file))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async function walkForResults(dir: string): Promise<string[]> {
|
||||
const out: string[] = []
|
||||
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
out.push(...(await walkForResults(full)))
|
||||
} else if (
|
||||
entry.name.startsWith('build-result-') &&
|
||||
entry.name.endsWith('.json')
|
||||
) {
|
||||
out.push(full)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function resolvePaths(result: BuildResult, dir: string): BuildResult {
|
||||
const resolve = (p: string): string =>
|
||||
path.isAbsolute(p) ? p : path.join(dir, path.basename(p))
|
||||
return {
|
||||
...result,
|
||||
rawQcowPath: resolve(result.rawQcowPath),
|
||||
compressedPath: resolve(result.compressedPath),
|
||||
buildLogPath: resolve(result.buildLogPath),
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Arch } from '../schema/arch'
|
||||
|
||||
export interface BaseImage {
|
||||
distro: 'debian'
|
||||
release: string
|
||||
channel: 'genericcloud'
|
||||
upstreamVersion: string
|
||||
arch: Arch
|
||||
url: string
|
||||
sha512: string
|
||||
}
|
||||
|
||||
// Debian bookworm genericcloud snapshot pin. Bump by hand when a newer
|
||||
// daily is needed; the scheduled base-image-bump workflow (follow-up PR)
|
||||
// will automate it. Pins are the SHA-512 values from the snapshot's
|
||||
// SHA512SUMS file at https://cloud.debian.org/images/cloud/bookworm/.
|
||||
// Debian publishes SHA512SUMS for these snapshots, not SHA256SUMS.
|
||||
const BOOKWORM_VERSION = '20260413-2447'
|
||||
|
||||
const PINNED_SHA512: Record<Arch, string> = {
|
||||
arm64:
|
||||
'15ad6c52e255c84eb0e91001c5907b27199d8a7164d8ac172cfe9c92850dfaf606a6c3161d6af7f0fd5a5fef2aa8dcd9a23c2eb0fedbfcddb38e2bc306cba98f',
|
||||
x64: 'db11b13c4efcc37828ffadae521d101e85079d349e1418074087bb7d306f11caccdc2b0b539d6fd50d623d40a898f83c6137268a048d7700397dc35b7dcbc927',
|
||||
}
|
||||
|
||||
export const DEBIAN_BASE_IMAGES: Record<Arch, BaseImage> = {
|
||||
arm64: {
|
||||
distro: 'debian',
|
||||
release: 'bookworm',
|
||||
channel: 'genericcloud',
|
||||
upstreamVersion: BOOKWORM_VERSION,
|
||||
arch: 'arm64',
|
||||
url: `https://cloud.debian.org/images/cloud/bookworm/${BOOKWORM_VERSION}/debian-12-genericcloud-arm64-${BOOKWORM_VERSION}.qcow2`,
|
||||
sha512: PINNED_SHA512.arm64,
|
||||
},
|
||||
x64: {
|
||||
distro: 'debian',
|
||||
release: 'bookworm',
|
||||
channel: 'genericcloud',
|
||||
upstreamVersion: BOOKWORM_VERSION,
|
||||
arch: 'x64',
|
||||
url: `https://cloud.debian.org/images/cloud/bookworm/${BOOKWORM_VERSION}/debian-12-genericcloud-amd64-${BOOKWORM_VERSION}.qcow2`,
|
||||
sha512: PINNED_SHA512.x64,
|
||||
},
|
||||
}
|
||||
|
||||
export const debianSha512SumsUrl = (upstreamVersion: string): string =>
|
||||
`https://cloud.debian.org/images/cloud/bookworm/${upstreamVersion}/SHA512SUMS`
|
||||
@@ -1,272 +0,0 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { copyFile, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { $ } from 'bun'
|
||||
import type { Arch } from '../schema/arch'
|
||||
import { assertCalver } from '../schema/arch'
|
||||
import {
|
||||
type BaseImage,
|
||||
DEBIAN_BASE_IMAGES,
|
||||
debianSha512SumsUrl,
|
||||
} from './base-image'
|
||||
import {
|
||||
composeVirtCustomizeArgv,
|
||||
parsePackagesOutput,
|
||||
parseRecipe,
|
||||
} from './recipe'
|
||||
import type { BuildOptions, BuildResult } from './types'
|
||||
|
||||
const DEFAULT_RECIPE_REL = '../../recipe/browseros-vm.recipe'
|
||||
|
||||
const SHA512_HEX = /^[a-f0-9]{128}$/
|
||||
|
||||
// Bun's file writer type is mildly hostile to `ReadableStream.pipeTo`, so we
|
||||
// hand-pump chunks through a lightweight sink type.
|
||||
type ChunkSink = ReturnType<ReturnType<typeof Bun.file>['writer']>
|
||||
|
||||
export async function buildDisk(opts: BuildOptions): Promise<BuildResult> {
|
||||
assertCalver(opts.version)
|
||||
const base = DEBIAN_BASE_IMAGES[opts.arch]
|
||||
const pinnedSha512 = await resolvePinnedSha512(base.upstreamVersion, base)
|
||||
|
||||
const prepared = await prepareCustomizedDisk(opts, base, pinnedSha512)
|
||||
const baseImageSha256 = await sha256File(prepared.basePath)
|
||||
if (
|
||||
opts.baseImageSha256Override &&
|
||||
opts.baseImageSha256Override !== baseImageSha256
|
||||
) {
|
||||
throw new Error(
|
||||
`base image sha256 mismatch: override ${opts.baseImageSha256Override}, computed ${baseImageSha256}`,
|
||||
)
|
||||
}
|
||||
const finalized = await finalizeArtifacts(opts, prepared.workPath)
|
||||
await rm(prepared.workPath, { force: true })
|
||||
await rm(prepared.basePath, { force: true })
|
||||
|
||||
return {
|
||||
arch: opts.arch,
|
||||
version: opts.version,
|
||||
baseImage: { ...base, sha512: pinnedSha512, sha256: baseImageSha256 },
|
||||
recipeSha256: prepared.recipeSha256,
|
||||
buildLogPath: prepared.buildLogPath,
|
||||
rawQcowPath: finalized.rawQcowPath,
|
||||
rawQcowSha256: finalized.rawQcowSha256,
|
||||
rawQcowSize: finalized.rawQcowSize,
|
||||
compressedPath: finalized.compressedPath,
|
||||
compressedSha256: finalized.compressedSha256,
|
||||
compressedSize: finalized.compressedSize,
|
||||
packages: finalized.packages,
|
||||
}
|
||||
}
|
||||
|
||||
interface PreparedDisk {
|
||||
basePath: string
|
||||
workPath: string
|
||||
buildLogPath: string
|
||||
recipeSha256: string
|
||||
}
|
||||
|
||||
async function prepareCustomizedDisk(
|
||||
opts: BuildOptions,
|
||||
base: BaseImage,
|
||||
pinnedSha512: string,
|
||||
): Promise<PreparedDisk> {
|
||||
await $`mkdir -p ${opts.outputDir}`.quiet()
|
||||
const basePath = path.join(opts.outputDir, `base-${opts.arch}.qcow2`)
|
||||
await downloadTo(base.url, basePath)
|
||||
await verifySha512(basePath, pinnedSha512)
|
||||
|
||||
const workPath = path.join(
|
||||
opts.outputDir,
|
||||
`work-${opts.version}-${opts.arch}.qcow2`,
|
||||
)
|
||||
await copyFile(basePath, workPath)
|
||||
|
||||
const recipePath =
|
||||
opts.recipePath ?? path.resolve(import.meta.dir, DEFAULT_RECIPE_REL)
|
||||
const recipeText = await readFile(recipePath, 'utf8')
|
||||
const recipeSha256 = sha256String(recipeText)
|
||||
|
||||
const buildMarkerPath = path.join(
|
||||
opts.outputDir,
|
||||
`build-marker-${opts.arch}.json`,
|
||||
)
|
||||
// The baked JSON is a build-time marker. The published per-version manifest
|
||||
// adds final provider hashes and URLs after artifact hashing and upload.
|
||||
await writeFile(
|
||||
buildMarkerPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
name: 'browseros-vm',
|
||||
version: opts.version,
|
||||
arch: opts.arch,
|
||||
phase: 'build',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
const argv = composeVirtCustomizeArgv({
|
||||
diskPath: workPath,
|
||||
recipe: parseRecipe(recipeText),
|
||||
substitutions: { version: opts.version, manifest_tmp: buildMarkerPath },
|
||||
recipeDir: path.dirname(recipePath),
|
||||
})
|
||||
const buildLogPath = path.join(opts.outputDir, `build-${opts.arch}.log`)
|
||||
await spawnToLog(['virt-customize', ...argv], buildLogPath)
|
||||
await $`virt-sparsify --in-place ${workPath}`.quiet()
|
||||
|
||||
return { basePath, workPath, buildLogPath, recipeSha256 }
|
||||
}
|
||||
|
||||
interface FinalizedArtifacts {
|
||||
rawQcowPath: string
|
||||
rawQcowSha256: string
|
||||
rawQcowSize: number
|
||||
compressedPath: string
|
||||
compressedSha256: string
|
||||
compressedSize: number
|
||||
packages: Record<string, string>
|
||||
}
|
||||
|
||||
async function finalizeArtifacts(
|
||||
opts: BuildOptions,
|
||||
workPath: string,
|
||||
): Promise<FinalizedArtifacts> {
|
||||
const rawQcowPath = path.join(
|
||||
opts.outputDir,
|
||||
`browseros-vm-${opts.version}-${opts.arch}.qcow2`,
|
||||
)
|
||||
await $`qemu-img convert -O qcow2 -c ${workPath} ${rawQcowPath}`.quiet()
|
||||
const rawQcowSha256 = await sha256File(rawQcowPath)
|
||||
const rawQcowSize = (await stat(rawQcowPath)).size
|
||||
|
||||
const compressedPath = `${rawQcowPath}.zst`
|
||||
await $`zstd -19 --long=30 -T0 -f -o ${compressedPath} ${rawQcowPath}`.quiet()
|
||||
const compressedSha256 = await sha256File(compressedPath)
|
||||
const compressedSize = (await stat(compressedPath)).size
|
||||
|
||||
const packages = await readPackagesFromDisk(
|
||||
workPath,
|
||||
opts.outputDir,
|
||||
opts.arch,
|
||||
)
|
||||
|
||||
return {
|
||||
rawQcowPath,
|
||||
rawQcowSha256,
|
||||
rawQcowSize,
|
||||
compressedPath,
|
||||
compressedSha256,
|
||||
compressedSize,
|
||||
packages,
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadTo(url: string, dest: string): Promise<void> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error(`download failed: ${url} (${response.status})`)
|
||||
}
|
||||
const sink = Bun.file(dest).writer()
|
||||
const reader = response.body.getReader()
|
||||
try {
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
sink.write(value)
|
||||
}
|
||||
} finally {
|
||||
await sink.end()
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySha512(filePath: string, expected: string): Promise<void> {
|
||||
const actual = await hashFile(filePath, 'sha512')
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`sha512 mismatch for ${filePath}: expected ${expected}, got ${actual}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256File(filePath: string): Promise<string> {
|
||||
return hashFile(filePath, 'sha256')
|
||||
}
|
||||
|
||||
async function hashFile(
|
||||
filePath: string,
|
||||
algo: 'sha256' | 'sha512',
|
||||
): Promise<string> {
|
||||
const hash = createHash(algo)
|
||||
for await (const chunk of createReadStream(filePath)) hash.update(chunk)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
function sha256String(text: string): string {
|
||||
return createHash('sha256').update(text).digest('hex')
|
||||
}
|
||||
|
||||
async function spawnToLog(argv: string[], logPath: string): Promise<void> {
|
||||
const log = Bun.file(logPath).writer()
|
||||
const proc = Bun.spawn(argv, {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
LIBGUESTFS_BACKEND: process.env.LIBGUESTFS_BACKEND ?? 'direct',
|
||||
},
|
||||
})
|
||||
await Promise.all([
|
||||
pumpStream(proc.stdout, log),
|
||||
pumpStream(proc.stderr, log),
|
||||
])
|
||||
const code = await proc.exited
|
||||
await log.end()
|
||||
if (code !== 0) {
|
||||
throw new Error(`${argv[0]} exited ${code}; see ${logPath}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function pumpStream(
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
sink: ChunkSink,
|
||||
): Promise<void> {
|
||||
const reader = stream.getReader()
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
sink.write(value)
|
||||
}
|
||||
}
|
||||
|
||||
async function readPackagesFromDisk(
|
||||
diskPath: string,
|
||||
outputDir: string,
|
||||
arch: Arch,
|
||||
): Promise<Record<string, string>> {
|
||||
const dumpPath = path.join(outputDir, `pkgs-${arch}.txt`)
|
||||
await $`virt-copy-out -a ${diskPath} /var/lib/browseros-vm-pkg-versions ${outputDir}`.quiet()
|
||||
await $`mv ${outputDir}/browseros-vm-pkg-versions ${dumpPath}`.quiet()
|
||||
const text = await readFile(dumpPath, 'utf8')
|
||||
return parsePackagesOutput(text)
|
||||
}
|
||||
|
||||
async function resolvePinnedSha512(
|
||||
upstreamVersion: string,
|
||||
base: BaseImage,
|
||||
): Promise<string> {
|
||||
if (SHA512_HEX.test(base.sha512)) return base.sha512
|
||||
const sumsUrl = debianSha512SumsUrl(upstreamVersion)
|
||||
const response = await fetch(sumsUrl)
|
||||
if (!response.ok) throw new Error(`SHA512SUMS fetch failed: ${sumsUrl}`)
|
||||
const text = await response.text()
|
||||
const filename = base.url.slice(base.url.lastIndexOf('/') + 1)
|
||||
for (const line of text.split('\n')) {
|
||||
const [sha, name] = line.trim().split(/\s+/)
|
||||
if (name === filename && sha) return sha
|
||||
}
|
||||
throw new Error(`SHA512SUMS missing entry for ${filename}`)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import path from 'node:path'
|
||||
|
||||
export type RecipeOp =
|
||||
| { op: 'run-command'; cmd: string }
|
||||
| { op: 'copy-in'; src: string; dest: string }
|
||||
| { op: 'write'; dest: string; content: string }
|
||||
| { op: 'truncate'; target: string }
|
||||
|
||||
export function parseRecipe(text: string): RecipeOp[] {
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0 && !line.startsWith('#'))
|
||||
.map(parseLine)
|
||||
}
|
||||
|
||||
function parseLine(line: string): RecipeOp {
|
||||
const space = line.indexOf(' ')
|
||||
if (space === -1) {
|
||||
throw new Error(`recipe line missing argument: ${line}`)
|
||||
}
|
||||
const op = line.slice(0, space)
|
||||
const arg = line.slice(space + 1).trim()
|
||||
switch (op) {
|
||||
case 'run-command':
|
||||
return { op, cmd: arg }
|
||||
case 'copy-in': {
|
||||
const colon = arg.indexOf(':')
|
||||
if (colon === -1) throw new Error(`copy-in missing ':': ${line}`)
|
||||
return { op, src: arg.slice(0, colon), dest: arg.slice(colon + 1) }
|
||||
}
|
||||
case 'write': {
|
||||
const colon = arg.indexOf(':')
|
||||
if (colon === -1) throw new Error(`write missing ':': ${line}`)
|
||||
return { op, dest: arg.slice(0, colon), content: arg.slice(colon + 1) }
|
||||
}
|
||||
case 'truncate':
|
||||
return { op, target: arg }
|
||||
default:
|
||||
throw new Error(`unknown recipe op: ${op}`)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComposeOptions {
|
||||
diskPath: string
|
||||
recipe: RecipeOp[]
|
||||
substitutions: Record<string, string>
|
||||
recipeDir: string
|
||||
}
|
||||
|
||||
export function composeVirtCustomizeArgv(opts: ComposeOptions): string[] {
|
||||
const substitute = (s: string): string =>
|
||||
s.replaceAll(/\{(\w+)\}/g, (match, key) => opts.substitutions[key] ?? match)
|
||||
const argv = ['-a', opts.diskPath]
|
||||
for (const op of opts.recipe) {
|
||||
switch (op.op) {
|
||||
case 'run-command':
|
||||
argv.push('--run-command', substitute(op.cmd))
|
||||
break
|
||||
case 'copy-in': {
|
||||
const substituted = substitute(op.src)
|
||||
const resolvedSrc = path.isAbsolute(substituted)
|
||||
? substituted
|
||||
: path.join(opts.recipeDir, substituted)
|
||||
argv.push('--copy-in', `${resolvedSrc}:${op.dest}`)
|
||||
break
|
||||
}
|
||||
case 'write':
|
||||
argv.push('--write', `${op.dest}:${substitute(op.content)}`)
|
||||
break
|
||||
case 'truncate':
|
||||
argv.push('--truncate', op.target)
|
||||
break
|
||||
}
|
||||
}
|
||||
argv.push(
|
||||
'--run-command',
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: dpkg format placeholder, not JS template
|
||||
"dpkg-query -W -f='${Package} ${Version}\\n' > /var/lib/browseros-vm-pkg-versions",
|
||||
)
|
||||
return argv
|
||||
}
|
||||
|
||||
export function parsePackagesOutput(text: string): Record<string, string> {
|
||||
const out: Record<string, string> = {}
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed) continue
|
||||
const space = trimmed.indexOf(' ')
|
||||
if (space === -1) continue
|
||||
out[trimmed.slice(0, space)] = trimmed.slice(space + 1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Arch } from '../schema/arch'
|
||||
import type { BaseImage } from './base-image'
|
||||
|
||||
// Runtime snapshot of the base image used for this build. `sha512` comes
|
||||
// from the pin (verified against Debian's signed SHA512SUMS); `sha256` is
|
||||
// computed locally after download because Debian doesn't publish SHA256
|
||||
// sidecars. The manifest exposes sha256 to WS4; sha512 is retained for
|
||||
// upstream-supply-chain traceability.
|
||||
export interface BuildBaseImage extends BaseImage {
|
||||
sha256: string
|
||||
}
|
||||
|
||||
export interface BuildResult {
|
||||
arch: Arch
|
||||
version: string
|
||||
baseImage: BuildBaseImage
|
||||
recipeSha256: string
|
||||
rawQcowPath: string
|
||||
rawQcowSha256: string
|
||||
rawQcowSize: number
|
||||
compressedPath: string
|
||||
compressedSha256: string
|
||||
compressedSize: number
|
||||
packages: Record<string, string>
|
||||
buildLogPath: string
|
||||
}
|
||||
|
||||
export interface BuildOptions {
|
||||
version: string
|
||||
arch: Arch
|
||||
outputDir: string
|
||||
recipePath?: string
|
||||
baseImageSha256Override?: string
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Arch } from '../schema/arch'
|
||||
import type { VmManifest } from '../schema/manifest'
|
||||
|
||||
// WS4 landing pad. Typed signatures only — bodies implemented by WS4 where
|
||||
// podman-runtime.ts consumes the shipped qcow2. Keeping the types here
|
||||
// prevents WS4's implementation from drifting from the producer's schema.
|
||||
|
||||
export async function downloadManifest(
|
||||
_version: string | 'latest',
|
||||
): Promise<VmManifest> {
|
||||
throw new Error('downloadManifest: implemented in WS4')
|
||||
}
|
||||
|
||||
export async function downloadQcow(
|
||||
_manifest: VmManifest,
|
||||
_arch: Arch,
|
||||
_destPath: string,
|
||||
): Promise<void> {
|
||||
throw new Error('downloadQcow: implemented in WS4')
|
||||
}
|
||||
|
||||
export async function verifySha256(
|
||||
_path: string,
|
||||
_expected: string,
|
||||
): Promise<void> {
|
||||
throw new Error('verifySha256: implemented in WS4')
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
export const ARCHES = ['arm64', 'x64'] as const
|
||||
export type Arch = (typeof ARCHES)[number]
|
||||
|
||||
export function parseArch(s: string): Arch {
|
||||
if (s === 'arm64' || s === 'x64') return s
|
||||
throw new Error(`invalid arch: ${s} (expected 'arm64' | 'x64')`)
|
||||
}
|
||||
|
||||
// YYYY.MM.DD with an optional numeric `-N` suffix (e.g. `-1`).
|
||||
export const CALVER_REGEX = /^\d{4}\.\d{2}\.\d{2}(-\d+)?$/
|
||||
|
||||
export function assertCalver(version: string): void {
|
||||
if (!CALVER_REGEX.test(version)) {
|
||||
throw new Error(
|
||||
`invalid CalVer: ${version} (expected YYYY.MM.DD[-N], e.g. 2026.04.22 or 2026.04.22-1)`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function todayCalver(suffix?: number): string {
|
||||
const now = new Date()
|
||||
const yyyy = now.getUTCFullYear()
|
||||
const mm = String(now.getUTCMonth() + 1).padStart(2, '0')
|
||||
const dd = String(now.getUTCDate()).padStart(2, '0')
|
||||
const base = `${yyyy}.${mm}.${dd}`
|
||||
return suffix == null ? base : `${base}-${suffix}`
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export type { Arch } from './arch'
|
||||
export {
|
||||
ARCHES,
|
||||
assertCalver,
|
||||
CALVER_REGEX,
|
||||
parseArch,
|
||||
todayCalver,
|
||||
} from './arch'
|
||||
export type { LatestPointer, VmManifest, VmProvider } from './manifest'
|
||||
export {
|
||||
latestPointerSchema,
|
||||
MANIFEST_SCHEMA_VERSION,
|
||||
parseLatestPointer,
|
||||
parseManifest,
|
||||
vmManifestSchema,
|
||||
vmProviderSchema,
|
||||
} from './manifest'
|
||||
export {
|
||||
keyForLatest,
|
||||
keyForManifest,
|
||||
keyForQcow,
|
||||
keyForSha,
|
||||
qcowFilename,
|
||||
R2_VM_PREFIX,
|
||||
} from './r2-keys'
|
||||
@@ -1,57 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
import { ARCHES, CALVER_REGEX } from './arch'
|
||||
|
||||
export const MANIFEST_SCHEMA_VERSION = 'v1' as const
|
||||
|
||||
const sha256Hex = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
|
||||
export const vmProviderSchema = z.object({
|
||||
arch: z.enum(ARCHES),
|
||||
filename: z.string().min(1),
|
||||
format: z.literal('qcow2+zstd'),
|
||||
compressed_sha256: sha256Hex,
|
||||
compressed_size_bytes: z.number().int().positive(),
|
||||
uncompressed_sha256: sha256Hex,
|
||||
uncompressed_size_bytes: z.number().int().positive(),
|
||||
base_image_sha256: sha256Hex,
|
||||
url: z.string().url(),
|
||||
})
|
||||
|
||||
export const vmManifestSchema = z.object({
|
||||
name: z.literal('browseros-vm'),
|
||||
version: z.string().regex(CALVER_REGEX),
|
||||
schema: z.literal(MANIFEST_SCHEMA_VERSION),
|
||||
build: z.object({
|
||||
git_sha: z.string().min(1),
|
||||
git_dirty: z.boolean(),
|
||||
built_at: z.string().datetime({ offset: true }),
|
||||
built_by: z.string().min(1),
|
||||
recipe_sha256: sha256Hex,
|
||||
}),
|
||||
base_image: z.object({
|
||||
distro: z.literal('debian'),
|
||||
release: z.string().min(1),
|
||||
channel: z.literal('genericcloud'),
|
||||
upstream_version: z.string().min(1),
|
||||
}),
|
||||
packages: z.record(z.string(), z.string()),
|
||||
providers: z.array(vmProviderSchema).length(2),
|
||||
})
|
||||
|
||||
export const latestPointerSchema = z.object({
|
||||
version: z.string().regex(CALVER_REGEX),
|
||||
updated_at: z.string().datetime({ offset: true }),
|
||||
url: z.string().url(),
|
||||
})
|
||||
|
||||
export type VmProvider = z.infer<typeof vmProviderSchema>
|
||||
export type VmManifest = z.infer<typeof vmManifestSchema>
|
||||
export type LatestPointer = z.infer<typeof latestPointerSchema>
|
||||
|
||||
export function parseManifest(raw: unknown): VmManifest {
|
||||
return vmManifestSchema.parse(raw)
|
||||
}
|
||||
|
||||
export function parseLatestPointer(raw: unknown): LatestPointer {
|
||||
return latestPointerSchema.parse(raw)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { Arch } from './arch'
|
||||
|
||||
export const R2_VM_PREFIX = 'vm'
|
||||
|
||||
export const qcowFilename = (version: string, arch: Arch): string =>
|
||||
`browseros-vm-${version}-${arch}.qcow2.zst`
|
||||
|
||||
export const keyForQcow = (version: string, arch: Arch): string =>
|
||||
`${R2_VM_PREFIX}/${version}/${qcowFilename(version, arch)}`
|
||||
|
||||
export const keyForSha = (version: string, arch: Arch): string =>
|
||||
`${keyForQcow(version, arch)}.sha256`
|
||||
|
||||
export const keyForManifest = (version: string): string =>
|
||||
`${R2_VM_PREFIX}/${version}/manifest.json`
|
||||
|
||||
export const keyForLatest = (): string => `${R2_VM_PREFIX}/latest.json`
|
||||
@@ -1,211 +0,0 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
PutObjectCommand,
|
||||
type S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
import type { BuildResult } from '../build/types'
|
||||
import {
|
||||
keyForLatest,
|
||||
keyForManifest,
|
||||
keyForQcow,
|
||||
keyForSha,
|
||||
latestPointerSchema,
|
||||
MANIFEST_SCHEMA_VERSION,
|
||||
qcowFilename,
|
||||
type VmManifest,
|
||||
type VmProvider,
|
||||
vmManifestSchema,
|
||||
} from '../schema'
|
||||
import type { Arch } from '../schema/arch'
|
||||
import { createR2Client, getBucket, getCdnBaseUrl } from './r2-client'
|
||||
|
||||
export interface PublishOptions {
|
||||
version: string
|
||||
results: Partial<Record<Arch, BuildResult>>
|
||||
updateLatest: boolean
|
||||
cdnBaseUrl?: string
|
||||
client?: S3Client
|
||||
bucket?: string
|
||||
}
|
||||
|
||||
export async function publishDisks(opts: PublishOptions): Promise<void> {
|
||||
const archs = Object.keys(opts.results) as Arch[]
|
||||
if (archs.length === 0) throw new Error('publishDisks: no results supplied')
|
||||
const client = opts.client ?? createR2Client()
|
||||
const bucket = opts.bucket ?? getBucket()
|
||||
const cdnBase = opts.cdnBaseUrl ?? getCdnBaseUrl()
|
||||
const uploaded: string[] = []
|
||||
|
||||
try {
|
||||
const providers: VmProvider[] = []
|
||||
let reference: BuildResult | undefined
|
||||
for (const arch of archs) {
|
||||
const result = opts.results[arch]
|
||||
if (!result) throw new Error(`missing BuildResult for arch ${arch}`)
|
||||
reference ??= result
|
||||
const qcowKey = keyForQcow(opts.version, arch)
|
||||
const shaKey = keyForSha(opts.version, arch)
|
||||
await uploadFile(client, bucket, qcowKey, result.compressedPath)
|
||||
uploaded.push(qcowKey)
|
||||
await uploadBody(
|
||||
client,
|
||||
bucket,
|
||||
shaKey,
|
||||
`${result.compressedSha256} ${qcowFilename(opts.version, arch)}\n`,
|
||||
)
|
||||
uploaded.push(shaKey)
|
||||
providers.push({
|
||||
arch,
|
||||
filename: qcowFilename(opts.version, arch),
|
||||
format: 'qcow2+zstd',
|
||||
compressed_sha256: result.compressedSha256,
|
||||
compressed_size_bytes: result.compressedSize,
|
||||
uncompressed_sha256: result.rawQcowSha256,
|
||||
uncompressed_size_bytes: result.rawQcowSize,
|
||||
base_image_sha256: result.baseImage.sha256,
|
||||
url: `${cdnBase}/${qcowKey}`,
|
||||
})
|
||||
}
|
||||
|
||||
if (!reference)
|
||||
throw new Error('publishDisks: no results yielded a reference build')
|
||||
const manifest = buildManifest(opts.version, reference, providers)
|
||||
const manifestKey = keyForManifest(opts.version)
|
||||
await uploadBody(
|
||||
client,
|
||||
bucket,
|
||||
manifestKey,
|
||||
JSON.stringify(manifest, null, 2),
|
||||
)
|
||||
uploaded.push(manifestKey)
|
||||
|
||||
if (opts.updateLatest) {
|
||||
const pointer = latestPointerSchema.parse({
|
||||
version: opts.version,
|
||||
updated_at: new Date().toISOString(),
|
||||
url: `${cdnBase}/${manifestKey}`,
|
||||
})
|
||||
const latestKey = keyForLatest()
|
||||
await uploadBody(
|
||||
client,
|
||||
bucket,
|
||||
latestKey,
|
||||
JSON.stringify(pointer, null, 2),
|
||||
)
|
||||
uploaded.push(latestKey)
|
||||
}
|
||||
} catch (err) {
|
||||
await rollback(client, bucket, uploaded)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function buildManifest(
|
||||
version: string,
|
||||
reference: BuildResult,
|
||||
providers: VmProvider[],
|
||||
): VmManifest {
|
||||
const manifest = {
|
||||
name: 'browseros-vm' as const,
|
||||
version,
|
||||
schema: MANIFEST_SCHEMA_VERSION,
|
||||
build: {
|
||||
git_sha: gitSha(),
|
||||
git_dirty: gitDirty(),
|
||||
built_at: new Date().toISOString(),
|
||||
built_by: builtBy(),
|
||||
recipe_sha256: reference.recipeSha256,
|
||||
},
|
||||
base_image: {
|
||||
distro: 'debian' as const,
|
||||
release: reference.baseImage.release,
|
||||
channel: 'genericcloud' as const,
|
||||
upstream_version: reference.baseImage.upstreamVersion,
|
||||
},
|
||||
packages: reference.packages,
|
||||
providers,
|
||||
}
|
||||
return vmManifestSchema.parse(manifest)
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
localPath: string,
|
||||
): Promise<void> {
|
||||
const { size } = await stat(localPath)
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: createReadStream(localPath),
|
||||
ContentLength: size,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function uploadBody(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
key: string,
|
||||
body: string,
|
||||
): Promise<void> {
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentLength: Buffer.byteLength(body),
|
||||
ContentType: key.endsWith('.json')
|
||||
? 'application/json; charset=utf-8'
|
||||
: 'text/plain; charset=utf-8',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function rollback(
|
||||
client: S3Client,
|
||||
bucket: string,
|
||||
keys: string[],
|
||||
): Promise<void> {
|
||||
const settled = await Promise.allSettled(
|
||||
keys.map((key) =>
|
||||
client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key })),
|
||||
),
|
||||
)
|
||||
const failures = settled.flatMap((result, index) =>
|
||||
result.status === 'rejected' ? [keys[index]] : [],
|
||||
)
|
||||
if (failures.length > 0) {
|
||||
console.warn(`rollback left orphaned keys: ${failures.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
function gitSha(): string {
|
||||
try {
|
||||
return execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim()
|
||||
} catch {
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
function gitDirty(): boolean {
|
||||
try {
|
||||
return (
|
||||
execSync('git status --porcelain', { encoding: 'utf8' }).trim().length > 0
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function builtBy(): string {
|
||||
const workflow = process.env.GITHUB_WORKFLOW
|
||||
const ref = process.env.GITHUB_REF
|
||||
if (workflow && ref) return `${workflow}@${ref}`
|
||||
return process.env.USER ?? 'unknown'
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
|
||||
export function createR2Client(): S3Client {
|
||||
const accountId = requireEnv('R2_ACCOUNT_ID')
|
||||
return new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: requireEnv('R2_ACCESS_KEY_ID'),
|
||||
secretAccessKey: requireEnv('R2_SECRET_ACCESS_KEY'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getBucket(): string {
|
||||
return requireEnv('R2_BUCKET')
|
||||
}
|
||||
|
||||
export function getCdnBaseUrl(): string {
|
||||
return process.env.CDN_BASE_URL ?? 'https://cdn.browseros.com'
|
||||
}
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name]
|
||||
if (!value) throw new Error(`missing required env var: ${name}`)
|
||||
return value
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { ARCHES, assertCalver, parseArch } from '../src/schema/arch'
|
||||
|
||||
describe('parseArch', () => {
|
||||
test('accepts supported arches', () => {
|
||||
expect(parseArch('arm64')).toBe('arm64')
|
||||
expect(parseArch('x64')).toBe('x64')
|
||||
})
|
||||
|
||||
test('rejects unsupported arches', () => {
|
||||
expect(() => parseArch('amd64')).toThrow(/invalid arch/)
|
||||
expect(() => parseArch('')).toThrow(/invalid arch/)
|
||||
})
|
||||
|
||||
test('is case-sensitive — uppercase is rejected', () => {
|
||||
expect(() => parseArch('ARM64')).toThrow(/invalid arch/)
|
||||
expect(() => parseArch('X64')).toThrow(/invalid arch/)
|
||||
})
|
||||
|
||||
test('ARCHES contains exactly the supported set', () => {
|
||||
expect([...ARCHES].sort()).toEqual(['arm64', 'x64'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('assertCalver', () => {
|
||||
test('accepts YYYY.MM.DD and YYYY.MM.DD-N', () => {
|
||||
assertCalver('2026.04.22')
|
||||
assertCalver('2026.04.22-1')
|
||||
assertCalver('2026.12.01-99')
|
||||
})
|
||||
|
||||
test('rejects semver and stray strings', () => {
|
||||
expect(() => assertCalver('1.2.3')).toThrow(/invalid CalVer/)
|
||||
expect(() => assertCalver('2026-04-22')).toThrow(/invalid CalVer/)
|
||||
expect(() => assertCalver('2026.04.22-dev1')).toThrow(/invalid CalVer/)
|
||||
expect(() => assertCalver('2026.04.22-rc1')).toThrow(/invalid CalVer/)
|
||||
expect(() => assertCalver('latest')).toThrow(/invalid CalVer/)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { DEBIAN_BASE_IMAGES } from '../src/build/base-image'
|
||||
|
||||
describe('DEBIAN_BASE_IMAGES', () => {
|
||||
test('pins valid SHA-512 digests for each arch', () => {
|
||||
for (const image of Object.values(DEBIAN_BASE_IMAGES)) {
|
||||
expect(image.sha512).toMatch(/^[a-f0-9]{128}$/)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,122 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import type { VmManifest } from '../src/schema/manifest'
|
||||
import {
|
||||
MANIFEST_SCHEMA_VERSION,
|
||||
parseLatestPointer,
|
||||
parseManifest,
|
||||
} from '../src/schema/manifest'
|
||||
|
||||
const sha = (c: string): string => c.repeat(64).slice(0, 64)
|
||||
|
||||
const validManifest: VmManifest = {
|
||||
name: 'browseros-vm',
|
||||
version: '2026.04.22-1',
|
||||
schema: MANIFEST_SCHEMA_VERSION,
|
||||
build: {
|
||||
git_sha: 'abc123',
|
||||
git_dirty: false,
|
||||
built_at: '2026-04-22T00:00:00.000Z',
|
||||
built_by: 'operator',
|
||||
recipe_sha256: sha('a'),
|
||||
},
|
||||
base_image: {
|
||||
distro: 'debian',
|
||||
release: 'bookworm',
|
||||
channel: 'genericcloud',
|
||||
upstream_version: '20260413-2447',
|
||||
},
|
||||
packages: { podman: '4.3.1+ds1-8+deb12u1' },
|
||||
providers: [
|
||||
{
|
||||
arch: 'arm64',
|
||||
filename: 'browseros-vm-2026.04.22-1-arm64.qcow2.zst',
|
||||
format: 'qcow2+zstd',
|
||||
compressed_sha256: sha('b'),
|
||||
compressed_size_bytes: 200_000_000,
|
||||
uncompressed_sha256: sha('c'),
|
||||
uncompressed_size_bytes: 500_000_000,
|
||||
base_image_sha256: sha('d'),
|
||||
url: 'https://cdn.browseros.com/vm/2026.04.22-1/browseros-vm-2026.04.22-1-arm64.qcow2.zst',
|
||||
},
|
||||
{
|
||||
arch: 'x64',
|
||||
filename: 'browseros-vm-2026.04.22-1-x64.qcow2.zst',
|
||||
format: 'qcow2+zstd',
|
||||
compressed_sha256: sha('e'),
|
||||
compressed_size_bytes: 210_000_000,
|
||||
uncompressed_sha256: sha('f'),
|
||||
uncompressed_size_bytes: 520_000_000,
|
||||
base_image_sha256: sha('d'),
|
||||
url: 'https://cdn.browseros.com/vm/2026.04.22-1/browseros-vm-2026.04.22-1-x64.qcow2.zst',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('parseManifest', () => {
|
||||
test('accepts a valid manifest', () => {
|
||||
expect(parseManifest(validManifest)).toEqual(validManifest)
|
||||
})
|
||||
|
||||
test('rejects bad CalVer', () => {
|
||||
expect(() =>
|
||||
parseManifest({ ...validManifest, version: '1.2.3' }),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
test('rejects unknown schema version', () => {
|
||||
expect(() => parseManifest({ ...validManifest, schema: 'v2' })).toThrow()
|
||||
})
|
||||
|
||||
test('rejects short sha256', () => {
|
||||
const bad = {
|
||||
...validManifest,
|
||||
build: { ...validManifest.build, recipe_sha256: 'tooshort' },
|
||||
}
|
||||
expect(() => parseManifest(bad)).toThrow()
|
||||
})
|
||||
|
||||
test('rejects invalid timestamps', () => {
|
||||
expect(() =>
|
||||
parseManifest({
|
||||
...validManifest,
|
||||
build: { ...validManifest.build, built_at: 'not-a-date' },
|
||||
}),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
test('rejects less than 2 providers', () => {
|
||||
const bad = { ...validManifest, providers: [validManifest.providers[0]] }
|
||||
expect(() => parseManifest(bad)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseLatestPointer', () => {
|
||||
test('accepts valid pointer', () => {
|
||||
const pointer = {
|
||||
version: '2026.04.22-1',
|
||||
updated_at: '2026-04-22T00:00:00.000Z',
|
||||
url: 'https://cdn.browseros.com/vm/2026.04.22-1/manifest.json',
|
||||
}
|
||||
expect(parseLatestPointer(pointer)).toEqual(pointer)
|
||||
})
|
||||
|
||||
test('rejects bad CalVer', () => {
|
||||
expect(() =>
|
||||
parseLatestPointer({
|
||||
version: 'latest',
|
||||
updated_at: '2026-04-22T00:00:00.000Z',
|
||||
url: 'https://cdn.browseros.com/vm/latest/manifest.json',
|
||||
}),
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
test('rejects invalid timestamps', () => {
|
||||
expect(() =>
|
||||
parseLatestPointer({
|
||||
version: '2026.04.22-1',
|
||||
updated_at: 'yesterday',
|
||||
url: 'https://cdn.browseros.com/vm/2026.04.22-1/manifest.json',
|
||||
}),
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,187 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
DeleteObjectCommand,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from '@aws-sdk/client-s3'
|
||||
import { mockClient } from 'aws-sdk-client-mock'
|
||||
import type { BuildResult } from '../src/build/types'
|
||||
import type { Arch } from '../src/schema/arch'
|
||||
import { publishDisks } from '../src/upload/publish'
|
||||
|
||||
// aws-sdk-client-mock's own @smithy/types can lag @aws-sdk/client-s3, so its
|
||||
// typed signatures reject our command classes at compile time even though
|
||||
// they work at runtime. `as never` sidesteps the version skew.
|
||||
const PutCmd = PutObjectCommand as never
|
||||
const DeleteCmd = DeleteObjectCommand as never
|
||||
const s3Mock = mockClient(S3Client as never)
|
||||
|
||||
const sha = (c: string): string => c.repeat(64).slice(0, 64)
|
||||
|
||||
let workDir: string
|
||||
|
||||
beforeEach(async () => {
|
||||
s3Mock.reset()
|
||||
s3Mock.on(PutCmd).resolves({})
|
||||
s3Mock.on(DeleteCmd).resolves({})
|
||||
workDir = await mkdtemp(path.join(tmpdir(), 'publish-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(workDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function makeResult(arch: Arch): Promise<BuildResult> {
|
||||
const compressed = path.join(
|
||||
workDir,
|
||||
`browseros-vm-2026.04.22-1-${arch}.qcow2.zst`,
|
||||
)
|
||||
await writeFile(compressed, Buffer.from('fake-compressed-data'))
|
||||
return {
|
||||
arch,
|
||||
version: '2026.04.22-1',
|
||||
baseImage: {
|
||||
distro: 'debian',
|
||||
release: 'bookworm',
|
||||
channel: 'genericcloud',
|
||||
upstreamVersion: '20260413-2447',
|
||||
arch,
|
||||
url: `https://cloud.debian.org/.../${arch}.qcow2`,
|
||||
sha512: sha('d').repeat(2),
|
||||
sha256: sha('d'),
|
||||
},
|
||||
recipeSha256: sha('a'),
|
||||
rawQcowPath: path.join(workDir, `browseros-vm-2026.04.22-1-${arch}.qcow2`),
|
||||
rawQcowSha256: sha('b'),
|
||||
rawQcowSize: 500_000_000,
|
||||
compressedPath: compressed,
|
||||
compressedSha256: sha('c'),
|
||||
compressedSize: 200_000_000,
|
||||
packages: { podman: '4.3.1-1' },
|
||||
buildLogPath: path.join(workDir, `build-${arch}.log`),
|
||||
}
|
||||
}
|
||||
|
||||
const client = new S3Client({})
|
||||
const bucket = 'test-bucket'
|
||||
const version = '2026.04.22-1'
|
||||
|
||||
describe('publishDisks', () => {
|
||||
test('happy path uploads qcow + sha per arch, then manifest, then latest', async () => {
|
||||
const results = {
|
||||
arm64: await makeResult('arm64'),
|
||||
x64: await makeResult('x64'),
|
||||
}
|
||||
await publishDisks({ version, results, updateLatest: true, client, bucket })
|
||||
|
||||
const puts = s3Mock.commandCalls(PutCmd)
|
||||
const keys = puts.map((c) => c.args[0].input.Key as string)
|
||||
expect(keys).toEqual([
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst`,
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst.sha256`,
|
||||
`vm/${version}/browseros-vm-${version}-x64.qcow2.zst`,
|
||||
`vm/${version}/browseros-vm-${version}-x64.qcow2.zst.sha256`,
|
||||
`vm/${version}/manifest.json`,
|
||||
'vm/latest.json',
|
||||
])
|
||||
const sidecarUpload = puts.find(
|
||||
(call) =>
|
||||
call.args[0].input.Key ===
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst.sha256`,
|
||||
)
|
||||
expect(sidecarUpload?.args[0].input.ContentType).toBe(
|
||||
'text/plain; charset=utf-8',
|
||||
)
|
||||
const manifestUpload = puts.find(
|
||||
(call) => call.args[0].input.Key === `vm/${version}/manifest.json`,
|
||||
)
|
||||
expect(manifestUpload?.args[0].input.ContentType).toBe(
|
||||
'application/json; charset=utf-8',
|
||||
)
|
||||
expect(s3Mock.commandCalls(DeleteCmd).length).toBe(0)
|
||||
})
|
||||
|
||||
test('updateLatest: false omits latest.json', async () => {
|
||||
const results = {
|
||||
arm64: await makeResult('arm64'),
|
||||
x64: await makeResult('x64'),
|
||||
}
|
||||
await publishDisks({
|
||||
version,
|
||||
results,
|
||||
updateLatest: false,
|
||||
client,
|
||||
bucket,
|
||||
})
|
||||
const keys = s3Mock
|
||||
.commandCalls(PutCmd)
|
||||
.map((c) => c.args[0].input.Key as string)
|
||||
expect(keys).not.toContain('vm/latest.json')
|
||||
})
|
||||
|
||||
test('rollback deletes already-uploaded keys on failure', async () => {
|
||||
s3Mock.reset()
|
||||
let callCount = 0
|
||||
s3Mock.on(PutCmd).callsFake(() => {
|
||||
callCount += 1
|
||||
if (callCount > 2) throw new Error('simulated R2 failure')
|
||||
return {}
|
||||
})
|
||||
s3Mock.on(DeleteCmd).resolves({})
|
||||
|
||||
const results = {
|
||||
arm64: await makeResult('arm64'),
|
||||
x64: await makeResult('x64'),
|
||||
}
|
||||
await expect(
|
||||
publishDisks({ version, results, updateLatest: true, client, bucket }),
|
||||
).rejects.toThrow(/simulated R2 failure/)
|
||||
|
||||
const deletedKeys = s3Mock
|
||||
.commandCalls(DeleteCmd)
|
||||
.map((c) => c.args[0].input.Key as string)
|
||||
.sort()
|
||||
expect(deletedKeys).toEqual([
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst`,
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst.sha256`,
|
||||
])
|
||||
})
|
||||
|
||||
test('rollback on mid-manifest failure deletes every qcow+sha already uploaded', async () => {
|
||||
s3Mock.reset()
|
||||
s3Mock.on(PutCmd).callsFake((input) => {
|
||||
if (input.Key?.endsWith('manifest.json')) {
|
||||
throw new Error('simulated manifest upload failure')
|
||||
}
|
||||
return {}
|
||||
})
|
||||
s3Mock.on(DeleteCmd).resolves({})
|
||||
|
||||
const results = {
|
||||
arm64: await makeResult('arm64'),
|
||||
x64: await makeResult('x64'),
|
||||
}
|
||||
await expect(
|
||||
publishDisks({ version, results, updateLatest: true, client, bucket }),
|
||||
).rejects.toThrow(/simulated manifest upload failure/)
|
||||
|
||||
const deletedKeys = s3Mock
|
||||
.commandCalls(DeleteCmd)
|
||||
.map((c) => c.args[0].input.Key as string)
|
||||
.sort()
|
||||
expect(deletedKeys).toEqual([
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst`,
|
||||
`vm/${version}/browseros-vm-${version}-arm64.qcow2.zst.sha256`,
|
||||
`vm/${version}/browseros-vm-${version}-x64.qcow2.zst`,
|
||||
`vm/${version}/browseros-vm-${version}-x64.qcow2.zst.sha256`,
|
||||
])
|
||||
const uploadedKeys = s3Mock
|
||||
.commandCalls(PutCmd)
|
||||
.filter((c) => !(c.args[0].input.Key as string).endsWith('manifest.json'))
|
||||
.map((c) => c.args[0].input.Key as string)
|
||||
expect(uploadedKeys).not.toContain('vm/latest.json')
|
||||
})
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
keyForLatest,
|
||||
keyForManifest,
|
||||
keyForQcow,
|
||||
keyForSha,
|
||||
qcowFilename,
|
||||
R2_VM_PREFIX,
|
||||
} from '../src/schema/r2-keys'
|
||||
|
||||
describe('R2 key helpers', () => {
|
||||
const version = '2026.04.22-1'
|
||||
|
||||
test('qcowFilename shape', () => {
|
||||
expect(qcowFilename(version, 'arm64')).toBe(
|
||||
'browseros-vm-2026.04.22-1-arm64.qcow2.zst',
|
||||
)
|
||||
expect(qcowFilename(version, 'x64')).toBe(
|
||||
'browseros-vm-2026.04.22-1-x64.qcow2.zst',
|
||||
)
|
||||
})
|
||||
|
||||
test('keyForQcow is versioned', () => {
|
||||
expect(keyForQcow(version, 'arm64')).toBe(
|
||||
`${R2_VM_PREFIX}/${version}/browseros-vm-${version}-arm64.qcow2.zst`,
|
||||
)
|
||||
})
|
||||
|
||||
test('keyForSha appends .sha256', () => {
|
||||
expect(keyForSha(version, 'x64')).toBe(
|
||||
`${keyForQcow(version, 'x64')}.sha256`,
|
||||
)
|
||||
})
|
||||
|
||||
test('keyForManifest + keyForLatest', () => {
|
||||
expect(keyForManifest(version)).toBe(
|
||||
`${R2_VM_PREFIX}/${version}/manifest.json`,
|
||||
)
|
||||
expect(keyForLatest()).toBe(`${R2_VM_PREFIX}/latest.json`)
|
||||
})
|
||||
})
|
||||
@@ -1,121 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import {
|
||||
composeVirtCustomizeArgv,
|
||||
parsePackagesOutput,
|
||||
parseRecipe,
|
||||
} from '../src/build/recipe'
|
||||
|
||||
describe('parseRecipe', () => {
|
||||
test('skips comments and blank lines, parses all four ops', () => {
|
||||
const text = `
|
||||
# comment
|
||||
run-command apt-get update
|
||||
|
||||
copy-in auth.json:/etc/containers/
|
||||
write /etc/browseros-vm-version:{version}
|
||||
truncate /etc/machine-id
|
||||
`
|
||||
expect(parseRecipe(text)).toEqual([
|
||||
{ op: 'run-command', cmd: 'apt-get update' },
|
||||
{ op: 'copy-in', src: 'auth.json', dest: '/etc/containers/' },
|
||||
{ op: 'write', dest: '/etc/browseros-vm-version', content: '{version}' },
|
||||
{ op: 'truncate', target: '/etc/machine-id' },
|
||||
])
|
||||
})
|
||||
|
||||
test('rejects unknown ops', () => {
|
||||
expect(() => parseRecipe('unknown-op something')).toThrow(
|
||||
/unknown recipe op/,
|
||||
)
|
||||
})
|
||||
|
||||
test('write with colon in content keeps everything after first colon', () => {
|
||||
expect(parseRecipe('write /etc/x:a:b:c')).toEqual([
|
||||
{ op: 'write', dest: '/etc/x', content: 'a:b:c' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('composeVirtCustomizeArgv', () => {
|
||||
test('substitutes variables and resolves copy-in relative to recipeDir', () => {
|
||||
const argv = composeVirtCustomizeArgv({
|
||||
diskPath: '/work/disk.qcow2',
|
||||
recipe: [
|
||||
{ op: 'run-command', cmd: 'echo {version}' },
|
||||
{ op: 'copy-in', src: 'auth.json', dest: '/etc/' },
|
||||
{ op: 'write', dest: '/etc/version', content: '{version}' },
|
||||
{ op: 'truncate', target: '/etc/machine-id' },
|
||||
],
|
||||
substitutions: { version: '2026.04.22-1' },
|
||||
recipeDir: '/recipe',
|
||||
})
|
||||
expect(argv).toEqual([
|
||||
'-a',
|
||||
'/work/disk.qcow2',
|
||||
'--run-command',
|
||||
'echo 2026.04.22-1',
|
||||
'--copy-in',
|
||||
'/recipe/auth.json:/etc/',
|
||||
'--write',
|
||||
'/etc/version:2026.04.22-1',
|
||||
'--truncate',
|
||||
'/etc/machine-id',
|
||||
'--run-command',
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: dpkg format placeholder, not JS template
|
||||
"dpkg-query -W -f='${Package} ${Version}\\n' > /var/lib/browseros-vm-pkg-versions",
|
||||
])
|
||||
})
|
||||
|
||||
test('absolute copy-in paths are passed through', () => {
|
||||
const argv = composeVirtCustomizeArgv({
|
||||
diskPath: '/disk.qcow2',
|
||||
recipe: [
|
||||
{ op: 'copy-in', src: '/tmp/manifest.json', dest: '/etc/m.json' },
|
||||
],
|
||||
substitutions: {},
|
||||
recipeDir: '/recipe',
|
||||
})
|
||||
expect(argv).toContain('--copy-in')
|
||||
expect(argv).toContain('/tmp/manifest.json:/etc/m.json')
|
||||
})
|
||||
|
||||
test('copy-in resolves absolutes from substituted placeholders', () => {
|
||||
const argv = composeVirtCustomizeArgv({
|
||||
diskPath: '/disk.qcow2',
|
||||
recipe: [{ op: 'copy-in', src: '{manifest_tmp}', dest: '/etc/m.json' }],
|
||||
substitutions: { manifest_tmp: '/tmp/vm-dist/manifest-stub.json' },
|
||||
recipeDir: '/recipe',
|
||||
})
|
||||
expect(argv).toContain('/tmp/vm-dist/manifest-stub.json:/etc/m.json')
|
||||
expect(argv.join(' ')).not.toContain('/recipe/tmp/')
|
||||
})
|
||||
|
||||
test('unresolved substitutions pass through unchanged', () => {
|
||||
const argv = composeVirtCustomizeArgv({
|
||||
diskPath: '/disk.qcow2',
|
||||
recipe: [
|
||||
{ op: 'run-command', cmd: 'echo {missing}' },
|
||||
{ op: 'write', dest: '/etc/x', content: '{also_missing}' },
|
||||
],
|
||||
substitutions: { version: '2026.04.22-1' },
|
||||
recipeDir: '/recipe',
|
||||
})
|
||||
expect(argv).toContain('echo {missing}')
|
||||
expect(argv).toContain('/etc/x:{also_missing}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePackagesOutput', () => {
|
||||
test('parses dpkg-query output', () => {
|
||||
const text = `podman 4.3.1+ds1-8+deb12u1
|
||||
crun 1.8.1-1+deb12u1
|
||||
|
||||
fuse-overlayfs 1.10-1+b1
|
||||
`
|
||||
expect(parsePackagesOutput(text)).toEqual({
|
||||
podman: '4.3.1+ds1-8+deb12u1',
|
||||
crun: '1.8.1-1+deb12u1',
|
||||
'fuse-overlayfs': '1.10-1+b1',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*", "scripts/**/*", "tests/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user