diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 721687d8..f21a4257 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,11 +10,10 @@ updates: minor-and-patch: update-types: ["minor", "patch"] - # JavaScript/TypeScript — CDK (/infra), Playwright (/tests/e2e), dashboard (/ui), root tooling + # JavaScript/TypeScript — Playwright (/tests/e2e), dashboard (/ui), root tooling - package-ecosystem: "npm" directories: - "/" - - "/infra" - "/tests/e2e" - "/ui" schedule: diff --git a/.github/scripts/package-artifacts.sh b/.github/scripts/package-artifacts.sh deleted file mode 100755 index 9e88fa87..00000000 --- a/.github/scripts/package-artifacts.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# Package the two release artifacts (run from the repo root by build-artifacts.yml): -# -# spa.tar.gz = CONTENTS of the built SPA dir (ui/.output/public/*), so it extracts -# straight into the nginx web root with _shell.html at the root. -# app.tar.gz = the Python source the box runs `uv sync` against. `git archive` -# gives a clean tree (no node_modules, no .venv, no local cruft); -# ui/ is intentionally excluded (it ships as spa.tar.gz). -set -euo pipefail - -SPA_DIR="ui/.output/public" -[ -f "${SPA_DIR}/_shell.html" ] || { - echo "ERROR: SPA build output missing ${SPA_DIR}/_shell.html (did 'bun run build' run?)" >&2 - exit 1 -} - -echo "==> spa.tar.gz from ${SPA_DIR}" -tar -C "${SPA_DIR}" -czf spa.tar.gz . - -echo "==> app.tar.gz from source (git archive HEAD)" -git archive --format=tar.gz -o app.tar.gz HEAD \ - agent deploy langgraph.json pyproject.toml uv.lock README.md - -# List the archive ONCE into a variable. (`tar -tzf ... | grep -q ...` is unsafe -# under `set -o pipefail`: grep -q exits on first match, SIGPIPEs tar -> "write -# error" -> the pipeline reports non-zero even though grep succeeded, a false -# failure. Listing once avoids the pipe entirely.) -APP_LIST="$(tar -tzf app.tar.gz)" - -# Sanity: the box's `uv sync --frozen` needs pyproject.toml + uv.lock at the root, -# and the package itself (agent/). Fail loudly here rather than on the box. -for required in pyproject.toml uv.lock agent/server.py langgraph.json; do - printf '%s\n' "${APP_LIST}" | grep -qx "${required}" || { - echo "ERROR: app.tar.gz is missing ${required}" >&2 - exit 1 - } -done - -# Fail-closed secret guard: the source is git-archived wholesale, so reject the -# release if a secret-shaped FILE slipped into the tracked tree (defense in depth -# on top of .gitignore — the artifact lands on the box + in S3). Scoped to data -# extensions so credential-handling *source* (e.g. team_credentials.py) is not a -# false positive. -SECRET_RE='(^|/)(\.env(\..+)?|id_rsa|.*\.(pem|key|p12|pfx)|.*(secret|credential|password|token)s?\.(json|ya?ml|txt|env|ini|cfg))$' -if printf '%s\n' "${APP_LIST}" | grep -qiE "${SECRET_RE}"; then - echo "ERROR: app.tar.gz contains a secret-shaped file — refusing to publish:" >&2 - printf '%s\n' "${APP_LIST}" | grep -iE "${SECRET_RE}" >&2 - exit 1 -fi - -echo "==> artifacts:" -ls -la spa.tar.gz app.tar.gz diff --git a/.github/scripts/publish-and-deploy.sh b/.github/scripts/publish-and-deploy.sh deleted file mode 100755 index 2551aa54..00000000 --- a/.github/scripts/publish-and-deploy.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# Publish the packaged artifacts to the env's S3 bucket and roll the box to them. -# Run by build-artifacts.yml AFTER aws creds are configured (env: ENV, BUCKET, -# DEPLOY_DOC). Each release is stored immutably under releases//. -# -# releases/latest/ (what the box's deploy.sh pulls) is advanced TRANSACTIONALLY: -# it is pointed at the new release, the box is rolled, and ONLY on a successful -# roll is it kept — a failed roll reverts releases/latest/ to the prior release so -# a later box boot / replacement never self-deploys a release that failed to come -# up. releases/last-good/ (rollback fallback) is advanced only after success and -# means "last release whose deploy.sh brought the service up active" (deploy.sh -# gates on `systemctl is-active`), not merely "last uploaded". -# -# The fire/wait/gate against the box lives in roll-box.sh (shared with rollback.sh); -# the deploy is fired by TAG (project=open-swe,env=), exactly what the app -# deploy role's tag-scoped ssm:SendCommand allows. -set -euo pipefail - -: "${ENV:?}" "${BUCKET:?}" "${DEPLOY_DOC:?}" -SHA="${GITHUB_SHA:?}" -[ -f app.tar.gz ] && [ -f spa.tar.gz ] || { echo "ERROR: artifacts not built" >&2; exit 1; } -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Re-point releases/latest/ at the release stored under releases//, recording -# the sha so the pointer is self-describing (used to revert on failure). -point_latest() { - local s="$1" f - for f in app.tar.gz spa.tar.gz; do - aws s3 cp "s3://${BUCKET}/releases/${s}/${f}" "s3://${BUCKET}/releases/latest/${f}" - done - printf '%s\n' "${s}" | aws s3 cp - "s3://${BUCKET}/releases/latest/sha.txt" -} - -# Which release does latest point at right now? (empty on the very first deploy.) -PREV_SHA="$(aws s3 cp "s3://${BUCKET}/releases/latest/sha.txt" - 2>/dev/null | tr -d '[:space:]' || true)" - -echo "==> upload release ${SHA} to s3://${BUCKET}/releases/${SHA}/" -for f in app.tar.gz spa.tar.gz; do - aws s3 cp "${f}" "s3://${BUCKET}/releases/${SHA}/${f}" -done - -echo "==> point releases/latest/ -> ${SHA} (was ${PREV_SHA:-})" -point_latest "${SHA}" - -# Roll the box to releases/latest/. On a non-Success aggregate, roll-box.sh exits -# non-zero; revert latest to the prior release so no later boot pulls the bad one. -if ! ROLL_COMMENT="release ${SHA}" bash "${HERE}/roll-box.sh"; then - if [ -n "${PREV_SHA}" ]; then - echo "!! deploy failed — reverting releases/latest/ -> ${PREV_SHA}" >&2 - point_latest "${PREV_SHA}" - else - echo "!! deploy failed on the FIRST release — leaving releases/latest/ = ${SHA} (no prior release to revert to)" >&2 - fi - exit 1 -fi - -# Roll succeeded (service came up active) -> this release is now the known-good one. -echo "==> mark releases/last-good/ = ${SHA} (rollback fallback target)" -for f in app.tar.gz spa.tar.gz; do - aws s3 cp "s3://${BUCKET}/releases/${SHA}/${f}" "s3://${BUCKET}/releases/last-good/${f}" -done -printf '%s\n' "${SHA}" | aws s3 cp - "s3://${BUCKET}/releases/last-good/sha.txt" - -echo "==> ${ENV} rolled to release ${SHA} (now last-good)" diff --git a/.github/scripts/roll-box.sh b/.github/scripts/roll-box.sh deleted file mode 100755 index a7dc42fc..00000000 --- a/.github/scripts/roll-box.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Fire the env's SSM deploy document (tag-targeted) and wait for it to finish, -# gating on the AGGREGATE command status. Shared by publish-and-deploy.sh (forward -# roll) and rollback.sh (backward roll) so the fire/wait/gate logic lives in ONE -# place. Requires ENV + DEPLOY_DOC in the environment and aws creds already set. -# -# Tag-targeting (project=open-swe,env=) is exactly what the app deploy role's -# tag-scoped ssm:SendCommand allows — no ec2:DescribeInstances, no instance id. -set -euo pipefail -: "${ENV:?}" "${DEPLOY_DOC:?}" -COMMENT="${ROLL_COMMENT:-roll ${ENV}}" - -echo "==> fire ${DEPLOY_DOC} via SSM (tag-targeted: project=open-swe, env=${ENV})" -CMD_ID="$(aws ssm send-command \ - --document-name "${DEPLOY_DOC}" \ - --targets "Key=tag:project,Values=open-swe" "Key=tag:env,Values=${ENV}" \ - --comment "${COMMENT}" \ - --query 'Command.CommandId' --output text)" -echo "command: ${CMD_ID}" - -echo "==> wait for the deploy to finish" -IID="" -for _ in $(seq 1 60); do - sleep 10 - IID="$(aws ssm list-command-invocations --command-id "${CMD_ID}" \ - --query 'CommandInvocations[0].InstanceId' --output text 2>/dev/null || echo None)" - [ -z "${IID}" ] || [ "${IID}" = "None" ] && continue - STATUS="$(aws ssm list-command-invocations --command-id "${CMD_ID}" \ - --query 'CommandInvocations[0].Status' --output text 2>/dev/null || echo Pending)" - case "${STATUS}" in - Success | Failed | Cancelled | TimedOut) break ;; - esac -done - -if [ -z "${IID}" ] || [ "${IID}" = "None" ]; then - echo "ERROR: no box picked up the deploy command (is a running open-swe ${ENV} box registered with SSM?)" >&2 - exit 1 -fi - -echo "==> deploy.sh output from ${IID}:" -echo "----- stdout -----" -aws ssm get-command-invocation --command-id "${CMD_ID}" --instance-id "${IID}" \ - --query 'StandardOutputContent' --output text || true -echo "----- stderr -----" -aws ssm get-command-invocation --command-id "${CMD_ID}" --instance-id "${IID}" \ - --query 'StandardErrorContent' --output text || true - -# Gate on the AGGREGATE command status (Success only if EVERY targeted invocation -# succeeded), not CommandInvocations[0] — during a userDataCausesReplacement window -# two instances can briefly share the project/env tags, and a partial failure on the -# other instance must not be reported as success. -TARGETS="$(aws ssm list-commands --command-id "${CMD_ID}" \ - --query 'Commands[0].TargetCount' --output text 2>/dev/null || echo 1)" -[ "${TARGETS}" = "1" ] || echo "WARNING: deploy fanned out to ${TARGETS} instances (expected 1)" -AGG="$(aws ssm list-commands --command-id "${CMD_ID}" \ - --query 'Commands[0].Status' --output text 2>/dev/null || echo Failed)" - -echo "==> aggregate deploy status: ${AGG} (across ${TARGETS} target(s))" -[ "${AGG}" = "Success" ] || { echo "ERROR: deploy did not succeed (${AGG})" >&2; exit 1; } diff --git a/.github/scripts/rollback.sh b/.github/scripts/rollback.sh deleted file mode 100755 index 1c277047..00000000 --- a/.github/scripts/rollback.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash -# Roll an env BACK to a prior release: re-point releases/latest/ at a chosen release -# and re-fire the deploy. Run by rollback.yml after aws creds are configured. -# -# ENV dev | prod (required) -# BUCKET open-swe--assets (required) -# DEPLOY_DOC open-swe--deploy (required) -# TARGET_SHA release sha to restore; blank => releases/last-good/ (optional) -# -# releases/latest/ is moved TRANSACTIONALLY (same as the forward deploy): pointed at -# the target, the box rolled, and on a failed roll latest is reverted to whatever it -# was before the rollback attempt — so a failed rollback never leaves latest at a -# release the box could not bring up. releases/last-good/ is left untouched; advance -# it by running a forward deploy. -# -# Reuses the app deploy role's existing releases/* write + tag-scoped ssm:SendCommand -# — no new IAM. The fire/wait/gate is the SAME roll-box.sh the forward deploy uses. -set -euo pipefail -: "${ENV:?}" "${BUCKET:?}" "${DEPLOY_DOC:?}" -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Copy a release prefix into releases/latest/ AND record the resolved sha, so -# releases/latest/sha.txt is always a real commit a later revert can resolve. -point_latest() { # $1 = source prefix (releases/|releases/last-good); $2 = sha to record - local src="$1" sha="$2" f - for f in app.tar.gz spa.tar.gz; do - aws s3 cp "s3://${BUCKET}/${src}/${f}" "s3://${BUCKET}/releases/latest/${f}" - done - printf '%s\n' "${sha}" | aws s3 cp - "s3://${BUCKET}/releases/latest/sha.txt" -} - -if [ -n "${TARGET_SHA:-}" ]; then - SRC="releases/${TARGET_SHA}" - LABEL="${TARGET_SHA}" - RESOLVED_SHA="${TARGET_SHA}" -else - SRC="releases/last-good" - LABEL="last-good" - # resolve last-good's real sha so latest/sha.txt records a commit, not a label. - RESOLVED_SHA="$(aws s3 cp "s3://${BUCKET}/releases/last-good/sha.txt" - 2>/dev/null | tr -d '[:space:]' || true)" - RESOLVED_SHA="${RESOLVED_SHA:-last-good}" -fi -echo "==> rollback ${ENV} to ${LABEL} (s3://${BUCKET}/${SRC}/, sha=${RESOLVED_SHA})" - -# Refuse to roll back to a release that is not fully present. -for f in app.tar.gz spa.tar.gz; do - aws s3 ls "s3://${BUCKET}/${SRC}/${f}" >/dev/null 2>&1 \ - || { echo "ERROR: ${SRC}/${f} not found in s3://${BUCKET} — cannot roll back to ${LABEL}" >&2; exit 1; } -done - -# Capture what latest points at now, so a failed rollback can be reverted. -PREV_SHA="$(aws s3 cp "s3://${BUCKET}/releases/latest/sha.txt" - 2>/dev/null | tr -d '[:space:]' || true)" - -echo "==> point releases/latest/ -> ${SRC} (was ${PREV_SHA:-})" -point_latest "${SRC}" "${RESOLVED_SHA}" - -if ! ROLL_COMMENT="rollback ${ENV} to ${LABEL}" bash "${HERE}/roll-box.sh"; then - if [ -n "${PREV_SHA}" ]; then - echo "!! rollback deploy failed — reverting releases/latest/ -> ${PREV_SHA}" >&2 - point_latest "releases/${PREV_SHA}" "${PREV_SHA}" - fi - exit 1 -fi - -echo "==> ${ENV} rolled back to ${LABEL}" -echo "NOTE: releases/last-good/ is left unchanged; re-run a forward deploy to advance it." diff --git a/.github/workflows/build-artifacts.yml b/.github/workflows/build-artifacts.yml deleted file mode 100644 index 6e295de0..00000000 --- a/.github/workflows/build-artifacts.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Build & publish app artifacts - -# T7 + T19 — build the release (SPA + Python source) and publish it to the per-env -# S3 artifact bucket, then roll the box to it. -# -# push to dev → publish to open-swe-dev-assets → deploy dev box (AUTO) -# push to main → publish to open-swe-prod-assets → deploy prod box (manual approval: env "prod") -# -# Two artifacts (the box's deploy.sh pulls both from releases/latest/): -# spa.tar.gz = the built dashboard SPA (vite -> ui/.output/public). Built HERE -# (not on the box) — the build is memory-heavy and the box is small. -# app.tar.gz = the Python source tree (NO ui/, NO .venv). The box runs -# `uv sync` to build a native-ARM64 venv at the real runtime path. -# -# Each release is uploaded under releases// (immutable, auditable) AND mirrored -# to releases/latest/ (what the box pulls). Then the open-swe--deploy SSM -# document is fired (tag-scoped to project=open-swe,env=) to roll the box. -# -# OIDC subject alignment (matches the per-env app-role trust in infra/lib/config.ts): -# - publish-dev declares NO `environment:` → sub = repo:…:ref:refs/heads/dev -# - publish-prod declares `environment: prod` → sub = repo:…:environment:prod -# (also triggers the prod Environment's required-reviewer approval gate). -# -# Prerequisites: -# - repo variables AWS_DEPLOY_ROLE_APP_DEV / AWS_DEPLOY_ROLE_APP_PROD = the -# githubdeploy-open-swe-app- role ARNs (open-swe-iam CfnOutputs). -# - the open-swe- stack deployed (creates the bucket + the SSM deploy doc). - -permissions: - contents: read - -on: - push: - branches: [dev, main] - paths: - - "agent/**" - - "ui/**" - - "deploy/**" - - "langgraph.json" - - "pyproject.toml" - - "uv.lock" - - ".github/workflows/build-artifacts.yml" - - ".github/scripts/**" - workflow_dispatch: - -concurrency: - # one publish+deploy per branch at a time; never cancel an in-flight release. - group: build-artifacts-${{ github.ref }} - cancel-in-progress: false - -jobs: - publish-dev: - name: Publish + deploy (dev) - if: ${{ github.ref == 'refs/heads/dev' }} - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - id-token: write - contents: read - env: - ENV: dev - BUCKET: open-swe-dev-assets - DEPLOY_DOC: open-swe-dev-deploy - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Build SPA (vite -> ui/.output/public) - working-directory: ui - # vite's bundle exceeds Node's default ~2 GB heap (the build that needed an - # 8 GB swapfile on-box); the runner has ~16 GB, so lift the heap cap. - env: - NODE_OPTIONS: "--max-old-space-size=8192" - run: | - bun install --frozen-lockfile - bun run build - - name: Package artifacts - run: bash .github/scripts/package-artifacts.sh - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_DEV }} - aws-region: us-east-1 - - name: Publish to S3 + roll the box - run: bash .github/scripts/publish-and-deploy.sh - - publish-prod: - name: Publish + deploy (prod) - if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ubuntu-latest - timeout-minutes: 30 - # Manual-approval gate: the "prod" Environment requires a reviewer (Adam). Also - # makes the OIDC sub …:environment:prod (matches the prod app-role trust). - environment: prod - permissions: - id-token: write - contents: read - env: - ENV: prod - BUCKET: open-swe-prod-assets - DEPLOY_DOC: open-swe-prod-deploy - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - name: Build SPA (vite -> ui/.output/public) - working-directory: ui - # vite's bundle exceeds Node's default ~2 GB heap (the build that needed an - # 8 GB swapfile on-box); the runner has ~16 GB, so lift the heap cap. - env: - NODE_OPTIONS: "--max-old-space-size=8192" - run: | - bun install --frozen-lockfile - bun run build - - name: Package artifacts - run: bash .github/scripts/package-artifacts.sh - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_PROD }} - aws-region: us-east-1 - - name: Publish to S3 + roll the box - run: bash .github/scripts/publish-and-deploy.sh diff --git a/.github/workflows/cd-infra.yml b/.github/workflows/cd-infra.yml deleted file mode 100644 index 8e6f4008..00000000 --- a/.github/workflows/cd-infra.yml +++ /dev/null @@ -1,124 +0,0 @@ -name: Infra CD - -# Path-filtered CDK deploy for /infra, per env, OIDC-only (no static keys). -# -# push to dev → CI (tsc+jest+synth) → deploy OpenSweDevStack (AUTO, CI-green-gated) -# push to main → CI → deploy OpenSweProdStack (manual approval: env "prod") -# -# Why this is NOT the reusable cd-cdk.yaml: that workflow runs `cdk deploy --all`, -# which would deploy ALL THREE stacks (incl. the OTHER env + the shared IAM stack) -# from a single-env push — breaking the per-env dev/prod boundary. So we target one -# stack explicitly per env. (Infra CI still uses the reusable ci-typescript-cdk.) -# -# The shared IAM stack (open-swe-iam — owns BOTH envs' OIDC deploy roles) is -# intentionally NOT deployed here: it is a privileged, human-gated apply (T6), so a -# routine dev push can never alter prod's deploy role. -# -# OIDC subject alignment (must match the per-env trust in infra/lib/config.ts): -# - deploy-dev declares NO `environment:` → token sub = repo:…:ref:refs/heads/dev, -# which is exactly what githubdeploy-open-swe-infra-dev trusts. -# - deploy-prod declares `environment: prod` → token sub = repo:…:environment:prod, -# which githubdeploy-open-swe-infra-prod trusts AND which triggers the GitHub -# Environment's required-reviewer (manual approval) gate. -# -# Prerequisites (post-T6, when the roles exist): -# - repo variables AWS_DEPLOY_ROLE_INFRA_DEV / AWS_DEPLOY_ROLE_INFRA_PROD = the -# githubdeploy-open-swe-infra- role ARNs (open-swe-iam CfnOutputs). -# - a GitHub Environment named "prod" with Adam as a required reviewer. - -permissions: - contents: read - -on: - push: - branches: [dev, main] - paths: - - "infra/**" - - ".github/workflows/cd-infra.yml" - workflow_dispatch: - -concurrency: - # one infra deploy per branch at a time; never cancel an in-flight deploy. - group: cd-infra-${{ github.ref }} - cancel-in-progress: false - -jobs: - # CI-green precondition — re-run tsc + jest + synth on the pushed commit before - # any deploy. A failure here blocks the deploy jobs (needs: ci). - ci: - name: Infra CI (pre-deploy) - uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main - with: - node-version: "24" - working-directory: infra - cache-dependency-path: infra/package-lock.json - run-typecheck: true - run-tests: true - run-cdk-synth: true - - deploy-dev: - name: Deploy open-swe-dev - needs: ci - if: ${{ github.ref == 'refs/heads/dev' }} - runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v4 - with: - node-version: "24" - cache: npm - cache-dependency-path: infra/package-lock.json - - name: Install deps - working-directory: infra - run: npm ci - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_INFRA_DEV }} - aws-region: us-east-1 - - name: CDK deploy (dev only) - working-directory: infra - # --outputs-file lets us print the stack outputs from CDK's own result - # (the deploy role intentionally lacks cloudformation:DescribeStacks; CDK - # gets outputs via the bootstrap cfn-exec role it assumes, so no extra grant). - run: npx cdk deploy OpenSweDevStack --require-approval never --outputs-file cdk-outputs.json - - name: Stack outputs - working-directory: infra - run: cat cdk-outputs.json - - deploy-prod: - name: Deploy open-swe-prod - needs: ci - if: ${{ github.ref == 'refs/heads/main' }} - runs-on: ubuntu-latest - timeout-minutes: 30 - # Manual-approval gate: the "prod" Environment requires a reviewer (Adam). - # Also makes the OIDC sub …:environment:prod (matches the prod role trust). - environment: prod - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v4 - with: - node-version: "24" - cache: npm - cache-dependency-path: infra/package-lock.json - - name: Install deps - working-directory: infra - run: npm ci - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_INFRA_PROD }} - aws-region: us-east-1 - - name: CDK deploy (prod only) - working-directory: infra - # See deploy-dev: --outputs-file avoids needing cloudformation:DescribeStacks. - run: npx cdk deploy OpenSweProdStack --require-approval never --outputs-file cdk-outputs.json - - name: Stack outputs - working-directory: infra - run: cat cdk-outputs.json diff --git a/.github/workflows/ci-infra.yml b/.github/workflows/ci-infra.yml deleted file mode 100644 index 0a40c69d..00000000 --- a/.github/workflows/ci-infra.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Infra CI - -# Path-filtered CI for the /infra CDK app (TypeScript). The existing "CI" -# (ci.yml) covers the Python agent; this adds tsc + jest + cdk synth for /infra so -# infra changes are gated on a PR the same way. Runs only when /infra changes. - -permissions: - contents: read - -on: - pull_request: - paths: - - "infra/**" - - ".github/workflows/ci-infra.yml" - -jobs: - infra-ci: - name: Infra CI (tsc + jest + synth) - uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main - with: - node-version: "24" - working-directory: infra - cache-dependency-path: infra/package-lock.json - run-typecheck: true - run-tests: true - run-cdk-synth: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55a35f02..f1a50e35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: - name: Run E2E working-directory: tests/e2e # Playwright's globalSetup runs the real `bun run build`, whose vite bundle - # exceeds Node's default ~2 GB heap (same OOM fixed in build-artifacts.yml). + # exceeds Node's default ~2 GB heap. # The runner has ~16 GB, so lift the heap cap. env: NODE_OPTIONS: "--max-old-space-size=8192" diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml deleted file mode 100644 index 5a4f30ec..00000000 --- a/.github/workflows/rollback.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Rollback (re-point env to a prior release) - -# Roll an env back to a previously published release without rebuilding. Re-points -# releases/latest/ at the chosen release and re-fires the open-swe--deploy SSM -# document — same fire/wait/gate path as a forward deploy (roll-box.sh). -# -# env=dev, sha blank → restore open-swe-dev-assets/releases/last-good/ (AUTO) -# env=prod, sha blank → restore open-swe-prod-assets/releases/last-good/ (manual -# approval: Environment "prod", same gate as a prod deploy) -# sha= → restore that exact releases// instead of last-good. -# -# No new IAM: reuses the githubdeploy-open-swe-app- role's existing releases/* -# write + tag-scoped ssm:SendCommand. OIDC subject alignment matches build-artifacts: -# - rollback-dev declares NO `environment:` → sub = repo:…:ref:refs/heads/ -# - rollback-prod declares `environment: prod` → sub = repo:…:environment:prod - -permissions: - contents: read - -on: - workflow_dispatch: - inputs: - env: - description: "Which environment to roll back" - required: true - type: choice - options: [dev, prod] - sha: - description: "Release SHA to restore (blank = releases/last-good)" - required: false - type: string - -concurrency: - # never overlap a rollback with another rollback/deploy of the same env. - group: rollback-${{ inputs.env }} - cancel-in-progress: false - -jobs: - rollback-dev: - name: Rollback (dev) - if: ${{ inputs.env == 'dev' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - id-token: write - contents: read - env: - ENV: dev - BUCKET: open-swe-dev-assets - DEPLOY_DOC: open-swe-dev-deploy - TARGET_SHA: ${{ inputs.sha }} - steps: - - uses: actions/checkout@v7 - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_DEV }} - aws-region: us-east-1 - - name: Re-point releases/latest + redeploy - run: bash .github/scripts/rollback.sh - - rollback-prod: - name: Rollback (prod) - if: ${{ inputs.env == 'prod' }} - runs-on: ubuntu-latest - timeout-minutes: 20 - # Manual-approval gate: the "prod" Environment requires a reviewer (Adam). Also - # makes the OIDC sub …:environment:prod (matches the prod app-role trust). - environment: prod - permissions: - id-token: write - contents: read - env: - ENV: prod - BUCKET: open-swe-prod-assets - DEPLOY_DOC: open-swe-prod-deploy - TARGET_SHA: ${{ inputs.sha }} - steps: - - uses: actions/checkout@v7 - - uses: aws-actions/configure-aws-credentials@v6 - with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_APP_PROD }} - aws-region: us-east-1 - - name: Re-point releases/latest + redeploy - run: bash .github/scripts/rollback.sh diff --git a/.security-review/suppressions.json b/.security-review/suppressions.json index 9fa487b8..ef384a42 100644 --- a/.security-review/suppressions.json +++ b/.security-review/suppressions.json @@ -10,16 +10,6 @@ "owner": "adam@seahavenind.com", "added": "2026-06-29" }, - { - "id": "OSWE-IAC-SECRETS-LIST-01", - "title": "EC2 instance role grants BatchGetSecretValue on \"*\" (operation-level; secret-NAME existence enumeration account-wide)", - "file": "infra/lib/constructs/instance-role.ts", - "severity": "low", - "status": "confirmed", - "suppression_justification": "ACCEPTED LOW residual, metadata-only. secretsmanager:BatchGetSecretValue is a collection action that AWS cannot scope to a per-secret ARN, so it is granted on `*` (documented in commit 3c69dd9d and the construct comment). Secret VALUES remain strictly gated by the PREFIX-scoped GetSecretValue/DescribeSecret on secret:open-swe-/* (checked per-secret even within the batch), so cross-env VALUE isolation is preserved; only a name EXISTENCE oracle remains, within Sea Haven's single-tenant account 328440206208. ListSecrets is intentionally NOT granted (so name FILTER enumeration AccessDenies). Confirmed by GPT-4.1 IAM cross-review (no BLOCK) and the iac-iam detector (one low residual, no critical/high).", - "owner": "adam@seahavenind.com", - "added": "2026-06-26" - }, { "id": "gitleaks-generic-api-key-89", "title": "Hardcoded credential flagged in encryption-roundtrip test fixture (CWE-798)", diff --git a/README.md b/README.md index 6b77ecff..343bda15 100644 --- a/README.md +++ b/README.md @@ -153,18 +153,20 @@ This is an area where you can extend Open SWE for your org: add deterministic CI ## Deployment (Sea Haven fork) -This fork is **self-hosted on AWS** and live in production. Each env (`dev` / -`prod`) runs the stock `langgraph dev` server (all three graphs + the FastAPI -webapp) bound to loopback `127.0.0.1:2024` on a single ARM64 EC2 box, fronted by -nginx (the sole ingress) behind the shared `seahaven-com` ALB. CDK (`infra/`) -owns the per-env stacks; GitHub Actions handle CDK deploys (`cd-infra.yml`) and -app-artifact releases to S3 rolled onto the box via an SSM document -(`build-artifacts.yml`), with `dev` auto-deploying and `prod` gated behind a -manual GitHub Environment approval. +This fork runs on a **managed deployment**: the backend (all three graphs + the +FastAPI webapp) runs on +[LangGraph Cloud / Platform](https://langchain-ai.github.io/langgraph/cloud/), +and the `ui/` dashboard deploys to [Vercel](https://vercel.com/). Configuration +and secrets live in the LangGraph deployment config and Vercel environment +variables. Promotion from `dev` to `prod` (`main`) is handled by +[`.github/workflows/promote-to-main.yml`](.github/workflows/promote-to-main.yml). -**[`deploy/seahaven/DEPLOYMENT.md`](deploy/seahaven/DEPLOYMENT.md) is the canonical -deploy runbook** — full end-to-end pipeline, config seeding, promotion/rollback, -and live prod facts. CDK specifics live in [`infra/README.md`](infra/README.md). +See **[INSTALLATION.md § 10 "Production deployment"](INSTALLATION.md#10-production-deployment)** +for the full backend + dashboard setup. + +> The earlier self-hosted AWS stack (CDK under `infra/`, an ARM64 EC2 box + nginx +> behind the shared ALB, and the `cd-infra` / `build-artifacts` release pipelines) +> was **decommissioned** in favor of the managed deployment above. ## License diff --git a/deploy/ami/README.md b/deploy/ami/README.md deleted file mode 100644 index e328533b..00000000 --- a/deploy/ami/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# Open SWE base AMI (T8) - -Packer recipe + first-boot user-data for the single EC2 instance per env -(`open-swe-dev` / `open-swe-prod`) in the Open SWE → AWS migration. Builds an -**ARM64 (Graviton) Ubuntu 24.04 LTS** base AMI and provisions the box on first -boot with the stock `langgraph dev` runtime, nginx, and the CloudWatch agent. - -The architecture is locked in the repo `TODO.md` ("Architecture (locked)"): ONE -EC2 ARM64 (~t4g.large) instance per env, `seahaven-vpc` **private subnet + NAT**, -inbound **only from the ALB SG**. Runtime is **stock `langgraph dev`** (in-memory -store, `--no-reload`) + nginx + systemd. The box has **no git auth** — it pulls -its deploy artifact from S3 via the instance role. The SPA build runs in GitHub -Actions (T7), **not** on the box, so the old 8 GB-swapfile OOM hack is gone. - -## Files - -| Path | Purpose | -|---|---| -| `open-swe-base.pkr.hcl` | Packer template (HCL2). Latest Canonical 24.04 arm64 source → base AMI. | -| `scripts/provision.sh` | Packer provisioner. System packages, uv+py3.12, node+bun, service user, stages templates. | -| `user-data.sh` | First-boot provisioning (S3 artifact pull, render templates, CW agent, start services). | -| `templates/open-swe.service` | systemd unit TEMPLATE (`@@tokens@@` rendered at boot). | -| `templates/open-swe.nginx.conf` | nginx site TEMPLATE (dashboard SPA + scoped `/dashboard/api/` proxy). | -| `templates/amazon-cloudwatch-agent.json` | CW agent config TEMPLATE — **30-day log retention**. | - -`deploy/seahaven/fetch-config.sh` and `deploy/seahaven/seed_store.sh` are owned by -the parallel T10 work and ship **inside the app artifact**; this AMI wires them in -but does not author them (see "Integration contract" below). - -## Build the AMI - -```bash -cd deploy/ami -packer init . -packer fmt -check . -packer validate -var aws_region=us-east-1 open-swe-base.pkr.hcl -packer build open-swe-base.pkr.hcl -``` - -Builds in account **328440206208 / us-east-1**. Source = latest Canonical Ubuntu -24.04 (Noble) **arm64** AMI (`source_ami_filter`, owner `099720109477`). Build host -is `t4g.medium` (ARM64). The output AMI is tagged: - -``` -Name=open-swe-base-arm64 Purpose=open-swe-runtime-base ManagedBy=packer -``` - -Pinned versions live in the template `variable` defaults (`uv_version`, -`python_version`, `node_major`, the CW-agent / awscli URLs) and the -`required_plugins` block (`amazon` 1.3.6) — bump deliberately. - -## AMI → `cdk.context.json` pinning contract - -The CDK stacks in `/infra` (owned by T3/T12) consume the AMI **by id, pinned in the -committed `infra/cdk.context.json`** — they never resolve "latest" at synth time. -This is the EBS/AMI-fix discipline: an uncached `MachineImage.lookup` resolves a new -AMI on every deploy and silently triggers instance replacement. - -Contract (CDK side does the wiring; this is the handshake): - -1. `packer build` prints the new AMI id (and tags it `open-swe-base-arm64`). -2. CDK looks the AMI up with **`cachedInContext: true`** (e.g. - `MachineImage.lookup({ name: "open-swe-base-arm64-*", owners: ["328440206208"], cachedInContext: true })`), - which writes the resolved id into `infra/cdk.context.json`. -3. **`infra/cdk.context.json` is committed.** From then on every synth/deploy uses - the pinned id — no surprise replacement when a newer AMI exists. -4. To adopt a new AMI: `cdk context --reset ` (or edit the pinned - value), commit the change, and review the cdk-diff — the PR will show - "requires replacement", which is the intended, visible signal. - -Record the built AMI id in project memory (`project_open_swe_migration`) per the -"memory updated for AMI id" build criterion. - -## `userDataCausesReplacement` rationale - -`user-data.sh` is **provisioning-only** — it runs once at first boot and never -carries durable runtime config. CDK sets **`userDataCausesReplacement: true`** so -that any change to it is a deliberate, diff-visible instance replacement rather than -a no-op edit that drifts from the running box. Durable runtime config is fetched -**fresh on every service start** by `fetch-config.sh` (ExecStartPre) — changing a -secret or SSM value needs only a `systemctl restart open-swe.service`, not a -replacement. - -## EBS discipline (binding — `feedback_inline_ebs_volumes`) - -**The box holds no durable state of its own:** - -| State | Lives in | On replacement | -|---|---|---| -| secrets / config | Secrets Manager + SSM → tmpfs `.env` | re-fetched at boot | -| app code + SPA | S3 `open-swe--assets` | re-pulled at boot | -| store (team_settings, user_mappings) | reseeded by `seed_store.sh` | re-seeded at boot | -| logs | CloudWatch (30-day) — **not** a CFN resource in the stack | survive replacement | - -→ **No local-only durable state ⇒ no standalone RETAIN volume is needed.** The root -volume is disposable; there is intentionally no inline data `blockDevices` to lose. - -**Even so, snapshot before any replacing deploy.** Per the operational guard, before -merging/deploying any change that REPLACES the instance (`userDataCausesReplacement`, -AMI bump, instance-type change): - -1. Enumerate the instance's volumes and assert **"no local-only durable state"** - (the table above is the checklist). -2. Take an **EBS snapshot of the root volume and WAIT for `state=completed`** before - letting the deploy proceed. Keep it as insurance; delete after a grace period. -3. Confirm the CloudWatch log groups are **not** CFN-managed in the stack so history - survives; re-verify history after the new instance is healthy. - -cdk-diff-on-PR must flag "requires replacement" at review time (T8/T12 acceptance -criterion). This is the enforced version — not just an assertion in the runbook. - -## Integration contract (T10 — `fetch-config.sh` + `seed_store.sh`) - -Both ship in the app artifact under `deploy/seahaven/` and are wired into the unit: - -- **`fetch-config.sh`** (ExecStartPre, runs as `openswe`): reads `/etc/open-swe/boot.env` - (`OPENSWE_ENV`, `AWS_REGION`, `SECRETS_PREFIX=open-swe-`, `SSM_PREFIX=/open-swe-`, - `ENV_FILE=/run/open-swe/.env`), pulls Secrets Manager `open-swe-/*` + SSM - `/open-swe-/*`, and writes: - - `/run/open-swe/.env` (**0600, tmpfs**, secret-bearing app env incl. the multiline - GitHub App PEM) — loaded by langgraph/dotenv via the `${APP_DIR}/.env` symlink. - - `/run/open-swe/seed.env` (**0600, tmpfs**, simple `OPENSWE_*` vars only: - `OPENSWE_DEFAULT_REPO`, `OPENSWE_OWNER_LOGIN`, `OPENSWE_OWNER_EMAIL`, model ids) — - loaded by systemd `EnvironmentFile` so `seed_store.sh` (ExecStartPost) has them. - - It must **fail-fast** (non-zero exit) if any required value is missing, so the - unit never starts half-configured. -- **`seed_store.sh`** (ExecStartPost): existing script, reseeds `team_settings/default` - + `user_mappings/` into the in-memory store after each start. - -## Smoke-boot checklist (after first boot) - -SSM Session Manager onto the instance (no public SSH — private subnet) and verify: - -- [ ] `cloud-init status --wait` → `done`; `/var/log/open-swe-user-data.log` ends with - "user-data done" and shows the S3 pulls + service starts. -- [ ] `systemctl is-active open-swe.service` → `active`. (If it failed, check - `ExecStartPre`/`fetch-config.sh` — fail-fast means missing config = failed unit.) -- [ ] **fetch-config fail-fast works:** `/run/open-swe/.env` exists, owner `openswe`, - mode `0600`, on tmpfs (`findmnt /run/open-swe`); `seed.env` present. -- [ ] `curl -fsS http://127.0.0.1:2024/ok` → `200` (raw LangGraph health). -- [ ] `systemctl is-active nginx` → `active`; `curl -fsS http://127.0.0.1/healthz` → - `200`; `curl -s http://127.0.0.1/threads` returns the SPA shell, **not** JSON - (proves the agent API is not proxied — the security boundary holds). -- [ ] `seed_store: done` in the journal / app.log (store reseeded). -- [ ] CloudWatch: log groups `/open-swe//{app,user-data,nginx-access,nginx-error}` - exist with **30-day** retention and are receiving events. -- [ ] **No swapfile** (`swapon --show` empty) — the on-box SPA build is gone. -- [ ] From the ALB only: dashboard host serves the SPA; `hooks` host reaches - `/webhooks/*` on :2024 and nothing else (raw API paths hit the ALB default, not - the box). - -## Assumptions - -- **Artifact bucket** `open-swe--assets` (T7), with objects - `${ARTIFACT_PREFIX}/app.tar.gz` (Python app incl. `deploy/seahaven/` and a prebuilt - arm64 `.venv`) and `${ARTIFACT_PREFIX}/spa.tar.gz` (built SPA → `/var/www/open-swe`). - `ARTIFACT_PREFIX` defaults to `releases/latest`; CDK renders the concrete value. -- **Instance role** (defined in `/infra`, least-privilege per T4/T12) grants: - `s3:GetObject` on `open-swe--assets/*`; `secretsmanager:GetSecretValue` on - `open-swe-/*`; `ssm:GetParameter(s)`/`GetParametersByPath` on `/open-swe-/*`; - `logs:*` for the CW agent log groups + `cloudwatch:PutMetricData`; SSM Session - Manager (`ssm:UpdateInstanceInformation`, `ssmmessages:*`) for shell access. -- **CDK substitutes** the `@@OPENSWE_ENV@@`, `@@ASSETS_BUCKET@@`, `@@SERVER_NAME@@`, - `@@ARTIFACT_PREFIX@@` tokens in `user-data.sh` when rendering the launch template. -- `:2024` binds `0.0.0.0` so the ALB hooks target group can reach `/webhooks/*`; it is - reachable only from the ALB SG (private subnet, SG-scoped inbound). The raw API is - never internet-exposed — the ALB hooks rule is path-scoped to `/webhooks/*`. diff --git a/deploy/ami/deploy.sh b/deploy/ami/deploy.sh deleted file mode 100755 index 2839503c..00000000 --- a/deploy/ami/deploy.sh +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -# Open SWE app deploy — RE-RUNNABLE (first boot + every subsequent release). -# -# Pulls the current release from S3 (open-swe--assets), builds the venv -# natively on the box, and restarts the service. This is the SINGLE source of the -# app-deploy procedure; it runs in two places: -# -# 1. first boot — user-data.sh decodes this script to /opt/open-swe/bin and -# calls it ONCE (non-fatal: if no release is published yet, -# nginx is already up and the box waits for the first deploy). -# 2. every release — the `open-swe--deploy` SSM document (CI fires it after -# uploading app.tar.gz / spa.tar.gz) runs this same script. -# -# It deploys CODE + STATIC ASSETS only. Secrets/config are NOT fetched here: the -# systemd unit's ExecStartPre=fetch-config.sh materializes the tmpfs .env on every -# (re)start, fail-fast — so `systemctl restart` below is what reloads config too. -# -# Contract: -# app.tar.gz = the Python source tree (pyproject.toml + uv.lock + agent/ + -# deploy/ + langgraph.json + README.md, NO ui/, NO .venv). The venv -# is built HERE with `uv sync` so it is native ARM64 and lives at -# the real runtime path (no cross-built / non-relocatable venv). -# spa.tar.gz = the built dashboard SPA (vite output: _shell.html + assets), -# extracted to the nginx web root. -set -euo pipefail -exec > >(tee -a /var/log/open-swe/deploy.log) 2>&1 -echo "==> open-swe deploy start $(date -u +%FT%TZ)" - -# Non-secret pointers written by user-data.sh (env, region, bucket, artifact prefix). -# shellcheck disable=SC1091 -. /etc/open-swe/boot.env -export AWS_DEFAULT_REGION="${AWS_REGION:?boot.env missing AWS_REGION}" -: "${ASSETS_BUCKET:?boot.env missing ASSETS_BUCKET}" -: "${ARTIFACT_PREFIX:?boot.env missing ARTIFACT_PREFIX}" - -# Fixed layout — must match provision.sh + user-data.sh + the templates. -SERVICE_USER="openswe" -APP_DIR="/opt/open-swe/app" -WWW_ROOT="/var/www/open-swe" -ENV_FILE="/run/open-swe/.env" -UV_BIN="/usr/local/bin/uv" -UV_PYTHON_INSTALL_DIR="/opt/uv/python" # where provision.sh pre-installed py3.12 -SERVICE_HOME="/opt/open-swe" - -# Benign-vs-failure distinction: on a brand-new env no release is published yet. -# Treat "app.tar.gz absent in S3" as a benign no-op (exit 0) so first boot is not a -# scary failure; ONCE a release exists, any later step failing is loud (set -e). -if ! aws s3 ls "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/app.tar.gz" >/dev/null 2>&1; then - echo "==> no release published at s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/ yet — nothing to deploy" - exit 0 -fi - -echo "==> pull release from s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/" -tmp="$(mktemp -d)" -trap 'rm -rf "$tmp"' EXIT -aws s3 cp "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/app.tar.gz" "${tmp}/app.tar.gz" -aws s3 cp "s3://${ASSETS_BUCKET}/${ARTIFACT_PREFIX}/spa.tar.gz" "${tmp}/spa.tar.gz" - -# Replace app source + SPA atomically-ish: clear the dirs (drops files removed in -# this release) then extract. The venv is rebuilt below, so wiping .venv too is -# fine — uv's cache (in the service home) makes the rebuild fast. -# -# Hardening: deploy.sh runs as root, so extract with --no-same-owner -# --no-same-permissions — files take root:root + umask perms (NOT the archive's -# uid/mode), so a tarball cannot land a setuid/setgid binary or a foreign-owned -# file; the chown -R below then hands the tree to the service user. (GNU tar also -# refuses `..`-escaping members by default.) Defense-in-depth: the only writer of -# this bucket is the CI OIDC app role, but the box never trusts the archive's -# ownership/mode regardless. -echo "==> install app source -> ${APP_DIR}" -install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$APP_DIR" "$WWW_ROOT" -find "$APP_DIR" -mindepth 1 -delete -find "$WWW_ROOT" -mindepth 1 -delete -tar --no-same-owner --no-same-permissions -xzf "${tmp}/app.tar.gz" -C "$APP_DIR" -tar --no-same-owner --no-same-permissions -xzf "${tmp}/spa.tar.gz" -C "$WWW_ROOT" -# langgraph reads ./.env from WorkingDirectory; point it at the tmpfs file the -# systemd ExecStartPre materializes. -ln -sfn "$ENV_FILE" "${APP_DIR}/.env" -chown -R "$SERVICE_USER":"$SERVICE_USER" "$APP_DIR" "$WWW_ROOT" - -echo "==> build venv natively (uv sync --frozen --no-dev)" -# Run as the service user so the venv + uv cache are owned by it. Pin the -# pre-baked interpreter dir so uv never reaches out to download Python at deploy. -cd "$APP_DIR" -sudo -u "$SERVICE_USER" env \ - HOME="$SERVICE_HOME" \ - UV_PYTHON_INSTALL_DIR="$UV_PYTHON_INSTALL_DIR" \ - UV_CACHE_DIR="${SERVICE_HOME}/.cache/uv" \ - "$UV_BIN" sync --frozen --no-dev - -echo "==> restart open-swe.service + reload nginx" -# ExecStartPre=fetch-config.sh fails-fast if secrets/config are missing, so a -# restart here surfaces a bad config as a failed unit (non-zero exit below). -systemctl restart open-swe.service -nginx -t && systemctl reload nginx - -if systemctl is-active --quiet open-swe.service; then - echo "==> open-swe deploy OK $(date -u +%FT%TZ)" -else - echo "!! open-swe.service is not active after deploy (check fetch-config/secrets)" - exit 1 -fi diff --git a/deploy/ami/open-swe-base.pkr.hcl b/deploy/ami/open-swe-base.pkr.hcl deleted file mode 100644 index 0c37d72a..00000000 --- a/deploy/ami/open-swe-base.pkr.hcl +++ /dev/null @@ -1,143 +0,0 @@ -# Open SWE base AMI — ARM64 (Graviton) Ubuntu 24.04 LTS. -# -# Builds the immutable base image for the single EC2 instance per env -# (open-swe-dev / open-swe-prod) in seahaven-vpc. The image bakes the runtime -# (uv + Python 3.12, nginx, awscli v2, CloudWatch agent) and the service-user / -# systemd / nginx TEMPLATES. It bakes NO secrets and NO env-specific values — -# those are materialized at first boot by user-data + deploy/seahaven/fetch-config.sh -# (Secrets Manager + SSM -> root-only tmpfs .env, fail-fast). -# -# Build: packer init . && packer build open-swe-base.pkr.hcl -# The resulting AMI id is pinned in infra/cdk.context.json (CDK cachedInContext:true); -# see README.md "AMI -> cdk.context.json pinning contract". - -packer { - required_version = ">= 1.11.0, < 2.0.0" - required_plugins { - amazon = { - source = "github.com/hashicorp/amazon" - version = "1.3.6" - } - } -} - -variable "aws_region" { - type = string - default = "us-east-1" -} - -variable "instance_type" { - type = string - default = "t4g.medium" # ARM64 (Graviton) build host; runtime instances are ~t4g.large -} - -variable "ami_name_prefix" { - type = string - default = "open-swe-base-arm64" -} - -# Versions baked into the image. Pin and bump deliberately. -variable "python_version" { - type = string - default = "3.12" -} - -variable "node_major" { - type = string - default = "24" -} - -variable "uv_version" { - type = string - default = "0.11.24" -} - -variable "cloudwatch_agent_deb_url" { - type = string - default = "https://amazoncloudwatch-agent.s3.amazonaws.com/ubuntu/arm64/latest/amazon-cloudwatch-agent.deb" -} - -variable "awscli_zip_url" { - type = string - default = "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -} - -locals { - timestamp = formatdate("YYYYMMDD-hhmmss", timestamp()) -} - -# Latest Canonical Ubuntu 24.04 (Noble) arm64 server image. -source "amazon-ebs" "open-swe" { - region = var.aws_region - instance_type = var.instance_type - ssh_username = "ubuntu" - - ami_name = "${var.ami_name_prefix}-${local.timestamp}" - # ASCII only — AWS rejects non-ASCII in the AMI Description attribute. - ami_description = "Open SWE base - Ubuntu 24.04 arm64 + uv/py3.12 + nginx + CW agent (templates only, no secrets)" - - source_ami_filter { - filters = { - name = "ubuntu/images/hvm-ssd*/ubuntu-noble-24.04-arm64-server-*" - architecture = "arm64" - root-device-type = "ebs" - virtualization-type = "hvm" - } - owners = ["099720109477"] # Canonical - most_recent = true - } - - # IMDSv2 required on the build host. - metadata_options { - http_endpoint = "enabled" - http_tokens = "required" - http_put_response_hop_limit = 1 - } - - # gp3 root, encrypted. Runtime root size is set by CDK; this is just the build host. - launch_block_device_mappings { - device_name = "/dev/sda1" - volume_size = 20 - volume_type = "gp3" - encrypted = true - delete_on_termination = true - } - - tags = { - Name = "open-swe-base-arm64" - Purpose = "open-swe-runtime-base" - ManagedBy = "packer" - } -} - -build { - name = "open-swe-base" - sources = ["source.amazon-ebs.open-swe"] - - # Stage the boot-time templates into the image. The destination dir must exist - # BEFORE a trailing-slash (contents-only) file upload — packer's file provisioner - # does not create it, and uploading the directory itself trips scp ("Is a - # directory"). So mkdir first, then upload the contents into it. - provisioner "shell" { - inline = ["mkdir -p /tmp/open-swe-templates"] - } - - provisioner "file" { - source = "${path.root}/templates/" - destination = "/tmp/open-swe-templates" - } - - provisioner "shell" { - environment_vars = [ - "PYTHON_VERSION=${var.python_version}", - "NODE_MAJOR=${var.node_major}", - "UV_VERSION=${var.uv_version}", - "CLOUDWATCH_AGENT_DEB_URL=${var.cloudwatch_agent_deb_url}", - "AWSCLI_ZIP_URL=${var.awscli_zip_url}", - ] - # {{ .Vars }} MUST be included or the environment_vars above never reach the - # script (provision.sh runs under `set -u` and fails on the first reference). - execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E bash '{{ .Path }}'" - script = "${path.root}/scripts/provision.sh" - } -} diff --git a/deploy/ami/scripts/provision.sh b/deploy/ami/scripts/provision.sh deleted file mode 100755 index b64a2043..00000000 --- a/deploy/ami/scripts/provision.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# Packer provisioner for the Open SWE base AMI (ARM64 Ubuntu 24.04). -# -# Bakes the runtime + boot-time templates ONLY. No secrets, no env-specific -# values. Everything env-specific is materialized at first boot by user-data.sh -# + deploy/seahaven/fetch-config.sh. -set -euo pipefail - -PYTHON_VERSION="${PYTHON_VERSION:-3.12}" -NODE_MAJOR="${NODE_MAJOR:-24}" -UV_VERSION="${UV_VERSION:-0.11.24}" -CLOUDWATCH_AGENT_DEB_URL="${CLOUDWATCH_AGENT_DEB_URL:?}" -AWSCLI_ZIP_URL="${AWSCLI_ZIP_URL:?}" - -# Layout (must match user-data.sh and the templates). -SERVICE_USER="openswe" -APP_DIR="/opt/open-swe/app" -SERVICE_HOME="/opt/open-swe" -WWW_ROOT="/var/www/open-swe" -TEMPLATE_DIR="/opt/open-swe/templates" -LOG_DIR="/var/log/open-swe" -UV_BIN="/usr/local/bin/uv" - -export DEBIAN_FRONTEND=noninteractive - -echo "==> apt base packages" -apt-get update -y -apt-get upgrade -y -apt-get install -y --no-install-recommends \ - nginx jq curl unzip ca-certificates gnupg lsb-release \ - build-essential pkg-config git acl - -echo "==> awscli v2 (aarch64)" -tmp="$(mktemp -d)" -curl -fsSL "$AWSCLI_ZIP_URL" -o "$tmp/awscliv2.zip" -unzip -q "$tmp/awscliv2.zip" -d "$tmp" -"$tmp/aws/install" --update -rm -rf "$tmp" -aws --version - -echo "==> CloudWatch agent (arm64)" -tmp="$(mktemp -d)" -curl -fsSL "$CLOUDWATCH_AGENT_DEB_URL" -o "$tmp/amazon-cloudwatch-agent.deb" -dpkg -i -E "$tmp/amazon-cloudwatch-agent.deb" -rm -rf "$tmp" -# Do NOT enable/start the agent during the build; user-data fetches its config -# (with env-specific log-group names + 30-day retention) and starts it at boot. -systemctl disable amazon-cloudwatch-agent.service || true - -echo "==> uv ${UV_VERSION} + Python ${PYTHON_VERSION} (system-wide)" -export UV_INSTALL_DIR=/usr/local/bin -curl -fsSL "https://astral.sh/uv/${UV_VERSION}/install.sh" | env UV_NO_MODIFY_PATH=1 sh -"$UV_BIN" --version -# Pre-install the interpreter so the box never reaches out at boot to build a venv. -UV_PYTHON_INSTALL_DIR=/opt/uv/python "$UV_BIN" python install "$PYTHON_VERSION" - -echo "==> node ${NODE_MAJOR} + bun (build-time UI tooling only; the SPA is built in CI)" -curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - -apt-get install -y --no-install-recommends nodejs -node --version -# bun installed system-wide; used only if any UI tooling must run on-box. The -# production SPA build runs in GitHub Actions -> S3 (no on-box build, no swapfile). -export BUN_INSTALL=/usr/local -curl -fsSL https://bun.sh/install | bash -/usr/local/bin/bun --version || true - -echo "==> non-login service user '${SERVICE_USER}'" -if ! id "$SERVICE_USER" >/dev/null 2>&1; then - useradd --system --create-home --home-dir "$SERVICE_HOME" \ - --shell /usr/sbin/nologin "$SERVICE_USER" -fi - -echo "==> directories" -install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$SERVICE_HOME" "$APP_DIR" -install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0755 "$WWW_ROOT" -install -d -o "$SERVICE_USER" -g "$SERVICE_USER" -m 0750 "$LOG_DIR" -install -d -o root -g root -m 0755 "$TEMPLATE_DIR" - -echo "==> stage boot-time templates into the image" -cp /tmp/open-swe-templates/* "$TEMPLATE_DIR/" -chown root:root "$TEMPLATE_DIR"/* -chmod 0644 "$TEMPLATE_DIR"/* -rm -rf /tmp/open-swe-templates - -echo "==> tmpfs for the runtime .env (root/owner-only, noexec/nosuid/nodev)" -# /run is already tmpfs on Ubuntu; this is an explicit, deliberately-small mount -# scoped to the service user so the materialized .env never touches disk. -if ! grep -q '/run/open-swe' /etc/fstab; then - cat >>/etc/fstab < disable nginx default site (open-swe site is installed at boot)" -rm -f /etc/nginx/sites-enabled/default -systemctl enable nginx - -echo "==> harden: no password auth, IMDSv2 already enforced by launch template" -# (sshd is not exposed publicly — instance is in a private subnet, SG inbound = ALB only.) - -echo "==> clean apt caches" -apt-get clean -rm -rf /var/lib/apt/lists/* - -echo "==> provision complete" diff --git a/deploy/ami/templates/amazon-cloudwatch-agent.json b/deploy/ami/templates/amazon-cloudwatch-agent.json deleted file mode 100644 index 079450cc..00000000 --- a/deploy/ami/templates/amazon-cloudwatch-agent.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "agent": { - "metrics_collection_interval": 60, - "run_as_user": "root" - }, - "metrics": { - "namespace": "open-swe/@@OPENSWE_ENV@@", - "append_dimensions": { - "InstanceId": "${aws:InstanceId}" - }, - "metrics_collected": { - "mem": { "measurement": ["mem_used_percent"] }, - "disk": { - "measurement": ["used_percent"], - "resources": ["/"] - } - } - }, - "logs": { - "logs_collected": { - "files": { - "collect_list": [ - { - "file_path": "/var/log/open-swe/app.log", - "log_group_name": "/open-swe/@@OPENSWE_ENV@@/app", - "log_stream_name": "{instance_id}", - "retention_in_days": 30, - "timezone": "UTC" - }, - { - "file_path": "/var/log/open-swe-user-data.log", - "log_group_name": "/open-swe/@@OPENSWE_ENV@@/user-data", - "log_stream_name": "{instance_id}", - "retention_in_days": 30, - "timezone": "UTC" - }, - { - "file_path": "/var/log/nginx/access.log", - "log_group_name": "/open-swe/@@OPENSWE_ENV@@/nginx-access", - "log_stream_name": "{instance_id}", - "retention_in_days": 30, - "timezone": "UTC" - }, - { - "file_path": "/var/log/nginx/error.log", - "log_group_name": "/open-swe/@@OPENSWE_ENV@@/nginx-error", - "log_stream_name": "{instance_id}", - "retention_in_days": 30, - "timezone": "UTC" - } - ] - } - } - } -} diff --git a/deploy/ami/templates/open-swe.nginx.conf b/deploy/ami/templates/open-swe.nginx.conf deleted file mode 100644 index 5a01ae1a..00000000 --- a/deploy/ami/templates/open-swe.nginx.conf +++ /dev/null @@ -1,62 +0,0 @@ -# Open SWE dashboard frontend (TanStack Start SPA) + scoped API proxy. -# TEMPLATE: tokens (@@...@@) are rendered at first boot by user-data.sh. -# -# nginx is the SOLE ingress and security boundary (T5 OSWE-IAC-03): the backend -# binds 127.0.0.1:2024 and is NOT network-reachable. nginx proxies exactly two -# prefixes to it — /dashboard/api/* and /webhooks/* — and nothing else. The -# unauthenticated LangGraph agent API (/threads, /runs, /assistants, /store) is -# NEVER proxied; those paths return the SPA shell. -# -# Both ALB target groups (dashboard host + hooks host) point at this nginx :80, -# not at :2024 directly, so there is no path to the raw control plane even from -# inside the SG. Webhook signature verification still happens in the app (the raw -# body + GitHub/Slack/Linear signature headers are passed through unmodified). -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name @@SERVER_NAME@@; - - root @@WWW_ROOT@@; - index _shell.html; - - # ALB target-group health check (dashboard TG). - location = /healthz { default_type text/plain; return 200 "ok\n"; } - - # Dashboard API + OAuth callback -> backend webapp. - location /dashboard/api/ { - proxy_pass http://@@BACKEND_ADDR@@; - proxy_http_version 1.1; - client_max_body_size 10m; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 300s; - } - - # Inbound webhooks (GitHub/Slack/Linear) -> backend webapp. Routed through - # nginx so :2024 stays loopback-only (T5 OSWE-IAC-03). The raw request body + - # signature headers pass through unmodified for in-app signature verification. - location /webhooks/ { - proxy_pass http://@@BACKEND_ADDR@@; - proxy_http_version 1.1; - # GitHub permits webhook payloads up to 25 MB; nginx's 1 MB default would - # 413 large push/PR events at the edge BEFORE in-app signature verification - # runs, silently dropping them (OSWE-T12-01). proxy_request_buffering off - # does not relax the size cap — set it explicitly. - client_max_body_size 25m; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; - proxy_request_buffering off; - proxy_read_timeout 300s; - } - - # Static assets + SPA shell fallback (client-side routing). - location / { - try_files $uri $uri/ /_shell.html; - } -} diff --git a/deploy/ami/templates/open-swe.service b/deploy/ami/templates/open-swe.service deleted file mode 100644 index f3fd8b64..00000000 --- a/deploy/ami/templates/open-swe.service +++ /dev/null @@ -1,54 +0,0 @@ -# Open SWE — stock LangGraph dev server (3+ graphs + FastAPI webapp, :2024). -# TEMPLATE: tokens (@@...@@) are rendered at first boot by user-data.sh. -# In-memory runtime (--no-reload) + ExecStartPost reseed; no Aegra/Postgres. -# -# Boot contract: -# ExecStartPre = fetch-config.sh -> runs as root (`+`) ONLY to materialize -# the SERVICE-USER-owned tmpfs .env (@@ENV_FILE@@) from Secrets -# Manager + SSM and chown it to @@SERVICE_USER@@, fail-fast (the -# unit does NOT start if config can't be fetched). -# ExecStart = langgraph dev (as @@SERVICE_USER@@, bound to 127.0.0.1 — nginx -# is the sole ingress; never binds 0.0.0.0). -# ExecStartPost = seed_store.sh -> reseeds team_settings + user_mappings -# that the in-memory store loses on every restart. -[Unit] -Description=Open SWE stock LangGraph dev server (graphs + webapp, :@@PORT@@) -After=network-online.target -Wants=network-online.target -RequiresMountsFor=/run/open-swe - -[Service] -Type=simple -User=@@SERVICE_USER@@ -Group=@@SERVICE_USER@@ -WorkingDirectory=@@APP_DIR@@ - -# No EnvironmentFile: the secret-bearing app .env (@@ENV_FILE@@) is loaded by -# langgraph/dotenv (so the multiline GitHub App PEM never hits systemd's env -# parser), and seed_store.sh reads the same .env directly (without sourcing it). -# -# ExecStartPre runs as root (`+`) so it can chown the tmpfs .env to the service -# user; the env arg (@@OPENSWE_ENV@@) selects the SSM/Secrets prefix (T5 BOOT-01). -ExecStartPre=+@@FETCH_CONFIG@@ @@OPENSWE_ENV@@ -# Bind 127.0.0.1 only — nginx proxies dashboard + webhooks; :@@PORT@@ is never -# directly network-reachable (T5 OSWE-IAC-03). -ExecStart=@@VENV@@/bin/langgraph dev --host 127.0.0.1 --port @@PORT@@ --no-browser --no-reload -ExecStartPost=@@SEED_STORE@@ @@OPENSWE_ENV@@ - -# App logs to a file CloudWatch collects (30-day retention set in the CW config). -StandardOutput=append:/var/log/open-swe/app.log -StandardError=append:/var/log/open-swe/app.log - -Restart=on-failure -RestartSec=5 -TimeoutStartSec=180 - -# Hardening — the box holds no durable state of its own. -NoNewPrivileges=true -ProtectSystem=full -ProtectHome=true -PrivateTmp=true -ReadWritePaths=/var/log/open-swe /var/www/open-swe /run/open-swe @@APP_DIR@@ - -[Install] -WantedBy=multi-user.target diff --git a/deploy/ami/user-data.sh b/deploy/ami/user-data.sh deleted file mode 100755 index 82bc503f..00000000 --- a/deploy/ami/user-data.sh +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env bash -# Open SWE EC2 user-data — PROVISIONING-ONLY (runs once, at first boot). -# -# This is the rationale for `userDataCausesReplacement: true` in CDK: user-data -# does FIRST-BOOT provisioning, never durable runtime config. Editing it is a -# deliberate instance replacement. Durable runtime config is fetched fresh on -# every service start by deploy/seahaven/fetch-config.sh (ExecStartPre). -# -# The box holds NO durable state of its own: -# - secrets/config -> Secrets Manager + SSM, materialized to a tmpfs .env at boot -# - app artifact -> pulled from S3 (open-swe--assets) via the instance role -# - store state -> reseeded by seed_store.sh (ExecStartPost) on every start -# => there is no RETAIN volume to protect; replacement is tolerated. The EBS -# discipline (snapshot root + wait state=completed BEFORE any replacing deploy) -# is the safety net, not durable on-box state. See README "EBS discipline". -# -# Tokens (@@...@@) are substituted by CDK when it renders this script into the -# launch template. region is read from IMDSv2 as a fallback. - -set -euo pipefail -exec > >(tee -a /var/log/open-swe-user-data.log) 2>&1 -echo "==> open-swe user-data start $(date -u +%FT%TZ)" - -# --- CDK-rendered values ----------------------------------------------------- -# NOTE: CDK-substituted tokens use %%...%% (rendered by app-service.ts), DISTINCT -# from the @@...@@ tokens this script seds into the baked systemd/nginx templates. -# The two MUST NOT share a delimiter: a shared @@OPENSWE_ENV@@ / @@SERVER_NAME@@ -# let CDK clobber the sed PATTERN, leaving the unit's token unsubstituted. -OPENSWE_ENV="%%OPENSWE_ENV%%" # dev | prod -ASSETS_BUCKET="%%ASSETS_BUCKET%%" # open-swe--assets -SERVER_NAME="%%SERVER_NAME%%" # openswe[-dev].seahaven.com -ARTIFACT_PREFIX="%%ARTIFACT_PREFIX%%" # e.g. releases/latest - -# --- fixed layout (must match provision.sh + templates) ---------------------- -SERVICE_USER="openswe" -APP_DIR="/opt/open-swe/app" -VENV="${APP_DIR}/.venv" -WWW_ROOT="/var/www/open-swe" -TEMPLATE_DIR="/opt/open-swe/templates" -ENV_FILE="/run/open-swe/.env" -PORT="2024" -FETCH_CONFIG="${APP_DIR}/deploy/seahaven/fetch-config.sh" -SEED_STORE="${APP_DIR}/deploy/seahaven/seed_store.sh" - -# region from IMDSv2 -TOKEN="$(curl -fsS -X PUT "http://169.254.169.254/latest/api/token" \ - -H "X-aws-ec2-metadata-token-ttl-seconds: 300" || true)" -AWS_REGION="$(curl -fsS -H "X-aws-ec2-metadata-token: ${TOKEN}" \ - http://169.254.169.254/latest/meta-data/placement/region || echo us-east-1)" -export AWS_DEFAULT_REGION="$AWS_REGION" -echo "env=${OPENSWE_ENV} region=${AWS_REGION} bucket=${ASSETS_BUCKET} host=${SERVER_NAME}" - -# --- boot.env: non-secret pointers fetch-config.sh reads --------------------- -install -d -o root -g root -m 0755 /etc/open-swe -cat >/etc/open-swe/boot.env <-deploy` SSM document runs this same script -# for every subsequent release. -echo "==> install /opt/open-swe/bin/deploy.sh" -install -d -o root -g root -m 0755 /opt/open-swe/bin -base64 -d >/opt/open-swe/bin/deploy.sh <<'DEPLOY_SH_B64' -%%DEPLOY_SH_B64%% -DEPLOY_SH_B64 -chmod 0755 /opt/open-swe/bin/deploy.sh - -# --- render + install the systemd unit --------------------------------------- -echo "==> install systemd unit" -sed \ - -e "s|@@SERVICE_USER@@|${SERVICE_USER}|g" \ - -e "s|@@APP_DIR@@|${APP_DIR}|g" \ - -e "s|@@VENV@@|${VENV}|g" \ - -e "s|@@PORT@@|${PORT}|g" \ - -e "s|@@ENV_FILE@@|${ENV_FILE}|g" \ - -e "s|@@OPENSWE_ENV@@|${OPENSWE_ENV}|g" \ - -e "s|@@FETCH_CONFIG@@|${FETCH_CONFIG}|g" \ - -e "s|@@SEED_STORE@@|${SEED_STORE}|g" \ - "${TEMPLATE_DIR}/open-swe.service" >/etc/systemd/system/open-swe.service -systemctl daemon-reload - -# --- render + install the nginx site ----------------------------------------- -echo "==> install nginx site" -sed \ - -e "s|@@SERVER_NAME@@|${SERVER_NAME}|g" \ - -e "s|@@WWW_ROOT@@|${WWW_ROOT}|g" \ - -e "s|@@BACKEND_ADDR@@|127.0.0.1:${PORT}|g" \ - "${TEMPLATE_DIR}/open-swe.nginx.conf" >/etc/nginx/sites-available/open-swe -ln -sfn /etc/nginx/sites-available/open-swe /etc/nginx/sites-enabled/open-swe -rm -f /etc/nginx/sites-enabled/default -nginx -t - -# --- CloudWatch agent: 30-day log retention ---------------------------------- -echo "==> configure CloudWatch agent (30-day retention)" -sed -e "s|@@OPENSWE_ENV@@|${OPENSWE_ENV}|g" \ - "${TEMPLATE_DIR}/amazon-cloudwatch-agent.json" \ - >/opt/aws/amazon-cloudwatch-agent/etc/open-swe-cw.json -/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ - -a fetch-config -m ec2 -s \ - -c file:/opt/aws/amazon-cloudwatch-agent/etc/open-swe-cw.json - -# --- start nginx FIRST (the security boundary + health surface) -------------- -# NOTE: intentionally NO swapfile here. The 8 GB-swapfile OOM hack existed only -# for the on-box Nitro SPA build, which now runs in GitHub Actions -> S3. -# nginx is brought up BEFORE the app is deployed so the ALB target-group health -# check (static `/healthz` -> 200) passes and the box is a healthy target even on -# the very first boot, before any release is published. open-swe.service is -# enabled (boot persistence) but STARTED by deploy.sh once the app is on disk. -echo "==> start nginx" -systemctl enable --now nginx -systemctl reload nginx -systemctl enable open-swe.service - -# --- deploy the app (NON-FATAL on first boot) -------------------------------- -# deploy.sh pulls the release, builds the venv, and starts open-swe.service. On a -# brand-new env no release exists yet, so this is allowed to fail WITHOUT aborting -# user-data: nginx is already up (healthy target), and the first `build-artifacts` -# run + `open-swe--deploy` SSM command will bring the app up. A failure here -# is logged, not fatal. -echo "==> initial app deploy (non-fatal if no release is published yet)" -if /opt/open-swe/bin/deploy.sh; then - echo "==> initial app deploy succeeded" -else - echo "==> no release yet (or deploy failed): open-swe.service deferred to the next SSM deploy" -fi - -echo "==> open-swe user-data done $(date -u +%FT%TZ)" diff --git a/deploy/seahaven/DEPLOYMENT.md b/deploy/seahaven/DEPLOYMENT.md deleted file mode 100644 index 0a54a18c..00000000 --- a/deploy/seahaven/DEPLOYMENT.md +++ /dev/null @@ -1,280 +0,0 @@ -# Sea Haven — Open SWE deployment runbook - -How this fork is deployed at Sea Haven. **PROD is LIVE as of 2026-06-29.** The -runtime is the **stock LangGraph dev server** (not the Aegra path — see -[Aegra](#aegra-deferred)), running on a self-hosted ARM64 EC2 box behind the -shared `seahaven-com` ALB. - -Account `328440206208`, region `us-east-1`. Internal addresses, ARNs, snapshot -ids, and account-scoped values that are sensitive are shown as ``; -the real values live in the private IT docs (Confluence "AWS Architecture Map", -id 1540098) and in AWS — **do not commit them to this public fork.** - -This is the canonical deploy runbook. The CDK details live in -[`infra/README.md`](../../infra/README.md); the rotation procedure in -[`ROTATION.md`](ROTATION.md). - -## Live prod facts (2026-06-29) - -| | | -|---|---| -| Dashboard | `https://openswe.seahaven.com` | -| Webhooks | `https://hooks.seahaven.com/webhooks/*` | -| Ingress | shared internet-facing ALB `app/seahaven-com` → target group `open-swe-prod-tg` → EC2 `i-08a729e50779c4b07` (`t4g.large`, ARM64) on nginx `:80` | -| Backend | `langgraph dev` bound to `127.0.0.1:2024` (loopback only); nginx is the sole ingress | -| CDK stacks | `open-swe-iam` (OIDC roles) · `open-swe-dev` · `open-swe-prod` | - -Dev mirrors prod with `-dev` hosts (`openswe-dev.seahaven.com` / -`hooks-dev.seahaven.com`), a `t4g.medium` box, and no GitHub-App/Slack/webhook -integration (it is a deployment-validation env, not a live-triggered agent). - -> The retired on-prem `*.seahavenind.com` ALB routing and DNS were removed on -> 2026-06-29; prod is now live exclusively on `*.seahaven.com`. - -## Hosting model - -``` -GitHub / Slack ──▶ hooks.seahaven.com ──┐ - │ (shared ALB :443, host+path rules) -Browser ─────────▶ openswe.seahaven.com ──┤ - ▼ - ALB app/seahaven-com ──▶ open-swe-prod-tg ──▶ EC2 box :80 (nginx) - ├─ nginx — SPA + scoped proxy - │ /dashboard/api/* and /webhooks/* - └─ langgraph dev 127.0.0.1:2024 - └─▶ LangSmith cloud sandbox (build/git/PR) -``` - -- A **single** VPC and a **single** internet-facing ALB (`app/seahaven-com`) are - shared with the on-prem `seahaven-site` stack. open-swe **imports** the VPC, - ALB SG, `:443` listener, and `seahaven.com` zone — it never owns/mutates them; - it only adds its own instance SG, a standalone ALB-egress rule, two listener - rules, a target group, and Route53 aliases. -- The EC2 box is in a **private** subnet (us-east-1a, same AZ as the single NAT - for in-AZ egress). It is reachable **only** from the shared ALB SG on `:80`. -- **nginx is the security boundary.** It serves the static dashboard SPA and - proxies exactly two prefixes to `:2024` — `/dashboard/api/*` and `/webhooks/*`. - The unauthenticated LangGraph API (`/threads`, `/runs`, `/assistants`, - `/store`) is never proxied; those paths return the SPA shell. `:2024` is - loopback-only and never network-reachable, even inside the SG. -- **Webhooks** ride listener rules below the on-prem host-agnostic `/webhooks/*` - rule (priority 2 dev / 3 prod, host-scoped to the open-swe hosts) so they reach - the open-swe box and never steal an on-prem host's webhooks. - -The box holds **no durable state of its own**: secrets/config are materialized to -a tmpfs `.env` at boot, the app artifact is pulled from S3, and the in-memory -LangGraph store is re-seeded on every start. Replacement is tolerated; there is no -RETAIN volume. - ---- - -## Deploy pipeline (end to end) - -Two independent CD lanes, both OIDC-only (no static keys), both with a manual -approval gate on prod via the GitHub **`prod` Environment** (required reviewer: -Adam). The `environment: prod` declaration both fires the approval gate and makes -the OIDC subject `…:environment:prod`, which is the only subject the prod deploy -roles trust — so a dev-branch token can never reach prod. - -### (a) Infra CD — `cd-infra.yml` - -Deploys the CDK stacks. Path-filtered to `infra/**`. - -``` -push to dev → Infra CI (tsc + jest + cdk synth) → cdk deploy OpenSweDevStack (AUTO, CI-green-gated) -push to main → Infra CI → cdk deploy OpenSweProdStack (manual approval: env "prod") -``` - -- Roles: `githubdeploy-open-swe-infra-{dev,prod}` (in the `open-swe-iam` stack; - set as repo variables `AWS_DEPLOY_ROLE_INFRA_{DEV,PROD}`). -- It targets **one stack explicitly per env** (`cdk deploy OpenSweDevStack` / - `OpenSweProdStack`), not `cdk deploy --all`, so a single-env push can never - deploy the other env or the shared IAM stack. -- The shared `open-swe-iam` stack (owns both envs' OIDC deploy roles) is **not** - deployed by CD — it is a privileged, human-gated apply. - -**Stack order on a clean account:** `open-swe-iam` first (creates the OIDC roles; -set the repo deploy-role variables and configure the `prod` Environment reviewer -from its outputs), then `open-swe-dev`, then `open-swe-prod`. - -### (b) Seed the config store — `put-config.sh ` - -Run **after** `cdk deploy open-swe-` and **before** the box first boots. CDK -creates the value-less Secrets Manager shells (`open-swe-/`) and the -IaC-managed SSM params (`/open-swe-/`); `put-config.sh` populates the -secret values plus the out-of-band SSM params that cannot live in IaC. - -```bash -deploy/seahaven/put-config.sh # set each value inline, via OPENSWE_PUT_, or from a vault -deploy/seahaven/fetch-config.sh # (on the box) fail-fast verify before first start -``` - -`put-config.sh` ships `` placeholders only — **no real secret values are -committed**. It does not touch the IaC-managed SSM params (CDK owns those). - -**13 prod boot-required vars** — `fetch-config.sh` fail-fasts (refuses to write a -partial `.env`, the unit does not start) if any are missing/empty: - -- **9 secrets** (Secrets Manager `open-swe-prod/`): `DASHBOARD_JWT_SECRET`, - `TOKEN_ENCRYPTION_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, - `LANGSMITH_API_KEY_PROD`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, - `GITHUB_WEBHOOK_SECRET`, `SLACK_SIGNING_SECRET`. -- **4 SSM params** (`/open-swe-prod/`): `DEFAULT_SANDBOX_SNAPSHOT_ID`, - `GITHUB_APP_ID`, `GITHUB_APP_INSTALLATION_ID`, `GITHUB_APP_CLIENT_ID`. - -(`ANTHROPIC_API_KEY` + `OPENAI_API_KEY` are required because that is the seeded -cross-family pair; the active set follows `REQUIRED_PROVIDER_KEYS`. The -`LANGSMITH_API_KEY_PROD` + `DEFAULT_SANDBOX_SNAPSHOT_ID` pair is required because -`SANDBOX_TYPE=langsmith`.) Dev boots without the GitHub-App / Slack / webhook -secrets — it has no such integration. - -`fetch-config.sh` runs as an `ExecStartPre=+` hook (root, only long enough to -write the `openswe`-owned `0600` tmpfs `.env`), reads all `/open-swe-/*` SSM -params + all `open-swe-/*` secrets via the instance role, and forces -`DEFAULT_REPO_OWNER` away from the upstream `langchain-ai` org. - -### (c) App artifact deploy — `build-artifacts.yml` - -Builds the release and rolls the box. Path-filtered to `agent/**`, `ui/**`, -`deploy/**`, `langgraph.json`, `pyproject.toml`, `uv.lock`. - -``` -push to dev → build SPA + package → open-swe-dev-assets/releases/ → SSM open-swe-dev-deploy (AUTO) -push to main → build SPA + package → open-swe-prod-assets/releases/ → SSM open-swe-prod-deploy (manual approval: env "prod") -``` - -1. The dashboard SPA is built **on the runner** (`bun run build` → vite → - `ui/.output/public`) — the box is small, so the memory-heavy build runs in CI. -2. `package-artifacts.sh` produces two tarballs: `spa.tar.gz` (built SPA) and - `app.tar.gz` (Python source tree — no `ui/`, no `.venv`). -3. Both are uploaded to S3 `open-swe--assets` under `releases//` - (immutable, auditable) and mirrored to `releases/latest/` (what the box pulls). -4. CI fires the `open-swe--deploy` SSM document (tag-scoped to - `project=open-swe,env=`), which runs `/opt/open-swe/bin/deploy.sh` on the - box: pull the release from S3, build a native-ARM64 venv with - `uv sync --frozen --no-dev`, extract the SPA to the nginx web root, - `systemctl restart open-swe.service`, reload nginx, then gate on - `systemctl is-active --quiet open-swe.service` (a non-active unit exits the - deploy non-zero). - -Roles: `githubdeploy-open-swe-app-{dev,prod}` (repo variables -`AWS_DEPLOY_ROLE_APP_{DEV,PROD}`) — tag-scoped `ssm:SendCommand` on the deploy -document only (not the generic `AWS-RunShellScript`) + write to the env's S3 -bucket. - -Secrets/config are **not** fetched by `deploy.sh`; the `systemctl restart`'s -`ExecStartPre=fetch-config.sh` re-materializes the `.env` on every restart, so a -bad config surfaces as a failed unit. - -### (d) dev → main promotion + rollback - -**Promotion — `promote-dev-to-prod.yml`** (nightly cron `0 8 * * *` + manual -dispatch): mints a GitHub App installation token (a bypass actor on the `main` -ruleset), gates on **every check-run on the dev HEAD commit being completed and -passing**, then **fast-forward-only** pushes `dev` → `main`. A diverged `main` -fails loudly rather than force-updating. The push to `main` is what triggers the -prod lanes of `cd-infra.yml` / `build-artifacts.yml` (each still behind the `prod` -Environment approval). Re-gating via a PR on `main` would be redundant since the -commit already passed every check on dev. - -**Rollback — `rollback.yml`** (manual dispatch, `env` + optional `sha`): re-points -`releases/latest/` at a prior release and re-fires the `open-swe--deploy` SSM -document — same fire/wait/gate path as a forward deploy, no rebuild. - -``` -env=dev, sha blank → restore open-swe-dev-assets/releases/last-good/ (AUTO) -env=prod, sha blank → restore open-swe-prod-assets/releases/last-good/ (manual approval: env "prod") -sha= → restore that exact releases// instead -``` - -It reuses the existing `githubdeploy-open-swe-app-` role (no new IAM). - ---- - -## On-box layout (reference) - -| Path | What | -|---|---| -| `open-swe.service` (systemd) | `langgraph dev --host 127.0.0.1 --port 2024 --no-browser --no-reload` as the unprivileged `openswe` user. In-memory runtime. | -| `fetch-config.sh` | `ExecStartPre=+` — materializes the tmpfs `.env` from Secrets Manager + SSM, fail-fast. | -| `seed_store.sh` | `ExecStartPost` — re-seeds `team_settings/default` + `user_mappings` (the in-memory store loses them on every restart). | -| nginx | SPA from `/var/www/open-swe`, proxy `/dashboard/api/` + `/webhooks/` → `127.0.0.1:2024`, `/healthz` → 200. | -| `deploy.sh` | the release procedure run on first boot (non-fatal) and by every SSM deploy. | -| CloudWatch logs | `/open-swe//{app,user-data,nginx-access,nginx-error}` at 30-day retention. | - -The live systemd unit + nginx site are the **AMI templates** -(`deploy/ami/templates/open-swe.service`, `open-swe.nginx.conf`), rendered at -first boot by `deploy/ami/user-data.sh`. The AMI is the baked -`open-swe-base-arm64` image (Ubuntu 24.04 + uv/py3.12 + nginx + CW agent), pinned -by exact id in `infra/lib/constructs/ami-cache.ts`. There is intentionally **no** -on-box swapfile — the OOM-prone SPA build now runs in CI, not on the box. - -> `deploy/seahaven/{nginx/openswe.conf,systemd/open-swe.service}` are the -> **retired on-prem VM** variants (run as `adam` from a home dir, bound `0.0.0.0`, -> Postgres-backed). They are kept only for on-prem-contrast reference and are not -> used by the AWS deployment. - -### Models -Model selection is **store-driven**, not env. The `team_settings/default` store -doc wins (then per-user profile, then per-thread); `LLM_MODEL_ID` is only a -seed-time fallback. Defaults seeded by `seed_store.sh`: -- builder: `bedrock_converse:us.anthropic.claude-opus-4-8` (effort `high`) -- reviewer: `bedrock_converse:us.anthropic.claude-opus-4-8` (effort `high`) — set - `SEED_REVIEWER_MODEL` (or change it in the UI) to a Fireworks model if you want a - cross-family reviewer. Only ids present in `SUPPORTED_MODELS` - (`agent/dashboard/options.py`) are valid; OpenAI/Google models were removed in the - Bedrock/Fireworks migration. -- the `analyzer` graph is hardcoded to the code default and ignores team settings. - -## Triggering - -Mention **`@openswe`** (or `@open-swe` / `@seahaven-openswe`) in a GitHub issue or -PR comment, a Linear comment, or a Slack thread. The commenter must have a -`user_mappings` entry (seeded by `seed_store.sh` from `CONFIGURED_ADMINS` / -`SEED_USER_MAPPINGS`) or the run is skipped. - -Live integration endpoints (set in each provider's app config): - -| Integration | URL | -|---|---| -| GitHub webhook | `https://hooks.seahaven.com/webhooks/github` | -| Slack events | `https://hooks.seahaven.com/webhooks/slack` (+ `/webhooks/slack/interactivity`) | -| Linear webhook | `https://hooks.seahaven.com/webhooks/linear` | -| GitHub OAuth callback | `https://openswe.seahaven.com/dashboard/api/auth/callback` | - ---- - -## Troubleshooting - -**RETAIN secret-shell orphan on stack re-create.** The Secrets Manager shells use -`DeletionPolicy: Retain` + a fixed `open-swe-/` name. If a stack's first -create rolls back (or on a teardown/rebuild, a secret logical-id refactor, or -standing up a new env), the empty shells survive and keep their global names, so -every later create fails `AlreadyExists` — and a plain `delete-secret` does not -free the name (it stays reserved for the 7–30 day recovery window). Before -re-creating the stack, **force-delete the empty orphans** (only shells with no -value version — never a populated secret). Hit on prod 2026-06-29 (PR #51 deploy -failure). Full recovery command + rationale: -[`infra/README.md`](../../infra/README.md) (PR #52). - -**`langgraph dev` won't start after a deploy.** `fetch-config.sh` fail-fasts on a -missing/empty required var and prints the offending variable **names** (never -values) to the unit journal. Confirm the 13 prod boot-required vars are populated -(`put-config.sh prod`), then `systemctl restart open-swe.service`. - -**ALB target unhealthy.** The TG health check is `GET /healthz` on nginx `:80` -(static 200). nginx starts before the app on first boot, so an unhealthy target -usually means the box can't reach the ALB SG on `:80` (the standalone ALB-egress -rule) rather than an app fault. - ---- - -## Aegra (deferred) - -`aegra/aegra.json` + `aegra/aegra_entry.py` are the self-hosted-runtime -alternative (Apache-2.0, avoids the LangGraph-Platform Elastic license). Not -active on the stock deployment. To use: place both at the repo root, run -`aegra serve` (:2026), and point `LANGGRAPH_URL` at `:2026`. Aegra gives a -Postgres-backed durable store/checkpointer, which removes the need for -`seed_store.sh` and survives restarts (paused HITL interrupts persist). - diff --git a/deploy/seahaven/ROTATION.md b/deploy/seahaven/ROTATION.md deleted file mode 100644 index 38f7de45..00000000 --- a/deploy/seahaven/ROTATION.md +++ /dev/null @@ -1,92 +0,0 @@ -# Sea Haven — Open SWE secret & config rotation - -How secrets and config reach the running app, and how to rotate either one. - -## How values flow at boot - -``` -AWS Secrets Manager open-swe-/* ─┐ -AWS SSM Param Store /open-swe-/* ─┤── fetch-config.sh ──▶ tmpfs /run/open-swe/.env (root:root 0600) - │ (systemd ExecStartPre=+, EC2 role) │ - ▼ ▼ - FAIL-FAST if a app symlink /.env - required var is empty python-dotenv reads at import -``` - -The app reads `.env` **once, at import**. There is no hot-reload of secrets. -Therefore the rotation contract is always the same two steps: - -> **Rotation = (1) update the value in Secrets Manager / SSM, then (2) restart the -> service** so `fetch-config.sh` re-materializes the `.env`. - -```bash -# after updating a secret/param in AWS: -sudo systemctl restart open-swe.service -# ExecStartPre=+ -> fetch-config.sh re-pulls + rewrites the tmpfs .env (fail-fast) -# ExecStartPost -> seed_store.sh re-seeds the in-memory store (team_settings + user_mappings) -``` - -There is **no zero-downtime path for most secrets** on the stock in-memory -runtime — a restart is required and it also wipes the in-memory store (re-seeded -by `seed_store.sh` automatically). The one secret built for zero-downtime overlap -is `TOKEN_ENCRYPTION_KEY` (see below), but even it needs the restart to load the -new key list. - -## Rotating a secret (Secrets Manager) - -```bash -ENV=prod # or dev -NAME=DASHBOARD_JWT_SECRET -aws secretsmanager put-secret-value \ - --secret-id "open-swe-${ENV}/${NAME}" \ - --secret-string 'NEW_VALUE' \ - --region us-east-1 -sudo systemctl restart open-swe.service # on the box -``` - -(`update-secret`/`put-secret-value` both create a new version; the boot hook -always reads `AWSCURRENT`.) - -## Rotating a config param (SSM) - -```bash -aws ssm put-parameter --overwrite \ - --name "/open-swe-${ENV}/DASHBOARD_BASE_URL" \ - --type String --value 'https://openswe.seahaven.com' \ - --region us-east-1 -sudo systemctl restart open-swe.service -``` - -## Per-secret rotation notes - -| Secret | Rotation notes | -|---|---| -| **TOKEN_ENCRYPTION_KEY** | Fernet key(s). Supports a **comma/newline-separated list** (`agent/encryption.py`) for zero-downtime key rotation: prepend the NEW key, keep the OLD key(s) in the list. New data is encrypted with the first key; old data still decrypts with the trailing keys. After all encrypted-at-rest tokens (per-user GitHub OAuth tokens in thread metadata) have been re-encrypted/expired, drop the old key. Store the list as one secret value; `fetch-config.sh` writes it verbatim. **Never** rotate to a single new key in one step or every existing encrypted token becomes undecryptable. | -| **GITHUB_APP_PRIVATE_KEY** | Multiline PEM. Generate a new private key in the GitHub App settings (you may have **two active keys** during overlap), put the new PEM into the secret, restart, verify install-token minting + a webhook delivery, then delete the old key in GitHub. `fetch-config.sh` writes the PEM as a double-quoted multiline value (python-dotenv-safe); paste the full `-----BEGIN…-----END-----` block including newlines. | -| **GITHUB_WEBHOOK_SECRET** | Webhook HMAC. GitHub allows only **one** webhook secret per App, so this is a brief-break rotation: update the secret in AWS **and** the GitHub App webhook config, restart. Deliveries signed with the old secret during the gap will 401 (GitHub auto-redelivers). Required in **prod** (fail-fast). | -| **SLACK_SIGNING_SECRET** | Slack request-signature secret. Rotate in the Slack app config and AWS together, restart. Required in **prod** (fail-fast). A stale value silently 401s `url_verification`/events until restart (known gotcha). | -| **LINEAR_WEBHOOK_SECRET** | Linear webhook signature. Required in prod only when the Linear integration is wired (`LINEAR_API_KEY` present). Rotate in Linear + AWS together, restart. | -| **SLACK_CLIENT_SECRET / GITHUB_APP_CLIENT_SECRET** | OAuth client secrets (dashboard login / Slack OAuth). Rotate in the provider console + AWS, restart. Existing dashboard sessions are JWT-signed by `DASHBOARD_JWT_SECRET`, not these, so they survive. | -| **DASHBOARD_JWT_SECRET** | Signs dashboard session cookies. Rotating **invalidates all active sessions** (users re-login). Hard-required (RuntimeError if empty). No overlap list — single value. | -| **Model provider keys** (`FIREWORKS_API_KEY`; legacy `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GOOGLE_API_KEY` / `GROQ_API_KEY`) | Standard API-key rotation: issue new key, update AWS, restart, revoke old. Bedrock (the seeded Claude builder + reviewer) authenticates via the **host IAM role — no API key**; the only fail-fast-required provider key is now `FIREWORKS_API_KEY` (fallback / subagents / any non-Claude model). Keep `REQUIRED_PROVIDER_KEYS` in sync if you change the seeded models. | -| **LANGSMITH_API_KEY_PROD** | LangSmith key powering the `langsmith` sandbox (the only provider with working in-sandbox git/gh auth). Required when `SANDBOX_TYPE=langsmith` (fail-fast). Rotate in LangSmith + AWS, restart; existing sandboxes keep their already-injected proxy token until recycled. | -| **SLACK_BOT_TOKEN / LINEAR_API_KEY / GITHUB_PAT / EXA_API_KEY / DAYTONA_API_KEY / RUNLOOP_API_KEY / CORRIDOR_* / USER_ID_API_KEY_MAP / X_SERVICE_AUTH_JWT_SECRET** | Plain API-token rotation: update AWS, restart, revoke old at the provider. None support an overlap list. | - -## Fail-fast safety - -`fetch-config.sh` refuses to write the `.env` (exit 1) if any required var is -empty after a rotation — so a botched rotation (e.g. an empty `put-secret-value`) -stops the service at `ExecStartPre` instead of starting it with a partial `.env`. -The missing variable **names** are printed to the journal (values never are): - -```bash -sudo journalctl -u open-swe.service -b | grep fetch-config -``` - -Required set enforced: `DASHBOARD_JWT_SECRET`, `TOKEN_ENCRYPTION_KEY`, -`GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_INSTALLATION_ID`, -`GITHUB_APP_CLIENT_ID`, `GITHUB_APP_CLIENT_SECRET`, the active provider keys -(`REQUIRED_PROVIDER_KEYS`, default `ANTHROPIC_API_KEY,OPENAI_API_KEY`), the -sandbox key for `SANDBOX_TYPE` (`langsmith` ⇒ `LANGSMITH_API_KEY_PROD` + -`DEFAULT_SANDBOX_SNAPSHOT_ID`), and — in **prod** — `GITHUB_WEBHOOK_SECRET`, -`SLACK_SIGNING_SECRET` (plus `LINEAR_WEBHOOK_SECRET` when Linear is wired). diff --git a/deploy/seahaven/aegra/aegra.json b/deploy/seahaven/aegra/aegra.json deleted file mode 100644 index 8e8af627..00000000 --- a/deploy/seahaven/aegra/aegra.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "dependencies": ["."], - "graphs": { - "agent": "./aegra_entry.py:agent_graph", - "reviewer": "./aegra_entry.py:reviewer_graph", - "analyzer": "./aegra_entry.py:analyzer_graph" - }, - "http": { - "app": "./aegra_entry.py:webapp_app" - } -} diff --git a/deploy/seahaven/aegra/aegra_entry.py b/deploy/seahaven/aegra/aegra_entry.py deleted file mode 100644 index a182f18d..00000000 --- a/deploy/seahaven/aegra/aegra_entry.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Aegra entrypoint for Open SWE graphs. - -Aegra loads graph files standalone via importlib.spec_from_file_location, which -gives them a synthetic module name and breaks agent/*.py's package-relative -imports (e.g. `from .dashboard.admin import ...`). Re-exporting the graphs here -through the installed ``agent`` package (absolute imports) restores correct -``__package__`` resolution, so the relative imports inside the agent modules work. -""" - -from agent.analyzer import traced_analyzer as analyzer_graph -from agent.reviewer import traced_reviewer_agent as reviewer_graph -from agent.server import traced_agent as agent_graph - -# Open SWE's FastAPI webapp (GitHub/Slack webhooks + dashboard API), mounted by -# Aegra via the "http" key in aegra.json. Absolute import for the same reason as -# the graphs above (relative imports break under Aegra's standalone file loader). -from agent.webapp import app as webapp_app - -__all__ = ["agent_graph", "reviewer_graph", "analyzer_graph", "webapp_app"] diff --git a/deploy/seahaven/fetch-config.sh b/deploy/seahaven/fetch-config.sh deleted file mode 100755 index 77457768..00000000 --- a/deploy/seahaven/fetch-config.sh +++ /dev/null @@ -1,363 +0,0 @@ -#!/usr/bin/env bash -# fetch-config.sh — AWS-sourced boot hook that materializes the app's .env. -# -# The stock `langgraph dev` runtime + the Open SWE app read a plain `.env` from -# the app working directory (python-dotenv). On the AWS lift-and-shift we do NOT -# commit a .env; instead every non-sensitive value lives in SSM Parameter Store -# (`/open-swe-/*`) and every secret lives in AWS Secrets Manager -# (`open-swe-/*`). This hook is run by systemd BEFORE the service starts; it -# pulls both sources via the EC2 instance role (no static keys), assembles a -# single .env on a tmpfs, and writes it owned by the unprivileged service user -# `chmod 600` (T5 SC-01: the privileged pre-hook materializes the secret; the app -# itself then runs as that NON-root service user, not root). -# -# It is intentionally FAIL-FAST: if any required secret/param is missing or empty -# it prints the offending variable NAMES (never values) and exits 1, so the -# service never starts with a partial .env. -# -# --------------------------------------------------------------------------- -# Naming contract (source of truth: T9 env/secret/config inventory) -# SSM /open-swe-/ -> exported as ENV_VAR_NAME -# Secrets open-swe-/ -> exported as ENV_VAR_NAME -# i.e. the last path segment IS the literal environment-variable name. This is a -# deliberate (documented) deviation from the handbook's kebab-case value-name -# example (`my-stack/slack-signing`): a .env materializer needs a lossless, -# unambiguous round-trip from store key -> env var, and the env var name is the -# only key that guarantees that. The `open-swe-` stack prefix still follows -# kebab-case per naming-conventions.md. -# --------------------------------------------------------------------------- -# -# Wiring into systemd (AWS EC2 variant): -# The unit runs as the unprivileged service user (User=openswe). ONLY the -# ExecStartPre pre-hook runs as root (the `+` prefix) so it can pull from AWS, -# write the tmpfs .env, and chown it to the service user. The app (ExecStart) -# and the seeder (ExecStartPost) then run as openswe and read the openswe-owned -# 0600 .env — the agent never runs as root (T5 SC-01). Pass the env as the -# positional arg (T5 BOOT-01): -# -# [Service] -# User=openswe -# Group=openswe -# Environment=ENV_DIR=/run/open-swe SERVICE_USER=openswe -# # ExecStartPre runs as root (+) so it can chown the .env to the service user. -# ExecStartPre=+/opt/open-swe/deploy/seahaven/fetch-config.sh prod -# ExecStart=/opt/open-swe/.venv/bin/langgraph dev --host 127.0.0.1 --port 2024 \ -# --no-browser --no-reload -# ExecStartPost=/opt/open-swe/deploy/seahaven/seed_store.sh prod -# -# tmpfs: /run is already a tmpfs on systemd hosts, so ENV_DIR=/run/open-swe is -# tmpfs-backed by default (the .env never touches disk). Set RUN_DEDICATED_TMPFS=1 -# to mount a private tmpfs at ENV_DIR instead. The app's CWD `.env` is a symlink -# into ENV_DIR (created idempotently below), so python-dotenv finds it unchanged. -# -# Idempotent, re-runnable on every (re)start. No secret is ever echoed. - -set -euo pipefail -umask 077 - -# --- Inputs ------------------------------------------------------------------ -ENV="${1:-${OPENSWE_ENV:-}}" -case "$ENV" in - dev | prod) ;; - *) - echo "fetch-config: ENV must be 'dev' or 'prod' (got '${ENV:-}')" >&2 - echo "usage: fetch-config.sh (or set OPENSWE_ENV)" >&2 - exit 2 - ;; -esac - -REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" -SSM_PREFIX="/open-swe-${ENV}/" -SECRET_PREFIX="open-swe-${ENV}/" - -ENV_DIR="${ENV_DIR:-/run/open-swe}" # tmpfs-backed (/run) by default -ENV_FILE="${ENV_DIR}/.env" -APP_DIR="${APP_DIR:-/opt/open-swe}" # where the app + its CWD .env live -APP_ENV_LINK="${APP_DIR}/.env" # symlink -> ENV_FILE - -# The unprivileged service user that runs the app and OWNS the .env (T5 SC-01). -# fetch-config runs as root (ExecStartPre=+) only to chown the secret to it. -SERVICE_USER="${SERVICE_USER:-openswe}" -SERVICE_GROUP="${SERVICE_GROUP:-${SERVICE_USER}}" - -# Sea Haven owner guard inputs (applied after the store is read, below). The owner -# normally comes from SSM /open-swe-/DEFAULT_REPO_OWNER; OPENSWE_REPO_OWNER is an -# explicit operator override that wins over the store. FORBIDDEN = the upstream org -# the fork must never target; SAFE = the fallback when the resolved owner is blank or -# forbidden. (Upper/lower + whitespace are normalized before the guard check.) -OPENSWE_REPO_OWNER="${OPENSWE_REPO_OWNER:-}" -FORBIDDEN_REPO_OWNER="langchain-ai" -# Fallback org when the resolved owner is blank/forbidden — PER-ENV (mirrors the -# iacManagedSsm owner) so a dev box can NEVER fall back into the real Sea Haven org; -# it stays isolated in its own dev org. Defends the blank/upstream cases in-env. -case "$ENV" in - dev) SAFE_REPO_OWNER="seahaven-open-swe-dev" ;; - *) SAFE_REPO_OWNER="Sea-Haven-Industries" ;; -esac - -for bin in aws jq; do - command -v "$bin" >/dev/null 2>&1 || { echo "fetch-config: '$bin' not found on PATH" >&2; exit 3; } -done - -log() { echo "fetch-config[$ENV]: $*"; } # NAMES/counts only — never values -b64d() { base64 --decode; } # GNU coreutils on the EC2 host - -# Accept a store key into VARS iff it is a valid env-var identifier and not a -# duplicate. Rejects non-identifier names (T5 SH-INJ-002 / set -e DoS hardening) -# and flat-namespace collisions (T5 SSM-05). $3 = source label for logs. -accept_var() { - local key="$1" value="$2" src="$3" - if ! [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]]; then - log "WARNING: skipping ${src} key with non-identifier name (rejected)" - return 0 - fi - if [ -n "${VARS[$key]+set}" ]; then - echo "fetch-config[$ENV]: FAIL-FAST — duplicate key '${key}' from ${src} (flat-namespace collision)" >&2 - exit 1 - fi - VARS["$key"]="$value" -} - -# --- tmpfs ------------------------------------------------------------------ -mkdir -p "$ENV_DIR" -# Owned by the service user so the unprivileged app can traverse it (T5 SC-01). -chown "${SERVICE_USER}:${SERVICE_GROUP}" "$ENV_DIR" 2>/dev/null || true -chmod 700 "$ENV_DIR" -if [ "${RUN_DEDICATED_TMPFS:-0}" = "1" ] && ! mountpoint -q "$ENV_DIR"; then - mount -t tmpfs -o nosuid,nodev,noexec,mode=0700,size=4m tmpfs "$ENV_DIR" - log "mounted dedicated tmpfs at $ENV_DIR" -fi - -# --- Collect values into an associative array -------------------------------- -declare -A VARS=() - -# 1) SSM Parameter Store (non-sensitive config). NOT --recursive: the contract is -# a FLAT namespace /open-swe-/, so a non-recursive list returns exactly -# those keys and cannot collapse two nested paths onto one name (T5 SSM-05). aws -# CLI v2 auto-paginates NextToken. -log "reading SSM params under ${SSM_PREFIX} ..." -ssm_json="$( - aws ssm get-parameters-by-path \ - --path "$SSM_PREFIX" \ - --with-decryption \ - --region "$REGION" \ - --no-cli-pager \ - --output json -)" -# Records are base64-encoded (namevalue) so values with spaces/newlines/tabs -# survive the line-based read intact. -ssm_count=0 -while IFS=$'\t' read -r nb vb; do - [ -n "$nb" ] || continue - name="$(printf '%s' "$nb" | b64d)" - value="$(printf '%s' "$vb" | b64d; printf 'x')"; value="${value%x}" - key="${name##*/}" # strip /open-swe-/ prefix - [ -n "$key" ] || continue - accept_var "$key" "$value" "SSM" - ssm_count=$((ssm_count + 1)) -done < <(jq -r '.Parameters[] | (.Name|@base64) + "\t" + (.Value|@base64)' <<<"$ssm_json") -log "loaded ${ssm_count} config param(s) from SSM" - -# 2) Secrets Manager (sensitive values). We request secrets by EXPLICIT id -# (`batch-get-secret-value --secret-id-list ...`) rather than a name-prefix -# `--filters` collection scan (OSWE-IAC-SECRETS-LIST-01). Two wins: -# (a) Least privilege — an explicit id list lets the instance role scope -# BatchGetSecretValue to the per-secret ARN prefix and DROP the account-wide -# `secretsmanager:ListSecrets` grant that a filtered scan unavoidably forces -# (ListSecrets has no resource-level scoping). A filtered batch call also only -# authorizes against `*`; an id-list call authorizes per-secret ARN. -# (b) Deterministic set — the value set is the fixed SECRET_VARS shells created by -# the CDK ConfigStore, so we no longer depend on a list scan returning every -# page. Value-less shells and ids with no current value come back in the -# response `.Errors[]` (ResourceNotFound), never in `.SecretValues[]`, so a -# genuinely-missing REQUIRED secret is still caught by the FAIL-FAST check -# below — an absent optional secret is simply skipped. -# `--secret-id-list` is capped at 20 ids per call, so we chunk it. `--no-cli-pager` -# disables only the OUTPUT pager. Each record is base64(namevalue). -# -# SOURCE OF TRUTH for this list: infra/lib/constructs/config-store.ts `SECRET_VARS`. -# Keep the two in lockstep — a new secret shell created there must be added here or it -# will never be fetched into the .env. -SECRET_VARS=( - ANTHROPIC_API_KEY CORRIDOR_API_TOKEN CORRIDOR_MCP_TOKEN CORRIDOR_TOKEN - DASHBOARD_JWT_SECRET DAYTONA_API_KEY EXA_API_KEY FIREWORKS_API_KEY - GITHUB_APP_CLIENT_SECRET GITHUB_APP_PRIVATE_KEY GITHUB_PAT GITHUB_WEBHOOK_SECRET - JUDGE_ANTHROPIC_API_KEY LANGSMITH_API_KEY - LANGSMITH_API_KEY_PROD LANGCHAIN_API_KEY LINEAR_API_KEY LINEAR_WEBHOOK_SECRET - RUNLOOP_API_KEY SLACK_BOT_TOKEN SLACK_CLIENT_SECRET - SLACK_SIGNING_SECRET TOKEN_ENCRYPTION_KEY USER_ID_API_KEY_MAP X_SERVICE_AUTH_JWT_SECRET -) - -batch_get_secrets_tsv() { - local -a ids=() - local v - for v in "${SECRET_VARS[@]}"; do ids+=("${SECRET_PREFIX}${v}"); done - - local i page - local -a chunk - for ((i = 0; i < ${#ids[@]}; i += 20)); do - chunk=("${ids[@]:i:20}") - # Capture the response into a variable FIRST so a non-zero `aws` exit (throttle, - # AccessDenied, KMS DecryptionFailure) aborts under set -e instead of being - # silently swallowed — then we'd FAIL-FAST below as "missing secret" with a wrong - # root cause. (Value-less / absent shells come back in .Errors[], not .SecretValues[].) - page="$(aws secretsmanager batch-get-secret-value \ - --secret-id-list "${chunk[@]}" \ - --region "$REGION" --no-cli-pager --output json)" - printf '%s' "$page" \ - | jq -r '.SecretValues[] | select(.SecretString != null) | (.Name|@base64) + "\t" + (.SecretString|@base64)' - done -} - -log "reading secrets under ${SECRET_PREFIX} ..." -secret_count=0 -# Capture into a variable (NOT `done < <(...)` process substitution) so a non-zero -# exit from batch_get_secrets_tsv propagates under set -e — process substitution hides -# the producer's exit status from the parent shell, which would let a failed AWS call -# fall through to a misleading "missing required var" FAIL-FAST. Mirrors the SSM read. -secrets_tsv="$(batch_get_secrets_tsv)" -while IFS=$'\t' read -r nb vb; do - [ -n "$nb" ] || continue - name="$(printf '%s' "$nb" | b64d)" - case "$name" in - "${SECRET_PREFIX}"*) ;; # defensive: exact-prefix only - *) continue ;; - esac - value="$(printf '%s' "$vb" | b64d; printf 'x')"; value="${value%x}" - key="${name##*/}" - [ -n "$key" ] || continue - accept_var "$key" "$value" "Secrets" - secret_count=$((secret_count + 1)) -done <<<"$secrets_tsv" -log "loaded ${secret_count} secret(s) from Secrets Manager" - -# --- Sea Haven DEFAULT_REPO_OWNER guard -------------------------------------- -# OSWE-OWNER-04 (revised for multi-org): HONOR the configured owner — the -# OPENSWE_REPO_OWNER env override if set, else the store value — so per-env orgs -# work (dev = seahaven-open-swe-dev, prod = Sea-Haven-Industries). But GUARD the two -# values that must NEVER reach the agent: blank, and the upstream 'langchain-ai' org -# (the fork's origin). Either falls back to the Sea Haven org so a stale/blank/mis-set -# value can never point the agent upstream. Comparison is case- and whitespace- -# insensitive. The POSITIVE org allowlist is enforced by the app (ALLOWED_GITHUB_ORGS). -resolved_owner="${OPENSWE_REPO_OWNER:-${VARS[DEFAULT_REPO_OWNER]:-}}" -# Normalize for the guard CHECK ONLY (the original value is what gets stored when -# allowed): lowercase, strip whitespace, take the FIRST path segment so a value like -# 'langchain-ai/open-swe' still trips the guard, and drop dots (GitHub owners contain -# none) so 'langchain-ai.' can't slip past. Homoglyph/unicode variants are out of scope -# here — the owner comes from admin-written SSM/IaC, not attacker-controlled input. -norm_owner="$(printf '%s' "$resolved_owner" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" -norm_owner="${norm_owner%%/*}" -norm_owner="${norm_owner//./}" -case "$norm_owner" in - "" | "$FORBIDDEN_REPO_OWNER") - log "WARNING: DEFAULT_REPO_OWNER ('${resolved_owner:-}') is blank or the upstream org -> forcing '${SAFE_REPO_OWNER}'" - resolved_owner="$SAFE_REPO_OWNER" - ;; -esac -VARS[DEFAULT_REPO_OWNER]="$resolved_owner" - -# --- FAIL-FAST: required vars ------------------------------------------------- -# Hard-required regardless of mode: -required=( - DASHBOARD_JWT_SECRET # RuntimeError on startup if missing (oauth.py) - TOKEN_ENCRYPTION_KEY # Fernet key(s); decrypts per-user GitHub tokens -) -# NOTE: the GitHub App is NOT created/duplicated for dev — only prod owns the -# (single, shared) GitHub App + Slack app. So the GitHub App quintet + Slack + -# webhook-signing secrets are required for PROD only (see the prod block below). -# Dev boots without them: it has no GitHub-App/Slack/webhook integration — it is a -# deployment-validation env (boot/health/boundary), not a live-triggered agent. - -# Active model-provider key(s): model selection is store-driven (team_settings), -# so fetch-config cannot infer it from .env. Bedrock (Claude builder + reviewer) -# authenticates via the host IAM role — no API key; Fireworks (fallback / subagents / -# any non-Claude model) needs its key. Override with a comma list if the active models change. -IFS=',' read -r -a provider_keys <<<"${REQUIRED_PROVIDER_KEYS:-FIREWORKS_API_KEY}" -for k in "${provider_keys[@]}"; do - k="${k//[[:space:]]/}" - [ -n "$k" ] && required+=("$k") -done - -# Sandbox provider key(s) — depends on SANDBOX_TYPE (default langsmith). -sandbox_type="${VARS[SANDBOX_TYPE]:-langsmith}" -case "$sandbox_type" in - langsmith) required+=(LANGSMITH_API_KEY_PROD DEFAULT_SANDBOX_SNAPSHOT_ID) ;; - daytona) required+=(DAYTONA_API_KEY) ;; - runloop) required+=(RUNLOOP_API_KEY) ;; - modal | local) ;; # no key required - *) log "WARNING: unknown SANDBOX_TYPE='${sandbox_type}' — not enforcing a sandbox key" ;; -esac - -# Prod-only: the GitHub App (installation-token minting + dashboard OAuth) and the -# webhook-signing secrets. Dev has no GitHub/Slack app, so none of these are -# required there; prod owns the single shared app and must have all of them. -if [ "$ENV" = "prod" ]; then - required+=( - GITHUB_APP_ID # GitHub App trio (installation-token minting) ... - GITHUB_APP_PRIVATE_KEY # ... multiline PEM ... - GITHUB_APP_INSTALLATION_ID # ... used by utils/github_app.py - GITHUB_APP_CLIENT_ID # dashboard OAuth login - GITHUB_APP_CLIENT_SECRET # dashboard OAuth login - GITHUB_WEBHOOK_SECRET # webhook signature verification - SLACK_SIGNING_SECRET # Slack webhook signature verification - ) - if [ -n "${VARS[LINEAR_API_KEY]:-}" ] && [ "${OPENSWE_REQUIRE_LINEAR:-1}" = "1" ]; then - required+=(LINEAR_WEBHOOK_SECRET) - fi -fi - -missing=() -for k in "${required[@]}"; do - [ -n "${VARS[$k]:-}" ] || missing+=("$k") -done -# de-dup the names for a clean report -if [ "${#missing[@]}" -gt 0 ]; then - mapfile -t missing < <(printf '%s\n' "${missing[@]}" | sort -u) - echo "fetch-config[$ENV]: FAIL-FAST — ${#missing[@]} required var(s) missing/empty:" >&2 - printf ' - %s\n' "${missing[@]}" >&2 - echo "fetch-config[$ENV]: refusing to write a partial .env; service will not start." >&2 - exit 1 -fi - -# --- Write the .env atomically (root-only on tmpfs) -------------------------- -# python-dotenv reads double-quoted values (incl. multiline PEMs). Its decoder -# unescapes ONLY backslash and double-quote (\\ -> \, \" -> "); it does NOT honor -# \$ or \` escapes, so escaping those would leave a spurious backslash. Escape -# exactly backslash then double-quote — real newlines stay literal (multiline OK). -# (Caveat: python-dotenv interpolates a literal `${VAR}` substring; the secret -# domain here — base64/hex/PEM keys — never contains one, so no extra guard.) -emit_var() { - local name="$1" value="$2" esc - esc="${value//\\/\\\\}" - esc="${esc//\"/\\\"}" - printf '%s="%s"\n' "$name" "$esc" -} - -tmp="$(mktemp "${ENV_DIR}/.env.XXXXXX")" -chmod 600 "$tmp" -{ - printf '# Generated by fetch-config.sh for env=%s at %s — DO NOT EDIT.\n' \ - "$ENV" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" - printf '# Source: SSM /open-swe-%s/* + Secrets Manager open-swe-%s/*\n\n' "$ENV" "$ENV" - for k in $(printf '%s\n' "${!VARS[@]}" | sort); do - emit_var "$k" "${VARS[$k]}" - done -} >"$tmp" - -mv -f "$tmp" "$ENV_FILE" -# Owned by the unprivileged service user (T5 SC-01) so the app reads it without -# running as root. fetch-config itself runs as root (ExecStartPre=+) to chown. -chown "${SERVICE_USER}:${SERVICE_GROUP}" "$ENV_FILE" -chmod 600 "$ENV_FILE" - -# Point the app's CWD .env at the tmpfs file (idempotent). -if [ "$APP_ENV_LINK" != "$ENV_FILE" ]; then - if [ -L "$APP_ENV_LINK" ] || [ ! -e "$APP_ENV_LINK" ]; then - ln -sfn "$ENV_FILE" "$APP_ENV_LINK" - elif [ "$(readlink -f "$APP_ENV_LINK" 2>/dev/null || true)" != "$(readlink -f "$ENV_FILE")" ]; then - log "WARNING: ${APP_ENV_LINK} exists and is not a symlink to ${ENV_FILE} — leaving it untouched" - fi -fi - -total=$((ssm_count + secret_count)) -log "wrote ${ENV_FILE} (${total} vars, sandbox=${sandbox_type}) — ${SERVICE_USER}:${SERVICE_GROUP} 0600" diff --git a/deploy/seahaven/nginx/openswe.conf b/deploy/seahaven/nginx/openswe.conf deleted file mode 100644 index fbf7e1b7..00000000 --- a/deploy/seahaven/nginx/openswe.conf +++ /dev/null @@ -1,36 +0,0 @@ -# Open SWE dashboard frontend (TanStack Start SPA) + scoped API proxy. -# RETIRED on-prem VM variant — kept for on-prem-contrast reference only. The LIVE -# AWS nginx site is the AMI template deploy/ami/templates/open-swe.nginx.conf -# (rendered from an @@SERVER_NAME@@ token at first boot). See DEPLOYMENT.md. -# -# nginx is the security boundary: ONLY /dashboard/api/* reaches the backend; -# the unauthenticated LangGraph agent API (/threads,/runs,/assistants,/store) is NOT proxied. -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name openswe.seahaven.com; - - root /var/www/openswe; - index _shell.html; - - # ALB health check - location = /healthz { default_type text/plain; return 200 "ok\n"; } - - # Dashboard API + OAuth callback -> backend webapp on :2024 (the ONLY proxied path) - location /dashboard/api/ { - proxy_pass http://127.0.0.1:2024; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto https; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 300s; - } - - # Static assets + SPA shell fallback (client-side routing) - location / { - try_files $uri $uri/ /_shell.html; - } -} diff --git a/deploy/seahaven/put-config.sh b/deploy/seahaven/put-config.sh deleted file mode 100755 index dddb9609..00000000 --- a/deploy/seahaven/put-config.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -# put-config.sh — out-of-band populator for the open-swe config store. -# -# T11 (CDK) creates the RESOURCE SHELLS: -# - 28 Secrets Manager secrets open-swe-/ (value-LESS shells) -# - the IaC-managed SSM params /open-swe-/ (real values, owned in CDK) -# This script sets the values that CANNOT live in IaC — every secret value, plus -# the out-of-band SSM params (operationally-variable / env-specific-unknown). Run -# it AFTER `cdk deploy open-swe-` and BEFORE the EC2/T12 box first boots, so -# fetch-config.sh finds every REQUIRED var populated and never writes a partial .env. -# -# The secret list below mirrors config-store.ts SECRET_VARS (28 names — the -# inventory's "29" double-counted JUDGE_ANTHROPIC_BASE_URL, which is config -# (SSM), not a secret). Keep the two lists in lockstep. -# -# SAFETY: -# - NO real secret values live in this file — every secret is a placeholder. -# Replace inline at run time, pipe from a vault, or export the -# matching OPENSWE_PUT_ env var; NEVER commit real values. -# - It does NOT touch the IaC-managed SSM params (SANDBOX_TYPE, DEFAULT_REPO_OWNER, -# ALLOWED_GITHUB_ORGS, DEFAULT_REPO_NAME, DASHBOARD_*_URL/ORIGINS, LLM_MODEL_ID) -# — CDK owns those; setting them here would cause drift. -# - Secrets go to Secrets Manager; the box's instance role grants read on -# open-swe-/* and /open-swe-/* (no kms:Decrypt — AWS-managed keys). -# -# Idempotent: put-secret-value adds a new AWSCURRENT version; put-parameter -# --overwrite updates in place. - -set -euo pipefail - -ENV="${1:-}" -case "$ENV" in - dev | prod) ;; - *) - echo "usage: put-config.sh " >&2 - exit 2 - ;; -esac - -REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-us-east-1}}" -SECRET_PREFIX="open-swe-${ENV}/" -SSM_PREFIX="/open-swe-${ENV}/" - -command -v aws >/dev/null 2>&1 || { echo "put-config: 'aws' not found on PATH" >&2; exit 3; } - -# --- helpers ----------------------------------------------------------------- -# put_secret VAR : set the value of an EXISTING secret shell open-swe-/VAR. -# Resolution order for the value: $OPENSWE_PUT_ env var, else the literal -# placeholder (which aborts so an unset secret is never silently shipped). -put_secret() { - local var="$1" - local override_name="OPENSWE_PUT_${var}" - local value="${!override_name:-}" - if [ "$value" = "" ]; then - echo "put-config[$ENV]: SKIP secret ${var} (no value; set ${override_name} or edit inline)" >&2 - return 0 - fi - aws secretsmanager put-secret-value \ - --secret-id "${SECRET_PREFIX}${var}" \ - --secret-string "$value" \ - --region "$REGION" \ - --no-cli-pager >/dev/null - echo "put-config[$ENV]: set secret ${var}" -} - -# put_param VAR [TYPE] : create/update an out-of-band SSM param /open-swe-/VAR. -# TYPE defaults to String; pass SecureString for anything sensitive-but-not-a-secret. -put_param() { - local var="$1" type="${2:-String}" - local override_name="OPENSWE_PUT_${var}" - local value="${!override_name:-}" - if [ "$value" = "" ]; then - echo "put-config[$ENV]: SKIP param ${var} (no value; set ${override_name} or edit inline)" >&2 - return 0 - fi - aws ssm put-parameter \ - --name "${SSM_PREFIX}${var}" \ - --value "$value" \ - --type "$type" \ - --overwrite \ - --region "$REGION" \ - --no-cli-pager >/dev/null - echo "put-config[$ENV]: set param ${var} (${type})" -} - -# --- 1) Secrets (open-swe-/) — 28 shells from config-store.ts ------- -# REQUIRED at boot (fetch-config fail-fast): DASHBOARD_JWT_SECRET, -# TOKEN_ENCRYPTION_KEY, GITHUB_APP_PRIVATE_KEY/CLIENT_SECRET, the active provider -# key(s) (ANTHROPIC_API_KEY + OPENAI_API_KEY by default), LANGSMITH_API_KEY_PROD, -# and (prod) GITHUB_WEBHOOK_SECRET + SLACK_SIGNING_SECRET. -put_secret ANTHROPIC_API_KEY # optional — eval judge only (JUDGE_ANTHROPIC_API_KEY fallback); Bedrock builder/reviewer use the host IAM role -put_secret DASHBOARD_JWT_SECRET # REQUIRED — dashboard session JWT signing -put_secret TOKEN_ENCRYPTION_KEY # REQUIRED — Fernet key(s) for GH-token crypto -put_secret GITHUB_APP_PRIVATE_KEY # REQUIRED — GitHub App PEM (multiline; quote it) -put_secret GITHUB_APP_CLIENT_SECRET # REQUIRED — dashboard OAuth login -put_secret LANGSMITH_API_KEY_PROD # REQUIRED (langsmith sandbox) — prod key -put_secret GITHUB_WEBHOOK_SECRET # prod-REQUIRED — GitHub webhook signature -put_secret SLACK_SIGNING_SECRET # prod-REQUIRED — Slack webhook signature -put_secret LINEAR_WEBHOOK_SECRET # required only when Linear is wired -# Optional / conditional secrets — set the ones this deployment actually uses. -put_secret CORRIDOR_API_TOKEN # optional — Corridor MCP -put_secret CORRIDOR_MCP_TOKEN # optional — Corridor MCP (alt name) -put_secret CORRIDOR_TOKEN # optional — Corridor MCP (alt name) -put_secret DAYTONA_API_KEY # only if SANDBOX_TYPE=daytona -put_secret EXA_API_KEY # optional — Exa web search -put_secret FIREWORKS_API_KEY # active non-Claude key (fallback/subagents); Bedrock uses the host IAM role -put_secret GITHUB_PAT # optional — PAT fallback -put_secret JUDGE_ANTHROPIC_API_KEY # optional — eval judge (falls back to ANTHROPIC) -put_secret LANGSMITH_API_KEY # optional — LangSmith (dev) -put_secret LANGCHAIN_API_KEY # optional — LangSmith alt name -put_secret LINEAR_API_KEY # optional — Linear API -put_secret RUNLOOP_API_KEY # only if SANDBOX_TYPE=runloop -put_secret SLACK_BOT_TOKEN # optional — Slack bot token -put_secret SLACK_CLIENT_SECRET # optional — Slack OAuth -put_secret USER_ID_API_KEY_MAP # optional — JSON map user id -> API key (per-user auth) -put_secret X_SERVICE_AUTH_JWT_SECRET # optional — service-auth JWT - -# --- 2) Out-of-band SSM params (/open-swe-/) ------------------------ -# These are NOT created by CDK (operationally-variable / env-specific-unknown). -# put_param creates them on first run. -put_param DEFAULT_SANDBOX_SNAPSHOT_ID # REQUIRED (langsmith) — changes every rebuild -put_param GITHUB_APP_ID # REQUIRED — GitHub App numeric id -put_param GITHUB_APP_INSTALLATION_ID # REQUIRED — GitHub App installation id -put_param GITHUB_APP_CLIENT_ID # REQUIRED — dashboard OAuth client id -put_param GITHUB_OAUTH_PROVIDER_ID # GitHub OAuth provider id -put_param LANGSMITH_TENANT_ID_PROD # LangSmith prod tenant id -put_param LANGSMITH_URL_PROD # LangSmith prod URL -put_param LANGSMITH_ENDPOINT # LangSmith API endpoint -put_param LANGSMITH_ENDPOINT_PROD # LangSmith prod API endpoint -put_param LANGSMITH_HOST_API_URL # LangSmith host API URL -put_param LANGGRAPH_URL # LangGraph server URL -put_param LANGGRAPH_URL_PROD # LangGraph server URL (prod) -put_param LANGCHAIN_REVISION_ID # LangChain revision id -put_param SLACK_CLIENT_ID # Slack OAuth client id -put_param SLACK_TEAM_ID # Slack workspace/team id -put_param SLACK_BOT_USER_ID # Slack bot user id -put_param SLACK_BOT_USERNAME # Slack bot username -put_param SLACK_REPO_OWNER # Slack default repo owner -put_param SLACK_REPO_NAME # Slack default repo name -put_param CONFIGURED_ADMINS # dashboard admin GitHub logins (comma list) -put_param OBSERVABILITY_AUTHORIZED_EMAILS # observability allowlist (comma list) -put_param PUBLIC_REPO_ORG_GATE # public-repo trigger gate (org name; empty=off) -put_param ALLOWED_GITHUB_REPOS # extra repo allowlist (comma list) -put_param LLM_FALLBACK_MODEL_ID # optional — model fallback id -put_param DATADOG_MCP_TOOLSETS # optional — Datadog MCP toolsets -put_param NOTION_MCP_CLIENT_NAME # optional — Notion MCP client name -put_param API_STANDARDS_SKILL_HANDLE # optional — API standards skill handle -put_param REPO_SNAPSHOT_BASE_IMAGE # optional — repo snapshot base image -put_param REPO_SNAPSHOT_BUILD_TIMEOUT_SECONDS # optional — snapshot build timeout -put_param REPO_SNAPSHOT_STALE_BUILD_SECONDS # optional — snapshot stale threshold - -echo "put-config[$ENV]: done. Verify with fetch-config.sh ${ENV} before first boot." diff --git a/deploy/seahaven/seed_store.sh b/deploy/seahaven/seed_store.sh deleted file mode 100755 index dfe1a416..00000000 --- a/deploy/seahaven/seed_store.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -# Seed the LangGraph store after a (re)start. -# -# The stock `langgraph dev` server uses an IN-MEMORY store, so anything written -# to it (team model settings, user mappings) is lost on every restart. This -# script idempotently re-PUTs that state and is wired as a systemd -# ExecStartPost on the open-swe.service unit so it runs after each start. -# IT MUST RE-RUN ON EVERY RESTART — the in-memory store starts empty each boot. -# -# Replace this with Postgres-backed durability (Aegra / `langgraph up`) to make -# the store survive restarts and drop this script. -# -# AWS-env-aware: pass the env as $1 (dev|prod). Seed values (default repo, model -# ids, user mappings) come from the fetch-config-materialized .env. -# -# SECURITY (T5 /sh-security-review): -# - SH-INJ-001: this script NEVER `source`s the .env. python-dotenv and bash -# have incompatible escaping, and a config value like `$(cmd)` would execute -# when sourced. We extract the few single-line seed keys with a non-eval -# reader (read_env) instead. -# - SH-INJ-003: the store PUT bodies are built with `jq --arg`, so values are -# always JSON-encoded (no string interpolation into a JSON heredoc). -# - SH-INJ-004: BASE is pinned to loopback — never derived from store/SSM -# config (LANGGRAPH_URL) — so a tampered value can't redirect the PUTs. -# - SC-02: not sourcing the .env means secrets are never exported into this -# script's (or curl's) environment. -# -# Seed values read from the materialized .env (set in SSM /open-swe-/*): -# DEFAULT_REPO_OWNER / DEFAULT_REPO_NAME -> team_settings default_repo -# LLM_MODEL_ID -> default builder model (fallback) -# SEED_AGENT_MODEL / SEED_AGENT_EFFORT -> builder model + effort (optional) -# SEED_REVIEWER_MODEL / SEED_REVIEWER_EFFORT -> reviewer model + effort (optional) -# SEED_USER_MAPPINGS -> "login:email,login:email" (optional) -# CONFIGURED_ADMINS -> "login,email" fallback for the mapping -# Legacy OPENSWE_* process-env overrides are still honored (highest precedence). -set -euo pipefail - -ENV="${1:-${OPENSWE_ENV:-}}" -case "$ENV" in - dev | prod | "") ;; # empty allowed: pure-env / on-prem backward-compat mode - *) - echo "seed_store: ENV must be 'dev' or 'prod' (got '$ENV')" >&2 - exit 2 - ;; -esac - -ENV_DIR="${ENV_DIR:-/run/open-swe}" -ENV_FILE="${ENV_FILE:-${ENV_DIR}/.env}" - -# read_env KEY -> prints the value of a SINGLE-LINE `KEY="..."` entry from the -# materialized .env WITHOUT shell evaluation (SH-INJ-001 fix). Seed keys are -# simple single-line values; multiline secrets (e.g. the PEM) are never read -# here. Returns empty if the key is absent/unreadable. -read_env() { - local key="$1" line - [ -r "$ENV_FILE" ] || return 0 - line="$(grep -m1 -- "^${key}=" "$ENV_FILE" 2>/dev/null || true)" - [ -n "$line" ] || return 0 - line="${line#*=}" - # strip one layer of surrounding double quotes (python-dotenv double-quoted form) - if [ "${line#\"}" != "$line" ]; then line="${line%\"}"; line="${line#\"}"; fi - # reverse python-dotenv double-quote escaping (only \" and \\ are escaped) - line="${line//\\\"/\"}"; line="${line//\\\\/\\}" - printf '%s' "$line" -} - -# Process-env override (legacy/on-prem) -> .env value -> default. -pick() { # pick DEFAULT OVERRIDE_VALUE FILE_KEY... - local def="$1" override="$2"; shift 2 - if [ -n "$override" ]; then printf '%s' "$override"; return; fi - local k v - for k in "$@"; do v="$(read_env "$k")"; [ -n "$v" ] && { printf '%s' "$v"; return; }; done - printf '%s' "$def" -} - -# BASE is loopback-pinned (SH-INJ-004): this on-box seeder only talks to the -# local server; OPENSWE_PORT may override the port but never the host. -BASE="http://127.0.0.1:${OPENSWE_PORT:-2024}" - -AGENT_MODEL="$(pick 'bedrock_converse:us.anthropic.claude-opus-4-8' "${OPENSWE_AGENT_MODEL:-}" SEED_AGENT_MODEL LLM_MODEL_ID)" -AGENT_EFFORT="$(pick 'high' "${OPENSWE_AGENT_EFFORT:-}" SEED_AGENT_EFFORT)" -REVIEWER_MODEL="$(pick 'bedrock_converse:us.anthropic.claude-opus-4-8' "${OPENSWE_REVIEWER_MODEL:-}" SEED_REVIEWER_MODEL)" -REVIEWER_EFFORT="$(pick 'high' "${OPENSWE_REVIEWER_EFFORT:-}" SEED_REVIEWER_EFFORT)" - -# default_repo = owner/name from AWS config (DEFAULT_REPO_OWNER is hard-pinned -# away from upstream by fetch-config.sh). -REPO_OWNER="$(pick '' '' DEFAULT_REPO_OWNER)" -REPO_NAME="$(pick '' '' DEFAULT_REPO_NAME)" -if [ -n "${OPENSWE_DEFAULT_REPO:-}" ]; then - DEFAULT_REPO="$OPENSWE_DEFAULT_REPO" -elif [ -n "$REPO_OWNER" ] && [ -n "$REPO_NAME" ]; then - DEFAULT_REPO="${REPO_OWNER}/${REPO_NAME}" -else - echo "seed_store: set OPENSWE_DEFAULT_REPO=owner/repo (or DEFAULT_REPO_OWNER + DEFAULT_REPO_NAME in .env)" >&2 - exit 1 -fi - -# user_mappings: explicit SEED_USER_MAPPINGS ("login:email,..."), then legacy -# OPENSWE_OWNER_LOGIN/EMAIL, then parse CONFIGURED_ADMINS ("login,email"). -SEED_MAP="$(pick '' "${SEED_USER_MAPPINGS:-}" SEED_USER_MAPPINGS)" -ADMINS="$(pick '' "${CONFIGURED_ADMINS:-}" CONFIGURED_ADMINS)" -declare -a MAPPINGS=() -if [ -n "$SEED_MAP" ]; then - IFS=',' read -r -a _pairs <<<"$SEED_MAP" - for p in "${_pairs[@]}"; do - p="${p//[[:space:]]/}" - [ -n "$p" ] && MAPPINGS+=("$p") - done -elif [ -n "${OPENSWE_OWNER_LOGIN:-}" ] && [ -n "${OPENSWE_OWNER_EMAIL:-}" ]; then - MAPPINGS+=("${OPENSWE_OWNER_LOGIN}:${OPENSWE_OWNER_EMAIL}") -elif [ -n "$ADMINS" ]; then - _login="" _email="" - IFS=',' read -r -a _toks <<<"$ADMINS" - for t in "${_toks[@]}"; do - t="${t//[[:space:]]/}" - [ -z "$t" ] && continue - case "$t" in - *@*) [ -z "$_email" ] && _email="$t" ;; - *) [ -z "$_login" ] && _login="$t" ;; - esac - done - [ -n "$_login" ] && [ -n "$_email" ] && MAPPINGS+=("${_login}:${_email}") -fi - -# No user mapping is NON-FATAL (OSWE-SEED-03 precedent: never fail the unit into a -# restart loop over a seeding gap — same as the server-not-ready path below). The -# server itself is healthy; an unseeded user_mappings table only means the @openswe -# trigger won't resolve a commenter, which a deployment-validation env (e.g. dev) -# does not need. team_settings is still seeded. Set SEED_USER_MAPPINGS (or -# CONFIGURED_ADMINS / OPENSWE_OWNER_LOGIN+EMAIL) to seed the mapping when wanted. -if [ "${#MAPPINGS[@]}" -eq 0 ]; then - echo "seed_store: no user mapping resolved — skipping user_mappings seed (set SEED_USER_MAPPINGS or OPENSWE_OWNER_LOGIN/EMAIL to enable)" >&2 -fi - -NOW="$(date -u +%Y-%m-%dT%H:%M:%S+00:00)" - -# Wait for the server to accept requests (up to ~60s). Authoritative (OSWE-SEED-03): -# if it never comes up, log and exit 0 — do NOT fail the unit into a restart loop. -READY=0 -for _ in $(seq 1 30); do - if [ "$(curl -s -o /dev/null -w '%{http_code}' "$BASE/ok" || true)" = "200" ]; then READY=1; break; fi - sleep 2 -done -if [ "$READY" -ne 1 ]; then - echo "seed_store: server not ready at $BASE after ~60s; skipping seed (will reseed on next restart)" >&2 - exit 0 -fi - -# 1) team_settings/default — JSON built with jq --arg (SH-INJ-003 fix). -team_body="$(jq -n \ - --arg am "$AGENT_MODEL" --arg ae "$AGENT_EFFORT" \ - --arg rm "$REVIEWER_MODEL" --arg re "$REVIEWER_EFFORT" \ - --arg repo "$DEFAULT_REPO" --arg now "$NOW" \ - '{namespace:["team_settings"],key:"default",value:{ - review_draft_prs:false, pr_summaries:true, review_trace_links:true, - org_guidelines:null, - default_agent_model:$am, default_agent_reasoning_effort:$ae, - default_agent_subagent_model:$am, default_agent_subagent_reasoning_effort:$ae, - default_repo:$repo, - default_reviewer_model:$rm, default_reviewer_reasoning_effort:$re, - default_reviewer_subagent_model:$rm, default_reviewer_subagent_reasoning_effort:$re, - default_grouping_model:null, default_grouping_reasoning_effort:null, - default_chat_model:null, default_chat_reasoning_effort:null, - updated_at:$now}}')" -curl -fsS -X PUT "$BASE/store/items" -H "Content-Type: application/json" -d "$team_body" >/dev/null \ - || echo "seed_store: WARN team_settings PUT failed (will reseed next restart)" >&2 - -# 2) user_mappings/ — required, or the @openswe trigger ignores the commenter. -for pair in "${MAPPINGS[@]}"; do - login="${pair%%:*}" - email="${pair#*:}" - if [ -z "$login" ] || [ -z "$email" ] || [ "$login" = "$pair" ]; then - echo "seed_store: skipping malformed mapping '$pair' (want login:email)" >&2 - continue - fi - map_body="$(jq -n --arg login "$login" --arg email "$email" --arg now "$NOW" \ - '{namespace:["user_mappings"],key:$login,value:{ - github_login:$login, work_email:$email, slack_user_id:null, - source:"slack_oauth", status:"active", created_at:$now, updated_at:$now}}')" - curl -fsS -X PUT "$BASE/store/items" -H "Content-Type: application/json" -d "$map_body" >/dev/null \ - || echo "seed_store: WARN user_mapping PUT failed for $login" >&2 -done - -echo "seed_store: done at $NOW (env=${ENV:-none}, repo=$DEFAULT_REPO, mappings=${#MAPPINGS[@]})" diff --git a/deploy/seahaven/systemd/open-swe.service b/deploy/seahaven/systemd/open-swe.service deleted file mode 100644 index e2fa2034..00000000 --- a/deploy/seahaven/systemd/open-swe.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Open SWE stock LangGraph dev server (graphs + webapp, :2024) -After=network-online.target postgresql.service -Wants=network-online.target - -[Service] -Type=simple -User=adam -WorkingDirectory=/home/adam/open-swe -ExecStart=/home/adam/open-swe/.venv/bin/langgraph dev --host 0.0.0.0 --port 2024 --no-browser --no-reload -ExecStartPost=/home/adam/open-swe/seed_store.sh -Restart=on-failure -RestartSec=5 -TimeoutStartSec=120 - -[Install] -WantedBy=multi-user.target diff --git a/infra/.gitignore b/infra/.gitignore deleted file mode 100644 index ba0cddd0..00000000 --- a/infra/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -# CDK / build output -cdk.out/ -*.js -*.d.ts -*.js.map -# ...but jest.config.js is hand-authored config, not build output — keep it. -!jest.config.js - -# deps -node_modules/ - -# env -.env - -# coverage -coverage/ - -# NOTE: cdk.context.json IS committed on purpose (pins the AMI / lookups so -# deploys are reproducible and don't implicitly pick up a newer AMI — see -# lib/constructs/ami-cache.ts and feedback_inline_ebs_volumes). -!cdk.context.json diff --git a/infra/README.md b/infra/README.md deleted file mode 100644 index fc49e49b..00000000 --- a/infra/README.md +++ /dev/null @@ -1,348 +0,0 @@ -# open-swe infra (CDK TypeScript) - -AWS infrastructure for the Open SWE → AWS migration. **Synth-only at this stage — -nothing here is deployed yet.** All IAM is applied only after the Phase-1 security -gate (T4 GPT-4.1 IAM cross-review + T5 `/sh-security-review`) clears (T6). - -## Layout - -``` -infra/ -├── bin/ -│ └── app.ts # CDK app entry — instantiates the 3 stacks, applies the naming Aspect -├── lib/ -│ ├── config.ts # account/region/org constants, env type, OIDC trust subjects -│ ├── open-swe-iam-stack.ts # account-level: shared OIDC deploy roles -│ ├── open-swe-stack.ts # per-env stack (instance role + config store + AppService) -│ ├── aspects/ -│ │ └── kebab-naming-aspect.ts # fails synth on any non-kebab-case explicit name -│ └── constructs/ -│ ├── github-deploy-roles.ts # githubdeploy-open-swe-infra + githubdeploy-open-swe-app -│ ├── instance-role.ts # open-swe--instance-role (least-privilege) -│ ├── config-store.ts # Secrets Manager + SSM Parameter Store shells (T11) -│ ├── app-service.ts # EC2 box + imported-ALB ingress + Route53 + logs (T12) -│ └── ami-cache.ts # baked open-swe AMI pin (by id) + EBS/replacement docs -├── test/ -│ └── kebab-naming-aspect.test.ts # jest: Aspect passes conforming names, flags bad ones -├── cdk.json -├── cdk.context.json # COMMITTED — {} (AMI is a static id pin; no lookups) -├── package.json # aws-cdk-lib pinned EXACT (2.260.0) -├── tsconfig.json -├── jest.config.js -└── .gitignore -``` - -## Stacks - -| Stack name (kebab) | Construct | Contents | -|---|---|---| -| `open-swe-iam` | `OpenSweIamStack` | Account-level shared GitHub OIDC deploy roles (singletons). | -| `open-swe-dev` | `OpenSweStack` (`envName: dev`) | `open-swe-dev-instance-role`, config store (T11), and the EC2 box + ALB ingress (T12, `AppService`). | -| `open-swe-prod` | `OpenSweStack` (`envName: prod`) | `open-swe-prod-instance-role`, config store, and the EC2 box + ALB ingress. | - -Account `328440206208`, region `us-east-1`. Stack names are set explicitly so CDK -never defaults to PascalCase; resource names follow `open-swe--*`. - -> The two env stacks (`open-swe-dev` / `open-swe-prod`) are the required pair. The -> shared OIDC deploy roles are account-wide singletons (one `RoleName` each), so -> they live in their own dedicated `open-swe-iam` stack rather than being -> duplicated across the env stacks — and that stack deploys first (see ordering). - -## IAM roles defined (unapplied) - -- **`githubdeploy-open-swe-infra`** — GitHub OIDC role for CDK/CFN infra deploys. - Trust scoped to `repo:Sea-Haven-Industries/open-swe` on the `main`/`dev` - branches only. Permission is the org-standard CDK pattern: `sts:AssumeRole` on - the CDK bootstrap roles (`cdk-hnb659fds-*`) — the real CFN/IAM/resource scope - lives in the bootstrap `cfn-exec-role`, not in this role. -- **`githubdeploy-open-swe-app`** — GitHub OIDC role for app deploys. Tag-scoped - `ssm:SendCommand` (instances tagged `project=open-swe` + `env in {dev,prod}`) + - read-only access to the `open-swe--assets` S3 artifact buckets. -- **`open-swe--instance-role`** — EC2 instance role, least-privilege: read - `open-swe--assets` (S3), read `/open-swe-/*` (SSM), read - `open-swe-/*` (Secrets Manager), put `/open-swe//*` CloudWatch Logs, - plus `AmazonSSMManagedInstanceCore` for SSM agent registration. No admin. - -The GitHub OIDC provider already exists account-wide (created for seahaven-site); -it is referenced by ARN, never re-created. - -## CDK bootstrap qualifiers — per-env deploy isolation (B-1 / OSWE-IAC-01) - -Each env's infra deploy role may assume **only its own bootstrap qualifier's** -roles, so a dev-branch token can never assume the bootstrap roles whose admin -`cfn-exec-role` deploys prod (closing the cross-env escalation that bypassed -prod's Environment approval gate). Mapping lives in `config.ts:bootstrapQualifier`: - -| Env | Qualifier | Toolkit stack | Infra role assumes | -|---|---|---|---| -| dev | `oswedev` | `CDKToolkit-oswedev` | `cdk-oswedev-*` | -| prod | `hnb659fds` (default) | `CDKToolkit` | `cdk-hnb659fds-*` | - -The dev stack synthesizes with `DefaultStackSynthesizer({ qualifier: "oswedev" })` -(`bin/app.ts`); prod uses the default. Bootstrap a new env qualifier with: - -```bash -npx cdk bootstrap --qualifier --toolkit-stack-name CDKToolkit- \ - --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \ - aws://328440206208/us-east-1 -``` - -**Deploy order matters** when changing an env's qualifier: bootstrap the new -qualifier and deploy the env stack onto it **before** re-scoping that env's infra -role in `open-swe-iam` — otherwise a pipeline deploy with the re-scoped role would -fail to assume the not-yet-targeted bootstrap roles. - -## Kebab-case naming Aspect - -`KebabNamingAspect` (applied app-wide in `bin/app.ts`) fails synth via -`Annotations.addError` when a stack name or an explicit physical resource name -(`RoleName`, `BucketName`, …) is not kebab-case. Path-style names (Secrets -Manager `a/b`, SSM `/a/b`, log groups `/aws/.../x`) are validated per `/`-segment. -CDK logical construct ids are intentionally NOT validated (they are conventionally -PascalCase). Covered by `test/kebab-naming-aspect.test.ts`. - -## Config store (Secrets Manager + SSM shells — T11) - -`ConfigStore` (`lib/constructs/config-store.ts`, one per env from `OpenSweStack`) -renders the resource shells the boot hook `deploy/seahaven/fetch-config.sh` reads. -The naming contract (T9 inventory + the fetch-config header) is LITERAL env-var -names as the last path segment — `open-swe-/` for secrets, -`/open-swe-/` (FLAT) for config — because fetch-config strips the prefix -and exports that segment verbatim. - -Three buckets: - -1. **Secret shells (Secrets Manager) — 27 secrets.** Created value-LESS (an L1 - `CfnSecret` with NEITHER `secretString` NOR `generateSecretString`, which - CloudFormation creates as an empty secret). The real value is set **out-of-band** - (`put-config.sh`) — CDK never owns it, so a later `cdk deploy` can never clobber - it. `UpdateReplacePolicy/DeletionPolicy: Retain` so a teardown can't destroy - operator-set secret material. AWS-managed key (no CMK — matches the instance - role, which omits `kms:Decrypt`). - > The T9 header says "29 secrets" but its table enumerates **27** distinct VAR - > names (the CORRIDOR row holds 3). We create 27 — we don't invent two to hit 29. - > **Confirm** the 27-vs-29 count (code-only candidates not in the table: - > `USER_ID_API_KEY_MAP`, `JUDGE_ANTHROPIC_BASE_URL`). - - > ⚠️ **`Retain` + fixed name orphans these shells on a failed FIRST create.** - > If the stack's initial create fails and rolls back, `Retain` keeps the shells - > instead of deleting them. The stack is then gone, but the secrets survive, - > still holding the global `open-swe-/` names — so every later create - > fails with `AlreadyExists`. A plain `delete-secret` does **not** clear it - > (the name stays reserved for the 7–30 day recovery window). This bites on a - > **teardown/rebuild, a secret logical-id change/refactor, or standing up a new - > env** — never on routine updates of an already-created stack. **Recovery —** - > before re-creating the stack, force-delete the *empty* orphans so the names - > free immediately: - > ```bash - > aws secretsmanager list-secrets --region us-east-1 \ - > --filters Key=name,Values=open-swe-/ \ - > --query 'SecretList[].Name' --output text | tr '\t' '\n' | while read -r n; do - > aws secretsmanager delete-secret --secret-id "$n" \ - > --region us-east-1 --force-delete-without-recovery - > done - > ``` - > Force-delete only shells with **no value version** — a populated secret holds - > real operator material. (Hit on prod 2026-06-29; see PR #51's deploy failure.) - -2. **IaC-managed SSM config — 8 params, real values owned in code:** - - | Param | dev | prod | - |---|---|---| - | `SANDBOX_TYPE` | `langsmith` | `langsmith` | - | `DEFAULT_REPO_OWNER` | `Sea-Haven-Industries` | `Sea-Haven-Industries` | - | `ALLOWED_GITHUB_ORGS` | `Sea-Haven-Industries` | `Sea-Haven-Industries` | - | `DEFAULT_REPO_NAME` | `open-swe-pilot` *(confirm)* | `open-swe-pilot` *(confirm)* | - | `DASHBOARD_BASE_URL` | `https://openswe-dev.seahaven.com` *(confirm host)* | `https://openswe.seahaven.com` *(confirm host)* | - | `DASHBOARD_API_BASE_URL` | same as base | same as base | - | `DASHBOARD_ALLOWED_ORIGINS` | same as base | same as base | - | `LLM_MODEL_ID` | `bedrock_converse:us.anthropic.claude-opus-4-8` | `bedrock_converse:us.anthropic.claude-opus-4-8` | - -3. **Out-of-band SSM config — NOT created by CDK.** Operationally-variable or - env-specific-unknown values listed in `OUT_OF_BAND_SSM` and populated by - `put-config.sh`. The keystone is `DEFAULT_SANDBOX_SNAPSHOT_ID` (changes on every - snapshot rebuild → must NOT be CDK-managed or a deploy clobbers it); also the - GitHub App ids, Slack ids, and LangSmith tenant/urls. - -### Kebab-Aspect deviation - -`KebabNamingAspect` exempts `AWS::SecretsManager::Secret` and `AWS::SSM::Parameter` -from the kebab check (see the `KEBAB_EXEMPT_RESOURCE_TYPES` set) — the UPPER_SNAKE -env-var segment is a required, documented deviation for a lossless store→env -round-trip. Every other explicitly-named resource is still validated. Covered by a -dedicated case in `test/kebab-naming-aspect.test.ts`. - -### Deploy ordering (values BEFORE the box boots) - -The shells are synth-able now (T11). Population is out-of-band and happens **after** -`cdk deploy open-swe-` but **before** the EC2/T12 box first boots: - -```bash -cdk deploy open-swe- # creates the 27 secret shells + 8 IaC params -deploy/seahaven/put-config.sh # sets the 27 secret values + out-of-band SSM -deploy/seahaven/fetch-config.sh # (on the box) fail-fast verify before first start -``` - -`put-config.sh` ships `` placeholders only (no real secret values committed); -provide each value inline, via `OPENSWE_PUT_` env vars, or from a vault. It does -NOT touch the IaC-managed params (CDK owns those — editing them here would drift). - -## Compute + ingress (`AppService` — T12) - -`AppService` (`lib/constructs/app-service.ts`, one per env from `OpenSweStack`) -builds the box and its path to the internet. A **single** internet-facing ALB -(`app/seahaven-com`) and a **single** VPC are shared with the on-prem -`seahaven-site` stack, so open-swe **imports** the VPC, the ALB security group -(`sg-0b0301deed193258a`), the `:443` listener, and the public `seahaven.com` -zone — and never owns/mutates them. It **adds**: - -- **One ARM64 EC2 box** (`open-swe--box`, `t4g.medium` dev / `t4g.large` - prod) in **private1 (us-east-1a)** — same AZ as the single NAT for in-AZ egress. - `requireImdsv2`, gp3 **encrypted** root, `deleteOnTermination` (no RETAIN - volume — see below). `userDataCausesReplacement: true`; user-data is rendered - from `deploy/ami/user-data.sh`. -- **A standalone instance SG** reachable **only** from the shared ALB SG on `:80` - (nginx). Egress open (NAT). The ALB SG is opened to the box via a **standalone - `CfnSecurityGroupEgress`** so the imported (on-prem-owned) SG is never mutated. -- **A target group → instance `:80`** (nginx is the sole ingress; the LangGraph - control plane stays on loopback `:2024`). Health check `GET /healthz`. -- **Two listener rules** on the imported `:443` listener, both → the same TG: - - **Webhooks** (priority **2** dev / **3** prod): `host ∈ {openswe-, hooks-}.seahaven.com` **AND** path `/webhooks/*`. - - **Site** (priority **10** dev / **11** prod): `host = openswe-.seahaven.com` (dashboard SPA + `/dashboard/api/`). -- **Route53 alias records** `openswe[-dev]` + `hooks[-dev]` → the shared ALB. -- **Four CloudWatch log groups** (`/open-swe//{app,user-data,nginx-access,nginx-error}`) at **30-day** retention (IaC-owned; mirrors the CW-agent config). - -### Listener-rule ordering (load-bearing) - -The shared listener already has a **host-agnostic** `/webhooks/*` PATH rule at -**priority 5** (on-prem). ALB rules are first-match by ascending priority, so the -open-swe webhook rule **must** sit below 5 or every `…/webhooks/*` request (any -host) is forwarded to the on-prem target first. Hence priority 2/3. The rule ANDs -a host condition, so it does **not** steal the on-prem hosts' webhooks. The -dashboard "site" rule carries no path that collides with rule 5, so it sits at -10/11. - -**Cross-stack coordination (T13 review).** The `seahaven-site` (on-prem) and -`open-swe` stacks both add resources to the *same imported* listener and ALB SG. -This is safe: each stack owns only the resources it declares (its own logical -ids), so an on-prem deploy can't delete open-swe's rules/egress and vice-versa, -and the standalone `CfnSecurityGroupEgress` never mutates the shared SG's own -definition (the pattern on-prem itself uses). The one shared namespace that needs -care is **listener-rule priority** (globally unique per listener; a collision is -a fail-*safe* deploy error, not silent drift). Ownership — keep disjoint: -`seahaven-site` = **4-7 + default**; `open-swe` = **2, 3, 10, 11**. open-swe's -webhook rules are host-scoped to its own `*.seahaven.com` hosts, so they never -match an on-prem `seahavenind.com` host. - -### Security review (T5/T12 `/sh-security-review`) - -The T12 surface was run through the detector-fan-out + proof-or-kill verifier. -One **confirmed medium** (OSWE-T12-01: nginx's 1 MB default `client_max_body_size` -would 413 large GitHub webhooks before in-app signature verification) is fixed in -`open-swe.nginx.conf` (`25m` on `/webhooks/`, `10m` on `/dashboard/api/`). The -hooks hostname is scoped to `/webhooks/*` only (OSWE-T12-02 hygiene). An -X-Forwarded-For spoof candidate was **killed** — no code trusts the leftmost XFF. -No confirmed critical/high; no block. - -## Baked AMI + EBS-replacement discipline - -`bakedOpenSweArm64()` (in `lib/constructs/ami-cache.ts`) pins the custom -**open-swe-base-arm64** image by EXACT id (`BAKED_OPEN_SWE_AMI_ID`) via -`MachineImage.genericLinux({ "us-east-1": "" })` — no SSM lookup, so synth -and deploy are fully offline/deterministic. The image is built by -`deploy/ami/open-swe-base.pkr.hcl` (ARM64 Ubuntu 24.04 + uv/py3.12 + nginx + CW -agent + boot templates, **no secrets**); the box's `user-data.sh` assumes that -baked layout (`/opt/open-swe`, `openswe` user, nginx, CW agent). - -Pinning by exact id (vs a `most_recent` name filter) is what prevents a routine -deploy from silently swapping the AMI → **EC2 instance replacement** (the -file-share data-loss root cause — memory `feedback_inline_ebs_volumes`). - -- `userDataCausesReplacement: true` is **deliberate** — user-data is - provisioning-only and carries no durable state. -- **No durable state on the box → no RETAIN volume.** The in-memory langgraph - store is rebuilt on every boot from S3 + Secrets Manager / SSM, so there is - intentionally no standalone `ec2.Volume` + `removalPolicy.RETAIN`. The goal is - replacement-*tolerance*, not avoidance. -- **Snapshot-before-replace** still applies operationally: before any replacing - deploy snapshot the root volume and wait `state=completed`, and re-verify "no - local-only durable state" first. - -Refresh the AMI deliberately: - -```bash -cd deploy/ami && packer build open-swe-base.pkr.hcl # prints the new ami-… id -# update BAKED_OPEN_SWE_AMI_ID in infra/lib/constructs/ami-cache.ts -cd infra && npx cdk diff OpenSweDevStack # WILL show "requires replacement" -``` - -> `cdk.context.json` is `{}` — nothing is resolved via context anymore (the AMI is -> a static id pin), so synth makes no live AWS call. - -## Commands - -```bash -npm install -npx cdk synth open-swe-iam -npx cdk synth open-swe-dev -npx cdk synth open-swe-prod -npm test # jest — naming Aspect -``` - -## CI/CD (T18 — `.github/workflows/ci-infra.yml` + `cd-infra.yml`) - -Path-filtered, OIDC-only (no static keys). The Python agent keeps its own -`ci.yml` ("CI"); these two add the `/infra` half. - -| Workflow | Trigger | Does | -|---|---|---| -| `ci-infra.yml` | PR touching `infra/**` | `tsc` + `jest` + `cdk synth` (reusable `ci-typescript-cdk.yaml`). | -| `cd-infra.yml` | push to `dev`/`main` touching `infra/**`, or dispatch | CI (pre-deploy) → per-env `cdk deploy`. | - -`cd-infra.yml` flow: - -- **push to `dev`** → CI green → **auto** `cdk deploy OpenSweDevStack` (assumes - `githubdeploy-open-swe-infra-dev`; the job declares **no** `environment:`, so the - OIDC subject is `…:ref:refs/heads/dev` — matching that role's trust). -- **push to `main`** → CI green → `cdk deploy OpenSweProdStack` behind the - **`prod` GitHub Environment** (required reviewer = Adam). The `environment: prod` - declaration both fires the manual-approval gate and makes the OIDC subject - `…:environment:prod` — matching `githubdeploy-open-swe-infra-prod`'s trust. - -**Why not the reusable `cd-cdk.yaml`:** it runs `cdk deploy --all`, which from a -single-env push would deploy the *other* env + the shared IAM stack — breaking the -per-env boundary. So CD targets one stack explicitly per env. The shared -`open-swe-iam` stack is **not** deployed by CD (privileged, human-gated — T6). - -**Gating note:** infra CI is enforced at the *deploy* boundary (`cd-infra`'s -`deploy-*` jobs `needs: ci`), not as a branch-protection required check — -path-filtering a *required* check would deadlock app-only PRs (a skipped required -check never satisfies). Making `Infra CI` a required check later needs a -skip-aware shim or dropping its path filter. - -**Prerequisites (set post-T6, when the roles exist):** - -- repo **variables** `AWS_DEPLOY_ROLE_INFRA_DEV` / `AWS_DEPLOY_ROLE_INFRA_PROD` - = the `githubdeploy-open-swe-infra-` role ARNs (`open-swe-iam` outputs). -- a GitHub **Environment** named `prod` with Adam as a required reviewer. - -> App-side CD (CI → S3 artifact → SSM deploy via `githubdeploy-open-swe-app-`) -> is **T19**, not here. - -## Deploy ordering (when the gate clears — NOT yet) - -1. **`open-swe-iam` first** — apply the IAM stack (T6, human-gated), then set the - repo `AWS_DEPLOY_ROLE_INFRA_{DEV,PROD}` variables from its role-ARN outputs and - configure the `prod` Environment reviewer (BLOCK#3). -2. **Security gate** — T4 GPT-4.1 IAM cross-review + T5 `/sh-security-review` on - the synth; resolve every confirmed critical/high. -3. **IAM applied** (T6) — only after the gate. -4. Env stacks: first `open-swe-dev` (T14, manual validate), then CD auto-deploys - dev on push; `open-swe-prod` (T21) behind the `prod` Environment approval. - -## Version policy - -`aws-cdk-lib` is pinned EXACT (`2.260.0`) — no `^`/`~`. Dependabot keeps it -current; CI (`npm ci` + `cdk synth`) + dependency review gate each bump. See -`aws-infrastructure.md` "CDK Version Policy" and memory -`feedback_cdk_lib_bundled_deps`. diff --git a/infra/bin/app.ts b/infra/bin/app.ts deleted file mode 100644 index 720d566b..00000000 --- a/infra/bin/app.ts +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env node -import "source-map-support/register"; -import * as cdk from "aws-cdk-lib"; -import { ACCOUNT, REGION, bootstrapQualifier } from "../lib/config"; -import { OpenSweIamStack } from "../lib/open-swe-iam-stack"; -import { OpenSweStack } from "../lib/open-swe-stack"; -import { KebabNamingAspect } from "../lib/aspects/kebab-naming-aspect"; - -const app = new cdk.App(); -const env = { account: ACCOUNT, region: REGION }; - -// Account-level shared OIDC deploy roles (singletons). Deployed FIRST. -new OpenSweIamStack(app, "OpenSweIamStack", { - stackName: "open-swe-iam", - env, -}); - -// The two env stacks — explicit kebab-case stackName (never let CDK default to -// PascalCase), env-parameterised so resources are `open-swe--*`. -// -// B-1 / OSWE-IAC-01: dev synthesizes against its OWN bootstrap qualifier -// (`oswedev`), so it deploys via the cdk-oswedev-* roles the dev infra role is -// scoped to — and NOT the default hnb659fds bootstrap roles that deploy prod. -// Prod stays on the default qualifier (no synthesizer override). -new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env, - envName: "dev", - synthesizer: new cdk.DefaultStackSynthesizer({ - qualifier: bootstrapQualifier("dev"), - }), -}); - -new OpenSweStack(app, "OpenSweProdStack", { - stackName: "open-swe-prod", - env, - envName: "prod", -}); - -// Fail synth on any non-kebab-case explicit resource/stack name. -cdk.Aspects.of(app).add(new KebabNamingAspect()); - -app.synth(); diff --git a/infra/cdk.context.json b/infra/cdk.context.json deleted file mode 100644 index 0967ef42..00000000 --- a/infra/cdk.context.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/infra/cdk.json b/infra/cdk.json deleted file mode 100644 index feff4c16..00000000 --- a/infra/cdk.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "app": "npx ts-node --prefer-ts-exts bin/app.ts", - "watch": { - "include": ["**"], - "exclude": [ - "README.md", - "cdk*.json", - "**/*.d.ts", - "**/*.js", - "tsconfig.json", - "package*.json", - "node_modules", - "cdk.out" - ] - }, - "context": { - "@aws-cdk/aws-lambda:recognizeLayerVersion": true, - "@aws-cdk/core:checkSecretUsage": true, - "@aws-cdk/core:target-partitions": ["aws"] - } -} diff --git a/infra/jest.config.js b/infra/jest.config.js deleted file mode 100644 index 66512147..00000000 --- a/infra/jest.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - testEnvironment: "node", - roots: ["/test"], - testMatch: ["**/*.test.ts"], - preset: "ts-jest", - transform: { - "^.+\\.tsx?$": ["ts-jest", { tsconfig: "tsconfig.json" }], - }, -}; diff --git a/infra/lib/aspects/kebab-naming-aspect.ts b/infra/lib/aspects/kebab-naming-aspect.ts deleted file mode 100644 index ff0be283..00000000 --- a/infra/lib/aspects/kebab-naming-aspect.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Annotations, CfnResource, IAspect, Stack, Token } from "aws-cdk-lib"; -import { IConstruct } from "constructs"; - -/** - * One "/"-delimited segment must be lower kebab-case: `a-b-c`, digits allowed. - */ -const KEBAB_SEGMENT = /^[a-z0-9]+(-[a-z0-9]+)*$/; - -/** - * CloudFormation property keys that carry an *explicit physical name*. The - * codegen'd L1 stores these either camelCased (`roleName`) or CFN-cased - * (`RoleName`) depending on the construct, so the aspect matches keys - * case-insensitively. - * - * We deliberately validate physical NAMES + the stack name only — not CDK - * logical construct ids (those are conventionally PascalCase, e.g. - * `InfraDeployRole`, and validating them would be wrong). - */ -const NAME_PROPERTY_KEYS = [ - "RoleName", - "BucketName", - "FunctionName", - "TableName", - "LogGroupName", - "QueueName", - "TopicName", - "SecretName", - "StreamName", - "RepositoryName", - "DBInstanceIdentifier", - "DBClusterIdentifier", - "StateMachineName", - "RuleName", - "UserPoolName", -]; -// NOTE: `PolicyName` is intentionally NOT checked — CDK auto-generates inline -// `DefaultPolicy` names (e.g. "InstanceRoleDefaultPolicyF15F...") from the -// logical id; those are not explicit, user-controlled physical names and are -// outside the naming convention's scope. - -const NAME_KEYS_LC = new Set(NAME_PROPERTY_KEYS.map((k) => k.toLowerCase())); - -/** - * Resource types whose physical NAME is a REQUIRED deviation from kebab-case: - * the open-swe config store names secrets `open-swe-/` and SSM - * params `/open-swe-/`, where the last segment is the LITERAL - * UPPER_SNAKE environment-variable name. The boot hook - * (deploy/seahaven/fetch-config.sh) strips the prefix and exports that segment - * verbatim, so a lossless store→env round-trip needs the exact env-var name — - * it cannot be kebab-cased. These two resource types are therefore exempt; every - * OTHER explicitly-named resource is still validated. (The `open-swe-` - * prefix is code-generated from `prefix(env)` and is always kebab-case.) - */ -const KEBAB_EXEMPT_RESOURCE_TYPES = new Set([ - "AWS::SecretsManager::Secret", - "AWS::SSM::Parameter", -]); - -/** - * `true` when every non-empty "/"-delimited segment is kebab-case. - * - * Path-style names are tolerated so the same check works for Secrets Manager - * (`open-swe-dev/foo`), SSM params (`/open-swe-dev/foo`) and log groups - * (`/open-swe/dev/agent`): each segment is validated independently, and a - * leading slash (empty first segment) is ignored. - */ -export function isKebabCase(value: string): boolean { - return value - .split("/") - .filter((seg) => seg.length > 0) - .every((seg) => KEBAB_SEGMENT.test(seg)); -} - -/** - * Aspect that FAILS synth (`Annotations.addError`) when an explicitly-named - * resource — or a stack name — is not kebab-case. Enforces the org naming - * convention (naming-conventions.md) deterministically at synth time so a - * non-conforming name can never reach a deploy. Wired in bin/app.ts via - * `Aspects.of(app).add(new KebabNamingAspect())`. - */ -export class KebabNamingAspect implements IAspect { - public visit(node: IConstruct): void { - if (node instanceof Stack) { - const name = node.stackName; - if (!Token.isUnresolved(name) && !isKebabCase(name)) { - Annotations.of(node).addError( - `Stack name "${name}" is not kebab-case (open-swe naming convention).`, - ); - } - return; - } - - if (node instanceof CfnResource) { - // The config store's Secret/Parameter names carry the literal UPPER_SNAKE - // env-var name per the fetch-config naming contract — a required deviation. - if (KEBAB_EXEMPT_RESOURCE_TYPES.has(node.cfnResourceType)) { - return; - } - // `_cfnProperties` is the props as set on the L1; resolve to collapse any - // intrinsic tokens (refs/getatt) so only literal strings are checked. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const raw = (node as any)._cfnProperties ?? {}; - const resolved = Stack.of(node).resolve(raw) ?? {}; - for (const [key, value] of Object.entries(resolved)) { - if ( - NAME_KEYS_LC.has(key.toLowerCase()) && - typeof value === "string" && - !Token.isUnresolved(value) && - !isKebabCase(value) - ) { - Annotations.of(node).addError( - `Resource "${node.node.path}" property ${key}="${value}" is not kebab-case ` + - `(open-swe naming convention).`, - ); - } - } - } - } -} diff --git a/infra/lib/config.ts b/infra/lib/config.ts deleted file mode 100644 index 41b550bd..00000000 --- a/infra/lib/config.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Shared, non-sensitive constants for the open-swe infra app. - * Account / region are locked per the migration spec (TODO.md "Architecture (locked)"). - */ - -export const ACCOUNT = "328440206208"; -export const REGION = "us-east-1"; - -export const GITHUB_ORG = "Sea-Haven-Industries"; -export const GITHUB_REPO = "open-swe"; - -export type EnvName = "dev" | "prod"; - -/** `open-swe-dev` / `open-swe-prod` — kebab-case stack + resource prefix. */ -export const prefix = (env: EnvName): string => `open-swe-${env}`; - -/** - * Per-ENV GitHub OIDC trust subject for the deploy roles (T5 OSWE-IAC-01/02 fix: - * the dev/prod boundary is enforced in the IAM trust, not by convention). - * - * - `dev` → the `dev` integration branch ref (auto-deploy on push to dev). - * - `prod` → the **GitHub `prod` Environment** subject. A workflow can only mint - * a token with sub `…:environment:prod` by declaring `environment: prod`, - * which triggers the Environment's manual-approval gate (Adam, T18). So the - * prod approval is now expressed at the IAM layer: a dev-branch token can - * never assume a prod deploy role. - * - * Each env gets its OWN infra + app role (githubdeploy-open-swe-{infra,app}-) - * so a dev token cannot reach prod. Exact subject → StringEquals (no `*`). - * - * Cross-env deploy isolation is enforced at the bootstrap layer too — see - * `bootstrapQualifier`: dev runs on its own qualifier so the dev infra role - * cannot assume the bootstrap roles that deploy prod. - */ -export const oidcSubject = (env: EnvName): string => - env === "prod" - ? `repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:prod` - : `repo:${GITHUB_ORG}/${GITHUB_REPO}:ref:refs/heads/dev`; - -/** - * Per-env CDK bootstrap qualifier (B-1 / OSWE-IAC-01 fix). Dev runs on its OWN - * qualifier `oswedev` (bootstrapped into the `CDKToolkit-oswedev` stack), so the - * dev infra deploy role only assumes `cdk-oswedev-*` and can NO LONGER assume the - * default `cdk-hnb659fds-*` set whose admin `cfn-exec-role` deploys prod. Prod - * stays on the default qualifier. This closes the cross-env escalation where a - * dev-branch token could `cdk deploy open-swe-prod` via the shared bootstrap - * roles, bypassing prod's Environment approval gate. - */ -export const DEFAULT_BOOTSTRAP_QUALIFIER = "hnb659fds"; -export const bootstrapQualifier = (env: EnvName): string => - env === "dev" ? "oswedev" : DEFAULT_BOOTSTRAP_QUALIFIER; - -/** - * The GitHub Actions OIDC provider already exists account-wide (created for - * seahaven-site; see .github/oidc-deploy-roles.yaml `CreateOIDCProvider=false`). - * Reference it by ARN — never create a duplicate `AWS::IAM::OIDCProvider` - * (CloudFormation rejects a second provider for the same URL). - */ -export const GITHUB_OIDC_PROVIDER_ARN = `arn:aws:iam::${ACCOUNT}:oidc-provider/token.actions.githubusercontent.com`; diff --git a/infra/lib/constructs/ami-cache.ts b/infra/lib/constructs/ami-cache.ts deleted file mode 100644 index 7687ac07..00000000 --- a/infra/lib/constructs/ami-cache.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as ec2 from "aws-cdk-lib/aws-ec2"; -import { REGION } from "../config"; - -/** - * The baked open-swe base AMI (ARM64 Ubuntu 24.04 + uv/py3.12 + nginx + CW agent - * + boot templates — NO secrets), produced by `deploy/ami/open-swe-base.pkr.hcl`. - * Pinned by EXACT id (not a name filter) so synth/deploy is fully offline and - * deterministic. - * - * Built 2026-06-26 from open-swe-base-arm64-20260626-203433. - * - * ── EBS / AMI replacement discipline (memory feedback_inline_ebs_volumes) ── - * - * Refresh DELIBERATELY: `cd deploy/ami && packer build open-swe-base.pkr.hcl`, - * then update this id. A new id → EC2 instance REPLACEMENT. Pinning by exact id - * (vs a `most_recent` name filter) is what prevents a routine deploy from silently - * swapping the AMI — the root cause of the file-share data-loss incidents - * (5/15, 5/27, 6/5). - * - * `userDataCausesReplacement: true` (AppService) is likewise DELIBERATE: user-data - * is provisioning-only and the box holds NO durable state (the langgraph store is - * in-memory, rebuilt every boot from S3 + Secrets Manager / SSM), so there is - * intentionally no standalone `ec2.Volume` + `removalPolicy.RETAIN`. The design - * goal is replacement-TOLERANCE, not avoidance. - * - * Operational guard before ANY replacing deploy (AMI / userData / instance-type): - * snapshot the root volume AND wait `state=completed`, re-verify "no local-only - * durable state", and review the `cdk diff` replacement at PR time. - */ -export const BAKED_OPEN_SWE_AMI_ID = "ami-00080084502093021"; - -/** The baked open-swe base image, pinned by id (offline, deterministic). */ -export function bakedOpenSweArm64(): ec2.IMachineImage { - return ec2.MachineImage.genericLinux({ [REGION]: BAKED_OPEN_SWE_AMI_ID }); -} diff --git a/infra/lib/constructs/app-service.ts b/infra/lib/constructs/app-service.ts deleted file mode 100644 index 48607575..00000000 --- a/infra/lib/constructs/app-service.ts +++ /dev/null @@ -1,368 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import * as cdk from "aws-cdk-lib"; -import * as ec2 from "aws-cdk-lib/aws-ec2"; -import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; -import * as elbTargets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets"; -import * as logs from "aws-cdk-lib/aws-logs"; -import * as route53 from "aws-cdk-lib/aws-route53"; -import * as iam from "aws-cdk-lib/aws-iam"; -import * as ssm from "aws-cdk-lib/aws-ssm"; -import { Construct, IConstruct } from "constructs"; -import { EnvName, prefix } from "../config"; -import { bakedOpenSweArm64 } from "./ami-cache"; - -/** - * Shared seahaven-vpc + internet-facing ALB facts (read-only recon 2026-06-26; - * scratchpad/T12-infra-facts.md). A SINGLE VPC and a SINGLE shared ALB front - * both the on-prem `seahaven-site` stack and open-swe. We IMPORT every one of - * these and NEVER own them - open-swe only ADDS its own instance SG, a standalone - * ALB-egress rule, listener rules, a target group, and DNS records. - * - * ── Cross-stack coordination on the SHARED listener + ALB SG (T13 review) ── - * Two CDK stacks (seahaven-site, open-swe) add resources to the same imported - * `:443` listener and ALB SG. This is safe because each stack owns ONLY the - * resources it declares (its own logical ids): an on-prem `cdk deploy` computes a - * changeset over its own template and cannot delete rules/egress it never - * declared. The standalone-egress pattern is what on-prem itself uses - * (sgr-0c57812752a3bca13), so it does not mutate the shared SG's own definition. - * - * The ONE shared namespace that REQUIRES coordination is listener-rule PRIORITY - * (globally unique per listener; a collision is a fail-SAFE deploy error, not - * silent drift). Ownership map - keep these disjoint when editing either stack: - * - seahaven-site (on-prem): priorities 4-7 + default. - * - open-swe: priorities 2, 3 (webhooks) and 10, 11 (site). - * open-swe's webhook rules are HOST-scoped to its own *.seahaven.com hosts, so - * they never match (let alone "steal") any seahavenind.com / on-prem host. - */ -const SHARED = { - vpcId: "vpc-0d3d4b67bd0cf8a68", - availabilityZones: ["us-east-1a", "us-east-1b"], - // Private subnets host the EC2 box. The single NAT gateway lives in 1a, so the - // box is pinned to private1 (1a) for in-AZ NAT egress (no cross-AZ data $). - privateSubnetIds: ["subnet-04e38c507e96f1926", "subnet-0a0b4fc6f296dfba5"], - instanceSubnetId: "subnet-04e38c507e96f1926", - instanceAz: "us-east-1a", - albDnsName: "seahaven-com-1856441924.us-east-1.elb.amazonaws.com", - albCanonicalHostedZoneId: "Z35SXDOTRQ7X7K", - albSecurityGroupId: "sg-0b0301deed193258a", - httpsListenerArn: - "arn:aws:elasticloadbalancing:us-east-1:328440206208:listener/app/seahaven-com/222c3257354ab559/bab8bcf0da0e2927", - publicZoneId: "Z06652411XKH89KTZD3XA", - publicZoneName: "seahaven.com", -} as const; - -/** - * Per-env public hostnames, listener-rule priorities, and instance size. - * - * ── Listener-rule ordering hazard (load-bearing) ── - * The shared listener already has a HOST-AGNOSTIC `/webhooks/*` PATH rule at - * priority 5 (the on-prem seahaven-site stack owns it). ALB rules are first-match - * by ASCENDING priority, so a `…/webhooks/*` request to our host would match - * rule 5 (priority 5) and be forwarded to the on-prem target BEFORE any host rule - * at 10+. Therefore our webhook rule MUST sit below priority 5. The catch-all - * "site" rule (dashboard SPA + /dashboard/api/) carries no path that collides - * with rule 5, so it can sit at any free higher number (10/11). Free priorities - * confirmed by recon: 1-3 and 8+ (4=forgejo, 5=/webhooks/*, 6/7=seahavenind). - */ -const ENV_NET: Record< - EnvName, - { - dashboardHost: string; - hooksHost: string; - webhookPriority: number; - sitePriority: number; - instanceType: string; - } -> = { - dev: { - dashboardHost: "openswe-dev.seahaven.com", - hooksHost: "hooks-dev.seahaven.com", - webhookPriority: 2, - sitePriority: 10, - instanceType: "t4g.medium", - }, - prod: { - dashboardHost: "openswe.seahaven.com", - hooksHost: "hooks.seahaven.com", - webhookPriority: 3, - sitePriority: 11, - instanceType: "t4g.large", - }, -}; - -export interface AppServiceProps { - readonly envName: EnvName; - /** Least-privilege EC2 instance role (per-env; from InstanceRole). */ - readonly instanceRole: iam.IRole; - /** S3 artifact key prefix the box pulls app.tar.gz / spa.tar.gz from. */ - readonly artifactPrefix?: string; -} - -/** - * The open-swe compute + ingress wiring for one env (T12): - * - one ARM64 EC2 box in private1 (1a), replacement-tolerant (no RETAIN volume), - * - a standalone instance SG reachable ONLY from the shared ALB SG on :80, - * - a target group -> instance:80 (nginx is the sole ingress; :2024 stays loopback), - * - two listener rules on the imported :443 listener (webhooks below the on-prem - * path rule; site catch-all above it), both -> the same TG, - * - Route53 alias records for both hostnames -> the shared ALB, - * - IaC-owned CloudWatch log groups at 30-day retention. - * - * Everything ALB/VPC/zone-side is IMPORTED. Synth is offline: the AMI is the - * cdk.context.json-pinned AL2023 ARM64 placeholder until the baked - * open-swe-base-arm64 id is pinned before the first real deploy. - */ -export class AppService extends Construct { - public readonly instance: ec2.Instance; - public readonly targetGroup: elbv2.ApplicationTargetGroup; - /** Name of the SSM document CI fires to roll the box to the latest release. */ - public readonly deployDocumentName: string; - - constructor(scope: Construct, id: string, props: AppServiceProps) { - super(scope, id); - const env = props.envName; - const p = prefix(env); - const net = ENV_NET[env]; - const artifactPrefix = props.artifactPrefix ?? "releases/latest"; - - // Import the shared VPC with explicit attributes (no fromLookup -> offline synth). - const vpc = ec2.Vpc.fromVpcAttributes(this, "Vpc", { - vpcId: SHARED.vpcId, - availabilityZones: [...SHARED.availabilityZones], - privateSubnetIds: [...SHARED.privateSubnetIds], - }); - - // Standalone instance SG. Egress open (NAT path); ingress only from the ALB SG. - const instanceSg = new ec2.SecurityGroup(this, "InstanceSg", { - vpc, - securityGroupName: `${p}-instance-sg`, - description: `${p} instance SG - ingress only from the shared ALB SG on :80; egress via NAT.`, - allowAllOutbound: true, - }); - instanceSg.addIngressRule( - ec2.Peer.securityGroupId(SHARED.albSecurityGroupId), - ec2.Port.tcp(80), - `${p}: shared ALB SG to nginx :80`, - ); - // Open the IMPORTED ALB SG to our instance via a STANDALONE egress rule, so we - // never mutate the ALB SG's own (on-prem-owned) definition. - new ec2.CfnSecurityGroupEgress(this, "AlbToInstanceEgress", { - groupId: SHARED.albSecurityGroupId, - ipProtocol: "tcp", - fromPort: 80, - toPort: 80, - destinationSecurityGroupId: instanceSg.securityGroupId, - description: `${p}: ALB to instance nginx :80`, - }); - - // The app-deploy procedure (deploy/ami/deploy.sh) is a normal reviewable repo - // file; CDK base64-encodes it (single line - no `$`/regex-special chars in the - // base64 alphabet) and renders it into user-data's @@DEPLOY_SH_B64@@ token, so - // user-data writes it verbatim to /opt/open-swe/bin/deploy.sh at first boot. - // The same file is run by the open-swe--deploy SSM document on every - // release - a single source of truth for "pull release, build venv, restart". - const deployShPath = path.join(__dirname, "..", "..", "..", "deploy", "ami", "deploy.sh"); - // Minify before embedding: strip full-line comments + blank lines (keep the - // shebang) so the base64 fits EC2's 25.6 KB user-data limit. The repo file - // keeps its comments; only the on-box copy is minified. deploy.sh becomes - // opaque base64 here, so this never affects user-data's heredoc parsing. - const deployShMin = fs - .readFileSync(deployShPath, "utf8") - .split("\n") - .filter((line, i) => i === 0 || (!/^\s*#/.test(line) && line.trim() !== "")) - .join("\n"); - const deployShB64 = Buffer.from(deployShMin, "utf8").toString("base64"); - - // Render the provisioning script's @@tokens@@ into the instance user-data. - // userDataCausesReplacement makes a bootstrap change roll a fresh box (the box - // holds no durable state - see ami-cache.ts / user-data.sh). Editing deploy.sh - // therefore also rolls the box (its base64 is embedded here) - acceptable: the - // box is replacement-tolerant, and ongoing releases never touch user-data. - const userDataPath = path.join(__dirname, "..", "..", "..", "deploy", "ami", "user-data.sh"); - const userData = ec2.UserData.custom( - fs - .readFileSync(userDataPath, "utf8") - // %%...%% tokens are CDK-substituted here; they are DELIBERATELY a - // different delimiter from the @@...@@ tokens user-data.sh seds into the - // baked systemd/nginx templates, so CDK can never clobber a sed pattern - // (a shared @@OPENSWE_ENV@@/@@SERVER_NAME@@ left the unit unsubstituted). - .replace(/%%OPENSWE_ENV%%/g, env) - .replace(/%%ASSETS_BUCKET%%/g, `${p}-assets`) - .replace(/%%SERVER_NAME%%/g, net.dashboardHost) - .replace(/%%ARTIFACT_PREFIX%%/g, artifactPrefix) - .replace(/%%DEPLOY_SH_B64%%/g, deployShB64), - ); - - this.instance = new ec2.Instance(this, "Instance", { - vpc, - vpcSubnets: { - subnets: [ - ec2.Subnet.fromSubnetAttributes(this, "InstanceSubnet", { - subnetId: SHARED.instanceSubnetId, - availabilityZone: SHARED.instanceAz, - }), - ], - }, - instanceType: new ec2.InstanceType(net.instanceType), - // The baked open-swe base AMI (deploy/ami packer build) - ARM64 Ubuntu 24.04 - // with the /opt/open-swe layout, openswe user, nginx, and CW agent that - // user-data.sh assumes. Pinned by exact id (see ami-cache.ts); refresh by - // rebuilding and updating BAKED_OPEN_SWE_AMI_ID. - machineImage: bakedOpenSweArm64(), - role: props.instanceRole, - securityGroup: instanceSg, - userData, - userDataCausesReplacement: true, - requireImdsv2: true, - instanceName: `${p}-box`, - blockDevices: [ - { - deviceName: "/dev/xvda", - // gp3 encrypted root; deleteOnTermination (no durable on-box state -> - // intentionally NO standalone RETAIN volume; see ami-cache.ts). - volume: ec2.BlockDeviceVolume.ebs(30, { - volumeType: ec2.EbsDeviceVolumeType.GP3, - encrypted: true, - deleteOnTermination: true, - }), - }, - ], - }); - - // `requireImdsv2: true` makes CDK auto-create a launch template, and it names - // that LT from the construct id ("Instance" -> "InstanceLaunchTemplate") with NO - // env qualifier — so OpenSweDevStack and OpenSweProdStack both want the identical - // LT name and the second env to deploy fails with - // InvalidLaunchTemplateName.AlreadyExistsException (prod rollback, 2026-06-29). - // Force a per-env LT name. Done via an aspect because the LT is created at synth - // time by the requireImdsv2 handling, not in this constructor. - cdk.Aspects.of(this.instance).add({ - visit(node: IConstruct) { - if (node instanceof ec2.CfnLaunchTemplate) { - node.launchTemplateName = `${p}-lt`; - } - // The instance references the LT BY NAME, so the reference must be renamed in - // lockstep (preserve the GetAtt version) or CFN can't find the template. - if (node instanceof ec2.CfnInstance && node.launchTemplate) { - const spec = node.launchTemplate as ec2.CfnInstance.LaunchTemplateSpecificationProperty; - node.launchTemplate = { ...spec, launchTemplateName: `${p}-lt` }; - } - }, - }); - - // SSM deploy document (open-swe--deploy): runs the baked - // /opt/open-swe/bin/deploy.sh to pull the latest release + restart. CI fires it - // (tag-scoped to project=open-swe,env=) after uploading a release, so the - // app deploy role needs SendCommand ONLY on this document - NOT on the generic - // AWS-RunShellScript (closes the T4 BLOCK#3 arbitrary-shell timebox). - this.deployDocumentName = `${p}-deploy`; - new ssm.CfnDocument(this, "DeployDoc", { - name: this.deployDocumentName, - documentType: "Command", - documentFormat: "YAML", - updateMethod: "NewVersion", - content: { - schemaVersion: "2.2", - description: `Roll the ${p} box to the latest published release (runs /opt/open-swe/bin/deploy.sh).`, - mainSteps: [ - { - action: "aws:runShellScript", - name: "deploy", - inputs: { - // Fixed command - no parameters, so nothing untrusted is interpolated - // into the shell. The script itself reads /etc/open-swe/boot.env. - runCommand: ["bash /opt/open-swe/bin/deploy.sh"], - }, - }, - ], - }, - }); - - // Target group -> instance:80 (nginx). Health check hits nginx's /healthz - // (returns 200; the dashboard TG health path defined in open-swe.nginx.conf). - this.targetGroup = new elbv2.ApplicationTargetGroup(this, "Tg", { - vpc, - targetGroupName: `${p}-tg`, - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.INSTANCE, - targets: [new elbTargets.InstanceTarget(this.instance)], - deregistrationDelay: cdk.Duration.seconds(15), - healthCheck: { - path: "/healthz", - healthyHttpCodes: "200", - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - healthyThresholdCount: 2, - unhealthyThresholdCount: 3, - }, - }); - - // Import the shared :443 listener (with its ALB SG) and ADD our two rules. - const albSg = ec2.SecurityGroup.fromSecurityGroupId(this, "AlbSg", SHARED.albSecurityGroupId, { - mutable: false, - }); - const listener = elbv2.ApplicationListener.fromApplicationListenerAttributes(this, "HttpsListener", { - listenerArn: SHARED.httpsListenerArn, - securityGroup: albSg, - }); - - // (1) Webhooks - accepted on EITHER host (integrations may target either), and - // MUST be below the on-prem path-only rule 5 (see ENV_NET note). - new elbv2.ApplicationListenerRule(this, "WebhooksRule", { - listener, - priority: net.webhookPriority, - conditions: [ - elbv2.ListenerCondition.hostHeaders([net.dashboardHost, net.hooksHost]), - elbv2.ListenerCondition.pathPatterns(["/webhooks/*"]), - ], - action: elbv2.ListenerAction.forward([this.targetGroup]), - }); - // (2) Dashboard SPA + /dashboard/api/ (OAuth) - DASHBOARD host ONLY. The hooks - // host intentionally serves nothing but /webhooks/* (rule 1), so the OAuth / - // dashboard surface stays single-origin (OSWE-T12-02). Non-webhook paths on the - // hooks host fall through to the on-prem default. - new elbv2.ApplicationListenerRule(this, "SiteRule", { - listener, - priority: net.sitePriority, - conditions: [elbv2.ListenerCondition.hostHeaders([net.dashboardHost])], - action: elbv2.ListenerAction.forward([this.targetGroup]), - }); - - // Route53 ALIAS records -> the shared ALB, for both hostnames. - const zone = route53.HostedZone.fromHostedZoneAttributes(this, "PublicZone", { - hostedZoneId: SHARED.publicZoneId, - zoneName: SHARED.publicZoneName, - }); - const albAlias: route53.IAliasRecordTarget = { - bind: () => ({ - dnsName: SHARED.albDnsName, - hostedZoneId: SHARED.albCanonicalHostedZoneId, - }), - }; - for (const [label, host] of [ - ["Dashboard", net.dashboardHost], - ["Hooks", net.hooksHost], - ] as const) { - new route53.ARecord(this, `${label}Alias`, { - zone, - recordName: host, - target: route53.RecordTarget.fromAlias(albAlias), - comment: `${p} ${label.toLowerCase()} -> shared seahaven-com ALB`, - }); - } - - // IaC-owned CloudWatch log groups at 30-day retention. Names mirror the - // CloudWatch-agent config (deploy/ami/templates/amazon-cloudwatch-agent.json); - // owning them here makes retention declarative rather than agent-set. Logs are - // not durable state -> DESTROY on stack delete. - for (const suffix of ["app", "user-data", "nginx-access", "nginx-error"]) { - new logs.LogGroup(this, `Log-${suffix}`, { - logGroupName: `/open-swe/${env}/${suffix}`, - retention: logs.RetentionDays.ONE_MONTH, - removalPolicy: cdk.RemovalPolicy.DESTROY, - }); - } - } -} diff --git a/infra/lib/constructs/assets-bucket.ts b/infra/lib/constructs/assets-bucket.ts deleted file mode 100644 index aa409ad3..00000000 --- a/infra/lib/constructs/assets-bucket.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as s3 from "aws-cdk-lib/aws-s3"; -import { Construct } from "constructs"; -import { EnvName, prefix } from "../config"; - -/** - * The per-env S3 artifact bucket (`open-swe--assets`) the box pulls its - * release from (T7). CI builds the SPA + packages the app source and uploads - * `app.tar.gz` / `spa.tar.gz` under `releases//` + `releases/latest/` - * (`build-artifacts.yml`, via the `githubdeploy-open-swe-app-` OIDC role); - * the box pulls `releases/latest/*` at boot / on deploy via its instance role. - * - * The bucket holds ONLY build artifacts — no secrets (those live in Secrets - * Manager + SSM), no durable runtime state (the langgraph store is in-memory and - * rebuilt every boot). It is therefore safe to treat as reproducible-from-CI, but - * we RETAIN it on stack delete so an accidental `cdk destroy` cannot strand the - * box with no artifact to pull on its next replacement. - * - * Security posture (locked, reviewed in T7): - * - `BLOCK_ALL` public access (this is an internal artifact store; ALB/nginx is - * the only public surface — never S3 directly). - * - SSE-S3 encryption at rest + `enforceSSL` (deny any non-TLS request). - * - versioned, so a bad release can be rolled back to the previous object - * version (the last-good-artifact story in T19); a lifecycle rule expires - * NONcurrent versions after 30 days so history does not grow unbounded. - * - aborts incomplete multipart uploads after 7 days (cost hygiene). - * - * The name is the load-bearing contract: `instance-role.ts` (read), the app - * deploy role in `github-deploy-roles.ts` (write), and `user-data.sh` / - * `deploy.sh` (`@@ASSETS_BUCKET@@`) all reference `open-swe--assets` by - * literal name, so it is set explicitly here rather than auto-generated. - */ -export class AssetsBucket extends Construct { - public readonly bucket: s3.Bucket; - - constructor(scope: Construct, id: string, envName: EnvName) { - super(scope, id); - const p = prefix(envName); - - this.bucket = new s3.Bucket(this, "Bucket", { - bucketName: `${p}-assets`, - blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, - encryption: s3.BucketEncryption.S3_MANAGED, - enforceSSL: true, - versioned: true, - // Artifacts are reproducible from CI, but RETAIN protects against an - // accidental stack delete leaving the box with nothing to pull (see above). - removalPolicy: cdk.RemovalPolicy.RETAIN, - lifecycleRules: [ - { - id: "expire-noncurrent-artifact-versions", - noncurrentVersionExpiration: cdk.Duration.days(30), - abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), - }, - ], - }); - } -} diff --git a/infra/lib/constructs/config-store.ts b/infra/lib/constructs/config-store.ts deleted file mode 100644 index 099749ae..00000000 --- a/infra/lib/constructs/config-store.ts +++ /dev/null @@ -1,265 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import * as ssm from "aws-cdk-lib/aws-ssm"; -import { Construct } from "constructs"; -import { EnvName } from "../config"; - -/** - * Config / secret "shells" for the boot hook (`deploy/seahaven/fetch-config.sh`). - * - * The naming contract (source of truth: the T9 env/secret/config inventory + the - * fetch-config header) is LITERAL env-var names as the last path segment: - * - * Secrets open-swe-/ (AWS Secrets Manager) - * Config /open-swe-/ (AWS SSM Parameter Store, FLAT) - * - * fetch-config reads secrets with `batch-get-secret-value --filters - * Key=name,Values=open-swe-/` and config with `get-parameters-by-path - * --path /open-swe-/` (NON-recursive), then strips the prefix so the last - * segment IS the exported variable name. So these resources MUST carry the - * UPPER_SNAKE env-var name verbatim — which is why both resource types are - * exempted from the kebab-naming Aspect (see aspects/kebab-naming-aspect.ts). - * - * Three buckets: - * - * 1. SECRETS_SHELLS — the 29 secrets. Created as value-LESS shells (an L1 - * `CfnSecret` with NEITHER `secretString` NOR `generateSecretString`, which - * CloudFormation creates as an empty secret with no version). The real value - * is set out-of-band via `deploy/seahaven/put-config.sh` (put-secret-value) - * BEFORE the box boots. Because CDK never owns the value, a later - * `cdk deploy` can never clobber the operator-set value. AWS-managed key - * (alias/aws/secretsmanager) — no CMK, matching the instance role which - * deliberately omits kms:Decrypt. - * - * 2. IAC_MANAGED_SSM — stable / derivable config. Real values are owned here in - * IaC (one StringParameter each) so they are reproducible and reviewed. - * - * 3. Out-of-band SSM (NOT created here) — operationally-variable or - * env-specific-unknown config (e.g. DEFAULT_SANDBOX_SNAPSHOT_ID, which - * changes on every snapshot rebuild and would be clobbered by a deploy if it - * were CDK-managed; GitHub App ids; Slack ids; LangSmith tenant/urls). These - * are listed in OUT_OF_BAND_SSM purely for documentation and are set by - * `put-config.sh`, never by CDK. - */ - -/** The 29 Secrets Manager secret VAR names (T9 inventory SECRETS table). */ -export const SECRET_VARS: readonly string[] = [ - "ANTHROPIC_API_KEY", - "CORRIDOR_API_TOKEN", - "CORRIDOR_MCP_TOKEN", - "CORRIDOR_TOKEN", - "DASHBOARD_JWT_SECRET", - "DAYTONA_API_KEY", - "EXA_API_KEY", - "FIREWORKS_API_KEY", - "GITHUB_APP_CLIENT_SECRET", - "GITHUB_APP_PRIVATE_KEY", - "GITHUB_PAT", - "GITHUB_WEBHOOK_SECRET", - "JUDGE_ANTHROPIC_API_KEY", - "LANGSMITH_API_KEY", - "LANGSMITH_API_KEY_PROD", - "LANGCHAIN_API_KEY", - "LINEAR_API_KEY", - "LINEAR_WEBHOOK_SECRET", - "RUNLOOP_API_KEY", - "SLACK_BOT_TOKEN", - "SLACK_CLIENT_SECRET", - "SLACK_SIGNING_SECRET", - "TOKEN_ENCRYPTION_KEY", - "USER_ID_API_KEY_MAP", - "X_SERVICE_AUTH_JWT_SECRET", -] as const; -// 25 secret shells (OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY removed in the -// Bedrock/Fireworks migration — those providers are dropped from SUPPORTED_MODELS and -// their keys revoked + secret objects deleted). The T9 inventory header said "29" vs -// 27 enumerated; reconciled -// (Adam confirm 2026-06-26): USER_ID_API_KEY_MAP (maps user ids -> API keys; flagged -// sensitive by the T5 security review) is a SECRET and is included here. -// JUDGE_ANTHROPIC_BASE_URL is a URL (non-sensitive config, eval-only) -> SSM/default, -// NOT a secret. So the inventory's "29" was effectively a miscount. - -/** Short, value-free descriptions for the secret shells (no secret material). */ -const SECRET_DESCRIPTIONS: Record = { - ANTHROPIC_API_KEY: "Claude LLM API key (primary builder provider).", - CORRIDOR_API_TOKEN: "Corridor MCP token (optional).", - CORRIDOR_MCP_TOKEN: "Corridor MCP token alt name (optional).", - CORRIDOR_TOKEN: "Corridor MCP token alt name (optional).", - DASHBOARD_JWT_SECRET: "JWT signing secret for dashboard session cookies (REQUIRED).", - DAYTONA_API_KEY: "Daytona sandbox key (only if SANDBOX_TYPE=daytona).", - EXA_API_KEY: "Exa web-search key (optional).", - FIREWORKS_API_KEY: "Fireworks LLM key (only if a fireworks: model is used).", - GITHUB_APP_CLIENT_SECRET: "GitHub App OAuth client secret (dashboard login).", - GITHUB_APP_PRIVATE_KEY: "GitHub App private key PEM (installation-token minting).", - GITHUB_PAT: "GitHub PAT fallback (optional).", - GITHUB_WEBHOOK_SECRET: "GitHub webhook signature secret (prod-required).", - JUDGE_ANTHROPIC_API_KEY: "Eval judge key (optional; falls back to ANTHROPIC_API_KEY).", - LANGSMITH_API_KEY: "LangSmith key (dev).", - LANGSMITH_API_KEY_PROD: "LangSmith key (prod / deployed sandbox).", - LANGCHAIN_API_KEY: "LangSmith key alt name (fallback).", - LINEAR_API_KEY: "Linear API key (optional).", - LINEAR_WEBHOOK_SECRET: "Linear webhook signature secret (required when Linear is wired).", - RUNLOOP_API_KEY: "Runloop sandbox key (only if SANDBOX_TYPE=runloop).", - SLACK_BOT_TOKEN: "Slack bot token (optional).", - SLACK_CLIENT_SECRET: "Slack OAuth client secret.", - SLACK_SIGNING_SECRET: "Slack webhook signing secret (prod-required).", - TOKEN_ENCRYPTION_KEY: "Fernet key(s) for per-user GitHub-token encryption (REQUIRED).", - USER_ID_API_KEY_MAP: "JSON map of user id -> API key for per-user auth (optional, sensitive).", - X_SERVICE_AUTH_JWT_SECRET: "Service-auth JWT secret (optional).", -}; - -/** - * IaC-managed SSM config: stable / derivable values owned in code, per env. - * Values are functions of envName so dev/prod render correct hosts. - * - * Anything operationally-variable or env-specific-unknown is deliberately NOT - * here — see OUT_OF_BAND_SSM. - */ -export function iacManagedSsm(env: EnvName): Record { - // Public host = seahaven.com (the migration's new AWS public face; confirmed by - // recon: seahaven.com Route53 zone + *.seahaven.com ACM cert are live on the ALB - // — distinct from the on-prem seahavenind.com). dev = openswe-dev, prod = openswe. - const host = `https://openswe${env === "dev" ? "-dev" : ""}.seahaven.com`; - // Repo targeting is PER-ENV: dev drives the disposable sandbox repo in the - // dedicated seahaven-open-swe-dev org (isolates dev-agent activity from the real - // Sea Haven org); prod stays on the Sea Haven org pilot repo. fetch-config's owner - // GUARD honors this value (rejecting only blank / the upstream langchain-ai org). - const repo = - env === "dev" - ? { owner: "seahaven-open-swe-dev", name: "openswe-dev-sandbox" } - : { owner: "Sea-Haven-Industries", name: "open-swe-pilot" }; - const managed: Record = { - // Sandbox provider — plan keeps stock langsmith (T9). Stable. - SANDBOX_TYPE: "langsmith", - DEFAULT_REPO_OWNER: repo.owner, - // Repo-owner allowlist (comma list) — the env's org. The app gates triggers on this. - ALLOWED_GITHUB_ORGS: repo.owner, - DEFAULT_REPO_NAME: repo.name, - // Dashboard URLs — derived from the public host. - DASHBOARD_BASE_URL: host, - DASHBOARD_API_BASE_URL: host, - DASHBOARD_ALLOWED_ORIGINS: host, - // Primary builder model. seed_store.sh's `pick` precedence is - // OPENSWE_AGENT_MODEL > SEED_AGENT_MODEL > LLM_MODEL_ID > script default, so this - // SSM value overrides the seed-script default — it MUST be a supported id. Post - // Bedrock/Fireworks migration the only Bedrock-Claude id is the inference profile; - // `anthropic:claude-opus-4-8` was removed from SUPPORTED_MODELS. - LLM_MODEL_ID: "bedrock_converse:us.anthropic.claude-opus-4-8", - }; - // Dev e2e smoke: seed the owner's user_mapping so an @openswe comment from the - // triggering GitHub login resolves (an unmapped commenter is silently skipped). - // Dev-only — prod seeds its mappings via its own operator config. login:email. - if (env === "dev") { - managed.SEED_USER_MAPPINGS = "amoussa1229:adam@seahavenind.com"; - } - return managed; -} - -/** - * Out-of-band SSM config: NOT created by CDK. Listed for documentation and for - * `put-config.sh` to populate before the box boots. Each MUST stay out of IaC - * because its value is operationally-variable or env-specific and unknown at - * synth time — making it CDK-managed would either clobber the operator value on - * the next deploy (e.g. DEFAULT_SANDBOX_SNAPSHOT_ID) or hardcode a secret-ish id. - */ -export const OUT_OF_BAND_SSM: readonly string[] = [ - // Sandbox snapshot id — changes on EVERY snapshot rebuild. MUST NOT be - // CDK-managed or a deploy clobbers it. Required for SANDBOX_TYPE=langsmith. - "DEFAULT_SANDBOX_SNAPSHOT_ID", - // GitHub App identifiers — set when the per-env GitHub App is created. - "GITHUB_APP_ID", - "GITHUB_APP_CLIENT_ID", - "GITHUB_APP_INSTALLATION_ID", - "GITHUB_OAUTH_PROVIDER_ID", - // LangSmith deployment coordinates (prod tenant/urls/endpoints). - "LANGSMITH_TENANT_ID_PROD", - "LANGSMITH_URL_PROD", - "LANGSMITH_ENDPOINT", - "LANGSMITH_ENDPOINT_PROD", - "LANGSMITH_HOST_API_URL", - "LANGGRAPH_URL", - "LANGGRAPH_URL_PROD", - "LANGCHAIN_REVISION_ID", - // Slack workspace ids — set after the Slack app is installed. - "SLACK_CLIENT_ID", - "SLACK_TEAM_ID", - "SLACK_BOT_USER_ID", - "SLACK_BOT_USERNAME", - "SLACK_REPO_OWNER", - "SLACK_REPO_NAME", - // Access / observability allowlists — operator-curated. - "CONFIGURED_ADMINS", - "OBSERVABILITY_AUTHORIZED_EMAILS", - "PUBLIC_REPO_ORG_GATE", - "ALLOWED_GITHUB_REPOS", - // Optional integrations + tuning knobs (left to code defaults unless set). - "LLM_FALLBACK_MODEL_ID", - "DATADOG_MCP_TOOLSETS", - "NOTION_MCP_CLIENT_NAME", - "API_STANDARDS_SKILL_HANDLE", - "REPO_SNAPSHOT_BASE_IMAGE", - "REPO_SNAPSHOT_BUILD_TIMEOUT_SECONDS", - "REPO_SNAPSHOT_STALE_BUILD_SECONDS", -] as const; - -export interface ConfigStoreProps { - readonly envName: EnvName; -} - -/** - * Per-env Secrets Manager + SSM Parameter Store shells the boot hook reads. - * Instantiated from OpenSweStack. Synth-able now (T11); values populated - * out-of-band BEFORE the EC2/T12 deploy. See infra/README.md "Config store". - */ -export class ConfigStore extends Construct { - public readonly secrets: secretsmanager.CfnSecret[] = []; - public readonly params: ssm.StringParameter[] = []; - - constructor(scope: Construct, id: string, props: ConfigStoreProps) { - super(scope, id); - const env = props.envName; - - // --- 1) Secret shells (value-LESS; populated out-of-band) ---------------- - for (const varName of SECRET_VARS) { - const secret = new secretsmanager.CfnSecret(this, `Secret-${varName}`, { - name: `open-swe-${env}/${varName}`, - description: SECRET_DESCRIPTIONS[varName] ?? `open-swe ${varName}`, - // Deliberately NO secretString / generateSecretString: CloudFormation - // creates an empty secret, so the out-of-band value is never clobbered. - }); - // RETAIN: a stack teardown must not destroy operator-set secret material. - // - // GOTCHA — RETAIN + fixed name orphans these shells on a FAILED FIRST - // CREATE. If the stack's initial create fails and rolls back, RETAIN keeps - // the shells instead of deleting them; the stack is then gone but the - // secrets survive, still holding the global `open-swe-/` names. - // Every later create then fails with `AlreadyExists` (and a normal - // delete-secret keeps the name reserved for the 7–30 day recovery window, - // so it does NOT clear the deadlock). Recovery: before re-creating the - // stack, force-delete the orphans so the names free immediately, e.g. - // aws secretsmanager list-secrets --filters Key=name,Values=open-swe-/ \ - // --query 'SecretList[].Name' --output text | tr '\t' '\n' | while read n; do - // aws secretsmanager delete-secret --secret-id "$n" \ - // --force-delete-without-recovery; done - // Only force-delete shells that are EMPTY (no value version) — a populated - // secret holds real operator material. This bites on teardown/rebuild, a - // secret logical-id change/refactor, or standing up a new env — NOT on - // routine updates of an already-created stack. (Hit on prod 2026-06-29.) - secret.applyRemovalPolicy(cdk.RemovalPolicy.RETAIN); - this.secrets.push(secret); - } - - // --- 2) IaC-managed SSM config (real, derivable values) ------------------ - const managed = iacManagedSsm(env); - for (const [varName, value] of Object.entries(managed)) { - this.params.push( - new ssm.StringParameter(this, `Param-${varName}`, { - parameterName: `/open-swe-${env}/${varName}`, - stringValue: value, - description: `IaC-managed open-swe ${varName} (${env}).`, - tier: ssm.ParameterTier.STANDARD, - }), - ); - } - } -} diff --git a/infra/lib/constructs/github-deploy-roles.ts b/infra/lib/constructs/github-deploy-roles.ts deleted file mode 100644 index e383a568..00000000 --- a/infra/lib/constructs/github-deploy-roles.ts +++ /dev/null @@ -1,173 +0,0 @@ -import * as iam from "aws-cdk-lib/aws-iam"; -import { Construct } from "constructs"; -import { - ACCOUNT, - EnvName, - GITHUB_OIDC_PROVIDER_ARN, - REGION, - bootstrapQualifier, - oidcSubject, -} from "../config"; - -/** - * Per-ENV GitHub Actions OIDC deploy roles. Created ONCE per env in the - * dedicated `open-swe-iam` stack. Two roles per env, per the locked architecture's - * "dual OIDC roles": - * - * - githubdeploy-open-swe-infra- → CFN/IAM (CDK) deploys of that env's stack - * - githubdeploy-open-swe-app- → app deploys (env-tag-scoped SSM + S3 read) - * - * T5 OSWE-IAC-01/02 fix: roles are split per env and the trust subject is - * env-scoped (dev = dev branch ref; prod = the GitHub `prod` Environment subject, - * so the manual-approval gate is IAM-enforced). A dev-branch token therefore - * cannot SendCommand to the prod box nor assume a prod deploy role. - * - * Reviewed at T4 (GPT-4.1 IAM cross-review) + T5 (/sh-security-review) and - * deployed FIRST (BLOCK#3 "OIDC-role-first" ordering) before any other infra or - * secrets CI step. - */ -export class GithubDeployRoles extends Construct { - public readonly infraRole: iam.Role; - public readonly appRole: iam.Role; - - constructor(scope: Construct, id: string, envName: EnvName) { - super(scope, id); - - // The provider already exists account-wide — reference, never re-create. - const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn( - this, - "GithubOidcProvider", - GITHUB_OIDC_PROVIDER_ARN, - ); - - // T4 BLOCK#2 + T5 IAC-01/02: exact env-scoped subject via StringEquals (no - // StringLike, no `*`). prod = environment:prod (manual-approval gate), - // dev = the dev branch ref. - const trust = new iam.WebIdentityPrincipal(provider.openIdConnectProviderArn, { - StringEquals: { - "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", - "token.actions.githubusercontent.com:sub": oidcSubject(envName), - }, - }); - - // ---- githubdeploy-open-swe-infra- -------------------------------- - this.infraRole = new iam.Role(this, "InfraDeployRole", { - roleName: `githubdeploy-open-swe-infra-${envName}`, - assumedBy: trust, - description: `GitHub OIDC role for CDK deploys of the open-swe-${envName} infra stack (assumes CDK bootstrap roles).`, - }); - - // Org-standard CDK deploy pattern (mirrors githubdeploy-seahaven-account- - // baseline / -forgejo / -apm-wo-analysis): the deploy role only needs to - // assume the CDK bootstrap roles. The actual CloudFormation + IAM + resource - // permissions are exercised by the bootstrap `cfn-exec-role`, whose scope is - // owned by the CDKToolkit stack — NOT granted directly here. - // - // B-1 / OSWE-IAC-01 fix: scope the assume to THIS env's bootstrap qualifier. - // Dev uses `oswedev` (its own CDKToolkit-oswedev bootstrap), prod uses the - // default `hnb659fds`. The dev infra role can therefore no longer assume the - // bootstrap roles whose admin cfn-exec-role deploys prod — closing the prior - // cross-env escalation (a dev-branch token could `cdk deploy open-swe-prod` - // via the shared account-wide bootstrap roles, bypassing prod's Environment - // approval gate). The qualifier wildcard still matches only the handful of - // roles `cdk bootstrap` creates for that qualifier. - this.infraRole.addToPolicy( - new iam.PolicyStatement({ - sid: "AssumeCdkBootstrapRoles", - actions: ["sts:AssumeRole"], - resources: [`arn:aws:iam::${ACCOUNT}:role/cdk-${bootstrapQualifier(envName)}-*`], - }), - ); - - // ---- githubdeploy-open-swe-app- ---------------------------------- - this.appRole = new iam.Role(this, "AppDeployRole", { - roleName: `githubdeploy-open-swe-app-${envName}`, - assumedBy: trust, - description: `GitHub OIDC role for open-swe-${envName} app deploys: env-tag-scoped ssm:SendCommand + read of the ${envName} S3 artifact bucket.`, - }); - - // T5 OSWE-IAC-01 fix: SendCommand only to instances tagged project=open-swe - // AND env= (a SINGLE value, not {dev,prod}). The dev app role can - // never command the prod box and vice versa — env isolation in IAM. - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "SsmSendCommandTagScoped", - actions: ["ssm:SendCommand"], - resources: [`arn:aws:ec2:${REGION}:${ACCOUNT}:instance/*`], - conditions: { - StringEquals: { - "ssm:resourceTag/project": "open-swe", - "ssm:resourceTag/env": envName, - }, - }, - }), - ); - - // SendCommand also has to reference the command document. Scope to this env's - // open-swe deploy document ONLY. - // T4 BLOCK#3 (CLOSED at T19): GPT-4.1 flagged AWS-RunShellScript as an - // arbitrary-shell escalation path. The dedicated `open-swe-${envName}-deploy` - // SSM document (app-service.ts) now runs the fixed, parameter-less command - // `bash /opt/open-swe/bin/deploy.sh`, so AWS-RunShellScript is dropped here: - // this role can run ONLY that one document, and only on its own env's box - // (tag-scoped by the SsmSendCommandTagScoped statement above). - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "SsmSendCommandDocuments", - actions: ["ssm:SendCommand"], - resources: [`arn:aws:ssm:${REGION}:${ACCOUNT}:document/open-swe-${envName}-deploy`], - }), - ); - - // Poll command results. These read actions do not support resource-level - // scoping, so `*` is required by the API (T4 FIX: API limitation, documented). - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "SsmReadCommandStatus", - actions: [ - "ssm:GetCommandInvocation", - "ssm:ListCommands", - "ssm:ListCommandInvocations", - ], - resources: ["*"], - }), - ); - - // Read+WRITE access to THIS env's artifact bucket only (T19): the - // build-artifacts workflow uploads app.tar.gz / spa.tar.gz under releases/*, - // then fires the deploy document so the box pulls them via its instance role. - // Object actions are scoped to releases/* (the only prefix CI writes), and to - // THIS env's bucket — a dev token can never write the prod bucket. No - // bucket-level mutation (no PutBucket*/Delete bucket) — that stays with CDK. - // GetObject + PutObject (S3-to-S3 copy = Get source + Put dest) is all the - // publish/rollback path uses; s3:DeleteObject is deliberately NOT granted so a - // CI token cannot erase an immutable release or the releases/last-good rollback - // fallback (lifecycle expiry handles old-version cleanup, not CI). - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "ReadWriteArtifactObjects", - actions: ["s3:GetObject", "s3:PutObject"], - resources: [`arn:aws:s3:::open-swe-${envName}-assets/releases/*`], - }), - ); - // ListBucket is constrained to the releases/ prefix (F-1/IAC-04): the - // publish/rollback scripts only ever list under releases/, so a leaked CI - // token cannot enumerate anything else in the bucket. GetBucketLocation - // carries no s3:prefix, so it stays a separate, unconditioned statement. - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "ListArtifactBucket", - actions: ["s3:ListBucket"], - resources: [`arn:aws:s3:::open-swe-${envName}-assets`], - conditions: { StringLike: { "s3:prefix": ["releases/*"] } }, - }), - ); - this.appRole.addToPolicy( - new iam.PolicyStatement({ - sid: "GetArtifactBucketLocation", - actions: ["s3:GetBucketLocation"], - resources: [`arn:aws:s3:::open-swe-${envName}-assets`], - }), - ); - } -} diff --git a/infra/lib/constructs/instance-role.ts b/infra/lib/constructs/instance-role.ts deleted file mode 100644 index 21d18859..00000000 --- a/infra/lib/constructs/instance-role.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as iam from "aws-cdk-lib/aws-iam"; -import { Construct } from "constructs"; -import { ACCOUNT, EnvName, REGION, prefix } from "../config"; - -/** - * Least-privilege EC2 instance role for the open-swe box (one per env). - * - * Grants exactly what the boot/runtime flow needs and NOTHING ELSE — no admin, - * no `*` resources except where the AWS action genuinely has no resource-level - * scoping. Per-env so the dev box can never read prod secrets/config and vice - * versa. Reviewed at T4 (GPT-4.1 IAM cross-review) / T5 (/sh-security-review) - * before it is ever deployed (T6). - */ -export class InstanceRole extends Construct { - public readonly role: iam.Role; - - constructor(scope: Construct, id: string, env: EnvName) { - super(scope, id); - const p = prefix(env); - - this.role = new iam.Role(this, "Role", { - roleName: `${p}-instance-role`, - assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), - description: `EC2 instance role for the ${p} open-swe box (least-privilege).`, - }); - - // AWS-managed: lets the SSM agent register the instance and RECEIVE the - // app-deploy `ssm:SendCommand` from githubdeploy-open-swe-app. This is the - // standard Session-Manager / RunCommand grant and is the only managed - // policy on the role. DELIBERATE — flag for T4 confirmation. - this.role.addManagedPolicy( - iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"), - ); - - // Read the build artifact from the env's S3 asset bucket (deploy = pull). - // Scoped to releases/* — the only prefix CI writes and the box pulls — so a - // compromised box (or stolen IMDS creds) cannot read anything else that might - // ever land in the bucket (least-privilege; mirrors the app role's write scope). - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "ReadArtifactObjects", - actions: ["s3:GetObject"], - resources: [`arn:aws:s3:::${p}-assets/releases/*`], - }), - ); - // ListBucket is constrained to the releases/ prefix (F-1/IAC-04) — the box - // only ever lists release artifacts, so a compromised box cannot enumerate - // any other object that might land in the bucket. GetBucketLocation has no - // s3:prefix in its request context, so it stays a separate, unconditioned - // statement (the condition would otherwise AccessDeny it). - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "ListArtifactBucket", - actions: ["s3:ListBucket"], - resources: [`arn:aws:s3:::${p}-assets`], - conditions: { StringLike: { "s3:prefix": ["releases/*"] } }, - }), - ); - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "GetArtifactBucketLocation", - actions: ["s3:GetBucketLocation"], - resources: [`arn:aws:s3:::${p}-assets`], - }), - ); - - // Read non-sensitive config from SSM Parameter Store under /open-swe-/*. - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "ReadSsmConfig", - actions: ["ssm:GetParameter", "ssm:GetParameters", "ssm:GetParametersByPath"], - resources: [`arn:aws:ssm:${REGION}:${ACCOUNT}:parameter/${p}/*`], - }), - ); - - // VALUE access — Secrets Manager under open-swe-/*. Secret ARNs carry a - // random 6-char suffix, hence the trailing `*`. This is the statement that - // actually gates which secret VALUES the box can read: prefix-scoped, so the - // dev box can never read prod secret values (and vice versa). GetSecretValue is - // checked per-secret even when the value is returned via the batch call below. - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "ReadSecretValues", - actions: ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"], - resources: [`arn:aws:secretsmanager:${REGION}:${ACCOUNT}:secret:${p}/*`], - }), - ); - - // BatchGetSecretValue MUST be granted on `*` — it is a collection action that - // AWS authorizes against the account, NOT the per-secret ARN, REGARDLESS of - // whether the caller uses `--filters` or `--secret-id-list`. A prefix-scoped - // BatchGetSecretValue AccessDenies the whole call ("no identity-based policy - // allows the secretsmanager:BatchGetSecretValue action") — VERIFIED on the live - // dev box 2026-06-29 (the OSWE-IAC-SECRETS-LIST-01 attempt to prefix-scope it - // crash-looped the box once the prior broad grant's eventual-consistency lapsed). - // This `*` does NOT widen VALUE access: a secret value is only returned when the - // prefix-scoped GetSecretValue above also allows it, so cross-env value isolation - // holds. The win that DID survive: fetch-config uses `--secret-id-list` (explicit - // names, no name filter), so `secretsmanager:ListSecrets` is NOT needed and is - // intentionally omitted — the box cannot enumerate secret names account-wide. - // F-2 (accepted residual): because the grant is `*`, a caller naming a secret - // in ANOTHER env's prefix learns whether that name EXISTS (an existence oracle - // via the per-secret AccessDenied-vs-not signal) even though the VALUE stays - // gated by the prefix-scoped GetSecretValue above. Accepted within Sea Haven's - // single-tenant account 328440206208 — cross-env VALUE isolation is preserved. - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "BatchGetSecretValues", - actions: ["secretsmanager:BatchGetSecretValue"], - resources: ["*"], - }), - ); - - // NOTE (T11): SSM SecureString + Secrets Manager here are assumed to use the - // AWS-managed keys (alias/aws/ssm, alias/aws/secretsmanager) for which the - // service grants Decrypt implicitly — so NO kms:Decrypt is granted. If T11 - // moves these to a customer CMK, add a scoped `kms:Decrypt` on that key ARN - // ONLY (not `*`). - - // Invoke the Bedrock Claude model. DEFAULT_MODEL_ID is - // `bedrock_converse:us.anthropic.claude-opus-4-8`, and the model runs in the - // LangGraph server PROCESS on this box (not in the sandbox), so the EC2 - // instance role is the calling principal. The `us.` cross-region inference - // profile fans out to us-east-1 / us-east-2 / us-west-2, and Bedrock authorizes - // InvokeModel against BOTH the inference-profile ARN AND the underlying - // foundation-model ARN in each routed region — all four resources are required - // or the call AccessDenies. Scoped to opus-4-8 ONLY (least-privilege): adding a - // new Bedrock model to SUPPORTED_MODELS means extending this resource list. - // IAM change — flag for T4 (GPT-4.1 IAM cross-review) / T5 (/sh-security-review). - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "InvokeBedrockClaude", - actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"], - resources: [ - `arn:aws:bedrock:${REGION}:${ACCOUNT}:inference-profile/us.anthropic.claude-opus-4-8`, - "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-8", - "arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-opus-4-8", - "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-opus-4-8", - ], - }), - ); - - // Ship application logs to CloudWatch Logs under /open-swe//*. - this.role.addToPolicy( - new iam.PolicyStatement({ - sid: "PutAppLogs", - actions: [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogStreams", - ], - resources: [ - `arn:aws:logs:${REGION}:${ACCOUNT}:log-group:/open-swe/${env}/*`, - `arn:aws:logs:${REGION}:${ACCOUNT}:log-group:/open-swe/${env}/*:*`, - ], - }), - ); - } -} diff --git a/infra/lib/open-swe-iam-stack.ts b/infra/lib/open-swe-iam-stack.ts deleted file mode 100644 index 81e99d76..00000000 --- a/infra/lib/open-swe-iam-stack.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { GithubDeployRoles } from "./constructs/github-deploy-roles"; - -/** - * Account-level IAM stack: the per-ENV GitHub OIDC deploy roles - * (githubdeploy-open-swe-{infra,app}-{dev,prod} — four roles). - * - * T5 OSWE-IAC-01/02 fix: roles are split per env with env-scoped OIDC trust, so - * a dev-branch token cannot reach prod (prod roles require the GitHub - * `prod` Environment manual-approval gate). They live in this dedicated stack - * rather than the env stacks because IAM roles are global and this stack ships - * FIRST (TODO.md BLOCK#3): the infra OIDC roles + the repo deploy-role-ARN - * secrets must exist before any infra/secrets CI step. Synth-only until the - * Phase-1 security gate (T4 + T5) clears (T6). - */ -export class OpenSweIamStack extends cdk.Stack { - constructor(scope: Construct, id: string, props?: cdk.StackProps) { - super(scope, id, props); - - const dev = new GithubDeployRoles(this, "DeployRolesDev", "dev"); - const prod = new GithubDeployRoles(this, "DeployRolesProd", "prod"); - - cdk.Tags.of(this).add("project", "open-swe"); - cdk.Tags.of(this).add("ManagedBy", "cdk"); - - const out = (id: string, role: { roleName?: string }, env: string, kind: string) => - new cdk.CfnOutput(this, id, { - value: `arn:aws:iam::${this.account}:role/${role.roleName}`, - description: `OIDC role ARN for ${env} ${kind} deploys — set as the ${env} deploy-role secret.`, - }); - - out("InfraDeployRoleDevArn", dev.infraRole, "dev", "infra (CDK)"); - out("AppDeployRoleDevArn", dev.appRole, "dev", "app (tag-scoped SSM + S3)"); - out("InfraDeployRoleProdArn", prod.infraRole, "prod", "infra (CDK)"); - out("AppDeployRoleProdArn", prod.appRole, "prod", "app (tag-scoped SSM + S3)"); - } -} diff --git a/infra/lib/open-swe-stack.ts b/infra/lib/open-swe-stack.ts deleted file mode 100644 index 78fccc94..00000000 --- a/infra/lib/open-swe-stack.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { EnvName, prefix } from "./config"; -import { AppService } from "./constructs/app-service"; -import { AssetsBucket } from "./constructs/assets-bucket"; -import { ConfigStore } from "./constructs/config-store"; -import { InstanceRole } from "./constructs/instance-role"; -import { BAKED_OPEN_SWE_AMI_ID } from "./constructs/ami-cache"; - -export interface OpenSweStackProps extends cdk.StackProps { - /** open-swe environment — drives the `open-swe--*` resource naming. */ - readonly envName: EnvName; -} - -/** - * Per-env open-swe stack (`open-swe-dev` / `open-swe-prod`). Resource names are - * prefixed `open-swe--*`. - * - * Composes: the per-env least-privilege instance role (T6), the Secrets/SSM - * config store (T11), and the compute + ingress wiring (T12, AppService — EC2 - * box, instance SG, target group, imported-listener rules, Route53 aliases, - * 30-day log groups). The shared VPC and ALB are imported, never owned. Synth is - * offline (AMI is the cdk.context.json-pinned placeholder until T12-deploy). - */ -export class OpenSweStack extends cdk.Stack { - public readonly instanceRole: InstanceRole; - public readonly configStore: ConfigStore; - public readonly assetsBucket: AssetsBucket; - public readonly appService: AppService; - - constructor(scope: Construct, id: string, props: OpenSweStackProps) { - super(scope, id, props); - - const envName = props.envName; - const p = prefix(envName); - - cdk.Tags.of(this).add("project", "open-swe"); - cdk.Tags.of(this).add("env", envName); - cdk.Tags.of(this).add("ManagedBy", "cdk"); - - // Per-env least-privilege EC2 instance role (open-swe--instance-role). - this.instanceRole = new InstanceRole(this, "Instance", envName); - - // Secrets Manager + SSM Parameter Store shells the boot hook reads - // (deploy/seahaven/fetch-config.sh). Secret shells are value-less and - // populated out-of-band; IaC-managed SSM params carry real derivable values. - // The instance role already grants read on open-swe-/* + /open-swe-/*. - this.configStore = new ConfigStore(this, "Config", { envName }); - - // T7: the S3 artifact bucket (open-swe--assets) CI uploads releases to - // and the box pulls app.tar.gz / spa.tar.gz from. The instance role already - // grants read on it by name; the app deploy role grants write. - this.assetsBucket = new AssetsBucket(this, "Assets", envName); - - // Surface the baked open-swe base AMI id the box runs on (pinned by id in - // ami-cache.ts; refreshed by a deliberate packer rebuild → replacement). - new cdk.CfnOutput(this, "BakedAmiId", { - value: BAKED_OPEN_SWE_AMI_ID, - description: "Baked open-swe-base-arm64 AMI id consumed by the EC2 instance.", - }); - - // T12: compute + ingress. Imports the shared seahaven-vpc + ALB and adds the - // env's EC2 box, instance SG, target group, listener rules, DNS, log groups. - this.appService = new AppService(this, "App", { - envName, - instanceRole: this.instanceRole.role, - }); - - new cdk.CfnOutput(this, "InstanceRoleArn", { - value: this.instanceRole.role.roleArn, - description: `${p} EC2 instance role ARN.`, - }); - new cdk.CfnOutput(this, "InstanceId", { - value: this.appService.instance.instanceId, - description: `${p} EC2 instance id.`, - }); - new cdk.CfnOutput(this, "TargetGroupArn", { - value: this.appService.targetGroup.targetGroupArn, - description: `${p} ALB target group ARN (→ instance:80 nginx).`, - }); - new cdk.CfnOutput(this, "AssetsBucketName", { - value: this.assetsBucket.bucket.bucketName, - description: `${p} S3 artifact bucket (CI uploads releases; box pulls).`, - }); - new cdk.CfnOutput(this, "DeployDocumentName", { - value: this.appService.deployDocumentName, - description: `${p} SSM document that rolls the box to the latest release.`, - }); - } -} diff --git a/infra/package-lock.json b/infra/package-lock.json deleted file mode 100644 index aa1f4dcc..00000000 --- a/infra/package-lock.json +++ /dev/null @@ -1,4487 +0,0 @@ -{ - "name": "open-swe-infra", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "open-swe-infra", - "version": "1.0.0", - "dependencies": { - "aws-cdk-lib": "2.260.0", - "constructs": "^10.0.0" - }, - "bin": { - "open-swe-infra": "bin/app.js" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^24.0.0", - "@types/source-map-support": "^0.5.10", - "aws-cdk": "^2.1029.0", - "jest": "^29.7.0", - "source-map-support": "^0.5.21", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - } - }, - "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.282", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.282.tgz", - "integrity": "sha512-7hKMi5tTxDcKGIMIOq14PnY0GBcugW33Uh/2YHDZiEwSxLeFOCYBwhR+BFXONb/EJeVI3RETFgailNZbkcKF6g==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.2.tgz", - "integrity": "sha512-pDiuqH+qY3zM9lhhLjbKJ1tnKOHzQ2V4Wr/3qsxyKeKAkuPMI/BVGvZG1PbrikUw949cGVTfVEt4ETKKYnrj0Q==", - "license": "Apache-2.0" - }, - "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "54.5.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.5.0.tgz", - "integrity": "sha512-X37oRfMQYO/wXBBDotbW8msJ6AgrcMio/W6TpDR/9To9TUWld1KtY/jveHpU58nZyLVPTYEhDgFP548w3JJGsQ==", - "bundleDependencies": [ - "jsonschema", - "semver" - ], - "license": "Apache-2.0", - "dependencies": { - "jsonschema": "^1.5.0", - "semver": "^7.8.4" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { - "version": "1.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.8.4", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", - "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/jest": { - "version": "29.5.14", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", - "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.13.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", - "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.0" - } - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/aws-cdk": { - "version": "2.1128.1", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1128.1.tgz", - "integrity": "sha512-y9OHn5/BOcIiq409vPvpypMIr7/8M1ScFe8IkFMSCN1/GI/5c73fQ4pfzNq+VDkj86T5zxs7BQ1qU2lQQytdXA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "cdk": "bin/cdk" - }, - "engines": { - "node": ">= 18.0.0" - } - }, - "node_modules/aws-cdk-lib": { - "version": "2.260.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.260.0.tgz", - "integrity": "sha512-2PPG+hbPDot8+ibkb5Jl9y3OY5rBE6TFwjzOi+yEyU4ZG6u8bM4DDKhhBi/S20NqqSFDso9rH1txVJAdwXNiuQ==", - "bundleDependencies": [ - "@balena/dockerignore", - "@aws-cdk/cloud-assembly-api", - "case", - "fs-extra", - "ignore", - "jsonschema", - "minimatch", - "punycode", - "semver", - "table", - "yaml", - "mime-types" - ], - "license": "Apache-2.0", - "dependencies": { - "@aws-cdk/asset-awscli-v1": "2.2.282", - "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", - "@aws-cdk/cloud-assembly-api": "^2.2.5", - "@aws-cdk/cloud-assembly-schema": "^54.0.0", - "@balena/dockerignore": "^1.0.2", - "case": "1.6.3", - "fs-extra": "^11.3.5", - "ignore": "^5.3.2", - "jsonschema": "^1.5.0", - "mime-types": "^2.1.35", - "minimatch": "^10.2.5", - "punycode": "^2.3.1", - "semver": "^7.8.1", - "table": "^6.9.0", - "yaml": "1.10.3" - }, - "engines": { - "node": ">= 20.0.0" - }, - "peerDependencies": { - "constructs": "^10.5.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { - "version": "2.2.5", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "jsonschema": "^1.5.0", - "semver": "^7.8.0" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": ">=53.28.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { - "version": "1.0.2", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/aws-cdk-lib/node_modules/ajv": { - "version": "8.20.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/astral-regex": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/balanced-match": { - "version": "4.0.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "5.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/aws-cdk-lib/node_modules/case": { - "version": "1.6.3", - "inBundle": true, - "license": "(MIT OR GPL-3.0-or-later)", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { - "version": "3.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/fast-uri": { - "version": "3.1.2", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, - "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/aws-cdk-lib/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/aws-cdk-lib/node_modules/ignore": { - "version": "5.3.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/jsonfile": { - "version": "6.2.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/jsonschema": { - "version": "1.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { - "version": "4.4.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/aws-cdk-lib/node_modules/mime-db": { - "version": "1.52.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/mime-types": { - "version": "2.1.35", - "inBundle": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/aws-cdk-lib/node_modules/minimatch": { - "version": "10.2.5", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/aws-cdk-lib/node_modules/punycode": { - "version": "2.3.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/aws-cdk-lib/node_modules/require-from-string": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.8.1", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aws-cdk-lib/node_modules/slice-ansi": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/aws-cdk-lib/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/aws-cdk-lib/node_modules/table": { - "version": "6.9.0", - "inBundle": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/universalify": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-cdk-lib/node_modules/yaml": { - "version": "1.10.3", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" - } - }, - "node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.40", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", - "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.28.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", - "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.38", - "caniuse-lite": "^1.0.30001799", - "electron-to-chromium": "^1.5.376", - "node-releases": "^2.0.48", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/constructs": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", - "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", - "license": "Apache-2.0" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.379", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", - "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/jest-leak-detector": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/jest-runtime": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.50", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", - "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve.exports": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", - "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-jest": { - "version": "29.4.11", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", - "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.9", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.8.0", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", - "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/infra/package.json b/infra/package.json deleted file mode 100644 index 028b9279..00000000 --- a/infra/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "open-swe-infra", - "version": "1.0.0", - "description": "Open SWE AWS infrastructure (CDK TypeScript) — open-swe-dev / open-swe-prod stacks + shared OIDC deploy roles.", - "private": true, - "bin": { - "open-swe-infra": "bin/app.js" - }, - "scripts": { - "build": "tsc", - "cdk": "cdk", - "synth": "cdk synth", - "diff": "cdk diff", - "test": "jest" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^24.0.0", - "@types/source-map-support": "^0.5.10", - "aws-cdk": "^2.1029.0", - "jest": "^29.7.0", - "source-map-support": "^0.5.21", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "~5.6.3" - }, - "dependencies": { - "aws-cdk-lib": "2.260.0", - "constructs": "^10.0.0" - } -} diff --git a/infra/test/ascii-aws-fields.test.ts b/infra/test/ascii-aws-fields.test.ts deleted file mode 100644 index 5de3a93d..00000000 --- a/infra/test/ascii-aws-fields.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Template } from "aws-cdk-lib/assertions"; -import { OpenSweStack } from "../lib/open-swe-stack"; - -const ENV = { account: "328440206208", region: "us-east-1" }; - -/** - * Several AWS APIs reject non-ASCII in fields that `cdk synth` happily emits and - * `tsc` happily compiles — so a stray em-dash/arrow only blows up at DEPLOY time - * (e.g. EC2 SecurityGroup GroupDescription: "Character sets beyond ASCII are not - * supported"). This has bitten us twice (the AMI Description, then the instance-SG - * description). This test fails the build at synth time instead. - * - * Scope: the EC2 fields with a documented ASCII/restricted-charset constraint — - * SecurityGroup GroupDescription and ingress/egress rule descriptions. (CloudFormation - * Output descriptions + Route53 comments accept UTF-8, so they are not asserted.) - * - * EC2 rule descriptions are stricter than ASCII: the allowed set is - * `a-zA-Z0-9. _-:/()#,@[]+=&;{}!$*` — note it EXCLUDES `<` and `>`, which is why a - * naive em-dash -> "->" replacement still fails at deploy. We assert that exact set. - */ -// Characters NOT in the EC2 description allowed set. -const DISALLOWED = /[^a-zA-Z0-9. _:/()#,@[\]+=&;{}!$*-]/; - -function synthDev(): Record }> { - const app = new cdk.App(); - const dev = new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env: ENV, - envName: "dev", - }); - return Template.fromStack(dev).toJSON().Resources; -} - -describe("ASCII-only EC2 description fields", () => { - const resources = synthDev(); - - it("SecurityGroup GroupDescription is ASCII", () => { - for (const [id, r] of Object.entries(resources)) { - if (r.Type !== "AWS::EC2::SecurityGroup") continue; - const desc = (r.Properties?.GroupDescription as string) ?? ""; - expect(DISALLOWED.test(desc) ? `${id}: ${desc}` : "ascii").toBe("ascii"); - } - }); - - it("SecurityGroup ingress/egress rule descriptions are ASCII", () => { - for (const [id, r] of Object.entries(resources)) { - const props = r.Properties ?? {}; - const groups: Array<{ Description?: string }> = []; - if (Array.isArray(props.SecurityGroupIngress)) groups.push(...props.SecurityGroupIngress); - if (Array.isArray(props.SecurityGroupEgress)) groups.push(...props.SecurityGroupEgress); - // Standalone AWS::EC2::SecurityGroupEgress / ...Ingress resources. - if (r.Type === "AWS::EC2::SecurityGroupEgress" || r.Type === "AWS::EC2::SecurityGroupIngress") { - groups.push(props as { Description?: string }); - } - for (const rule of groups) { - const desc = rule.Description ?? ""; - expect(DISALLOWED.test(desc) ? `${id}: ${desc}` : "ascii").toBe("ascii"); - } - } - }); - - // EC2 caps base64-encoded user-data at 25600 bytes; CDK + tsc don't check it, so - // an oversized boot script (e.g. an embedded deploy.sh) only fails at deploy. - it("EC2 user-data fits the 25600-byte encoded limit", () => { - for (const [id, r] of Object.entries(resources)) { - if (r.Type !== "AWS::EC2::Instance") continue; - const ud = (r.Properties?.UserData as { "Fn::Base64"?: string }) ?? {}; - const script = typeof ud["Fn::Base64"] === "string" ? ud["Fn::Base64"] : ""; - const encoded = Buffer.from(script, "utf8").toString("base64").length; - expect(`${id}: ${encoded} bytes`).toBe(encoded < 25600 ? `${id}: ${encoded} bytes` : "OVER 25600"); - } - }); - - // CDK substitutes %%...%% tokens in user-data at synth. Any %%TOKEN%% left in the - // rendered script means a token wasn't wired in app-service.ts (the @@...@@ tokens - // are intentional — user-data seds those into the baked templates at boot). - it("user-data has no unresolved %%CDK%% tokens", () => { - for (const [id, r] of Object.entries(resources)) { - if (r.Type !== "AWS::EC2::Instance") continue; - const ud = (r.Properties?.UserData as { "Fn::Base64"?: string }) ?? {}; - const script = typeof ud["Fn::Base64"] === "string" ? ud["Fn::Base64"] : ""; - const leftover = script.match(/%%[A-Z0-9_]+%%/g) ?? []; - expect(`${id}: ${leftover.join(",")}`).toBe(`${id}: `); - } - }); -}); diff --git a/infra/test/bootstrap-qualifier.test.ts b/infra/test/bootstrap-qualifier.test.ts deleted file mode 100644 index 51ff5852..00000000 --- a/infra/test/bootstrap-qualifier.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Match, Template } from "aws-cdk-lib/assertions"; -import { OpenSweIamStack } from "../lib/open-swe-iam-stack"; -import { bootstrapQualifier } from "../lib/config"; - -const ENV = { account: "328440206208", region: "us-east-1" }; - -// B-1 / OSWE-IAC-01: each env's infra deploy role may assume ONLY its own -// bootstrap qualifier's roles. Dev runs on `oswedev`, so a dev-branch token can -// no longer assume the default `hnb659fds` bootstrap roles whose admin -// cfn-exec-role deploys prod. Prod stays on the default qualifier. -describe("Per-env CDK bootstrap qualifier isolation (B-1/OSWE-IAC-01)", () => { - it("maps dev -> oswedev and prod -> hnb659fds", () => { - expect(bootstrapQualifier("dev")).toBe("oswedev"); - expect(bootstrapQualifier("prod")).toBe("hnb659fds"); - }); - - it("dev infra deploy role assumes only cdk-oswedev-* bootstrap roles", () => { - const app = new cdk.App(); - const stack = new OpenSweIamStack(app, "OpenSweIamStack", { - stackName: "open-swe-iam", - env: ENV, - }); - Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", { - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Sid: "AssumeCdkBootstrapRoles", - Action: "sts:AssumeRole", - Resource: "arn:aws:iam::328440206208:role/cdk-oswedev-*", - }), - ]), - }), - }); - }); - - it("prod infra deploy role stays on the default cdk-hnb659fds-* bootstrap roles", () => { - const app = new cdk.App(); - const stack = new OpenSweIamStack(app, "OpenSweIamStack", { - stackName: "open-swe-iam", - env: ENV, - }); - Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", { - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Sid: "AssumeCdkBootstrapRoles", - Action: "sts:AssumeRole", - Resource: "arn:aws:iam::328440206208:role/cdk-hnb659fds-*", - }), - ]), - }), - }); - }); -}); diff --git a/infra/test/kebab-naming-aspect.test.ts b/infra/test/kebab-naming-aspect.test.ts deleted file mode 100644 index 6004a789..00000000 --- a/infra/test/kebab-naming-aspect.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Annotations, Match, Template } from "aws-cdk-lib/assertions"; -import * as iam from "aws-cdk-lib/aws-iam"; -import { KebabNamingAspect, isKebabCase } from "../lib/aspects/kebab-naming-aspect"; -import { OpenSweIamStack } from "../lib/open-swe-iam-stack"; -import { OpenSweStack } from "../lib/open-swe-stack"; - -const ENV = { account: "328440206208", region: "us-east-1" }; - -describe("isKebabCase", () => { - it.each([ - "open-swe-dev", - "open-swe-prod-instance-role", - "githubdeploy-open-swe-infra", - "open-swe-dev/slack-signing", // Secrets Manager path - "/open-swe-dev/feature-flag", // SSM param path - "/open-swe/dev/agent", // log group path - "abc123", - ])("accepts conforming name %s", (name) => { - expect(isKebabCase(name)).toBe(true); - }); - - it.each([ - "OpenSweDev", - "open_swe_dev", - "openSweDev", - "Open-Swe-Dev", - "open-swe-dev/SlackSigning", - ])("rejects non-conforming name %s", (name) => { - expect(isKebabCase(name)).toBe(false); - }); -}); - -describe("KebabNamingAspect", () => { - it("passes the real app stacks (no errors)", () => { - const app = new cdk.App(); - cdk.Aspects.of(app).add(new KebabNamingAspect()); - - new OpenSweIamStack(app, "OpenSweIamStack", { stackName: "open-swe-iam", env: ENV }); - const dev = new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env: ENV, - envName: "dev", - }); - const prod = new OpenSweStack(app, "OpenSweProdStack", { - stackName: "open-swe-prod", - env: ENV, - envName: "prod", - }); - - for (const s of [dev, prod]) { - Annotations.fromStack(s).hasNoError("*", Match.anyValue()); - } - }); - - it("exempts Secrets Manager + SSM names that carry the literal env-var segment", () => { - // The config store names a secret open-swe-dev/ANTHROPIC_API_KEY and a param - // /open-swe-dev/SANDBOX_TYPE — the UPPER_SNAKE last segment is a REQUIRED - // deviation from kebab (fetch-config naming contract). Must NOT be flagged. - const app = new cdk.App(); - cdk.Aspects.of(app).add(new KebabNamingAspect()); - - const dev = new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env: ENV, - envName: "dev", - }); - - const tpl = Template.fromStack(dev); - // Sanity: the shells actually render with the literal env-var names. - tpl.hasResourceProperties("AWS::SecretsManager::Secret", { - Name: "open-swe-dev/ANTHROPIC_API_KEY", - }); - tpl.hasResourceProperties("AWS::SSM::Parameter", { - Name: "/open-swe-dev/SANDBOX_TYPE", - Value: "langsmith", - }); - Annotations.fromStack(dev).hasNoError("*", Match.anyValue()); - }); - - it("flags a deliberately non-kebab-case resource name", () => { - const app = new cdk.App(); - const stack = new cdk.Stack(app, "ConformingStackId", { stackName: "open-swe-test", env: ENV }); - cdk.Aspects.of(stack).add(new KebabNamingAspect()); - - // Deliberately bad physical name — must be flagged. - new iam.Role(stack, "BadlyNamedRole", { - roleName: "OpenSweBadRole", - assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), - }); - - Annotations.fromStack(stack).hasError( - "*", - Match.stringLikeRegexp("not kebab-case"), - ); - }); - - it("flags a deliberately non-kebab-case stack name", () => { - const app = new cdk.App(); - // PascalCase stackName — the convention CDK defaults to and that we forbid. - const stack = new cdk.Stack(app, "BadStack", { stackName: "OpenSweBadStack", env: ENV }); - cdk.Aspects.of(stack).add(new KebabNamingAspect()); - - Annotations.fromStack(stack).hasError( - "*", - Match.stringLikeRegexp("Stack name .* is not kebab-case"), - ); - }); -}); diff --git a/infra/test/s3-list-prefix.test.ts b/infra/test/s3-list-prefix.test.ts deleted file mode 100644 index ba76fcd4..00000000 --- a/infra/test/s3-list-prefix.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as cdk from "aws-cdk-lib"; -import { Match, Template } from "aws-cdk-lib/assertions"; -import { OpenSweIamStack } from "../lib/open-swe-iam-stack"; -import { OpenSweStack } from "../lib/open-swe-stack"; - -const ENV = { account: "328440206208", region: "us-east-1" }; - -// F-1 / IAC-04: s3:ListBucket must be constrained to the releases/ prefix so a -// compromised box / leaked CI token cannot enumerate the rest of the bucket. -const RELEASES_PREFIX_CONDITION = { StringLike: { "s3:prefix": ["releases/*"] } }; - -describe("S3 ListBucket prefix scoping (F-1/IAC-04)", () => { - it("instance role ListBucket is constrained to releases/*", () => { - const app = new cdk.App(); - const stack = new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env: ENV, - envName: "dev", - }); - Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", { - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Sid: "ListArtifactBucket", - Action: "s3:ListBucket", - Condition: RELEASES_PREFIX_CONDITION, - }), - ]), - }), - }); - }); - - it("github deploy app role ListBucket is constrained to releases/*", () => { - const app = new cdk.App(); - const stack = new OpenSweIamStack(app, "OpenSweIamStack", { - stackName: "open-swe-iam", - env: ENV, - }); - Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", { - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Sid: "ListArtifactBucket", - Action: "s3:ListBucket", - Condition: RELEASES_PREFIX_CONDITION, - }), - ]), - }), - }); - }); - - it("GetBucketLocation stays a separate, unconditioned statement", () => { - const app = new cdk.App(); - const stack = new OpenSweStack(app, "OpenSweDevStack", { - stackName: "open-swe-dev", - env: ENV, - envName: "dev", - }); - Template.fromStack(stack).hasResourceProperties("AWS::IAM::Policy", { - PolicyDocument: Match.objectLike({ - Statement: Match.arrayWith([ - Match.objectLike({ - Sid: "GetArtifactBucketLocation", - Action: "s3:GetBucketLocation", - Condition: Match.absent(), - }), - ]), - }), - }); - }); -}); diff --git a/infra/tsconfig.json b/infra/tsconfig.json deleted file mode 100644 index 55c51452..00000000 --- a/infra/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "commonjs", - "lib": ["ES2022"], - "types": ["node", "jest"], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "inlineSourceMap": true, - "inlineSources": true, - "strictPropertyInitialization": false, - "outDir": "./cdk.out", - "rootDir": ".", - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "esModuleInterop": true - }, - "exclude": ["node_modules", "cdk.out"] -}